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

273 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
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).