Files
gpui-native-kit/docs/PLAN.md
T
2026-10-30 09:54:59 +07:00

41 KiB
Raw Blame History

gpui-native-kit — Project Plan

Rev 6 (2026-10-28). Changes from rev 5: components are built one element at a time behind a review gate — each unit is designed, implemented, tested, shown in the gallery, and reviewed before the next starts (batch waves are gone). gpui-kit base is re-admitted as the API-design reference: its unstyled-control conventions (controlled state, one activation path for pointer + Enter + Space, semantic states, a11y labels) shape our component APIs, and small utilities may be adapted where they help. Everything else (empty-project reset, theme engine as the product, crate split, per-component features) stands.

1. Vision

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

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

2. Evaluated foundations (2026-10-28)

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

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

3. Background facts (verified)

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

4. Architecture

4.1 Workspace layout (Phase A)

gpui-native-kit/                 # repo root = workspace root
├── Cargo.toml                   # [workspace]; root package = facade
├── rust-toolchain.toml
├── AGENTS.md
├── docs/PLAN.md
├── src/lib.rs                   # gpui-native-kit: thin facade (re-exports, feature-driven)
├── crates/
│   ├── theme/                   # gpui-native-theme: the native theme engine
│   ├── icons/                   # gpui-native-icons: system icons + bundled fallback
│   ├── 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

  • 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 gpui-kit base conventions (adapted, simplified where we can):
    • 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 base: 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. Base never opens URLs; we do unless told otherwise.
  • Each component has a named native reference per platform (NSButton / WinUI Button / Adwaita / Breeze) recorded on its gallery page; "native feel" is reviewed against those references.
  • Naming seams per AGENTS.md: is_/has_ readers, with_ builders, set_ mutation, on_ callbacks, State/Delegate suffixes, stable domain-derived ElementIds.

4.4 Icons (crates/icons)

  • IconRole — a semantic enum of the glyphs UI actually needs (~25 to start): chevrons, check, close, plus/minus, search, folder, warning/info/success, copy, arrow-left/right, calendar, clock, settings, dots-horizontal, … Components and apps reference roles, never glyph names.
  • Resolver cascade per platform, behind one Icon element:
    • macOS: SF Symbols via NSImage(systemSymbolName:), rasterized through AppKit at (size, weight, appearance, tint) and cached; never bundled.
    • Windows: Segoe Fluent Icons codepoints rendered as text (system font).
    • Linux/Omarchy: freedesktop icon-theme lookup (active theme → hicolor), SVG painted via gpui's svg().
    • Fallback everywhere: a bundled Lucide subset (ISC; attribution in NOTICE) — ships first so components work before platform resolvers land.
  • Public API stays tiny: icon(role) → element with .size(...); tint defaults to theme tokens. A missing glyph logs once and falls back — never panics.
  • Gallery page: icon browser showing every role resolved on the current platform.

4.5 Theme pipeline

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

5. Theme engine (crates/theme)

5.1 Architecture

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

5.2 Role mapping

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

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

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

  • Overlay scrim + shadow/elevation tokens — ✅ landed with unit 11 (2026-10-29): colors.overlay (translucent-black scrim, denser in dark; TOML-overridable; no platform states one), Radius.overlay (10 pt), Elevations.popover/dialog (transcribed NSPopover-class shadows, builder → Vec<BoxShadow>), and Motion.overlay (200 ms enter transition). Popover consumes elevation + motion; the scrim waits for Dialog.
  • Scrollbar thumb token + metrics — ✅ landed with the ScrollArea unit (2026-10-29): colors.scrollbar_thumb (translucent over content: black in light, light gray in dark; TOML-overridable) and Theme.scrollbar (width 8 pt, inset 2 pt, min_thumb 24 pt).
  • success / warning roles — ✅ landed with Toast (2026-10-29): colors.success/warning (+ foregrounds; light and dark variants; TOML-overridable; no platform states them) and derived Ramps.success/warning via the OKLCH engine, matching the destructive treatment.
  • Motion tokens — attempted (unit 14, 2026-10-30) and reverted the same day: the working set (Curve + CSS cubic-bezier, Springs presets, [motion] TOML overrides, an ungated motion module with an exit-lifecycle state machine and spring-blended hover washes, exit ghosts on all three overlays, a spring Switch thumb, a store-held toast exiting list) judged too complex for the value, and the motion itself not good enough. The motion system goes back to the drawing board — see the unit 14 row (§6) for what was tried. Two cheap pieces remain worth taking regardless of the rethink: forwarding reduce_motion to cx.set_reduce_motion in reapply (the OS flag is still read but ignored — every animation shipped so far disobeys Reduce Motion), and a 30 fps cap on Progress's indeterminate repeat.
  • knob / control-fill role — only if Slider (unit 7) confirms the Switch's borrowed roles (primary_foreground thumb, border off-track) as a second consumer; until then no expansion.

