You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In the GTK backend, clicking on a character in the editor places the cursor/selection 2–3 characters to the left of the character under the pointer. The TUI backend is unaffected — its click lands on the correct character.
This is not a bug to patch with per-backend nudge math in src/gtk/. The true cause is architectural: vimcode's GTK backend re-derives the inverse of quadraui's editor text layout by hand, independently of the rasteriser that actually draws the glyphs. The two computations must agree pixel-for-pixel but are built differently, so they drift. Per the Platform-Neutrality Rule, the fix belongs in quadraui (own the hit-test), not in vimcode.
Reproduction
Open any file in the GTK build.
Click on a character partway along a line (further right = more noticeable).
Observe: the cursor lands ~2–3 columns to the left of the clicked glyph.
Repeat in the TUI build — cursor lands correctly.
Root cause — vimcode hand-rolls quadraui's layout inverse
Glyph positions are owned by quadraui's editor rasteriser, quadraui/src/gtk/editor.rs::draw_editor. It lays out each line with a Pango layout carrying the syntax/style attributes and draws it at a computed origin:
But the click → column mapping is done back in vimcode, in src/gtk/click.rs::pixel_to_click_target (the WindowZone::TextArea arm), by building a separate Pango layout and running xy_to_index on it:
text_rel_x itself is reconstructed in render::window_zone_hit_test as rel_x - (gutter_char_width * char_width).
So the clicked column is recovered by three independent reconstructions of what the rasteriser already computed:
Gutter origin — gutter_char_width * char_width recomputed here (uses self.cached_char_width), vs paint's text_x_offset (uses cw = cached_char_width.max(backend.char_width()) in render_content). A metric divergence is multiplied by gutter_char_width (~5–7), so a sub-pixel char-width difference becomes a multi-character gutter-width error → a roughly constant column shift. This matches the "constant 2–3 chars" symptom.
Horizontal scroll — scroll_left * char_width recomputed here vs paint's h_scroll_offset, same char-width-source hazard.
Glyph advances — xy_to_index runs on an attribute-less layout, while paint positions glyphs with build_pango_attrs(&rl.spans) (bold/italic/font_scale change advances). Wherever a span is bold or scaled, the two layouts disagree and the mapping drifts.
Each of these is a place the click math can (and does) diverge from the draw math. The bug is the sum of them.
Why TUI is immune
TUI's rasteriser draws on a fixed monospace cell grid, so its inverse is a trivial integer division with no font metrics involved (src/tui_main/mouse.rs):
let col_in_text = text_rel_x asusize + rw.scroll_left + seg_col_offset;
There is nothing to drift — char_width is 1.0, there is no Pango layout, no attribute set, no sub-pixel origin. That's why only GTK is wrong.
Why this is a Platform-Neutrality violation (not a GTK bugfix)
Per CLAUDE.md: "Before writing any code in a backend file, compare against the relevant quadraui example. If the example achieves the same feature with zero backend-specific code, your approach is wrong."
The editor hit-test is currently ~15 lines of GTK-specific Pango/metric juggling in src/gtk/click.rs that exists only to invert a layout quadraui already owns. Patching the offset in vimcode (subtracting a fudge factor, switching char-width source, etc.) would be exactly the kind of per-backend band-aid the rule forbids — and it would still drift on bold/scaled spans because the two layouts remain independent.
Proposed platform-neutral fix
quadraui must own the inverse of its own editor text layout, so paint and hit-test share one source of truth:
Add a quadraui API — e.g. Editor::col_at_x(view_row, x_px) -> col (or a Backend::editor_hit_test / a FrameHitMap entry, per Migrate GTK click dispatch to FrameHitMap #449) — that internally uses the same Pango layout, samebuild_pango_attrs(&rl.spans) attributes, and sametext_x_offset/gutter/scroll math as draw_editor. One layout, computed once, inverted for the click.
vimcode then calls that shared API from both backends in 1–3 lines of wiring:
GTK: pass the pixel x.
TUI: the cell-grid inverse (the integer division it already does) is provided by the same quadraui API, so the two backends can never diverge again.
Delete the bespoke pango_layout.xy_to_index(...) block from src/gtk/click.rs and the ad-hoc text_rel_x pixel reconstruction.
Prerequisite (quadraui supply side)
This needs a quadraui infrastructure change first, and per the workflow this repo must not edit ~/src/quadraui directly. A companion issue must be filed on JDonaghy/quadraui describing the gap ("editor rasteriser owns paint but not its hit-test inverse; expose col_at_x backed by the same layout"), and this issue stays blocked on it. (Aligns with milestone #5 = quadraui build/supply side; this issue is the #7 consume side.)
Relationship to existing issues
Migrate GTK click dispatch to FrameHitMap #449 — Migrate GTK click dispatch to FrameHitMap: this is the umbrella; the editor text-column mapping is the concrete sub-case that actually mis-selects characters. The col_at_x inverse could be delivered as part of the FrameHitMap work or land first as a narrower API.
Clicking a glyph in the GTK editor selects that glyph's column (no left/right drift), including on lines with bold / italic / font_scale (e.g. markdown headings) spans and with horizontal scroll applied.
The click→column mapping is provided by a shared quadraui API driven by the same layout as draw_editor; the bespoke xy_to_index / text_rel_x block is removed from src/gtk/click.rs.
TUI click behaviour is unchanged (routed through the same shared API).
No per-backend offset/fudge constants introduced in src/gtk/.
Blocked on the companion JDonaghy/quadraui issue landing.
Summary
In the GTK backend, clicking on a character in the editor places the cursor/selection 2–3 characters to the left of the character under the pointer. The TUI backend is unaffected — its click lands on the correct character.
This is not a bug to patch with per-backend nudge math in
src/gtk/. The true cause is architectural: vimcode's GTK backend re-derives the inverse of quadraui's editor text layout by hand, independently of the rasteriser that actually draws the glyphs. The two computations must agree pixel-for-pixel but are built differently, so they drift. Per the Platform-Neutrality Rule, the fix belongs in quadraui (own the hit-test), not in vimcode.Reproduction
Root cause — vimcode hand-rolls quadraui's layout inverse
Glyph positions are owned by quadraui's editor rasteriser,
quadraui/src/gtk/editor.rs::draw_editor. It lays out each line with a Pango layout carrying the syntax/style attributes and draws it at a computed origin:But the click → column mapping is done back in vimcode, in
src/gtk/click.rs::pixel_to_click_target(theWindowZone::TextAreaarm), by building a separate Pango layout and runningxy_to_indexon it:text_rel_xitself is reconstructed inrender::window_zone_hit_testasrel_x - (gutter_char_width * char_width).So the clicked column is recovered by three independent reconstructions of what the rasteriser already computed:
gutter_char_width * char_widthrecomputed here (usesself.cached_char_width), vs paint'stext_x_offset(usescw = cached_char_width.max(backend.char_width())inrender_content). A metric divergence is multiplied bygutter_char_width(~5–7), so a sub-pixel char-width difference becomes a multi-character gutter-width error → a roughly constant column shift. This matches the "constant 2–3 chars" symptom.scroll_left * char_widthrecomputed here vs paint'sh_scroll_offset, same char-width-source hazard.xy_to_indexruns on an attribute-less layout, while paint positions glyphs withbuild_pango_attrs(&rl.spans)(bold/italic/font_scalechange advances). Wherever a span is bold or scaled, the two layouts disagree and the mapping drifts.Each of these is a place the click math can (and does) diverge from the draw math. The bug is the sum of them.
Why TUI is immune
TUI's rasteriser draws on a fixed monospace cell grid, so its inverse is a trivial integer division with no font metrics involved (
src/tui_main/mouse.rs):There is nothing to drift —
char_widthis1.0, there is no Pango layout, no attribute set, no sub-pixel origin. That's why only GTK is wrong.Why this is a Platform-Neutrality violation (not a GTK bugfix)
Per
CLAUDE.md: "Before writing any code in a backend file, compare against the relevant quadraui example. If the example achieves the same feature with zero backend-specific code, your approach is wrong."The editor hit-test is currently ~15 lines of GTK-specific Pango/metric juggling in
src/gtk/click.rsthat exists only to invert a layout quadraui already owns. Patching the offset in vimcode (subtracting a fudge factor, switching char-width source, etc.) would be exactly the kind of per-backend band-aid the rule forbids — and it would still drift on bold/scaled spans because the two layouts remain independent.Proposed platform-neutral fix
quadraui must own the inverse of its own editor text layout, so paint and hit-test share one source of truth:
Editor::col_at_x(view_row, x_px) -> col(or aBackend::editor_hit_test/ aFrameHitMapentry, per Migrate GTK click dispatch to FrameHitMap #449) — that internally uses the same Pango layout, samebuild_pango_attrs(&rl.spans)attributes, and sametext_x_offset/gutter/scroll math asdraw_editor. One layout, computed once, inverted for the click.x.pango_layout.xy_to_index(...)block fromsrc/gtk/click.rsand the ad-hoctext_rel_xpixel reconstruction.Prerequisite (quadraui supply side)
This needs a quadraui infrastructure change first, and per the workflow this repo must not edit
~/src/quadrauidirectly. A companion issue must be filed onJDonaghy/quadrauidescribing the gap ("editor rasteriser owns paint but not its hit-test inverse; exposecol_at_xbacked by the same layout"), and this issue stays blocked on it. (Aligns with milestone #5 = quadraui build/supply side; this issue is the #7 consume side.)Relationship to existing issues
FrameHitMap: this is the umbrella; the editor text-column mapping is the concrete sub-case that actually mis-selects characters. Thecol_at_xinverse could be delivered as part of theFrameHitMapwork or land first as a narrower API.Acceptance criteria
font_scale(e.g. markdown headings) spans and with horizontal scroll applied.draw_editor; the bespokexy_to_index/text_rel_xblock is removed fromsrc/gtk/click.rs.src/gtk/.JDonaghy/quadrauiissue landing.