Commit Graph

80 Commits

Author SHA1 Message Date
eb725ee781 Fix word-wrap toggle: actually stop wrapping when disabled
The bottom-bar 'Wrap: On/Off' toggle flipped State.WordWrap and relabelled
itself, but the render path never read the flag: NewTextField hardcoded
WordWrap: true and drawWrappedText always shaped at the region width with
WrapHeuristically, so lines wrapped in both modes.

NewTextField now takes the wordWrap flag, TextField.Draw passes tf.WordWrap
to drawWrappedText, and with wrap disabled the shaper gets unlimited width
(MaxWidth = maxInt32, as in single-line layout) so over-long lines extend
past the region and are clipped instead of wrapping. EditorLayout passes
its existing wordWrap parameter through.
2026-08-19 22:35:13 -04:00
d8b5bc704b Handle drags: 1:1 finger tracking with cross-flip; fix caret/taps on empty lines
Selection handles now track the finger 1:1 (anchor grab point + displacement)
instead of snapping by whole lines, and crossing the opposite handle flips
the selection (native behaviour) instead of clearing it.

Caret and tap/handle line resolution use VisualLineStarts instead of the
min-Y baseline: the window's first visual line may be an empty line with no
recorded glyphs, which used to draw boundary carets one line too low per
leading empty line and land taps/dragged handles one line below the finger.
New exported ui.CaretPoint centralises byte->insertion-point mapping.

The off-screen caret no longer clamps to the window edge: EditorLayout ships
the true (possibly negative / past-end) window-relative cursor and the
renderer skips the caret when the cursor is outside the shaped window, so
scrolling past the caret no longer makes it jump onto the top/bottom line.

IME/router replay fixes: key.FocusCmd is issued only on a focus transition
(a per-frame no-op still takes the immediate-command path and re-queues all
pointer events), and the key.SelectionCmd IME sync is deferred while a
handle drag is in progress (each push re-injected the drag into every
gesture). Handle drags forward only Grabbed events; a tap inside a handle
grab box is a no-op.

Also: key.FocusEvent no longer logs as unexpected in main; dead code removed
(worker taskWrapper, browser applyXxxResult stubs, scrollIndex, mock_setup
sortModeKey/lineSpan helpers); mock FileSystem.ListPaths prefix match uses
strings.HasPrefix; build scripts run the new scripts/check.sh static gate
(go vet + staticcheck). Tests: caret_point_test, touch_selection updates
(flip/empty-line cases), off-window caret e2e, selection drag e2e grab step.
2026-08-19 22:34:12 -04:00
275a78efaa Fix selection-handle drags, menu anchoring, and left-edge back-gesture theft
Three user-reported selection bugs, one root cause each:

1. Start handle ungrabbable at line start. Two interacting causes:
   a) The 48dp grab box straddles two visual lines; a finger in the
      lower half mapped (by y-to-line) to the neighbouring line, whose
      byte past the other handle clamped to a zero-length selection ->
      cleared on the first drag event. The cleared selection
      un-registered the drag op, so the router silently stopped
      delivering drag events (the observed 'stream cutoff'). Fix:
      handle drags now project the finger's x onto the anchor's own
      visual line (visualLineOfByte + textPosOnLineAtX); the anchor
      never crosses lines during a handle drag.
   b) A horizontal flick from the line-start handle (screen x~26px)
      started the system back gesture, which cancelled the touch
      stream. Fix: report the handle grab rects as system gesture
      exclusion rects (setSystemGestureExclusionRects, API 29+),
      marshalled to the UI thread via a PadExcl smali Runnable
      (generated identically by build_emu.sh/build_phone.sh).

2. End-handle drag downward made the menu chase the finger and cover
   the selection. Fix: the menu anchors to the STABLE end of the
   selection (the end not being dragged), so it stays parked by the
   selection start, clear of the finger and the highlighted text.

3. Menu above the selection vanished permanently when the selection
   was extended onto the top line. Fix: off-window anchors no longer
   hide the menu while any part of the selection is visible (keep-last
   rect, clamped); hiding happens only for fully off-window selections.

