refactor: unified Element interface with Draw method and Container type
- Add Draw(gtx, r *Renderer) to Element interface - Add Container type that implements Element and holds []Element children - Leaf types (Label, Icon, Button) implement Draw themselves - Single recursive drawElement function in Renderer — no switch/case - Remove StatusBar/BottomBar — replaced by Container - Remove shaper.go — no longer needed with Gio material labels - Simplify main.go — scale via ScaleProvider interface - 5 files changed, shaper.go deleted
This commit is contained in:
parent
93a7a0a0b2
commit
8739bae517
|
|
@ -33,16 +33,17 @@ func run(w *app.Window) error {
|
||||||
|
|
||||||
shaper := text.NewShaper(text.WithCollection(gofont.Collection()))
|
shaper := text.NewShaper(text.WithCollection(gofont.Collection()))
|
||||||
|
|
||||||
// Initial DP size from app.Size() — used before we know the scale factor
|
// Initial DP size from app.Size()
|
||||||
initialDPW := ui.Dp(390)
|
initialDPW := ui.Dp(390)
|
||||||
initialDPH := ui.Dp(844)
|
initialDPH := ui.Dp(844)
|
||||||
|
|
||||||
// Create logic instance with initial DP size and scale=1.0
|
// Create logic instance
|
||||||
logic := editor.NewLogic(initialDPW, initialDPH)
|
logic := editor.NewLogic(initialDPW, initialDPH)
|
||||||
|
|
||||||
|
// Create renderer — reads scale from State via ScaleProvider
|
||||||
renderer := ui.New(ui.Theme{FontSize: 14}, shaper, logic.State())
|
renderer := ui.New(ui.Theme{FontSize: 14}, shaper, logic.State())
|
||||||
|
|
||||||
// Track pixel dimensions from ConfigEvent (for resize handling)
|
// Track pixel dimensions from ConfigEvent
|
||||||
var pixelW, pixelH int
|
var pixelW, pixelH int
|
||||||
|
|
||||||
// Shared state protected by mutex
|
// Shared state protected by mutex
|
||||||
|
|
@ -54,7 +55,6 @@ func run(w *app.Window) error {
|
||||||
|
|
||||||
// Start frame receiver goroutine
|
// Start frame receiver goroutine
|
||||||
go frameReceiver(w, &mu, &elems, logic.FrameChan())
|
go frameReceiver(w, &mu, &elems, logic.FrameChan())
|
||||||
scale := float32(1.0)
|
|
||||||
|
|
||||||
// Main event loop
|
// Main event loop
|
||||||
for {
|
for {
|
||||||
|
|
@ -62,13 +62,12 @@ func run(w *app.Window) error {
|
||||||
case app.DestroyEvent:
|
case app.DestroyEvent:
|
||||||
return e.Err
|
return e.Err
|
||||||
case app.ConfigEvent:
|
case app.ConfigEvent:
|
||||||
// Store pixel dimensions from ConfigEvent
|
|
||||||
pixelW = e.Config.Size.X
|
pixelW = e.Config.Size.X
|
||||||
pixelH = e.Config.Size.Y
|
pixelH = e.Config.Size.Y
|
||||||
// Convert to DP using current scale
|
// Convert pixels to DP using current scale
|
||||||
|
scale := logic.Scale()
|
||||||
dpW := ui.ToDp(ui.Px(pixelW), scale)
|
dpW := ui.ToDp(ui.Px(pixelW), scale)
|
||||||
dpH := ui.ToDp(ui.Px(pixelH), scale)
|
dpH := ui.ToDp(ui.Px(pixelH), scale)
|
||||||
// Send config event to logic goroutine (in DP)
|
|
||||||
logic.ConfigChan() <- editor.ConfigEvent{
|
logic.ConfigChan() <- editor.ConfigEvent{
|
||||||
Width: dpW,
|
Width: dpW,
|
||||||
Height: dpH,
|
Height: dpH,
|
||||||
|
|
@ -76,19 +75,16 @@ func run(w *app.Window) error {
|
||||||
case app.FrameEvent:
|
case app.FrameEvent:
|
||||||
gtx := app.NewContext(&ops, e)
|
gtx := app.NewContext(&ops, e)
|
||||||
|
|
||||||
// Get scale from State — this is updated by ScaleEvent
|
// Update scale if changed
|
||||||
newScale := gtx.Metric.PxPerDp
|
newScale := gtx.Metric.PxPerDp
|
||||||
|
curScale := logic.Scale()
|
||||||
if newScale != scale {
|
if newScale != curScale {
|
||||||
// Send updated scale to logic goroutine
|
|
||||||
logic.ConfigChan() <- editor.ScaleEvent{newScale}
|
logic.ConfigChan() <- editor.ScaleEvent{newScale}
|
||||||
scale = newScale
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acquire mutex, read frame, draw, release mutex
|
// Acquire mutex, read frame, draw, release mutex
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
currentElems := elems
|
currentElems := elems
|
||||||
|
|
||||||
renderer.Draw(gtx, currentElems)
|
renderer.Draw(gtx, currentElems)
|
||||||
e.Frame(&ops)
|
e.Frame(&ops)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,6 @@ type State struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewState creates a new State with initial editor layout.
|
// NewState creates a new State with initial editor layout.
|
||||||
// screenWidth and screenHeight are in device-independent pixels (Dp).
|
|
||||||
// scale defaults to 1.0 until updated by ScaleEvent.
|
|
||||||
func NewState(screenWidth, screenHeight ui.Dp) *State {
|
func NewState(screenWidth, screenHeight ui.Dp) *State {
|
||||||
return &State{
|
return &State{
|
||||||
ScreenWidth: screenWidth,
|
ScreenWidth: screenWidth,
|
||||||
|
|
@ -36,50 +34,46 @@ func (s *State) Scale() float32 {
|
||||||
return s.scale
|
return s.scale
|
||||||
}
|
}
|
||||||
|
|
||||||
// EditorLayout computes regions for the editor page.
|
// EditorLayout computes the element tree for the editor page.
|
||||||
// It takes screen dimensions in Dp and returns []Element with regions in Dp.
|
// It takes screen dimensions in Dp and returns []Element.
|
||||||
// This function works exclusively in Dp — no pixel conversions.
|
|
||||||
func EditorLayout(screenWidth, screenHeight ui.Dp) []ui.Element {
|
func EditorLayout(screenWidth, screenHeight ui.Dp) []ui.Element {
|
||||||
// Margin to avoid clipping on macOS round corners
|
|
||||||
margin := ui.Dp(10)
|
margin := ui.Dp(10)
|
||||||
|
|
||||||
// Compute StatusBar region
|
// --- Top container: status bar ---
|
||||||
statusBarHeight := ui.Dp(80)
|
statusBarHeight := ui.Dp(52)
|
||||||
statusBarRegion := ui.Region{
|
statusBarRegion := ui.Region{
|
||||||
X: margin,
|
X: margin, Y: margin,
|
||||||
Y: margin,
|
|
||||||
W: screenWidth - margin*2,
|
W: screenWidth - margin*2,
|
||||||
H: statusBarHeight,
|
H: statusBarHeight,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Compute BottomBar region (fixed height, at bottom of screen)
|
statusBar := ui.NewContainer(
|
||||||
|
statusBarRegion,
|
||||||
|
ui.Color{R: 230, G: 230, B: 230, A: 255},
|
||||||
|
[]ui.Element{
|
||||||
|
ui.NewIcon("cut", ui.Region{X: ui.Dp(8), Y: ui.Dp(2), W: ui.Dp(16), H: ui.Dp(16)}, ui.Dp(16)),
|
||||||
|
ui.NewLabel("Ln 47, Col 12", 12, ui.Region{X: ui.Dp(32), Y: ui.Dp(2)}),
|
||||||
|
ui.NewLabel("Ready", 12, ui.Region{X: ui.Dp(8), Y: ui.Dp(28)}),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Bottom container: bottom bar ---
|
||||||
bottomBarHeight := ui.Dp(24)
|
bottomBarHeight := ui.Dp(24)
|
||||||
bottomBarY := screenHeight - margin - bottomBarHeight
|
bottomBarY := screenHeight - margin - bottomBarHeight
|
||||||
bottomBarRegion := ui.Region{
|
bottomBarRegion := ui.Region{
|
||||||
X: margin,
|
X: margin, Y: bottomBarY,
|
||||||
Y: bottomBarY,
|
|
||||||
W: screenWidth - margin*2,
|
W: screenWidth - margin*2,
|
||||||
H: bottomBarHeight,
|
H: bottomBarHeight,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create StatusBar with mocked filename
|
bottomBar := ui.NewContainer(
|
||||||
statusBar := ui.NewStatusBar(
|
|
||||||
statusBarRegion,
|
|
||||||
"very_long_filename_that_does_not_fit_on_a_single_line_and_it_just_keeps_on_going_and_going_and_going_and_going_and_going.txt",
|
|
||||||
false, // FilenameExp
|
|
||||||
true, // CutCopy
|
|
||||||
false, // Copy
|
|
||||||
true, // Paste
|
|
||||||
false, // ConflictIcon
|
|
||||||
true, // Search
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create BottomBar with mocked data
|
|
||||||
bottomBar := ui.NewBottomBar(
|
|
||||||
bottomBarRegion,
|
bottomBarRegion,
|
||||||
"Ln 47, Col 12",
|
ui.Color{R: 230, G: 230, B: 230, A: 255},
|
||||||
"1024 / 50000",
|
[]ui.Element{
|
||||||
true, // WordWrap
|
ui.NewLabel("Ln 47, Col 12", 12, ui.Region{X: ui.Dp(8), Y: ui.Dp(2)}),
|
||||||
|
ui.NewLabel("1024 / 50000", 12, ui.Region{X: ui.Dp(150), Y: ui.Dp(2)}),
|
||||||
|
ui.NewLabel("Wrap: On", 12, ui.Region{X: screenWidth - margin - ui.Dp(60), Y: ui.Dp(2)}),
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return []ui.Element{statusBar, bottomBar}
|
return []ui.Element{statusBar, bottomBar}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import "gioui.org/unit"
|
import (
|
||||||
|
"gioui.org/layout"
|
||||||
|
"gioui.org/unit"
|
||||||
|
"gioui.org/widget/material"
|
||||||
|
)
|
||||||
|
|
||||||
// Region defines a screen area in device-independent pixels (Dp).
|
// Region defines a screen area in device-independent pixels (Dp).
|
||||||
// All element positions and sizes use Dp for device independence.
|
// All element positions and sizes use Dp for device independence.
|
||||||
|
|
@ -10,45 +14,47 @@ type Region struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Element is the base interface for all UI elements.
|
// Element is the base interface for all UI elements.
|
||||||
|
// Elements know how to draw themselves when given a Renderer.
|
||||||
type Element interface {
|
type Element interface {
|
||||||
Region() Region
|
Region() Region
|
||||||
Visible() bool
|
Visible() bool
|
||||||
|
Draw(gtx layout.Context, r *Renderer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TextAlign specifies horizontal text alignment.
|
// Container holds child elements and draws them within its bounds.
|
||||||
type TextAlign int
|
// Children's regions are screen-space; clipping handles containment.
|
||||||
|
type Container struct {
|
||||||
const (
|
id string
|
||||||
AlignStart TextAlign = iota
|
region Region
|
||||||
AlignCenter
|
visible bool
|
||||||
AlignEnd
|
background Color
|
||||||
)
|
children []Element
|
||||||
|
|
||||||
// InputType specifies the type of user input event.
|
|
||||||
type InputType int
|
|
||||||
|
|
||||||
const (
|
|
||||||
Tap InputType = iota
|
|
||||||
DoubleTap
|
|
||||||
LongPress
|
|
||||||
Scroll
|
|
||||||
KeyDown
|
|
||||||
KeyUp
|
|
||||||
)
|
|
||||||
|
|
||||||
// InputEvent represents a user input event routed to an element.
|
|
||||||
type InputEvent struct {
|
|
||||||
ElementID string
|
|
||||||
Type InputType
|
|
||||||
Data any
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConfigEvent represents a window configuration change (resize, orientation).
|
func (c Container) Region() Region { return c.region }
|
||||||
// Width and Height are in device-independent pixels (Dp).
|
func (c Container) Visible() bool { return c.visible }
|
||||||
type ConfigEvent struct {
|
func (c Container) Draw(gtx layout.Context, r *Renderer) {
|
||||||
Width Dp
|
// Draw background
|
||||||
Height Dp
|
if c.background != (Color{}) {
|
||||||
|
r.drawBg(gtx, c.region, c.background)
|
||||||
}
|
}
|
||||||
|
// Draw children
|
||||||
|
for _, child := range c.children {
|
||||||
|
child.Draw(gtx, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewContainer creates a Container with the given region, background, and children.
|
||||||
|
func NewContainer(region Region, bg Color, children []Element) Container {
|
||||||
|
return Container{
|
||||||
|
region: region,
|
||||||
|
visible: true,
|
||||||
|
background: bg,
|
||||||
|
children: children,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Leaf element types (each implements Element and knows how to Draw itself) ---
|
||||||
|
|
||||||
// Label displays static text.
|
// Label displays static text.
|
||||||
type Label struct {
|
type Label struct {
|
||||||
|
|
@ -64,6 +70,9 @@ type Label struct {
|
||||||
|
|
||||||
func (l Label) Region() Region { return l.region }
|
func (l Label) Region() Region { return l.region }
|
||||||
func (l Label) Visible() bool { return l.visible }
|
func (l Label) Visible() bool { return l.visible }
|
||||||
|
func (l Label) Draw(gtx layout.Context, r *Renderer) {
|
||||||
|
r.drawText(gtx, l.Text, l.FontSize, l.region, l.Color)
|
||||||
|
}
|
||||||
func (l Label) ID() string { return l.id }
|
func (l Label) ID() string { return l.id }
|
||||||
|
|
||||||
// NewLabel creates a visible Label element.
|
// NewLabel creates a visible Label element.
|
||||||
|
|
@ -76,6 +85,40 @@ func NewLabel(text string, fontSize unit.Sp, region Region) Label {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Icon displays a named icon image.
|
||||||
|
type Icon struct {
|
||||||
|
id string
|
||||||
|
region Region
|
||||||
|
visible bool
|
||||||
|
Name string // icon name, e.g. "cut"
|
||||||
|
Size Dp // 0 = default icon size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i Icon) Region() Region { return i.region }
|
||||||
|
func (i Icon) Visible() bool { return i.visible }
|
||||||
|
func (i Icon) Draw(gtx layout.Context, r *Renderer) {
|
||||||
|
img := r.icon(i.Name)
|
||||||
|
if img == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
size := i.Size
|
||||||
|
if size == 0 {
|
||||||
|
size = Dp(16)
|
||||||
|
}
|
||||||
|
r.drawPng(gtx, img, i.region, size, i.Size)
|
||||||
|
}
|
||||||
|
func (i Icon) ID() string { return i.id }
|
||||||
|
|
||||||
|
// NewIcon creates a visible Icon element.
|
||||||
|
func NewIcon(name string, region Region, size Dp) Icon {
|
||||||
|
return Icon{
|
||||||
|
region: region,
|
||||||
|
visible: true,
|
||||||
|
Name: name,
|
||||||
|
Size: size,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TextField accepts text input or displays multiline text.
|
// TextField accepts text input or displays multiline text.
|
||||||
type TextField struct {
|
type TextField struct {
|
||||||
id string
|
id string
|
||||||
|
|
@ -156,6 +199,13 @@ type Button struct {
|
||||||
|
|
||||||
func (b Button) Region() Region { return b.region }
|
func (b Button) Region() Region { return b.region }
|
||||||
func (b Button) Visible() bool { return b.visible }
|
func (b Button) Visible() bool { return b.visible }
|
||||||
|
func (b Button) Draw(gtx layout.Context, r *Renderer) {
|
||||||
|
th := material.NewTheme()
|
||||||
|
th.Shaper = r.shp
|
||||||
|
gtx.Constraints.Min.X = int(r.toPx(b.region.X))
|
||||||
|
gtx.Constraints.Min.Y = int(r.toPx(b.region.Y))
|
||||||
|
material.Label(th, r.theme.FontSize, b.Text).Layout(gtx)
|
||||||
|
}
|
||||||
func (b Button) ID() string { return b.id }
|
func (b Button) ID() string { return b.id }
|
||||||
|
|
||||||
// NewButton creates a visible Button element.
|
// NewButton creates a visible Button element.
|
||||||
|
|
@ -245,77 +295,6 @@ const (
|
||||||
MergeBoth
|
MergeBoth
|
||||||
)
|
)
|
||||||
|
|
||||||
// StatusBar displays the top bar with filename, action icons, and conflict indicator.
|
|
||||||
// IconLeft contains icon names drawn left-aligned before the filename.
|
|
||||||
// IconRow contains icon names drawn right-aligned after the filename.
|
|
||||||
type StatusBar struct {
|
|
||||||
id string
|
|
||||||
region Region
|
|
||||||
visible bool
|
|
||||||
Filename string
|
|
||||||
IconLeft []string // icon names for left side (e.g., ["cut"])
|
|
||||||
IconRow []string // icon names for right side (e.g., ["conflict", "search"])
|
|
||||||
ConflictIcon bool
|
|
||||||
Search bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sb StatusBar) Region() Region { return sb.region }
|
|
||||||
func (sb StatusBar) Visible() bool { return sb.visible }
|
|
||||||
func (sb StatusBar) ID() string { return sb.id }
|
|
||||||
|
|
||||||
// NewStatusBar creates a visible StatusBar element.
|
|
||||||
func NewStatusBar(region Region, filename string, filenameExp bool, cutCopy, copy, paste, conflictIcon, search bool) StatusBar {
|
|
||||||
return StatusBar{
|
|
||||||
region: region,
|
|
||||||
visible: true,
|
|
||||||
Filename: filename,
|
|
||||||
IconLeft: []string{"cut"},
|
|
||||||
IconRow: []string{"search"},
|
|
||||||
ConflictIcon: conflictIcon,
|
|
||||||
Search: search,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BarAlignment specifies how a bar item is positioned within its bar.
|
|
||||||
type BarAlignment int
|
|
||||||
|
|
||||||
const (
|
|
||||||
BarLeft BarAlignment = iota
|
|
||||||
BarCenter
|
|
||||||
BarRight
|
|
||||||
)
|
|
||||||
|
|
||||||
// BottomBar displays the bottom bar with cursor position, byte position, and word wrap toggle.
|
|
||||||
type BottomBar struct {
|
|
||||||
id string
|
|
||||||
region Region
|
|
||||||
visible bool
|
|
||||||
CursorPos string
|
|
||||||
CursorAlign BarAlignment
|
|
||||||
BytePos string
|
|
||||||
BytePosAlign BarAlignment
|
|
||||||
WordWrap bool
|
|
||||||
WrapAlign BarAlignment
|
|
||||||
}
|
|
||||||
|
|
||||||
func (bb BottomBar) Region() Region { return bb.region }
|
|
||||||
func (bb BottomBar) Visible() bool { return bb.visible }
|
|
||||||
func (bb BottomBar) ID() string { return bb.id }
|
|
||||||
|
|
||||||
// NewBottomBar creates a visible BottomBar element.
|
|
||||||
func NewBottomBar(region Region, cursorPos, bytePos string, wordWrap bool) BottomBar {
|
|
||||||
return BottomBar{
|
|
||||||
region: region,
|
|
||||||
visible: true,
|
|
||||||
CursorPos: cursorPos,
|
|
||||||
CursorAlign: BarLeft,
|
|
||||||
BytePos: bytePos,
|
|
||||||
BytePosAlign: BarCenter,
|
|
||||||
WordWrap: wordWrap,
|
|
||||||
WrapAlign: BarRight,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Toast displays a temporary notification.
|
// Toast displays a temporary notification.
|
||||||
type Toast struct {
|
type Toast struct {
|
||||||
id string
|
id string
|
||||||
|
|
@ -358,6 +337,8 @@ func NewSpacer(height Dp) Spacer {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Utility types ---
|
||||||
|
|
||||||
// Color is an RGBA color.
|
// Color is an RGBA color.
|
||||||
type Color struct {
|
type Color struct {
|
||||||
R, G, B, A uint8
|
R, G, B, A uint8
|
||||||
|
|
@ -367,3 +348,38 @@ type Color struct {
|
||||||
type Theme struct {
|
type Theme struct {
|
||||||
FontSize unit.Sp // Gio's shaper requires unit.Sp for font sizes
|
FontSize unit.Sp // Gio's shaper requires unit.Sp for font sizes
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TextAlign specifies horizontal text alignment.
|
||||||
|
type TextAlign int
|
||||||
|
|
||||||
|
const (
|
||||||
|
AlignStart TextAlign = iota
|
||||||
|
AlignCenter
|
||||||
|
AlignEnd
|
||||||
|
)
|
||||||
|
|
||||||
|
// InputType specifies the type of user input event.
|
||||||
|
type InputType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
Tap InputType = iota
|
||||||
|
DoubleTap
|
||||||
|
LongPress
|
||||||
|
Scroll
|
||||||
|
KeyDown
|
||||||
|
KeyUp
|
||||||
|
)
|
||||||
|
|
||||||
|
// InputEvent represents a user input event routed to an element.
|
||||||
|
type InputEvent struct {
|
||||||
|
ElementID string
|
||||||
|
Type InputType
|
||||||
|
Data any
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConfigEvent represents a window configuration change (resize, orientation).
|
||||||
|
// Width and Height are in device-independent pixels (Dp).
|
||||||
|
type ConfigEvent struct {
|
||||||
|
Width Dp
|
||||||
|
Height Dp
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"image/color"
|
"image/color"
|
||||||
_ "image/png"
|
_ "image/png"
|
||||||
|
|
||||||
"gioui.org/f32"
|
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
"gioui.org/op/clip"
|
"gioui.org/op/clip"
|
||||||
|
|
@ -15,12 +14,16 @@ import (
|
||||||
"gioui.org/text"
|
"gioui.org/text"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
"gioui.org/widget/material"
|
"gioui.org/widget/material"
|
||||||
"golang.org/x/image/math/fixed"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed icons/*.png
|
//go:embed icons/*.png
|
||||||
var iconFS embed.FS
|
var iconFS embed.FS
|
||||||
|
|
||||||
|
// ScaleProvider provides access to the current scale factor.
|
||||||
|
type ScaleProvider interface {
|
||||||
|
Scale() float32
|
||||||
|
}
|
||||||
|
|
||||||
// Renderer consumes a slice of elements and draws them.
|
// Renderer consumes a slice of elements and draws them.
|
||||||
type Renderer struct {
|
type Renderer struct {
|
||||||
theme Theme
|
theme Theme
|
||||||
|
|
@ -29,11 +32,6 @@ type Renderer struct {
|
||||||
icons map[string]image.Image
|
icons map[string]image.Image
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScaleProvider provides access to the current scale factor.
|
|
||||||
type ScaleProvider interface {
|
|
||||||
Scale() float32
|
|
||||||
}
|
|
||||||
|
|
||||||
// New creates a new Renderer.
|
// New creates a new Renderer.
|
||||||
func New(th Theme, shp *text.Shaper, scale ScaleProvider) *Renderer {
|
func New(th Theme, shp *text.Shaper, scale ScaleProvider) *Renderer {
|
||||||
r := &Renderer{theme: th, shp: shp, scale: scale, icons: make(map[string]image.Image)}
|
r := &Renderer{theme: th, shp: shp, scale: scale, icons: make(map[string]image.Image)}
|
||||||
|
|
@ -54,6 +52,11 @@ func (r *Renderer) loadIcons() {
|
||||||
r.icons["cut"] = img
|
r.icons["cut"] = img
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// icon returns a loaded icon image by name, or nil if not found.
|
||||||
|
func (r *Renderer) icon(name string) image.Image {
|
||||||
|
return r.icons[name]
|
||||||
|
}
|
||||||
|
|
||||||
// toPx converts Dp to physical pixels using State's scale.
|
// toPx converts Dp to physical pixels using State's scale.
|
||||||
func (r *Renderer) toPx(dp Dp) Px {
|
func (r *Renderer) toPx(dp Dp) Px {
|
||||||
return ToPx(dp, r.scale.Scale())
|
return ToPx(dp, r.scale.Scale())
|
||||||
|
|
@ -65,374 +68,78 @@ func (r *Renderer) toDp(px Px) Dp {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw iterates elements and draws each in slice order (back-to-front).
|
// Draw iterates elements and draws each in slice order (back-to-front).
|
||||||
// It captures the initial constraints once at the start, then passes them
|
|
||||||
// to each render function for consistent positioning.
|
|
||||||
func (r *Renderer) Draw(gtx layout.Context, elems []Element) {
|
func (r *Renderer) Draw(gtx layout.Context, elems []Element) {
|
||||||
// Capture initial constraints once — before any clips modify them.
|
// Clip to window bounds
|
||||||
// These are in physical pixels and serve as the window bounds for all positioning.
|
clipRect := clip.Rect{
|
||||||
initialConstraints := WindowConstraints{
|
|
||||||
Min: gtx.Constraints.Min,
|
Min: gtx.Constraints.Min,
|
||||||
Max: gtx.Constraints.Max,
|
Max: gtx.Constraints.Max,
|
||||||
}
|
}.Push(gtx.Ops)
|
||||||
|
|
||||||
for _, e := range elems {
|
for _, e := range elems {
|
||||||
if !e.Visible() {
|
if !e.Visible() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch v := e.(type) {
|
r.drawElement(gtx, e)
|
||||||
case Label:
|
|
||||||
r.drawLabel(gtx, v, initialConstraints)
|
|
||||||
case ListView:
|
|
||||||
r.drawListView(gtx, v, initialConstraints)
|
|
||||||
case StatusBar:
|
|
||||||
r.drawStatusBar(gtx, v, initialConstraints)
|
|
||||||
case BottomBar:
|
|
||||||
r.drawBottomBar(gtx, v, initialConstraints)
|
|
||||||
case Button:
|
|
||||||
r.drawButton(gtx, v, initialConstraints)
|
|
||||||
default:
|
|
||||||
// unknown element type, skip
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clipRect.Pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawElement clips to the element's bounds, draws background if any,
|
||||||
|
// then dispatches to the element's Draw method.
|
||||||
|
func (r *Renderer) drawElement(gtx layout.Context, e Element) {
|
||||||
|
reg := e.Region()
|
||||||
|
|
||||||
|
// Clip to element bounds
|
||||||
|
clipRect := clip.Rect{
|
||||||
|
Min: image.Point{X: int(r.toPx(reg.X)), Y: int(r.toPx(reg.Y))},
|
||||||
|
Max: image.Point{X: int(r.toPx(reg.X + reg.W)), Y: int(r.toPx(reg.Y + reg.H))},
|
||||||
|
}.Push(gtx.Ops)
|
||||||
|
|
||||||
|
// Draw children if this is a container
|
||||||
|
if container, ok := e.(Container); ok {
|
||||||
|
for _, child := range container.children {
|
||||||
|
child.Draw(gtx, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Renderer) drawLabel(gtx layout.Context, l Label, _ WindowConstraints) {
|
// Dispatch to element's Draw method
|
||||||
reg := l.Region()
|
e.Draw(gtx, r)
|
||||||
th := material.NewTheme()
|
|
||||||
th.Shaper = r.shp
|
clipRect.Pop()
|
||||||
size := l.FontSize
|
|
||||||
if size == 0 {
|
|
||||||
size = r.theme.FontSize
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert region to pixels for Gio interop
|
// drawBg draws a solid-color background for the given region.
|
||||||
gtx.Constraints.Min.X = int(r.toPx(reg.X))
|
func (r *Renderer) drawBg(gtx layout.Context, reg Region, col Color) {
|
||||||
gtx.Constraints.Min.Y = int(r.toPx(reg.Y))
|
|
||||||
material.Label(th, size, l.Text).Layout(gtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Renderer) drawListView(gtx layout.Context, lv ListView, _ WindowConstraints) {
|
|
||||||
reg := lv.Region()
|
|
||||||
th := material.NewTheme()
|
|
||||||
th.Shaper = r.shp
|
|
||||||
|
|
||||||
// Convert region to pixels for Gio interop
|
|
||||||
gtx.Constraints.Min.X = int(r.toPx(reg.X))
|
|
||||||
gtx.Constraints.Min.Y = int(r.toPx(reg.Y))
|
|
||||||
for _, item := range lv.Items {
|
|
||||||
material.Label(th, r.theme.FontSize, item.Text).Layout(gtx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Renderer) drawStatusBar(gtx layout.Context, sb StatusBar, _ WindowConstraints) {
|
|
||||||
reg := sb.Region()
|
|
||||||
|
|
||||||
// ── Draw background (outside clip) ──
|
|
||||||
bgColor := color.NRGBA{R: 230, G: 230, B: 230, A: 255}
|
|
||||||
bgClip := clip.Rect{
|
bgClip := clip.Rect{
|
||||||
Min: image.Point{X: int(r.toPx(reg.X)), Y: int(r.toPx(reg.Y))},
|
Min: image.Point{X: int(r.toPx(reg.X)), Y: int(r.toPx(reg.Y))},
|
||||||
Max: image.Point{X: int(r.toPx(reg.X + reg.W)), Y: int(r.toPx(reg.Y + reg.H))},
|
Max: image.Point{X: int(r.toPx(reg.X + reg.W)), Y: int(r.toPx(reg.Y + reg.H))},
|
||||||
}.Push(gtx.Ops)
|
}.Push(gtx.Ops)
|
||||||
paint.ColorOp{Color: bgColor}.Add(gtx.Ops)
|
paint.ColorOp{Color: color.NRGBA{R: col.R, G: col.G, B: col.B, A: col.A}}.Add(gtx.Ops)
|
||||||
paint.PaintOp{}.Add(gtx.Ops)
|
paint.PaintOp{}.Add(gtx.Ops)
|
||||||
bgClip.Pop()
|
bgClip.Pop()
|
||||||
|
|
||||||
// ── Layout: shape all items ──
|
|
||||||
marginDp := Dp(8)
|
|
||||||
iconSpacingDp := Dp(12)
|
|
||||||
lineHeight := r.theme.FontSize
|
|
||||||
|
|
||||||
// Shape filename
|
|
||||||
shapedFilename := r.ShapeText(gtx, sb.Filename, lineHeight)
|
|
||||||
|
|
||||||
// Measure left icons (PNG, fixed size)
|
|
||||||
var leftIconWidthPx int
|
|
||||||
for range sb.IconLeft {
|
|
||||||
leftIconWidthPx += int(r.toPx(Dp(16))) + int(r.toPx(iconSpacingDp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Measure right icons (text-based)
|
|
||||||
var rightIconWidthPx int
|
|
||||||
for _, name := range sb.IconRow {
|
|
||||||
w := r.ShapeText(gtx, name, lineHeight).Width
|
|
||||||
rightIconWidthPx += w + int(r.toPx(iconSpacingDp))
|
|
||||||
}
|
|
||||||
if sb.ConflictIcon {
|
|
||||||
w := r.ShapeText(gtx, "⚠", lineHeight).Width
|
|
||||||
rightIconWidthPx += w + int(r.toPx(iconSpacingDp))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Available width for filename
|
|
||||||
totalRightPx := leftIconWidthPx + rightIconWidthPx + int(r.toPx(marginDp)*2)
|
|
||||||
availableWidthPx := int(r.toPx(reg.W)) - totalRightPx
|
|
||||||
if availableWidthPx < 0 {
|
|
||||||
availableWidthPx = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Truncate filename if needed
|
|
||||||
displayFilename := sb.Filename
|
|
||||||
spaceForTextPx := availableWidthPx - int(r.toPx(Dp(30)))
|
|
||||||
if shapedFilename.Width > availableWidthPx && spaceForTextPx > 0 {
|
|
||||||
// Find truncation point
|
|
||||||
r.shp.LayoutString(text.Parameters{
|
|
||||||
PxPerEm: fixed.I(gtx.Sp(lineHeight)),
|
|
||||||
MinWidth: 0,
|
|
||||||
MaxWidth: availableWidthPx,
|
|
||||||
MaxLines: 1,
|
|
||||||
}, sb.Filename)
|
|
||||||
var widths []int
|
|
||||||
var cumWidth fixed.Int26_6
|
|
||||||
for {
|
|
||||||
g, ok := r.shp.NextGlyph()
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
cumWidth += g.Advance
|
|
||||||
widths = append(widths, int(cumWidth>>6))
|
|
||||||
}
|
|
||||||
for i := len(widths) - 1; i >= 0; i-- {
|
|
||||||
if widths[i] <= spaceForTextPx {
|
|
||||||
displayFilename = sb.Filename[:i] + "..."
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Render ──
|
|
||||||
|
|
||||||
// Clip to status bar region
|
|
||||||
c := clip.Rect{
|
|
||||||
Min: image.Point{X: int(r.toPx(reg.X)), Y: int(r.toPx(reg.Y))},
|
|
||||||
Max: image.Point{X: int(r.toPx(reg.X + reg.W)), Y: int(r.toPx(reg.Y + reg.H))},
|
|
||||||
}.Push(gtx.Ops)
|
|
||||||
|
|
||||||
// Line 1: Left icons + filename
|
|
||||||
filenameLineY := reg.Y + Dp(2)
|
|
||||||
leftX := reg.X + marginDp
|
|
||||||
|
|
||||||
// Draw left icons
|
|
||||||
for _, name := range sb.IconLeft {
|
|
||||||
if img, ok := r.icons[name]; ok {
|
|
||||||
iconSize := Dp(16)
|
|
||||||
r.drawPng(gtx, img, leftX, filenameLineY, iconSize, iconSize, color.NRGBA{R: 0, G: 0, B: 0, A: 255})
|
|
||||||
leftX += Dp(16) + iconSpacingDp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw filename
|
|
||||||
r.drawTruncatedText(gtx, r.shp, displayFilename, lineHeight, leftX, filenameLineY, availableWidthPx, spaceForTextPx, color.NRGBA{R: 0, G: 0, B: 0, A: 255})
|
|
||||||
|
|
||||||
// Line 2: Icons
|
|
||||||
iconsLineY := reg.Y + Dp(26)
|
|
||||||
|
|
||||||
// Left side: Cut icon
|
|
||||||
leftX = reg.X + Dp(8)
|
|
||||||
r.drawPng(gtx, r.icons["cut"], leftX, iconsLineY, Dp(16), Dp(16), color.NRGBA{R: 0, G: 0, B: 0, A: 255})
|
|
||||||
leftX += Dp(36)
|
|
||||||
r.drawText(gtx, r.shp, "⎘", r.theme.FontSize, leftX, iconsLineY, color.NRGBA{R: 0, G: 0, B: 0, A: 255})
|
|
||||||
leftX += Dp(36)
|
|
||||||
r.drawText(gtx, r.shp, "⎘", r.theme.FontSize, leftX, iconsLineY, color.NRGBA{R: 0, G: 0, B: 0, A: 255})
|
|
||||||
leftX += Dp(12)
|
|
||||||
|
|
||||||
// Right side: Conflict (conditional), Search (always visible)
|
|
||||||
rightX := reg.X + reg.W - Dp(8)
|
|
||||||
if sb.ConflictIcon {
|
|
||||||
rightX -= Dp(36)
|
|
||||||
r.drawText(gtx, r.shp, "⚠", r.theme.FontSize, rightX, iconsLineY, color.NRGBA{R: 0, G: 0, B: 0, A: 255})
|
|
||||||
}
|
|
||||||
rightX -= Dp(36)
|
|
||||||
r.drawText(gtx, r.shp, "🔍", r.theme.FontSize, rightX, iconsLineY, color.NRGBA{R: 0, G: 0, B: 0, A: 255})
|
|
||||||
|
|
||||||
c.Pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Renderer) drawBottomBar(gtx layout.Context, bb BottomBar, constraints WindowConstraints) {
|
|
||||||
reg := bb.Region()
|
|
||||||
|
|
||||||
// Draw light gray background
|
|
||||||
bgColor := color.NRGBA{R: 230, G: 230, B: 230, A: 255}
|
|
||||||
bgClip := clip.Rect{
|
|
||||||
Min: image.Point{X: int(r.toPx(reg.X)), Y: int(r.toPx(reg.Y))},
|
|
||||||
Max: image.Point{X: int(r.toPx(reg.X + reg.W)), Y: int(r.toPx(reg.Y + reg.H))},
|
|
||||||
}.Push(gtx.Ops)
|
|
||||||
paint.ColorOp{Color: bgColor}.Add(gtx.Ops)
|
|
||||||
paint.PaintOp{}.Add(gtx.Ops)
|
|
||||||
bgClip.Pop()
|
|
||||||
|
|
||||||
// Use captured constraints for positioning
|
|
||||||
windowW := constraints.Max.X
|
|
||||||
|
|
||||||
regXPx := r.toPx(reg.X)
|
|
||||||
regWPx := r.toPx(reg.W)
|
|
||||||
|
|
||||||
rightXPx := regXPx + regWPx
|
|
||||||
if rightXPx > Px(windowW) {
|
|
||||||
rightXPx = Px(windowW)
|
|
||||||
}
|
|
||||||
barW := rightXPx - regXPx
|
|
||||||
|
|
||||||
bottomBarHeightPx := r.toPx(Dp(24))
|
|
||||||
marginPx := r.toPx(Dp(10))
|
|
||||||
drawYPx := Px(constraints.Max.Y) - bottomBarHeightPx - marginPx
|
|
||||||
drawY := r.toDp(drawYPx)
|
|
||||||
|
|
||||||
col := color.NRGBA{R: 0, G: 0, B: 0, A: 255}
|
|
||||||
|
|
||||||
// Item 1: cursor position (left-aligned) — shape, position, draw
|
|
||||||
cursorShaped := r.ShapeText(gtx, bb.CursorPos, r.theme.FontSize)
|
|
||||||
cursorXDp := r.toDp(regXPx + r.toPx(Dp(8)))
|
|
||||||
r.DrawShapedText(gtx, cursorShaped, cursorXDp, drawY, col)
|
|
||||||
|
|
||||||
// Item 2: byte position (centered) — shape, position, draw
|
|
||||||
bytePosShaped := r.ShapeText(gtx, bb.BytePos, r.theme.FontSize)
|
|
||||||
byteXDp := r.toDp(regXPx + barW/2 - Px(bytePosShaped.Width/2))
|
|
||||||
r.DrawShapedText(gtx, bytePosShaped, byteXDp, drawY, col)
|
|
||||||
|
|
||||||
// Item 3: wrap (right-aligned) — shape, position, draw
|
|
||||||
wrapText := "Wrap:" + boolToString(bb.WordWrap)
|
|
||||||
wrapShaped := r.ShapeText(gtx, wrapText, r.theme.FontSize)
|
|
||||||
wrapXDp := r.toDp(regXPx + barW - r.toPx(Dp(8)) - Px(wrapShaped.Width))
|
|
||||||
r.DrawShapedText(gtx, wrapShaped, wrapXDp, drawY, col)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Renderer) drawButton(gtx layout.Context, btn Button, _ WindowConstraints) {
|
|
||||||
reg := btn.Region()
|
|
||||||
th := material.NewTheme()
|
|
||||||
th.Shaper = r.shp
|
|
||||||
|
|
||||||
gtx.Constraints.Min.X = int(r.toPx(reg.X))
|
|
||||||
gtx.Constraints.Min.Y = int(r.toPx(reg.Y))
|
|
||||||
material.Label(th, r.theme.FontSize, btn.Text).Layout(gtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
func boolToString(b bool) string {
|
|
||||||
if b {
|
|
||||||
return "On"
|
|
||||||
}
|
|
||||||
return "Off"
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawTruncatedText layouts text, measures widths, truncates if needed, and draws.
|
|
||||||
func (r *Renderer) drawTruncatedText(gtx layout.Context, shp *text.Shaper, str string, size unit.Sp, x, y Dp, availableWidth, spaceForText int, col color.NRGBA) {
|
|
||||||
shp.LayoutString(text.Parameters{
|
|
||||||
PxPerEm: fixed.I(gtx.Sp(size)),
|
|
||||||
MinWidth: 0,
|
|
||||||
MaxWidth: availableWidth,
|
|
||||||
MaxLines: 1,
|
|
||||||
}, str)
|
|
||||||
|
|
||||||
var widths []int
|
|
||||||
var cumWidth fixed.Int26_6
|
|
||||||
fullStr := str
|
|
||||||
for {
|
|
||||||
g, ok := shp.NextGlyph()
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
cumWidth += g.Advance
|
|
||||||
w := int(cumWidth >> 6)
|
|
||||||
widths = append(widths, w)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(widths) > 0 && widths[len(widths)-1] > availableWidth {
|
|
||||||
for i := len(widths) - 1; i >= 0; i-- {
|
|
||||||
if widths[i] <= spaceForText {
|
|
||||||
fullStr = str[:i] + "..."
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shp.LayoutString(text.Parameters{
|
|
||||||
PxPerEm: fixed.I(gtx.Sp(size)),
|
|
||||||
MinWidth: 0,
|
|
||||||
MaxWidth: availableWidth,
|
|
||||||
MaxLines: 1,
|
|
||||||
}, fullStr)
|
|
||||||
r.drawLineText(gtx, shp, x, y, col)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawText draws text using Gio's textView approach.
|
// drawText draws text using Gio's textView approach.
|
||||||
func (r *Renderer) drawText(gtx layout.Context, shp *text.Shaper, str string, size unit.Sp, x, y Dp, col color.NRGBA) {
|
func (r *Renderer) drawText(gtx layout.Context, str string, size unit.Sp, reg Region, col Color) {
|
||||||
shp.LayoutString(text.Parameters{
|
th := material.NewTheme()
|
||||||
PxPerEm: fixed.I(gtx.Sp(size)),
|
th.Shaper = r.shp
|
||||||
MinWidth: 0,
|
gtx.Constraints.Min.X = int(r.toPx(reg.X))
|
||||||
MaxWidth: 1000,
|
gtx.Constraints.Min.Y = int(r.toPx(reg.Y))
|
||||||
MaxLines: 1,
|
material.Label(th, size, str).Layout(gtx)
|
||||||
}, str)
|
|
||||||
r.drawLineText(gtx, shp, x, y, col)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawLineText draws already-laid-out glyphs using shaper.Shape() and shaper.Bitmaps().
|
// drawPng draws a PNG image at the given region and size.
|
||||||
func (r *Renderer) drawLineText(gtx layout.Context, shp *text.Shaper, x, y Dp, col color.NRGBA) {
|
func (r *Renderer) drawPng(gtx layout.Context, img image.Image, reg Region, width, height Dp) {
|
||||||
xPx := r.toPx(x)
|
|
||||||
yPx := r.toPx(y)
|
|
||||||
|
|
||||||
m := op.Record(gtx.Ops)
|
|
||||||
var glyphs [32]text.Glyph
|
|
||||||
line := glyphs[:0]
|
|
||||||
for g, ok := shp.NextGlyph(); ok; g, ok = shp.NextGlyph() {
|
|
||||||
line = append(line, g)
|
|
||||||
if g.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 {
|
|
||||||
r.drawLine(gtx, shp, line, xPx, yPx, col)
|
|
||||||
line = line[:0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(line) > 0 {
|
|
||||||
r.drawLine(gtx, shp, line, xPx, yPx, col)
|
|
||||||
}
|
|
||||||
call := m.Stop()
|
|
||||||
call.Add(gtx.Ops)
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawLine draws a line of glyphs using shaper.Shape() and shaper.Bitmaps().
|
|
||||||
func (r *Renderer) drawLine(gtx layout.Context, shp *text.Shaper, line []text.Glyph, x, y Px, col color.NRGBA) {
|
|
||||||
if len(line) == 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
first := line[0]
|
|
||||||
offX := float32(x) + float32(first.X)/64.0
|
|
||||||
offY := float32(y) + float32(first.Y)
|
|
||||||
t := op.Affine(f32.Affine2D{}.Offset(f32.Pt(offX, offY))).Push(gtx.Ops)
|
|
||||||
|
|
||||||
path := shp.Shape(line)
|
|
||||||
outline := clip.Outline{Path: path}.Op().Push(gtx.Ops)
|
|
||||||
paint.ColorOp{Color: col}.Add(gtx.Ops)
|
|
||||||
paint.PaintOp{}.Add(gtx.Ops)
|
|
||||||
outline.Pop()
|
|
||||||
|
|
||||||
if call := shp.Bitmaps(line); call != (op.CallOp{}) {
|
|
||||||
call.Add(gtx.Ops)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
// WindowConstraints tracks the initial window constraints captured at the start of Draw.
|
|
||||||
type WindowConstraints struct {
|
|
||||||
Min image.Point
|
|
||||||
Max image.Point
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawPng draws a PNG image at the given position and size.
|
|
||||||
func (r *Renderer) drawPng(gtx layout.Context, img image.Image, x, y Dp, width, height Dp, col color.NRGBA) {
|
|
||||||
if img == nil {
|
if img == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
xPx := int(r.toPx(x))
|
xPx := int(r.toPx(reg.X))
|
||||||
yPx := int(r.toPx(y))
|
yPx := int(r.toPx(reg.Y))
|
||||||
wPx := int(r.toPx(width))
|
wPx := int(r.toPx(width))
|
||||||
hPx := int(r.toPx(height))
|
hPx := int(r.toPx(height))
|
||||||
|
_ = image.Rect(xPx, yPx, xPx+wPx, yPx+hPx)
|
||||||
dst := image.Rect(xPx, yPx, xPx+wPx, yPx+hPx)
|
|
||||||
|
|
||||||
if col.A < 255 {
|
|
||||||
bgClip := clip.Rect(dst).Push(gtx.Ops)
|
|
||||||
paint.ColorOp{Color: col}.Add(gtx.Ops)
|
|
||||||
paint.PaintOp{}.Add(gtx.Ops)
|
|
||||||
bgClip.Pop()
|
|
||||||
}
|
|
||||||
|
|
||||||
stack := op.Offset(image.Pt(xPx, yPx)).Push(gtx.Ops)
|
stack := op.Offset(image.Pt(xPx, yPx)).Push(gtx.Ops)
|
||||||
paint.NewImageOp(img).Add(gtx.Ops)
|
paint.NewImageOp(img).Add(gtx.Ops)
|
||||||
|
|
|
||||||
|
|
@ -1,93 +0,0 @@
|
||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"image/color"
|
|
||||||
|
|
||||||
"gioui.org/f32"
|
|
||||||
"gioui.org/layout"
|
|
||||||
"gioui.org/op"
|
|
||||||
"gioui.org/op/clip"
|
|
||||||
"gioui.org/op/paint"
|
|
||||||
"gioui.org/text"
|
|
||||||
"gioui.org/unit"
|
|
||||||
"golang.org/x/image/math/fixed"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ShapedText holds the result from ShapeText: width for positioning,
|
|
||||||
// plus the path data ready to draw. Must be drawn before the next ShapeText call.
|
|
||||||
type ShapedText struct {
|
|
||||||
Width int // total width in pixels
|
|
||||||
PathSpec clip.PathSpec // glyph path data
|
|
||||||
Bitmaps op.CallOp // bitmap glyphs (may be empty)
|
|
||||||
FirstX fixed.Int26_6 // first glyph X offset
|
|
||||||
FirstY int32 // first glyph Y offset
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShapeText layouts text and returns width + path data for drawing.
|
|
||||||
// The returned ShapedText must be drawn before the next ShapeText call.
|
|
||||||
func (r *Renderer) ShapeText(gtx layout.Context, str string, size unit.Sp) ShapedText {
|
|
||||||
r.shp.LayoutString(text.Parameters{
|
|
||||||
PxPerEm: fixed.I(gtx.Sp(size)),
|
|
||||||
MinWidth: 0,
|
|
||||||
MaxWidth: 1000,
|
|
||||||
MaxLines: 1,
|
|
||||||
}, str)
|
|
||||||
|
|
||||||
// Collect glyphs and measure width
|
|
||||||
var glyphs []text.Glyph
|
|
||||||
var cumWidth fixed.Int26_6
|
|
||||||
var firstX fixed.Int26_6
|
|
||||||
var firstY int32
|
|
||||||
for {
|
|
||||||
g, ok := r.shp.NextGlyph()
|
|
||||||
if !ok {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if len(glyphs) == 0 {
|
|
||||||
firstX = g.X
|
|
||||||
firstY = g.Y
|
|
||||||
}
|
|
||||||
cumWidth += g.Advance
|
|
||||||
glyphs = append(glyphs, g)
|
|
||||||
}
|
|
||||||
|
|
||||||
// capture path spec
|
|
||||||
var spec clip.PathSpec
|
|
||||||
var bitmaps op.CallOp
|
|
||||||
|
|
||||||
spec = r.shp.Shape(glyphs)
|
|
||||||
bitmaps = r.shp.Bitmaps(glyphs)
|
|
||||||
|
|
||||||
return ShapedText{
|
|
||||||
Width: int(cumWidth >> 6),
|
|
||||||
PathSpec: spec,
|
|
||||||
Bitmaps: bitmaps,
|
|
||||||
FirstX: firstX,
|
|
||||||
FirstY: firstY,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DrawShapedText draws a ShapedText at the given position.
|
|
||||||
func (r *Renderer) DrawShapedText(gtx layout.Context, st ShapedText, x, y Dp, col color.NRGBA) {
|
|
||||||
xPx := r.toPx(x)
|
|
||||||
yPx := r.toPx(y)
|
|
||||||
|
|
||||||
m := op.Record(gtx.Ops)
|
|
||||||
|
|
||||||
offX := float32(xPx) + float32(st.FirstX)/64.0
|
|
||||||
offY := float32(yPx) + float32(st.FirstY)
|
|
||||||
t := op.Affine(f32.Affine2D{}.Offset(f32.Pt(offX, offY))).Push(gtx.Ops)
|
|
||||||
|
|
||||||
outline := clip.Outline{Path: st.PathSpec}.Op().Push(gtx.Ops)
|
|
||||||
paint.ColorOp{Color: col}.Add(gtx.Ops)
|
|
||||||
paint.PaintOp{}.Add(gtx.Ops)
|
|
||||||
outline.Pop()
|
|
||||||
|
|
||||||
if st.Bitmaps != (op.CallOp{}) {
|
|
||||||
st.Bitmaps.Add(gtx.Ops)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Pop()
|
|
||||||
call := m.Stop()
|
|
||||||
call.Add(gtx.Ops)
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue
Block a user