Composer and New Tab rims draw one device pixel, concentric at every scale

Merged
TE
22d ago
+231-13

Summary

The composer and New Tab box rims were a fixed inset 0 0 0 .5px shadow. macOS WebKit paints an inset spread shadow as the outer rounded rect minus an inner hole. It snaps the hole to device pixels but not the rounded outer clip. On a 1x display, or at a non-100% uiScale or pane zoom, half a CSS pixel is less than one device pixel, so the hole shifts by a pixel: whole edges vanish and the corner arcs thin out or break. Reproduced in a tagged DEV build on a 1x capture mini (the New Tab box lost its right edge and arcs) and in an offscreen WKWebView at device scale 1.

Fix: draw the rim as one device pixel and the focus ring as whole device pixels, using one shared token.

  • pages/shared/desktop.css and desktop.ts: --cmux-hairline (1/DPR px) and --cmux-hairline-ring (the whole device pixels nearest 1 CSS px, which is 1px at 1x and 2x). desktop.ts sets them from devicePixelRatio, which in WKWebView follows pageZoom (uiScale ร— pane zoom), and updates them on a resolution change. A min-resolution: 2dppx default covers the first paint.
  • composerLocation.css (.acpmux-composer-box, #20303's rim): rim and focus ring use the tokens. The rim now clears with appearance.borders none, as the New Tab box's does. Colors and alphas are unchanged. The 0 8px 24px drop shadow is untouched; per Leo it is still heavy and left for a follow-up.
  • newtab/screen.css (.nt-box): rim and focus ring use the tokens.
  • composerStates.css: removed the dead base .acpmux-composer-box:focus-within rule (composerLocation.css always overrides it). The shell composer's rest and focus rings use --cmux-hairline-ring.

The focus ring keeps #20188's 1px look at 1x and 2x. No ring layers were added.

Overlap: #20443 also edits composerLocation.css. This PR touches only the rim and ring rules there (.acpmux-composer-box and its :focus-within), not #20443's layout hunks.

desktop.ts also refreshes the tokens on resize, because a live pageZoom or uiScale change resizes the viewport but may not fire the resolution query. Only one query is watched per document.

Pre-existing, not changed here: under Increase Contrast, the plain composer's prefers-contrast focus rule in composerStates.css loses to composerLocation's rule of equal specificity, so the plain composer keeps the 38% ring. Its 1.5px contrast ring width is also fractional at 1x. Both are left for the rims follow-up.

Follow-up: about 23 other inset 0 0 0 .5px rims (cards, popups, changes, conversation, minimap) have the same 1x problem. They can switch to var(--cmux-hairline) in a separate PR.

Testing

  • Red: f30d214adaa7 adds webviews/test/composer-rim-hairline.test.ts (Chromium at DPR 1, 1.25 and 2: the rim spread ร— DPR is 1, focus rings are whole device px and 1px at integer scales, and the rims clear with borders none). On the base, cd webviews && CMUX_BROWSER_TESTS=1 bun test test/composer-rim-hairline.test.ts gives 1 pass, 3 fail (the composer rim is 0.5 device px at 1x and 0.625 at 1.25x, and has alpha 0.16 with borders none).
  • Green: 68d6ab249566, same command: 4 pass, 41 expects. It is still green after the base merge.
  • ui-rules, desktopLayer, check:browser-lanes, check:type-scale, check:system-blue and check:text-focus all pass. check:layers and vp check formatting are red on the current base only (summary/summary.css z-index; App.tsx, modelPickerLayout.ts, subagents.gallery.ts, SubagentStatusList.tsx, cardState.ts); none of those files is in this PR.
  • Pixel check, macOS WKWebView offscreen snapshot of the gallery build (cosmic-gray), dark theme. Each edge's contrast is normalized to the mean edge contrast; arcs give the weakest point:
    • Composer at 1x, rest. Before: top 1.22, bottom 0.78, left 0.78, right 1.22, weakest arc 0.27, and the bottom edge is gone in the crop. After: all edges 1.00, arcs 0.45 (normal antialiasing, the same as the 1px focus ring's 0.48).
    • New Tab at 1x: before, the right and bottom edges drop out; after, it is continuous.
    • At 2x the rim was already one device pixel (pixel rows read 71 on top, left and bottom in both builds), so it is unchanged. At DPR 2.3 (pageZoom 1.15) the rim is 0.4348px, which is one device pixel.
    • Focus rings are unchanged at 1x and 2x, and continuous at DPR 2.3 (weakest point 0.91โ€“0.98).
  • Real app at 1x: tagged DEV builds of the base and of this head on a 1x capture mini, about 10 s each, with New Tab focus in, out and in again, and the chat composer at rest. In the base, the resting rim drops its right and bottom edges and the arcs fade on both boxes (pixel at the right edge: 221 against a 221 fill, so nothing is drawn). In the head, a 1px rim reads 181 on all four sides of New Tab and 196 on all four sides of the chat composer. The focus ring is the same 1px in both builds.

Captures

Real app at 1x, tagged DEV builds on a capture mini. In the corner zooms, the base rest rim loses its right and bottom edges; in the head it is continuous. The focus ring is the same in both builds.

Before and after at 1x: New Tab rest, New Tab focus, chat composer rest

macOS WKWebView snapshot at 1x, dark theme, composer and New Tab, before and after:

WKWebView corners at 1x before and after

Before (base)After (this PR)
New Tab, focus in, out and inbase New Tab head New Tab
Chat composer at restbase chat head chat rest
Chat composer focusednot captured: cua could not switch focus in the base chathead chat focus

Changelog

Fixed: The composer and New Tab box outlines no longer break at the corners on standard-resolution displays or at non-100% zoom.

๐Ÿค– Generated with Claude Code