Density (pure hi-DPI) was already scale-free: all bookkeeping is in
density-dp and the scale enters only at the px<->dp boundary. But Android
also has a second axis, the user font-size setting (PxPerSp = fontScale *
PxPerDp), and the shaper draws baselines in sp. At a non-default font
scale the rendered line pitch is 16.8*fontScale dp while every logic-side
consumer used the raw 16.8 dp: taps would misplace by up to (fontScale-1)
viewportfuls of lines and scroll clamping would stop short of the bottom.
- ScaleEvent.FontScale + Frame.FontScale closed loop (main reads
gtx.Metric, logic tracks it in State.fontScale).
- EffectiveLineHeight()/EffectiveLineHeightAt(): the font-scale-applied
line height, now used by every consumer (window start, sub-line
remainder, tap mapping, scroll clamp, page size, cursor vertical move,
menu position, chunk-prefetch fallbacks).
- Renderer: GlyphLayout.LineHeight, caret, selection handles, and
highlight all use the scaled ascent/line-height from gtx.Metric.
- font_scale_test.go: 2000-pair tap property test at fontScale 1.3 with
the glyph layout fabricated at the scaled pitch (independent ground
truth), plus EffectiveLineHeight unit test.
- On-device: tap markers landed on exactly the tapped line at font_scale
1.3 (fsline060/080/081) and 0.8 (fsline039); rendered pitch measured
57/35/44 px at 1.3/0.8/1.0 (matches 16.8*fs*2.625); settled-position
window start k = floor(s/lh_eff) verified against the visible top line.
- Docs: architecture.md 6.2 font-scale axis, README two-scale note +
profiler 2s flush staleness note, development plan v10 Phase 11.
Implement the Android-native touch selection model, verified on-device:
- long-press selects the word under the finger (blank -> caret + paste-only
menu); double-tap selects the word; drag handles resize the selection,
drag the highlighted body to move it; floating menu offers copy/cut/paste
(selection) or paste (bare caret), closing on any item tap.
- Renderer reports finger positions (app-local Dp) as tap/double-tap/
long-press/selection-drag events; the logic goroutine owns all geometry
(EditorRegion, menu rect, hit-testing, handles) and the renderer only
draws the frame snapshot.
- Long press: 400 ms still-press on the editor, cancelled by movement
(non-grabbing raw pointer probe) or by a scroll/handle grab. The main
loop keeps invalidating while a press is pending (Gio renders on demand;
a stationary finger produces no frames).
- Clipboard crosses the goroutine boundary via buffered channels
(clipboardSetChan/pasteReqChan logic->main, pasteChan main->logic);
main executes the Gio ops and, on Android, invalidates after ReadCmd
because a queued transfer.DataEvent schedules no frame of its own.
Renderer fixes found while validating on-device:
- clickReg was stored by value in a map; range yielded copies so per-frame
press bookkeeping (long-press state) was silently discarded. Now pointers.
- On Android a tap's press+release arrive in the same frame and
gesture.Click/Drag return one event per Update call; without draining
each gesture's queue every frame the release was lost on an idle window
and every menu tap was swallowed (needed a second tap to 'rescue' it).
Click and drag loops now drain to exhaustion (scroll already does).
- pointer.Filter queries must name Kinds: a zero-kinds filter matches
nothing (the press-probe query was dead).
- Menu.Draw offsets items by the menu origin; the clippable drawElement
branch registers SelDrag (handles now draw for the TextField).
Tests: internal/editor/touch_selection_test.go (word range, long-press,
double-tap, tap/menu guards, handle drags, menu actions, selection edits)
and internal/test/e2e/touch_selection_e2e_test.go; full suite green under
-race. Docs: spec.md §2.2 + §7, architecture.md §6.3a, development_plan.md
Phases 8-9.
Selection: shift+arrow extends a selection (absolute byte offsets,
anchor/caret model); insert/backspace/delete replace the selection; the
IME unions its reported range with the active selection; the highlight
is drawn in the TextField and the selection is pushed to the IME.
Android key input (the blocker found during on-device validation):
Gio v0.10 on Android (a) drops modifier state in the JNI bridge and
(b) wraps plain arrow-key presses in input.SystemEvent for focus
navigation, so arrow keys never reached the editor. main.go now
registers explicit named key.Filters for the four arrows (delivers the
press and suppresses the focus jump) and tracks the shift key itself.
Verified on the emulator: plain arrows move the caret, shift+arrow
shows a highlight, typing replaces the selection.
Real-file e2e tests (real on-disk files via the real FileSystem,
multi-chunk 256KB files, chunk-boundary and multi-byte edits) found
and fixed two real bugs:
1. Line index: UpdateLineIndexAfterEdit only shifted offsets; edits
involving newlines left it permanently inconsistent. Replaced with
newline-aware UpdateLineIndexAfterInsert/UpdateLineIndexAfterDelete.
2. Rune granularity: HandleBackspace/HandleDelete deleted one byte,
corrupting multi-byte UTF-8 characters (e.g. a 2-byte char
straddling a chunk boundary). Now rune-granular.
Also: airtight e2e harness load-wait (StatFile/ReadFile/BuildLineIndex
interleaving could satisfy the old condition early).
Full suite green under -race; on-device verified.
Drop the log.Printf/fmt.Printf traces that fired on every config/input/
click/scroll/open/worker-result during IME and tap debugging. These were
noise (and some were commented out). Error, limit, and recovery logs are
kept; the default-off profiler and gated IME debug remain untouched.
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.
Frame now carries view state (scale/focus/query) to the main goroutine,
which reads only the frame-receiver-stored snapshot. Renderer owns Gio
widget editors (registered by ID) and the draw scale. Autosave timer
sends a token to the logic goroutine instead of touching state;
Shutdown waits for the owner to exit before FlushAll.
Tests access state only through owner-side Inspect/WithState helpers
(harness + in-package). Fixed double-Harness.Run race in two e2e tests,
made TestWorkerPool_PriorityPreemption deterministic, and raised the
lazy-loading test timeout that was too short under -race.
go test -race ./... is now green.
- JNI: open_file_in_termux via ACTION_SEND intent (text/plain + file:// uri),
global context ref kept from registerFragment
- impl_android.go: OpenFile(path) attaches current thread if needed
- NewLogic takes openfunc; State.open + ui.OpenFile now func(string)
- ChunkedBuffer.VisibleByteRange: word-wrap path using
GlyphLayout.VisualLineStarts (+byteOffset, lineHeight, visual index param)
- GlyphLayout gains LineHeight; drawWrappedText records VisualLineStarts
- WordWrap default true; State.ByteOffset tracks first visible line
- types: VisualLineIndex
- scroll_fix_test.go (new)
- debug prints left in place (WIP; cleanup in later phase)
- Initialize browser path to the actual startup directory instead of '/'
- Improve '..' navigation to be relative to the filesystem root
- Fix path duplication issues by avoiding redundant absolute path joining
- 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
- 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.
- Changed BrowserScrollOffset and ListView.ScrollOffset from int (row index)
to ui.Dp (pixel offset) for smooth per-pixel scrolling
- Updated HandleBrowserScroll to use raw Dp delta instead of row-based
scrolling with minimum ±1 row jumps
- Fixed search not triggering new frames by detecting query changes in
main.go FrameEvent handler
- Fixed click misalignment by indexing RowFilenames with local index i
instead of rowGlobalIndex
- Fixed bottom clamping using actual BrowserListHeight and corrected
formula: maxScroll = totalRows * rowHeight - BrowserListHeight
- Renderer computes word wrap via single LayoutString pass with
WrapHeuristically policy; glyphs drawn inline at FlagLineBreak
- Fixed line height (fontSize * 1.2), independent of glyph metrics
- Two-finger trackpad scroll via gesture.Scroll with vertical axis
- Display line feedback: renderer reports last glyph Y after each
Draw; logic uses it to clamp scroll offset so last line stops
at bottom of viewport with lineHeight/2 padding
- ScrollRange fix: {Min: -(1<<30), Max: 1<<30} ensures scroll
delta is consumed (empty range consumes nothing via clampSplit)
- Line counting fix: only FlagLineBreak increments count; buffer
flushes (32-glyph cap) draw but don't count
- Fix render.go: replace broken material.Label with shaper-based text
rendering (LayoutString + Shape + glyph iteration) per Gio's paintGlyph
- Fix render.go: only apply clip rects at container boundaries; leaf
elements have zero-size regions that were clipping all text out
- Fix render.go: call r.drawElement recursively for container children
so nested containers work and clips are applied correctly
- Fix render.go: add drawLineText/drawLine helpers matching Gio's
paintGlyph offset calculation (x + first.X, y + first.Y)
- Fix element.go: Button.Draw now uses r.drawText instead of duplicate
broken material.Label code; Label defaults to black when color is unset
- Fix state.go: container children use relative coordinates instead of
mixed screen-space/container-relative positions
- Fix main.go: ConfigEvent carries raw pixel dimensions only; ScaleEvent
carries scale only; layout is computed once per frame using both,
eliminating the infinite Invalidate() loop
- Add Draw(gtx, r *Renderer) to Element interface
- Add Container type that implements Element and holds []Element children
- Leaf types (Label, Icon, Button) implement Draw themselves
- Single recursive drawElement function in Renderer — no switch/case
- Remove StatusBar/BottomBar — replaced by Container
- Remove shaper.go — no longer needed with Gio material labels
- Simplify main.go — scale via ScaleProvider interface
- 5 files changed, shaper.go deleted
- Remove Renderer.scale field and SetScale method
- Add ScaleProvider interface to avoid editor/ui import cycle
- Add Scale() method to State, rename Scale field to scale
- Renderer reads scale through r.scale.Scale()
- Fix icons.go blank import preventing embed.FS usage
- Update main.go to pass State as ScaleProvider to Renderer
- StateReader interface in ui package (Scale(), ScreenWidth(), ScreenHeight())
- State implements StateReader
- Renderer reads scale/window size via interface, never imports editor
- State fields are private (screenWidth, screenHeight, scale) with accessor methods
- No SetScale/SetWindowSize calls from main.go — Renderer reads from State directly
- ConfigEvent: ONLY from app.ConfigEvent, sets width/height in State
- FrameEvent: gets scale from gtx.Metric.PxPerDp, sends ScaleEvent only if changed
- ScaleEvent: updates State.Scale
- Renderer reads windowW/windowH from State (SetWindowSize), not gtx.Constraints
- Renderer reads scale from State (SetScale), not gtx.Metric.PxPerDp
- main.go: ConfigEvent calls SetWindowSize, FrameEvent calls SetScale
- State.Scale stores the scale factor (default 1.0)
- ScaleEvent applies SetScale() which updates State.Scale
- Logic.Scale() accessor returns current scale
- Renderer.SetScale() updates renderer's scale from State
- main.go: uses logic.Scale() to get scale, calls renderer.SetScale()
- No more references to gtx.Metric.PxPerDp in render.go or main.go
- ConfigUpdate interface with apply(*State) method
- ConfigEvent and ScaleEvent both implement ConfigUpdate
- Logic.ConfigChan() accepts both types
- Type switch in Run() differentiates ConfigEvent vs ScaleEvent
- main.go sends both on same ConfigChan()
- Logic layer has separate ConfigChan and ScaleChan
- ConfigEvent: width/height in Dp, set on ConfigEvent
- ScaleEvent: scale factor, set on FrameEvent when scale changes
- Logic has ScreenSize() accessor for layout/rendering pipeline
- Logic has SetScale() to update scale and recompute layout
- main.go: ConfigEvent stores pixels, converts to DP using current scale
- main.go: FrameEvent gets actual scale, sends ScaleEvent if changed
The op.Record macro was leaving a clip on the stack that prevented
subsequent elements from drawing. Use clip.Rect{}.Push()/clip.Pop()
instead for immediate push/pop.
- 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
- Create internal/editor/state.go with State struct and EditorLayout
- Create internal/editor/logic.go with Logic struct and Run method
- Logic owns all state, provides channels for config, frames, input, results
- Main goroutine uses editor.NewLogic() and logic.Run()
- Proper separation for testing later
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Elements implemented:
- StatusBar (top bar with filename, action icons, conflict/search icons)
- BottomBar (bottom bar with cursor position, byte position, word wrap button)
- Button (interactive button element)
- AlphaIndex, SearchBar, Cursor, MergeHunk, Toast, Spacer (stubs)
Layout:
- EditorLayout function computes regions for StatusBar and BottomBar
- Filename truncation with ellipsis (mocked filename for testing)
- Mocked data: "very_long_filename...", "Ln 47, Col 12", "1024 / 50000"
Renderer:
- drawStatusBar: draws filename line + icon line
- drawBottomBar: draws cursor pos, byte pos, word wrap button
- drawButton: draws button text
- drawIconButton: draws icon buttons
Window resize:
- configChan setup in main.go
- ConfigEvent handling in event loop
- Layout recomputation on resize
Builds and runs successfully.
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
- Element interface with Region() and Visible() methods
- Label and ListView element types with constructors
- Renderer that clips and draws elements in slice order
- Main app loop with Gioui window and frame events
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>