Files
gpui-native-kit/docs/PLAN.md
T
2026-10-28 09:34:22 +07:00

260 lines
24 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```toml
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)
```text
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.
```mermaid
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 `ElementId`s.
### 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
```mermaid
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 `IconRole`s 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~~ ✅ — workspace, toolchain pin, CI, theme seed (neutral light/dark presets + `Theme` global + appearance sync), icons crate (`IconRole` + Lucide fallback + `IconAssets`), core feature scaffolding, facade, gallery shell.
2. Phase B theme engine core, macOS adapter first.
3. Then wave 1 controls.