Add the invariant that GlyphLayout offsets are relative to
IMEWindowStartByte and that cursor positioning must add/subtract the window
base. Record the second tap-to-cursor fix (base offset) alongside the earlier
tapLocalY fix in the development plan.
Tapping in the editor repositions the cursor, but it was never actually
verified (the old unit test only checked in-bounds/no-panic). On-device
verification found that on a large file scrolled deep, the cursor clamped to
the bottom of the viewport regardless of tap position.
Root cause: the tap handler computed the tap's text-local Y in content space
(pt.Y - region.Y + full ScrollOffset) and passed it to SetCursorFromPoint,
whose visualLine = y/lineHeight then produced a huge content-line number
(e.g. 91,640). But the GlyphLayout is window-relative (layout.Y==0 is the top
of the visible window), so the number far exceeded the window's line count and
clamped to the last group (bottom line). The Phase 3 windowing refactor
introduced the windowed layout without updating the tap handler.
Fix: tapLocalY() converts the tap Y to window-relative space by adding only the
sub-line remainder (ScrollOffset mod lineHeight), never the full scroll.
Extracted as a named helper so it is unit-testable. Added two regression tests
that fail on the pre-fix formula (cursor clamps to the bottom line) and pass on
the fix. Verified on-device: taps now map linearly across the viewport.
Also removed the per-tap/per-glyph log.Printf debug lines in SetCursorFromPoint.
internal/perf: single-goroutine profiler (no locks). When enabled by the
/storage/emulated/0/PadPerf/enable marker it records one CSV row per logic
frame (seq, ms, frame delta, page, scroll Dp, max-scroll Dp, total lines,
visible byte range), logs a rolling ~1/s summary, and on Stop reports
nearest-rank p50/p90/p99/max. Flushes per row-batch but never fsyncs per
flush (avoids periodic hitches in the logic path).
editor: PerfRecord package-level hook (nil when off) + ProbeRecord;
Logic.emitFrame() now centralizes every frame emission so the profiler sees
each logic frame exactly once, on the owner goroutine. State gains
VisibleStart/VisibleEnd so the probe can confirm shaping stays
viewport-bounded.
logic: optional debug cmd poller (off by default) watches <dir>/cmd as a
one-shot file (top/bottom/frac <0..1>/dp <int>) and the owner applies a
clamped [0,MaxScroll] jump + frame. Enables deterministic large-offset scroll
tests without pixel taps.
main: wires the profiler when the marker file exists; logs present-fps
every 2s; stops the profiler on Destroy.
docs: README package inventory (add internal/perf); architecture §11
profiler facility; spec §5 invariant 7 (scroll always clamped to
[0,maxScroll]) + §6 measured rows; development_plan v5 + Phase 6 results.
Verified: go build/vet + full -race green. On-device 10 MB file:
logic-frame cadence flat across offsets 0.02->1.0 (no large-offset
degradation), visible byte range <=4.3 KB at every offset, clamping exact
across 2->130,955-line files, PSS plateaus ~250 MB (bounded, no leak);
profiler overhead negligible.
- IME commit path validated on emulator (Gboard/API 35): single-char,
multi-char (human cadence), deletion (deleteSurroundingText->EditEvent),
and Unicode all commit correctly with cursor in sync; autosave persists.
- Build recipe scripted (gogio + apktool MANAGE_EXTERNAL_STORAGE + apksigner).
- Bugs: (1) open-file crash via Termux bridge -> fixed (c9c0d47); (2) viewport
opens at EOF because the opening tap leaks into the editor (open, Phase 3);
(3) rapid synthetic IME commits desync via per-frame snippet re-push (edge).
v1 (widget rebuild) was drafted against a stale snapshot. The live repo
already has the real filesystem, the Android APK, and a chunked buffer
(the 10 MB+ approach widget.Editor cannot do). The Android IME is
reachable from the op layer with only a small, precisely-defined gap:
missing key.SelectionCmd/SnippetCmd, an EditEvent handler that ignores
Range (corrupts swipe/autocorrect replacement commits), and a missing
InputHint. Completing those is now Phase 1; the emulator (now available
on the 16 GB VM) is the verification instrument.
NewLogic now takes (FileSystem, path, openfunc); update the e2e harness
and editor tests accordingly. The harness uses a no-op openfunc, matching
impl_other.go (the real app's OpenFile is a platform bridge, no-op off
Android).
Also adds doc/development_plan.md (draft, with corrections note re: the
live repo state).
- Add WriteFile method to mock filesystem (non-atomic path, creates files)
- Implement WriteFileAtomic in mock with temp file + rename pattern
using .tmp/ directory, matching real filesystem semantics
- Update WriteFileTask.Execute() to use WriteFileAtomic
- Update FlushAll() to use WriteFileAtomic
- Fix mock to create files on write (matching os.WriteFile behavior)
- Update tests to match new semantics (create-if-not-exists)
- Define pool.FileSystem interface in internal/io/pool/filesystem.go
- Move DirEntry interface to internal/io/pool/types/types.go to break
the pool <-> mock import cycle
- Implement real.FileSystem with atomic writes (temp file + os.Rename)
- Update mock.FileSystem to satisfy pool.FileSystem interface
- Update worker pool tasks to accept pool.FileSystem interface
- Update main.go to use RealFileSystem by default, with -root flag
- Update all browser/editor/test references to types.DirEntry
- Add Section 8 describing GlyphLayout data structure and feedback path
- Renderer captures byte offsets, X/Y positions, and advance widths during shaping
- Layout flows via layoutChan to logic goroutine, replacing lastLineYChan
- Editor uses layout for accurate cursor rendering and navigation
- Handles word wrap, screen resize, editing, scroll, and non-fixed-width fonts
- Replace gtx.Event(nil) with key.Filter{Focus: focusedID} and
key.FocusFilter{Target: focusedID} so Gio correctly routes key
and edit events to the focused editor text field.
- Convert EditorState.CursorPosition (byte offset) to line/column
coordinates for accurate cursor rendering.
- Add debug logging in HandleKeyDown and HandleCursorMove.
- Add detailed §15 lazy loading plan with 7 implementation steps
- Update Phase 2 checklist with specific tasks
- Add E2E test harness for browser files
- Refactor browser layout and handler structure
- Add .gitignore for common patterns
WIP: Actual lazy loading not yet implemented.
- Add clippableElement interface for elements that need their own clip region
- ListView implements NeedsClip() so drawElement pushes a clip before Draw
- RegisterScroll no longer has its own clip; relies on element clip context
- Aligns with Gio's clip-based gesture registration model
- Fix scroll events firing outside the visible list area on browser page
Also:
- Update touch.md documentation to reflect clip-based interaction model
- Remove debug logging from ListView.Draw
- Remove TODO-click-fixes.md (all items resolved)
- Add ui.Dp and ui.Px as distinct named types
- Logic layer works exclusively in Dp (positions, sizes, regions)
- Renderer converts Dp→Px at Gio interop boundaries
- Capture gtx.Constraints once at start of Draw() for consistent positioning
- main.go converts app.ConfigEvent pixels to Dp before passing to logic layer
- Update documentation with new coordinate system architecture
StatusBar (top bar):
- Icons re-arranged: [Cut] [Copy] [Paste] on left, [Conf] [Search] on right
- Removed Right/BytePosition field (moved to BottomBar)
BottomBar:
- Added BytePos field (e.g., "1024 / 50000 bytes")
- Word wrap is now a button (tap to toggle On/Off)
- Layout: CursorPos (left) | BytePos (center) | WordWrap button (right)
Element changes:
- StatusBar: removed Right field, added Search field
- BottomBar: added BytePos field, WordWrap is now a button
Interaction:
- Word wrap tap sends InputEvent{ElementID: "word_wrap"} to Logic
- Logic toggles word_wrap_enabled state field
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
StatusBar (top bar):
- Added Search icon (always visible, toggles search bar)
- Removed cursor position from top bar (moved to bottom bar)
- Fixed icon slots: Cut, Copy, Search, Paste (left to right)
- File size between Paste and Conflict icons
BottomBar (new element):
- Always visible at bottom of editor page (24 DP fixed height)
- Left: cursor position (Ln X, Col Y)
- Right: word wrap status (On/Off)
- Cursor position updated on every cursor movement
- Word wrap status persisted to .pad/state.json
Search button:
- Tap toggles search bar visible/invisible
- When active: shows SearchBar element, focuses text field, shows keyboard
- When inactive: hides SearchBar, clears query, hides keyboard
- Primary search activation method on Android (Ctrl+F still works as secondary)
Layout:
- Top: StatusBar (filename + action icons)
- Middle: SearchBar (when active) + TextField (editor)
- Bottom: BottomBar (cursor position + word wrap status)
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Cut/Copy/Paste:
- Separate icons in StatusBar at fixed positions (no reflow)
- Clipboard persisted to .pad/clipboard.json (survives process death)
- Cut: copies to clipboard, deletes from buffer, appends to undo chain
- Copy: copies to clipboard, keeps text in buffer
- Paste: inserts clipboard text at cursor, appends to undo chain
- Visibility: Cut/Copy when selection exists, Paste when clipboard non-empty
Filename ellipsis toggle:
- Filename on separate line at top of StatusBar
- Truncated with ellipsis if too long
- Tap ellipsis toggles between truncated and full multi-line view
- filename_expanded state field (not persisted)
StatusBar layout:
- Line 1: filename (truncated or full)
- Line 2: [Cut] [Copy] Ln X, Col Y [Paste] size [Conflict]
- Fixed icon slots: Cut (left), Copy (36DP from Cut), Paste (right), Conflict (rightmost)
- Region.H computed dynamically based on filename expansion
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Update block diagram to show correct channel/mutex flow
- Frame receiver now calls w.Invalidate() inside the mutex lock
- Logic goroutine now waits on batched []InputEvent and configChan
- Main loop batches events and sends outside the lock
- Remove select-with-default batching from logic
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Line index: cached on disk in .pad/indices/, invalidated by mtime/size,
incremental updates during editing sessions
- Atomic writes: temp files live in .pad/tmp/, excluded from Syncthing via .stignore
- State storage: multi-file JSON approach (state.json, cursors.json, undo/*.json)
over SQLite — simpler, debuggable, sufficient for expected workload
- Undo limits: 1 MB inline cap on deleted field; larger deletions stored in
separate backup files; 10 MB total disk budget for undo directory
- Added search and word wrap sections to the editor spec
- Updated performance guarantees with search/jump targets