Skip to content

Fix IME text doubling and baseline jitter with glyph-level vertical offsets#601

Open
Cupnfish wants to merge 2 commits intolinebender:mainfrom
Cupnfish:fix/ime-glyph-y-offset
Open

Fix IME text doubling and baseline jitter with glyph-level vertical offsets#601
Cupnfish wants to merge 2 commits intolinebender:mainfrom
Cupnfish:fix/ime-glyph-y-offset

Conversation

@Cupnfish
Copy link
Copy Markdown

@Cupnfish Cupnfish commented Apr 10, 2026

Summary

Fixes two issues with IME input and text layout stability:

1. IME text doubling on Windows (commit_compose)

On Windows (and possibly other platforms), Ime::Commit arrives while the preedit text is still in the buffer, before Ime::Preedit("") clears it. The existing insert_or_replace_selection inserts the committed text in addition to the preedit, causing a doubled-text flash. The new commit_compose method atomically replaces the preedit with the committed text.

2. Baseline jitter from font fallback (glyph-level vertical offsets)

When mixing scripts on the same line (e.g., typing English after Chinese), font fallback changes line metrics between layout rebuilds, causing visible vertical jitter. This PR introduces glyph-level vertical offset stabilization:

  • Before each layout rebuild, per-line baselines are snapshotted into a reusable scratch Vec<f32>
  • After rebuild, the delta between old and new baselines is computed per line
  • The delta is applied directly to glyph y-coordinates in layout.data.glyphs (for array glyphs) and to line.metrics.baseline (for inline glyphs and cursor/selection consistency)
  • A baseline_snapshots scratch Vec<f32> is reused across updates to avoid per-update heap allocation

Performance

The stabilization adds minimal overhead:

Operation Cost When
Baseline snapshot O(lines) float writes Every layout update
Delta comparison O(lines) float subtractions Every layout update
Glyph y-offset modification O(changed glyphs) Only lines with significant baseline shift
Scratch Vec allocation Zero (capacity reused) After first layout

For context, the layout build itself is O(text length) and involves expensive font shaping, BiDi processing, and line breaking. The stabilization overhead (O(lines) float operations) is negligible in comparison, even for documents with tens of thousands of lines.

The stabilization is also gated: it skips entirely when there are no previous lines to compare against (e.g., first layout or after buffer clear).

Comparison with PR #599

PR #599 (branch fix/ime-windows-commit) solves the same two problems with a different stabilization strategy. Here is a detailed comparison:

Shared parts

Both PRs include the identical commit_compose method for the IME text doubling fix.

Difference: baseline stabilization approach

Aspect PR #599 (line-level snapshot/restore) This PR (glyph-level vertical offset)
What is snapshotted Full LineVerticalSnapshot (baseline, min_coord, max_coord, line_height) Only baseline per line (Vec<f32>)
How stabilization works Restores the entire old LineMetrics struct, overriding line_height, leading, etc. Computes a baseline delta and applies it to individual glyph y values + line position
line_height / leading Restored to old values Preserved from the layout engine (reflects actual font metrics)
ascent / descent Not modified (kept from new layout) Not modified (kept from new layout)
Inline glyphs (y=0, positioned purely by baseline) Handled via full metric restore Handled via baseline adjustment
Array glyphs (with y-offsets from shaping) Shifted implicitly via baseline restore Shifted explicitly by modifying glyph.y in layout data
Scratch allocation Vec<LineVerticalSnapshot> (~16 bytes/line) Vec<f32> (~4 bytes/line)

Advantages of this approach

  • Preserves real font metrics: line_height and leading remain as computed by the layout engine, so the line box size reflects the actual content. PR Fix IME text doubling on Windows and baseline jitter during composition #599 restores old values, which can cause line boxes to be too small (or too large) when font fallback genuinely changes metrics.
  • Lighter snapshot: Only one f32 per line vs four fields per line. Less memory, fewer writes.
  • Separation of concerns: Position stabilization (delta applied to glyphs) is distinct from metric computation (left to the layout engine). The layout engine's output is respected for everything except the final vertical position.
  • Inline glyph correctness: Inline glyphs (which have a fixed y=0 and can only be repositioned via baseline) are handled the same way as in PR Fix IME text doubling on Windows and baseline jitter during composition #599, so cursor and selection geometry remain correct.

Trade-offs

  • More complex glyph traversal: To apply offsets to array glyphs, the code walks line → line_items → runs → clusters → glyphs, which is more code than PR Fix IME text doubling on Windows and baseline jitter during composition #599's direct metric restoration.
  • Both approaches are heuristic: Neither approach distinguishes between "legitimate" metric changes (user deliberately changed content) vs "noise" (font fallback during composition). The stabilization threshold (STABILIZATION_THRESHOLD = 0.01) is used to filter out insignificant changes.

Notes

… vertical offsets

- Add `commit_compose` to atomically replace preedit with committed text,
  fixing text doubling on Windows where `Ime::Commit` arrives before
  `Ime::Preedit("")` clears the buffer.
- Add glyph-level vertical offset stabilization: after layout rebuild,
  compute per-line baseline deltas and apply them directly to glyph
  y-coordinates in `layout.data.glyphs`, preventing visual jitter when
  font fallback changes line metrics (e.g., mixing CJK and Latin scripts).
- Reuse a `baseline_snapshots` scratch Vec to avoid per-update allocation.
- Extract `STABILIZATION_THRESHOLD` constant for the significance threshold.
- Gate stabilization on num_old_lines > 0 to skip empty layouts entirely
- Track any_stabilized flag to avoid unnecessary generation nudges
- Add performance notes in code comments explaining the O(lines) overhead
  is negligible compared to the O(text) layout build cost
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant