Skip to content

fix(terminal): re-fit once the font has loaded, not on a timing guess - #113

Merged
AThraen merged 1 commit into
mainfrom
fix/fit-after-fonts-load
Sep 5, 2026
Merged

fix(terminal): re-fit once the font has loaded, not on a timing guess#113
AThraen merged 1 commit into
mainfrom
fix/fit-after-fonts-load

Conversation

@AThraen

@AThraen AThraen commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Reported as initial rendering looking wrong — text wrapping and running together mid-line — which corrected itself as soon as the view changed.

Cause

xterm derives its column count from the measured advance width of the font. The first fit() runs immediately after term.open(). If Cascadia Code hasn't loaded yet, xterm measures the fallback's metrics, computes the wrong cols, and reports a width to the PTY that doesn't match what's drawn.

The ResizeObserver cannot correct this: the element size never changed, only the glyph metrics, so no resize event fires. It stays wrong until something else forces a fit — which is exactly why switching layouts appeared to fix it.

The two existing setTimeout fits (50ms, 250ms) are guesses at "fonts are probably ready by now", and are easily too early during a restore with many WebView2s initialising at once.

Fix

document.fonts.ready is the actual signal. The timeouts stay, since they also cover the separate 0×0-container case, but they're no longer load-bearing for font metrics.

Same treatment where a profile override changes fontFamily/fontSize — that can switch to a face that isn't loaded either, and the fit() there had the identical problem.

Worth noting for the #105 discussion

This got worse recently rather than appearing from nowhere. #105 stopped rebuilding the grid on activation, and that rebuild used to force an incidental re-fit that masked the mismeasurement.

That's now three things #105 turned out to be propping up — the lost re-fit on click (#108), this one, and the signature/version-counter machinery it needed to avoid breaking pane paging and restarted-session rendering. Its own benefit was never measured, and the restore cost turned out to be elsewhere. I still think reverting it is the cleaner call, and I'd rather raise that a third time than keep patching around it.

Check Result
Unit tests 312/312
App + Tests build 0 errors, 0 warnings

How to verify

Restore a full session set and look at the panes before touching anything. Text should be correctly wrapped from the first paint, with no mid-line overlap and no need to switch layouts to correct it.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NDsEfog5kVkT5NmX1Ya5be

Reported as initial rendering looking wrong — text wrapping and running
together mid-line — which then corrected itself as soon as the view changed.

xterm derives its column count from the MEASURED advance width of the font. The
first fit() runs immediately after term.open(); if Cascadia Code hasn't loaded
yet, xterm measures the fallback's metrics, computes the wrong cols, and reports
a width to the PTY that doesn't match what is drawn.

The ResizeObserver cannot correct this: the ELEMENT size never changed, only the
glyph metrics, so no resize event fires. It stays wrong until something else
forces a fit — which is exactly why switching layouts appeared to fix it.

The two existing setTimeout fits (50ms, 250ms) are guesses at "fonts are
probably ready by now", and are easily too early during a restore with many
WebView2s initialising at once. They stay, since they also cover the separate
0x0-container case, but document.fonts.ready is the actual signal.

Same treatment where a profile override changes fontFamily/fontSize — that can
switch to a face that isn't loaded either, and fit() there had the same problem.

Note this got worse recently rather than appearing from nowhere: #105 stopped
rebuilding the grid on activation, and that rebuild used to force an incidental
re-fit that masked the mismeasurement.

312/312 pass, 0 warnings.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NDsEfog5kVkT5NmX1Ya5be
@AThraen
AThraen merged commit 4c79aed into main Sep 5, 2026
1 check passed
@AThraen
AThraen deleted the fix/fit-after-fonts-load branch September 5, 2026 18:53
AThraen added a commit that referenced this pull request Sep 5, 2026
Release review of the 20 commits since v0.6.0. Three documentation defects,
one of them actively wrong.

WRONG: the active-terminal highlight section claimed the ring's accent "is
stashed on Border.Tag at build time so the highlight method doesn't need to
look up the VM". The code does the opposite and its own comment says so —
RepoRoot is populated asynchronously by GitService and AccentColor changes when
it lands, so a cached Tag goes stale and stops matching the sidebar. The Tag is
only a fallback.

MISSING: three services added this cycle were undocumented — DbGate (#102),
PwshLocator (#104), ClaudeConfigGate (now shutdown-only after #111).

MISSING: nothing described what actually makes a session active, which is now
the most subtle thing in the codebase and was got wrong twice. Added a section
covering all three routes and, more importantly, why two of them must come from
the page:

  - WebView2 is an HwndHost, so mouse input on hosted native content raises no
    WPF routed events at all — a Preview handler on the host Border only ever
    sees the ring around the terminal (#108).
  - xterm's onData is not "the user typed": it also carries device-attribute
    replies, cursor-position reports, OSC colour replies and focus in/out, none
    of which are distinguishable from typing by inspecting the bytes (#106).

Also recorded why the font-load re-fit exists (#113) and why the ClaudeLaunch
stagger must stay a flat delay — including that a plain Task.Delay(2000) still
logged gate=36339ms after the revert, which is what finally showed the gate was
measuring an app-wide stall rather than causing one. Written as a
"do not try this again" note with the measurements, since the adaptive version
looks obviously better on paper and was attempted three times.

Verified while here: every service on disk is now documented, and all nine
keybindings in MainWindow match the documented table.


Claude-Session: https://claude.ai/code/session_01NDsEfog5kVkT5NmX1Ya5be

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
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