Controls through unit 10 paint from existing roles and ramps.

5.3 Platform matrix

Target Detection Sources read Live changes
macOS compile target NSColor semantic palette (windowBackgroundColor, labelColor, controlColor, controlAccentColor, selectedTextBackgroundColor, keyboardFocusIndicatorColor, …) in the effective appearance via objc2-app-kit; NSWorkspace accessibility flags distributed notifications + GPUI appearance events
Windows 11 (WinUI) compile target WinRT UISettings (AccentColor, AccentLight1–3, AccentDark1–2, Background, Foreground), TextScaleFactor, AccessibilitySettings high-contrast; Personalize registry fallback UISettings.ColorValuesChanged
GNOME / generic Linux portal reachable portal org.freedesktop.appearance (color-scheme, accent-color, contrast, reduced-motion); gsettings fallback portal SettingChanged
KDE kdeglobals present kdeglobals color groups + scheme hash + accent; portal for mode/accent inotify on kdeglobals
Omarchy ~/.local/state/omarchy or ~/.config/omarchy marker current/theme/colors.toml + theme.name (semantic keys or ANSI color0..15); portal for contrast/reduced-motion inotify on the current dir (palette edits + symlink replacement)
Fallback / unknown none of the above, or reader failure bundled neutral light/dark theme manual only

5.4 Accent ramp engine

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

5.5 Native metrics

Native feel is half geometry. Per-platform tables for: control heights and paddings, corner radii (WinUI control vs card rounding; macOS control radii), font fallback lists resolved through .SystemUIFont (SF Pro / Segoe UI Variable / Adwaita Sans / Noto Sans — no fonts bundled), focus-ring policy (macOS keyboard ring, WinUI two-layer ring, GTK outline), scrollbar style (overlay vs inline), popup elevation and menu metrics. Motion presets are metrics too: each platform states its curves and durations (macOS tuned to NSPopover/NSMenu transitions, Windows the Fluent decelerate cubic-bezier, GNOME/KDE shell timings) — the token shape is open again after the unit 14 revert, and Phase F sources fill it once the rethink lands. Values are transcribed from each toolkit's reference metrics and verified with side-by-side screenshots (macOS in Phase B, the rest in Phase F).

5.6 Custom themes

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

5.7 Implementation vehicles

objc2 + objc2-app-kit (macOS), windows (WinRT UISettings), zbus (portal DBus), notify (file watches), serde + toml (definitions) — all behind the ThemeSource seam so the engine core stays platform-free and testable.

5.8 What we do differently from native-theme

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

6. Component roadmap (review units, in order)

Each row is one review unit: build it, show it in the gallery, review, then move on.

Unit Crate Component Notes
1 core Button Variants (primary/secondary/ghost/destructive) painted from token ramps; single activation path; disabled + a11y role. The API template for everything after.
2 core Checkbox Controlled checked + on_change(next); Check icon from the icons crate.
3 core Switch Controlled pill + thumb; thumb slides once motion exists.
4 core Radio + RadioGroup Group owns the selected value; radios never self-uncheck.
5 core Toggle Controlled pressed (aria-pressed).
6 core Link href opens via platform by default; on_click overrides.
7 core Slider Click-to-set + in-track drag; controlled value.
8 core Progress Determinate + indeterminate.
9 core Avatar Initials; image slot later.
10 core Input (single-line) then multiline, NumberInput String buffer + unicode-segmentation word movement; the field owns its own selection. May seed crates/editor. Single-line finishing pass: §6.2.
11.5 core ScrollArea + Scrollbar ✅ Landed (2026-10-29). Inserted before the remaining overlays — Menus, Lists and Tables all scroll. gpui owns the mechanics (overflow_*_scroll + caller-held ScrollHandle); the component paints an overlay thumb (scrollbar_thumb token, Scrollbar metrics) with drag and geometry unit-tested. Wheel input is gpui's; the area stops wheel propagation so nested areas don't co-scroll. Horizontal thumb deferred; auto-hide on idle deferred
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 ⏪ Attempted and reverted (2026-10-30). What was tried: theme Motion tokens (overlay duration + Curve with a CSS cubic-bezier + spring presets, [motion] TOML); an ungated core motion module (curve→easing, ExitLifecycle exit ghosts via keyed window state, spring hover washes); consumers — exit ghosts on Popover/Menu/Dialog (scrim fading both ways), Switch thumb on with_spring, Toast fade/slide + a store-held exiting list, Button + toast-close hover washes, Progress capped at 30 fps. All reverted — judged too complex for the value and not good enough to watch. The rethink starts from what the attempt proved: gpui's with_spring/with_animation are the right substrate (D21) and keyed window state CAN drive overlay exits; the cost centered on the exit-lifecycle plumbing and per-control wash machinery. Keep the plan open, redesign before reimplementing
then data (new) List, Table, Tree, Tabs, Sidebar Created on demand, same gate.
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, a motion system (unit 14 reverted; rethink before reimplementing), charts.

