# 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 The project uses distinct Go types to prevent accidental mixing of coordinate units at compile time: | Unit | Go Type | Use | |---|---|---| | **Dp** (device-independent pixels) | `ui.Dp` (alias of `unit.Dp`) | Element positions, sizes, spacing in the logic/layout layer | | **Px** (physical pixels) | `ui.Px` (alias of `int`) | Gio interop only (`gtx.Constraints`, `gtx.Dp()`, etc.) | | **Sp** (scaled pixels) | `unit.Sp` (`float32`) | Font sizes (respects user text scaling preference; Gio's shaper requires `unit.Sp`) | ### Type Safety `ui.Dp` and `ui.Px` are distinct named types. The compiler prevents accidental mixing: ```go var pos ui.Dp = ui.Dp(100) var winW ui.Px = ui.Px(780) // Compile error: invalid operation: pos + winW (mismatched types ui.Dp and ui.Px) // Must use explicit conversion: converted := ui.ToPx(pos, scale) // Dp → Px ``` ### Conversion Functions ```go // Convert Dp to Px using the scale factor (pixels per DP) func ToPx(dp Dp, scale float32) Px // Convert Px to Dp using the scale factor func ToDp(px Px, scale float32) float32 ``` ### Architecture - **Logic/Layout layer**: Works exclusively in `Dp`. Element regions, positions, and sizes are all in `Dp`. - **Renderer**: Converts `Dp` → `Px` at Gio interop boundaries using `gtx.Metric.PxPerDp`. - **main.go**: Converts `app.ConfigEvent` pixel dimensions to `Dp` before passing to the logic layer. The renderer captures `gtx.Constraints` once at the start of `Draw()` (before clips modify them) and passes this snapshot to all render functions for consistent positioning. ### 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 type Region struct { X, Y unit.Dp W, H unit.Dp } 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 ``` 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.1 Static Text ```go type Label struct { /* region, visible, id — unexported */ 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 { /* region, visible, id — unexported */ 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 WordWrap bool // true = wrap long lines visually (default) WrapWidth unit.Dp // width at which wrapping occurs (auto if 0) } 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. **Word wrap**: when `WordWrap` is true, long lines are broken visually at word boundaries within the element's region width. This is a display-only feature — no newlines are inserted into the underlying text. The logic layer computes wrapped display lines from the raw text, region width, and estimated character advance. Users can toggle word wrap off (horizontal scroll instead). ### 3.3 Search Bar ```go type SearchBar struct { /* region, visible, id — unexported */ Query string // current search text Match int // current match index (0-based) Total int // total number of matches Forward bool // true = last search was forward, false = backward } ``` Used for: in-editor text search. Appears as a narrow bar below the header. Contains a text field and up/down arrows. The logic layer: 1. Finds all occurrences of `Query` in the visible buffer (or full file for small files) 2. Sets `Match` to the current position in the match list 3. Moves the editor cursor to the matched text on each keystroke or arrow tap 4. Up arrow = previous match, down arrow = next match ### 3.4 Cursor ```go type Cursor struct { /* region, visible, id — unexported */ 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.5 Lists ```go type ListView struct { /* region, visible, id — unexported */ Items []ListItem ScrollOffset int // index of the first visible item Selected int // index of selected item (-1 = none) } 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.6 Alphabet Index ```go type AlphaIndex struct { /* region, visible, id — unexported */ Letters []string // visible letters (e.g., ["A", "B", "C", ...]) ActiveLetter string // currently pressed letter (for highlighting) } ``` Used for: quick navigation in the directory browser. ### 3.7 Buttons ```go type Button struct { /* region, visible, id — unexported */ Text string Enabled bool Primary bool // true = emphasized style (e.g., filled background) } ``` Used for: merge resolution (ours/theirs/both), dismiss, apply. ### 3.8 Merge Hunk ```go type MergeHunk struct { /* region, visible, id — unexported */ 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.9 Status Bar (Top Bar) ```go type StatusBar struct { /* region, visible, id — unexported */ Filename string // e.g., "notes.txt" (truncated with ellipsis if too long) FilenameExp bool // true = show full filename (multi-line), false = truncated CutCopy bool // true = show cut icon in left slot Copy bool // true = show copy icon in left slot (next to cut) Paste bool // true = show paste icon in left slot (next to copy) ConflictIcon bool // true = show conflict warning icon (right side) Search bool // true = show search icon (right side, next to conflict) } ``` The status bar appears at the top of the editor page. It has a **two-line layout**: - **Line 1**: Filename (truncated with ellipsis if too long). Tapping the ellipsis toggles between truncated and full multi-line view. - **Line 2**: Action icons (Cut, Copy, Paste) on the left + conflict icon + search icon on the right. **Fixed icon slots** (24×24 DP each, fixed positions): - **Cut icon**: leftmost action icon - **Copy icon**: second from left (36 DP from Cut icon) - **Paste icon**: third from left (36 DP from Copy icon) - **Conflict icon**: right side (36 DP from Search icon) - **Search icon**: rightmost (36 DP from Conflict icon) Icons appear/disappear without reflowing other elements. The StatusBar's `Region.H` is computed dynamically based on whether the filename is truncated (2 lines) or expanded (3+ lines). When a sync conflict is detected for the active file, `ConflictIcon` is set to true. Tapping the icon navigates to the merge resolution page. The icon persists across process death — if a conflict file exists for the active file on restore, the icon appears. ### 3.10 Bottom Bar ```go type BottomBar struct { /* region, visible, id — unexported */ CursorPos string // e.g., "Ln 10, Col 5" BytePos string // e.g., "1024 / 50000 bytes" WordWrap bool // true = word wrap is enabled (button: tap to toggle) } ``` The bottom bar appears at the bottom of the editor page. It displays cursor position, byte position, and word wrap status. It is always visible. **Layout**: ``` ┌──────────────────────────────────────────────────────────────┐ │ Ln 10, Col 5 1024 / 50000 [Word Wrap: On] │ └──────────────────────────────────────────────────────────────┘ ``` - **Left**: Cursor position (line number, column number) - **Center**: Byte position (current byte / total file bytes) - **Right**: Word wrap button (tap to toggle On/Off) The BottomBar's `Region.H` is fixed (24 DP). It sits below the editor content. ### 3.11 Toast / Notification ```go type Toast struct { /* region, visible, id — unexported */ Text string Timeout time.Duration // auto-dismiss after this duration } ``` Used for: "undo skipped (text changed)", "file saved", "conflict detected". ### 3.11 Spacer ```go type Spacer struct { /* region, visible, id — unexported */ // 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. Status bar (top) 3. Search bar or main content 4. List or text area 5. Overlay elements (cursor, selection highlight) 6. Bottom bar ``` ## 7. Theme Minimal theming via a `Theme` struct passed to the renderer: ```go type Theme struct { 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. 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.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{ StatusBar{ Filename: "notes.txt", FilenameExp: false, CutCopy: true, Copy: false, Paste: true, ConflictIcon: false, Search: false, }, SearchBar{Query: "foo", Match: 1, Total: 3}, // search (when active) TextField{Multiline: true, VisibleLines: [...], ScrollOffset: 420, WordWrap: true}, Cursor{Line: 5, Column: 12, Selection: &Selection{...}}, BottomBar{CursorPos: "Ln 47, Col 12", BytePos: "1024 / 50000", WordWrap: true}, } ``` The search bar is only present when the user has activated search. When visible, it sits between the StatusBar and the text area. Typing in the search bar moves the cursor to the current match. Up/down arrows cycle through matches. The StatusBar contains: - **Filename** on line 1 (truncated with ellipsis if too long) - **Action icons** (Cut, Copy, Paste) on line 2, left side - **Conflict icon** and **Search icon** on line 2, right side The BottomBar contains: - **Cursor position** on the left (e.g., "Ln 47, Col 12") - **Byte position** in the center (e.g., "1024 / 50000") - **Word wrap button** on the right (tap to toggle On/Off) ### 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 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, screen) 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, screen) 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, screen) 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) } ``` 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