- Gioui primitives: pointer.InputOp, key.InputOp, key.FocusOp, SoftKeyboardOp - Op/Event flow: Logic computes elements, Renderer submits ops, Main batches events - Coordinate mapping: UI space → text space → byte space, with hit-region tags - Gesture state machine: tap, double-tap, long-press, drag-select, scroll - Gesture disambiguation: timing thresholds (500ms long-press, 300ms double-tap, 16DP drag) - Selection logic: selection_start/selection_end byte offsets - Input batching: Main accumulates events, sends batch to Logic per frame - Keyboard interaction: focus acquisition/loss, key event table, IME composition - Scroll momentum: fling logic with exponential decay, scroll clamping Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
11 KiB
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 (returned by w.Event()) | Use Case |
|---|---|---|
pointer.InputOp |
pointer.Event |
Defines hit-regions (editor, buttons, search bar). Captures Press, Release, Move, Drag, Scroll. |
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. |
1.1 Op/Event Flow
Because Logic does not have access to *op.Ops, the Op/Event flow is split:
- Logic → Ops: Logic computes
[]Element. Each element carries metadata about what input ops it needs (e.g., "this TextField needspointer.InputOpwithKind: Tapon region X"). - Renderer → Ops: During the paint function, the Renderer iterates
[]Elementand submits the appropriatepointer.InputOp/key.InputOpcalls into*op.Ops. - Gioui → Events: Gioui returns
pointer.Event/key.Eventviaw.Event()in subsequent cycles. - Main → Logic: The Main goroutine batches these events into
[]InputEventand sends them to Logic. - Logic → State: Logic processes events, updates state, and computes a new frame.
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:
- UI Space (DP): The raw (X, Y) coordinates from Gioui events, relative to the window origin.
- Text Space (Lines/Cols): The logical position within the text file, accounting for scroll offset and word wrap.
- Byte Space (Offsets): The raw byte offset in the UTF-8 file buffer.
2.1 Hit-Region Tags
Gioui pointer.InputOp takes a tag (an op.Tag). When an event fires, event.Tag identifies which region was touched.
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.
2.2 Mapping UI → Byte Offset (Editor/TextField)
When a pointer.Event occurs at (Ex, Ey) on the editor tag:
- Adjust for Scroll:
Y = Ey + scroll_offset. - Identify Visual Line:
visual_line = floor(Y / line_height). - 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. - Identify Column:
- Retrieve text for
logical_linefrom the chunked buffer. - Use the font shaper to measure glyph widths until the cumulative width exceeds
Ex. - Clamp to the line length.
- Retrieve text for
- 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→Releaseoccurs within 500ms, it's a tap. Otherwise, it's a long press. - Tap vs Drag: If the finger moves more than 16 DP between
PressandRelease, 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: Setselection_start = selection_end = offset_at(X, Y). - On
Drag: Updateselection_end = offset_at(X, Y). - The UI renders a
TextFieldwith highlighted regions between the two offsets.
- On
3.4 Double Click (Word Selection)
When a double-click is detected:
- Identify the character at the click offset.
- Expand left and right until a non-word character (space, punctuation, newline) is hit.
- Set
selection_startandselection_endto 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:
- Accumulates all events into a
[]InputEventslice. - On the next
FrameEvent, it sends the entire batch to Logic viainputChan. - Logic processes the batch sequentially, updating the gesture state and cursor/selection.
- 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:
Pressat A → Logic entersPressedstate. - Frame 2:
Dragto B → Logic entersSelectingstate, updatesselection_end. - Frame 3:
Dragto C → Logic updatesselection_endagain. - Frame 4:
Release→ Logic finalizes selection, entersIdlestate.
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
- Tap on Editor: User taps the
TextFieldregion. Main goroutine sendspointer.Event(Type:Press) to Logic. - Focus Request: Logic sets
focused = trueand computes a frame withkey.FocusOpandkey.SoftKeyboardOp(true). - Keyboard Appears: Gioui shows the Android soft keyboard. Subsequent
w.Event()calls returnkey.Event.
5.2 Focus Loss
- Tap Outside Editor: User taps a non-editor region (e.g., status bar, directory browser). Logic sets
focused = falseand computes a frame withkey.SoftKeyboardOp(false). - Keyboard Disappears: Gioui hides the soft keyboard.
5.3 Key Events
| Event | Type | Name | Action |
|---|---|---|---|
| Text insertion | Edit |
— | Insert event.Text at cursor, append to undo chain |
| Backspace | Press |
Backspace |
Delete character before cursor, append to delete chain |
| Enter | Press |
Enter |
Insert newline at cursor |
| Tab | Press |
Tab |
Insert spaces (configurable, e.g., 4 spaces) |
| Arrow Up/Down | Press |
Up/Down |
Move cursor up/down one line |
| Arrow Left/Right | Press |
Left/Right |
Move cursor left/right one character |
| Ctrl+A | Press |
A (with Ctrl) |
Select all text |
| Ctrl+Z | Press |
Z (with Ctrl) |
Undo |
| Ctrl+Y | Press |
Y (with Ctrl) |
Redo |
| Ctrl+F | Press |
F (with Ctrl) |
Show search bar |
5.4 IME and Composition
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.
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:
- Velocity Calculation: Logic tracks the position and timestamp of the last few
Moveevents. OnRelease, it calculates the velocity (DP/ms). - Momentum Start: If velocity > threshold (e.g., 0.5 DP/ms), Logic enters a
Flingingstate. - Decay: On each subsequent frame (triggered by a timer or periodic
sendFrame()), Logic updatesscroll_offsetbyvelocity * elapsed_msand multipliesvelocityby a decay factor (e.g., 0.95). - 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)