Skip to content

GTK editor click lands 2–3 columns left of the clicked glyph — vimcode re-derives quadraui's text-layout inverse (needs quadraui hit-test API) #560

Description

@JDonaghy

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

  1. Open any file in the GTK build.
  2. Click on a character partway along a line (further right = more noticeable).
  3. Observe: the cursor lands ~2–3 columns to the left of the clicked glyph.
  4. 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:

// quadraui gtk/editor.rs
let gutter_width  = editor.gutter_char_width as f64 * char_width;
let text_x_offset = rect.x + gutter_width - h_scroll_offset;
...
layout.set_text(&rl.raw_text);
let attrs = build_pango_attrs(&rl.spans);   // fg/bg + BOLD + ITALIC + font_scale
layout.set_attributes(Some(&attrs));
cr.move_to(text_x_offset, y);
pangocairo::show_layout(cr, layout);        // ← glyphs positioned by THIS layout

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:

// src/gtk/click.rs (WindowZone::TextArea)
pango_layout.set_text(raw_text);
pango_layout.set_attributes(None);                 // ← NO attributes (paint has them)
let scroll_px = rw.scroll_left as f64 * char_width;
let x_pango = ((text_rel_x + scroll_px).max(0.0) * pango::SCALE as f64) as i32;
let (_inside, byte_index, _trailing) = pango_layout.xy_to_index(x_pango, 0);
let clamped = (byte_index as usize).min(raw_text.len());
let col = raw_text[..clamped].chars().count() + seg_col_offset;

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:

  1. Gutter origingutter_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.
  2. Horizontal scrollscroll_left * char_width recomputed here vs paint's h_scroll_offset, same char-width-source hazard.
  3. Glyph advancesxy_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 as usize + 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, same build_pango_attrs(&rl.spans) attributes, and same text_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

Acceptance criteria

  • 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    blockedBlocked on a prereq (linked in body)bugSomething isn't workingcoordTracked by coord-tui pipelinestatus:readyRefined and ready to enter the work pipelineuiUI/rendering

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions