From dfc112f5d48086ec7e3ad719e0f875d6e644f66c Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Thu, 7 May 2026 18:19:54 -0400 Subject: [PATCH] Update specs to match Element interface implementation - Element is an interface with Region() and Visible() methods - Concrete types have unexported region/visible/id fields - Constructors (NewLabel, NewListView) replace embedded Element struct - Theme simplified to FontSize only - Removed OnSelect/OnPress callback IDs from element types - Added internal/ui/ to architecture in spec.md Co-authored-by: Qwen-Coder --- doc/SPEC.md | 3 +- doc/element_model.md | 97 ++++++++++++++++++++++---------------------- 2 files changed, 51 insertions(+), 49 deletions(-) diff --git a/doc/SPEC.md b/doc/SPEC.md index 2498317..893b613 100644 --- a/doc/SPEC.md +++ b/doc/SPEC.md @@ -28,7 +28,7 @@ Pad is a minimal, high-performance plain text editor for Android. It is configur ``` cmd/ - pad/ # Mobile entry point + pad/ # Mobile entry point (Gioui window + frame loop) internal/ browser/ # Directory listing, pagination, search, alphabetical index @@ -38,6 +38,7 @@ internal/ conflict/ # External change reconciliation, merge strategies state/ # App state persistence, per-file cursor map, restore logic undo/ # Undo operation log, context anchoring, rebasing + ui/ # Element types, renderer, Gioui drawing ``` ## 4. File Browser diff --git a/doc/element_model.md b/doc/element_model.md index f8f44ca..9032da0 100644 --- a/doc/element_model.md +++ b/doc/element_model.md @@ -33,23 +33,33 @@ Consistent with Gioui's `unit` package: The logic layer works exclusively in `Dp` (positions/sizes) and `Sp` (fonts). The renderer converts to raw pixels using the `Metric` provided by Gioui's transaction context. +### 2.3 Element Interface + +All UI elements implement the `Element` interface, which exposes the region and visibility: + +```go +type Element interface { + Region() Region + Visible() bool +} +``` + +Concrete elements are plain Go structs with unexported fields for `region`, `visible`, and `id`. Constructors (`NewLabel`, `NewListView`, etc.) set these fields, keeping the API clean and preventing external mutation. + ```go -// Region defines a screen area in device-independent pixels (Dp). type Region struct { X, Y unit.Dp W, H unit.Dp } -// Element is the base of all UI elements. -type Element struct { - ID string // unique identifier for input routing - Region Region // where to draw (computed by logic/layout pass, in Dp) - Visible bool // false = skip rendering -} -``` +type Label struct { /* unexported region, visible, id + exported Text, Align, ... */ } +func NewLabel(text string, fontSize unit.Sp, region Region) Label +func (l Label) Region() Region +func (l Label) Visible() bool +func (l Label) ID() string ``` -Each concrete element embeds `Element` and adds its own fields. +The renderer accepts `[]Element` and dispatches via type switches. This keeps the logic layer testable — tests assert on concrete element values without any framework or interface indirection in the test code. ## 3. Element Catalog @@ -57,7 +67,7 @@ Each concrete element embeds `Element` and adds its own fields. ```go type Label struct { - Element + /* region, visible, id — unexported */ Text string Align TextAlign // start, center, end FontSize unit.Sp // 0 = theme default @@ -74,7 +84,7 @@ Used for: headers, status bar text, file sizes, line numbers. ```go type TextField struct { - Element + /* region, visible, id — unexported */ Value string Placeholder string Focused bool // true = show cursor, accept keyboard input @@ -97,7 +107,7 @@ The editor's `TextField` only contains lines visible in the current viewport. Th ```go type Cursor struct { - Element // region is the cursor rectangle within the text field + /* region, visible, id — unexported */ Line int // line number (0-indexed into VisibleLines) Column int // character offset within the line Blinking bool // current blink state @@ -116,11 +126,10 @@ Rendered as an overlay inside a `TextField`. The logic layer computes cursor pos ```go type ListView struct { - Element + /* region, visible, id — unexported */ Items []ListItem ScrollOffset int // index of the first visible item Selected int // index of selected item (-1 = none) - OnSelect string // callback ID for routing } type ListItem struct { @@ -136,10 +145,9 @@ Used for: directory browser. Only contains items for the current viewport. ```go type AlphaIndex struct { - Element - Letters []string // visible letters (e.g., ["A", "B", "C", ...]) - ActiveLetter string // currently pressed letter (for highlighting) - OnTap string // callback ID + /* region, visible, id — unexported */ + Letters []string // visible letters (e.g., ["A", "B", "C", ...]) + ActiveLetter string // currently pressed letter (for highlighting) } ``` @@ -149,11 +157,10 @@ Used for: quick navigation in the directory browser. ```go type Button struct { - Element - Text string - Enabled bool - Primary bool // true = emphasized style (e.g., filled background) - OnPress string // callback ID + /* region, visible, id — unexported */ + Text string + Enabled bool + Primary bool // true = emphasized style (e.g., filled background) } ``` @@ -163,7 +170,7 @@ Used for: merge resolution (ours/theirs/both), dismiss, apply. ```go type MergeHunk struct { - Element + /* region, visible, id — unexported */ HunkNumber int // N of M TotalHunks int LineRange string // display text, e.g., "Lines 142–148" @@ -182,9 +189,9 @@ Used for: conflict resolution UI. The logic layer produces one `MergeHunk` eleme ```go type StatusBar struct { - Element - Left string // e.g., "Ln 10, Col 5" - Right string // e.g., "1024 / 50000 bytes" + /* region, visible, id — unexported */ + Left string // e.g., "Ln 10, Col 5" + Right string // e.g., "1024 / 50000 bytes" } ``` @@ -192,7 +199,7 @@ type StatusBar struct { ```go type Toast struct { - Element + /* region, visible, id — unexported */ Text string Timeout time.Duration // auto-dismiss after this duration } @@ -204,7 +211,7 @@ Used for: "undo skipped (text changed)", "file saved", "conflict detected". ```go type Spacer struct { - Element + /* region, visible, id — unexported */ // Region.H defines the spacer height } ``` @@ -276,24 +283,15 @@ Elements are rendered in slice order. Later elements draw on top of earlier ones ## 7. Theme -Minimal theming via a `Theme` struct passed to the layout pass: +Minimal theming via a `Theme` struct passed to the renderer: ```go type Theme struct { - ScreenWidth unit.Dp - ScreenHeight unit.Dp - FontSize unit.Sp - HeaderH unit.Dp - StatusBarH unit.Dp - Padding unit.Dp - TextColor Color - BgColor Color - AccentColor Color - Metric unit.Metric // PxPerDp, PxPerSp — for renderer use + FontSize unit.Sp } ``` -Elements that don't specify explicit colors/sizes use theme defaults. The theme is part of the app state, not a global. +Elements that don't specify explicit colors/sizes use theme defaults. The theme is part of the app state, not a global. Screen dimensions, header/status bar heights, and padding are layout constants known to the logic layer's layout functions, not theme fields. ## 8. Page Compositions @@ -335,32 +333,35 @@ Elements that don't specify explicit colors/sizes use theme defaults. The theme ## 9. Testing -Tests import the logic layer, call render functions, and assert on the resulting `[]Element`: +Tests import the logic layer, call layout functions, and assert on the resulting `[]Element`. Since `Element` is an interface, tests type-assert to concrete types: ```go func TestBrowserFiltersBySearch(t *testing.T) { state := BrowserState{Entries: allFiles, Search: "foo"} - elems := BrowserLayout(state, theme) + elems := BrowserLayout(state, screen) - list := elems[2].(ListView) + list, ok := elems[2].(ListView) + assert.True(t, ok) assert.Equal(t, 3, len(list.Items)) assert.Equal(t, "foo.txt", list.Items[0].Text) } func TestEditorCursorPosition(t *testing.T) { state := EditorState{Cursor: 1024, Buffer: buffer} - elems := EditorLayout(state, theme) + elems := EditorLayout(state, screen) - cursor := elems[1].(Cursor) + cursor, ok := elems[1].(Cursor) + assert.True(t, ok) assert.Equal(t, 42, cursor.Line) assert.Equal(t, 10, cursor.Column) } func TestMergeHunkDisplay(t *testing.T) { state := MergeState{CurrentHunk: 2, Hunks: hunks} - elems := MergeLayout(state, theme) + elems := MergeLayout(state, screen) - hunk := elems[2].(MergeHunk) + hunk, ok := elems[2].(MergeHunk) + assert.True(t, ok) assert.Equal(t, 3, hunk.HunkNumber) assert.Equal(t, 7, hunk.TotalHunks) assert.Equal(t, Unresolved, hunk.Resolution)