From ae04aabb70cd311c32cc8665c39e3d41f01a0405 Mon Sep 17 00:00:00 2001 From: Greg Pomerantz Date: Tue, 26 May 2026 10:00:14 -0400 Subject: [PATCH] Documentation update. --- doc/element_model.md | 88 +++++++++++++++++++++++++++++++++++++++++--- doc/touch.md | 52 ++++++++++++++------------ 2 files changed, 110 insertions(+), 30 deletions(-) diff --git a/doc/element_model.md b/doc/element_model.md index 16c5874..c3faaa8 100644 --- a/doc/element_model.md +++ b/doc/element_model.md @@ -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 diff --git a/doc/touch.md b/doc/touch.md index b997ac6..c8fce2d 100644 --- a/doc/touch.md +++ b/doc/touch.md @@ -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.