332 lines
47 KiB
Markdown
332 lines
47 KiB
Markdown
# 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`](https://github.com/longbridge/gpui-kit/tree/main/crates/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:
|
||
```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
|
||
│ ├── 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.
|
||
|
||
```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
|
||
|
||
- **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 `ElementId`s.
|
||
- **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
|
||
|
||
```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.
|
||
|
||
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 (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 an `async_channel` and a main-thread `App::spawn` loop reapplies.
|
||
|
||
### 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` 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.
|
||
|
||
| 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 `TreeItem`s (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 | table)` registers at startup (gallery + tests call it). Verified gpui semantics first: matched bindings consume the keystroke before `on_key_down`, and actions claim by default in bubble (`cx.propagate()` yields — Copy does when the selection is empty). macOS jump extras (`cmd-left/right/up/down`, `cmd-shift-left/right`) are appended in `into_bindings`, not field-configurable |
|
||
| 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 `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 |
|
||
| 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. **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`. Remaining Phase F: the **Windows source (WinRT `UISettings`)** and the **Segoe Fluent/freedesktop icon resolvers**, slotted in as verification environments allow — 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.
|