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 <qwen-coder@alibabacloud.com>
This commit is contained in:
Greg Pomerantz 2026-05-07 18:19:54 -04:00
parent 6720359360
commit dfc112f5d4
2 changed files with 51 additions and 49 deletions

View File

@ -28,7 +28,7 @@ Pad is a minimal, high-performance plain text editor for Android. It is configur
``` ```
cmd/ cmd/
pad/ # Mobile entry point pad/ # Mobile entry point (Gioui window + frame loop)
internal/ internal/
browser/ # Directory listing, pagination, search, alphabetical index browser/ # Directory listing, pagination, search, alphabetical index
@ -38,6 +38,7 @@ internal/
conflict/ # External change reconciliation, merge strategies conflict/ # External change reconciliation, merge strategies
state/ # App state persistence, per-file cursor map, restore logic state/ # App state persistence, per-file cursor map, restore logic
undo/ # Undo operation log, context anchoring, rebasing undo/ # Undo operation log, context anchoring, rebasing
ui/ # Element types, renderer, Gioui drawing
``` ```
## 4. File Browser ## 4. File Browser

View File

@ -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. 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 ```go
// Region defines a screen area in device-independent pixels (Dp).
type Region struct { type Region struct {
X, Y unit.Dp X, Y unit.Dp
W, H unit.Dp W, H unit.Dp
} }
// Element is the base of all UI elements. type Label struct { /* unexported region, visible, id + exported Text, Align, ... */ }
type Element struct { func NewLabel(text string, fontSize unit.Sp, region Region) Label
ID string // unique identifier for input routing func (l Label) Region() Region
Region Region // where to draw (computed by logic/layout pass, in Dp) func (l Label) Visible() bool
Visible bool // false = skip rendering 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 ## 3. Element Catalog
@ -57,7 +67,7 @@ Each concrete element embeds `Element` and adds its own fields.
```go ```go
type Label struct { type Label struct {
Element /* region, visible, id — unexported */
Text string Text string
Align TextAlign // start, center, end Align TextAlign // start, center, end
FontSize unit.Sp // 0 = theme default FontSize unit.Sp // 0 = theme default
@ -74,7 +84,7 @@ Used for: headers, status bar text, file sizes, line numbers.
```go ```go
type TextField struct { type TextField struct {
Element /* region, visible, id — unexported */
Value string Value string
Placeholder string Placeholder string
Focused bool // true = show cursor, accept keyboard input 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 ```go
type Cursor struct { 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) Line int // line number (0-indexed into VisibleLines)
Column int // character offset within the line Column int // character offset within the line
Blinking bool // current blink state Blinking bool // current blink state
@ -116,11 +126,10 @@ Rendered as an overlay inside a `TextField`. The logic layer computes cursor pos
```go ```go
type ListView struct { type ListView struct {
Element /* region, visible, id — unexported */
Items []ListItem Items []ListItem
ScrollOffset int // index of the first visible item ScrollOffset int // index of the first visible item
Selected int // index of selected item (-1 = none) Selected int // index of selected item (-1 = none)
OnSelect string // callback ID for routing
} }
type ListItem struct { type ListItem struct {
@ -136,10 +145,9 @@ Used for: directory browser. Only contains items for the current viewport.
```go ```go
type AlphaIndex struct { type AlphaIndex struct {
Element /* region, visible, id — unexported */
Letters []string // visible letters (e.g., ["A", "B", "C", ...]) Letters []string // visible letters (e.g., ["A", "B", "C", ...])
ActiveLetter string // currently pressed letter (for highlighting) ActiveLetter string // currently pressed letter (for highlighting)
OnTap string // callback ID
} }
``` ```
@ -149,11 +157,10 @@ Used for: quick navigation in the directory browser.
```go ```go
type Button struct { type Button struct {
Element /* region, visible, id — unexported */
Text string Text string
Enabled bool Enabled bool
Primary bool // true = emphasized style (e.g., filled background) Primary bool // true = emphasized style (e.g., filled background)
OnPress string // callback ID
} }
``` ```
@ -163,7 +170,7 @@ Used for: merge resolution (ours/theirs/both), dismiss, apply.
```go ```go
type MergeHunk struct { type MergeHunk struct {
Element /* region, visible, id — unexported */
HunkNumber int // N of M HunkNumber int // N of M
TotalHunks int TotalHunks int
LineRange string // display text, e.g., "Lines 142148" LineRange string // display text, e.g., "Lines 142148"
@ -182,7 +189,7 @@ Used for: conflict resolution UI. The logic layer produces one `MergeHunk` eleme
```go ```go
type StatusBar struct { type StatusBar struct {
Element /* region, visible, id — unexported */
Left string // e.g., "Ln 10, Col 5" Left string // e.g., "Ln 10, Col 5"
Right string // e.g., "1024 / 50000 bytes" Right string // e.g., "1024 / 50000 bytes"
} }
@ -192,7 +199,7 @@ type StatusBar struct {
```go ```go
type Toast struct { type Toast struct {
Element /* region, visible, id — unexported */
Text string Text string
Timeout time.Duration // auto-dismiss after this duration Timeout time.Duration // auto-dismiss after this duration
} }
@ -204,7 +211,7 @@ Used for: "undo skipped (text changed)", "file saved", "conflict detected".
```go ```go
type Spacer struct { type Spacer struct {
Element /* region, visible, id — unexported */
// Region.H defines the spacer height // 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 ## 7. Theme
Minimal theming via a `Theme` struct passed to the layout pass: Minimal theming via a `Theme` struct passed to the renderer:
```go ```go
type Theme struct { type Theme struct {
ScreenWidth unit.Dp
ScreenHeight unit.Dp
FontSize unit.Sp 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
} }
``` ```
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 ## 8. Page Compositions
@ -335,32 +333,35 @@ Elements that don't specify explicit colors/sizes use theme defaults. The theme
## 9. Testing ## 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 ```go
func TestBrowserFiltersBySearch(t *testing.T) { func TestBrowserFiltersBySearch(t *testing.T) {
state := BrowserState{Entries: allFiles, Search: "foo"} 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, 3, len(list.Items))
assert.Equal(t, "foo.txt", list.Items[0].Text) assert.Equal(t, "foo.txt", list.Items[0].Text)
} }
func TestEditorCursorPosition(t *testing.T) { func TestEditorCursorPosition(t *testing.T) {
state := EditorState{Cursor: 1024, Buffer: buffer} 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, 42, cursor.Line)
assert.Equal(t, 10, cursor.Column) assert.Equal(t, 10, cursor.Column)
} }
func TestMergeHunkDisplay(t *testing.T) { func TestMergeHunkDisplay(t *testing.T) {
state := MergeState{CurrentHunk: 2, Hunks: hunks} 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, 3, hunk.HunkNumber)
assert.Equal(t, 7, hunk.TotalHunks) assert.Equal(t, 7, hunk.TotalHunks)
assert.Equal(t, Unresolved, hunk.Resolution) assert.Equal(t, Unresolved, hunk.Resolution)