Files
gpui-native-kit/docs/PLAN.md
T
2026-11-01 17:26:30 +07:00

64 KiB
Raw Blame History

gpui-native-kit — Project Plan

Rev 7 (2026-11-01). Changes from rev 6: units 1–14 plus the data crate are landed and marked (§6, §12); per-platform elevation stacks landed (D24); §6.3 adds the migration-driven roadmap for signed — the first downstream app — after fresh bezel and gpuikit component surveys; the migration itself is Phase H (§7). The rev 6 rules stand: components are built one element at a time behind a review gate — each unit is designed, implemented, tested, shown in the gallery, and reviewed before the next starts (batch waves are gone). gpui-kit base is re-admitted as the API-design reference: its unstyled-control conventions (controlled state, one activation path for pointer + Enter + Space, semantic states, a11y labels) shape our component APIs, and small utilities may be adapted where they help. Everything else (empty-project reset, theme engine as the product, crate split, per-component features) stands.

1. Vision

GPUI components that feel native to the OS they run on — macOS, Windows 11 (WinUI), GNOME, KDE, Omarchy — with a designed neutral fallback for unknown systems. Every color, radius, spacing, metric, and icon comes from engines that read the running system or a user-supplied theme. The theme and icon engines are the product; components are their proof.

Consumption is flexible: small focused crates, per-component cargo features, and a thin optional facade. A hello-world app can depend on gpui-native-core with two features; a full app can depend on the gpui-native-kit facade with --all-features.

2. Evaluated foundations (2026-10-28)

Crate What it is Verdict What we take
gpui-kit (longbridge) Unstyled base behavior layer (controlled state, one activation path, semantic states, a11y roles, focus utilities) + styled component library Not a wholesale foundation: the full base is larger than we need and pins gpui-pre snapshots. But base is the API-design reference for our components (D20), and its small utilities are adapted where they help API shapes: Button::new("save") unstyled + .on_click as the single path for pointer/Enter/Space; controlled checked + on_change(next); disabled/selected semantic states resolved after the builder chain, disabled last; accessibility_label; composable parts (CheckboxIndicator, SwitchTrack/SwitchThumb) where a component needs them. Utilities worth adapting: h_flex/v_flex, StyledExt, StateStyle, focus-ring helper
gpuikit (iamnbutler) Web/SwiftUI-style toolkit (editor, input, keymap, markdown, selection, a11y modules) on gpui-unofficial Not a foundation: builds on the gpui-unofficial crates.io distribution (conflicts with D3); generic theme system with no per-OS native look Component API ergonomics; showcase/gallery pattern
bezel (crabtalk) SwiftUI-lean library; style flows through the environment — Theme::of(cx) + catalog traits, zero style parameters, Brand/Tint hue re-theming; own gpui fork (bezel-gpui) Not a foundation: gpui fork (conflicts with D3); one visual language (macOS-leaning HIG), not per-OS native Environment-first styling: components read the theme global at paint time and take no color/size parameters; Brand-style hue overrides inform custom themes
zeron (zeronsh) Production gpui app (coding-agent control plane; crates: ui, theme, text, syntax, term, …) Not a foundation — an application, not a library Existence proof that gpui scales; app-level patterns for data-heavy screens

Decision D15: we depend on no UI toolkit and port no wholesale subsystems. These crates (plus native-theme, D1) are read for API and design ideas — and always simplified, never mirrored 1:1. When in doubt, ship the smaller API.

3. Background facts (verified)

  • GPUI install shape — the only framework dependency:
    gpui          = { git = "https://github.com/zed-industries/zed", rev = "ec86047d67acd6fddd81392f44ed6ebcc6e7f531" }
    gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "ec86047d67acd6fddd81392f44ed6ebcc6e7f531", features = ["font-kit"] }
    
    font-kit is required on macOS for glyph rasterization; Linux needs wayland and/or x11; Windows needs nothing. No gpui-pre, no forks, no gpui-unofficial. Toolchain 1.98.1 via rust-toolchain.toml (matches zed at the pinned rev; verified building on this machine). Bumping the pin is a deliberate act: re-verify, fix drift, note it in the bump commit message.
  • native-theme design (the blueprint we adapt, not a dependency): sparse TOML-shaped Theme (Option fields) resolves into a fully populated ResolvedTheme; bundled presets; AccessibilityPreferences; thin per-toolkit connectors. Its GPUI connector targets gpui-component — a case study only.
  • xdg-desktop-portal Settings (verified against the spec): org.freedesktop.appearance standardizes color-scheme (u), accent-color ((ddd) RGB 0–1), contrast (u), reduced-motion (u), with SettingChanged — one DBus interface covering mode, accent, contrast, and reduce-motion across GNOME, KDE, and generic Linux.
  • Omarchy theme contract (verified against the manual and the shipped tokyo-night theme): active theme is $HOME/.local/state/omarchy/current/theme/colors.toml with theme.name beside it (legacy ~/.config/omarchy/current; current/theme is a symlink into the theme set). colors.toml states mode = "dark" | "light" plus semantic keys — accent, selection, muted, background/dark_background/darker_background/lighter_background, foreground/dark_foreground/light_foreground/bright_foreground, and red/yellow/orange/green/cyan/blue/magenta/brown with bright_*; the ANSI color0..15 format is an accepted alternative.
  • Platform icon systems: macOS SF Symbols — thousands of template glyphs, runtime lookup via NSImage(systemSymbolName:) rendered at a requested size/weight; Apple's license forbids redistributing the set, so it is used at runtime only, never bundled. Windows 11 Segoe Fluent Icons — a system font addressed by codepoint. Linux (and Omarchy) — the freedesktop icon-theme spec: lookup across the active theme (Breeze, Adwaita) and hicolor. Lucide (ISC) is the bundled offline fallback everywhere else.
  • Prior art — gpui-omarchy (huacnlee, MIT): validates the Omarchy adapter design: read colors.toml + theme.name, watch filesystem events for palette edits and theme-symlink replacement, follow_system / explicit apply to pause/resume following, whole-palette fallback to Tokyo Night when files are missing or invalid.

4. Architecture

4.1 Workspace layout (Phase A)

gpui-native-kit/                 # repo root = workspace root
├── Cargo.toml                   # [workspace]; root package = facade
├── rust-toolchain.toml
├── AGENTS.md
├── docs/PLAN.md
├── src/lib.rs                   # gpui-native-kit: thin facade (re-exports, feature-driven)
├── crates/
│   ├── theme/                   # gpui-native-theme: the native theme engine
│   ├── icons/                   # gpui-native-icons: system icons + bundled fallback
│   ├── utils/                   # gpui-native-utils: internal shared helpers (not re-exported by the facade)
│   ├── core/                    # gpui-native-core: components every app needs
│   │                            # (one cargo feature per component)
│   ├── data/                    # gpui-native-data: List, Tree, Tabs, Sidebar (one cargo feature each)
│   └── editor/                  # gpui-native-editor: created when the editor units need it (D12, §6.3 M15)
└── examples/
    └── gallery/                 # showcase; a page per component + theme/icon inspector

Advanced crates (data for Tree/List/Tabs/Sidebar, charts, pickers) are created on demand when their wave starts — not speculative scaffolding.

