diff --git a/docs/CHECKLIST.md b/docs/CHECKLIST.md index a05caa4..3f134d2 100644 --- a/docs/CHECKLIST.md +++ b/docs/CHECKLIST.md @@ -13,6 +13,14 @@ is the first downstream consumer; its screens drive the order. Gaps surfaced by the signed comparison; ordered by signed's usage. +- [ ] **Platform metric fidelity** — native-sizing pass over the theme and + components: stated `control_height` per platform (macOS 24 measured / + Windows 32 / GNOME ≈34 / KDE ≈33), Linux root font sizes (GNOME 14.67px, + KDE 13.33px), platform overlay/sm radii, per-source border + focus + policy (macOS/GNOME borderless fills, Windows/KDE 1px), titlebar + heights (GNOME 47), a theme spacing scale, switch/icon calibration; + ranked findings in + [platform-metrics-audit.md](platform-metrics-audit.md). - [ ] **Virtual list** — generic virtualized variable-height rows with a scroll handle, promoted from the editor views' pattern over gpui's `list`; signed's PR lists and commit diff drive it. diff --git a/docs/platform-metrics-audit.md b/docs/platform-metrics-audit.md new file mode 100644 index 0000000..232b102 --- /dev/null +++ b/docs/platform-metrics-audit.md @@ -0,0 +1,266 @@ +# Platform metrics audit — Apple HIG, GNOME HIG, Fluent, KDE HIG + +Why our controls read small and foreign next to native ones. Assessed 2026-11 +against the primary sources: the libadwaita stylesheet (`libadwaita` `main`, +`src/stylesheet/**.scss`), WinUI 3 theme resources (`microsoft-ui-xaml` +CommonStyles) and the Learn design pages, Kirigami `units.cpp` + Breeze +`breezemetrics.h`/`breezestyle.cpp`, and the Apple HIG pages. Apple publishes +no pixel metrics, so macOS numbers are **measured on this machine via AppKit +`fittingSize`** — note the OS-version variance called out below. Queue entry: +[CHECKLIST.md](CHECKLIST.md) — "Platform metric fidelity". + +## Verdict + +Three real problems, none of them "spacing values are off by a bit": + +1. **Heights.** `control_height` is derived as `1.75 × root_font_size`. + Native platforms do not size controls that way — Windows fixes buttons at + 32px, GNOME and Breeze add fixed padding to the font height. Result: our + buttons are −23% vs native on Windows, −18% on GNOME. The main "looks + small" driver. +2. **Radii.** Only `radius.control` adapts per platform; `sm`/`overlay` are + frozen at 4/10. Native overlay radii span 5px (KDE) to 15px (GNOME) — we + are 5px off in both directions. +3. **Borders.** We paint `border_1()` on nearly everything (shadcn-style). + That matches Windows and KDE and contradicts macOS and GNOME/libadwaita, + whose controls are borderless fills at normal contrast. The "feels weird" + driver. + +Structural gap underneath the spacing complaint: **the theme has no spacing +tokens** — components hardcode gpui utilities (`p_1`, `px_2`, `gap_1p5`, +`min_w_56`), so platform rhythm (GNOME's 6px grid, KDE's 4/6/8) cannot be +expressed. + +## What the platforms specify + +### Apple / macOS + +The HIG publishes no pixel metrics for macOS — verified across the buttons, +text-fields, toolbars, toggles, menus, windows, layout, sidebars, sheets, +materials and designing-for-macos pages. What is published: the type ramp +(body 13pt, min 10pt) and the 44×44pt hit-region rule. Everything else below +was measured locally (AppKit, regular control size, this macOS build): + +| Control | Measured | Confidence | +|---|---|---| +| Push button | **24pt** (small 20, mini 16) | measured | +| Rounded text field | **24pt** | measured | +| Pop-up button / segmented | **24pt** / **24pt** | measured | +| Switch | **54×24** | measured | +| Checkbox (empty control) | **16×16** | measured | +| Slider control height | **16pt** | measured | +| Window titlebar | **32pt** on this build; 28pt on older macOS | measured, version-dependent | +| Body text | 13pt | published | + +Border language: toolbar items and image buttons are *explicitly borderless* +("toolbar items don't include a bezel"); hierarchy is expressed by fill, not +borders; fields keep a hairline bezel; menus/popovers separate by shadow and +material, not borders. Concentricity rule: "buttons, text fields, headers, +and footers have corner radii that are concentric with bar corners" (rule +only, no values). + +### GNOME / libadwaita + +Exact values from the stylesheet; GTK px are logical pixels at 96dpi. + +| Metric | Value | +|---|---| +| Button total height | min-height 24 + padding 5px 10px ⇒ **≈34px** | +| Entry | **34px** (border-box, no vertical padding) | +| Header bar | **47px** | +| Menu item | min-height **32px**, padding `0 12px`; container padding 6px | +| Switch | knob 20px, trough ≈**46×26** | +| Checkbox | 14px + 3px pad each side ⇒ **20px**, radius **6px** | +| Radius ladder | button/entry **9px** (`$button_radius`), checkbox 6px, cards 12px, menu/popover/dialog/window **15px**, menu items 9px | +| Font | Adwaita Sans 11pt = **14.67px** | +| Focus | **2px accent outline** (accent @50% default, 80% high contrast) | + +Borders: **controls are borderless at normal contrast** — raised buttons, +entries, switches, menus draw shape from translucent `currentColor`-mix +fills. 1px borders are a high-contrast affordance only. Always-on hairlines +are reserved for the window outline, headerbar bottom shade, frames, and +boxed-list row separators. + +### Windows 11 / WinUI 3 (Fluent 2) + +Published tokens from the theme-resource XAML and Learn pages, at 100% +scaling: + +| Metric | Value | +|---|---| +| Button | **32px** (padding 11,5,11,6 + 1px border + Body 14/20); `ButtonHeight=32` lives in OS generic.xaml | +| TextBox | **32px** (`TextControlThemeMinHeight`, OS generic.xaml — not fetched) | +| CheckBox box | **20×20**, 1px stroke (row 32) | +| ToggleSwitch | track **40×20** pill; knob **12×12** resting → 14 hover | +| Radius | ControlCornerRadius **4px**, OverlayCornerRadius **8px** | +| Title bar | **32px** standard, 48px "Tall" | +| Type ramp | Caption 12, Body **14**, Body Large 18, Subtitle 20, Title 28 | +| Spacing | 8 between buttons, 12 control↔label, 16 edge↔text — a **4px grid** | +| Focus | system ring drawn outside the control (negative focus margin) | + +Borders: buttons and textboxes carry a **1px elevation border** (gradient +stroke, lighter top + darker bottom); the accent button keeps a border too; +checkbox box 1px; switch track 1px while off; slider borderless except the +thumb. + +### KDE Plasma 6 / Breeze + +Source constants (`breezemetrics.h`, `units.cpp`); the HIG names the usage +rules, not px. + +| Metric | Value | +|---|---| +| Kirigami units | gridUnit **18**, smallSpacing **4**, mediumSpacing **6**, largeSpacing **8**, cornerRadius **5** | +| Button | fontHeight + 2×(6+2) ⇒ **≈33px** at the default 10pt font | +| Line edit | fontHeight + 12 ⇒ **≈29px** | +| Checkbox | **20×20**, radius 4 | +| Switch (qqc2) | **36×18**, knob 18 | +| Radius | **5px** everywhere — buttons, line edits, menus, popups, windows | +| Font | Noto Sans 10pt = **13.33px** | +| Titlebar (KWin) | max(fontHeight, 2×gridUnit = 36) + margins ⇒ **≈44px** | +| Focus | rounded ring filled highlight @30%; item views get a focus underline | + +Borders: 1px outline (`PenWidth::Frame`) on all controls; **flat buttons are +borderless until hover/press/check**; menus and popups framed 1px. + +## What our tokens resolve to today + +`SizeTable::with_root_font_size` medium class (height 1.75rem, padding +1rem) fed by each adapter's inputs in `crates/theme/src/source/`: + +| | macOS (root 13) | Windows (root 14) | GNOME (fallback 16) | KDE (fallback 16) | +|---|---|---|---|---| +| control_height | 22.75 | 24.5 | 28 | 28 | +| input_height | 24 (stated) | 32 (stated) | 30 | 30 | +| padding_x | 13 | 14 | 16 | 16 | +| text / icon / indicator | 13 / 13 / 15.9 | 14 / 14 / 17.2 | 16 / 16 / 19.6 | 16 / 16 / 19.6 | +| radius sm / control / overlay | 4 / 6 (stated) / 10 | 4 / 4 (stated) / 10 | 4 / 6 / 10 | 4 / 6 / 10 | +| titlebar | max(22.75, 34 floor) = 34 | 32 (stated) | 34 | 34 | +| switch (1.65×h × h, knob 1.225rem) | 37.5×22.75 | 40.4×24.5 | 46.2×28 | 46.2×28 | + +## Gap analysis + +### Heights — the "looks small" bug + +| Button height | Native | Ours | Delta | +|---|---|---|---| +| Windows | 32 | 24.5 | **−7.5px (−23%)** | +| GNOME | ≈34 | 28 | **−6px (−18%)** | +| KDE | ≈33 | 28 | −5px | +| macOS | 24 | 22.75 | −1.25pt | + +Root cause: native heights are not `k × font`. Windows fixes 32px; GNOME is +font + 10px padding; Breeze is fontHeight + 16px. One rem factor cannot hit +any of them — and the trap is that reading the real Linux fonts (14.67 / +13.33px) without changing the model makes our buttons *smaller* (24.4 / +23.3). The codebase already has the right seam: `SystemTheme.input_height`, +an optional platform-stated value with ratio scaling for the other classes. +`control_height` needs the same treatment. Our stated `input_height` values +(macOS 24, Windows 32) are already exactly right — the derived button height +next to them is what is off. + +Text compounds it on Linux: the GNOME/KDE adapters never state +`root_font_size`, so the 16px fallback makes labels oversized (16 vs 14.67 / +13.33) while controls read undersized. + +### Radii + +| Radius | macOS | Windows | GNOME | KDE | +|---|---|---|---|---| +| control | 6 ~ bezel ≈6 | 4 ✓ | **6 vs 9** | 6 vs 5 ~ | +| overlay (menu/popover/dialog) | 10 ~ (unpublished, ≈10–12) | **10 vs 8** | **10 vs 15** | **10 vs 5** | +| sm (checkbox) | 4 ~ | 4 ~ | 4 vs 6 | 4 ✓ | + +Only `control` is platform-overridable (`resolve`); `Radius::default` +freezes sm/overlay/full. GNOME's whole ladder sits 3–5px above ours; KDE's +overlays sit 5px below. + +### Borders — the "feels weird" bug + +Our current policy: `border_1()` on secondary button, input, select, +checkbox, radio, menu, popover, dialog, combobox popup, command palette, +toast, tooltip, alert, even slider/switch knobs. Primary button is +borderless. + +| Element | macOS | GNOME | Windows | KDE | Ours | +|---|---|---|---|---|---| +| secondary button | fill, **no border** | fill, **no border** | 1px elevation border | 1px border | 1px — ✗mac/GNOME | +| primary button | fill, no border | fill, no border | **accent fill + border** | accent fill + border | no border — ✗Win/KDE | +| input | hairline bezel | **borderless**, 2px accent outline on focus | 1px border → accent on focus | 1px border → focus color | 1px always | +| menu / popover | **no border** (shadow/material) | no border (shadow + 1px black@5% ring) | framed flyout | 1px frame | 1px | +| separator roles | hairline ✓ | hairline ✓ | ✓ | ✓ | ✓ | + +Half-right on Windows/KDE, systematically wrong on macOS/GNOME — and even on +Windows/KDE our primary button drops a border the natives keep. Our +high-contrast mode sharpening borders toward the foreground is exactly +GNOME's model (borders as a high-contrast affordance); it just never +switches off at normal contrast. + +### Spacing — structural, not numeric + +No spacing tokens exist in `Theme`; components hardcode a fixed 4px grid: +menu `p_1` + items `px_2 py_1`, popover `mt_1 min_w_32`, menu `min_w_56`, +toast/tooltip `px_3`/`px_2 py_1`, button `gap_1p5`, hardcoded +`max_w(480)`/`max_w(360)`. Native rhythms: GNOME = 6px base (menu padding +12px, item min-height 32px — our GNOME menu rows ≈24px), KDE = 4/6/8, +Windows and macOS ≈ 4px grid (we happen to match those two). Menu/popover +density is frozen at a macOS-ish tuning. + +### Per-component calibration + +- **Switch**: macOS native 54×24 (ours 37.5 wide — too narrow); Windows + 40×20 with a 12px knob (ours 24.5 tall, 17px knob — too big); KDE 36×18 + (ours 46×28); only GNOME ≈ matches, by accident. +- **Checkbox**: Windows/GNOME/KDE ≈20 (ours 17.2–19.6); macOS 16 ✓. +- **Icons**: macOS ≈16 (ours 13), Windows 16 (ours 14); GNOME/KDE 16 ✓. +- **Titlebar**: measured 32 on this macOS build vs our 34 floor (fine-ish; + the floor silently drifted past older macOS's 28); GNOME headerbar 47 vs + 34 — a visible −13px; KDE ≈44 vs 34; Windows 32 stated ✓. + +## What is already right + +- The `input_height` stated-value seam — the pattern to generalize. +- Windows `ControlCornerRadius` 4; stated titlebar 32. +- Body text on macOS (13) and Windows (14); scrollbar autohide mapping; + per-platform elevation stacks; high-contrast border sharpening mirrors + GNOME's model. +- GNOME accent via portal/gsettings, KDE `DecorationFocus`/`DecorationHover` + role mapping. + +## Recommended changes, in priority order + +1. **State `control_height` per platform** — add `control_height: + Option` beside `input_height` on `SystemTheme`, reuse the + ratio-scaling pattern: macOS 24 (measured), Windows 32, GNOME ≈34, KDE + ≈33. Fixes the smallness everywhere in one change. +2. **State `root_font_size` from Linux font settings** — GNOME + `font-name` (11pt ⇒ 14.67px), KDE `kdeglobals [General] font=` (10pt ⇒ + 13.33px); the parsing seams already exist for colors. +3. **Make the radius ladder platform-overridable** — stated overlay/sm + radius on `SystemTheme`: GNOME 15/6, Windows 8/4, KDE 5/4, macOS ≈10/4; + keep preset > system precedence. +4. **Per-source border and focus policy** — a theme-level token consumed by + components: secondary button and inputs drop borders on macOS/GNOME + (fill tints + 2px accent focus outline), keep 1px on Windows/KDE; + primary gains a border on Windows/KDE; menus/popovers shadow-only on + macOS/GNOME. +5. **State `titlebar_height` per platform** — GNOME 47, KDE ≈44; macOS + queried rather than a drifted floor (32 on current builds, 28 older); + Windows stays stated. +6. **Add a minimal spacing scale to `Theme`** driven by a per-platform base + unit (GNOME 6, KDE 4/6/8, others 4); route menu/popover paddings, menu + item min-height (GNOME 32), and the hardcoded `min_w_`/`max_w` values + through it. +7. **Calibrate switches and icons per platform** — macOS 54×24, Windows + 40×20 with 12px knob, KDE 36×18; icon 16 on macOS/Windows. + +## Source confidence notes + +Everything in the GNOME/Windows/KDE tables is quoted from the cited +stylesheets, sources, or Learn pages (published). KDE button/line-edit/title +heights are formulas from source constants (derived). Windows TextBox height +token and the focus-stroke pixel spec live in the OS `generic.xaml` and were +not retrievable — low confidence, commonly cited as 32. Apple publishes no +macOS control metrics; all macOS control numbers here are AppKit +measurements from 2026-11 on this machine and may shift across macOS +releases (titlebar already differs from older builds).