| root: Add Base window hosting and a single Kit startup entry point (#3152) ## Summary Window startup now always mounts a `gpui_base::Root`, regardless of Cargo feature unification. Base owns application content, overlay hosting, keyboard traversal, and text-selection copying. Component initialization registers its per-window presentation state as a Root plugin, so dialogs, sheets, notifications, menus, tooltips, touch selection, and window chrome remain automatic without making Base depend on Component. - Add `gpui_kit::open_window` as the standard Kit application entry point. It always mounts Base Root and returns both the window handle and content entity. - Move Root ownership and unconditional overlay hosting into `gpui-base`; `gpui_component::Root` is now a re-export. Root now renders sheet, dialog, and notification layers automatically. - Add the typed `RootPlugin` interface. Plugins are registered before window creation, instantiated independently per window, rendered in registration order, and can prepare, style, and decorate the root surface. - Keep Component presentation in a `WindowState` Root plugin while application-facing operations remain on `WindowExt`. - Make `window_border()` solely responsible for client-side window chrome. Server-decorated windows pass content through unchanged; client-decorated windows receive borders, shadow inset, rounded corners, and resize hit zones. - Remove `Root::bordered` and `Root::window_shadow_size`, plus the obsolete `root_borderless` example. - Migrate Kit examples, stories, tests, documentation, and pending 0.7.0 release notes to the new startup and Root APIs. ## Public API ### gpui-base ```rust pub trait RootPlugin: Render + Sized { fn prepare(&mut self, window: &mut Window, cx: &mut Context<Self>) {} fn style(&self, surface: &mut Stateful<Div>, window: &mut Window, cx: &mut App) {} fn decorate( &self, surface: AnyElement, root: &Root, window: &mut Window, cx: &mut App, ) -> impl IntoElement { surface } } impl Root { pub fn new( view: impl Into<AnyView>, window: &mut Window, cx: &mut Context<Self>, ) -> Self; pub fn register_plugin<V: RootPlugin>( cx: &mut App, build: fn(&mut Window, &mut Context<V>) -> V, ); pub fn plugin<V: RootPlugin>(&self) -> Option<Entity<V>>; pub fn view(&self) -> &AnyView; pub fn read<'a>(window: &'a Window, cx: &'a App) -> &'a Self; pub fn update<R>( window: &mut Window, cx: &mut App, f: impl FnOnce(&mut Self, &mut Window, &mut Context<Self>) -> R, ) -> R; } ``` `Root` also implements `Styled`. Instance style refinements are applied after plugin defaults and therefore take precedence: ```rust impl Styled for Root { fn style(&mut self) -> &mut StyleRefinement; } ``` Register plugins during explicit application initialization, before creating windows. Re-registering a plugin type replaces its factory for future windows rather than mounting it twice. Registration does not retrofit existing roots. ### gpui-kit ```rust pub fn open_window<V: Render>( options: WindowOptions, cx: &mut App, build: impl FnOnce(&mut Window, &mut App) -> Entity<V>, ) -> Result<(AnyWindowHandle, Entity<V>)>; ``` The builder returns application content, not a Root. Kit mounts Base Root around it. ### gpui-component `pub use gpui_base::Root;` replaces the former Component-owned Root. `gpui_component::init(cx)` registers Component `WindowState` as a Root plugin. Manual layer-rendering and Root-owned dialog, sheet, and notification operations are removed. In particular, `Root::render_sheet_layer`, `Root::render_dialog_layer`, and `Root::render_notification_layer` no longer exist because Root hosts those layers automatically. Use the existing `WindowExt` operations to open and update them. ## Breaking Changes Targeted for 0.7.0; package versions remain unchanged. Use the Kit window entry point and return application content instead of constructing `Root` manually. Retain the returned content entity when direct content access is needed, because the window root is now `gpui_base::Root`: ```diff - cx.open_window(options, |window, cx| { - let content = build_content(window, cx); - cx.new(|cx| Root::new(content, window, cx)) - }) + let (window_handle, content) = + gpui_kit::open_window(options, cx, |window, cx| { + build_content(window, cx) + })?; ``` Delete manual overlay placement. `Root::render_sheet_layer`, `Root::render_dialog_layer`, and `Root::render_notification_layer` are removed because `Root` now hosts these layers automatically: ```diff - let sheet_layer = Root::render_sheet_layer(window, cx); - let dialog_layer = Root::render_dialog_layer(window, cx); - let notification_layer = Root::render_notification_layer(window, cx); - div() .child(content) - .children(sheet_layer) - .children(dialog_layer.map(|layer| deferred(layer).with_priority(1))) - .children(notification_layer) ``` Root window-chrome configuration is removed. Decoration policy comes from GPUI `WindowOptions`; `window_border()` applies client chrome only for `Decorations::Client`: ```diff - Root::bordered - Root::window_shadow_size + window_border() ``` The Root- and WindowExt-owned text-selection methods are removed in favor of `gpui_base::TextSelection`: ```diff - window.selected_text(cx) + TextSelection::selected_text(window, cx) - window.has_text_selection(cx) + TextSelection::has_selection(window, cx) - root.clear_text_selection(window, cx) - window.clear_text_selection(cx) + TextSelection::clear(window, cx) - window.end_text_selection(cx) + TextSelection::end(window, cx) ``` The remaining Component-owned Root operations are removed in favor of the corresponding `WindowExt` methods: ```diff - root.open_dialog(build, window, cx) + window.open_dialog(cx, build) - root.close_dialog(window, cx) + window.close_dialog(cx) - root.close_all_dialogs(window, cx) + window.close_all_dialogs(cx) - root.open_sheet_at(placement, build, window, cx) + window.open_sheet_at(placement, cx, build) - root.close_sheet(window, cx) + window.close_sheet(cx) - root.push_notification(notification, window, cx) + window.push_notification(notification, cx) - root.remove_notification::<T>(window, cx) + window.remove_notification::<T>(cx) - root.remove_notification1::<T>(key, window, cx) + window.remove_notification1::<T>(key, cx) - root.clear_notifications(window, cx) + window.clear_notifications(cx) ``` ## Test Plan - `cargo test -p gpui-base --lib` — 993 passed. - `cargo test -p gpui-component --lib` — 559 passed. - `cargo test -p gpui-kit --features test-support,component,assets --test root` — 6 passed. - `cargo test -p gpui-kit --features test-support,component,assets --tests` — 114 passed during the window-startup migration. - `cargo test -p gpui-kit --features test-support,component,assets --test rendering` — 2 Metal pixel checks passed. - `cargo clippy -p gpui-base -p gpui-component -p gpui-kit --all-targets --features gpui-kit/test-support -- --deny warnings` — passed. - `cargo check -p gpui-kit --no-default-features` — passed. - `cargo fmt --all --check` — passed. - `git diff --check` — passed. - `script/check-ai-recipes` — 9 published recipe fragments passed. AI-assisted changes prepared with Codex. --------- Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com> Co-authored-by: Codex <codex@openai.com> | 30 天前 |
| tests: Build each crate's integration tests as one binary (#3481) ## Description Every file directly under a `tests/` directory is its own test binary. Each one is linked with GPUI and every component, about 230 MB in a debug build. CI's test step and `cargo test -p gpui-shell --tests` built 90 binaries between them, and a fresh worktree held 34G of `target` after running both. This PR makes each crate's integration tests one binary: - **`crates/kit/tests/integration/`**: the 30 files that had a `[[test]]` entry (or none) are now modules of one binary. Each module is behind the `cfg(feature = ...)` that its `required-features` used to state, so `cargo test -p gpui-kit` with or without `test-support` builds what it did before. Two stay as targets of their own: - `rendering` needs a real renderer and its own harness. - `ui` is the example `website/docs/test.md` shows, and readers are told to copy it as their own `tests/ui.rs` next to `tests/common/mod.rs`. It is unchanged. - **`crates/component/tests/integration/`**: the 7 compat tests are now modules of one binary. - **`crates/shell/tests/integration/`**: the 31 host tests are now modules of one binary. - Relative `#[path]` and `include_str!` paths gain one `../`. - `support.rs` and `typed_child.rs` are loaded once for the binary, and the other files now `use` them. Clippy's `duplicate_mod` refuses a file loaded as a module twice. - Each file's `NEXT_APP` counter and its probes stay its own. The scratch directories already carry a per-file prefix, so files sharing one process do not collide. - **Commands that named a target**: `crates/kit/TESTING.md`, the input suite README, `script/test-input`, and the en and zh-CN `test.md` and `multi-window.md` now use `--test integration` with a module filter, e.g. `-- input::history::`. `examples/ai_recipes` includes `test_macro.rs` from its new path. Every exact test name in those docs was checked against `--list`. Before merging, I checked the source for process-wide state that the merged files would now share: environment variables, the working directory, statics that can be switched, and registries the tests write to. I found none. The statics in `src` are ID counters, `thread_local`s, or caches filled once. ## Measurements From a fresh worktree, running CI's test step and then `cargo test -p gpui-shell --tests`: | | `main` | this PR | |---|---|---| | `target` | 34G | 18G | | test-step binaries (`Running`) | 59 | 25 | | `gpui-shell --tests` binaries | 33 | 3 | - CI's test step passes 2577 before and after. - The kit, component and shell binaries match the files they replace: 385, 48 and 128 tests. - `gpui-shell --tests` goes from 989 tests to 977. The 12 are `support.rs`'s and `typed_child.rs`'s own unit tests, which ran once per file that loaded them (9 and 5 times) and now run once. - Each merged binary passed three runs in a row, and once each at `--test-threads=2` and `--test-threads=1`. - `cargo fmt --check` and both of CI's clippy invocations are clean. `cargo clippy --all-targets` over kit, component, shell and the recipes is clean too. Two `gpui-shell` lib tests failed once locally, on `main` as well as here, and passed when run alone: - `tests::template::what_automatic_templating_of_the_safe_helpers_would_buy`, a timing comparison that CI runs in `--release`; - `tests::network::websocket_pending_read_does_not_block_write_or_close`. This PR does not touch either. ## Breaking Changes None for users of the crates. For contributors, `--test input`, `--test lifecycle` and the other per-file targets are now `--test integration -- <module>::`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Jason Lee <huacnlee@gmail.com> | 9 天前 |