fix(@wterm/dom): pixel-snap upper-block gradients to prevent sub-pixel misalignment#81
Open
codevibesmatter wants to merge 1 commit into
Open
Conversation
…lignment Claude Code's TUI draws horizontal rules as a run of U+2594 (UPPER ONE EIGHTH BLOCK) cells. The DOM renderer painted these via `linear-gradient(<fg> 12.5%, <bg> 12.5%)` on a `.term-block` span. At the canonical row-height of 17px, 12.5% resolves to 2.125px — a sub-pixel boundary the browser rounds inconsistently from cell to cell. The result is a visible per-cell jog along the rule and 1px gaps/overlaps against the row immediately below (typically `│` box-drawing or empty cells). Fix: route every percentage-based vertical block gradient stop through `round(calc(var(--term-row-height) * N/8), 1px)` so the boundary lands on a whole device pixel. All U+2580..U+2587 and U+2594 stops are converted in one pass — they share the same sub-pixel hazard, U+2594 is just the one Claude Code's UI surfaces most often. Horizontal-axis gradients (U+2589..U+258F, U+2590, U+2595) and the quadrant codepoints are unaffected because their gradient line is the cell width (1ch ≈ font-advance), and the rounding hazard is far less visible across cells than along a continuous horizontal rule. Tests: `packages/@wterm/dom/src/__tests__/upper-block-alignment.test.ts` asserts the emitted CSS string contains a pixel-typed stop (no bare `12.5%`) and that every U+2594 cell paints with the same background string so adjacent cells stay on the same pixel. Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
|
@codevibesmatter is attempting to deploy a commit to the Vercel Labs Team on Vercel. A member of the Team first needs to authorize it. |
Author
|
FYI cross-link: validated this stacked on top of #75 against Claude Code's TUI on our staging deploy — the combined branches fix both the U+2594 horizontal-rule jog (this PR) and the wide-char layout displacement (#75). Comment with details on #75. Both fixes are needed for clean Claude TUI rendering; this one is the simpler self-contained piece. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Claude Code's TUI draws horizontal rules with a run of U+2594 (
▔UPPER ONE EIGHTH BLOCK) cells. The DOM renderer paints these vialinear-gradient(<fg> 12.5%, <bg> 12.5%)set as inlinebackgroundon a.term-blockspan (packages/@wterm/dom/src/renderer.ts:165-166).At the canonical row-height of 17px,
12.5%resolves to 2.125px — a sub-pixel boundary the browser rounds inconsistently from cell to cell. The result is a visible per-cell jog along the horizontal rule, and 1px gaps or overlaps against the row immediately below (usually│box-drawing or empty cells).I noticed this running Claude Code inside a tmux pane wrapped by
@wterm/react— every/login,/diff, panel screen, etc. paints a U+2594 horizontal rule that's visibly jagged.Compare:
@xterm/xterm(canvas)@wterm/dom(today)@wterm/dom(this PR)Fix
Route every percentage-based vertical-axis block gradient stop through
round(calc(var(--term-row-height) * N/8), 1px)so the boundary lands on a whole device pixel. All U+2580..U+2587 and U+2594 stops are converted in one pass — they share the same sub-pixel hazard, U+2594 is just the one Claude Code surfaces most often.Horizontal-axis gradients (U+2589..U+258F, U+2590, U+2595) and quadrant codepoints are left unchanged. Their gradient line is the cell width (
1ch≈ font-advance), and the rounding hazard is far less visible across cells than along a continuous horizontal rule. Worth a separate look later but the user-visible bug is the vertical-axis one.Tests
packages/@wterm/dom/src/__tests__/upper-block-alignment.test.ts(new file, 2 tests):12.5%stop, AND does contain a pixel-typed length (round(...)/px).The CSS-string assertion is the right level: the bug symptom (the pixel jog) only appears in a real browser, but the bug cause (a sub-pixel gradient stop) is fully visible in emitted CSS, which is what the renderer controls.
Verification
Full CI gauntlet passes locally:
pnpm test— 13/13 groups, DOM count 68 → 70pnpm lint— clean (one pre-existing warning inapps/docs/postcss.config.mjs, unrelated)pnpm type-check— 22/22pnpm build— 15/15pnpm test:e2e— 11/11 (validates we didn't regress rendering structure)Did not touch the Zig core, so the committed WASM is unchanged.
Out of scope
I noticed two related issues while debugging Claude Code rendering that aren't in this PR:
❯(U+276F) — looks like it overlaps PR Fix wide Unicode cell rendering #75 from @ctate, happy to wait for that to land.?1049— lives in Zig (src/terminal.zig:481-501 switchScreen), much bigger change.Happy to file these as separate issues with byte-sequence repros if it helps.
🤖 Drafted with Claude Code