6.1 After the core units: sizing API — ✅ landed

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

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

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

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

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

Remaining, in order:

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

Deferred until a real need: bidi/RTL; prefix/suffix adornments and a form field wrapper (label/description/error — separate units); SubmitOn enter/cmd-enter modes (multiline concern); Undo/Redo events; selection/cursor color overrides (theme tokens are deliberate); password masking, max length, clear button (gpuikit has none of these either — decide on first need).

7. Milestones

Phase Scope Acceptance
A — scaffold (next) Workspace per §4.1: theme stub, icons crate (IconRole + Lucide fallback), core crate with feature scaffolding (zero components), facade, gallery shell, CI incl. feature checks gallery opens a window reading the theme global; core builds at default / no-default / all / each-feature-alone
B — theme engine core Token model + resolve + presets (neutral fallback, tokyo-night) + macOS source + ThemeWatcher; gallery theme inspector. Done: resolve overlay chain (base → system → preset → mode override → high-contrast) is pure and tested; OKLCH accent-ramp engine; NSColor/NSWorkspace adapter; distributed-notification watchers; inspector with live mode/preset switching. (The "Button + Checkbox painted from live tokens" proof moves to Phase C — those components land there; the gallery inspector proves the live pipeline meanwhile.)
C — controls One review unit at a time (§6, units 1–9), each: API per §4.3 + tests + gallery page → review gate. macOS SF Symbols resolver behind the icon seam lands with the first unit that shows it matters Per unit: activation, controlled round-trip, and disabled inertness tested; gallery page shows the live control against the theme; your approval closes the unit
D — input Wave 2 Single-line input with cursor, selection, clipboard; NumberInput
E — overlays Wave 3 Focus trapping and escape/dismiss semantics per overlay
E+ — motion Unit 14 (§6): Motion tokens + reduce-motion forwarding + overlay exits + Switch thumb + hover washes ⏪ Attempted and reverted (2026-10-30); rethink pending. The reduce-motion forwarding fix and the Progress 30 fps cap stay on the table as standalone wins
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.
  • gpui-kit base: API-design reference (D20); where a small utility is adapted rather than reinvented, keep Apache-2.0 compliance — note the origin in a file-header comment or NOTICE.
  • 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 gpui-kit base conventions where they fit (D20), always simplified — never mirrored 1:1; small utilities may be adapted with attribution
D16 No separate behavior crate; behavior lives in each component; shared helpers promoted at the third use
D17 UPSTREAM.md removed; provenance = §3 pins + git history
D18 Crate split for consumption: core (every-app components) → editor, data, … (advanced) → thin gpui-native-kit facade; one cargo feature per component, additive, module-gated
D19 System icons via semantic IconRoles with per-platform resolvers (SF Symbols / Segoe Fluent Icons / freedesktop) and a bundled Lucide fallback; platform glyph sets are used at runtime only, never bundled
D20 Components land one element at a time behind a review gate: API sketch → implementation → tests → gallery page → review → next. gpui-kit base is the API-design reference for these units; its conventions (controlled state, one activation path, semantic states, a11y labels) and selected utilities are adopted, simplified
D21 Motion is a thin layer over gpui's pinned-rev animation/spring primitives (with_animation, with_spring with velocity retention, built-in reduce-motion snapping, max_fps) — no parallel motion framework (bezel's Painter/Fade store and pulse clock rejected); the theme owns durations, curves (ease_out_quint default + CSS-spec CubicBezier), and spring presets; hover washes ride settled-spring idleness on controls only, data rows snap. ⏪ Under re-evaluation after the unit 14 revert (2026-10-30): the substrate still holds, the execution complexity does not — the rethink may narrow scope to overlay enters/exits only

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 ⏪ Attempted and reverted (2026-10-30) — judged too complex, and the motion not good enough; the plan and the lessons stay (§6 row 14, D21). Next: Phase F — crates/data (List, Table, Tree, Tabs, Sidebar) + Windows/GNOME/KDE/Omarchy theme sources (each states its Motion preset, §5.5, once the rethink lands) + Segoe Fluent/freedesktop icon resolvers, with side-by-side gallery screenshots per platform. NumberInput/multiline return as deferred units when needed. Cheap standalone wins to fold in anywhere: cx.set_reduce_motion forwarding in reapply (§5.2), Progress 30 fps cap.
  7. 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.