# Touch and Input Handling This document specifies how Pad handles low-level pointer and keyboard events using Gioui primitives, and how these are translated into editor actions (cursor movement, selection, scrolling). ## 1. Gioui Primitives 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 / Gesture | Use Case | |---|---|---| | `gesture.Click.Add` | `gesture.ClickEvent` | Tap detection for UI elements (buttons, status bar labels, icons). | | `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). | | `gtx.Execute(key.FocusCmd` | — | Requests keyboard focus for the active element. | | `gtx.Execute(key.SoftKeyboardCmd)` | — | Explicitly shows/hides the Android soft keyboard. | ### 1.1 Op/Event Flow Because Logic does not have access to `*op.Ops`, the Op/Event flow is split: 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`. Click registration happens within the element's clip context. 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. ## 2. Coordinate Mapping The editor operates in three coordinate spaces: 1. **UI Space (DP)**: The raw (X, Y) coordinates from Gioui events, relative to the window origin. 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 Regions Each interactive element registers its hit region via `gesture.Click.Add(gtx.Ops)`. The click area is determined by the **current clip context** on the operation stack at the time `Add()` is called, not by explicit screen coordinates. **Gio's Clip-Based Model**: Gio doesn't need explicit screen coordinates for click registration. Instead, it uses the clip stack to determine the click area. When you call `clip.Rect{...}.Push(gtx.Ops)`, you add a clipping region. When you call `.Pop()`, you remove it. Everything drawn or registered between Push and Pop is constrained to that region. **The Correct Pattern**: ```go // 1. Set the clip to the element's bounds clipRect := clip.Rect{ Min: image.Point{X: pxMinX, Y: pxMinY}, Max: image.Point{X: pxMaxX, Y: pxMaxY}, }.Push(gtx.Ops) // 2. Register the click area (uses current clip) click.Add(gtx.Ops) // 3. Draw the element (also clipped to same region) drawElement(gtx) // 4. Pop the clip clipRect.Pop() ``` **Why This Works**: The clip stack ensures that: 1. Click registration happens within the same coordinate system as drawing 2. The click area is automatically constrained to the element's visible bounds 3. Nested elements inherit their parent's clip context 4. No manual coordinate conversion is needed **Critical Detail**: The click area matches the **drawn content position**, not the entire clip region. If text is positioned on the right side of a parent region, the click area will be at the text's actual position, not the entire parent clip. 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 hit-region: 1. **Adjust for Scroll**: `Y = Ey + scroll_offset`. 2. **Identify Visual Line**: `visual_line = floor(Y / line_height)`. 3. **Resolve to Logical Line**: If word wrap is enabled, use the cached wrap positions to map `visual_line` → `logical_line`. Otherwise, they are the same. 4. **Identify Column**: - Retrieve text for `logical_line` from the chunked buffer. - Use the font shaper to measure glyph widths until the cumulative width exceeds `Ex`. - Clamp to the line length. 5. **Result**: Convert `(logical_line, col_index)` to a byte offset using the **Line Index**. ### 2.3 Mapping UI → Element (Non-Text Elements) For buttons, search bar, alpha index, merge hunks: - Each element has a `Region()` (rectangle). The Main goroutine checks if `(Ex, Ey)` falls within the region. - For the **Alpha Index**, the Y-coordinate maps to a letter (e.g., `letter = alphabet[floor((Y - alpha_top) / (alpha_height / 26))]`). - For **Merge Hunks**, the tag identifies which hunk button (accept-ours / accept-theirs) was tapped. ## 3. Gestures and State Machine The Logic goroutine maintains a small state machine for the active gesture. The state is transient (does not survive process death) and is reset on gesture completion. ### 3.1 Gesture States | State | Entry Condition | Exit Condition | Action | |---|---|---|---| | `Idle` | Default state | `Press` received | — | | `Pressed` | `Press` at (X, Y) | `Release` or `Drag` | Record press time and position | | `Tapping` | `Release` received (short duration, no move) | Timeout or next `Press` | Move cursor, clear selection | | `DoubleTap` | Second `Tapping` within 300ms | Gesture complete | Select word at (X, Y) | | `LongPress` | `Press` held > 500ms | `Release` | Select word (future: magnifier) | | `Selecting` | `Drag` received | `Release` | Update `selection_end` to current (X, Y) | | `Scrolling` | `Drag` on non-focused area or two-finger drag | `Release` | Update `scroll_offset` | ### 3.2 Gesture Disambiguation The Logic goroutine distinguishes gestures using timing and movement thresholds: - **Tap vs Long Press**: If `Press` → `Release` occurs within 500ms, it's a tap. Otherwise, it's a long press. - **Tap vs Drag**: If the finger moves more than 16 DP between `Press` and `Release`, it's a drag. Otherwise, it's a tap. - **Single vs Double Tap**: If two taps occur within 300ms, the second tap triggers a word selection. - **Select vs Scroll**: If the keyboard is visible (focused `TextField`), a single-finger drag is a selection. If the keyboard is hidden, a single-finger drag is a scroll. Two-finger drags are always scrolls. ### 3.3 Selection Logic Selection is defined by two byte offsets: `selection_start` and `selection_end`. - If `selection_start == selection_end`, there is no selection (just a cursor). - During a **Drag** gesture: - On `Press`: Set `selection_start = selection_end = offset_at(X, Y)`. - On `Drag`: Update `selection_end = offset_at(X, Y)`. - The UI renders a `TextField` with highlighted regions between the two offsets. ### 3.4 Double Click (Word Selection) When a double-click is detected: 1. Identify the character at the click offset. 2. Expand left and right until a non-word character (space, punctuation, newline) is hit. 3. Set `selection_start` and `selection_end` to these boundaries. ## 4. Input Batching and Latency As specified in `architecture.md`, the Main goroutine batches events. For touch, this is critical: ### 4.1 Event Compression in the Main Loop The Main goroutine collects events during its event cycle. For a drag gesture, multiple `pointer.Event` (Type: `Move`) may fire before the next `FrameEvent`. The Main goroutine: 1. Accumulates all events into a `[]InputEvent` slice. 2. On the next `FrameEvent`, it sends the entire batch to Logic via `inputChan`. 3. Logic processes the batch sequentially, updating the gesture state and cursor/selection. 4. Logic produces a single frame reflecting the final state after all events. This means that if 3 `Drag` events fire in one frame, Logic sees all 3 and produces one frame with the cursor/selection at the final position. There is no "churn" of intermediate frames. ### 4.2 Latency Budget - **Event Capture**: Gioui captures the touch event immediately (sub-ms). - **Batching**: Main goroutine holds events until the next `FrameEvent` (up to 16ms, but typically < 8ms). - **Processing**: Logic processes the batch and computes layout (< 16ms). - **Rendering**: Frame receiver stores and invalidates (sub-ms). - **Total**: < 32ms from touch to visual feedback (typically < 16ms). ### 4.3 Gesture Continuity Because Logic maintains the gesture state machine across frames, a multi-frame drag gesture is continuous: - Frame 1: `Press` at A → Logic enters `Pressed` state. - Frame 2: `Drag` to B → Logic enters `Selecting` state, updates `selection_end`. - Frame 3: `Drag` to C → Logic updates `selection_end` again. - Frame 4: `Release` → Logic finalizes selection, enters `Idle` state. Each frame produces a new `[]Element` with the updated selection highlight. ## 5. Keyboard Interaction The `TextField` element must be "focused" to receive keyboard events. Focus is a transient state managed by Logic. ### 5.1 Focus Acquisition 1. **Tap on Editor**: User taps the `TextField` region. Main goroutine sends `pointer.Event` (Type: `Press`) to Logic. 2. **Focus Request**: Logic sets `focused = true` and computes a frame with `gtx.Execute(key.FocusCmd` and `gtx.Execute(key.SoftKeyboardCmd{Show: true}`. 3. **Keyboard Appears**: Gioui shows the Android soft keyboard. Subsequent `w.Event()` calls return `key.Event`. ### 5.2 Focus Loss 1. **Tap Outside Editor**: User taps a non-editor region (e.g., status bar, directory browser). Logic sets `focused = false` and computes a frame with `gtx.Execute(key.SoftKeyboardOp{Show: false})`. 2. **Keyboard Disappears**: Gioui hides the soft keyboard. ### 5.3 Key Events | Event | Type | Gio Constant | Action | |---|---|---|---| | Text insertion | `key.EditEvent` | — | Insert `event.Text` at cursor, append to undo chain | | Backspace | `key.Event` (Press) | `key.NameDeleteBackward` | Delete character before cursor, append to delete chain | | Delete | `key.Event` (Press) | `key.NameDeleteForward` | Delete character after cursor | | Enter | `key.Event` (Press) | `key.NameReturn` | Insert newline at cursor | | Tab | `Press` | `key.NameTab` | Insert spaces (configurable, e.g., 4 spaces) | | Arrow Up | `key.Event` (Press) | `key.NameUpArrow` | Move cursor up one visual line (GlyphLayout-based) | | Arrow Down | `key.Event` (Press) | `key.NameDownArrow` | Move cursor down one visual line (GlyphLayout-based) | | Arrow Left | `key.Event` (Press) | `key.NameLeftArrow` | Move cursor left one byte | | Arrow Right | `key.Event` (Press) | `key.NameRightArrow` | Move cursor right one byte | | Ctrl+A | `Press` | `A` (with Ctrl) | Select all text (future) | | Ctrl+Z | `Press` | `Z` (with Ctrl) | Undo (future) | | Ctrl+Y | `Press` | `Y` (with Ctrl) | Redo (future) | | Ctrl+F | `Press` | `F` (with Ctrl) | Show search bar (future) | **Current implementation** (as of June 2026): `HandleKeyDown` in `state.go` dispatches `key.Name` events via a `switch` on `key.Name` constants. `key.EditEvent` is handled separately for text input. Up/Down arrow navigation uses `HandleVerticalCursorMove` which is GlyphLayout-based. Left/Right uses `HandleCursorMove` which is byte-based ±1. ### 5.4 IME and Composition (Pure Gioui) Android soft keyboards use IME composition for languages like Chinese, Japanese, or Korean. Gioui's `key.Event` (Type: `Edit`) handles the final committed text. Logic treats the committed text as a single insertion at the cursor. **Limitation**: Gioui's `key.InputOp` on Android does not provide a full `InputConnection`. The IME has no context — it cannot read surrounding text for autocorrect, cannot anchor swipe gestures to our cursor, and cannot provide predictive suggestions. Only raw character commits are received. ## 5.5 Android IME Bridge via Fragment To access the full suite of Android IME features (autocorrect, swipe typing, voice input, predictions), Pad uses a **hidden `EditText`** hosted in a native `Fragment`. This is the same pattern used by Passgo (see `cmd/passgo-gui/impl_android.go`, `PgpConnect.java`). ### 5.5.1 Fragment Registration The Fragment is registered on app startup using `app.ViewEvent`: 1. **Go side**: `handleEvent` receives `app.ViewEvent` on app start. `e.View` is a `uintptr` pointing to the `GioView` (`android.view.View`). 2. **C side (JNI)**: `registerFragment()` receives the `jobject view`: - Gets the `Context` from the View via `getContext()` - Gets the `ClassLoader` from the Context via `getClassLoader()` - Loads the Fragment class via `findClass("pad/ime/ImeFragment")` - Creates an instance by calling the constructor with the View 3. **Java side (`ImeFragment.java`)**: - `ImeFragment` extends `Fragment` - Constructor receives the `View` (GioView), extracts `Context` - In `onAttach()`, casts `Context` → `Activity` - Uses `act.getFragmentManager().beginTransaction().add(inst, "ImeFragment").commitNow()` - Inflates a layout containing a transparent, zero-size `EditText` ### 5.5.2 IME Fragment Architecture ``` ┌─────────────────────────────────────────────────────────────┐ │ Android Activity │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ GioView (OpenGL rendering) │ │ │ │ - pointer.InputOp → pointer.Event │ │ │ │ - key.InputOp → key.Event │ │ │ └───────────────────────────────────────────────────────┘ │ │ ┌───────────────────────────────────────────────────────┐ │ │ │ ImeFragment (transparent overlay) │ │ │ │ ┌─────────────────────────────────────────────────┐ │ │ │ │ │ EditText (hidden, zero-size) │ │ │ │ │ │ - Holds text window around cursor │ │ │ │ │ │ - Selection synced to cursor position │ │ │ │ │ │ - IME receives all keyboard events │ │ │ │ │ └─────────────────────────────────────────────────┘ │ │ │ └───────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘ ``` ### 5.5.3 Two-Way Sync: Gio ↔ EditText The IME bridge maintains a tight sync loop between the Gio editor state and the hidden `EditText`: **Gio → EditText (Cursor Sync)**: 1. User taps to move cursor in Gio editor. 2. Logic updates `cursor_pos` in state. 3. Main goroutine calls a JNI function: `SetEditTextSelection(byteOffset)`. 4. C side calls `editText.setSelection(byteOffset)` on the `EditText`. 5. IME now knows the cursor position and can provide context-aware suggestions. **EditText → Gio (IME Events)**: 1. User types/swipes/uses voice input on the soft keyboard. 2. IME commits text to the `EditText` via `EditText.onTextChanged()`. 3. A `TextWatcher` on the `EditText` captures the change. 4. The change is sent via JNI callback to Go: `ImeTextChanged(replacementText, start, before, count)`. 5. C side allocates memory for the string and calls a Go export function. 6. Go side creates a synthetic `key.Event` (Type: `Edit`) and sends it to the Main goroutine. 7. Main goroutine batches it into `[]InputEvent` alongside any pointer/keyboard events. 8. Logic processes the synthetic event as a text insertion at the cursor. ### 5.5.4 Text Window Management The `EditText` does not hold the entire file — only a **text window** around the cursor: - **Window size**: ±1KB of text around the cursor (configurable). - **On cursor move**: The Main goroutine sends a JNI call to update the `EditText`'s text and selection. - **On text insertion/deletion**: The Logic goroutine updates the in-memory buffer, then sends a JNI call to update the `EditText`'s text and selection. This ensures the IME always has context for suggestions, even for multi-gigabyte files. ### 5.5.5 IME Feature Support | Feature | Supported | How | |---|---|---| | Autocorrect | Yes | IME reads text window from `EditText`, suggests corrections | | Swipe typing | Yes | IME anchors swipe to `EditText` selection (cursor position) | | Voice input | Yes | IME commits voice text to `EditText` | | Predictive bar | Yes | IME reads text window for context | | Emoji keyboard | Yes | IME commits emoji to `EditText` | | CJK composition | Yes | IME composition buffer → `EditText` → Gio | ### 5.5.6 Event Merging in the Main Loop IME events from the Fragment are merged with Gioui events in the Main goroutine: ``` Main goroutine event cycle: 1. w.Event() → returns next event (FrameEvent, pointer.Event, key.Event) 2. Check for pending IME events (non-blocking channel read) 3. If IME event exists → create InputEvent, add to batch 4. If Gioui event exists → create InputEvent, add to batch 5. Send entire batch to Logic via inputChan ``` IME events are **not** returned by `w.Event()` — they arrive asynchronously via a JNI callback channel. The Main goroutine drains this channel in every cycle, ensuring IME events are batched alongside Gioui events and delivered to Logic atomically. ### 5.5.7 Implementation Notes - The `ImeFragment` lives in `cmd/pad/impl_android.go` (Go), `jni_android.c` (C), and `ime/ImeFragment.java` (Java). - The JNI callback uses a `BlockingQueue` or unbuffered channel to pass events from the Java main looper to the Main goroutine. - The `EditText` is transparent (`android:background="@android:color/transparent"`) and zero-size (`android:layout_width="0dp" android:layout_height="0dp"`), so it does not affect the UI. - The `EditText` is **not** focusable by touch — it only receives focus programmatically when the Gio editor needs the keyboard. ## 5.6 Cut, Copy, and Paste Pad implements its own clipboard (not Android's `ClipboardManager`) with **persistent storage** — the clipboard survives process death and app restart. ### 5.6.1 Clipboard Storage The clipboard is stored as a string field in the Logic goroutine's state and persisted to disk: - **File**: `.pad/clipboard.json` - **Format**: `{"text": "..."}` (UTF-8 string) - **Write trigger**: Every cut or copy operation triggers an async write to disk (debounced, 100ms). - **Load trigger**: On app start, Logic reads `.pad/clipboard.json` and restores the clipboard. - **Atomic write**: Written via temp file + rename in `.pad/tmp/` (same pattern as auto-save). This ensures the clipboard is available even if the system kills the app while it is in the background. ### 5.6.2 StatusBar Layout with Cut/Copy/Paste Icons The StatusBar (top bar) has **fixed icon slots** for cut, copy, and paste. Icons appear/disappear without reflowing other elements. **Layout** (vertical stack, top to bottom): ``` ┌──────────────────────────────────────────────────────────────┐ │ filename.txt │ ← Line 1: filename (truncated with ellipsis) │ [Cut] [Copy] [Paste] [Conf] [Search] │ ← Line 2: action icons (left/right) └──────────────────────────────────────────────────────────────┘ StatusBar (top bar) ┌──────────────────────────────────────────────────────────────┐ │ TextField (editor content) │ └──────────────────────────────────────────────────────────────┘ ┌──────────────────────────────────────────────────────────────┐ │ Ln 10, Col 5 1024 / 50000 [Word Wrap: On] │ ← BottomBar └──────────────────────────────────────────────────────────────┘ ``` **Fixed slot positions** (in Dp, from the edges): - **Cut icon**: leftmost action icon (24×24 DP, X: 0) - **Copy icon**: second from left (24×24 DP, X: 48) - **Paste icon**: third from left (24×24 DP, X: 96) - **Conflict icon**: right side (24×24 DP, future) - **Search icon**: rightmost (24×24 DP, future) - **Filename**: top line, truncated with ellipsis if too long **Visibility rules**: | Icon | Visible When | |---|---| | **Cut** | `selection_start != selection_end` | | **Copy** | `selection_start != selection_end` | | **Paste** | `clipboard != ""` | | **Conflict** | sync conflict detected for active file | | **Search** | Always visible (toggle search bar) | Icons are rendered as `Icon` elements with `Size=0`, so they auto-scale to fill their `24×24` DP regions via an affine transform in `r.drawPng()`. The StatusBar's `Region` height is computed dynamically based on whether the filename line is visible (2 lines when filename is shown, 1 line when in merge/search mode). ### 5.6.3 Cut Operation 1. User taps the Cut icon. 2. Logic extracts text: `clipboard = buffer[selection_start:selection_end]`. 3. Logic writes clipboard to `.pad/clipboard.json` (async). 4. Logic deletes selected text from buffer (append to delete chain). 5. Logic sets `selection_start = selection_end = cursor_pos` (clears selection). 6. Logic produces a new frame (Cut/Copy icons hidden, Paste icon may appear). ### 5.6.4 Copy Operation 1. User taps the Copy icon. 2. Logic extracts text: `clipboard = buffer[selection_start:selection_end]`. 3. Logic writes clipboard to `.pad/clipboard.json` (async). 4. Selection is **not** cleared (text remains in buffer). 5. Logic produces a new frame (Cut/Copy icons still visible, Paste icon appears). ### 5.6.4a Search Button The Search button toggles the search bar visible/invisible. 1. User taps the Search icon. 2. Logic toggles `search_active` state field. 3. If `search_active` is true: - Show `SearchBar` element below the StatusBar. - Set focus to the search bar's text field. - Show the soft keyboard (`gtx.Execute(key.SoftKeyboardCmd{Show: true}`). 4. If `search_active` is false: - Hide `SearchBar` element. - Clear search query. - Remove focus from search bar. - Hide soft keyboard (`gtx.Execute(key.SoftKeyboardCmd{Show: false}`). 5. Logic produces a new frame. The Search button is always visible in the StatusBar (unlike Cut/Copy/Paste which are conditional). It serves as the primary way to activate search on Android (where Ctrl+F may not be available). ### 5.6.5 Paste Operation 1. User taps the Paste icon. 2. Logic inserts `clipboard` text at cursor position. 3. Logic appends an insert operation to the undo chain. 4. Logic advances cursor past the inserted text. 5. Logic produces a new frame (no change to Cut/Copy/Paste visibility). ### 5.6.6 Input Routing 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. **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 within the element's clip context. The clip defines both the coordinate system and the clipping bounds for the click area. The click area matches the drawn content position within that clip, not the entire clip region. 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 The clipboard is a simple string field. It has no state machine — it is either empty or contains text. On cut/copy, the text is replaced. On paste, the text is unchanged. **Edge cases**: - **Paste into empty clipboard**: No-op (Paste icon is hidden when clipboard is empty). - **Cut with no selection**: No-op (Cut icon is hidden when there is no selection). - **Copy with no selection**: No-op (Copy icon is hidden when there is no selection). - **Multiple cuts/copies**: Each cut/copy replaces the previous clipboard content. - **Clipboard persistence**: If the app is killed, the clipboard is restored from `.pad/clipboard.json` on restart. ## 5.7 Filename Display with Ellipsis Toggle The filename is displayed on a separate line at the top of the StatusBar. When the filename is too long to fit the screen width, it is truncated with an ellipsis (`...`). Tapping the ellipsis toggles between the truncated single-line view and a full multi-line view. ### 5.7.1 Truncation Behavior - **Truncated view**: Filename is truncated to fit the screen width, with `...` at the end. Example: `very_long_filename...` - **Full view**: Filename is displayed in full, potentially spanning multiple lines. Example: `very_long_filename_that_does_not_fit_on_a_single_line.txt` ### 5.7.2 Toggle Interaction 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 a `Tap` interaction. Tapping it triggers the toggle. ### 5.7.3 Input Routing - 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 The Logic goroutine maintains a boolean field `filename_expanded`: - `false` (default): truncated view with ellipsis. - `true`: full multi-line view. This field is **not** persisted to disk — it is reset on app restart (same as gesture state). ### 5.7.5 Layout In the truncated view, the StatusBar has 2 lines: ``` Line 1: filename.txt... Line 2: [Cut] [Copy] [Paste] [Conf] [Search] ``` In the full view, the StatusBar has 3+ lines (depending on filename length): ``` Line 1: very_long_filename_that_does_not_fit_on_a_single_line.txt Line 2: [Cut] [Copy] [Paste] [Conf] [Search] ``` The `Region.H` of the StatusBar is computed dynamically based on the number of lines. The BottomBar is always at the bottom of the screen (24 DP fixed height). ## 5.8 Bottom Bar 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. ### 5.8.1 Layout ``` ┌──────────────────────────────────────────────────────────────┐ │ Ln 10, Col 5 1024 / 50000 [Word Wrap: On] │ └──────────────────────────────────────────────────────────────┘ ``` - **Left**: Cursor position (e.g., "Ln 10, Col 5") - **Center**: Byte position (e.g., "1024 / 50000") - **Right**: Word wrap button (tap to toggle On/Off) The BottomBar's `Region.H` is fixed at 24 DP. It sits below the editor content. ### 5.8.2 Cursor Position The cursor position is computed from the current byte offset: 1. Logic converts byte offset to (line, column) using the **Line Index**. 2. The `CursorPos` field is set to `"Ln {line}, Col {column}"`. 3. The field is updated on every cursor movement (tap, arrow keys, backspace, etc.). ### 5.8.3 Byte Position The byte position shows the current byte offset and total file size: - **Format**: `"current / total"` (e.g., "1024 / 50000") - **Current**: byte offset of the cursor in the file - **Total**: total file size in bytes - The field is updated on every cursor movement. ### 5.8.4 Word Wrap Button The word wrap button toggles word wrap on/off: - `true` → "Word Wrap: On" - `false` → "Word Wrap: Off" **Interaction**: 1. User taps the word wrap button in the BottomBar. 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. ## 6. Scroll Momentum Pad implements a momentum model in the Logic goroutine to provide smooth scrolling. ### 6.1 Scroll Sources | Source | Event | Behavior | |---|---|---| | **Mouse Wheel** | `pointer.Event` (Source: `MouseWheel`) | Direct scroll by `event.Scroll.Y` DP | | **Finger Drag (No Focus)** | `pointer.Event` (Type: `Move`, no keyboard) | Direct 1:1 mapping of finger movement to `scroll_offset` | | **Fling** | `pointer.Event` (Type: `Release`, high velocity) | Momentum-based scroll with exponential decay | ### 6.2 Fling Logic When a `Release` event occurs with a high vertical velocity: 1. **Velocity Calculation**: Logic tracks the position and timestamp of the last few `Move` events. On `Release`, it calculates the velocity (DP/ms). 2. **Momentum Start**: If velocity > threshold (e.g., 0.5 DP/ms), Logic enters a `Flinging` state. 3. **Decay**: On each subsequent frame (triggered by a timer or periodic `sendFrame()`), Logic updates `scroll_offset` by `velocity * elapsed_ms` and multiplies `velocity` by a decay factor (e.g., 0.95). 4. **Stop**: When velocity drops below a minimum (e.g., 0.01 DP/ms), Logic stops the fling and returns to `Idle`. ### 6.3 Scroll Clamping `scroll_offset` is clamped to ensure the viewport never scrolls past the top or bottom of the file: - **Min**: 0 (top of file) - **Max**: `total_file_height - viewport_height` (bottom of file)