update avatar
This commit is contained in:
+30
-31
@@ -1,6 +1,6 @@
|
||||
# gpui-native-kit — Project Plan
|
||||
|
||||
Rev 8 (2026-11-01). Changes from rev 7: the signed migration step is removed — its §10 and §12 traces cleaned up; [signed](https://git.reya.info/reya/signed) stays the first downstream consumer whose screens drive §6.3 and §6.4, but porting the app stays app-side and off the kit roadmap. A new `layout` crate (§6.4, D26) bootstraps the application shell — left sidebar on, right sidebar off, center dock with drag-to-split panes — reversing the old "dock stays app-side" deferral; Phase H now covers it. The rev 7 rules stand: 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.
|
||||
Rev 9 (2026-11-03). Changes from rev 8: the layout crate (§6.4) is complete through L5 — dock with drag-to-split, titlebar + window controls, panel-declared sidebar auto-open — and the gallery is now the component browser: the app shell with the component catalog in its sidebar, every component a dock panel. Titlebar lessons landed in the theme and layout: `Theme.titlebar_height` (§5.5), and app-owned window dragging so a dragged tab never drags the window with it (L4.5). The `Sidebar` scrolls internally. The rev 8 rules stand: 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 (theme engine as the product, crate split, per-component features) stands.
|
||||
|
||||
## 1. Vision
|
||||
|
||||
@@ -25,8 +25,9 @@ Decision D15: we depend on no UI toolkit and port no wholesale subsystems. These
|
||||
```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"] }
|
||||
reqwest_client = { git = "https://github.com/zed-industries/zed", rev = "ec86047d67acd6fddd81392f44ed6ebcc6e7f531" }
|
||||
```
|
||||
`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.
|
||||
`font-kit` is required on macOS for glyph rasterization; Linux needs `wayland` and/or `x11`; Windows needs nothing. Apps that show remote images install a real client at startup — `application().with_http_client(Arc::new(reqwest_client::ReqwestClient::new()))` — because gpui defaults to a `NullHttpClient` and `img(http…)` silently loads nothing without one. 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.
|
||||
@@ -54,7 +55,8 @@ gpui-native-kit/ # repo root = workspace root
|
||||
│ ├── layout/ # gpui-native-layout: application layout bootstrap + center dock (§6.4)
|
||||
│ └── editor/ # gpui-native-editor: created when the editor units need it (D12, §6.3 M15)
|
||||
└── examples/
|
||||
└── gallery/ # showcase; a page per component + theme/icon inspector
|
||||
└── gallery/ # the component browser: the app shell with the
|
||||
# catalog in its sidebar, every component a dock panel
|
||||
```
|
||||
|
||||
Advanced crates (`data` for Tree/List/Tabs/Sidebar, `layout` for the application shell (§6.4), `charts`, `pickers`) are created on demand when their wave starts — not speculative scaffolding.
|
||||
@@ -153,16 +155,15 @@ graph TD
|
||||
|
||||
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:
|
||||
The set stays role-minimal and grows one concept at a time, driven by the unit that needs it. Landed additions, each with its consumer:
|
||||
|
||||
- ~~**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` (builder → `Vec<BoxShadow>`), and `Motion.overlay` (200 ms enter transition). Popover consumes elevation + motion; the scrim waits for Dialog. **Per-platform stacks (2026-11-01, D24)**: `Elevation` became a two-layer key + ambient stack and `resolve` picks it per `SourceKind` — macOS soft and layered (key shadow at 12% alpha), Windows tight and offset, Linux compact and darker; dark mode shares one stack — deleting the Alert's light-mode alpha hack
|
||||
- ~~**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. **`info` joined them (2026-10-31)** — the fourth severity, same treatment (`colors.info` + foreground + `Ramps.info`, neutral sky blue, no platform states it, TOML `info =`). Trigger: the Alert's Info severity was painting from the primary ramp, so a warm platform accent (GNOME red, Omarchy red themes) made Info read as Danger; the token keeps Info calm however the accent turns out. Severity tokens now use the same picked value for the ramp and the token (the success/warning tokens used to re-pick with the system slot dropped, diverging from their own ramps if a platform ever stated one).
|
||||
- **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.
|
||||
- ~~**Ghost hover token**~~ — ✅ landed with the dock tabs (2026-11-02): `colors.muted_hover` (light zinc-200 / dark zinc-700, one step past `muted`; TOML `muted_hover =`; no platform states a hover wash). Trigger: the ghost Button's hover painted `muted` — the same token the selected pill fills with — so hovering a close control on a selected tab was invisible. `Button::Ghost` and the selected pill hover from `muted_hover` now.
|
||||
- ~~**`knob` / control-fill role**~~ — resolved without a token: Slider (unit 7) shipped on the Switch's borrowed roles (`primary_foreground` fill, `border` off-track); a dedicated role waits for a third consumer.
|
||||
- **Overlay scrim + elevation + motion tokens** (unit 11, 2026-10-29): `colors.overlay`, `Radius.overlay`, `Elevations.popover`/`dialog`, `Motion.overlay` (200 ms) — with per-platform two-layer shadow stacks picked per `SourceKind` (D24, 2026-11-01).
|
||||
- **Scrollbar thumb token + metrics** (ScrollArea, 2026-10-29): `colors.scrollbar_thumb`, `Theme.scrollbar` (`width` 8 pt, `inset` 2 pt, `min_thumb` 24 pt).
|
||||
- **`success` / `warning` / `info` severities** (Toast 2026-10-29; `info` 2026-10-31, D22): tokens + foregrounds + OKLCH ramps, no platform states them, TOML-overridable — so a warm platform accent never makes Info read as Danger.
|
||||
- **`colors.muted_hover`** (dock tabs, 2026-11-02): one step past `muted`; the ghost Button's hover used to paint the same `muted` a selected pill fills with.
|
||||
- **`Theme.titlebar_height`** (2026-11-03, §5.5): the height the window's titlebar strips draw at, stated per platform where the OS declares one.
|
||||
|
||||
Controls through unit 10 paint from existing roles and ramps.
|
||||
Resolved without tokens: Slider rides the Switch's borrowed roles (no knob role — no third consumer); motion stays exactly `Motion.overlay` (D21), with the reduce-motion forwarding fix landed 2026-10-30 (`reapply` calls `cx.set_reduce_motion`). Controls through unit 10 paint from existing roles and ramps.
|
||||
|
||||
### 5.3 Platform matrix
|
||||
|
||||
@@ -183,7 +184,7 @@ Every target supplies one accent color (or a stated ramp). Where the platform st
|
||||
|
||||
### 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), menu metrics (popup elevation landed 2026-11-01 as static per-platform profiles, D24). 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).
|
||||
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), menu metrics (popup elevation landed 2026-11-01 as static per-platform profiles, D24). **`titlebar_height` landed (2026-11-03)**: `Theme.titlebar_height` — the height the window's titlebar strips draw at. Windows states 32 px (the Win32 caption at 100% scaling, via `SystemTheme.titlebar_height`); macOS and Linux derive `max(medium control_height, 34 px)` — the height custom titlebars draw at there, per the pinned zed reference. The dock's pane bar centers its tabs in it. 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
|
||||
|
||||
@@ -223,7 +224,7 @@ Each row is one review unit: build it, show it in the gallery, review, then move
|
||||
| 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), Alert + Toast rework ✅ (2026-10-31) | 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. **The 2026-10-31 rework split the old Toast in two (D23):** `Alert` (`alert` feature) — a persistent, fully controlled callout banner rendered inline where the app places it: severity icon (`Ramps.<sev>.base` on a `surface` + `border` + `Radius.overlay` card with the platform popover shadow (D24)), title and message both optional (a message-only banner paints it as the primary line), close button only when `on_dismiss` is set, reporting intent instead of hiding anything. A pushed, macOS-style stacking `Alerts`/`AlertsView` (collapse-slivers + hover-unroll + slide animations) was tried and rolled back the same day — inline-only won. The reworked `Toast` (`toast` feature) — **one at a time** (pushing replaces the one on screen), a succinct severity-less pill at the bottom center: optional icon + one-line truncating content + one optional action (`ToastAction`, the suggested-action pill) + an always-present close button, laid out as content, small gap, action, large gap, close; fades in and out (`Motion.overlay` each — exits via a store-held exiting slot whose handler runs once the fade completes). The store keeps GPUI-executor timers (`Toast::DURATION` 5 s, zero = sticky). Toast landed the deferred `success`/`warning` tokens (§5.2); Alert is the `info` token's consumer (D22). 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 | 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). **Follow-up (2026-11-03):** the Sidebar scrolls internally — entries sit in a `ScrollArea` whose handle lives in element state (the overlay thumb paints; the offset survives re-renders); the gallery's 29-entry catalog outgrew the fixed region. `sidebar` pulls `core/scroll_area` |
|
||||
| then | pickers or data | Calendar/DatePicker, TimeField, ColorPicker | Decide at unit time. |
|
||||
| editor | editor (new) | Text/code editor, syntax highlighting (D12) | Same gate. |
|
||||
|
||||
@@ -233,7 +234,7 @@ Deferred until a real need: window-wide text selection, overlay exit animations
|
||||
|
||||
**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.
|
||||
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. An `XSmall` class was tried (2026-11-03) and removed — three classes cover the components so far; explicit sides are a caller style refinement (`Styled::size`). 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)
|
||||
|
||||
@@ -269,7 +270,7 @@ Wave M1 — each through the review gate:
|
||||
| M5 | core | Empty state ✅ Landed (2026-11-01) | inbox, issues, search misses | bezel: icon + headline + one line of hint. Shipped as `EmptyState::new(headline)` with optional `.icon(role)`, `.hint(text)`, and `.action(element)` — no id (nothing interactive), no size classes (content, always medium metrics); the illustration is the component's own `icon_size × 2` decoration, muted like the hint, `line_height 1.3` on the headline, everything centered. Plain `empty_state` feature, no deps |
|
||||
| M6 | core | Password input ✅ Landed (2026-11-01) | identity unlock + `nsec` import passphrase | Lifts §6.2's "password masking on first need" — the need arrived. `InputState::set_password(bool)` masks the painted line one bullet per grapheme (paint-only: the buffer, undo, and caret all stay real; a mask map of grapheme byte offsets converts between the buffer and the painted display for hit-testing, the caret, selection, the IME underline, and `bounds_for_range`). Masked fields refuse copy and cut so the secret never reaches the clipboard; paste, editing, and reveal stay available. No element builder — the flag is state, like read-only. **Follow-up (2026-11-01):** a built-in reveal toggle — a ghost icon-only Button (`IconRole::Eye`/`EyeOff`, native glyphs per platform) pinned inside the field's right edge, shown on every password field. The state now tracks the field kind (`set_password`/`is_password`) and its visibility (`set_revealed`/`is_revealed`) separately, so toggling reveal never changes the field's kind; the paint reserves the button's corner (`right_padding` threaded through `paint`/`store_layout`, with a content mask so text never runs under the icon). The reveal press stops propagation so it never places the caret. The `input` feature now depends on `button` |
|
||||
| M7 | input | Multiline textarea ✅ Landed (2026-11-01) | discussion composer, patch + commit messages | bezel: fixed rows or min/max auto-grow; gpuikit: rows on InputState. Reuses Input's buffer; D12's editor crate stays separate. Shipped as a state mode — `InputState::set_multiline(bool)` — sharing the single-line machinery: shaping moved to gpui `shape_text`, which wraps to the field's inner width, and the paint layout gained a visual-row table (byte range + y per wrapped row) that the caret, selection, IME underline, hit-testing, triple-click and `bounds_for_range` all consume; `scroll_offset` became 2-D (x single-line, y multiline) with scroll-to-caret per axis. Height = the ambient line-height ratio × the element's `rows(usize)` (default 4); content taller than the field scrolls, and the field stops wheel propagation only while overflowing (min/max auto-grow deferred). Enter inserts a newline and modifier+Enter submits — `SubmitOn::ModifiedEnter` is the default, `SubmitOn::Enter` plus the `InsertNewline` shift-enter binding gives chat-style enter-submit — lifting §6.2's deferral. Up/down (+ shift variants) move between visual rows with a goal column; triple-click selects the clicked row; the sanitizer is mode-aware (multiline keeps newlines, drops `\r`; single-line unchanged), and the mask map gained per-grapheme display lengths so masked fields survive line breaks. No theme changes — all geometry derives from existing tokens. 7 headless tests join `tests/input.rs` |
|
||||
| M8 | core | Avatar image | profile pictures in rows and panel tabs | lifts the deferred image slot: gpui `img` + initials fallback (signed adds its pixel-art fallback app-side) |
|
||||
| M8 | core | Avatar image ✅ Landed (2026-11-03) | profile pictures in rows and panel tabs | Lifts the deferred image slot — with the pixel-art fallback the row originally left to signed (the initials placeholder never shipped). `Avatar::new(id).name(..).picture(Option<url>)`: the picture renders through gpui `img` on a `secondary` container, `Role::Image` with the aria label from name-or-id; a failed download paints the container blank — the pixel art is the **no-picture** fallback, matching signed. The fallback adapts signed's pixel avatar (D15/D20): FNV-1a of the id seeds an xorshift PRNG that draws a mirror-symmetric 8×8 grid (1-cell margin, 42% fill, 25% shade cells, ≥5 left-half cells); the hue rotates the accent's OKLCH hue per seed, the shade cells lighten in dark mode and darken in light, the tray fills at 16% alpha — deterministic across runs and platforms, colored only from theme-derived OKLCH. Sizes ride the size table's `avatar_size` (`Sizable`). 4 pattern tests (mirror symmetry, empty outer ring, determinism + minimum fill, hash stability); the gallery panel shows a loaded image, the seeded fallback, and both sizes. The gallery also installs zed's `reqwest_client` at startup (`with_http_client`) — without it gpui defaults to a `NullHttpClient` and remote pictures never load (§3). **Follow-up (2026-11-03):** `.fully_rounded(bool)` renders the avatar circular (`Radius.full` instead of the control radius — threaded through the picture, the pixel grid, and the tray) |
|
||||
|
||||
Wave M2 — completes the surface: Field wrapper (M9: label/description/error around any control — the settings dialog; lifts another §6.2 deferral), Tag (M10: removable GRASP-server and relay entries), Status dot (M11: sync-drift beads on rows), Combobox (M12: type-to-filter pickers; gpuikit's `on_create`/`keep_unmatched` blur modes are worth copying, adopt that), Command palette (M13: filtered actions — gpuikit keeps the matcher caller-owned, adopt that), VirtualList in `data` (M14: a thin `uniform_list` binding for commit histories and the inbox), and the Spinner (M18: the indeterminate/determinate loading ring — `Tab::busy`'s placeholder ring in the `Pill` tab waits on it, as do async buttons and refresh states; picked up next time a consumer lands, before signed wires repo loading into dock tabs).
|
||||
|
||||
@@ -279,7 +280,7 @@ Survey notes — worth adopting later, not now: bezel's hover card (profile prev
|
||||
|
||||
### 6.4 The layout crate (`crates/layout`) — application shell bootstrap
|
||||
|
||||
A new crate (D18: advanced crates are created on demand, not speculatively) that bootstraps the application layout the way zed's workspace and gpui-kit's dock do, with a deliberately smaller API (D26): a left sidebar (on by default), a right sidebar (off by default), and a center dock whose panes the user can drag and split. A panel can declare that it needs the right sidebar; activating it opens the sidebar automatically. [signed](https://git.reya.info/reya/signed)'s `crates/dock` — a gpui-component `DockArea` behind a custom skin — is the usage reference and the cautionary tale: its renderer seams, serialization placeholders, and zoom/collapse modes mark the complexity we skip. gpui-kit's dock and zed's editor dock are read as API-design references only (D15, D20). Facade feature `layout`; the crate depends on `core` and composes `data` — the L3 header decision, revised when L4 landed: pane headers compose the data crate's `Tabs` in its `Pill` variant (the signed dock tab — pills on a chrome-less bar: `[lead element][name][wide gap][togglable busy slot][close control]`; the bar carries no background or border because it doubles as the titlebar strip in L4.5), with `Tab::closeable` + `Tabs::on_close` for close controls, `Tab::lead` for avatar-or-custom leading elements, `Tab::busy` for the loading slot, and a `Tab::wrap` seam so the dock attaches its drag payload to the rendered tab; data's `Tabs` keeps the underline design as the default. A `window_frame` module wraps a window root in the client-side frame the OS owes — border, `Radius::overlay` corners, dialog-elevation shadow, resize bands — under `Decorations::Client` and passes through under `Server` (adapted from bezel's `window::frame` and gpui-kit's `WindowBorder`, simplified; the window must open transparent). New `IconRole`s (panel-left/right toggles, split directions) land in the icons crate with the units that need them. Everything is theme-baked per §4.3 — no style parameters, no renderer/skin seam.
|
||||
A new crate (D18: advanced crates are created on demand, not speculatively) that bootstraps the application layout the way zed's workspace and gpui-kit's dock do, with a deliberately smaller API (D26): a left sidebar (on by default), a right sidebar (off by default), and a center dock whose panes the user can drag and split. A panel can declare a right-sidebar **view**; activating it points the sidebar at that view and the dock asks the app to open the sidebar. [signed](https://git.reya.info/reya/signed)'s `crates/dock` — a gpui-component `DockArea` behind a custom skin — is the usage reference and the cautionary tale: its renderer seams, serialization placeholders, and zoom/collapse modes mark the complexity we skip. gpui-kit's dock and zed's editor dock are read as API-design references only (D15, D20). Facade feature `layout`; the crate depends on `core` and composes `data` — the L3 header decision, revised when L4 landed: pane headers compose the data crate's `Tabs` in its `Pill` variant (the signed dock tab — pills on a chrome-less bar: `[lead element][name][wide gap][togglable busy slot][close control]`; the bar carries no background or border because it doubles as the titlebar strip in L4.5), with `Tab::closeable` + `Tabs::on_close` for close controls, `Tab::lead` for avatar-or-custom leading elements, `Tab::busy` for the loading slot, and a `Tab::wrap` seam so the dock attaches its drag payload to the rendered tab; data's `Tabs` keeps the underline design as the default. A `window_frame` module wraps a window root in the client-side frame the OS owes — border, `Radius::overlay` corners, dialog-elevation shadow, resize bands — under `Decorations::Client` and passes through under `Server` (adapted from bezel's `window::frame` and gpui-kit's `WindowBorder`, simplified; the window must open transparent). New `IconRole`s (panel-left/right toggles, split directions) land in the icons crate with the units that need them. Everything is theme-baked per §4.3 — no style parameters, no renderer/skin seam.
|
||||
|
||||
Units, each through the review gate:
|
||||
|
||||
@@ -290,7 +291,7 @@ Units, each through the review gate:
|
||||
| L3 | layout | Dock ✅ Landed (2026-11-02) | The center pane tree, controlled: `DockState` holds the `Pane { tabs, active }` / `Split { axis, ratio, a, b }` tree plus a registry of `PanelDescriptor`s (`id` + title + optional icon + a view builder called once per open; the built view is retained, so panel state survives tab switches). `open` / `activate` / `close` / `close_others` / `close_pane` / `split` / `move` are plain state ops keyed by panel id — opening an open panel re-activates it, one instance per descriptor for now, and `active_panel()` reports what the active pane shows. Any op emptying a pane collapses it: the pane detaches and single-child splits are replaced by the surviving subtree, so only the root can sit empty. Split seams are `ResizeHandle`s (arrow keys move them for free); the ratio lives in the state and the seam turns pixel deltas into fractions against a per-split span that a canvas captures into dock-namespaced keyed state each frame — double-click resets to 0.5, both sides clamp at 10% of the split. Panes render a `strip_height` header row composing the data crate's `Tabs` (revised in L4 — see below), `TabList`/`Tab`/`TabPanel` roles, and a core `ContextMenu` on the header — Close / Close Others / Close Pane; the active tab's view fills the pane. Feature `dock` pulls core `button` + `context_menu` and re-uses `resize_handle`; 7 headless tests in `crates/layout/tests/dock.rs` (open/activate, tab clicks, neighbor-on-close, split + collapse, seam drag/reset/clamp with a 1 px flex-rounding tolerance, merge-by-move, close-others/close-pane + reopen); the gallery's shell center hosts a dock with demo panels and open/split buttons |
|
||||
| L4 | layout | Dock drag-to-split ✅ Landed (2026-11-02) | Dragging a tab uses gpui's own drag system: the dock attaches `on_drag` through data `Tabs`' `Tab::wrap` seam with a `PanelDrag { panel }` payload (ghost = a small bordered title card). While `App::has_active_drag()`, each pane's content mounts five absolute drop zones — four edge bands that call the new `DockState::split_panel_at(panel, anchor, direction)` (a split beside the *target* pane, not the dragged tab's own; the empty source still collapses) and a center zone that calls `move_panel`. Zones paint a `selection` wash via `drag_over::<PanelDrag>` while hovered and are mounted only during a drag, so content clicks are never blocked; gpui cancels the drag on a mouse-up over nothing, which unmounts them. Dropped tabs land active in their new pane. Same pass: pane content renders through `AnyView::cached` (`StyleRefinement` absolute + size full, the gpui-kit pattern) so an un-notified panel never re-renders on dock frames; pane headers moved from hand-rolled tabs to data `Tabs` (`Pill` variant — renamed from `Card` and reshaped to signed's exact tab: leading element via `Tab::lead`, wide title-to-controls gap, `Tab::busy` loading slot standing in with a quiet ring until the Spinner control lands, a ghost core `Button` close control (the first data → core dependency edge, feature-gated: `tabs` pulls `core/button`), and the pill radius is the button radius, not `full`; the pill bar is chrome-less because the dock's tab strip doubles as the L4.5 titlebar), which now ships the two designs (default stays `Underline`); tab strips carry `{tabs-id}-tab-{label}` / `{tabs-id}-close-{label}` debug selectors for headless tests. 10 headless tests in `crates/layout/tests/dock.rs` — the new ones cover a real simulated drag (mouse down → threshold move → drag starts → drop on a right edge band → split) and `split_panel_at`'s target-side placement; the rest carried over with the new tab selectors |
|
||||
| L2.5 | layout | window frame ✅ Landed (2026-11-01) | `window_frame::frame(child, window, cx)` — the client-side window frame for windows the OS does not decorate: under `Decorations::Server` (macOS, Windows, KDE, X11) it passes the child through untouched; under `Decorations::Client` (Wayland/GNOME) it insets the content, draws the hairline border and `Radius::overlay` corners (tiled sides stay square and lose their band), casts the dialog elevation's shadow dimmed to 0.7× when the window is inactive, and turns a 4 px band inside each live edge into resize hits (`start_window_resize`, edge cursors; a maximized window counts as flush everywhere). Requires a transparent window background, and must be the window's root — the bands hit-test against `Window::window_bounds`. **The `Shell` applies it by default**, so the module ships ungated — no separate feature. Adapted from bezel's `window::frame` + gpui-kit's `WindowBorder` (D15/D20), simplified to one inset band and theme-owned tokens |
|
||||
| L4.5 | layout | Titlebar + window controls ✅ Landed (2026-11-02) | The strips are the window's titlebar: every strip (shell sidebars, dock pill bar) marks itself `WindowControlArea::Drag` and double-click-maximizes (`titlebar_double_click` on macOS, `zoom_window` on Linux; Windows does it natively through the caption hit test). New `window_controls` module (feature `window_controls`, pulled by `shell` and `dock`): a `WindowControls` element rendered at the trailing end of the right-hand strip — the right sidebar's strip when visible, else the dock pill bar via `Dock::window_controls(bool)`. **Windows**: caption buttons (36 px wide, Segoe codepoints through the icons crate — new `WindowMinimize`/`WindowMaximize`/`WindowRestore` roles, E921/E922/E923 verified against zed's own table) marked `WindowControlArea::Min/Max/Close` so the OS owns the clicks and snap layouts; maximize flips to restore via `is_maximized`; capability-gated by `Window::window_controls()` + `is_minimizable`/`is_resizable`; close hovers the platform red (a platform convention const, like the Segoe codepoints). **Linux**: Adwaita circular buttons (the platform dress, hand-rolled like zed's) under `Decorations::Client` only, clicking `minimize_window`/`zoom_window`/`remove_window`. **macOS** draws nothing — system traffic lights; the shell reserves `TRAFFIC_LIGHT_PAD` (78 px, Tahoe SDK) in the left sidebar strip's leading edge, and `Dock::traffic_light_pad(bool)` pads the pill bar when no left sidebar sits over the lights. The gpui dispatch fact that makes this safe: Windows forwards `WM_NCLBUTTONDOWN` into gpui's normal event stream, so buttons inside a Drag-marked strip still get their clicks, and only unconsumed presses fall through to native caption behavior. Adapted from zed's `platform_title_bar` (D15/D20); the gallery wires both dock flags from `LayoutState`. 2 platform-gated headless tests (macOS pad shifts the pill bar's tab by the pad width; controls render only under the flag on their platforms); CI feature loop gained `window_controls` |
|
||||
| L4.5 | layout | Titlebar + window controls ✅ Landed (2026-11-02) | The strips are the window's titlebar: every strip (shell sidebars, dock pill bar) marks itself `WindowControlArea::Drag` and double-click-maximizes (`titlebar_double_click` on macOS, `zoom_window` on Linux; Windows does it natively through the caption hit test). New `window_controls` module (feature `window_controls`, pulled by `shell` and `dock`): a `WindowControls` element rendered at the trailing end of the right-hand strip — the right sidebar's strip when visible, else the dock pill bar via `Dock::window_controls(bool)`. **Windows**: caption buttons (36 px wide, Segoe codepoints through the icons crate — new `WindowMinimize`/`WindowMaximize`/`WindowRestore` roles, E921/E922/E923 verified against zed's own table) marked `WindowControlArea::Min/Max/Close` so the OS owns the clicks and snap layouts; maximize flips to restore via `is_maximized`; capability-gated by `Window::window_controls()` + `is_minimizable`/`is_resizable`; close hovers the platform red (a platform convention const, like the Segoe codepoints). **Linux**: Adwaita circular buttons (the platform dress, hand-rolled like zed's) under `Decorations::Client` only, clicking `minimize_window`/`zoom_window`/`remove_window`. **macOS** draws nothing — system traffic lights; the shell reserves `TRAFFIC_LIGHT_PAD` (78 px, Tahoe SDK) in the left sidebar strip's leading edge, and `Dock::traffic_light_pad(bool)` pads the pill bar when no left sidebar sits over the lights. The gpui dispatch fact that makes this safe: Windows forwards `WM_NCLBUTTONDOWN` into gpui's normal event stream, so buttons inside a Drag-marked strip still get their clicks, and only unconsumed presses fall through to native caption behavior. Adapted from zed's `platform_title_bar` (D15/D20); the gallery wires both dock flags from `LayoutState`. 2 platform-gated headless tests (macOS pad shifts the pill bar's tab by the pad width; controls render only under the flag on their platforms); CI feature loop gained `window_controls`. **Amended (2026-11-03, app-owned dragging):** the gallery hit the macOS trap — with `appears_transparent` alone, AppKit still owns the native titlebar band and dragged the window from a tab press (the `Drag` marking is a no-op on macOS at this rev; `on_hit_test_window_control` is unimplemented there, unlike Windows). Windows now open with `app_owns_titlebar_drag: true` and the new `titlebar_strip` helper carries the strip behavior — drag-to-move (guarded by `has_active_drag`, so a dragged tab's session always wins), double-click maximize, and the `Drag` area — applied to the sidebar strips and to the dock bar's dead-zone span after the tabs, never to the tabs themselves (signed's pattern). The pane bar's height reads `Theme.titlebar_height` (§5.5), tabs centered in it |
|
||||
| L5 | layout | Sidebar auto-open ✅ Landed (2026-11-02) | Panels declare a right-sidebar **view**, not a flag: `PanelDescriptor::right_sidebar(builder)` — the view builds once when the panel opens (retained alongside the panel view, so its state survives too) and lives on the open tab. Activating a panel that has one points the sidebar at that view and runs the app-registered `DockState::on_open_right_sidebar(handler)` so the app opens it — visibility stays on the shell's `LayoutState`, the dock only asks. Panels without a view leave the sidebar showing the last view (sticky — no yanking on tab switches); closing the displayed panel's tab falls back to the first remaining tab's view, else blank. The sidebar renders through a new `DockSidebar` element (the cached-view pattern), which the app places in `Shell::right_sidebar`. Gallery: Overview and Inbox declare details views, Settings doesn't — switching to Settings keeps the sidebar, closing a details tab falls back. 2 headless tests (activation fires the callback and renders the panel's view; sticky-keep on view-less panels + close-fallback) |
|
||||
|
||||
Deferred until a real need: layout persistence/serialization, pane zoom (maximize one pane), a bottom dock, collapse-to-edge docks, floating windows, drag between windows.
|
||||
@@ -306,8 +307,8 @@ Deferred until a real need: layout persistence/serialization, pane zoom (maximiz
|
||||
| E — overlays ✅ | Popover (11), ScrollArea (11.5), Menu (12), Tooltip + Dialog/AlertDialog + Toast/Alert (11+ row) | Escape/dismiss semantics and focus behavior verified 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 | ✅ Landed (2026-10-30): `crates/data` (List, Tree, Tabs, Sidebar), the Linux theme sources (Omarchy/KDE/GNOME), the Windows source, the Segoe Fluent + freedesktop icon resolvers, plus per-platform elevation stacks (D24, 2026-11-01) | Remaining acceptance: side-by-side gallery screenshots — a Linux run (Omarchy/KDE/GNOME) and the first Windows hardware build |
|
||||
| G — migration additions (§6.3) | Waves M1–M2: Badge, Select, Segmented, Context menu, Empty state, password + multiline input, Avatar image, then M9+ → 0.1 | Each unit through the review gate; each wave unblocks a real signed screen |
|
||||
| H — layout crate (§6.4) | ResizeHandle, Shell bootstrap (left sidebar on, right sidebar off, center dock) + the client-side window frame + titlebar and window controls, Dock with drag-to-split, panel-declared sidebar auto-open | Each unit through the review gate; the gallery workspace page doubles as the bootstrap example apps copy |
|
||||
| G — migration additions (§6.3) | M1 ✅ (2026-11-01): Badge, Select, Segmented, Context menu, Empty state, password + multiline input. M8 ✅ (2026-11-03): Avatar image. Next: Wave M2 (M9–M14, M18) → 0.1 | Each unit through the review gate; each wave unblocks a real signed screen |
|
||||
| H — layout crate (§6.4) ✅ Landed (2026-11-01 → 11-03) | ResizeHandle, Shell bootstrap + client-side window frame, Dock with drag-to-split, titlebar + window controls (app-owned dragging), panel-declared sidebar auto-open | Each unit through the review gate; the gallery is the bootstrap example — the shell with the catalog sidebar and a dock of component panels |
|
||||
|
||||
Gate for every phase: `cargo fmt --check`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test`, gallery smoke run.
|
||||
|
||||
@@ -363,20 +364,18 @@ Pure tests for resolve/overlay chain, payload parsing (fixtures), icon resolutio
|
||||
| D23 | The old Toast split into `Alert` + `Toast` (2026-10-31): one component can't be both a persistent page callout and a fleeting bottom-center nudge — different lifetime (persists until acted on vs auto-dismisses), different placement (inline in flow vs floating bottom center), different content shape (title + message + severity tint vs one line + icon + action). Alert is fully controlled — the app renders it while the condition holds and its close button reports intent; Toast keeps the store + timers. Toast drops severity and titles: Adwaita toasts are single-line nudges, and severity belongs to the banners that stay on the page |
|
||||
| D24 | Shadows follow the platform's design language (2026-11-01): in-app shadows are painted by gpui, so "native" means each platform's shadow *values* — `Elevation` is a two-layer key + ambient stack and `resolve` picks it per `SourceKind` (macOS soft and layered at 12% alpha, Windows tight with more offset, Linux compact and darker; one shared dark stack where borders do the separation work). Static design constants, never OS-read (no platform API states shadow values); replaces the Alert's light-mode alpha hack, which was a single-profile workaround |
|
||||
| D25 | Components fetch, never scale (2026-11-01): every derived metric a component kept re-deriving from root metrics — dense label text (`×0.8/0.85`), dense inline icons (`×0.75`), dense horizontal padding (`×0.5/0.75/2`), plate insets (`0.125/0.25/0.375 rem`), strip band heights (`×1/1.15/1.35`) — moved into the per-size-class `ControlMetrics` table (`caption_size`, `dense_icon_size`, `dense_padding_x`, `plate_inset`, `strip_height`, plus `indicator_size` for checkboxes, radios, and switch knobs), computed once in `with_root_font_size`. The table stays role-based — one value per concept per size class, shared by every component — not gpuikit's per-component token layers (D22). Component-unique geometry that has no second consumer keeps its ratios over root metrics (switch track length, slider knob, avatar initials, tree indent). Two icon roles are fetch-only: content icons (alert info, segmented options) take `icon_size` — never smaller than the label — and indicator glyphs (checks, chevrons, close buttons) take `dense_icon_size` |
|
||||
| D26 | Application layout is a crate, not app-side code (2026-11-01): `crates/layout` bootstraps the shell — left sidebar (on by default), right sidebar (off), and a center dock whose panes drag-to-split — reversing the old "dock stays app-side" deferral. Prompted by signed's `crates/dock` (a gpui-component `DockArea` behind a custom skin): it proves the demand and marks the complexity we skip — no renderer/skin seam (one theme-baked appearance), no serialization, no zoom/collapse modes, no floating windows, no JS host. The API stays smaller than gpui-kit's dock and zed's editor dock (both D15/D20 references, read only): an application-owned `LayoutState`, a controlled pane tree, registered panel descriptors, and a handful of ops (split / close / activate / toggle sidebar); a panel declares `needs_right_sidebar` and the layout opens it on activation |
|
||||
| D26 | Application layout is a crate, not app-side code (2026-11-01): `crates/layout` bootstraps the shell — left sidebar (on by default), right sidebar (off), and a center dock whose panes drag-to-split — reversing the old "dock stays app-side" deferral. Prompted by signed's `crates/dock` (a gpui-component `DockArea` behind a custom skin): it proves the demand and marks the complexity we skip — no renderer/skin seam (one theme-baked appearance), no serialization, no zoom/collapse modes, no floating windows, no JS host. The API stays smaller than gpui-kit's dock and zed's editor dock (both D15/D20 references, read only): an application-owned `LayoutState`, a controlled pane tree, registered panel descriptors, and a handful of ops (split / close / activate / toggle sidebar); a panel declares a right-sidebar view and the dock asks the app to open the sidebar on activation |
|
||||
|
||||
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
|
||||
## 12. 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 stays deferred; multiline returns as §6.3 M7.
|
||||
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), plus the `StyledExt` trait (`refine_style` — adapted from gpui-kit `base`, D15 — replacing the per-component `style().refine` pattern; 5 sites) — `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`. **Windows source + icon resolvers landed (2026-10-30)**: `SourceKind::Windows` (WinRT `UISettings` + `Personalize` fallback + `ColorValuesChanged`/`HighContrastChanged`, §5.3), the Segoe Fluent codepoint renderer, and the freedesktop theme walker (§4.4); the WinRT surface was verified against the real `windows` 0.62 bindings via a standalone cross-check (gpui's own manifest build needs llvm-rc from macOS). Remaining Phase F: **side-by-side gallery screenshots per platform** — Omarchy/KDE/GNOME need a Linux run, Windows needs its first hardware build. NumberInput stays deferred until needed; multiline landed as §6.3 M7 (2026-11-01).
|
||||
8. Then — Phase G is the migration additions (§6.3): Badge ✅, Select ✅, Segmented control ✅, Context menu ✅, Empty state ✅, password input ✅, multiline textarea ✅, then Avatar image (M8) — each unblocks a concrete signed screen. Pickers (Calendar/DatePicker, TimeField, ColorPicker) stay decided-at-unit-time. `crates/editor` starts when file viewing (M15) is reached.
|
||||
9. Then — the layout crate (§6.4): ResizeHandle ✅, Shell ✅, window frame ✅, Dock ✅, drag-to-split ✅, titlebar + window controls ✅, sidebar auto-open ✅ — §6.4 is complete; further layout work (persistence, pane zoom, bottom dock) waits for a real consumer. Order against M9+ follows signed's needs.
|
||||
Complete, in landing order: Phase A scaffold · B theme engine (resolve pipeline, OKLCH ramps, presets, macOS source + watchers, gallery inspector) · C controls, units 1–9 + the sizing API (§6.1) · D input (§6.2) · E overlays · E+ motion (D21) · F platforms + data (`crates/data`; `crates/utils` extracted; Linux theme sources; Windows source; Segoe Fluent + freedesktop icon resolvers; per-platform elevation stacks) · G waves M1 + M8 (Badge, Select, Segmented, Context menu, Empty state, password input, multiline textarea, Avatar image) · H the layout crate end to end (§6.4: ResizeHandle, Shell, window frame, Dock, drag-to-split, titlebar + window controls, sidebar auto-open). Landed notes live in §4–§7; nothing below is started.
|
||||
|
||||
Batched polish candidates between units: scrollbar auto-hide + horizontal thumb, `Undo`/`Redo` menu events.
|
||||
Next, in order:
|
||||
|
||||
1. **Wave M2** (§6.3): Field wrapper (M9, the settings-dialog label/description/error wrapper), Tag (M10), Status dot (M11), Combobox (M12), Command palette (M13), VirtualList (M14). The Spinner (M18) lands with its next consumer — before signed wires repo loading into dock tabs (`Tab::busy` holds a placeholder ring today). Each unit through the review gate; order follows signed's needs.
|
||||
2. **Wave M3 opens `crates/editor`** (D12): code view (M15), diff view (M16), Markdown view (M17) when a consumer confirms.
|
||||
3. **Phase F tail:** side-by-side gallery screenshots — a Linux run (Omarchy/KDE/GNOME) and the first Windows hardware build.
|
||||
|
||||
Pickers (Calendar/DatePicker, TimeField, ColorPicker) stay decided-at-unit-time. Deferred layout work (persistence, pane zoom, bottom dock, collapse-to-edge, floating windows) waits for a real consumer. Batched polish candidates between units: scrollbar auto-hide + horizontal thumb, `Undo`/`Redo` menu events.
|
||||
|
||||
Reference in New Issue
Block a user