graph TD
    FAC[gpui-native-kit facade — optional] --> CORE[gpui-native-core]
    FAC --> FUT[editor / data / …]
    CORE --> T[gpui-native-theme]
    CORE --> I[gpui-native-icons]
    T --> P[gpui + gpui_platform — zed, pinned rev]
    I --> P

One-way, no cycles. Nothing depends on gpui-component, gpui-kit, gpuikit, bezel, or native-theme. Applications may depend on gpui-native-core directly and skip the facade.

4.2 Feature model

  • One feature per component in core (button, checkbox, switch, radio, toggle, link, slider, progress, avatar, input, popover, tooltip, menu, dialog, alert, toast, …) and in data (list, tree, tabs, sidebar). default = ["full"]; lean apps use default-features = false + the features they want.
  • Features gate whole modules only (#[cfg(feature = "…")] pub mod button;) and are additive. A component that composes another declares the dependency in Cargo.toml (dialog = ["button"]). No cfg logic may cross modules.
  • Facade features map to crates: core (default), editor, data, …
  • Shared helpers promoted per D16 live in an ungated common module so every feature combination sees them.
  • CI checks three points, not a full matrix: default, --no-default-features, --all-features, plus a compile check of each feature alone.

4.3 Component design

  • One element at a time, review-gated (D20). Each component is one unit: API sketch → implementation → tests → gallery page → review. The next unit starts only after the previous one is approved. No batch waves.
  • API shape follows bezel and gpuikit conventions where they fit — both are read as references only, never ported (D15) — always simplified:
    • Controlled components: the owner passes the current value, the callback reports the next value, the owner updates state and calls cx.notify(). Checkbox::new(id).checked(v).on_change(move |next, window, cx| …).
    • One activation path: pointer, Enter, and Space all fire the same on_click (gpui synthesizes keyboard clicks for tracked focus elements).
    • Semantic states as builders — disabled(bool), selected(bool), checked(bool), pressed(bool) — resolved after the builder chain and painted from theme state colors; disabled resolves last and always wins.
    • accessibility_label(...) on every control; correct accesskit role per control (Button, CheckBox, Switch, RadioButton, Link, …).
    • Composable parts (CheckboxIndicator, SwitchTrack/Thumb) only where a component genuinely needs independently painted pieces — we paint from tokens, so most parts collapse into the component itself; decide per review.
  • Value-like controls are #[derive(IntoElement)] + RenderOnce; stateful ones are Entity<XxxState> the application owns.
  • No style parameters (bezel's model): a component reads the theme global at paint time and takes zero color/size arguments. Overrides are gpui style modifiers chained by the caller.
  • Behavior is inline. Activation, focus, keyboard, dragging, and a11y are written per component with gpui primitives (Interactivity, FocusHandle + tab_stop(true), actions!, a11y builders). A helper is promoted to shared common only when a third component needs it — no pre-built behavior layer.
  • Deliberate divergence from the references: our Link opens href through the platform by default (cx.open_url) — native feel is the product — with on_click as an override for apps that route internally. Neither bezel nor gpuikit opens URLs; we do unless told otherwise.
  • Each component has a named native reference per platform (NSButton / WinUI Button / Adwaita / Breeze) recorded on its gallery page; "native feel" is reviewed against those references.
  • Naming seams per AGENTS.md: is_/has_ readers, with_ builders, set_ mutation, on_ callbacks, State/Delegate suffixes, stable domain-derived ElementIds.
  • Motion is inline and per component (D21). Each component animates with gpui's own primitives (with_animation, with_spring, the gpui easings, with_max_fps) directly in its own file, a few lines at most, using theme durations where they're shared (Motion.overlay). No shared motion module, no ghost panels, no hover-wash plumbing; overlay exit animations and hover washes are dropped until a genuinely simple approach exists.
  • Theme tokens are read via cx.theme(), held when a render can hold it — a component's own render does let theme = cx.theme(); (tooltip-style) and reads theme.x at the paint sites, provided nothing after it needs &mut cx; helpers passed cx: &App hold their own borrow the same way. Never pass &Theme between functions or re-bundle theme values into local structs. Compute repeatedly-needed allocations (shadow lists) once per render where cheap.

4.4 Icons (crates/icons)

  • IconRole — a semantic enum of the glyphs UI actually needs (~25 to start): chevrons, check, close, plus/minus, search, folder, warning/info/success, copy, arrow-left/right, calendar, clock, settings, dots-horizontal, … Components and apps reference roles, never glyph names.
  • Resolver cascade per platform, behind one Icon element:
    • macOS: SF Symbols via NSImage(systemSymbolName:), rasterized through AppKit at (size, weight, appearance, tint) and cached; never bundled. Deferred — macOS rides the Lucide fallback until it shows it matters.
    • Windows: Segoe Fluent Icons codepoints rendered as text (system font). ✅ Landed (2026-10-30): role→codepoint table transcribed from Microsoft's glyph list (Close uses ChromeClose E8BB, not Cancel E711), drawn at the icon's size in Segoe Fluent Icons; unverified on hardware until the Windows screenshot pass.
    • Linux/Omarchy: freedesktop icon-theme lookup (active theme → hicolor), SVG painted via gpui's svg(). ✅ Landed (2026-10-30): bounded walk over $HOME/.icons + the XDG data dirs handling both real layouts (size/context/ for Adwaita and Papirus, context/size/ for Breeze); scalable wins, else the closest numbered size; inherits chain capped at five themes, then hicolor; theme name read from Omarchy's icons.theme → kdeglobals [Icons] → gsettings → Adwaita; results cached per (role, size) for the process; only .svg matches (PNG-only themes fall through); a miss falls back to the bundled Lucide. Resolver fixtures run on every OS.
    • Fallback everywhere: a bundled Lucide subset (ISC; attribution in NOTICE) — ships first so components work before platform resolvers land.
  • Public API stays tiny: icon(role) → element with .size(...); tint defaults to theme tokens. A missing glyph logs once and falls back — never panics.
  • Gallery page: icon browser showing every role resolved on the current platform.

4.5 Theme pipeline

graph TD
    OS[Adapters: macOS / Windows 11 / GNOME / KDE / Omarchy / fallback] --> ST[SystemTheme normalized snapshot]
    PR[ThemeDefinition presets + user overlays] --> RS[resolve]
    ST --> RS
    RS --> RT[ResolvedTheme + AccessibilityPreferences]
    RT --> TK[token projection onto the Theme global]
    TK --> WIN[all windows repaint]
    EVT[runtime change events] -->|re-read, re-resolve, re-install| TK
  1. Read — one adapter per platform produces a normalized SystemTheme (mode, accent, semantic palette, accessibility flags, style hint).
  2. Detect — capability cascade: Omarchy marker → KDE (kdeglobals present) → portal reachable (GNOME/generic Linux) → fallback. macOS/Windows by compile target.
  3. Resolve — overlay chain: platform reader → bundled preset → user theme → runtime overrides → accessibility adjustments. Pure function, unit-testable without a GUI.
  4. Project — ResolvedTheme → the token structs components consume, installed on the theme Global.
  5. Observe — ThemeWatcher forwards real change events (distributed notifications, WinRT ColorValuesChanged, portal SettingChanged, inotify on KDE/Omarchy files); each event re-runs read → resolve → project → repaint.

5. Theme engine (crates/theme)

5.1 Architecture

  • ThemeSource trait — one adapter per platform: snapshot() -> SystemTheme plus watch(callback). Adapters are thin over parseable payloads (ini text, registry dumps, portal replies) so parsing is fixture-testable on any OS.
  • SystemTheme — normalized snapshot: mode, accent, semantic palette, high-contrast, reduce-motion, reduce-transparency, text-scaling, style hint, platform identity.
  • ThemeDefinition — sparse TOML-serializable Option-field struct; bundled presets and user themes share one schema.
  • ResolvedTheme — fully populated; resolve(system, definitions…, prefs) is pure.
  • AccessibilityPreferences — text scaling multiplies font sizes (through rem, all rem-relative sizing); reduce-motion forwards to GPUI; reduce-transparency drops overlay backdrops; high-contrast sharpens borders and removes subtle fills.
  • ThemeWatcher — owns adapter watches, drives the observe step.

5.2 Role mapping

Token role Source
background / foreground window background / default text
surface / surface_foreground cards / secondary areas
primary, secondary, accent button + selection colors (macOS: accent-derived)
muted secondary text and hover shades
destructive danger role
border / input control border line / field background
ring focus ring
selection text selection background

Omarchy colors.toml maps almost 1:1: lighter_background → surface, dark_background/darker_background → elevation depth, dark_foreground → muted foreground, accent → primary/accent/ring, red → destructive, green/yellow/orange → success/warning. Every mapping lives in one module; never literals. Tests assert finite, distinct, semantically sane tokens for every preset × mode.

The set stays role-minimal and grows one concept at a time, driven by the unit that needs it. Known planned additions:

  • Overlay scrim + shadow/elevation tokens — ✅ landed with unit 11 (2026-10-29): colors.overlay (translucent-black scrim, denser in dark; TOML-overridable; no platform states one), Radius.overlay (10 pt), Elevations.popover/dialog (builder → Vec<BoxShadow>), and Motion.overlay (200 ms enter transition). Popover consumes elevation + motion; the scrim waits for Dialog. Per-platform stacks (2026-11-01, D24): Elevation became a two-layer key + ambient stack and resolve picks it per SourceKind — macOS soft and layered (key shadow at 12% alpha), Windows tight and offset, Linux compact and darker; dark mode shares one stack — deleting the Alert's light-mode alpha hack
  • Scrollbar thumb token + metrics — ✅ landed with the ScrollArea unit (2026-10-29): colors.scrollbar_thumb (translucent over content: black in light, light gray in dark; TOML-overridable) and Theme.scrollbar (width 8 pt, inset 2 pt, min_thumb 24 pt).
  • success / warning roles — ✅ landed with Toast (2026-10-29): colors.success/warning (+ foregrounds; light and dark variants; TOML-overridable; no platform states them) and derived Ramps.success/warning via the OKLCH engine, matching the destructive treatment. info joined them (2026-10-31) — the fourth severity, same treatment (colors.info + foreground + Ramps.info, neutral sky blue, no platform states it, TOML info =). Trigger: the Alert's Info severity was painting from the primary ramp, so a warm platform accent (GNOME red, Omarchy red themes) made Info read as Danger; the token keeps Info calm however the accent turns out. Severity tokens now use the same picked value for the ramp and the token (the success/warning tokens used to re-pick with the system slot dropped, diverging from their own ramps if a platform ever stated one).
  • Motion tokens — after the reverted framework attempt (2026-10-30), motion stays minimal: Motion.overlay (200 ms) is the only token. Components reach for gpui's own easings and SpringConfig inline (D21); a token is added only when a third component shares the same value (D16). TOML [motion] overrides and per-platform motion presets wait for a real need. The reduce-motion forwarding fix ✅ landed (pass a, 2026-10-30): reapply calls cx.set_reduce_motion(resolved.accessibility.reduce_motion), so gpui's animation elements snap under the OS setting.
  • knob / control-fill role — resolved without a token: Slider (unit 7) shipped on the Switch's borrowed roles (primary_foreground fill, border off-track); a dedicated role waits for a third consumer.

Controls through unit 10 paint from existing roles and ramps.

5.3 Platform matrix

Target Detection Sources read Live changes
macOS compile target NSColor semantic palette (windowBackgroundColor, labelColor, controlColor, controlAccentColor, selectedTextBackgroundColor, keyboardFocusIndicatorColor, …) in the effective appearance via objc2-app-kit; NSWorkspace accessibility flags distributed notifications + GPUI appearance events
Windows 11 (WinUI) compile target ✅ Landed (2026-10-30): WinRT UISettings (UIColorType Background/Foreground/Accent — the stated AccentLight/Dark variants wait until the engine takes a full ramp, D13), TextScaleFactor scales the root font, AccessibilitySettings high-contrast; Personalize registry fallback (mode + transparency only — the accent key is skipped). WinUI metrics: 4px control radius, 32px field height, 14px root font. Reduce-motion not read yet (Win32 SPI). Verified against the windows 0.62 bindings; a full cross-build waits for hardware (gpui's manifest embedding needs llvm-rc) UISettings.ColorValuesChanged + HighContrastChanged
GNOME / generic Linux portal reachable (else gsettings) portal org.freedesktop.appearance (color-scheme, accent-color as (ddd), contrast, reduced-motion); gsettings fallback (color-scheme name + the libadwaita accent-name table) portal SettingChanged
KDE kdeglobals present kdeglobals [Colors:*] groups (Window/View/Button/Selection) + [General] AccentColor (-1,-1,-1 = wallpaper-derived, covered by the portal); portal for mode/accent inotify on ~/.config, filtered to kdeglobals (survives kconfig's replace-on-write)
Omarchy ~/.local/state/omarchy marker (theme.name, current/theme, or ~/.config/omarchy) current/theme/colors.toml (semantic keys, or the ANSI color0..15 fallback the alacritty generator writes); portal for contrast/reduced-motion; mode from the file's mode key or background lightness inotify on the current dir — catches theme-set's whole-directory swaps and theme.name writes (hand edits of the deployed copy need a switch to re-fire)
Fallback / unknown none of the above, or reader failure bundled neutral light/dark theme manual only

Linux adapters landed 2026-10-30 (fixture-tested parsers on every OS; payload facts transcribed from the portal spec, plasma's Breeze schemes, omarchy's bin/omarchy-theme-set, and libadwaita). The pinned gpui's AsyncApp is not Send, so watcher threads signal through the shared source/signal.rs bridge — an async_channel whose drain loop runs on the main thread via App::spawn — which the Windows events reuse.

5.4 Accent ramp engine

Every target supplies one accent color (or a stated ramp). Where the platform states shades (WinUI AccentLight1–3/AccentDark1–2, KDE scheme ramps) use them as-is; elsewhere derive the state shades components need (hover, pressed, selected, disabled, subtle fills) in OKLCH with fixed lightness/chroma offsets toward the background or foreground, hue preserved. Derived colors compute once per resolve, never per frame.

5.5 Native metrics

Native feel is half geometry. Per-platform tables for: control heights and paddings, corner radii (WinUI control vs card rounding; macOS control radii), font fallback lists resolved through .SystemUIFont (SF Pro / Segoe UI Variable / Adwaita Sans / Noto Sans — no fonts bundled), focus-ring policy (macOS keyboard ring, WinUI two-layer ring, GTK outline), scrollbar style (overlay vs inline), menu metrics (popup elevation landed 2026-11-01 as static per-platform profiles, D24). Motion stays per-component timing on gpui's own primitives; when a platform source needs to state it (macOS NSPopover-class timing, Fluent's decelerate curve, GNOME/KDE shell timings), it states Motion.overlay first and further tokens arrive per the role-minimal rule. Values are transcribed from each toolkit's reference metrics and verified with side-by-side screenshots (macOS in Phase B, the rest in Phase F).

5.6 Custom themes

  • ThemeDefinition TOML: presets and user files share one schema; may override any role or metric, or just the accent (accent = "#7c3aed" → full derived ramp).
  • Merge priority: platform reader → preset → user file(s) → runtime overrides → accessibility adjustments. mode = "system" | "light" | "dark" forces a mode.
  • Sources: --theme <preset|path> flag, a config directory scanned at startup, and an apply API for in-app settings UIs.
  • Omarchy interop: the loader accepts Omarchy colors.toml (semantic or ANSI) directly, so community themes drop in unchanged.
  • Live reload via notify; parse errors surface as a notification, never a panic.
  • Follow-mode API: follow_system(cx) keeps adapting to system changes; explicit apply detaches; system_or_default() takes a one-time snapshot.

5.7 Implementation vehicles

objc2 + objc2-app-kit (macOS), windows (WinRT UISettings), zbus 5 blocking API on a dedicated thread (portal DBus), async-channel (watcher→main-thread reapply bridge), notify (file watches), serde + toml (definitions) — all behind the ThemeSource seam so the engine core stays platform-free and testable.

5.8 What we do differently from native-theme

Real semantic colors per platform (NSColor / UISettings / portal) instead of hardcoded approximations; OKLCH accent-ramp derivation; native metrics as a first-class deliverable; Omarchy as a first-class target; a designed neutral fallback; custom themes with overlay merge priority and live reload; a role-minimal token set instead of a 139-field toolkit mapping.

6. Component roadmap (review units, in order)

Each row is one review unit: build it, show it in the gallery, review, then move on. Units 1–10 ✅ landed (2026-10-29) — Button through Input (§6.2); the rows keep their original scope notes. Rows 11 onward carry their status inline.

Unit Crate Component Notes
1 core Button Variants (primary/secondary/ghost/destructive) painted from token ramps; single activation path; disabled + a11y role. The API template for everything after.
2 core Checkbox Controlled checked + on_change(next); Check icon from the icons crate.
3 core Switch Controlled pill + thumb; thumb slides once motion exists.
4 core Radio + RadioGroup Group owns the selected value; radios never self-uncheck.
5 core Toggle Controlled pressed (aria-pressed).
6 core Link href opens via platform by default; on_click overrides.
7 core Slider Click-to-set + in-track drag; controlled value.
8 core Progress Determinate + indeterminate.
9 core Avatar Initials; image slot later.
10 core Input (single-line) then multiline, NumberInput String buffer + unicode-segmentation word movement; the field owns its own selection. May seed crates/editor. Single-line finishing pass: §6.2.
11.5 core ScrollArea + Scrollbar ✅ Landed (2026-10-29). Inserted before the remaining overlays — Menus, Lists and Tables all scroll. gpui owns the mechanics (overflow_*_scroll + caller-held ScrollHandle); the component paints an overlay thumb (scrollbar_thumb token, Scrollbar metrics) with drag and geometry unit-tested. Wheel input is gpui's; the area stops wheel propagation so nested areas don't co-scroll. Horizontal thumb deferred; auto-hide on idle deferred. ✅ Fixed (2026-10-30): the caller's size refinement now lands on the outer wrapper too — previously it applied to the pane alone, so a percentage-sized pane resolved against an auto-height wrapper and a window-filling scroll area (the gallery's root shell) never actually scrolled; a headless repro of the gallery shell locks the behavior
12 core Menu ✅ Landed (2026-10-29). Controlled open/on_change + trigger/items (MenuItem: label, icon, shortcut hint, disabled, destructive, checked, separator). Popover shell patterns duplicated per D16 (promote at Dialog); panel = surface + overlay radius + popover elevation, role Menu; claims focus while open; keyboard nav — arrows/home/end over selectable items (wrapping, skipping separators + disabled), enter/space activates (closes menu before the action), escape dismisses; hover selects, click activates; trigger clicks toggle (popover-owned); selection resets on each open; long menus scroll via the ScrollArea once a height is styled. Deferred: submenus, type-ahead, scroll-selected-item-into-view, real shortcut handling
11 core Popover ✅ Landed (2026-10-29). First overlay. Controlled open + on_change (Rc-shared handler); trigger/content element closures; panel = surface + Radius.overlay + Elevations.popover + border, role Dialog with label; anchored() + deferred() (window-overflow flip for free); enter fade via gpui with_animation at Motion.overlay (respects reduce-motion); claims keyboard focus while open; escape and click-outside dismiss (outside clicks swallowed). No scrim — native macOS popover model; the overlay token waits for Dialog. Known v1 gaps: no exit animation, trigger aria-expanded left to the app
11+ core Tooltip ✅ (2026-10-29), Dialog + AlertDialog ✅ (2026-10-29), Toast ✅ (2026-10-29), Alert + Toast rework ✅ (2026-10-31) One unit each, same gate. Tooltip rides gpui's own .tooltip() lifecycle (hover delay, anchoring, dismissal — all framework-owned); ours is the styled view (surface + border + Radius.sm, small text) plus a Tooltip::text closure and a Button passthrough. Dialog + AlertDialog (one component, alert(true) switches semantics): scrim (colors.overlay, occluding) + centered panel (surface, Radius.overlay, Elevations.dialog), role Dialog/AlertDialog, focus claimed while open; modal kind closes on escape + scrim click, alert kind is inert to both (the user answers through the actions); actions footer is caller-built elements closing through owner state. The 2026-10-31 rework split the old Toast in two (D23): Alert (alert feature) — a persistent, fully controlled callout banner rendered inline where the app places it: severity icon (Ramps.<sev>.base on a surface + border + Radius.overlay card with the platform popover shadow (D24)), title and message both optional (a message-only banner paints it as the primary line), close button only when on_dismiss is set, reporting intent instead of hiding anything. A pushed, macOS-style stacking Alerts/AlertsView (collapse-slivers + hover-unroll + slide animations) was tried and rolled back the same day — inline-only won. The reworked Toast (toast feature) — one at a time (pushing replaces the one on screen), a succinct severity-less pill at the bottom center: optional icon + one-line truncating content + one optional action (ToastAction, the suggested-action pill) + an always-present close button, laid out as content, small gap, action, large gap, close; fades in and out (Motion.overlay each — exits via a store-held exiting slot whose handler runs once the fade completes). The store keeps GPUI-executor timers (Toast::DURATION 5 s, zero = sticky). Toast landed the deferred success/warning tokens (§5.2); Alert is the info token's consumer (D22). Sheet dropped from the roadmap
14 core + theme Motion (per component) ✅ Landed in five passes (2026-10-30), replacing the reverted same-day framework attempt — no token tower, no motion module, no ghost panels; just gpui's own primitives used inline, one separately reviewed pass per component: (a) theme — reapply forwards reduce_motion via cx.set_reduce_motion, so gpui snaps every animation under the OS setting; (b) Switch — the thumb absolute-positions in the track and slides on with_spring (inline SpringConfig::new(500, 35, 1), Pixels target, mounts at its target so the initial render never animates; velocity survives mid-flight retargets); (c) Toast — each toast slides up 8 px into its stack slot on with_animation at Motion.overlay with ease_out_quint (margin-driven, so only the entering toast moves; keyed per entry so the clock survives unrelated stack re-renders); (d) Dialog — the scrim enters with the panel (same timing and easing, its own dialog-scrim-enter animation id so the clocks start in the same frame); (e) Progress — the indeterminate sweep caps at 30 fps via with_max_fps. Overlay enter fades already existed from units 11/12. Exits and hover washes stay deferred — they were the complexity sink of the reverted attempt (§ deferred list)
then data (new) List ✅ (2026-10-30), Tree ✅ (2026-10-30), Tabs ✅ (2026-10-30), Sidebar ✅ (2026-10-30) Created on demand, same gate. Table dropped from the roadmap. List: controlled single-selection rows (selected: Option<usize> + on_change); click selects and focuses the list, arrows/home/end move selection skipping disabled rows and never wrapping (native list behavior), hover is paint-only, click focus rides a keyed FocusHandle namespaced by the list id; roles List/ListItem with aria_selected and position/size-of-set; internal scrolling left to the caller's ScrollArea; sheet feature dropped from core as dead config. Tree: the caller flattens its tree into depth-annotated TreeItems (bezel's model — the component never walks a tree); the caller owns expansion and receives toggles via on_disclosure(index) (chevron click, or left/right on the selected row); same controlled selection and row paint as List; roles Tree/TreeItem with aria_selected/aria_level/aria_expanded/set-size; indent guides deferred. Tabs: a controlled strip only — selected: usize + on_change; the panel is the app's to render; active tab marked by an underline on the strip's hairline (bezel's model); arrows/left-right/home/end switch tabs with automatic activation, skipping disabled and never wrapping; re-selecting the active tab reports nothing; roles TabList/Tab with aria_selected; no wrap, no segmented-pill variant yet. Sidebar: List with section headers — SidebarEntry::header(label) / row(label).icon().detail().disabled(); headers are never selectable, selection indices address the flat entry list; same selection/keyboard/paint rules as List; roles List/ListItem (a sidebar is a list to accesskit, like macOS AXList); selection stays filled when unfocused (native)
then pickers or data Calendar/DatePicker, TimeField, ColorPicker Decide at unit time.
editor editor (new) Text/code editor, syntax highlighting (D12) Same gate.

Deferred until a real need: Dock (signed ships its own), window-wide text selection, overlay exit animations and hover washes (dropped with the 2026-10-30 revert; only return if a genuinely simple approach appears), charts. Markdown/TextView and VirtualList gained consumers through the signed migration — promoted to §6.3 (M14, M17).

6.1 After the core units: sizing API — ✅ landed

Landed (2026-10-29): the Size enum (XSmall / Small / Medium default / Large / Custom(Pixels)), the Sizable trait (with_size(impl Into<Size>) + xsmall() / small() / large()), and the SizeTable of per-class control metrics (height, padding-x, text size, icon/indicator size, avatar size) now live where they belong — Size/SizeTable/ControlMetrics in the theme (they are native-metric values, §5.5), the trait in core's ungated common.

Our divergence from gpui-kit held: the per-size metrics resolve from the theme's size table at paint time, not from constants in the component layer. All ten controls implement Sizable; Size::Custom(side) derives the other metrics from the side length. Still open, when a real need arrives: TOML overrides for the size table in ThemeDefinition, and per-platform size tables from the Phase F native-metrics work.

6.2 Input completion (unit 10 finishing pass) — ✅ landed (2026-10-29)

Gap analysis against gpuikit's input (2026-10-29; source read as an API reference, §9 — no porting). gpuikit splits input three ways: core state whose edits are actions, infrastructure (bindings / blink / bidi), and a chrome-less element with chrome in wrapper elements. We keep one component with chrome included; only the ideas below are adopted.

Already at parity: buffer + selection + caret, grapheme/word movement, home/end, clipboard, select-all, IME (EntityInputHandler, marked range, UTF-16 conversions), double-click word / shift-click / drag selection, placeholder, disabled, focus ring, a11y TextInput role, TextChanged/Focused/Blurred/Submitted events, set_value + silent variant.

Remaining, in order:

Step Scope Notes
1 macOS modifier split ✅ Landed. cmd-left/right → home/end, cmd-up/down → start/end of field, cmd-backspace → delete to start, alt modifiers → word ops. The handler used to treat cmd and alt alike — wrong on macOS (D8); ctrl stays the word-modifier fallback off-mac
2 Undo/redo ✅ Landed. Patch-based history (range, replaced text, new length, prior selection), 300 ms grouping (set_group_interval, Duration::ZERO disables merging), cmd-z / cmd-shift-z, 100-entry cap; set_value clears the stacks, any new edit clears redo. Unlike gpuikit — which skips recording inside the interval and so undoes groups lossily — entries merge so a group fully reverses. Redo lands the caret at the end of the restored text; undo restores the pre-edit selection
3 Read-only, Escape-blur, triple-click ✅ Landed. Read-only: focus, movement, selection, copy still work; edits, paste, cut (clipboard included), undo/redo and IME refused — distinct from disabled; programmatic setters unaffected; the caret is hidden while read-only (a caret promises typing lands there). Escape → window.blur(). Triple-click selects all (single-line)
4 Actions + key context ✅ Landed. 25 actions!(input, …) + an "Input" key context; the element registers on_action handlers and the residual on_key_down only inserts printable typed chars. InputBindings table of Option<KeyBinding> fields — Default is platform defaults (cmd/alt on macOS, ctrl elsewhere), empty() starts bare, any field replaceable/unbindable; `bind_input_keys(cx, None
5 Caret blink ✅ Landed. A private CaretBlink entity per InputState: 500 ms tick on the GPUI executor, visible on focus and hidden after one interval. Any field change (InputState::notify) pauses it — caret solid, cycle restarts — so typing, moving and dragging never hide it; generation counters invalidate stale ticks. Read-only suppresses the caret entirely. caret_visible(focused, cx) is the single gate used by paint and tests
6 Paint polish ✅ Landed. IME composition underline (1 px, foreground token) under the marked range. I-beam hover cursor via a canvas-prepaint hitbox + set_cursor_style in paint. Two-sided horizontal scroll: scroll_offset lives in state; caret-affecting changes set a pending flag that the next paint consumes with a fresh layout (two-sided pull-back + clamp to content), while scroll_by (wheel, ScrollEvent, lines→pixels via a fixed unit) bypasses the flag so manual pans survive. Scroll-aware hit-testing, IME bounds_for_range and drag select all come free through the scroll-adjusted origin

Deferred until a real need: bidi/RTL; prefix/suffix adornments; SubmitOn enter/cmd-enter modes (multiline concern); Undo/Redo events; selection/cursor color overrides (theme tokens are deliberate); max length, clear button (gpuikit has none of these either — decide on first need). Password masking and the form field wrapper lifted 2026-11-01 — §6.3 M6/M9.

6.3 Migration-driven additions — signed is the first downstream app

The first real consumer is signed, a Nostr Git platform desktop client (GPL-3.0, v0.2.0) currently built on gpui-pre 0.3.8 + gpui-component 0.7.1 + native-theme 0.6.1. Migrating it means the zed-pinned gpui under D3, gpui-native-theme in place of native-theme(-gpui), and our components replacing gpui-component view by view; signed_ui, workspace, and its dock stay app-side (dock stays off the roadmap — the app owns it). Its screens drive the units below. A survey of bezel's full catalog (bezel.gallery docs, 2026-11-01) and gpuikit's showcase source shaped the shapes, not the scope — API ideas only per D15.

Wave M1 — unblocks the port; each through the review gate:

Unit Crate Component Signed consumer Survey notes
M1 core Badge ✅ Landed (2026-11-01) unsynced-count pills on sidebar repo rows bezel ships quiet + active variants; gpuikit secondary/outline/destructive. Shipped five semantic variants — Neutral (muted fill), Accent, Success, Warning, Danger (the ramps) — painted from existing roles, no badge palette (D22); pill = Radius.full, size-table text at 0.8×, caller-refinable via Styled
M2 core Select ✅ Landed (2026-11-01) default-branch chooser, event-fetch-strategy setting, explore filter bezel: field-shaped trigger over a caller-assembled menu; gpuikit: placeholder + clear. Shipped as a controlled value (selected(Option<T>) + on_change(Option<T>), tuples in) with the popup owned per instance via element-namespace-keyed state; composes Menu — field-shaped trigger (input/border/ring tokens, Role::ComboBox), options as checked items. Menu gained initial_selection so a popup opens keyboard-highlighted on the current value, like native popups, and its panel dropped the max_h(relative(0.75)) fraction — resolved against the wrong box inside nested flex/scroll shells, it clipped the last items (found in the gallery); row height is governed by the ScrollArea's 320 px cap and anchored handles window fit. Feature select = ["menu"]. Follow-ups (2026-11-01): Menu's check indicator moved to the row's right end (Adwaita dropdown look; the optional icon keeps the leading slot), and Select options gained optional leading icons — SelectOption::new(value, label).icon(role), with (value, label) tuples still accepted through From
M3 core Segmented control ✅ Landed (2026-11-01) All/Popular/Recent explore filters bezel's toggle-group pill; single-select only, multiple waits. Shipped as a controlled single-select (Segmented::new(id, selected) + options((value, label)) + on_change(T) — exactly one segment always active); muted pill track, the selected segment on a raised surface plate with border; left/right/home/end move and activate when the strip has focus (no wrap), RadioGroup/RadioButton roles with aria_toggled; renders hugging via a wrapper so it works in block and flex parents alike. Plain segmented feature, no deps. Follow-ups (2026-11-01): the selected plate dropped its border for a small raised shadow — a third Elevations.control class, profiled per platform like popover/dialog (D24); Size::Small became the default and medium/large resolve through the size table — taller tracks (control_height × 1.15 / 1.35), a scaled track inset (0.125 / 0.25 / 0.375 rem) and segment padding (0.5× / 0.75× / 0.75× of padding_x), with the large label staying at the medium size; options take optional leading icons (SegmentedOption::new(value, label).icon(role), tuples still accepted through From); and the selected plate is now a measured, absolutely-positioned layer that springs between segments (per-segment measure canvases feed keyed geometry; the plate springs on left and width separately, Switch-style — mounts at its target so the first frame never animates, retargets mid-flight, and snaps under reduce-motion). Follow-up 2 (2026-11-01): the track inset also scales per size (0.125 / 0.25 / 0.375 rem), and a settled-plate regression test guards the plate's existence and slot (debug_bounds("segmented-plate"))
M4 core Context menu ✅ Landed (2026-11-01) repo rows, file-tree rows, tab bar gpuikit attaches the menu to an existing element; bezel keys one-open-at-a-time. Shipped as a self-contained ContextMenu::new(id).target(el).items(...) — open state and pointer position are namespaced keyed state, right-click opens at the pointer, left clicks pass through to the target untouched, and any click outside, escape, or activation closes. Composes Menu, which gained trigger_button(MouseButton) and open_at(Point) for pointer-anchored opening (outside-click dismissal now closes on any button). Plain context_menu feature, depends on menu
M5 core Empty state ✅ Landed (2026-11-01) inbox, issues, search misses bezel: icon + headline + one line of hint. Shipped as EmptyState::new(headline) with optional .icon(role), .hint(text), and .action(element) — no id (nothing interactive), no size classes (content, always medium metrics); the illustration is the component's own icon_size × 2 decoration, muted like the hint, line_height 1.3 on the headline, everything centered. Plain empty_state feature, no deps
M6 core Password input ✅ Landed (2026-11-01) identity unlock + nsec import passphrase Lifts §6.2's "password masking on first need" — the need arrived. InputState::set_password(bool) masks the painted line one bullet per grapheme (paint-only: the buffer, undo, and caret all stay real; a mask map of grapheme byte offsets converts between the buffer and the painted display for hit-testing, the caret, selection, the IME underline, and bounds_for_range). Masked fields refuse copy and cut so the secret never reaches the clipboard; paste, editing, and reveal stay available. No element builder — the flag is state, like read-only. Follow-up (2026-11-01): a built-in reveal toggle — an eye button (IconRole::Eye/EyeOff, native glyphs per platform) pinned inside the field's right edge, shown on every password field. The state now tracks the field kind (set_password/is_password) and its visibility (set_revealed/is_revealed) separately, so toggling reveal never changes the field's kind; the paint reserves the button's corner (right_padding threaded through paint/store_layout, with a content mask so text never runs under the icon)
M7 input Multiline textarea discussion composer, patch + commit messages bezel: fixed rows or min/max auto-grow; gpuikit: rows on InputState. Reuses Input's buffer; D12's editor crate stays separate
M8 core Avatar image profile pictures in rows and panel tabs lifts the deferred image slot: gpui img + initials fallback (signed adds its pixel-art fallback app-side)

Wave M2 — completes the surface: Field wrapper (M9: label/description/error around any control — the settings dialog; lifts another §6.2 deferral), Tag (M10: removable GRASP-server and relay entries), Status dot (M11: sync-drift beads on rows), Combobox (M12: type-to-filter pickers; gpuikit's on_create/keep_unmatched blur modes are worth copying), Command palette (M13: filtered actions — gpuikit keeps the matcher caller-owned, adopt that), VirtualList in data (M14: a thin uniform_list binding for commit histories and the inbox).

Wave M3 — content, where crates/editor starts (D12): code view with syntax highlighting (M15 — signed renders highlighted files with tree-sitter today), then the color-coded diff view (M16), then a Markdown view (M17) if the port confirms READMEs and issue bodies need one.

Survey notes — worth adopting later, not now: bezel's hover card (profile previews), skeleton rows, breadcrumb, pagination, sortable lists, follow-scroll; gpuikit's table (stays dropped — signed's explore lists read fine as Sidebar rows), kbd chips (our Menu already renders shortcut hints), form fieldset (the Field wrapper covers it), accordion.

7. Milestones

Phase Scope Acceptance
A — scaffold ✅ Workspace per §4.1: theme stub, icons crate (IconRole + Lucide fallback), core crate with feature scaffolding (zero components), facade, gallery shell, CI incl. feature checks gallery opens a window reading the theme global; core builds at default / no-default / all / each-feature-alone
B — theme engine core ✅ Token model + resolve + presets (neutral fallback, tokyo-night) + macOS source + ThemeWatcher; gallery theme inspector. Done: resolve overlay chain (base → system → preset → mode override → high-contrast) is pure and tested; OKLCH accent-ramp engine; NSColor/NSWorkspace adapter; distributed-notification watchers; inspector with live mode/preset switching. (The "Button + Checkbox painted from live tokens" proof moved to Phase C — those components landed there; the gallery inspector proves the live pipeline meanwhile.) ✅ Landed (§12 item 2)
C — controls ✅ One review unit at a time (§6, units 1–9), each: API per §4.3 + tests + gallery page → review gate. macOS SF Symbols resolver behind the icon seam lands with the first unit that shows it matters Per unit: activation, controlled round-trip, and disabled inertness tested; gallery page shows the live control against the theme; your approval closes the unit
D — input ✅ Single-line Input (unit 10) + the §6.2 completion pass; NumberInput stays deferred (multiline is §6.3 M7) Modifiers, undo/redo, read-only, actions + key context, caret blink, paint polish all landed
E — overlays ✅ Popover (11), ScrollArea (11.5), Menu (12), Tooltip + Dialog/AlertDialog + Toast/Alert (11+ row) Escape/dismiss semantics and focus behavior verified per overlay
E+ — motion Unit 14 (§6): per-component passes, each a few lines on gpui's own primitives — forwarding, Switch thumb, Toast slide, Dialog scrim, Progress cap ✅ Landed (2026-10-30), each pass reviewed in the gallery; reduce-motion snaps gpui-side with the flag forwarded
F — platforms + data ✅ Landed (2026-10-30): crates/data (List, Tree, Tabs, Sidebar), the Linux theme sources (Omarchy/KDE/GNOME), the Windows source, the Segoe Fluent + freedesktop icon resolvers, plus per-platform elevation stacks (D24, 2026-11-01) Remaining acceptance: side-by-side gallery screenshots — a Linux run (Omarchy/KDE/GNOME) and the first Windows hardware build
G — migration additions (§6.3) Waves M1–M2: Badge, Select, Segmented, Context menu, Empty state, password + multiline input, Avatar image, then M9+ in signed's port order → 0.1 Each unit through the review gate; each wave unblocks a real signed screen
H — signed migration Port signed to gpui-native-kit: the zed-pinned gpui replaces gpui-pre 0.3.8, gpui-native-theme replaces native-theme(-gpui) 0.6.1, our components replace gpui-component 0.7.1 view by view behind signed_ui One screen per pass, reviewed side by side against the gpui-component build; no regressions in identity, explore, repository, sync, or settings flows

Gate for every phase: cargo fmt --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test, gallery smoke run.

8. Testing

Pure tests for resolve/overlay chain, payload parsing (fixtures), icon resolution tables, and metrics; context tests for the theme global lifecycle; headless UI tests for activation, controlled round-trips, focus, disabled; gallery smoke run. GPUI executor timers in tests. Only meaningful tests — state semantics, a11y mapping, invariants (AGENTS.md Tests rule).

9. Licensing

  • All code in this repo is original: Apache-2.0 (D6).
  • gpui: Apache-2.0 dependency, nothing copied.
  • bezel and gpuikit: API-design references (D20); inspiration only, no code copied. gpui-kit is no longer consulted (D20). If that ever changes, respect their licenses.
  • Lucide icons: ISC — bundled subset with attribution in NOTICE.
  • SF Symbols and Segoe Fluent Icons: used through the operating system at runtime only; never extracted or redistributed.
  • native-theme, gpui-omarchy, gpuikit, bezel: inspiration only; no code copied. If that ever changes, respect their licenses.

10. Risks

Risk Mitigation
zed main churns gpui APIs Pinned rev; bumps are deliberate (re-verify, fix, note in commit message); CI catches drift early
Writing our own input/editor is the biggest lift Single-line first; no LSP/highlighting/composition until needed; selection stays private to the field
Per-component features multiply configurations Features gate whole modules, additive only; CI checks default / none / all / each-alone, not the full matrix
Platform icon lookups vary (missing fonts, themes) Resolver cascade always ends in the bundled Lucide fallback; missing glyph logs once and falls back
Per-OS readers are a maintenance burden Thin readers over parseable payloads + fixture tests; graceful fallback chain (system → preset → fallback theme)
The signed migration crosses gpui distributions (gpui-pre 0.3.8 → zed pinned rev) Port incrementally behind the signed_ui seam; a kit-side unit lands before its screen ports (§7 Phase G → H); the gallery doubles as the API-parity reference
"Native feel" is judgment Named native reference per component; per-platform screenshot review each phase
Omarchy colors.toml key set evolves Parse tolerantly (unknown keys ignored, missing keys fall back); track the theme-set script each release cycle
Portal accent-color absent on older desktops Treat as unset (spec-defined); fall back to gsettings/KDE accent, then ramp from a neutral accent

11. Decisions

# Decision
D1 Custom theme engine inspired by native-theme; no native-theme dependency
D3 gpui from zed's repository only, pinned rev; no gpui-pre, no forks, no gpui-unofficial
D4 Desktop only; no WASM
D6 Apache-2.0 for this repo's code
D7 gpui-native-kit stays the thin facade package name; it only re-exports crates behind features. Applications may depend on gpui-native-core directly
D8 macOS first, then Windows 11 (WinUI), then GNOME → KDE → Omarchy; fallback theme from day one
D11 Bundled presets: neutral fallback, tokyo-night, per-platform presets for testing readers; Omarchy files load as presets too
D12 Editor deferred; crates/editor is created when the editor units start — the signed migration's file viewing is the trigger (§6.3 M15–M17)
D13 Accent shades hybrid: stated platform ramps where available (WinUI, KDE), OKLCH derivation elsewhere
D14 Custom-theme live reload via notify
D15 No wholesale porting, no UI-toolkit dependencies. Component APIs follow bezel and gpuikit conventions where they fit (D20), always simplified — never mirrored 1:1; small utilities may be adapted with attribution
D16 No separate behavior crate; behavior lives in each component; shared helpers promoted at the third use
D17 UPSTREAM.md removed; provenance = §3 pins + git history
D18 Crate split for consumption: core (every-app components) → editor, data, … (advanced) → thin gpui-native-kit facade; one cargo feature per component, additive, module-gated
D19 System icons via semantic IconRoles with per-platform resolvers (SF Symbols / Segoe Fluent Icons / freedesktop) and a bundled Lucide fallback; platform glyph sets are used at runtime only, never bundled
D20 Components land one element at a time behind a review gate: API sketch → implementation → tests → gallery page → review → next. bezel and gpuikit are the API-design references for these units; conventions like controlled state, one activation path, semantic states, and a11y labels are adopted, simplified. gpui-kit is no longer consulted — judged over-engineered for our needs
D21 Motion is per component and inline: each component uses gpui's own animation/spring primitives (with_animation, with_spring, the gpui easings, with_max_fps) directly in its file; the theme owns only the durations components actually share (today Motion.overlay), growing per the role-minimal rule. No shared motion module, no exit ghosts, no hover-wash machinery — exits and washes stay out until a genuinely simple approach exists. Reduce motion is gpui-side: forward the OS flag once in reapply and every primitive snaps. Supersedes the reverted 2026-10-30 framework attempt (which proved the substrate but over-built the plumbing)
D22 The color set does not expand along gpuikit's Themeable shape (evaluated 2026-10-31): their per-component token layers (button_*, input_*) are passthroughs of their own primitives — a drift-prone parallel layer our role-plus-ramp model replaces with OKLCH-derived hover/pressed shades; depth variants (surface_secondary), the badge palette, and fg_disabled wait for real consumers (Badge, nested elevation, a theme that can't express disabled by opacity). The one adoption: info as the fourth severity — the Alert's Info severity had a live consumer painting from the primary ramp, which collides with Danger under warm accents. Domain palettes (editor diff colors) may revisit gpuikit's ThemeExtension idea when crates/editor lands
D23 The old Toast split into Alert + Toast (2026-10-31): one component can't be both a persistent page callout and a fleeting bottom-center nudge — different lifetime (persists until acted on vs auto-dismisses), different placement (inline in flow vs floating bottom center), different content shape (title + message + severity tint vs one line + icon + action). Alert is fully controlled — the app renders it while the condition holds and its close button reports intent; Toast keeps the store + timers. Toast drops severity and titles: Adwaita toasts are single-line nudges, and severity belongs to the banners that stay on the page
D24 Shadows follow the platform's design language (2026-11-01): in-app shadows are painted by gpui, so "native" means each platform's shadow values — Elevation is a two-layer key + ambient stack and resolve picks it per SourceKind (macOS soft and layered at 12% alpha, Windows tight with more offset, Linux compact and darker; one shared dark stack where borders do the separation work). Static design constants, never OS-read (no platform API states shadow values); replaces the Alert's light-mode alpha hack, which was a single-profile workaround
D25 Components fetch, never scale (2026-11-01): every derived metric a component kept re-deriving from root metrics — dense label text (×0.8/0.85), dense inline icons (×0.75), dense horizontal padding (×0.5/0.75/2), plate insets (0.125/0.25/0.375 rem), strip band heights (×1/1.15/1.35) — moved into the per-size-class ControlMetrics table (caption_size, dense_icon_size, dense_padding_x, plate_inset, strip_height, plus indicator_size for checkboxes, radios, and switch knobs), computed once in with_root_font_size. The table stays role-based — one value per concept per size class, shared by every component — not gpuikit's per-component token layers (D22). Component-unique geometry that has no second consumer keeps its ratios over root metrics (switch track length, slider knob, avatar initials, tree indent). Two icon roles are fetch-only: content icons (alert info, segmented options) take icon_size — never smaller than the label — and indicator glyphs (checks, chevrons, close buttons) take dense_icon_size

Dropped from rev 3: D2, D5, D9 (port-era decisions); the review gate returns in a lighter form as D20. D10 (no icon bundling) is superseded by D19.

12. Immediate next steps

  1. Phase A scaffold ✅ — workspace, toolchain pin, CI, theme seed, icons crate, core feature scaffolding, facade, gallery shell.
  2. Phase B theme engine core ✅ — resolve pipeline, OKLCH ramps, presets, macOS source + watchers, gallery theme inspector.
  3. Phase C controls ✅ — units 1–9 (Button → Avatar), each through the review gate; sizing API (§6.1).
  4. Phase D input ✅ — single-line Input (unit 10) + the §6.2 completion pass (modifiers, undo/redo, read-only, actions + key context, caret blink, paint polish). NumberInput stays deferred; multiline returns as §6.3 M7.
  5. Phase E overlays ✅ — Popover (unit 11), ScrollArea (unit 11.5), Menu (unit 12), Tooltip + Dialog/AlertDialog + Toast (unit 11+ row; Sheet dropped). Phase gate held: escape/dismiss semantics and focus behavior per overlay.
  6. Unit 14 — motion, per component (§6 row 14, D21) ✅ Landed in five passes (2026-10-30): (a) theme reduce_motion → cx.set_reduce_motion in reapply; (b) Switch thumb with_spring; (c) Toast 8 px slide-in; (d) Dialog scrim enters with the panel; (e) Progress 30 fps cap. Exits and hover washes stay deferred (§6 deferred list).
  7. Phase F, data first ✅ data complete (2026-10-30) — crates/data opened with List, then Tree, Tabs, Sidebar (the dead sheet feature dropped from core along the way; Table dropped from the roadmap). Also extracted crates/utils (2026-10-30): h_flex/v_flex (core's common re-exports them), focus_handle (the namespaced keyed handle, 8 call sites), focus_ring (7 sites), adjacent_selectable + nav_key (4 sites), and the selectable-row paint helpers selectable_label_color/paint_selectable_row (3 sites, reading &Theme directly — no intermediate color struct), plus the StyledExt trait (refine_style — adapted from gpui-kit base, D15 — replacing the per-component style().refine pattern; 5 sites) — core and data depend on it; the facade doesn't re-export it. Linux theme sources landed (2026-10-30): Omarchy (colors.toml parse + state-dir detection + inotify on current), KDE (kdeglobals [Colors:*] + AccentColor + ~/.config inotify), GNOME/generic (portal ReadAll + SettingChanged via zbus 5 blocking on its own thread, gsettings fallback with the libadwaita accent table) — SourceKind::Omarchy/Kde/Gnome, detection cascade Omarchy → KDE → portal → fallback, all parsers fixture-tested on every OS and the Linux build compile-checked via cargo check --target x86_64-unknown-linux-gnu. Windows source + icon resolvers landed (2026-10-30): SourceKind::Windows (WinRT UISettings + Personalize fallback + ColorValuesChanged/HighContrastChanged, §5.3), the Segoe Fluent codepoint renderer, and the freedesktop theme walker (§4.4); the WinRT surface was verified against the real windows 0.62 bindings via a standalone cross-check (gpui's own manifest build needs llvm-rc from macOS). Remaining Phase F: side-by-side gallery screenshots per platform — Omarchy/KDE/GNOME need a Linux run, Windows needs its first hardware build. NumberInput stays deferred until needed; multiline is §6.3 M7.
  8. Then — Phase G is the migration additions (§6.3): Badge, Select, Segmented control, Context menu, Empty state, password input, multiline Textarea, Avatar image first — each unblocks a concrete signed screen. Pickers (Calendar/DatePicker, TimeField, ColorPicker) stay decided-at-unit-time. crates/editor starts when the port reaches file viewing (M15).
  9. Then — Phase H, the signed migration itself (§7): theme swap first (native-theme → gpui-native-theme), then views one at a time, keeping the gpui-component build side by side until parity.

Batched polish candidates between units: scrollbar auto-hide + horizontal thumb, Undo/Redo menu events.