Files
gpui-native-kit/docs/PLAN.md
T
2026-10-28 08:54:15 +07:00

24 KiB
Raw Blame History

gpui-native-kit — Project Plan

Rev 5 (2026-10-28). Changes from rev 4: the workspace is split into focused crates — theme, icons, core, and sibling crates for advanced elements (editor, data, …) — with per-component cargo features so applications enable only what they use. System icons are now in scope: semantic roles resolved to SF Symbols / Segoe Fluent Icons / freedesktop icons per platform, with a bundled Lucide fallback. Everything else (empty-project reset, own lean implementations, theme engine as the product) 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 + styled component library Dropped: over-engineered base; review-gated porting too slow; upstream pins gpui-pre snapshots Nothing; previous ports reverted
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 nothing. These crates (plus native-theme, D1) are read for API and design ideas only — 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
│   ├── core/                    # gpui-native-core: components every app needs
│   │                            # (one cargo feature per component)
│   └── 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 Table/Tree/List, 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

  • Controlled components: the owner passes the current value, callbacks report intent, the owner updates state and calls cx.notify(). No second source of truth.
  • 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.
  • APIs are our own, shaped by what our components need; inspiration from other crates is distilled, never copied 1:1.
  • 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.

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.

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. 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 (written from scratch)

Wave Crate Components
1 — controls core Button, Checkbox, Switch, Radio + RadioGroup, Toggle, Link, Slider, Progress, Avatar; plus the IconRole catalog + Lucide fallback
2 — input core Input (single-line first, then multiline), NumberInput. String buffer + unicode-segmentation word movement; the field owns its own selection. Multiline editing may seed crates/editor.
3 — overlays core Popover, Tooltip, Menu, Dialog, AlertDialog, Sheet, Toast
4 — data & navigation data (new) List, Table, Tree, Tabs, Sidebar
5 — pickers pickers or data (decide at wave time) Calendar/DatePicker, TimeField, ColorPicker
editor editor (new) Text/code editor, syntax highlighting (D12)

Deferred until a real need: Markdown/TextView, Dock, VirtualList, window-wide text selection, a motion system (use gpui's built-in animations first), charts.

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 Button + Checkbox painted from live macOS system tokens; flipping system appearance repaints live
C — controls Wave 1 in core, one feature each; macOS SF Symbols resolver behind the icon seam Each control: tests + gallery page + native-reference screenshots; icon browser shows per-role resolution
D — input Wave 2 Single-line input with cursor, selection, clipboard; NumberInput
E — overlays Wave 3 Focus trapping and escape/dismiss semantics per overlay
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.
  • 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 porting, no UI-toolkit dependencies; inspiration only, always simplified — never mirrored 1:1
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

Dropped from rev 3: D2, D5, D9 (port-era decisions). D10 (no icon bundling) is superseded by D19.

12. Immediate next steps

  1. Phase A scaffold (§4.1, §7): workspace, toolchain pin, CI, theme stub, icons crate with IconRole + Lucide fallback, core feature scaffolding, facade, gallery shell.
  2. Phase B theme engine core, macOS adapter first.
  3. Then wave 1 controls.