Also: registerDrag simplified (single shared drag path, body before
handles in z-order), debug logging removed, regression tests
(mutation-verified) for line projection and menu anchoring, docs
section 17. Verified on device: start-handle drag shrinks the word
without clearing or triggering back navigation; end-handle vertical
drag leaves menu/highlight/handles undisturbed; menu stays visible
with the selection at the top line.
2026-08-18 13:09:29 -04:00
dc35aa2e36 Redesign selection handles (teardrop + 48dp grab); place menu above selection
- Handles are now teardrops (stem + filled circle) like the native
  Android selector: 20dp circle normally, 28dp while dragging.
- The grab region is a 48dp box around the circle centre, independent
  of the visual size; the old 16dp target was not grabbable by finger.
- The copy/cut/paste menu is placed ABOVE the selected line (native
  behaviour), flipping below only when there is no room above. The menu
  is drawn last (top of the z-order) and Gio routes a touch to the
  topmost op whose clip contains it, so a below-placed menu covered the
  handles' grab boxes and silently stole every handle-drag press.
- Regressions: menu above-placement arithmetic, first-line flip-below,
  and the existing tracking/clamp tests updated for the new policy.
- On-device verified: handle drags work with the menu up (previously
  dead lower grab region), first-line handles still grabbable from the
  uncovered top strip, menu taps and tap-to-clear unchanged.
- doc/development_plan.md section 15 records the z-order/placement
  contract.
2026-08-18 08:08:51 -04:00
941285e7cc Fix scroll-anchored selection (shadowing) + IME insets keyboard handling
Two user-reported bugs in the post-wrap build:

1. Selection 'jumped' when scrolling: frameOf's chunked branch shadowed the
   outer start/end with 'start, end, winLine := cb.VisibleByteRange(...)',
   leaving IMEWindowStartByte at 0 for chunked files while scrolled, so the
   window-relative selection highlight (and IME commits) mis-mapped near the
   file top. Fix: declare winLine outside, assign. Mutation-verified
   regression tests: real_file_scroll_selection_test.go.

2. Bottom bar hidden behind the keyboard: GioView extends SurfaceView, which
   unconditionally marks the window FORMAT_TRANSLUCENT; translucent windows
   are never IME-resized, so adjustResize is dead. Fix (build scripts):
   smali-patch a PadInsetsListener OnApplyWindowInsetsListener onto the
   GioView (shrinks it by the IME inset bottom) + setDecorFitsSystemWindows
   (false) on API 30+ so insets are dispatched. targetSdk kept at 34.

3. (Found while fixing 2) Keyboard could not be dismissed: TextField.Draw
   issued SoftKeyboardCmd{Show:true} every frame; with insets-driven
   resizes, the hide animation triggered redraws that re-showed the keyboard
   mid-animation. Fix: ShowIMESeq pulse from the logic layer (open/tap/
   double-tap); renderer shows only on pulse change, re-arming on focus
   loss — matching widget.Editor.

On-device (emulator): BACK dismisses and stays down; tap re-shows; typing
works; bottom bar above keyboard; word selection anchored across flick
scroll. go test -race ./... green. Phone APK rebuilt with the same patch.
2026-08-17 21:52:53 -04:00
83f7affee9 Fix word-wrap scroll jump: visual-line mapping via WrapIndex
Every scroll<->content mapping site (window start, sub-line shift, tap
mapping, max-scroll clamp, selection menu/handle positions) assumed
1 logical line = 1 visual line. When the viewport top crossed the
bottom of a wrapped line, the view jumped past the wrapped remainder
(jump magnitude (count-1)*lh) instead of moving pixel-by-pixel.

- WrapIndex (internal/editor/wrap_index.go): Fenwick tree of
  per-logical-line visual-line counts, parallel to the LineIndex;
  built at index-build time, bookkept by the same
  UpdateLineIndexAfter{Insert,Delete} hooks (never under-stale: every
  touched line resets to the estimate, the next shaping pass
  re-corrects it).
- scrollVisualDecompose: the scroll offset lives in visual-line space:
  k = LineForVisual(floor(s/lh)), r = s - V(k)*lh. All mapping sites
  go through it, so the viewport top is always exactly s into the
  document's visual space (V(k)*lh + r = s) — the jump invariant.
  All-ones index reduces to the legacy 1:1 mapping (pre-shaping and
  non-wrapped behavior unchanged by construction).
- Correction pipeline: the renderer's per-frame VisualLineStarts are
  grouped per logical line and written back (applyWrapCounts). The
  layout feedback now carries the exact window text the layout was
  shaped for (carried in the frame) plus the window start line and the
  content-edit counter; corrections apply only on edit-counter match,
  and grouping over the current window text (wrong after a scroll moved
  the window) is no longer possible.
