fix: correct text layout and rendering pipeline
- Fix render.go: replace broken material.Label with shaper-based text rendering (LayoutString + Shape + glyph iteration) per Gio's paintGlyph - Fix render.go: only apply clip rects at container boundaries; leaf elements have zero-size regions that were clipping all text out - Fix render.go: call r.drawElement recursively for container children so nested containers work and clips are applied correctly - Fix render.go: add drawLineText/drawLine helpers matching Gio's paintGlyph offset calculation (x + first.X, y + first.Y) - Fix element.go: Button.Draw now uses r.drawText instead of duplicate broken material.Label code; Label defaults to black when color is unset - Fix state.go: container children use relative coordinates instead of mixed screen-space/container-relative positions - Fix main.go: ConfigEvent carries raw pixel dimensions only; ScaleEvent carries scale only; layout is computed once per frame using both, eliminating the infinite Invalidate() loop
This commit is contained in:
parent
8739bae517
commit
22c1d5bed1
|
|
@ -30,59 +30,31 @@ func main() {
|
||||||
|
|
||||||
func run(w *app.Window) error {
|
func run(w *app.Window) error {
|
||||||
var ops op.Ops
|
var ops op.Ops
|
||||||
|
|
||||||
shaper := text.NewShaper(text.WithCollection(gofont.Collection()))
|
shaper := text.NewShaper(text.WithCollection(gofont.Collection()))
|
||||||
|
logic := editor.NewLogic()
|
||||||
// Initial DP size from app.Size()
|
|
||||||
initialDPW := ui.Dp(390)
|
|
||||||
initialDPH := ui.Dp(844)
|
|
||||||
|
|
||||||
// Create logic instance
|
|
||||||
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
|
|
||||||
var pixelW, pixelH int
|
|
||||||
|
|
||||||
// Shared state protected by mutex
|
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
var elems []ui.Element
|
var elems []ui.Element
|
||||||
|
|
||||||
// Start logic goroutine
|
|
||||||
go logic.Run()
|
go logic.Run()
|
||||||
|
|
||||||
// Start frame receiver goroutine
|
|
||||||
go frameReceiver(w, &mu, &elems, logic.FrameChan())
|
go frameReceiver(w, &mu, &elems, logic.FrameChan())
|
||||||
|
|
||||||
// Main event loop
|
|
||||||
for {
|
for {
|
||||||
switch e := w.Event().(type) {
|
switch e := w.Event().(type) {
|
||||||
case app.DestroyEvent:
|
case app.DestroyEvent:
|
||||||
return e.Err
|
return e.Err
|
||||||
case app.ConfigEvent:
|
case app.ConfigEvent:
|
||||||
pixelW = e.Config.Size.X
|
// ConfigEvent: raw pixel dimensions only.
|
||||||
pixelH = e.Config.Size.Y
|
|
||||||
// Convert pixels to DP using current scale
|
|
||||||
scale := logic.Scale()
|
|
||||||
dpW := ui.ToDp(ui.Px(pixelW), scale)
|
|
||||||
dpH := ui.ToDp(ui.Px(pixelH), scale)
|
|
||||||
logic.ConfigChan() <- editor.ConfigEvent{
|
logic.ConfigChan() <- editor.ConfigEvent{
|
||||||
Width: dpW,
|
PixelWidth: e.Config.Size.X,
|
||||||
Height: dpH,
|
PixelHeight: e.Config.Size.Y,
|
||||||
}
|
}
|
||||||
case app.FrameEvent:
|
case app.FrameEvent:
|
||||||
gtx := app.NewContext(&ops, e)
|
gtx := app.NewContext(&ops, e)
|
||||||
|
|
||||||
// Update scale if changed
|
|
||||||
newScale := gtx.Metric.PxPerDp
|
newScale := gtx.Metric.PxPerDp
|
||||||
curScale := logic.Scale()
|
curScale := logic.State().Scale()
|
||||||
if newScale != curScale {
|
if newScale != curScale {
|
||||||
logic.ConfigChan() <- editor.ScaleEvent{newScale}
|
logic.ConfigChan() <- editor.ScaleEvent{newScale}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Acquire mutex, read frame, draw, release mutex
|
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
currentElems := elems
|
currentElems := elems
|
||||||
renderer.Draw(gtx, currentElems)
|
renderer.Draw(gtx, currentElems)
|
||||||
|
|
@ -92,7 +64,6 @@ func run(w *app.Window) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// frameReceiver runs in a separate goroutine and bridges frameChan to the main loop.
|
|
||||||
func frameReceiver(w *app.Window, mu *sync.Mutex, elems *[]ui.Element, frameChan <-chan []ui.Element) {
|
func frameReceiver(w *app.Window, mu *sync.Mutex, elems *[]ui.Element, frameChan <-chan []ui.Element) {
|
||||||
for {
|
for {
|
||||||
frame := <-frameChan
|
frame := <-frameChan
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,10 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// ConfigEvent represents a window configuration change (resize, orientation).
|
// ConfigEvent represents a window configuration change (resize, orientation).
|
||||||
// Width and Height are in device-independent pixels (Dp).
|
// PixelWidth and PixelHeight are the raw pixel dimensions from Gio.
|
||||||
type ConfigEvent struct {
|
type ConfigEvent struct {
|
||||||
Width ui.Dp
|
PixelWidth int
|
||||||
Height ui.Dp
|
PixelHeight int
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScaleEvent represents a metric change (HiDPI scale factor).
|
// ScaleEvent represents a metric change (HiDPI scale factor).
|
||||||
|
|
@ -25,9 +25,8 @@ type ConfigUpdate interface {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e ConfigEvent) apply(s *State) {
|
func (e ConfigEvent) apply(s *State) {
|
||||||
s.ScreenWidth = e.Width
|
s.PixelWidth = e.PixelWidth
|
||||||
s.ScreenHeight = e.Height
|
s.PixelHeight = e.PixelHeight
|
||||||
s.Elems = EditorLayout(e.Width, e.Height)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e ScaleEvent) apply(s *State) {
|
func (e ScaleEvent) apply(s *State) {
|
||||||
|
|
@ -57,10 +56,9 @@ type Logic struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLogic creates a new Logic instance.
|
// NewLogic creates a new Logic instance.
|
||||||
// screenWidth and screenHeight are in device-independent pixels (Dp).
|
func NewLogic() *Logic {
|
||||||
func NewLogic(screenWidth, screenHeight ui.Dp) *Logic {
|
|
||||||
return &Logic{
|
return &Logic{
|
||||||
state: NewState(screenWidth, screenHeight),
|
state: NewState(),
|
||||||
configChan: make(chan ConfigUpdate),
|
configChan: make(chan ConfigUpdate),
|
||||||
frameChan: make(chan []ui.Element),
|
frameChan: make(chan []ui.Element),
|
||||||
inputChan: make(chan []InputEvent),
|
inputChan: make(chan []InputEvent),
|
||||||
|
|
@ -85,17 +83,10 @@ func (l *Logic) InputChan() chan<- []InputEvent {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResultChan returns the result channel for the logic goroutine.
|
// ResultChan returns the result channel for the logic goroutine.
|
||||||
func (l *Logic) ResultChan() <-chan ResultEvent {
|
func (l *Logic) ResultChan() chan<- ResultEvent {
|
||||||
return l.resultChan
|
return l.resultChan
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScreenSize returns the current screen dimensions in Dp.
|
|
||||||
func (l *Logic) ScreenSize() (ui.Dp, ui.Dp) {
|
|
||||||
l.mu.Lock()
|
|
||||||
defer l.mu.Unlock()
|
|
||||||
return l.state.ScreenWidth, l.state.ScreenHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
// Scale returns the current scale factor (pixels per DP).
|
// Scale returns the current scale factor (pixels per DP).
|
||||||
func (l *Logic) Scale() float32 {
|
func (l *Logic) Scale() float32 {
|
||||||
return l.state.Scale()
|
return l.state.Scale()
|
||||||
|
|
@ -106,21 +97,12 @@ func (l *Logic) Run() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case update := <-l.configChan:
|
case update := <-l.configChan:
|
||||||
// Type switch to differentiate ConfigEvent vs ScaleEvent
|
update.apply(l.state)
|
||||||
switch e := update.(type) {
|
l.frameChan <- l.state.layout()
|
||||||
case ConfigEvent:
|
|
||||||
e.apply(l.state)
|
|
||||||
l.frameChan <- l.state.Elems
|
|
||||||
case ScaleEvent:
|
|
||||||
e.apply(l.state)
|
|
||||||
l.frameChan <- l.state.Elems
|
|
||||||
}
|
|
||||||
case <-l.inputChan:
|
case <-l.inputChan:
|
||||||
// Process input (not implemented in mockup)
|
l.frameChan <- l.state.layout()
|
||||||
l.frameChan <- l.state.Elems
|
|
||||||
case <-l.resultChan:
|
case <-l.resultChan:
|
||||||
// Handle result (not implemented in mockup)
|
l.frameChan <- l.state.layout()
|
||||||
l.frameChan <- l.state.Elems
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,64 @@
|
||||||
package editor
|
package editor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"pad/internal/ui"
|
"pad/internal/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// State holds all application state owned by the logic goroutine.
|
// State holds all application state owned by the logic goroutine.
|
||||||
// All dimensions are in device-independent pixels (Dp).
|
|
||||||
type State struct {
|
type State struct {
|
||||||
ScreenWidth ui.Dp
|
PixelWidth int // raw pixel width from Gio ConfigEvent
|
||||||
ScreenHeight ui.Dp
|
PixelHeight int // raw pixel height from Gio ConfigEvent
|
||||||
scale float32 // pixels per DP, default 1.0
|
scale float32
|
||||||
Elems []ui.Element
|
Elems []ui.Element
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewState creates a new State with initial editor layout.
|
func NewState() *State {
|
||||||
func NewState(screenWidth, screenHeight ui.Dp) *State {
|
return &State{scale: 1.0}
|
||||||
return &State{
|
|
||||||
ScreenWidth: screenWidth,
|
|
||||||
ScreenHeight: screenHeight,
|
|
||||||
scale: 1.0,
|
|
||||||
Elems: EditorLayout(screenWidth, screenHeight),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetScale updates the scale factor and recomputes layout.
|
|
||||||
func (s *State) SetScale(scale float32) {
|
func (s *State) SetScale(scale float32) {
|
||||||
s.scale = scale
|
s.scale = scale
|
||||||
s.Elems = EditorLayout(s.ScreenWidth, s.ScreenHeight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scale returns the current scale factor (pixels per DP).
|
|
||||||
func (s *State) Scale() float32 {
|
func (s *State) Scale() float32 {
|
||||||
return s.scale
|
return s.scale
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// layout converts stored pixel dimensions to Dp using the current scale
|
||||||
|
// and computes the element tree. Called only when a frame is needed.
|
||||||
|
func (s *State) layout() []ui.Element {
|
||||||
|
dpW := ui.ToDp(ui.Px(s.PixelWidth), s.scale)
|
||||||
|
dpH := ui.ToDp(ui.Px(s.PixelHeight), s.scale)
|
||||||
|
s.Elems = EditorLayout(dpW, dpH)
|
||||||
|
return s.Elems
|
||||||
|
}
|
||||||
|
|
||||||
// EditorLayout computes the element tree for the editor page.
|
// EditorLayout computes the element tree for the editor page.
|
||||||
// It takes screen dimensions in Dp and returns []Element.
|
|
||||||
func EditorLayout(screenWidth, screenHeight ui.Dp) []ui.Element {
|
func EditorLayout(screenWidth, screenHeight ui.Dp) []ui.Element {
|
||||||
margin := ui.Dp(10)
|
margin := ui.Dp(10)
|
||||||
|
|
||||||
// --- Top container: status bar ---
|
// --- Top bar: filename on row 1, icons on row 2 ---
|
||||||
statusBarHeight := ui.Dp(52)
|
|
||||||
statusBarRegion := ui.Region{
|
statusBarRegion := ui.Region{
|
||||||
X: margin, Y: margin,
|
X: margin, Y: margin,
|
||||||
W: screenWidth - margin*2,
|
W: screenWidth - margin*2,
|
||||||
H: statusBarHeight,
|
H: ui.Dp(52),
|
||||||
}
|
}
|
||||||
|
statusBarW := statusBarRegion.W
|
||||||
statusBar := ui.NewContainer(
|
statusBar := ui.NewContainer(
|
||||||
statusBarRegion,
|
statusBarRegion,
|
||||||
ui.Color{R: 230, G: 230, B: 230, A: 255},
|
ui.Color{R: 230, G: 230, B: 230, A: 255},
|
||||||
[]ui.Element{
|
[]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)),
|
// Row 1: filename centered (relative to container)
|
||||||
ui.NewLabel("Ln 47, Col 12", 12, ui.Region{X: ui.Dp(32), Y: ui.Dp(2)}),
|
ui.NewLabel("a very long file name that probably will eventually need to be truncated.txt", 14, ui.Region{X: statusBarW/2 - ui.Dp(40), Y: ui.Dp(2)}),
|
||||||
ui.NewLabel("Ready", 12, ui.Region{X: ui.Dp(8), Y: ui.Dp(28)}),
|
// Row 2: cut icon left (relative to container)
|
||||||
|
ui.NewIcon("cut", ui.Region{X: ui.Dp(8), Y: ui.Dp(28)}, ui.Dp(16)),
|
||||||
|
// Row 2: status icons right (relative to container)
|
||||||
|
ui.NewLabel("Ln 47, Col 12", 12, ui.Region{X: statusBarW - ui.Dp(70), Y: ui.Dp(28)}),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Bottom container: bottom bar ---
|
// --- Bottom bar ---
|
||||||
bottomBarHeight := ui.Dp(24)
|
bottomBarHeight := ui.Dp(24)
|
||||||
bottomBarY := screenHeight - margin - bottomBarHeight
|
bottomBarY := screenHeight - margin - bottomBarHeight
|
||||||
bottomBarRegion := ui.Region{
|
bottomBarRegion := ui.Region{
|
||||||
|
|
@ -65,16 +66,17 @@ func EditorLayout(screenWidth, screenHeight ui.Dp) []ui.Element {
|
||||||
W: screenWidth - margin*2,
|
W: screenWidth - margin*2,
|
||||||
H: bottomBarHeight,
|
H: bottomBarHeight,
|
||||||
}
|
}
|
||||||
|
bottomBarW := bottomBarRegion.W
|
||||||
bottomBar := ui.NewContainer(
|
bottomBar := ui.NewContainer(
|
||||||
bottomBarRegion,
|
bottomBarRegion,
|
||||||
ui.Color{R: 230, G: 230, B: 230, A: 255},
|
ui.Color{R: 230, G: 230, B: 230, A: 255},
|
||||||
[]ui.Element{
|
[]ui.Element{
|
||||||
ui.NewLabel("Ln 47, Col 12", 12, ui.Region{X: ui.Dp(8), Y: ui.Dp(2)}),
|
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("1024 / 50000", 12, ui.Region{X: bottomBarW/2 - ui.Dp(40), Y: ui.Dp(2)}),
|
||||||
ui.NewLabel("Wrap: On", 12, ui.Region{X: screenWidth - margin - ui.Dp(60), Y: ui.Dp(2)}),
|
ui.NewLabel("Wrap: On", 12, ui.Region{X: bottomBarW - ui.Dp(60), Y: ui.Dp(2)}),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fmt.Println("[DEBUG EditorLayout] created", len([]ui.Element{statusBar, bottomBar}), "containers")
|
||||||
return []ui.Element{statusBar, bottomBar}
|
return []ui.Element{statusBar, bottomBar}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/unit"
|
"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).
|
||||||
|
|
@ -34,18 +34,15 @@ type Container struct {
|
||||||
func (c Container) Region() Region { return c.region }
|
func (c Container) Region() Region { return c.region }
|
||||||
func (c Container) Visible() bool { return c.visible }
|
func (c Container) Visible() bool { return c.visible }
|
||||||
func (c Container) Draw(gtx layout.Context, r *Renderer) {
|
func (c Container) Draw(gtx layout.Context, r *Renderer) {
|
||||||
// Draw background
|
// Draw background — children are drawn by drawElement, not here
|
||||||
if c.background != (Color{}) {
|
if c.background != (Color{}) {
|
||||||
r.drawBg(gtx, c.region, c.background)
|
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.
|
// NewContainer creates a Container with the given region, background, and children.
|
||||||
func NewContainer(region Region, bg Color, children []Element) Container {
|
func NewContainer(region Region, bg Color, children []Element) Container {
|
||||||
|
fmt.Printf("[DEBUG NewContainer] region={X=%.0f Y=%.0f W=%.0f H=%.0f} children=%d\n", region.X, region.Y, region.W, region.H, len(children))
|
||||||
return Container{
|
return Container{
|
||||||
region: region,
|
region: region,
|
||||||
visible: true,
|
visible: true,
|
||||||
|
|
@ -71,7 +68,12 @@ 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) {
|
func (l Label) Draw(gtx layout.Context, r *Renderer) {
|
||||||
r.drawText(gtx, l.Text, l.FontSize, l.region, l.Color)
|
fmt.Printf("[DEBUG Label.Draw] text=%q region={X=%.0f Y=%.0f}\n", l.Text, l.region.X, l.region.Y)
|
||||||
|
col := l.Color
|
||||||
|
if col == (Color{}) {
|
||||||
|
col = Color{R: 0, G: 0, B: 0, A: 255}
|
||||||
|
}
|
||||||
|
r.drawText(gtx, l.Text, l.FontSize, l.region, col)
|
||||||
}
|
}
|
||||||
func (l Label) ID() string { return l.id }
|
func (l Label) ID() string { return l.id }
|
||||||
|
|
||||||
|
|
@ -97,6 +99,7 @@ type Icon struct {
|
||||||
func (i Icon) Region() Region { return i.region }
|
func (i Icon) Region() Region { return i.region }
|
||||||
func (i Icon) Visible() bool { return i.visible }
|
func (i Icon) Visible() bool { return i.visible }
|
||||||
func (i Icon) Draw(gtx layout.Context, r *Renderer) {
|
func (i Icon) Draw(gtx layout.Context, r *Renderer) {
|
||||||
|
fmt.Printf("[DEBUG Icon.Draw] name=%q icon=%v region={X=%.0f Y=%.0f}\n", i.Name, r.icon(i.Name) != nil, i.region.X, i.region.Y)
|
||||||
img := r.icon(i.Name)
|
img := r.icon(i.Name)
|
||||||
if img == nil {
|
if img == nil {
|
||||||
return
|
return
|
||||||
|
|
@ -200,11 +203,8 @@ 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) {
|
func (b Button) Draw(gtx layout.Context, r *Renderer) {
|
||||||
th := material.NewTheme()
|
col := Color{R: 0, G: 0, B: 0, A: 255}
|
||||||
th.Shaper = r.shp
|
r.drawText(gtx, b.Text, r.theme.FontSize, b.region, col)
|
||||||
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 }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,24 @@ package ui
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"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"
|
||||||
"gioui.org/op/paint"
|
"gioui.org/op/paint"
|
||||||
"gioui.org/text"
|
"gioui.org/text"
|
||||||
"gioui.org/unit"
|
"gioui.org/unit"
|
||||||
"gioui.org/widget/material"
|
"golang.org/x/image/math/fixed"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// maxInt32 is a large value used as MaxWidth for single-line text layout.
|
||||||
|
const maxInt32 = 1<<31 - 1
|
||||||
|
|
||||||
//go:embed icons/*.png
|
//go:embed icons/*.png
|
||||||
var iconFS embed.FS
|
var iconFS embed.FS
|
||||||
|
|
||||||
|
|
@ -69,13 +74,19 @@ 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).
|
||||||
func (r *Renderer) Draw(gtx layout.Context, elems []Element) {
|
func (r *Renderer) Draw(gtx layout.Context, elems []Element) {
|
||||||
// Clip to window bounds
|
fmt.Println("[DEBUG Draw] elems:", len(elems), "scale:", r.scale.Scale())
|
||||||
clipRect := clip.Rect{
|
|
||||||
Min: gtx.Constraints.Min,
|
|
||||||
Max: gtx.Constraints.Max,
|
|
||||||
}.Push(gtx.Ops)
|
|
||||||
|
|
||||||
for _, e := range elems {
|
// Gio sets constraints to layout.Exact(windowSize), so Min==Max. Use (0,0) as Min.
|
||||||
|
winW := gtx.Constraints.Max.X
|
||||||
|
winH := gtx.Constraints.Max.Y
|
||||||
|
clipRect := clip.Rect{
|
||||||
|
Min: image.Point{X: 0, Y: 0},
|
||||||
|
Max: image.Point{X: winW, Y: winH},
|
||||||
|
}.Push(gtx.Ops)
|
||||||
|
fmt.Println("[DEBUG Draw] window clip:", image.Point{X: 0, Y: 0}, image.Point{X: winW, Y: winH})
|
||||||
|
|
||||||
|
for i, e := range elems {
|
||||||
|
fmt.Printf("[DEBUG Draw] elem[%d]: %T visible=%v\n", i, e, e.Visible())
|
||||||
if !e.Visible() {
|
if !e.Visible() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
@ -85,32 +96,34 @@ func (r *Renderer) Draw(gtx layout.Context, elems []Element) {
|
||||||
clipRect.Pop()
|
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) {
|
func (r *Renderer) drawElement(gtx layout.Context, e Element) {
|
||||||
reg := e.Region()
|
reg := e.Region()
|
||||||
|
fmt.Printf("[DEBUG drawElement] %T region={X=%.0f Y=%.0f W=%.0f H=%.0f}\n", e, reg.X, reg.Y, reg.W, reg.H)
|
||||||
|
|
||||||
// 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 {
|
if container, ok := e.(Container); ok {
|
||||||
for _, child := range container.children {
|
// Clip to container bounds, draw background, then offset children
|
||||||
child.Draw(gtx, r)
|
fmt.Printf("[DEBUG drawElement] container children=%d originPx=(%d,%d)\n", len(container.children), int(r.toPx(reg.X)), int(r.toPx(reg.Y)))
|
||||||
|
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)
|
||||||
|
e.Draw(gtx, r) // draw container background
|
||||||
|
offset := op.Offset(image.Pt(int(r.toPx(reg.X)), int(r.toPx(reg.Y)))).Push(gtx.Ops)
|
||||||
|
for i, child := range container.children {
|
||||||
|
fmt.Printf("[DEBUG drawElement] child[%d]: %T region={X=%.0f Y=%.0f}\n", i, child, child.Region().X, child.Region().Y)
|
||||||
|
r.drawElement(gtx, child)
|
||||||
}
|
}
|
||||||
|
offset.Pop()
|
||||||
|
clipRect.Pop()
|
||||||
|
fmt.Println("[DEBUG drawElement] clip popped")
|
||||||
|
} else {
|
||||||
|
// Leaf element: no self-clip (region may be zero-sized; clip is handled by parent container)
|
||||||
|
e.Draw(gtx, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dispatch to element's Draw method
|
|
||||||
e.Draw(gtx, r)
|
|
||||||
|
|
||||||
clipRect.Pop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawBg draws a solid-color background for the given region.
|
|
||||||
func (r *Renderer) drawBg(gtx layout.Context, reg Region, col Color) {
|
func (r *Renderer) drawBg(gtx layout.Context, reg Region, col Color) {
|
||||||
|
fmt.Printf("[DEBUG drawBg] region={X=%.0f Y=%.0f W=%.0f H=%.0f} col={%d,%d,%d,%d}\n", reg.X, reg.Y, reg.W, reg.H, col.R, col.G, col.B, col.A)
|
||||||
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))},
|
||||||
|
|
@ -118,31 +131,86 @@ func (r *Renderer) drawBg(gtx layout.Context, reg Region, col Color) {
|
||||||
paint.ColorOp{Color: color.NRGBA{R: col.R, G: col.G, B: col.B, A: col.A}}.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()
|
||||||
|
fmt.Println("[DEBUG drawBg] bg drawn")
|
||||||
}
|
}
|
||||||
|
|
||||||
// drawText draws text using Gio's textView approach.
|
|
||||||
func (r *Renderer) drawText(gtx layout.Context, str string, size unit.Sp, reg Region, col Color) {
|
func (r *Renderer) drawText(gtx layout.Context, str string, size unit.Sp, reg Region, col Color) {
|
||||||
th := material.NewTheme()
|
fmt.Printf("[DEBUG drawText] str=%q size=%.0f region={X=%.0f Y=%.0f} col={%d,%d,%d,%d}\n", str, float32(size), reg.X, reg.Y, col.R, col.G, col.B, col.A)
|
||||||
th.Shaper = r.shp
|
if str == "" {
|
||||||
gtx.Constraints.Min.X = int(r.toPx(reg.X))
|
|
||||||
gtx.Constraints.Min.Y = int(r.toPx(reg.Y))
|
|
||||||
material.Label(th, size, str).Layout(gtx)
|
|
||||||
}
|
|
||||||
|
|
||||||
// drawPng draws a PNG image at the given region and size.
|
|
||||||
func (r *Renderer) drawPng(gtx layout.Context, img image.Image, reg Region, width, height Dp) {
|
|
||||||
if img == nil {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// Layout text with width constraints (per layout_rendering.md)
|
||||||
|
r.shp.LayoutString(text.Parameters{
|
||||||
|
PxPerEm: fixed.I(gtx.Sp(size)),
|
||||||
|
MinWidth: 0,
|
||||||
|
MaxWidth: maxInt32,
|
||||||
|
MaxLines: 1,
|
||||||
|
}, str)
|
||||||
|
r.drawLineText(gtx, reg.X, reg.Y, col)
|
||||||
|
fmt.Println("[DEBUG drawText] text drawn")
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawLineText iterates laid-out glyphs and draws each line using op.Record for clipping safety.
|
||||||
|
func (r *Renderer) drawLineText(gtx layout.Context, x, y Dp, col Color) {
|
||||||
|
m := op.Record(gtx.Ops)
|
||||||
|
var glyphs [32]text.Glyph
|
||||||
|
line := glyphs[:0]
|
||||||
|
for g, ok := r.shp.NextGlyph(); ok; g, ok = r.shp.NextGlyph() {
|
||||||
|
line = append(line, g)
|
||||||
|
if g.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 {
|
||||||
|
r.drawLine(gtx, line, x, y, col)
|
||||||
|
line = line[:0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(line) > 0 {
|
||||||
|
r.drawLine(gtx, line, x, y, col)
|
||||||
|
}
|
||||||
|
call := m.Stop()
|
||||||
|
call.Add(gtx.Ops)
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawLine draws a single line of glyphs at the given position.
|
||||||
|
// Matches Gio's paintGlyph: offset by (x + first.X, y + first.Y).
|
||||||
|
func (r *Renderer) drawLine(gtx layout.Context, line []text.Glyph, x, y Dp, col Color) {
|
||||||
|
if len(line) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
first := line[0]
|
||||||
|
// Offset: desired document position + first glyph's relative position.
|
||||||
|
// first.X is in fixed.Int26_6 (divide by 64 for pixels), first.Y is in pixels.
|
||||||
|
offX := float32(gtx.Dp(unit.Dp(x))) + float32(first.X)/64.0
|
||||||
|
offY := float32(gtx.Dp(unit.Dp(y))) + float32(first.Y)
|
||||||
|
t := op.Affine(f32.Affine2D{}.Offset(f32.Pt(offX, offY))).Push(gtx.Ops)
|
||||||
|
|
||||||
|
// Draw vector glyphs
|
||||||
|
path := r.shp.Shape(line)
|
||||||
|
outline := clip.Outline{Path: path}.Op().Push(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)
|
||||||
|
outline.Pop()
|
||||||
|
|
||||||
|
// Draw bitmap glyphs (emoji, etc.)
|
||||||
|
if call := r.shp.Bitmaps(line); call != (op.CallOp{}) {
|
||||||
|
call.Add(gtx.Ops)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Renderer) drawPng(gtx layout.Context, img image.Image, reg Region, width, height Dp) {
|
||||||
|
fmt.Printf("[DEBUG drawPng] icon=%v region={X=%.0f Y=%.0f} size={W=%.0f H=%.0f}\n", img != nil, reg.X, reg.Y, width, height)
|
||||||
|
if img == nil {
|
||||||
|
fmt.Println("[DEBUG drawPng] icon nil, skipping")
|
||||||
|
return
|
||||||
|
}
|
||||||
xPx := int(r.toPx(reg.X))
|
xPx := int(r.toPx(reg.X))
|
||||||
yPx := int(r.toPx(reg.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)
|
_ = image.Rect(xPx, yPx, xPx+wPx, yPx+hPx)
|
||||||
|
|
||||||
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)
|
||||||
paint.PaintOp{}.Add(gtx.Ops)
|
paint.PaintOp{}.Add(gtx.Ops)
|
||||||
stack.Pop()
|
stack.Pop()
|
||||||
|
fmt.Println("[DEBUG drawPng] png drawn")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user