From fe3e7c3d75ce189389d38682ccdf47700184adba Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Sun, 10 May 2026 06:45:47 -0400 Subject: [PATCH] Revert to last working state (68437c3) before PNG icon changes --- cmd/pad/main.go | 40 ++++++++----- internal/editor/logic.go | 8 ++- internal/editor/state.go | 32 +++------- internal/ui/render.go | 123 +++++++++------------------------------ 4 files changed, 67 insertions(+), 136 deletions(-) diff --git a/cmd/pad/main.go b/cmd/pad/main.go index f3027cf..db1e1aa 100644 --- a/cmd/pad/main.go +++ b/cmd/pad/main.go @@ -38,15 +38,12 @@ func run(w *app.Window) error { initialDPW := ui.Dp(390) initialDPH := ui.Dp(844) - // Track previous scale to detect changes - prevScale := float32(1.0) + // Track pixel dimensions from ConfigEvent (for resize handling) + var pixelW, pixelH int // Create logic instance with initial DP size and scale=1.0 logic := editor.NewLogic(initialDPW, initialDPH) - // Set State reference on Renderer — it reads scale/window size from State directly - renderer.SetState(logic.State()) - // Shared state protected by mutex var mu sync.Mutex var elems []ui.Element @@ -63,9 +60,12 @@ func run(w *app.Window) error { case app.DestroyEvent: return e.Err case app.ConfigEvent: - // Convert pixel dimensions to DP using current scale (starts at 1.0) - dpW := ui.ToDp(ui.Px(e.Config.Size.X), 1.0) - dpH := ui.ToDp(ui.Px(e.Config.Size.Y), 1.0) + // Store pixel dimensions from ConfigEvent + pixelW = e.Config.Size.X + pixelH = e.Config.Size.Y + // Convert to DP using current scale (starts at 1.0) + dpW := ui.ToDp(ui.Px(pixelW), 1.0) + dpH := ui.ToDp(ui.Px(pixelH), 1.0) // Send config event to logic goroutine (in DP) logic.ConfigChan() <- editor.ConfigEvent{ Width: dpW, @@ -73,20 +73,30 @@ func run(w *app.Window) error { } case app.FrameEvent: gtx := app.NewContext(&ops, e) - newScale := gtx.Metric.PxPerDp - // If scale changed, send scale event to logic goroutine - if newScale != prevScale { - logic.ConfigChan() <- editor.ScaleEvent{Scale: newScale} - prevScale = newScale + // Get scale from State — this is updated by ScaleEvent + newScale := logic.Scale() + + // Convert pixel dimensions to DP using actual scale + dpW := ui.ToDp(ui.Px(pixelW), newScale) + dpH := ui.ToDp(ui.Px(pixelH), newScale) + + // Send updated config to logic goroutine + logic.ConfigChan() <- editor.ConfigEvent{ + Width: dpW, + Height: dpH, } - // Acquire mutex, read frame, draw, e.Frame(), release mutex + // Update renderer with current scale from State + renderer.SetScale(newScale) + + // Acquire mutex, read frame, draw, release mutex mu.Lock() currentElems := elems + mu.Unlock() + renderer.Draw(gtx, currentElems) e.Frame(&ops) - mu.Unlock() } } } diff --git a/internal/editor/logic.go b/internal/editor/logic.go index f20db5c..f334bf0 100644 --- a/internal/editor/logic.go +++ b/internal/editor/logic.go @@ -25,7 +25,9 @@ type ConfigUpdate interface { } func (e ConfigEvent) apply(s *State) { - s.SetSize(e.Width, e.Height) + s.ScreenWidth = e.Width + s.ScreenHeight = e.Height + s.Elems = EditorLayout(e.Width, e.Height) } func (e ScaleEvent) apply(s *State) { @@ -91,14 +93,14 @@ func (l *Logic) ResultChan() <-chan ResultEvent { func (l *Logic) ScreenSize() (ui.Dp, ui.Dp) { l.mu.Lock() defer l.mu.Unlock() - return l.state.ScreenWidth(), l.state.ScreenHeight() + return l.state.ScreenWidth, l.state.ScreenHeight } // Scale returns the current scale factor (pixels per DP). func (l *Logic) Scale() float32 { l.mu.Lock() defer l.mu.Unlock() - return l.state.Scale() + return l.state.Scale } // Run runs the logic goroutine loop. diff --git a/internal/editor/state.go b/internal/editor/state.go index 641c259..e9783e0 100644 --- a/internal/editor/state.go +++ b/internal/editor/state.go @@ -7,9 +7,9 @@ import ( // State holds all application state owned by the logic goroutine. // All dimensions are in device-independent pixels (Dp). type State struct { - screenWidth ui.Dp - screenHeight ui.Dp - scale float32 // pixels per DP, default 1.0 + ScreenWidth ui.Dp + ScreenHeight ui.Dp + Scale float32 // pixels per DP, default 1.0 Elems []ui.Element } @@ -18,33 +18,17 @@ type State struct { // scale defaults to 1.0 until updated by ScaleEvent. func NewState(screenWidth, screenHeight ui.Dp) *State { return &State{ - screenWidth: screenWidth, - screenHeight: screenHeight, - scale: 1.0, + ScreenWidth: screenWidth, + ScreenHeight: screenHeight, + Scale: 1.0, Elems: EditorLayout(screenWidth, screenHeight), } } -// Scale returns the current scale factor (pixels per DP). -func (s *State) Scale() float32 { return s.scale } - -// ScreenWidth returns the current screen width in Dp. -func (s *State) ScreenWidth() ui.Dp { return s.screenWidth } - -// ScreenHeight returns the current screen height in Dp. -func (s *State) ScreenHeight() ui.Dp { return s.screenHeight } - // SetScale updates the scale factor and recomputes layout. func (s *State) SetScale(scale float32) { - s.scale = scale - s.Elems = EditorLayout(s.screenWidth, s.screenHeight) -} - -// SetSize updates the screen dimensions and recomputes layout. -func (s *State) SetSize(width, height ui.Dp) { - s.screenWidth = width - s.screenHeight = height - s.Elems = EditorLayout(width, height) + s.Scale = scale + s.Elems = EditorLayout(s.ScreenWidth, s.ScreenHeight) } // EditorLayout computes regions for the editor page. diff --git a/internal/ui/render.go b/internal/ui/render.go index 64a4467..cf34263 100644 --- a/internal/ui/render.go +++ b/internal/ui/render.go @@ -1,12 +1,8 @@ package ui import ( - "bytes" - "embed" "image" "image/color" - _ "image/png" - "strings" "gioui.org/f32" "gioui.org/layout" @@ -19,75 +15,43 @@ import ( "golang.org/x/image/math/fixed" ) -//go:embed icons/*.png -var iconFS embed.FS - -// StateReader provides access to scale and window dimensions from State. -// Implemented by editor.State to avoid import cycles. -type StateReader interface { - Scale() float32 - ScreenWidth() Dp - ScreenHeight() Dp -} - // Renderer consumes a slice of elements and draws them. type Renderer struct { theme Theme shp *text.Shaper - state StateReader // read-only access to State for scale/window size - icons map[string]image.Image // loaded PNG icons + scale float32 // pixels per DP, from State.Scale } // New creates a new Renderer. func New(th Theme, shp *text.Shaper) *Renderer { - r := &Renderer{theme: th, shp: shp, icons: make(map[string]image.Image)} - r.loadIcons() - return r + return &Renderer{theme: th, shp: shp, scale: 1.0} } -// loadIcons loads all PNG icons from the embedded filesystem. -func (r *Renderer) loadIcons() { - files := []string{"cut.png", "copy.png", "paste.png", "search.png", "conflict.png", "wrapOn.png", "wrapOff.png"} - for _, filename := range files { - data, err := iconFS.ReadFile(filename) - if err != nil { - continue // skip missing icons - } - img, _, err := image.Decode(bytes.NewReader(data)) - if err != nil { - continue - } - name := strings.TrimSuffix(filename, ".png") - r.icons[name] = img - } +// SetScale updates the renderer's scale factor from State.Scale. +func (r *Renderer) SetScale(scale float32) { + r.scale = scale } -// SetState sets the State reference for the Renderer. -// The Renderer reads scale and window size from State directly. -func (r *Renderer) SetState(state StateReader) { - r.state = state -} - -// toPx converts Dp to physical pixels using the scale from State. +// toPx converts Dp to physical pixels using the renderer's scale. func (r *Renderer) toPx(dp Dp) Px { - return ToPx(dp, r.state.Scale()) + return ToPx(dp, r.scale) } -// toDp converts physical pixels to Dp using the scale from State. +// toDp converts physical pixels to Dp using the renderer's scale. func (r *Renderer) toDp(px Px) Dp { - return ToDp(px, r.state.Scale()) + return ToDp(px, r.scale) } // Draw iterates elements and draws each in slice order (back-to-front). -// All scale and window size values come from State, never from gtx. +// It captures the initial constraints once at the start, then passes them +// to each render function for consistent positioning. +// The scale is set externally via SetScale() from State.Scale. func (r *Renderer) Draw(gtx layout.Context, elems []Element) { - // Use window dimensions from State — convert DP to pixels using State.Scale - state := r.state - windowWPx := int(r.toPx(state.ScreenWidth())) - windowHPx := int(r.toPx(state.ScreenHeight())) + // Capture initial constraints once — before any clips modify them. + // These are in physical pixels and serve as the window bounds for all positioning. initialConstraints := WindowConstraints{ - Min: image.Point{X: 0, Y: 0}, - Max: image.Point{X: windowWPx, Y: windowHPx}, + Min: gtx.Constraints.Min, + Max: gtx.Constraints.Max, } for _, e := range elems { @@ -194,15 +158,22 @@ func (r *Renderer) drawStatusBar(gtx layout.Context, sb StatusBar, _ WindowConst // Left side: Cut, Copy, Paste icons (always visible) leftX := reg.X + Dp(8) iconY := iconsLineY - iconSize := Dp(16) // 16dp icons - r.drawPng(gtx, r.icons["cut"], leftX, iconY, iconSize, iconSize, color.NRGBA{R: 0, G: 0, B: 0, A: 255}) - leftX += Dp(28) - r.drawPng(gtx, r.icons["copy"], leftX, iconY, iconSize, iconSize, color.NRGBA{R: 0, G: 0, B: 0, A: 255}) - leftX += Dp(28) - r.drawPng(gtx, r.icons["paste"], leftX, iconY, iconSize, iconSize, color.NRGBA{R: 0, G: 0, B: 0, A: 255}) + r.drawText(gtx, th.Shaper, "✂", r.theme.FontSize, leftX, iconY, color.NRGBA{R: 0, G: 0, B: 0, A: 255}) + leftX += Dp(36) + r.drawText(gtx, th.Shaper, "⎘", r.theme.FontSize, leftX, iconY, color.NRGBA{R: 0, G: 0, B: 0, A: 255}) + leftX += Dp(36) + r.drawText(gtx, th.Shaper, "⎘", r.theme.FontSize, leftX, iconY, 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, th.Shaper, "⚠", r.theme.FontSize, rightX, iconY, color.NRGBA{R: 0, G: 0, B: 0, A: 255}) + } + rightX -= Dp(36) + r.drawText(gtx, th.Shaper, "🔍", r.theme.FontSize, rightX, iconY, color.NRGBA{R: 0, G: 0, B: 0, A: 255}) c.Pop() } @@ -392,42 +363,6 @@ func (r *Renderer) drawLine(gtx layout.Context, shp *text.Shaper, line []text.Gl t.Pop() } -// drawPng draws a PNG image at the given position and size. -// x, y are in Dp (top-left corner). -// width, height are in Dp (target size). -// col is the tint color — if alpha < 255, the image is tinted using blendOp.DstIn. -func (r *Renderer) drawPng(gtx layout.Context, img image.Image, x, y Dp, width, height Dp, col color.NRGBA) { - if img == nil { - return - } - - // Convert Dp to pixels - xPx := int(r.toPx(x)) - yPx := int(r.toPx(y)) - wPx := int(r.toPx(width)) - hPx := int(r.toPx(height)) - - // Compute source rectangle (full image) - src := image.Rect(0, 0, img.Bounds().Dx(), img.Bounds().Dy()) - _ = src // unused, but kept for clarity - - // Compute destination rectangle - dst := image.Rect(xPx, yPx, xPx+wPx, yPx+hPx) - - // If tint color has alpha < 255, apply tint using blendOp.DstIn - if col.A < 255 { - // Draw tinted rectangle over the image - bgClip := clip.Rect(dst).Push(gtx.Ops) - paint.ColorOp{Color: col}.Add(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) - bgClip.Pop() - } - - // Draw the image - paint.NewImageOp(img).Add(gtx.Ops) - paint.PaintOp{}.Add(gtx.Ops) -} - // WindowConstraints tracks the initial window constraints captured at the start of Draw. // All elements are positioned relative to these bounds to ensure consistency. type WindowConstraints struct {