Pad/doc/element_model.md
Greg Pomerantz 6720359360 Add Gioui scaffolding with Label and ListView rendering
- Element interface with Region() and Visible() methods
- Label and ListView element types with constructors
- Renderer that clips and draws elements in slice order
- Main app loop with Gioui window and frame events

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-05-07 17:45:56 -04:00

10 KiB
Raw Blame History

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.

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

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

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

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

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

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

type MergeHunk struct {
    Element
    HunkNumber   int       // N of M
    TotalHunks   int
    LineRange    string    // display text, e.g., "Lines 142148"
    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

type StatusBar struct {
    Element
    Left   string  // e.g., "Ln 10, Col 5"
    Right  string  // e.g., "1024 / 50000 bytes"
}

3.9 Toast / Notification

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

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:

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:

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 142148)"},
    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:

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