# Element Model Specification ## 1. Overview Pad's logic layer produces a slice of positioned element structs. The renderer consumes the slice, draws each element, and routes input events back to the logic layer. Elements are plain Go structs — no framework types, no interfaces required for testing. ``` Logic: (State, Event) → ([]Element, Commands) Render: []Element → pixels Test: assert on []Element directly ``` ## 2. Core Types ### 2.1 Coordinate System Consistent with Gioui and standard 2D graphics: - **Origin (0, 0)**: top-left corner of the application surface - **Y-axis**: increases downward - **X-axis**: increases rightward - **Screen orientation**: the application surface rotates with the device (handled by the OS / Gioui surface). The logic layer receives the current surface dimensions and recomputes layouts accordingly. ### 2.2 Units Consistent with Gioui's `unit` package: | Unit | Go Type | Use | |---|---|---| | **Dp** (device-independent pixels) | `unit.Dp` (`float32`) | Element positions, sizes, spacing | | **Sp** (scaled pixels) | `unit.Sp` (`float32`) | Font sizes (respects user text scaling preference) | | **Px** (raw device pixels) | `int` | Never used in the logic layer; the renderer converts Dp→Px using `Metric.PxPerDp` | 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. ```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 } ``` ``` Each concrete element embeds `Element` and adds its own fields. ## 3. Element Catalog ### 3.1 Static Text ```go type Label struct { Element Text string Align TextAlign // start, center, end FontSize unit.Sp // 0 = theme default Color Color // 0 = theme default Bold bool } type TextAlign int // Start, Center, End ``` Used for: headers, status bar text, file sizes, line numbers. ### 3.2 Interactive Text ```go type TextField struct { Element Value string Placeholder string Focused bool // true = show cursor, accept keyboard input Multiline bool // true = full-height text area (editor) ScrollOffset unit.Dp // vertical scroll position (Dp) VisibleLines []Line // for multiline: the lines to render } type Line struct { Text string LineNumber int // 1-indexed, for display } ``` Used for: search bar (`Multiline: false`), editor buffer (`Multiline: true`). The editor's `TextField` only contains lines visible in the current viewport. The logic layer determines which lines to include based on scroll offset and viewport height. ### 3.3 Cursor ```go type Cursor struct { Element // region is the cursor rectangle within the text field Line int // line number (0-indexed into VisibleLines) Column int // character offset within the line Blinking bool // current blink state Selection *Selection } type Selection struct { StartLine, StartCol int EndLine, EndCol int } ``` Rendered as an overlay inside a `TextField`. The logic layer computes cursor position from byte offset. ### 3.4 Lists ```go type ListView struct { Element 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 { Text string Subtext string // optional secondary text (e.g., file size, date) Selected bool // highlighted } ``` Used for: directory browser. Only contains items for the current viewport. ### 3.5 Alphabet Index ```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 } ``` Used for: quick navigation in the directory browser. ### 3.6 Buttons ```go type Button struct { Element Text string Enabled bool Primary bool // true = emphasized style (e.g., filled background) OnPress string // callback ID } ``` Used for: merge resolution (ours/theirs/both), dismiss, apply. ### 3.7 Merge Hunk ```go type MergeHunk struct { Element HunkNumber int // N of M TotalHunks int LineRange string // display text, e.g., "Lines 142–148" ContextLines []string // unchanged lines (shared) OurLines []string // our version of the changed region TheirLines []string // their version of the changed region Resolution HunkResolution } type HunkResolution int // Unresolved, KeepOurs, KeepTheirs, MergeBoth ``` Used for: conflict resolution UI. The logic layer produces one `MergeHunk` element plus navigation buttons. ### 3.8 Status Bar ```go type StatusBar struct { Element Left string // e.g., "Ln 10, Col 5" Right string // e.g., "1024 / 50000 bytes" } ``` ### 3.9 Toast / Notification ```go type Toast struct { Element Text string Timeout time.Duration // auto-dismiss after this duration } ``` Used for: "undo skipped (text changed)", "file saved", "conflict detected". ### 3.10 Spacer ```go type Spacer struct { Element // Region.H defines the spacer height } ``` ## 4. Layout Model The logic layer performs a layout pass before emitting elements. Each element's `Region` is already computed. The renderer draws elements in slice order (back-to-front). ### Layout Pass ``` 1. Given: screen width, screen height, state 2. Compute: region for each element 3. Emit: []Element with filled Region fields ``` The layout pass is pure: `(State, ScreenSize) → []Element`. It is testable. Example for the browser page (values in Dp): ``` Screen: 390 × 844 Dp Label (header) → Region{X:0, Y:0, W:390, H:48} TextField (search) → Region{X:8, Y:48, W:330, H:40} AlphaIndex → Region{X:354, Y:48, W:28, H:716} ListView → Region{X:8, Y:96, W:346, H:716} ``` The logic layer knows the screen dimensions (from the renderer on init) and computes regions accordingly. ### Scrolling Scrollable elements (ListView, TextField multiline) contain only the visible items. The logic layer: 1. Tracks scroll offset (pixels or item index) 2. Computes which items are visible given the offset and viewport height 3. Emits only those items 4. Updates `ScrollOffset` field for the renderer to maintain scroll position Scroll events from the renderer are routed back to the logic layer, which updates the offset and re-renders. ## 5. Input Routing Interactive elements have an `ID` field. The renderer reports input events as: ```go type InputEvent struct { ElementID string Type InputType // Tap, DoubleTap, LongPress, Scroll, KeyDown, KeyUp Data any // type-specific payload (key code, scroll delta, etc.) } ``` The logic layer routes `InputEvent` to the appropriate handler based on `ElementID`. ## 6. Rendering Order Elements are rendered in slice order. Later elements draw on top of earlier ones. Typical order: ``` 1. Background (full screen) 2. Header / Label 3. Search bar or main content 4. List or text area 5. Overlay elements (cursor, selection highlight, toast) 6. Status bar (bottom) ``` ## 7. Theme Minimal theming via a `Theme` struct passed to the layout pass: ```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. ## 8. Page Compositions ### 8.1 Browser Page ``` []Element{ Label{Text: "My Documents"}, // header TextField{Placeholder: "Search..."}, // search bar ListView{Items: [...], ScrollOffset: 0}, // file list AlphaIndex{Letters: ["A"..."Z"]}, // right sidebar } ``` ### 8.2 Editor Page ``` []Element{ Label{Text: "notes.txt"}, // header (filename) TextField{Multiline: true, VisibleLines: [...], ScrollOffset: 420}, Cursor{Line: 5, Column: 12}, StatusBar{Left: "Ln 47, Col 12", Right: "1024 / 50000"}, } ``` ### 8.3 Merge Page ``` []Element{ Label{Text: "notes.txt — Conflict"}, Label{Text: "Hunk 3 of 7 (Lines 142–148)"}, MergeHunk{ContextLines: [...], OurLines: [...], TheirLines: [...]}, Button{Text: "Ours", OnPress: "resolve_ours"}, Button{Text: "Theirs", OnPress: "resolve_theirs"}, Button{Text: "Both", OnPress: "resolve_both"}, Button{Text: "Next", Primary: true, OnPress: "next_hunk"}, } ``` ## 9. Testing Tests import the logic layer, call render functions, and assert on the resulting `[]Element`: ```go func TestBrowserFiltersBySearch(t *testing.T) { state := BrowserState{Entries: allFiles, Search: "foo"} elems := BrowserLayout(state, theme) list := elems[2].(ListView) 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) cursor := elems[1].(Cursor) 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) hunk := elems[2].(MergeHunk) assert.Equal(t, 3, hunk.HunkNumber) assert.Equal(t, 7, hunk.TotalHunks) assert.Equal(t, Unresolved, hunk.Resolution) } ``` No Android SDK, no Gioui, no display server. Pure Go tests. ## 10. Out of Scope - Element animations (may be added later) - Element transitions between pages - Right-to-left text - Dynamic font sizing / accessibility scaling - Custom element types beyond the catalog above