- bytePosToScreenXY now applies the sub-line shift and the scaled line
  pitch: the selection menu/handles were off by up to a full line.
- maxScroll uses TotalVisuals() with the effective (font-scaled) line
  height; the bottom clamp lands exactly on the file end for wrapped
  content.
- VisibleByteRange returns the real start line (was hardcoded 0).
- emitFrame: replace the unread handoff frame with the newer snapshot
  instead of dropping it — a dropped final frame was never re-emitted
  (emission is event-driven), leaving the consumer one state behind
  forever; fixes the pre-existing TestRealFile_ShiftSelectionInsert
  failure. Still non-blocking.

Tests (mutation-verified where practical): wrap_index_test.go (Fenwick
vs naive model, 3000 ops), wrap_bookkeeping_test.go (edit hooks vs
shadow-string oracle, 400 ops — caught a real m=0 under-marking),
wrap_mapping_test.go (the jump regression: V(k)*lh + r == s over sweeps
+ random offsets; legacy-identity pin; boundary sweep), wrap_apply_test.go
(VisualLineStarts grouping + guards — the first version exposed the
always-true WindowStartByte guard that blocked all post-scroll
corrections). go test -race ./... green.

On-device (emulator, 60 wrapped lines): dp sweep 0/17/50/67/134/340
lands on LINE000-vl0/1/3, LINE001-vl0, LINE002-vl0, LINE005-vl0 —
pixel-exact 1:1, no jump (dp 134 is where the old code jumped to
LINE008); bottom clamp exact.

Docs: architecture.md §6.2 (visual-line space invariant),
development_plan.md (Phase 13), spec.md (wrap + clamp lines).
2026-08-17 19:46:47 -04:00
b24aa26446 Track Android user font scale in all line-height geometry (Phase 11)
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.
2026-08-17 10:59:34 -04:00
2a16c0017c Touch selection (v1): long-press/double-tap word selection, drag handles, floating copy/cut/paste menu
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.
2026-08-17 08:57:55 -04:00
ec11abf8f1 Add text selection, real-file e2e tests, and Android arrow-key support
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.
2026-08-17 00:32:53 -04:00
e11d705196 chore: remove per-event debug logging from the normal path
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.
2026-08-16 20:58:49 -04:00
46383c1fc1 ui: dedup IME snippet/selection to fix rapid-commit desync
TextField.Draw re-pushed key.SnippetCmd and key.SelectionCmd every frame
while focused. Re-pushing an unchanged snippet resets the IME's composition
and caret, so commits arriving faster than a frame interleaved with those
resets and desynced the cursor (garbled/duplicated text).

Mirror widget.Editor's updateSnippet/selection gating: track the last-pushed
snippet and caret in the main-owned Renderer (keyed by field ID, reset on
focus (re)gain), and only emit the ops when they actually change. A fresh
push is forced whenever the field (re)gains focus so the IME always starts
from a known state. Also drop a leftover per-frame 'Focused:' debug print.

On-device: rapid back-to-back commits (0.08-0.1s cadence, faster than a
frame) now land cleanly - type HELLO, append+backspace, and a
browser->editor navigation round-trip all produce exact content.
go test -race ./... green.
2026-08-16 13:00:25 -04:00
72b3c3f9c1 editor+ui: complete Android IME wiring (SelectionCmd, SnippetCmd, InputHintOp)
Finish Phase 1 items 1, 2, 4, on top of the range-handling done in 9b78219:

- TextField.Draw now emits, when focused:
  * key.InputHintOp{HintText}        (item 4: enable text keyboard/autocorrect)
  * key.SnippetCmd (the visible window as the snippet, Range {0,len})
                                              (item 2: swipe/autocorrect source)
  * key.SelectionCmd (caret, window-relative rune index)
                                              (item 1: IME selection sync)
  The snippet is the visible window (not the whole file), so the IME treats
  the window as the document and reports EditEvent.Range window-relative.

- HandleReplaceRange now resolves the window-relative range against
  IMEWindowText and offsets by IMEWindowStartByte to address the buffer
  (string and chunked paths). Falls back to the whole buffer when layout
  has not set the window (tests).

- EditorState gains IMEWindowStartByte / IMEWindowText, set during layout.

