# 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).