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/
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

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.
### 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
/* region, visible, id — unexported */
Letters []string // visible letters (e.g., ["A", "B", "C", ...])
ActiveLetter string // currently pressed letter (for highlighting)
OnTap string // callback ID
}
```
@ -149,11 +157,10 @@ Used for: quick navigation in the directory browser.
```go
type Button struct {
Element
/* region, visible, id — unexported */
Text string
Enabled bool
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
type MergeHunk struct {
Element
/* region, visible, id — unexported */
HunkNumber int // N of M
TotalHunks int
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
type StatusBar struct {
Element
/* 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
}
```
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)