- Add runeCount helper (utf8 leading-byte scan) in the ui package.

- Fix a data race in three editor e2e/integration tests: they called
  OpenFile from the test goroutine while the logic goroutine ran layout;
  now wrapped in withState so the state write happens on the owner.

Tests: ime_range_test.go gains windowed-path coverage (string+chunked).
go build, go test, go vet, and go test -race are all green.
2026-08-16 02:43:58 -04:00
58725a5e6c Make state single-owner and stabilize race detector
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.
2026-08-16 01:32:27 -04:00
7240b62a61 WIP baseline: Termux open-file bridge + word-wrap-aware viewport/scroll
- 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)
2026-08-16 00:31:26 -04:00
93a5f879f5 feat: implement atomic writes for filesystem backend
- 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)
2026-06-04 18:01:51 -04:00
d090447f2d Show soft keyboard on editor focus 2026-06-03 22:31:49 -04:00
bab239a38a Implement click-to-move cursor in the editor 2026-06-03 22:04:16 -04:00
62d1f827e0 Fix cursor rendering lag and implement Enter key newline insertion 2026-06-03 20:19:35 -04:00
d33e68ab2c fix(editor): fix cursor positioning, scrolling, and clipping
- Fix cursor vertical alignment and ensure it respects ScrollOffset.
- Fix scrolling in editor page by correctly registering scroll interactions in the renderer.
- Ensure cursor is clipped to the editor text area to prevent drawing over status bars.
2026-06-03 19:19:14 -04:00
a5d1c4bee6 Compute glyphLayout after text rendering. Implement correct cursor
movement.
2026-06-03 19:01:05 -04:00
0dac354f2a fix(editor): wire key events to cursor movement
- 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.
2026-06-03 07:41:18 -04:00
57a185fa36 Fix bugs in e2e tests and remove list item selection highlight 2026-06-02 13:30:09 -04:00
2c3702f421 fix(browser): fix directory navigation and entry tapping 2026-06-02 08:14:59 -04:00
dc80c8a8e8 Remove scroll debug statements. Add one extra element to browser listview
so that we have a partial item displayed at the bottom and not a blank space.
2026-06-01 11:58:57 -04:00
6709f52e0f Fix browser page scrolling by initializing EntryHeight and correcting scroll handler type assertion 2026-06-01 10:57:34 -04:00
ce8dd82bb0 Remove duplicate browser state and fix sort mode toggle
- Remove currentBrowserState global and SetBrowserState from browser package
- Browser now receives sortHandler as a parameter instead of using global state
- Fix sort mode toggle: reload pages after clearing cache (was showing 'Loading...')
- Add String() methods to all UI element types for debugging
- Add e2e tests for sort mode functionality (5 tests)

The editor now owns all state; the browser is a pure function receiving
state and handlers through parameters.
2026-05-31 23:20:21 +00:00
8f185341b3 Implement per-pixel smooth scrolling for ListView
- 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
2026-05-28 16:14:11 -04:00
23fdeabad4 Fix scroll clipping: ListView now clips scroll gestures to its region
- 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)
2026-05-28 14:56:12 -04:00
ad3049dd22 Browser page: scroll + click coexist via register order fix
- Added Page state (BrowserPage/EditorPage) and SortMode
- BrowserLayout with 30 static entries, sort toggle, search placeholder
- ListView rows clickable via per-row RegisterClick
- Scroll gesture registered at START of ListView.Draw (before clicks)
  so click gestures registered later are checked first during hit test
- Unique IDs for interactive elements: editor_text, browser_list, search_bar
- ListView.Interactions() filters out Scroll (registered in Draw, not registerInteraction)
- RegisterScroll added to Renderer, called from element Draw methods
- lastLineYChan frame loop fixed: only re-layout if value changed
- Back icon added, editor status bar shows active filename
2026-05-27 12:14:25 -04:00
0246316d8f Add word-wrapped text display with two-finger scroll
- 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
2026-05-26 20:26:20 -04:00
1596dab72d Initial InputEvent handling. Clickable "Wrap" text label. 2026-05-26 09:46:35 -04:00
50e9ca5b34 Updates to rendering pipeline and documentation. 2026-05-25 17:40:35 -04:00
22c1d5bed1 fix: correct text layout and rendering pipeline
- 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
2026-05-18 21:03:22 -04:00
8739bae517 refactor: unified Element interface with Draw method and Container type
- 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
2026-05-18 11:25:43 -04:00
93a7a0a0b2 fix: remove duplicate scale storage, read from State via ScaleProvider interface
- 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
2026-05-17 10:59:29 -04:00
fa43bf2402 Add ShapeText/DrawShapedText: single shaper call, zero-call drawing
- New shaper.go: ShapeText() returns ShapedText with measured width +
  stored PathSpec + BitmapsCall, DrawShapedText() draws with zero shaper calls
