Skip to content

fix(terminal): font visibly swaps when selecting text#131

Merged
simion merged 1 commit into
simion:mainfrom
MHohlios:feature/font-issues
Jul 22, 2026
Merged

fix(terminal): font visibly swaps when selecting text#131
simion merged 1 commit into
simion:mainfrom
MHohlios:feature/font-issues

Conversation

@MHohlios

@MHohlios MHohlios commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Symptom

Yet another font issue.

With the terminal font set to a user-installed family (e.g. a Nerd Font under /Library/Fonts), highlighting text in an agent TUI renders the selection in a different family: another sans mono, or Courier (serifed) in the worst case. Deselecting reverts it. Same atlas mechanics as the residual cold-spawn gap in #70.

Screenshot 2026-07-22 at 12 52 53 PM

Using obnoxiously loud font here for demonstration don't judge me.

Root cause

Two WKWebView behaviors, verified with a standalone WKWebView probe that pixel-classifies which family actually renders:

  1. A canvas that is not connected to the document silently fails to resolve user-installed families. The stack falls through to a FontFace-registered webfont (bundled JetBrains Mono), or to generic monospace (Courier) when no webfont matches.
  2. The bad resolution sticks: WebKit short-circuits ctx.font = <identical string>, so reattaching the canvas never heals it. Only a different string forces a re-parse.

xterm's WebGL TextureAtlas rasterizes every glyph on one long-lived hidden scratch canvas shared by same-config terminals. Real cell draws attach it to the drawing terminal's element before touching ctx.font, but two windows rasterize detached:

  • the ASCII warm-up draws container-less on idle callbacks, so on a cold spawn with an idle main thread the whole warm-up can run before any real draw has attached the canvas;
  • closing the pane that currently hosts the canvas orphans it.

Glyphs cache per (char, fg, bg, ext) with no font in the key, so default-colored text keeps wrong-family glyphs indefinitely while a selection (new bg = new key = fresh, connected draw) rasterizes the same characters with the right family. The font swaps under the highlight.

Fix

lib/atlasCanvasGuard.ts: park the scratch canvas under document.body wherever it might be orphaned, and poke ctx.font with a sentinel so the next draw re-resolves while connected. Wired in loadTerminalRenderer at three points:

  • directly after loadAddon (the initial atlas fires onChangeTextureAtlas during renderer construction, before the addon can forward it);
  • on onChangeTextureAtlas + microtask (font/theme/dpr atlas swaps; the queued warm-up runs later, on idle);
  • in dispose() with the dying pane's element (unmount orphaning). Covers TerminalPane and AuxTerminal.

Why this is low risk (the #99 lesson)

  • Never touches atlas pages, glyph caches, textures, or renderer state. It moves a scratch canvas (an operation xterm itself performs on every real draw, just to a safer parent) and re-parses a font string that every draw assigns before painting.
  • All reach-ins optional-chained: worst case is a silent no-op (the bug returns), not rendering corruption. xtermInternals.test.ts now pins the addon-webgl private names so an upgrade that renames them fails in CI instead.

Testing

  • New unit tests for the guard + the canary extension; full suite passes (369 tests), npm run build clean.
  • Standalone WKWebView probe reproduces both WebKit behaviors and confirms connected-canvas resolution is correct in every state the guard produces.
  • Currently soaking as a make beta install on a machine that hit the bug in daily use. Ran for a day of use and seem to be fine.

Also corrects docs/gotchas.md: the detached-canvas bullet claimed xterm was unaffected; it is affected, and a new bullet documents this failure mode.

@MHohlios MHohlios changed the title fix(terminal): font visibly swaps when selecting text (glyph atlas rasterizes on a detached canvas) fix(terminal): font visibly swaps when selecting text Jul 22, 2026
@MHohlios
MHohlios marked this pull request as ready for review July 22, 2026 16:55
…nts resolve

WKWebView cannot resolve user-installed font families on a disconnected
canvas, and the bad resolution sticks: assigning an identical ctx.font
string is a WebKit no-op, so reattaching never heals it. xterm's WebGL
TextureAtlas rasterizes every glyph on one long-lived scratch canvas
that draws container-less during the ASCII warm-up and is orphaned when
the pane hosting it unmounts, so glyphs could cache with the wrong
family and visibly swap under selection (new bg = new atlas key = fresh
connected draw).

lib/atlasCanvasGuard.ts parks the canvas under document.body at atlas
creation/swap and pane dispose, and pokes ctx.font with a sentinel so
the next draw re-resolves while connected. Reach-ins optional-chained;
xtermInternals.test.ts pins the addon-webgl private names.
@MHohlios
MHohlios force-pushed the feature/font-issues branch from e5062b0 to 4718c11 Compare July 22, 2026 17:05

@simion simion left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct fix. Root cause diagnosis checks out against the installed addon-webgl bundle — the microtask timing is sound (event fires before _charAtlas assignment, warm-up runs on idle after the microtask), the two-part fix targets both halves of the WebKit sticky-resolution bug, and all reach-ins are optional-chained for soft failure. Tests cover the branch matrix and the canary pattern is the right upgrade-safety net. Nice work.

@simion
simion merged commit a602aee into simion:main Jul 22, 2026
2 checks passed
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.

2 participants