Files
gpui-native-kit/docs/platform-metrics-audit.md
T

13 KiB
Raw Blame History

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":

  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.

Landed 2026-11 with two deviations: Omarchy is grouped with GNOME (it styles its apps with adwaita), and macOS's titlebar is stated at the measured 32pt rather than queried at runtime (28pt on older builds).

  1. State control_height per platform — add control_height: Option<Pixels> 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 and menu row min-heights through it — GNOME 32 (published), Windows ≈33 (padding 4/5 + a 20px Body line + 2px item margins), KDE ≈29 (max(font, 20px floors) + 1 + 2×4); the hardcoded min_w_/max_w surface values stay put — they are platform-invariant.
  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).