- drawStatusBar: ShapeText for filename + right icons, positions from widths
- drawBottomBar: ShapeText for cursor/bytepos/wrap, positions by alignment
- No separate measureText helper: ShapeText.Width provides measurements
- StatusBar uses IconLeft/IconRow string slices for icon configuration
- BottomBar uses BarAlignment enum for item positioning
2026-05-12 12:06:28 -04:00
5da60ac9b3 Fix Y positioning. Expand status bar height. 2026-05-10 20:12:35 -04:00
dc4962a6c4 Fix icon Y positioning: offset by icon height to align bottom with baseline
- drawPng uses top-left corner, drawText uses baseline
- Icon bottom (Y=42) now aligns with baseline at Y=42
- Icon top is at Y=26 (42-16), occupies Y=26 to Y=42
2026-05-10 07:32:15 -04:00
1416e2db26 Move icon Y position from +20 to +32 DP below filename
- Icon was overlapping the first row of status bar
- Increased vertical spacing to avoid overlap
2026-05-10 07:14:20 -04:00
fdd1e841ae Use original iconY position (30 DP) without offset
- User confirmed icon showed up at this position
- Icon renders in second row of status bar as intended
2026-05-10 07:12:49 -04:00
28c7253622 Fix icon path and Y positioning
- Read icons/cut.png (not cut.png) to match embed directive
- Offset icon Y by iconSize so bottom aligns with text baseline
- drawText uses baseline positioning, drawPng uses top-left corner
2026-05-10 07:10:02 -04:00
f259145084 Fix icon Y positioning: offset by icon height to align bottom with baseline
- drawText positions glyphs by baseline, drawPng uses top-left corner
- Subtract iconSize from iconY so icon bottom aligns with text baseline
- Icon now renders within status bar instead of overlapping top
2026-05-10 07:03:43 -04:00
0ac2941802 Replace unicode cut icon with PNG icon in StatusBar
- Load cut.png from embedded filesystem
- drawStatusBar uses drawPng() for cut icon (16dp)
- No other files modified
2026-05-10 06:52:54 -04:00
106e91832a Add drawPng function to Renderer for PNG icon rendering
- Takes image.Image, position (x,y) in Dp, size (width,height) in Dp
- Tint color with alpha < 255 applies color overlay using clip.Rect
- Converts Dp to pixels using Renderer.toPx()
- Uses paint.NewImageOp for image rendering
2026-05-10 06:50:03 -04:00
fe3e7c3d75 Revert to last working state (68437c3) before PNG icon changes 2026-05-10 06:45:47 -04:00
1b1cf51528 Remove search/conflict icon references — only cut/copy/paste exist
- StatusBar now only draws cut, copy, paste PNG icons
- Removed code that tried to draw non-existent search.png and conflict.png
2026-05-10 06:39:22 -04:00
d2509123b0 Replace Unicode icons with PNG icons in StatusBar
- Move icons/ to internal/ui/icons/ for go:embed
- Renderer.loadIcons() loads PNGs from embedded filesystem
- drawStatusBar() uses drawPng() for cut/copy/paste/search/conflict icons
- Icons are 16dp, rendered from 48x48 PNGs (2x density)
2026-05-10 06:29:19 -04:00
14284afa73 Add drawPng function to Renderer for PNG icon rendering
- Takes image.Image, position (x,y) in Dp, size (width,height) in Dp
- Tint color with alpha < 255 applies color overlay using clip.Rect
- Converts Dp to pixels using Renderer.toPx()
- Uses paint.NewImageOp for image rendering
2026-05-10 06:26:29 -04:00
620d534454 Break import cycle: use StateReader interface instead of direct State reference
- 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
2026-05-09 20:33:00 -04:00
21b4df59a3 Fix event flow: ConfigEvent sets size, FrameEvent sets scale only
- 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
2026-05-09 20:26:10 -04:00