13 KiB
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 — "Platform metric fidelity".
Verdict
Three real problems, none of them "spacing values are off by a bit":
- Heights.
control_heightis derived as1.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. - Radii. Only
radius.controladapts per platform;sm/overlayare frozen at 4/10. Native overlay radii span 5px (KDE) to 15px (GNOME) — we are 5px off in both directions. - 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_heightstated-value seam — the pattern to generalize. - Windows
ControlCornerRadius4; 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/DecorationHoverrole mapping.
Recommended changes, in priority order
- State
control_heightper platform — addcontrol_height: Option<Pixels>besideinput_heightonSystemTheme, reuse the ratio-scaling pattern: macOS 24 (measured), Windows 32, GNOME ≈34, KDE ≈33. Fixes the smallness everywhere in one change. - State
root_font_sizefrom Linux font settings — GNOMEfont-name(11pt ⇒ 14.67px), KDEkdeglobals [General] font=(10pt ⇒ 13.33px); the parsing seams already exist for colors. - 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. - 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.
- State
titlebar_heightper platform — GNOME 47, KDE ≈44; macOS queried rather than a drifted floor (32 on current builds, 28 older); Windows stays stated. - Add a minimal spacing scale to
Themedriven 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 hardcodedmin_w_/max_wvalues through it. - 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).