From 0f535413f2f3e58637cb9096e9a261d14128ad7b Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Mon, 2 Nov 2026 09:28:47 +0700 Subject: [PATCH] rename app layout to shell and add window frame --- .github/workflows/ci.yml | 2 +- crates/layout/Cargo.toml | 4 +- crates/layout/src/lib.rs | 6 +- crates/layout/src/{app_layout.rs => shell.rs} | 16 +- crates/layout/src/window_frame.rs | 179 ++++++++++++++++++ .../layout/tests/{app_layout.rs => shell.rs} | 4 +- docs/PLAN.md | 9 +- examples/gallery/src/main.rs | 4 +- 8 files changed, 205 insertions(+), 19 deletions(-) rename crates/layout/src/{app_layout.rs => shell.rs} (96%) create mode 100644 crates/layout/src/window_frame.rs rename crates/layout/tests/{app_layout.rs => shell.rs} (98%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab6f6a9..ad7cbd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: run: | cargo check -p gpui-native-layout --no-default-features cargo check -p gpui-native-layout --all-features - for f in resize_handle app_layout; do + for f in resize_handle shell; do cargo check -p gpui-native-layout --no-default-features --features "$f" done - name: Feature checks (facade) diff --git a/crates/layout/Cargo.toml b/crates/layout/Cargo.toml index 3238329..87c8cc1 100644 --- a/crates/layout/Cargo.toml +++ b/crates/layout/Cargo.toml @@ -17,9 +17,9 @@ gpui = { workspace = true, features = ["test-support"] } [features] default = ["full"] -full = ["resize_handle", "app_layout"] +full = ["resize_handle", "shell"] resize_handle = [] -app_layout = [ +shell = [ "resize_handle", "dep:gpui-native-core", "dep:gpui-native-data", diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index e66f28e..acd53aa 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -3,5 +3,7 @@ #[cfg(feature = "resize_handle")] pub mod resize_handle; -#[cfg(feature = "app_layout")] -pub mod app_layout; +#[cfg(feature = "shell")] +pub mod shell; + +mod window_frame; diff --git a/crates/layout/src/app_layout.rs b/crates/layout/src/shell.rs similarity index 96% rename from crates/layout/src/app_layout.rs rename to crates/layout/src/shell.rs index 4064568..263cbfd 100644 --- a/crates/layout/src/app_layout.rs +++ b/crates/layout/src/shell.rs @@ -13,6 +13,7 @@ use gpui_native_icons::IconRole; use gpui_native_theme::{ActiveTheme as _, Size}; use crate::resize_handle::ResizeHandle; +use crate::window_frame::frame; /// The width a sidebar opens at. const DEFAULT_SIDEBAR_WIDTH: f32 = 240.0; @@ -123,7 +124,7 @@ impl Side { /// The application shell. #[derive(IntoElement)] -pub struct AppLayout { +pub struct Shell { id: ElementId, state: Entity, left_sidebar: Option, @@ -131,7 +132,7 @@ pub struct AppLayout { center: Option, } -impl AppLayout { +impl Shell { /// Creates the shell bound to `state`. pub fn new(id: I, state: Entity) -> Self where @@ -171,8 +172,8 @@ impl AppLayout { } } -impl RenderOnce for AppLayout { - fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { +impl RenderOnce for Shell { + fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let metrics = theme.sizes.resolve(Size::default()); @@ -186,7 +187,7 @@ impl RenderOnce for AppLayout { let left_visible = state.read(cx).is_left_sidebar_visible(); let right_visible = state.read(cx).is_right_sidebar_visible(); - div() + let layout = div() .flex() .flex_row() .w_full() @@ -237,7 +238,10 @@ impl RenderOnce for AppLayout { metrics.strip_height, metrics.dense_padding_x, )) - }) + }); + + // Wraps the layout in a window frame. + frame(layout, window, cx) } } diff --git a/crates/layout/src/window_frame.rs b/crates/layout/src/window_frame.rs new file mode 100644 index 0000000..bc0c3d5 --- /dev/null +++ b/crates/layout/src/window_frame.rs @@ -0,0 +1,179 @@ +//! The client-side window frame: the border, corners, shadow, and resize +//! edges of a window the system does not decorate. + +use gpui::prelude::FluentBuilder as _; +use gpui::{ + App, Bounds, BoxShadow, CursorStyle, Decorations, Div, HitboxBehavior, InteractiveElement as _, + IntoElement, MouseButton, ParentElement as _, Pixels, Point, ResizeEdge, Size, Styled as _, + Tiling, Window, canvas, div, point, px, size, +}; +use gpui_native_theme::ActiveTheme as _; + +/// The width of the band inside the window's edge that resizes it. +const RESIZE_INSET: f32 = 4.0; +/// The hairline around the content. +const BORDER_WIDTH: f32 = 1.0; +/// How much of the shadow's strength survives losing window focus. +const INACTIVE_SHADOW_OPACITY: f32 = 0.7; + +/// Wraps a window's root in the frame it owes. +pub fn frame(child: impl IntoElement, window: &mut Window, cx: &App) -> Div { + let Decorations::Client { tiling } = window.window_decorations() else { + return div().size_full().child(child); + }; + + let tiling = if window.is_maximized() { + Tiling::tiled() + } else { + tiling + }; + + let inset = px(RESIZE_INSET); + let border = cx.theme().colors.border; + let radius = cx.theme().radius.overlay; + + let shadow = frame_shadow(cx, window.is_window_active()); + window.set_client_inset(inset); + + div() + .size_full() + .child( + canvas( + move |_, window, _| { + let size = window.window_bounds().get_bounds().size; + bands(size, inset) + .into_iter() + .filter(|(edge, _)| live(*edge, tiling)) + .map(|(edge, band)| { + (edge, window.insert_hitbox(band, HitboxBehavior::Normal)) + }) + .collect::>() + }, + |_, bands, window, _| { + for (edge, band) in bands { + window.set_cursor_style(cursor(edge), &band); + } + }, + ) + .absolute() + .size_full(), + ) + .when(!tiling.top, |frame| frame.pt(inset)) + .when(!tiling.bottom, |frame| frame.pb(inset)) + .when(!tiling.left, |frame| frame.pl(inset)) + .when(!tiling.right, |frame| frame.pr(inset)) + .on_mouse_down(MouseButton::Left, move |event, window, _| { + let size = window.window_bounds().get_bounds().size; + if let Some(edge) = + resize_edge(event.position, inset, size).filter(|edge| live(*edge, tiling)) + { + window.start_window_resize(edge); + } + }) + .child( + div() + .size_full() + .overflow_hidden() + .border_color(border) + .when(!tiling.top, |surface| surface.border_t(px(BORDER_WIDTH))) + .when(!tiling.bottom, |surface| surface.border_b(px(BORDER_WIDTH))) + .when(!tiling.left, |surface| surface.border_l(px(BORDER_WIDTH))) + .when(!tiling.right, |surface| surface.border_r(px(BORDER_WIDTH))) + .when(!(tiling.top || tiling.left), |surface| { + surface.rounded_tl(radius) + }) + .when(!(tiling.top || tiling.right), |surface| { + surface.rounded_tr(radius) + }) + .when(!(tiling.bottom || tiling.left), |surface| { + surface.rounded_bl(radius) + }) + .when(!(tiling.bottom || tiling.right), |surface| { + surface.rounded_br(radius) + }) + .when(!tiling.is_tiled(), |surface| surface.shadow(shadow)) + .child(child), + ) +} + +/// The dialog elevation's shadow, dimmed while the window is inactive. +fn frame_shadow(cx: &App, active: bool) -> Vec { + let opacity = if active { 1.0 } else { INACTIVE_SHADOW_OPACITY }; + cx.theme() + .elevations + .dialog + .shadows() + .iter() + .map(|shadow| { + let mut dimmed = shadow.clone(); + dimmed.color.a *= opacity; + dimmed + }) + .collect() +} + +/// Which edge a press at `pos` resizes, or `None` for a press on the content. +fn resize_edge(pos: Point, inset: Pixels, size: Size) -> Option { + let (top, bottom) = (pos.y < inset, pos.y > size.height - inset); + let (left, right) = (pos.x < inset, pos.x > size.width - inset); + + Some(match (top, bottom, left, right) { + (true, _, true, _) => ResizeEdge::TopLeft, + (true, _, _, true) => ResizeEdge::TopRight, + (_, true, true, _) => ResizeEdge::BottomLeft, + (_, true, _, true) => ResizeEdge::BottomRight, + (true, ..) => ResizeEdge::Top, + (_, true, ..) => ResizeEdge::Bottom, + (_, _, true, _) => ResizeEdge::Left, + (_, _, _, true) => ResizeEdge::Right, + _ => return None, + }) +} + +/// The eight bands, as the rects [`resize_edge`] classifies to. +fn bands(window: Size, inset: Pixels) -> [(ResizeEdge, Bounds); 8] { + let band = |x: Pixels, y: Pixels, width: Pixels, height: Pixels| Bounds { + origin: point(x, y), + size: size(width, height), + }; + + let (far_x, far_y) = (window.width - inset, window.height - inset); + let (span_x, span_y) = (window.width - inset - inset, window.height - inset - inset); + let zero = px(0.0); + + [ + (ResizeEdge::TopLeft, band(zero, zero, inset, inset)), + (ResizeEdge::TopRight, band(far_x, zero, inset, inset)), + (ResizeEdge::BottomLeft, band(zero, far_y, inset, inset)), + (ResizeEdge::BottomRight, band(far_x, far_y, inset, inset)), + (ResizeEdge::Top, band(inset, zero, span_x, inset)), + (ResizeEdge::Bottom, band(inset, far_y, span_x, inset)), + (ResizeEdge::Left, band(zero, inset, inset, span_y)), + (ResizeEdge::Right, band(far_x, inset, inset, span_y)), + ] +} + +/// Whether `edge` resizes: none of the sides it touches is flush. +fn live(edge: ResizeEdge, tiling: Tiling) -> bool { + let (top, bottom, left, right) = match edge { + ResizeEdge::Top => (true, false, false, false), + ResizeEdge::Bottom => (false, true, false, false), + ResizeEdge::Left => (false, false, true, false), + ResizeEdge::Right => (false, false, false, true), + ResizeEdge::TopLeft => (true, false, true, false), + ResizeEdge::TopRight => (true, false, false, true), + ResizeEdge::BottomLeft => (false, true, true, false), + ResizeEdge::BottomRight => (false, true, false, true), + }; + !(top && tiling.top || bottom && tiling.bottom || left && tiling.left || right && tiling.right) +} + +/// The pointer an edge shows. +fn cursor(edge: ResizeEdge) -> CursorStyle { + match edge { + ResizeEdge::Top | ResizeEdge::Bottom => CursorStyle::ResizeUpDown, + ResizeEdge::Left | ResizeEdge::Right => CursorStyle::ResizeLeftRight, + ResizeEdge::TopLeft | ResizeEdge::BottomRight => CursorStyle::ResizeUpLeftDownRight, + ResizeEdge::TopRight | ResizeEdge::BottomLeft => CursorStyle::ResizeUpRightDownLeft, + } +} diff --git a/crates/layout/tests/app_layout.rs b/crates/layout/tests/shell.rs similarity index 98% rename from crates/layout/tests/app_layout.rs rename to crates/layout/tests/shell.rs index 95e8c90..c75a400 100644 --- a/crates/layout/tests/app_layout.rs +++ b/crates/layout/tests/shell.rs @@ -4,7 +4,7 @@ use gpui::{ VisualTestContext, Window, div, point, px, }; use gpui_native_data::sidebar::Sidebar; -use gpui_native_layout::app_layout::{AppLayout, LayoutState}; +use gpui_native_layout::shell::{LayoutState, Shell}; use gpui_native_theme::{ActiveTheme as _, Size, Theme}; struct Harness { @@ -18,7 +18,7 @@ impl Render for Harness { _cx: &mut gpui::Context, ) -> impl gpui::IntoElement { div().size_full().child( - AppLayout::new("test-layout", self.state.clone()) + Shell::new("test-layout", self.state.clone()) .left_sidebar(Sidebar::new("test-left")) .center( div() diff --git a/docs/PLAN.md b/docs/PLAN.md index 3b71637..e1ca6a2 100644 --- a/docs/PLAN.md +++ b/docs/PLAN.md @@ -278,16 +278,17 @@ 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 may compose `data` — at L3, decide whether `Tabs` gains optional close buttons + drag sources for pane headers or panes render their own header row. 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 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` (`Sidebar`) — at L3, decide whether `Tabs` gains optional close buttons + drag sources for pane headers or panes render their own header row. 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: | Unit | Crate | Component | Notes | | --- | --- | --- | --- | | L1 | layout | ResizeHandle ✅ Landed (2026-11-01) | The drag divider primitive: a thin themed handle that resizes the adjacent region (sidebar edge, split seam). Pointer drag, double-click resets to the default fraction, splitter a11y role. Lives in `layout` — promotes to `core` if a second consumer appears (D16). Shipped as a delta reporter: `ResizeHandle::new(id).axis(Axis).on_resize(delta)` hands the owner the signed seam movement per event — the owner stores a pixel size and clamps, the `InputState::scroll_by` pattern — with `.on_reset()` for double-click; no geometry parameters, so it works at any edge or seam. The hairline is `border`, highlights to `ring` on hover/drag; hit slop = `dense_padding_x` (component-own decoration, D25), hairline 1 px like the input's paint hairlines; `Role::Splitter` + focus ring, arrow keys move the seam (`KEYBOARD_STEP` 32 px, input-style const). One gpui dispatch lesson locked in tests: bubble runs user mouse-down listeners before the div's own focus transfer, so a listener that stops propagation must focus explicitly first — the keyboard test caught it. Feature `resize_handle`; 6 headless tests in `crates/layout/tests/resize_handle.rs`; gallery section shows a draggable split with live width | -| L2 | layout | AppLayout ✅ Landed (2026-11-01) | The bootstrap shell: left sidebar region (on by default) + center + right sidebar region (off by default). Sidebar toggle in each sidebar's header, drag-resize widths through `ResizeHandle`; visibility and widths live on an application-owned `LayoutState` (the `InputState` pattern: `set_left_sidebar_visible` / `set_right_sidebar_visible` / `is_*` readers). The center region hosts the `Dock`. Shipped so: the left sidebar is the data crate's `Sidebar` component (`.left_sidebar(sidebar)` — selection, keyboard nav, and a11y come for free; `app_layout` pulls `data/sidebar`, per the "may compose data" note), the right sidebar and center are app elements (the right side is a details pane, not a list). Each visible sidebar renders a `strip_height` header strip with an end-aligned ghost icon toggle (`IconRole::PanelLeft`/`PanelRight` — new roles this unit: Segoe `DockLeft`/`DockRight` E90C/E90D per the MDL2 table, Lucide `panel-left`/`panel-right` bundled, freedesktop `sidebar-expand-*` → `sidebar-show-symbolic` cascade); `LayoutState` holds visibility + pixel widths with 160/480 px drag clamps (menu-cap px precedent); the right handle inverts the delta so dragging left grows it. `app_layout` pulls only core's `button` + `data/sidebar`; the facade `layout` feature re-requests `full` (core/data workspace deps are now `default-features = false`). 5 headless tests in `crates/layout/tests/app_layout.rs`: defaults, toggling via the state API, the header toggle button (clicked at metric-derived coordinates — gpui dispatch lesson from L1 applies to `on_click` too), seam resize + both clamps with no drift, and the inverted right seam. **Follow-up (2026-11-01):** the left sidebar is now the data crate's `Sidebar` component itself (`.left_sidebar(sidebar)` — selection, keyboard nav, and a11y come free; `app_layout` pulls `data/sidebar`), the right sidebar stays a free element (a details pane is not a list), and `Sidebar` gained a `sidebar-{id}` debug selector for headless tests | +| L2 | layout | Shell ✅ Landed (2026-11-01) | The bootstrap shell: left sidebar region (on by default) + center + right sidebar region (off by default). Sidebar toggle in each sidebar's header, drag-resize widths through `ResizeHandle`; visibility and widths live on an application-owned `LayoutState` (the `InputState` pattern: `set_left_sidebar_visible` / `set_right_sidebar_visible` / `is_*` readers). The center region hosts the `Dock`. Shipped so: the left sidebar is the data crate's `Sidebar` component (`.left_sidebar(sidebar)` — selection, keyboard nav, and a11y come for free; `shell` pulls `data/sidebar`, per the "may compose data" note), the right sidebar and center are app elements (the right side is a details pane, not a list); `Sidebar` gained a `sidebar-{id}` debug selector for headless tests. Each visible sidebar renders a `strip_height` header strip with an end-aligned ghost icon toggle (`IconRole::PanelLeft`/`PanelRight` — new roles this unit: Segoe `DockLeft`/`DockRight` E90C/E90D per the MDL2 table, Lucide `panel-left`/`panel-right` bundled, freedesktop `sidebar-expand-*` → `sidebar-show-symbolic` cascade); `LayoutState` holds visibility + pixel widths with 160/480 px drag clamps (menu-cap px precedent); the right handle inverts the delta so dragging left grows it. `shell` pulls only core's `button` + `data/sidebar`; the facade `layout` feature re-requests `full` (core/data workspace deps are now `default-features = false`). The shell always paints the window frame around itself (L2.5), so it must be the window's root. 5 headless tests in `crates/layout/tests/shell.rs`: defaults, toggling via the state API, the header toggle button (clicked at metric-derived coordinates — gpui dispatch lesson from L1 applies to `on_click` too), seam resize + both clamps with no drift, and the inverted right seam | | L3 | layout | Dock | The center pane tree, controlled: `DockState` = `Pane { tabs, active }` or `Split { axis, ratio, a, b }`; panels are registered descriptors (`id` + view builder); add / close / activate are plain state ops; a pane emptied by closing its last tab collapses its split. Pane headers show titles, the active tab's close button, and a context menu (via core `ContextMenu`). | | L4 | layout | Dock drag-to-split | Dragging a tab: drop on a pane's edge splits a new pane there; drop on a pane's center moves the tab into that pane; a pane emptied by the move collapses. Edge drop zones paint from theme tokens while a drag is live. Split/merge go through the same `DockState` ops L3 exposed — the drag is interaction, not a second API. | +| 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 | | L5 | layout | Sidebar auto-open | Panels declare `needs_right_sidebar` on their descriptor; activating such a panel opens the right sidebar; closing it leaves visibility to the user. The gallery gains a workspace-style shell page — the bootstrap example apps copy. | 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. @@ -304,7 +305,7 @@ Deferred until a real need: layout persistence/serialization, pane zoom (maximiz | 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, AppLayout bootstrap (left sidebar on, right sidebar off, center dock), 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 | +| H — layout crate (§6.4) | ResizeHandle, Shell bootstrap (left sidebar on, right sidebar off, center dock) + the client-side window frame, 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 | Gate for every phase: `cargo fmt --check`, `cargo clippy --workspace --all-targets -- -D warnings`, `cargo test`, gallery smoke run. @@ -374,6 +375,6 @@ Dropped from rev 3: D2, D5, D9 (port-era decisions); the review gate returns in 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 ✅, AppLayout ✅, the center Dock with drag-to-split, and panel-declared sidebar auto-open — each through the review gate, with signed's `crates/dock` as the usage reference and the gallery workspace page as the example. Order against M9+ is decided when the crate starts. +9. Then — the layout crate (§6.4): ResizeHandle ✅, Shell ✅, window frame ✅, the center Dock with drag-to-split, and panel-declared sidebar auto-open — each through the review gate, with signed's `crates/dock` as the usage reference and the gallery workspace page as the example. Order against M9+ is decided when the crate starts. Batched polish candidates between units: scrollbar auto-hide + horizontal thumb, `Undo`/`Redo` menu events. diff --git a/examples/gallery/src/main.rs b/examples/gallery/src/main.rs index 88ee0a7..d2bdb6b 100644 --- a/examples/gallery/src/main.rs +++ b/examples/gallery/src/main.rs @@ -34,8 +34,8 @@ use gpui_native_data::sidebar::{Sidebar, SidebarEntry}; use gpui_native_data::tabs::{Tab, Tabs}; use gpui_native_data::tree::{Tree, TreeItem}; use gpui_native_icons::{IconAssets, IconRole, icon}; -use gpui_native_layout::app_layout::{AppLayout, LayoutState}; use gpui_native_layout::resize_handle::ResizeHandle; +use gpui_native_layout::shell::{LayoutState, Shell}; use gpui_native_theme::{ActiveTheme as _, Size, Theme}; use gpui_platform::application; @@ -216,7 +216,7 @@ impl Gallery { .child("Center \u{2014} the dock lands here (L3)") .text_color(colors.muted_foreground); - AppLayout::new("gallery-shell", state) + Shell::new("gallery-shell", state) .left_sidebar(left_sidebar) .center(center) .right_sidebar(right_sidebar)