603 lines
23 KiB
Markdown
603 lines
23 KiB
Markdown
# Element Model Specification
|
||
|
||
## 1. Overview
|
||
|
||
Pad's logic layer produces a slice of positioned elements. Each element knows how to draw itself: the `Element` interface includes a `Draw` method that delegates low-level rendering to a `*Renderer`. Elements are Go structs with both structural data (text, alignment, etc.) and behavioral code (their `Draw` implementation).
|
||
|
||
```
|
||
Logic: (State, Event) → []Element
|
||
Draw: Element.Draw(gtx, r) → r.drawText(), r.drawBg(), ...
|
||
Test: assert on []Element directly (no renderer needed)
|
||
```
|
||
|
||
The Renderer provides low-level primitives (`drawText`, `drawBg`, `drawPng`) that elements call during their `Draw` method. It handles Dp→Px conversion and Gio shaper interop. The Renderer's `Draw(gtx, elems)` iterates the slice and calls each element's `Draw` method.
|
||
|
||
## 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 holds a `ScaleProvider` interface (backed by `*editor.State`) that provides the current scale factor. It converts Dp↔Px on demand via `r.toPx()` / `r.toDp()`. The main window clip is applied at the top of `Renderer.Draw()` to bound all drawing to the window area.
|
||
|
||
### 2.3 Element Interface
|
||
|
||
All UI elements implement the `Element` interface. Elements know their region and visibility, and **each element draws itself** by delegating to the `*Renderer`:
|
||
|
||
```go
|
||
type Element interface {
|
||
Region() Region
|
||
Visible() bool
|
||
Draw(gtx layout.Context, r *Renderer)
|
||
}
|
||
```
|
||
|
||
Concrete elements are Go structs with unexported fields for `region`, `visible`, and `id`. Constructors (`NewLabel`, `NewContainer`, `NewIcon`, etc.) set these fields, keeping the API clean and preventing external mutation.
|
||
|
||
```go
|
||
type Region struct {
|
||
X, Y Dp // ui.Dp, not unit.Dp
|
||
W, H Dp
|
||
}
|
||
|
||
type Label struct { /* unexported region, visible, id + exported Text, Align, ... */ }
|
||
func NewLabel(text string, fontSize unit.Sp, region Region, align TextAlign) Label
|
||
func (l Label) Region() Region
|
||
func (l Label) Visible() bool
|
||
func (l Label) Draw(gtx layout.Context, r *Renderer) { r.drawText(...) }
|
||
func (l Label) ID() string
|
||
```
|
||
|
||
The Renderer's `Draw(gtx, elems)` iterates the element slice, skips invisible elements, and calls each element's `Draw` method. `Container` elements additionally clip to their bounds and offset their children. 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.0 Container
|
||
|
||
Composite element that holds child elements within a clipped region with an optional background. Children's regions are relative to the Container's origin.
|
||
|
||
```go
|
||
type Container struct {
|
||
/* region, visible, id — unexported */
|
||
background Color
|
||
children []Element
|
||
}
|
||
func NewContainer(region Region, bg Color, children []Element) Container
|
||
func (c Container) Draw(gtx layout.Context, r *Renderer)
|
||
```
|
||
|
||
`Container.Draw()` draws its background (if any). The Renderer's `drawElement` handles clipping to the container's bounds and offsetting children before recursing into them. Children are drawn in slice order.
|
||
|
||
Used for: StatusBar, BottomBar, and any grouped UI region with a shared background and containment clipping.
|
||
|
||
### 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.1b Icon
|
||
|
||
Displays a named icon image (loaded from embedded PNGs). The icon **automatically scales to fill its region** when `Size` is 0, using the region's `W` and `H` as the target dimensions.
|
||
|
||
```go
|
||
type Icon struct {
|
||
/* region, visible, id — unexported */
|
||
Name string // icon name, e.g. "cut", "copy", "paste"
|
||
Size Dp // 0 = auto-scale to region W×H
|
||
}
|
||
func NewIcon(name string, region Region, size Dp) Icon
|
||
func (i Icon) Draw(gtx layout.Context, r *Renderer)
|
||
```
|
||
|
||
Icons are loaded from the embedded filesystem (`icons/*.png`). The `Renderer` caches them in a map and renders via `r.drawPng()`. When `Size` is 0 (the default), `Icon.Draw` passes `region.W` and `region.H` as the target dimensions, and `r.drawPng()` applies an affine scale transform so the source image fills that space. The `Size` field can be set to a non-zero value to override the region-based sizing.
|
||
|
||
Used for: action icons in the StatusBar (cut, copy, paste).
|
||
|
||
### 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)
|
||
}
|
||
func NewButton(text string, enabled bool, primary bool, region Region) Button
|
||
func (b Button) Draw(gtx layout.Context, r *Renderer)
|
||
```
|
||
|
||
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) — Composition
|
||
|
||
The status bar is **not a standalone element type**. It is composed from `Container`, `Label`, and `Icon` elements:
|
||
|
||
```go
|
||
// EditorLayout builds the status bar as:
|
||
Container{
|
||
region: Region{X: margin, Y: margin, W: screenWidth - margin*2, H: 52},
|
||
background: Color{R: 230, G: 230, B: 230, A: 255},
|
||
children: []Element{
|
||
// Row 1: filename
|
||
NewLabel("filename.txt", 14, Region{X: 0, Y: 2, W: statusBarW, H: 20}, AlignStart),
|
||
// Row 2: cut, copy, paste icons (Size=0 → auto-scale to region W×H)
|
||
NewIcon("cut", Region{X: 0, Y: 28, W: 24, H: 24}, 0),
|
||
NewIcon("copy", Region{X: 48, Y: 28, W: 24, H: 24}, 0),
|
||
NewIcon("paste", Region{X: 96, Y: 28, W: 24, H: 24}, 0),
|
||
},
|
||
}
|
||
```
|
||
|
||
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. Icons are `24×24` DP each with `Size=0`, so they auto-scale to fill their regions.
|
||
|
||
The Container clips its children to its bounds and provides a shared background. Children use regions relative to the Container's origin — the Renderer applies `op.Offset` automatically when drawing Container children.
|
||
|
||
When a sync conflict is detected for the active file, a conflict `Icon` appears. Tapping it navigates to the merge resolution page.
|
||
|
||
### 3.10 Bottom Bar — Composition
|
||
|
||
The bottom bar is **not a standalone element type**. It is composed from `Container` and `Label` elements:
|
||
|
||
```go
|
||
// EditorLayout builds the bottom bar as:
|
||
Container{
|
||
region: Region{X: margin, Y: screenHeight - margin - 24, W: screenWidth - margin*2, H: 24},
|
||
background: Color{R: 230, G: 230, B: 230, A: 255},
|
||
children: []Element{
|
||
NewLabel("Ln 47, Col 12", 12, Region{X: 0, Y: 2, W: barW, H: 20}, AlignStart),
|
||
NewLabel("1024 / 50000", 12, Region{X: 0, Y: 2, W: barW, H: 20}, AlignCenter),
|
||
NewLabel("Wrap: On", 12, Region{X: 0, Y: 2, W: barW, H: 20}, AlignEnd),
|
||
},
|
||
}
|
||
```
|
||
|
||
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 with a fixed height of 24 DP.
|
||
|
||
**Layout**:
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ Ln 10, Col 5 1024 / 50000 Wrap: On │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
- **Left**: Cursor position (line number, column number)
|
||
- **Center**: Byte position (current byte / total file bytes)
|
||
- **Right**: Word wrap status (tap to toggle On/Off)
|
||
|
||
### 3.11 Toast / Notification
|
||
|
||
```go
|
||
type Toast struct {
|
||
/* region, visible, id — unexported */
|
||
Text string
|
||
Timeout int // auto-dismiss after this many milliseconds
|
||
}
|
||
func NewToast(region Region, text string, timeout int) Toast
|
||
```
|
||
|
||
Used for: "undo skipped (text changed)", "file saved", "conflict detected".
|
||
|
||
### 3.12 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. Elements draw themselves: the Renderer iterates the slice and calls `Element.Draw(gtx, r)` for each visible element. `Container` elements additionally clip to their bounds and offset their children.
|
||
|
||
### 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: `(ScreenSize) → []Element`. It is testable.
|
||
|
||
Currently implemented: `EditorLayout(screenWidth, screenHeight ui.Dp) []ui.Element` in `internal/editor/state.go`. It produces:
|
||
|
||
```
|
||
Screen: 390 × 844 Dp
|
||
|
||
Container (StatusBar) → Region{X:10, Y:10, W:370, H:52}
|
||
Container (BottomBar) → Region{X:10, Y:810, W:370, H:24}
|
||
```
|
||
|
||
The logic layer knows the screen dimensions (converted from raw pixels to Dp in `main.go`) 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. Interaction Model
|
||
|
||
Elements declare their input behaviors at construction time via the `Interactive` interface. Each element can register one or more `Interaction`s, pairing a gesture type with a handler function.
|
||
|
||
### 5.1 Core Types
|
||
|
||
```go
|
||
type InputType int
|
||
|
||
const (
|
||
Tap InputType = iota
|
||
DoubleTap
|
||
)
|
||
|
||
// Interaction pairs a gesture type with a handler function.
|
||
type Interaction struct {
|
||
Gesture InputType
|
||
Handler func(any) // called with raw Gio gesture event data
|
||
}
|
||
|
||
// Interactive is implemented by elements that respond to input events.
|
||
type Interactive interface {
|
||
ID() string
|
||
Interactions() []Interaction
|
||
}
|
||
|
||
// InputEvent carries a handler and its raw event data.
|
||
type InputEvent struct {
|
||
Handler func(any)
|
||
Data any
|
||
}
|
||
```
|
||
|
||
### 5.2 Declaring Interactions
|
||
|
||
Elements declare interactions when constructed:
|
||
|
||
```go
|
||
// Wrap label: tap toggles word wrap
|
||
wrapLabel := NewLabel("Wrap: On", 12, region, AlignEnd, "wrap",
|
||
[]Interaction{{Gesture: Tap, Handler: ToggleWordWrap}})
|
||
```
|
||
|
||
Every interactive element type (`Label`, `Icon`, `Button`, `TextField`, `ListView`, `AlphaIndex`, `MergeHunk`, `SearchBar`, `Cursor`, `Toast`, `Spacer`) has an `interactions` field and implements `Interactions() []Interaction`.
|
||
|
||
### 5.3 Click Area Clipping
|
||
|
||
The click area is **not** the element's full region. It is clipped to the actual painted content:
|
||
|
||
- **Text elements** (`Label`, `Button`): The click area is the bounding box of the shaped text glyphs. For `AlignEnd` labels (e.g., "Wrap: On"), only the text area at the right edge is clickable, not the full container width.
|
||
|
||
- **Icon elements**: The click area is the icon image's bounding box.
|
||
|
||
- **Text fields and lists**: The click area covers the element's full region (the entire text area or list viewport).
|
||
|
||
This clipping is implemented in `Renderer.drawText()`: after shaping the text, a `clip.Rect` is pushed around the text's bounding box, then `gesture.Click.Add()` is called inside that clip. Only paint ops within the clip contribute to the click area.
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────┐
|
||
│ Ln 10, Col 5 1024 / 50000 [Wrap: On] │
|
||
│ ──── click area ──── ──── click area ──── click area │
|
||
│ (full width) (full width) (text only) │
|
||
└──────────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
### 5.4 Gesture Registration and Event Routing
|
||
|
||
Click gesture registration and event routing follow this flow:
|
||
|
||
1. **Registration**: In `drawElement`, the renderer checks if the element implements `Interactive`. For each `Interaction`, it stores the `*gesture.Click` and handler in `clicks map[string]clickReg`.
|
||
2. **Click area**: During `drawText` (or `drawPng` for icons), `click.Add(gtx.Ops)` is called inside the content's clip, associating the gesture with the painted area.
|
||
3. **Polling**: After `renderer.Draw()`, `renderer.CheckGestures()` polls all registered `gesture.Click` instances via `click.Update(q)`.
|
||
4. **Routing**: Click events are returned as `InputEvent{Handler, Data}`. The main loop sends them to the logic goroutine via `inputChan`.
|
||
5. **Execution**: The logic goroutine calls `evt.Handler(evt.Data)`. Handlers are static functions (e.g., `ToggleWordWrap`) that read/write global `TheState` directly, avoiding closures and circular imports.
|
||
|
||
### 5.5 Handler Design
|
||
|
||
Handlers are static functions defined in the `editor` package:
|
||
|
||
```go
|
||
func ToggleWordWrap(data any) {
|
||
state := editor.TheState.Load()
|
||
state.Lock()
|
||
state.WordWrap = !state.WordWrap
|
||
state.Unlock()
|
||
}
|
||
```
|
||
|
||
They receive the raw Gio gesture event (`gesture.ClickEvent`) as `any`. They access global `TheState` directly, avoiding the need for closures or element-ID dispatch tables.
|
||
|
||
## 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: Container with Labels and Icons
|
||
Container{
|
||
background: gray,
|
||
children: []Element{
|
||
NewLabel("notes.txt", 14, ..., AlignStart), // filename
|
||
NewIcon("cut", Region{X:0, Y:28, W:24, H:24}, 0), // cut (auto-scale)
|
||
NewIcon("copy", Region{X:48, Y:28, W:24, H:24}, 0), // copy (auto-scale)
|
||
NewIcon("paste", Region{X:96, Y:28, W:24, H:24}, 0), // paste (auto-scale)
|
||
},
|
||
},
|
||
// (future) TextField{Multiline: true, ...},
|
||
// (future) Cursor{Line: 5, Column: 12, Selection: &Selection{...}},
|
||
// BottomBar: Container with Labels
|
||
Container{
|
||
background: gray,
|
||
children: []Element{
|
||
NewLabel("Ln 47, Col 12", 12, ..., AlignStart),
|
||
NewLabel("1024 / 50000", 12, ..., AlignCenter),
|
||
NewLabel("Wrap: On", 12, ..., AlignEnd),
|
||
},
|
||
},
|
||
}
|
||
```
|
||
|
||
StatusBar and BottomBar are compositions of `Container`, `Label`, and `Icon` — not standalone element types. The `EditorLayout` function in `internal/editor/state.go` builds these compositions. Icon elements use `Size=0` so they auto-scale to fill their `24×24` DP regions.
|
||
|
||
Future pages will add `TextField` (editor content), `SearchBar`, `Cursor`, and `Button` elements.
|
||
|
||
### 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 TestEditorLayout(t *testing.T) {
|
||
elems := EditorLayout(ui.Dp(390), ui.Dp(844))
|
||
|
||
// StatusBar is a Container
|
||
status, ok := elems[0].(Container)
|
||
assert.True(t, ok)
|
||
assert.Equal(t, ui.Dp(52), status.Region().H)
|
||
assert.Equal(t, 3, len(status.children))
|
||
|
||
// BottomBar is a Container
|
||
bottom, ok := elems[1].(Container)
|
||
assert.True(t, ok)
|
||
assert.Equal(t, ui.Dp(24), bottom.Region().H)
|
||
}
|
||
```
|
||
|
||
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
|