Documentation update.

This commit is contained in:
Greg Pomerantz 2026-05-26 10:00:14 -04:00
parent 1596dab72d
commit ae04aabb70
2 changed files with 110 additions and 30 deletions

View File

@ -396,19 +396,95 @@ Scrollable elements (ListView, TextField multiline) contain only the visible ite
Scroll events from the renderer are routed back to the logic layer, which updates the offset and re-renders.
## 5. Input Routing
## 5. Interaction Model
Interactive elements have an `ID` field. The renderer reports input events as:
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 {
ElementID string
Type InputType // Tap, DoubleTap, LongPress, Scroll, KeyDown, KeyUp
Data any // type-specific payload (key code, scroll delta, etc.)
Handler func(any)
Data any
}
```
The logic layer routes `InputEvent` to the appropriate handler based on `ElementID`.
### 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

View File

@ -6,9 +6,10 @@ This document specifies how Pad handles low-level pointer and keyboard events us
Pad avoids high-level widgets and uses the following low-level Gioui ops and events. Ops are submitted in the paint function; Events are returned by `w.Event()`.
| Op (submitted in paint) | Event (returned by w.Event()) | Use Case |
| Op (submitted in paint) | Event / Gesture | Use Case |
|---|---|---|
| `pointer.InputOp` | `pointer.Event` | Defines hit-regions (editor, buttons, search bar). Captures `Press`, `Release`, `Move`, `Drag`, `Scroll`. |
| `gesture.Click.Add` | `gesture.ClickEvent` | Tap detection for UI elements (buttons, status bar labels, icons). Click area is clipped to painted content. |
| `pointer.InputOp` | `pointer.Event` | Editor hit-region. Captures `Press`, `Release`, `Move`, `Drag`, `Scroll` for cursor movement, selection, scrolling. |
| `key.InputOp` | `key.Event` | Enables keyboard focus. Captures `Edit` (text insertion), `Press` (Backspace, Enter, Arrows). |
| `key.FocusOp` | — | Requests keyboard focus for the active element. |
| `key.SoftKeyboardOp` | — | Explicitly shows/hides the Android soft keyboard. |
@ -17,11 +18,11 @@ Pad avoids high-level widgets and uses the following low-level Gioui ops and eve
Because Logic does not have access to `*op.Ops`, the Op/Event flow is split:
1. **Logic → Ops**: Logic computes `[]Element`. Each element carries metadata about what input ops it needs (e.g., "this TextField needs `pointer.InputOp` with `Kind: Tap` on region X").
2. **Renderer → Ops**: During the paint function, the Renderer iterates `[]Element` and submits the appropriate `pointer.InputOp` / `key.InputOp` calls into `*op.Ops`.
3. **Gioui → Events**: Gioui returns `pointer.Event` / `key.Event` via `w.Event()` in subsequent cycles.
4. **Main → Logic**: The Main goroutine batches these events into `[]InputEvent` and sends them to Logic.
5. **Logic → State**: Logic processes events, updates state, and computes a new frame.
1. **Logic → Elements**: Logic computes `[]Element`. Interactive elements declare `Interaction{Gesture, Handler}` entries.
2. **Renderer → Ops**: During `drawElement`, the renderer registers interactions (creates `gesture.Click` instances) and submits ops into `*op.Ops`. For text elements, `click.Add()` is called inside the text's content clip.
3. **Gioui → Events**: Gioui returns events via `w.Event()`. Gesture events are polled from `input.Source` via `click.Update(q)`.
4. **Main → Logic**: The Main goroutine batches gesture events into `[]InputEvent` (each carrying its own handler) and sends them to Logic via `inputChan`.
5. **Logic → State**: Logic calls `evt.Handler(evt.Data)`. Handlers are static functions that access global `TheState` directly.
This ensures that hit-regions are always defined in the same frame where the elements are drawn, and Logic remains testable without the SDK.
@ -33,17 +34,15 @@ The editor operates in three coordinate spaces:
2. **Text Space (Lines/Cols)**: The logical position within the text file, accounting for scroll offset and word wrap.
3. **Byte Space (Offsets)**: The raw byte offset in the UTF-8 file buffer.
### 2.1 Hit-Region Tags
### 2.1 Hit Regions
Gioui `pointer.InputOp` takes a `tag` (an `op.Tag`). When an event fires, `event.Tag` identifies which region was touched.
Each interactive element registers its hit region via `gesture.Click.Add(gtx.Ops)`, called inside the element's content clip. The click area is the bounding box of the painted content (text glyphs, icon image, or full element region), not the element's declared region.
**Tagging Strategy**:
- The Renderer assigns a unique tag per element (e.g., `editor`, `search_bar`, `status_bar`, `alpha_index`, `merge_hunk_accept_ours`).
- When Logic receives a `pointer.Event`, it first checks the tag to determine which element was touched, then applies element-specific coordinate logic.
When Logic receives a click event, the `InputEvent` carries its own handler function. No tag-based dispatch is needed — the handler knows exactly what to do.
### 2.2 Mapping UI → Byte Offset (Editor/TextField)
When a `pointer.Event` occurs at `(Ex, Ey)` on the `editor` tag:
When a `pointer.Event` occurs at `(Ex, Ey)` on the editor hit-region:
1. **Adjust for Scroll**: `Y = Ey + scroll_offset`.
2. **Identify Visual Line**: `visual_line = floor(Y / line_height)`.
@ -380,11 +379,16 @@ The Search button is always visible in the StatusBar (unlike Cut/Copy/Paste whic
### 5.6.6 Input Routing
Cut, Copy, and Paste icons are tagged with unique `op.Tag` values. When the user taps an icon:
Interactive elements declare their behavior at construction time via the `Interactive` interface. Each element registers `Interaction` entries pairing a gesture type (`Tap`, `DoubleTap`) with a handler function.
1. Main goroutine receives `pointer.Event` (Type: `Press`) with the icon's tag.
2. Main goroutine sends `InputEvent{ElementID: "cut"}`, `InputEvent{ElementID: "copy"}`, or `InputEvent{ElementID: "paste"}` to Logic.
3. Logic processes the event and updates state.
**Flow**:
1. **Registration**: `drawElement` registers interactions via `registerInteraction`. For `Tap` gestures, a `*gesture.Click` is created and stored in the renderer's `clicks` map, keyed by element ID. The handler is stored alongside it.
2. **Click area**: `click.Add(gtx.Ops)` is called inside the element's content clip (e.g., the text bounding box for labels, the icon image area for icons). Only the painted content is clickable.
3. **Polling**: After `renderer.Draw()`, `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`, `DoCut`) that access global `TheState` directly.
This design eliminates element-ID dispatch tables — each click event carries its own handler, and the handler knows exactly what to do.
### 5.6.7 Clipboard State Machine
@ -410,13 +414,13 @@ The filename is displayed on a separate line at the top of the StatusBar. When t
1. **Tap on ellipsis**: Toggling between truncated and full views.
2. **Tap elsewhere on filename**: No action (filename text is not interactive).
3. **Ellipsis region**: The ellipsis (`...`) is a separate `Button` element with its own `op.Tag`. Tapping it triggers the toggle.
3. **Ellipsis region**: The ellipsis (`...`) is a separate `Button` element with a `Tap` interaction. Tapping it triggers the toggle.
### 5.7.3 Input Routing
- The ellipsis is tagged with `op.Tag` value `"filename_toggle"`.
- When the user taps the ellipsis, the Main goroutine sends `InputEvent{ElementID: "filename_toggle"}` to Logic.
- Logic toggles the `filename_expanded` state field.
- The ellipsis button declares a `Tap` interaction with handler `ToggleFilename`.
- When the user taps the ellipsis, the handler is called via `evt.Handler(evt.Data)`.
- The handler toggles the `filename_expanded` state field.
- Logic produces a new frame with the updated filename display.
### 5.7.4 State Field
@ -484,9 +488,9 @@ The word wrap button toggles word wrap on/off:
**Interaction**:
1. User taps the word wrap button in the BottomBar.
2. Main goroutine receives `pointer.Event` (Type: `Press`) with the button's tag.
3. Main goroutine sends `InputEvent{ElementID: "word_wrap"}` to Logic.
4. Logic toggles the `word_wrap_enabled` state field.
2. Main goroutine receives a click event from `CheckGestures()`.
3. Main goroutine sends `InputEvent{Handler: ToggleWordWrap, Data: clickEvent}` to Logic.
4. The handler toggles the `WordWrap` field in `TheState`.
5. Logic produces a new frame (BottomBar shows updated status).
The word wrap status is persisted to `.pad/state.json` and restored on app start.