Files
gpui-native-kit/docs/PLAN.md
T
2026-10-30 21:03:06 +07:00

45 KiB
Raw Blame History

gpui-native-kit — Project Plan

Rev 6 (2026-10-28). Changes from rev 5: 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 wave 2 needs it (D12)
└── 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, toast, list, tabs, …). 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.
    • Windows: Segoe Fluent Icons codepoints rendered as text (system font).
    • Linux/Omarchy: freedesktop icon-theme lookup (active theme → hicolor), SVG painted via gpui's svg().
    • 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 (transcribed NSPopover-class shadows, builder → Vec<BoxShadow>), and Motion.overlay (200 ms enter transition). Popover consumes elevation + motion; the scrim waits for Dialog.
  • 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.
  • 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 — only if Slider (unit 7) confirms the Switch's borrowed roles (primary_foreground thumb, border off-track) as a second consumer; until then no expansion.

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 WinRT UISettings (AccentColor, AccentLight1–3, AccentDark1–2, Background, Foreground), TextScaleFactor, AccessibilitySettings high-contrast; Personalize registry fallback UISettings.ColorValuesChanged
GNOME / generic Linux portal reachable portal org.freedesktop.appearance (color-scheme, accent-color, contrast, reduced-motion); gsettings fallback portal SettingChanged
KDE kdeglobals present kdeglobals color groups + scheme hash + accent; portal for mode/accent inotify on kdeglobals
Omarchy ~/.local/state/omarchy or ~/.config/omarchy marker current/theme/colors.toml + theme.name (semantic keys or ANSI color0..15); portal for contrast/reduced-motion inotify on the current dir (palette edits + symlink replacement)
Fallback / unknown none of the above, or reader failure bundled neutral light/dark theme manual only

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), popup elevation and menu metrics. 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 (portal DBus), 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.

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) 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. Toast: Toasts store entity (push/dismiss/clear + auto-dismiss timers on the GPUI executor, Toast::DURATION 5 s, zero = sticky) + ToastsView deferred stack (top-right, Radius.overlay + popover elevation, severity icon from the new success/warning ramps, close button, on_dismiss windowless handler). Toast landed the deferred success/warning tokens (§5.2). 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: Markdown/TextView, Dock, VirtualList, 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.

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 and a form field wrapper (label/description/error — separate units); SubmitOn enter/cmd-enter modes (multiline concern); Undo/Redo events; selection/cursor color overrides (theme tokens are deliberate); password masking, max length, clear button (gpuikit has none of these either — decide on first need).

7. Milestones

Phase Scope Acceptance
A — scaffold (next) 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 moves to Phase C — those components land there; the gallery inspector proves the live pipeline meanwhile.)
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 Wave 2 Single-line input with cursor, selection, clipboard; NumberInput
E — overlays Wave 3 Focus trapping and escape/dismiss semantics 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 crates/data + Windows/GNOME/KDE/Omarchy theme sources + Segoe Fluent/freedesktop icon resolvers Side-by-side gallery screenshots per platform
G — pickers + polish Wave 5 → 0.1; start crates/editor if wanted —

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)
"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 wave 2's multiline editing or highlighting demands it
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)

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 and multiline remain open as deferred units.
  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) — core and data depend on it; the facade doesn't re-export it. Remaining Phase F: Windows/GNOME/KDE/Omarchy theme sources and the Segoe Fluent/freedesktop icon resolvers, slotted in as verification environments allow — a source may state Motion.overlay when it needs platform timing (§5.5) — with side-by-side gallery screenshots per platform. NumberInput/multiline return as deferred units when needed.
  8. Then — Phase G: pickers, polish, → 0.1; start crates/editor if wanted.

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