# Editor Implementation Plan ## 1. Design Principles - **Single-Owner Pattern**: The logic goroutine is the exclusive authority for modifying the editor buffer, cursor, and selection state. - **Asynchronous I/O**: File saving and loading operations are delegated to the worker pool. The UI never blocks on disk operations. - **Buffer-Based Editing**: For performance, the editor will eventually utilize a gap buffer or rope data structure. For MVP, string manipulation is acceptable if performance targets are met. - **Event-Driven**: Keyboard input, mouse interactions, and menu actions are passed through the existing `inputChan` to the logic goroutine. ## 2. Core State Structure The `State` struct in `internal/editor/state.go` will be extended to track active editing session data: ```go type EditorState struct { Buffer string // Document content (or gap buffer) CursorPosition int // Byte offset SelectionStart int // -1 if no selection SelectionEnd int // -1 if no selection UndoStack []EditCommand // For undo/redo Dirty bool // Needs save } ``` ## 3. Implementation Phases ### Phase 1: Basic Buffer Management & Cursor - [ ] Define `EditorState` (Cursor, Selection, Dirty flag, GlyphLayout). - [ ] Implement `GlyphLayout` capture in `drawWrappedText` and feedback via `layoutChan`. - [ ] Implement cursor navigation (Arrow keys: Left, Right, Up, Down) using GlyphLayout. - [ ] Implement basic buffer updates (Insert character, Delete/Backspace). - [ ] Implement cursor display and blink animation. - [ ] Update `EditorLayout` to position cursor from GlyphLayout (no fixed-width approximation). ### Phase 2: File IO Integration - [ ] Implement `SaveFile` handler: Dispatch `WriteFileTask` to worker pool. - [ ] Implement `LoadFile` handler: Dispatch `ReadFileTask` to worker pool (existing in `logic.go`). - [ ] Status bar integration: Show "Saving..." indicator, "Modified" status. ### Phase 3: Text Editing Operations - [ ] Implement Cut/Copy/Paste interactions. - [ ] Implement multi-line text navigation (Home/End, PageUp/PageDown). - [ ] Implement text selection display and mouse interaction. - [ ] Implement soft-wrap toggle (already exists, need to verify logic). ### Phase 4: Polish & Advanced Features - [ ] Undo/Redo stack implementation. - [ ] Performance testing with large files (>1MB). --- ## 4. Interaction Flow (Example: Keyboard/Mouse Input) 1. **User** performs an action (types a character, taps an icon). 2. **UI (Renderer)** registers the element's interaction (`event.Op` for keys, `gesture.Add` for mouse) within its clip context. 3. **Main Loop** captures the event (using `key.Filter` for keys, `gesture.Update` for mouse). 4. **Main Loop** generates an `InputEvent` and sends it to `inputChan`. 5. **Logic Goroutine** receives the event, triggers the corresponding `Handler` in `TheState`. 6. **Logic Goroutine** modifies state, and sends updated elements back through `frameChan`. 7. **Renderer** paints the new frame. --- ## 5. Performance Targets | Operation | Target | Mechanism | |---|---|---| | Typing latency | < 16ms | Single-owner logic update, immediate frame redraw | | File Load (1MB) | < 100ms | Async worker pool read, background loading | | File Save | < 100ms | Async worker pool write | | Undo/Redo | < 10ms | O(1) or O(N) memory-based buffer update | --- ## 7. Input Handling Mechanism ### 7.1 Mouse/Touch Input - Uses `gesture.Click` and `gesture.Scroll`. - Registered via `event.Op` and gesture `Add()` within the clip context of the element. - Processed by `Renderer.CheckGestures` and dispatched via `InputEvent` to the logic goroutine. ### 7.2 Keyboard Input - **Registration**: Elements register as input handlers using `event.Op(gtx.Ops, elementID)`. This tags the current clip context for input routing. - **Focus**: Focus is managed by the logic goroutine. When an element is focused, `key.FocusCmd{Tag: elementID}` is submitted to the operation stack. - **Filtering**: Keyboard events are processed in the main loop using `key.Filter{Focus: focusedElementID}` to ensure events are only routed to the active element. - **Routing**: `key.Event` (for key presses) and `key.EditEvent` (for text input) are converted into `InputEvent` structs and sent to the logic goroutine via `inputChan` for state updates. --- ## 8. Glyph Layout Architecture ### 8.1 Problem The editor must know the exact screen position of every character after shaping and word wrapping. This is required for: - Accurate cursor rendering (no fixed-width approximation) - Correct cursor navigation (arrow keys respect visual line boundaries) - Scroll-aware positioning (cursor follows text when viewport changes) - Screen resize resilience (cursor tracks text as wrap points shift) - Future features: mouse click-to-place, text selection, search highlight positioning ### 8.2 Core Principle **The renderer is the single source of truth for where characters appear on screen.** Logic never guesses positions — it reads the layout the renderer already computed during the draw pass. ### 8.3 Data Structure ```go type GlyphLayout struct { ByteOffsets []int // byte offset of each glyph in the buffer X []Dp // screen X (Dp) of each glyph, relative to text region origin Y []Dp // screen Y (Dp) of each glyph, relative to text region origin Advance []Dp // advance width (Dp) of each glyph } ``` Each index `i` represents one glyph. `ByteOffsets[i]` is the byte position in the buffer, `(X[i], Y[i])` is its screen location, and `Advance[i]` is its width. The slice length equals the total number of glyphs (one per rune). **Derived values:** - `LastLineY` = `Y[len(Y)-1]` (last glyph's baseline Y) — replaces the separate `lastLineY` feedback - Visual line breaks = any index `i` where `Y[i] > Y[i-1]` - Cursor at byte offset `b` → binary search `ByteOffsets` for exact match, then read `(X[idx], Y[idx])` Storing all X advances is not expensive — a single page of text is a tiny amount of memory. ### 8.4 Capture Point `Renderer.drawWrappedText` already iterates every glyph via `r.shp.NextGlyph()`. It captures layout data during that loop and returns it: ```go func (r *Renderer) drawWrappedText(...) GlyphLayout { // ... existing shaping setup ... r.shp.LayoutString(params, str) var layout GlyphLayout byteOffset := 0 for g, ok := r.shp.NextGlyph(); ok; g, ok = r.shp.NextGlyph() { glyphX := Dp(float32(g.Xdot >> 6)) - scrollOffset glyphY := Dp(float32(g.Ydot)) layout.ByteOffsets = append(layout.ByteOffsets, byteOffset) layout.X = append(layout.X, glyphX) layout.Y = append(layout.Y, glyphY) layout.Advance = append(layout.Advance, Dp(float32(g.Advance >> 6))) byteOffset += utf8.RuneLen(runeAtThisGlyph) // ... existing draw logic ... } r.lastLineY = layout.Y[len(layout.Y)-1] // derived, not separate return layout } ``` `TextField.Draw` stores the returned layout on the renderer for the main loop to pick up. ### 8.5 Feedback Path A new channel carries the full layout from renderer to logic: ```go layoutChan chan GlyphLayout // in Logic struct ``` Main loop sends `r.GlyphLayout()` after each draw frame. Logic stores it in `State.Editor.GlyphLayout`. The old `lastLineYChan` is removed — `LastLineY` is derived from `GlyphLayout.Y[len-1]`. ### 8.6 Editor State Consumption `EditorState` gains the layout field: ```go type EditorState struct { Buffer string CursorPosition int GlyphLayout GlyphLayout // ... } ``` **Cursor rendering** in `EditorLayout`: ```go func EditorLayout(...) []ui.Element { layout := TheState.Editor.GlyphLayout pos := TheState.Editor.CursorPosition idx := sort.Search(len(layout.ByteOffsets), func(i int) bool { return layout.ByteOffsets[i] >= pos }) var cursorX, cursorY Dp if idx < len(layout.ByteOffsets) { cursorX = editorRegion.X + layout.X[idx] cursorY = editorRegion.Y + layout.Y[idx] } else { // cursor at end of buffer — place after last glyph cursorX = editorRegion.X + layout.X[len(layout.X)-1] + layout.Advance[len(layout.Advance)-1] cursorY = editorRegion.Y + layout.Y[len(layout.Y)-1] } // ... create cursor element ... } ``` **Cursor movement** (`HandleCursorMove`): ```go func HandleCursorMove(delta int) { layout := TheState.Editor.GlyphLayout pos := TheState.Editor.CursorPosition idx := findGlyphIndex(layout, pos) // binary search ByteOffsets if delta > 0 { idx++ // next glyph } else { idx-- // previous glyph } if idx >= 0 && idx < len(layout.ByteOffsets) { TheState.Editor.CursorPosition = layout.ByteOffsets[idx] } } ``` **Arrow-up / arrow-down** (future): find current glyph's Y, search for the glyph with the closest X on the target Y line. ### 8.7 Correctness Guarantees | Scenario | How it works | |---|---| | **Word wrap** | Shaper's `WrapHeuristically` produces different Y values at wrap points. Layout captures them exactly. | | **Screen resize** | Next frame → new `wrapWidth` → shaper produces new layout → main loop sends new layout → cursor follows text. | | **Editing** | Buffer changes → layout re-computed on next frame → `ByteOffsets` reflect new positions. | | **Scroll** | Y values are computed minus `scrollOffset` → cursor Y tracks naturally. | | **LastLineY** | Derived from `layout.Y[len-1]` — no separate tracking needed. | | **Non-fixed-width fonts** | `Advance` comes from actual glyph metrics, not approximation. | | **Empty buffer** | Layout is empty (`len == 0`). Cursor rendering handles this as the "after last glyph" case. | ### 8.8 Summary of Changes | File | Change | |---|---| | `internal/ui/unit.go` | Add `GlyphLayout` struct | | `internal/ui/render.go` | `drawWrappedText` returns `GlyphLayout`; derive `lastLineY` from it; expose via `r.GlyphLayout()` | | `internal/ui/element.go` | `TextField.Draw` stores returned layout on renderer | | `internal/editor/logic.go` | Add `layoutChan`; handle it in `Run()` loop; remove `lastLineYChan` | | `internal/editor/state.go` | `EditorState.GlyphLayout` field; `EditorLayout` uses it for cursor positioning; `HandleCursorMove` uses it for navigation | | `cmd/pad/main.go` | Main loop sends `r.GlyphLayout()` on `layoutChan` after draw; remove `lastLineYChan` usage |