From e62c506dff3e13b5c8cecf8d4f25e1096d527edb Mon Sep 17 00:00:00 2001 From: Tryanks Date: Mon, 28 Sep 2026 12:48:48 +0800 Subject: [PATCH] Bump gpui-kit to 0.7.0 and gpui-pre to 0.3.7 Host every window on gpui-base's Root: tcode's dialogs, notifications and touch edit menu become a RootPlugin, and Root owns focus traversal, selection copy and the selection layer. The terminal unbinds Root's Tab and copy keys so they still reach the PTY, and Shift-Tab in the composer is a ToggleInteractionMode action in its own context. Also adopt the upstream fixes that apply to tcode's copies: weak captures in the menu subscriptions (the PopoverState leak), Selectable::open for popover triggers, and with_rendered_element for Markdown selection registrations. --- Cargo.lock | 121 ++++++------- crates/android/Cargo.toml | 2 +- crates/app/Cargo.toml | 4 +- crates/app/src/preview_smoke.rs | 12 +- crates/ios/Cargo.toml | 2 +- crates/platform/gpui-android/Cargo.toml | 6 +- .../gpui-android/src/android/window.rs | 2 +- crates/platform/gpui-ios/Cargo.toml | 6 +- crates/platform/gpui-ios/src/ios/window.rs | 2 +- crates/ui/Cargo.toml | 16 +- crates/ui/examples/gallery.rs | 3 +- crates/ui/src/add_project_dialog.rs | 3 +- crates/ui/src/composer/mod.rs | 18 +- crates/ui/src/markdown/mod.rs | 9 +- crates/ui/src/markdown/selection_adapter.rs | 3 +- crates/ui/src/markdown/view.rs | 12 +- crates/ui/src/overlay/mod.rs | 167 ++++++------------ crates/ui/src/project_icon.rs | 2 +- crates/ui/src/run.rs | 16 +- crates/ui/src/settings_page.rs | 2 +- crates/ui/src/shortcut.rs | 101 +++++++++++ crates/ui/src/store/images.rs | 2 +- crates/ui/src/terminal_drawer.rs | 3 + crates/ui/src/theme.rs | 6 +- .../ui/src/touch_selection/window_overlay.rs | 7 +- crates/ui/src/widgets/menu.rs | 18 +- crates/ui/src/widgets/popover.rs | 4 +- crates/web/Cargo.toml | 4 +- 28 files changed, 305 insertions(+), 248 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 07fd58f1..fc244d13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3597,7 +3597,7 @@ dependencies = [ [[package]] name = "gpui-android" -version = "0.3.6" +version = "0.3.7" dependencies = [ "android-activity", "anyhow", @@ -3616,9 +3616,9 @@ dependencies = [ [[package]] name = "gpui-base" -version = "0.6.6" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e904ee0d351f4e85b967e26ae986d3aa195a8d9d35fcd18184a2c40989694e" +checksum = "64bb52b29a8dcdc8d3f595b8b0839c076f237572ab601ba4577d3da8ae085728" dependencies = [ "aho-corasick", "anyhow", @@ -3632,9 +3632,11 @@ dependencies = [ "gpui-pre-sum-tree", "html5ever 0.27.0", "instant", + "itertools 0.13.0", "lsp-types", "markdown", "markup5ever_rcdom", + "num-traits", "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", @@ -3654,9 +3656,9 @@ dependencies = [ [[package]] name = "gpui-component-macros" -version = "0.6.6" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3867545508c7cf6a5f6da586f60d894a3f8e7cce94b7fe82df7fad5f868ec20e" +checksum = "830b3574bd6e108b9a09c9d584e60626c9140add6d8fe839bf57627577b7977e" dependencies = [ "proc-macro-crate 3.5.0", "proc-macro2", @@ -3666,7 +3668,7 @@ dependencies = [ [[package]] name = "gpui-ios" -version = "0.3.6" +version = "0.3.7" dependencies = [ "anyhow", "core-foundation 0.10.1", @@ -3687,9 +3689,9 @@ dependencies = [ [[package]] name = "gpui-kit-assets" -version = "0.6.6" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "111d3d03a68454318a193f137419864ccebdc7d3dc0194b0b0b3f818a6bc3d55" +checksum = "885a2abd416e29a76ef1553aea06eb2645a9290cf5e1d3fa02d218ecac6d318f" dependencies = [ "anyhow", "gpui-pre", @@ -3702,9 +3704,9 @@ dependencies = [ [[package]] name = "gpui-pre" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0437c0b83e636a92bd1a39fa1d05fb632ae671289537497b35871ffbe231b84" +checksum = "0e87a42bb37c7cb4e76dd1ac0ce88851e46e976e0373a47ab3e0757abffee54d" dependencies = [ "accesskit", "anyhow", @@ -3774,12 +3776,12 @@ dependencies = [ [[package]] name = "gpui-pre-apple" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3af534f0746eb9e014114bcc0ee00dc250c4b8d75df2e437180c403aa65649c1" +checksum = "00053551517815ab169fda28fff78dbebf2712722593c7671c511785cb3f79d5" dependencies = [ "anyhow", - "block", + "block2 0.6.2", "cbindgen", "cocoa 0.26.0", "core-foundation 0.10.1", @@ -3798,9 +3800,9 @@ dependencies = [ [[package]] name = "gpui-pre-collections" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15e7a65204039187d1f4b3d4c2e0233eeac54d4acb0b13f9dc1a3b5adad26c1f" +checksum = "f79760925c31ed06924d28d68981b5b22b2ce25665f58f0a33dfea11170e6e28" dependencies = [ "gpui-pre-util", "indexmap 2.14.0", @@ -3809,9 +3811,9 @@ dependencies = [ [[package]] name = "gpui-pre-derive-refineable" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79dfb4a05f3cd9893d7768dbf5dba1bbbe99cab7394e89b360ccb3b2bcb6b34e" +checksum = "d1aea3f941581a6a9dd545ecd351ef2d336fe6e2a1a4555cb6414944a4984260" dependencies = [ "proc-macro2", "quote", @@ -3820,9 +3822,9 @@ dependencies = [ [[package]] name = "gpui-pre-http-client" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd5a7542c77887a5aff997d78c95884119c8463409569b63fcf060956da1682" +checksum = "ddce27e989e877a3a1a243f97d7ea4f1ade2532abec418d5a0be9cca802702c7" dependencies = [ "anyhow", "async-compression", @@ -3841,9 +3843,9 @@ dependencies = [ [[package]] name = "gpui-pre-http-client-tls" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72aed510adb5e7723f8f57b9751007e3451d46a84edb59b92227178cf59cc19c" +checksum = "0076f6cf87b87b5629d55692638df4edc04fab3da92f897c2294d520efac17d3" dependencies = [ "log", "rustls", @@ -3853,9 +3855,9 @@ dependencies = [ [[package]] name = "gpui-pre-linux" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aab655cda1d6dcd549ac869e8678babc84cf573ab6794a1fe1a39c6c70c60b48" +checksum = "0aac7022e347409454777701082201742710052813964a1e25160f7e22c965ad" dependencies = [ "accesskit", "accesskit_unix", @@ -3900,9 +3902,9 @@ dependencies = [ [[package]] name = "gpui-pre-macos" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951d17e41a72067ad2d35c60e08a62e8ad7fa9400b41c802e26cfb459f6c05da" +checksum = "5a43af845b260b09393e923c4f1e1a67a10fcfc847b4815192bbfd02ed9fe725" dependencies = [ "accesskit", "accesskit_macos", @@ -3947,9 +3949,9 @@ dependencies = [ [[package]] name = "gpui-pre-macros" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6324092b40ea8ec7c327a4a28427e366a489b4d5d23b5d1bd0243fc2f4ca7a9e" +checksum = "e1627c5dd3c3351e39e0621d3f3e31dbbe7c7658df90b1d2ec12bfe95d54cd37" dependencies = [ "heck 0.5.0", "proc-macro-crate 3.5.0", @@ -3960,9 +3962,9 @@ dependencies = [ [[package]] name = "gpui-pre-perf" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94bff26ccd8105f6206144f8dab16a2dc02e7a68f39dada02cdf492f0afd940" +checksum = "01954497dd02ba96ab4c252a8668643b0cc3085e7a75dca62e7ba0e3dcd83b02" dependencies = [ "gpui-pre-collections", "serde", @@ -3971,10 +3973,11 @@ dependencies = [ [[package]] name = "gpui-pre-platform" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8423d1e0a693d2f9f16c0326bb01b5e81ec0531854d596a18476934ac6880478" +checksum = "0112105c2ac6757fbf527c9c92a753be9bcbce9f4cf26959a11eaa1db6565aa5" dependencies = [ + "anyhow", "console_error_panic_hook", "gpui-pre", "gpui-pre-linux", @@ -3985,9 +3988,9 @@ dependencies = [ [[package]] name = "gpui-pre-refineable" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93b4b0a8d296a3ee5beb1136bc48f8167a6028eb1e243b56851dcd8c9fc6d7a1" +checksum = "ffd14639e5dfab906bbaeabfe0f94289120dcf2e6ed37217deb13964867e89a5" dependencies = [ "gpui-pre-derive-refineable", ] @@ -4045,9 +4048,9 @@ dependencies = [ [[package]] name = "gpui-pre-reqwest-client" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f32ccea5d1a7c8d2868d7b346c2c1f76aa3f86ec9e0575c4e7340af3afd9e9d" +checksum = "53fa47d680f9e186b33af323f97345d67b6150c0e4c0bc894406bff822a4d9b4" dependencies = [ "anyhow", "bytes", @@ -4063,9 +4066,9 @@ dependencies = [ [[package]] name = "gpui-pre-scheduler" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff7b10b3d2fc3fda24847a04436eca9531612c8414874a37ddc005f20e521e12" +checksum = "8049963b716df01b67b58f86000597f222d5b31b15b35069b23a3b07f3c68352" dependencies = [ "async-task", "backtrace", @@ -4080,9 +4083,9 @@ dependencies = [ [[package]] name = "gpui-pre-shared-string" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe17f81bd98a0253140ea79f7f76442d7499bd8d17db302861af8e46c11d38df" +checksum = "8ef6314bdf5ab162c1913712ec4a91b013ce2a9cfb0b557ecdfdd5793c8b61d3" dependencies = [ "schemars 1.2.2", "serde", @@ -4091,9 +4094,9 @@ dependencies = [ [[package]] name = "gpui-pre-sum-tree" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6d35e998a51d24248dad731de1f09d386cbbf46a04edde3b67f469b2ce3f16" +checksum = "5cca7e5fad9d53b2d265860fefdffc5f171966cb99c3fd99ecac15c033f19209" dependencies = [ "gpui-pre-ztracing", "heapless", @@ -4104,9 +4107,9 @@ dependencies = [ [[package]] name = "gpui-pre-util" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a026a30c578930d20974fd8c3d77de36223ac7ba193bd4259e7238929bc8806" +checksum = "d82e06359ae76f8bb07ed713c54adb80cea6a9a2d3dc715f41aec30cd2aadb4d" dependencies = [ "anyhow", "log", @@ -4115,9 +4118,9 @@ dependencies = [ [[package]] name = "gpui-pre-util-macros" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbf79627a6afc626d4e887dad9d2d92bb069413e8cc692ba3e90bff219eb7a9" +checksum = "b8a0da4143bb0e7eb926eed7edd3a61c9d17344207543dcc25fb398f0cb96b2f" dependencies = [ "gpui-pre-perf", "quote", @@ -4126,9 +4129,9 @@ dependencies = [ [[package]] name = "gpui-pre-web" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34be433b9fcb370b1fe9a1c7fe911f0eb6058791ef8167dcf6c5a5deb624975c" +checksum = "ab3870cc909471bb830c3d79496770c754401aad6cfa122147ccb13e8908ad08" dependencies = [ "anyhow", "console_error_panic_hook", @@ -4155,9 +4158,9 @@ dependencies = [ [[package]] name = "gpui-pre-wgpu" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05515b32799b1767d69300921c1b562cd53104965211a0e8905acdc7aa18e693" +checksum = "f0b02657b56b09140ce542f5e4434fb96d9fdaba0fd0035ba7dba13c171ddb9b" dependencies = [ "anyhow", "bytemuck", @@ -4182,9 +4185,9 @@ dependencies = [ [[package]] name = "gpui-pre-windows" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27662e65bfcf4445d2cccfad91f1570ec07f4b1b5de98400a9f207fd19c464cf" +checksum = "f05592a6f9e3e6bb7a9f2a0d4779271cf747a948db1c07b02448de777ffff8f3" dependencies = [ "accesskit", "accesskit_windows", @@ -4211,9 +4214,9 @@ dependencies = [ [[package]] name = "gpui-pre-zlog" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3a5af0b734a391a84872eb770b3ee9830091ec2487b79d06371fe14eefb2b5d" +checksum = "a7506302091e66d7fb5e04a5b3480e90cff0bbe3157458d7df91a41c59238c48" dependencies = [ "anyhow", "chrono", @@ -4223,9 +4226,9 @@ dependencies = [ [[package]] name = "gpui-pre-ztracing" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a24cb19f78c5bc3aab8ddf1e8e0bd6b402fbcb9c7f112f64ddbedef8f8768a59" +checksum = "4ed1bc6d53b3ca93785a8573d1066d555b67bc475b344fb28871f9811eb6f218" dependencies = [ "gpui-pre-zlog", "gpui-pre-ztracing-macro", @@ -4235,15 +4238,15 @@ dependencies = [ [[package]] name = "gpui-pre-ztracing-macro" -version = "0.3.6" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1bb25850ad31b88bbdef454afb93ec4cdb81f5e200ba5b3f1a0368111780d2c" +checksum = "829258540ea51bb9ea4a71686beffe5c99586901ba9f39944d06e4a180ab63fe" [[package]] name = "gpui-wry" -version = "0.6.6" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "529390779b0b86f97444b0512e4d5605e755f16210a53617eaa7c54bee21a4d0" +checksum = "2448441bc9e8415b8128f4e02f534462368c1205ba5a864fd9aa85963f53b478" dependencies = [ "anyhow", "gpui-pre", diff --git a/crates/android/Cargo.toml b/crates/android/Cargo.toml index d3a5f972..0610fd09 100644 --- a/crates/android/Cargo.toml +++ b/crates/android/Cargo.toml @@ -9,7 +9,7 @@ name = "tcode_android" crate-type = ["cdylib", "rlib"] [dependencies] -gpui = { package = "gpui-pre", version = "=0.3.6", default-features = false } +gpui = { package = "gpui-pre", version = "=0.3.7", default-features = false } gpui-android = { path = "../platform/gpui-android" } log = "0.4" tcode-client = { path = "../client" } diff --git a/crates/app/Cargo.toml b/crates/app/Cargo.toml index 86e74b4e..17637863 100644 --- a/crates/app/Cargo.toml +++ b/crates/app/Cargo.toml @@ -23,8 +23,8 @@ preview-mcp = { path = "../preview-mcp" } orchestrate-mcp = { path = "../orchestrate-mcp" } computer-use-mcp = { path = "../computer-use-mcp" } mcp-host = { path = "../mcp-host" } -gpui = { package = "gpui-pre", version = "0.3.6" } -gpui_platform = { package = "gpui-pre-platform", version = "0.3.6", features = ["font-kit", "x11", "wayland", "runtime_shaders"] } +gpui = { package = "gpui-pre", version = "0.3.7" } +gpui_platform = { package = "gpui-pre-platform", version = "0.3.7", features = ["font-kit", "x11", "wayland", "runtime_shaders"] } env_logger = "0.11" log = "0.4" smol = "2" diff --git a/crates/app/src/preview_smoke.rs b/crates/app/src/preview_smoke.rs index c6fae7b7..c102843f 100644 --- a/crates/app/src/preview_smoke.rs +++ b/crates/app/src/preview_smoke.rs @@ -5,8 +5,8 @@ use std::sync::{ }; use std::time::{Duration, Instant}; -use gpui::{AsyncApp, Entity, WindowHandle, px, size}; -use tcode_ui::{AppShell, overlay::OverlayHost}; +use gpui::{AnyWindowHandle, AsyncApp, Entity, px, size}; +use tcode_ui::AppShell; const STEP_DELAY: Duration = Duration::from_millis(20); const RAPID_DELAY: Duration = Duration::from_millis(5); @@ -85,7 +85,7 @@ async fn yield_for(cx: &AsyncApp, delay: Duration) { fn create_once( shell: &Entity, - window: WindowHandle, + window: AnyWindowHandle, key: &str, url: &str, cx: &mut AsyncApp, @@ -128,7 +128,7 @@ fn creation_is_pending(error: &str) -> bool { async fn create_and_wait( shell: &Entity, - window: WindowHandle, + window: AnyWindowHandle, key: &str, url: &str, cx: &mut AsyncApp, @@ -154,7 +154,7 @@ async fn create_and_wait( #[cfg(target_os = "windows")] async fn start_and_wait_until_in_flight( shell: &Entity, - window: WindowHandle, + window: AnyWindowHandle, key: &str, url: &str, cx: &mut AsyncApp, @@ -178,7 +178,7 @@ async fn start_and_wait_until_in_flight( pub async fn run( watchdog: Watchdog, shell: Entity, - window: WindowHandle, + window: AnyWindowHandle, cx: &mut AsyncApp, ) { watchdog.start_phase("create-first"); diff --git a/crates/ios/Cargo.toml b/crates/ios/Cargo.toml index 5d8c6aea..0e0a61cb 100644 --- a/crates/ios/Cargo.toml +++ b/crates/ios/Cargo.toml @@ -9,7 +9,7 @@ name = "tcode_ios" crate-type = ["staticlib", "rlib"] [dependencies] -gpui = { package = "gpui-pre", version = "=0.3.6", default-features = false } +gpui = { package = "gpui-pre", version = "=0.3.7", default-features = false } gpui-ios = { path = "../platform/gpui-ios" } log = "0.4" tcode-client = { path = "../client" } diff --git a/crates/platform/gpui-android/Cargo.toml b/crates/platform/gpui-android/Cargo.toml index a4a50b5c..ee284758 100644 --- a/crates/platform/gpui-android/Cargo.toml +++ b/crates/platform/gpui-android/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "gpui-android" -version = "0.3.6" +version = "0.3.7" edition = "2024" license = "Apache-2.0" publish = false description = "Android platform backend for gpui-pre (Vulkan via gpui-pre-wgpu)" [dependencies] -gpui = { package = "gpui-pre", version = "=0.3.6", default-features = false } -gpui_wgpu = { package = "gpui-pre-wgpu", version = "=0.3.6" } +gpui = { package = "gpui-pre", version = "=0.3.7", default-features = false } +gpui_wgpu = { package = "gpui-pre-wgpu", version = "=0.3.7" } anyhow = "1" log = "0.4" parking_lot = "0.12" diff --git a/crates/platform/gpui-android/src/android/window.rs b/crates/platform/gpui-android/src/android/window.rs index ebab83f8..73fcbe06 100644 --- a/crates/platform/gpui-android/src/android/window.rs +++ b/crates/platform/gpui-android/src/android/window.rs @@ -1007,7 +1007,7 @@ impl PlatformWindow for AndroidWindow { } fn gpu_specs(&self) -> Option { - Some(self.0.state.borrow().renderer.gpu_specs()) + self.0.state.borrow().renderer.gpu_specs() } fn update_ime_position(&self, _bounds: Bounds) {} diff --git a/crates/platform/gpui-ios/Cargo.toml b/crates/platform/gpui-ios/Cargo.toml index 44b2209d..94ab54fd 100644 --- a/crates/platform/gpui-ios/Cargo.toml +++ b/crates/platform/gpui-ios/Cargo.toml @@ -1,14 +1,14 @@ [package] name = "gpui-ios" -version = "0.3.6" +version = "0.3.7" edition = "2024" license = "Apache-2.0" publish = false description = "iOS platform backend for gpui-pre (Metal via gpui-pre-wgpu)" [dependencies] -gpui = { package = "gpui-pre", version = "=0.3.6", default-features = false } -gpui_wgpu = { package = "gpui-pre-wgpu", version = "=0.3.6" } +gpui = { package = "gpui-pre", version = "=0.3.7", default-features = false } +gpui_wgpu = { package = "gpui-pre-wgpu", version = "=0.3.7" } anyhow = "1" log = "0.4" parking_lot = "0.12" diff --git a/crates/platform/gpui-ios/src/ios/window.rs b/crates/platform/gpui-ios/src/ios/window.rs index 797b8adf..7ef01e0f 100644 --- a/crates/platform/gpui-ios/src/ios/window.rs +++ b/crates/platform/gpui-ios/src/ios/window.rs @@ -689,7 +689,7 @@ impl PlatformWindow for IosWindow { } fn gpu_specs(&self) -> Option { - Some(self.renderer.borrow().gpu_specs()) + self.renderer.borrow().gpu_specs() } fn update_ime_position(&self, _bounds: Bounds) {} diff --git a/crates/ui/Cargo.toml b/crates/ui/Cargo.toml index 057c1f0b..a43f9628 100644 --- a/crates/ui/Cargo.toml +++ b/crates/ui/Cargo.toml @@ -58,10 +58,10 @@ tcode-client = { path = "../client" } tcode-core = { path = "../core", default-features = false } tcode-protocol = { path = "../protocol", default-features = false } qrcode = { version = "0.14", default-features = false } -gpui = { package = "gpui-pre", version = "0.3.6" } -gpui-base = "0.6.6" -gpui-component-assets = { package = "gpui-kit-assets", version = "0.6.6" } -gpui-component-macros = "0.6.6" +gpui = { package = "gpui-pre", version = "0.3.7" } +gpui-base = "0.7.0" +gpui-component-assets = { package = "gpui-kit-assets", version = "0.7.0" } +gpui-component-macros = "0.7.0" image = { version = "0.25.10", default-features = false, features = ["png", "jpeg", "gif", "webp", "bmp", "tiff"] } base64 = "0.23" async-channel = "2" @@ -88,14 +88,14 @@ web-time = "1" # iOS off), so it is left to the composition roots. [target.'cfg(not(target_family = "wasm"))'.dependencies] tcode-traverse = { path = "../traverse", default-features = false, optional = true } -reqwest-client = { package = "gpui-pre-reqwest-client", version = "=0.3.6" } +reqwest-client = { package = "gpui-pre-reqwest-client", version = "=0.3.7" } # Android apps do not have the Unix certificate files used by native-roots discovery. reqwest = { package = "gpui-pre-reqwest", version = "0.12.15", default-features = false, features = ["rustls-tls-webpki-roots"] } # The preview backend exists only where a system webview can be embedded as a # child of the GPUI window, so its whole dependency set is target-scoped. [target.'cfg(any(target_os = "macos", target_os = "windows"))'.dependencies] -gpui-wry = { version = "0.6.6", optional = true } +gpui-wry = { version = "0.7.0", optional = true } wry = { version = "0.53.3", package = "lb-wry", features = ["mac-proxy"], optional = true } raw-window-handle = { version = "0.6", features = ["std"] } # Only the Windows WebContext needs it, but Cargo has no per-feature target @@ -135,8 +135,8 @@ windows-core = { version = "0.61", optional = true } [dev-dependencies] criterion = "0.8" -gpui = { package = "gpui-pre", version = "0.3.6", features = ["test-support"] } -gpui_platform = { package = "gpui-pre-platform", version = "0.3.6", features = ["font-kit", "x11", "wayland", "runtime_shaders"] } +gpui = { package = "gpui-pre", version = "0.3.7", features = ["test-support"] } +gpui_platform = { package = "gpui-pre-platform", version = "0.3.7", features = ["font-kit", "x11", "wayland", "runtime_shaders"] } smol = "2" tcode-runtime = { path = "../runtime", features = ["test-support"] } # dump-create is only needed by examples/sanitize_syntaxes.rs and the syntax-set diff --git a/crates/ui/examples/gallery.rs b/crates/ui/examples/gallery.rs index 4c87826f..ad426d5f 100644 --- a/crates/ui/examples/gallery.rs +++ b/crates/ui/examples/gallery.rs @@ -10,7 +10,6 @@ use gpui::{ }; use gpui_base::{StyledExt as _, h_flex, v_flex}; use tcode_core::session::{EntryContent, OrchestrateCallback, TimelineEntry}; -use tcode_ui::overlay::OverlayHost; use tcode_ui::theme::{self, ActiveTheme as _}; use tcode_ui::{assets, gallery_support, markdown::MarkdownState}; @@ -537,7 +536,7 @@ fn main() { }; cx.open_window(options, |window, cx| { let gallery = cx.new(Gallery::new); - cx.new(|cx| OverlayHost::new(gallery, window, cx)) + cx.new(|cx| gpui_base::Root::new(gallery, window, cx)) }) .expect("failed to open component gallery window"); }); diff --git a/crates/ui/src/add_project_dialog.rs b/crates/ui/src/add_project_dialog.rs index a1cc6750..b529d37c 100644 --- a/crates/ui/src/add_project_dialog.rs +++ b/crates/ui/src/add_project_dialog.rs @@ -606,6 +606,7 @@ mod tests { /// against its own path rules. #[gpui::test] fn a_remote_project_root_is_judged_by_the_host(cx: &mut TestAppContext) { + cx.update(crate::theme::init); let root = std::env::temp_dir().join(format!( "tcode-add-project-remote-{}", tcode_services::store::now_millis() @@ -638,7 +639,7 @@ mod tests { let (_root, cx) = cx.add_window_view(move |window, cx| { let dialog = cx.new(|cx| AddProjectDialog::new(store_for_view.clone(), window, cx)); *capture.borrow_mut() = Some(dialog.clone()); - crate::overlay::OverlayHost::new(dialog, window, cx) + gpui_base::Root::new(dialog, window, cx) }); let cx: &mut VisualTestContext = cx; let dialog = built.borrow().clone().expect("dialog was built"); diff --git a/crates/ui/src/composer/mod.rs b/crates/ui/src/composer/mod.rs index 97dc2769..441bacb6 100644 --- a/crates/ui/src/composer/mod.rs +++ b/crates/ui/src/composer/mod.rs @@ -38,6 +38,10 @@ use gpui::{ Subscription, Task, Window, div, img, prelude::FluentBuilder as _, px, rgb, }; use gpui_base::PopoverState; + +pub(crate) const CONTEXT: &str = "Composer"; + +gpui::actions!(tcode_composer, [ToggleInteractionMode]); use gpui_base::{ElementExt as _, StyledExt as _, h_flex, v_flex}; use crate::attachments::attach_error_message; @@ -1303,13 +1307,15 @@ impl Render for Composer { crate::chat::CONTENT_MIN_PADDING })) .pb_2() - .on_key_down(cx.listener(|this, ev: &gpui::KeyDownEvent, _, cx| { - if this.interactive(cx) && ev.keystroke.key == "tab" && ev.keystroke.modifiers.shift - { - this.workspace_store - .update(cx, |store, _cx| store.toggle_interaction_mode()); - cx.notify(); + .key_context(CONTEXT) + .on_action(cx.listener(|this, _: &ToggleInteractionMode, _, cx| { + if !this.interactive(cx) { + cx.propagate(); + return; } + this.workspace_store + .update(cx, |store, _cx| store.toggle_interaction_mode()); + cx.notify(); })) .child( v_flex() diff --git a/crates/ui/src/markdown/mod.rs b/crates/ui/src/markdown/mod.rs index 5c559566..34f3b0fb 100644 --- a/crates/ui/src/markdown/mod.rs +++ b/crates/ui/src/markdown/mod.rs @@ -17,7 +17,7 @@ mod utils; mod view; use gpui::{App, KeyBinding}; -use gpui_base::input::{Copy, SelectAll}; +use gpui_base::input::SelectAll; #[cfg(test)] pub(crate) use parse::parse; @@ -26,13 +26,10 @@ pub use view::MarkdownView; pub(super) const CONTEXT: &str = "MarkdownView"; -/// Register Markdown copy/select-all bindings. +/// Register Markdown select-all bindings. Copying the window selection is +/// the window root's. pub fn init(cx: &mut App) { cx.bind_keys(vec![ - #[cfg(target_os = "macos")] - KeyBinding::new("cmd-c", Copy, Some(CONTEXT)), - #[cfg(not(target_os = "macos"))] - KeyBinding::new("ctrl-c", Copy, Some(CONTEXT)), #[cfg(target_os = "macos")] KeyBinding::new("cmd-a", SelectAll, Some(CONTEXT)), #[cfg(not(target_os = "macos"))] diff --git a/crates/ui/src/markdown/selection_adapter.rs b/crates/ui/src/markdown/selection_adapter.rs index 7d1f1c16..44d30273 100644 --- a/crates/ui/src/markdown/selection_adapter.rs +++ b/crates/ui/src/markdown/selection_adapter.rs @@ -276,7 +276,8 @@ impl MarkdownSelectionAdapter { let registration = TextSelectionRegistration::new(hitbox, bounds) .with_scroll_offset(scroll_offset) .with_document_order(document_order) - .with_text_bounds(frame.text_bounds.clone()); + .with_text_bounds(frame.text_bounds.clone()) + .with_rendered_element(&self.selection, window, cx); match frame.selection_edges { Some((start, end)) => registration.with_selection_edges(start, end), None => registration, diff --git a/crates/ui/src/markdown/view.rs b/crates/ui/src/markdown/view.rs index 0ad85896..bac39bcf 100644 --- a/crates/ui/src/markdown/view.rs +++ b/crates/ui/src/markdown/view.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use crate::overlay::{Notification, OverlayExt as _}; use crate::theme::ActiveTheme as _; use crate::touch_selection::SelectAllTouched; -use crate::widgets::input::{Copy, SelectAll}; +use crate::widgets::input::SelectAll; use crate::widgets::menu::ContextMenuExt as _; use gpui::{ Action, AnyElement, App, Bounds, ClipboardItem, Element, ElementId, Entity, GlobalElementId, @@ -146,16 +146,6 @@ impl Element for MarkdownView { .track_focus(&focus_handle) .w_full() .relative() - .on_action(move |_: &Copy, window, cx| { - let text = gpui_base::TextSelection::selected_text(window, cx) - .trim() - .to_string(); - if text.is_empty() { - cx.propagate(); - } else { - cx.write_to_clipboard(ClipboardItem::new_string(text)); - } - }) .on_action({ let state = state.clone(); move |_: &SelectAll, window, cx| { diff --git a/crates/ui/src/overlay/mod.rs b/crates/ui/src/overlay/mod.rs index 49e6e7e7..b36afe82 100644 --- a/crates/ui/src/overlay/mod.rs +++ b/crates/ui/src/overlay/mod.rs @@ -7,9 +7,9 @@ pub use notification::{Notification, NotificationType}; use std::rc::Rc; use gpui::{ - AnyView, App, AppContext as _, Context, ElementId, Entity, InteractiveElement as _, - IntoElement, ParentElement as _, Render, Styled as _, Window, div, prelude::FluentBuilder as _, - px, + App, AppContext as _, Context, Div, ElementId, Entity, InteractiveElement as _, IntoElement, + ParentElement as _, Refineable as _, Render, Stateful, StyleRefinement, Styled as _, Window, + div, prelude::FluentBuilder as _, px, }; use crate::theme::ActiveTheme as _; @@ -55,30 +55,23 @@ impl OutsideDismissal { } } -/// Window root that owns tcode's modal and toast layers, and the window text -/// selection with the touch surfaces it leaves behind. -pub struct OverlayHost { - view: AnyView, +/// tcode's per-window presentation on the Base [`gpui_base::Root`]: the +/// modal and toast layers, and the touch surfaces the window text selection +/// leaves behind. +pub(crate) struct Overlays { dialogs: Vec, notifications: Entity, touch_selection: Entity, } -struct DetachedView; - -impl Render for DetachedView { - fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { - div().size_full() - } +/// Register the overlays every window root mounts. Call before opening windows. +pub(crate) fn init(cx: &mut App) { + gpui_base::Root::register_plugin(cx, Overlays::new); } -impl OverlayHost { - pub fn new(view: impl Into, window: &mut Window, cx: &mut Context) -> Self { - #[cfg(all(target_os = "macos", not(test)))] - gpui_base::install_window_hit_test_forwarder(window); - +impl Overlays { + fn new(window: &mut Window, cx: &mut Context) -> Self { Self { - view: view.into(), dialogs: Vec::new(), notifications: cx.new(|cx| NotificationList::new(window, cx)), touch_selection: cx.new(|cx| WindowTouchSelectionOverlay::new(window, cx)), @@ -90,11 +83,10 @@ impl OverlayHost { cx: &mut App, f: impl FnOnce(&mut Self, &mut Window, &mut Context) -> R, ) -> R { - let root = window - .root::() - .flatten() - .expect("window root must be tcode_ui::overlay::OverlayHost"); - root.update(cx, |root, cx| f(root, window, cx)) + let overlays = gpui_base::Root::read(window, cx) + .plugin::() + .expect("tcode_ui::overlay::init must run before the window opens"); + overlays.update(cx, |overlays, cx| f(overlays, window, cx)) } fn close_dialog(&mut self, window: &mut Window, cx: &mut Context) { @@ -124,7 +116,18 @@ impl OverlayHost { } } -impl Render for OverlayHost { +impl gpui_base::RootPlugin for Overlays { + fn style(&self, surface: &mut Stateful
, _window: &mut Window, cx: &mut App) { + surface.style().refine( + &StyleRefinement::default() + .bg(crate::material::canvas(cx)) + .text_color(cx.theme().foreground) + .font_family(cx.theme().font_family.clone()), + ); + } +} + +impl Render for Overlays { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let dialog_count = self.dialogs.len(); let dialogs = self @@ -145,52 +148,9 @@ impl Render for OverlayHost { let compact = crate::window_seam::window_is_compact(window, cx); let seam = crate::window_seam::content_insets(window); div() - .relative() - .size_full() - .on_key_down(|event, window, cx| { - let modifiers = event.keystroke.modifiers; - if event.keystroke.key == "tab" - && !modifiers.control - && !modifiers.alt - && !modifiers.platform - && !modifiers.function - { - let step = |window: &mut Window, cx: &mut App| { - if modifiers.shift { - window.focus_prev(cx); - } else { - window.focus_next(cx); - } - }; - let before = window.focused(cx); - let trap = gpui_base::active_focus_trap(window, cx); - step(window, cx); - if let Some(trap) = trap { - let first = window.focused(cx); - while !trap.contains_focused(window, cx) { - step(window, cx); - if window.focused(cx) == first { - if let Some(before) = before { - before.focus(window, cx); - } - break; - } - } - } - cx.stop_propagation(); - } - }) - .bg(crate::material::canvas(cx)) - .text_color(cx.theme().foreground) - .font_family(cx.theme().font_family.clone()) - // The window selection layer is the first child of the window: its - // bubble-phase handlers then run after every control's, owning a - // press only when it propagates, and its capture-phase scroll - // handler runs before the touch pan capture below it, so the edit - // menu steps aside while a finger scrolls. - .child(gpui_base::TextSelectionLayer) - .child(self.view.clone()) - // After the content, so the edit menu floats above what was selected. + .absolute() + .inset_0() + // The edit menu floats above what was selected, under any dialog. .child(self.touch_selection.clone()) .when(!dialogs.is_empty(), |root| { root.child( @@ -240,9 +200,6 @@ fn open_notification_dialog(note: Entity, window: &mut Window, cx: /// Imperative overlay operations used by application views. pub trait OverlayExt { - /// Release the current view graph while its replacement is constructed. - fn detach_view(&mut self, cx: &mut App); - fn replace_view(&mut self, view: impl Into, cx: &mut App); fn open_dialog(&mut self, cx: &mut App, build: F) where F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static; @@ -258,33 +215,11 @@ pub trait OverlayExt { } impl OverlayExt for Window { - fn detach_view(&mut self, cx: &mut App) { - let view = cx.new(|_| DetachedView).into(); - OverlayHost::update(self, cx, move |host, window, cx| { - host.close_all_dialogs(window, cx); - host.notifications - .update(cx, |list, cx| list.clear(window, cx)); - host.view = view; - cx.notify(); - }); - } - - fn replace_view(&mut self, view: impl Into, cx: &mut App) { - let view = view.into(); - OverlayHost::update(self, cx, move |host, window, cx| { - host.close_all_dialogs(window, cx); - host.notifications - .update(cx, |list, cx| list.clear(window, cx)); - host.view = view; - cx.notify(); - }); - } - fn open_dialog(&mut self, cx: &mut App, build: F) where F: Fn(Dialog, &mut Window, &mut App) -> Dialog + 'static, { - OverlayHost::update(self, cx, move |host, window, cx| { + Overlays::update(self, cx, move |host, window, cx| { let focus_handle = cx.focus_handle(); let previous_focus_handle = window.focused(cx).map(|focus| focus.downgrade()); focus_handle.focus(window, cx); @@ -307,11 +242,11 @@ impl OverlayExt for Window { } fn close_dialog(&mut self, cx: &mut App) { - OverlayHost::update(self, cx, |host, window, cx| host.close_dialog(window, cx)); + Overlays::update(self, cx, |host, window, cx| host.close_dialog(window, cx)); } fn close_all_dialogs(&mut self, cx: &mut App) { - OverlayHost::update(self, cx, |host, window, cx| { + Overlays::update(self, cx, |host, window, cx| { host.close_all_dialogs(window, cx) }); } @@ -323,14 +258,14 @@ impl OverlayExt for Window { open_notification_dialog(note, self, cx); return; } - OverlayHost::update(self, cx, |host, window, cx| { + Overlays::update(self, cx, |host, window, cx| { host.notifications .update(cx, |list, cx| list.push(note, window, cx)); }); } fn remove_notification(&mut self, cx: &mut App) { - OverlayHost::update(self, cx, |host, window, cx| { + Overlays::update(self, cx, |host, window, cx| { host.notifications.update(cx, |list, cx| { list.close_by_type(std::any::TypeId::of::(), window, cx) }); @@ -343,7 +278,7 @@ impl OverlayExt for Window { cx: &mut App, ) { let key = key.into(); - OverlayHost::update(self, cx, |host, window, cx| { + Overlays::update(self, cx, |host, window, cx| { host.notifications.update(cx, |list, cx| { list.close((std::any::TypeId::of::(), key), window, cx) }); @@ -351,7 +286,7 @@ impl OverlayExt for Window { } fn clear_notifications(&mut self, cx: &mut App) { - OverlayHost::update(self, cx, |host, window, cx| { + Overlays::update(self, cx, |host, window, cx| { host.notifications .update(cx, |list, cx| list.clear(window, cx)); }); @@ -363,6 +298,18 @@ mod tests { use super::*; use gpui::{TestAppContext, VisualTestContext, size}; + struct Body; + + impl Render for Body { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + div().size_full() + } + } + + fn overlays(root: &Entity, cx: &mut VisualTestContext) -> Entity { + root.read_with(cx, |root, _| root.plugin::().unwrap()) + } + fn draw(cx: &mut VisualTestContext) { cx.run_until_parked(); cx.update(|window, cx| { @@ -375,8 +322,8 @@ mod tests { cx.update(crate::theme::init); cx.update(|cx| crate::window_seam::override_mobile_for_test(cx, true)); let (root, cx) = cx.add_window_view(|window, cx| { - let body = cx.new(|_| DetachedView); - OverlayHost::new(body, window, cx) + let body = cx.new(|_| Body); + gpui_base::Root::new(body, window, cx) }); cx.simulate_resize(size(px(393.), px(852.))); crate::window_seam::occlude_for_test( @@ -402,7 +349,7 @@ mod tests { assert!(bounds.size.width <= px(361.)); assert!(bounds.size.height >= px(44.)); cx.update(|window, cx| window.push_notification("Second", cx)); - root.read_with(cx, |root, cx| { + overlays(&root, cx).read_with(cx, |root, cx| { root.notifications.read(cx).assert_messages(cx, &["Second"]); }); cx.simulate_resize(size(px(1200.), px(800.))); @@ -424,8 +371,8 @@ mod tests { cx.update(crate::theme::init); cx.update(|cx| crate::window_seam::override_mobile_for_test(cx, true)); let (root, cx) = cx.add_window_view(|window, cx| { - let body = cx.new(|_| DetachedView); - OverlayHost::new(body, window, cx) + let body = cx.new(|_| Body); + gpui_base::Root::new(body, window, cx) }); cx.simulate_resize(size(px(393.), px(852.))); cx.update(|window, cx| window.push_notification("Copied", cx)); @@ -460,7 +407,7 @@ mod tests { window.push_notification(Notification::error("Repair required").autohide(false), cx) }); draw(cx); - root.read_with(cx, |root, cx| { + overlays(&root, cx).read_with(cx, |root, cx| { assert_eq!(root.dialogs.len(), 1); root.notifications.read(cx).assert_messages(cx, &[]); }); @@ -473,7 +420,7 @@ mod tests { assert!(cx.debug_bounds("wide-toast").is_some()); cx.simulate_resize(size(px(393.), px(852.))); draw(cx); - root.read_with(cx, |root, cx| { + overlays(&root, cx).read_with(cx, |root, cx| { assert_eq!(root.dialogs.len(), 1); root.notifications.read(cx).assert_messages(cx, &[]); }); diff --git a/crates/ui/src/project_icon.rs b/crates/ui/src/project_icon.rs index 6bcd69f8..5c6ea7cf 100644 --- a/crates/ui/src/project_icon.rs +++ b/crates/ui/src/project_icon.rs @@ -586,7 +586,7 @@ mod tests { }); let (_, cx) = cx.add_window_view(|window, cx| { let view = cx.new(|_| EmptyView); - crate::overlay::OverlayHost::new(view, window, cx) + gpui_base::Root::new(view, window, cx) }); cx.simulate_resize(size(px(390.), px(700.))); cx.update(|window, cx| open(store.clone(), project, window, cx)); diff --git a/crates/ui/src/run.rs b/crates/ui/src/run.rs index 4eca4fad..15e6bfc7 100644 --- a/crates/ui/src/run.rs +++ b/crates/ui/src/run.rs @@ -10,10 +10,11 @@ use std::borrow::Cow; use std::cell::RefCell; use std::rc::Rc; -use gpui::{App, AppContext as _, Entity, KeyBinding, SharedString, WindowHandle, WindowOptions}; +use gpui::{ + AnyWindowHandle, App, AppContext as _, Entity, KeyBinding, SharedString, WindowOptions, +}; use tcode_client::host::ClientHost; -use crate::overlay::OverlayHost; use crate::remote::{AttachmentTarget, ClientAttachment}; use crate::shell::{AppShell, ShellSetup, TogglePalette}; use crate::theme; @@ -106,7 +107,7 @@ pub fn run_shell( cx: &mut App, host: Rc, options: ShellOptions, -) -> (WindowHandle, Entity) { +) -> (AnyWindowHandle, Entity) { // Browser bootstrap supplies its Fetch client; native image URLs need an HTTP client too. #[cfg(not(target_family = "wasm"))] { @@ -164,7 +165,7 @@ pub fn run_shell( let captured = mounted.clone(); #[cfg(target_os = "macos")] let window_background = options.window.window_background; - let window = cx + let window: AnyWindowHandle = cx .open_window(options.window, move |window, cx| { window.set_window_title(&title); theme::sync_system_appearance(Some(window), cx); @@ -178,9 +179,10 @@ pub fn run_shell( ) }); *captured.borrow_mut() = Some(shell.clone()); - cx.new(|cx| OverlayHost::new(shell, window, cx)) + cx.new(|cx| gpui_base::Root::new(shell, window, cx)) }) - .expect("failed to open the tcode window"); + .expect("failed to open the tcode window") + .into(); // A transparent macOS window gets its blur from a stock semantic material // rather than GPUI's `Blurred` path; see `macos_backdrop`. #[cfg(target_os = "macos")] @@ -195,7 +197,7 @@ pub fn run_shell( .borrow_mut() .take() .expect("the shell is built while the window opens"); - crate::shell::set_back_target(window.into(), &shell, cx); + crate::shell::set_back_target(window, &shell, cx); if let Some(wakes) = options.lifecycle.as_deref().map(lifecycle_wakes) { let shell = shell.downgrade(); cx.spawn(async move |cx| { diff --git a/crates/ui/src/settings_page.rs b/crates/ui/src/settings_page.rs index e9f1993a..a63530d3 100644 --- a/crates/ui/src/settings_page.rs +++ b/crates/ui/src/settings_page.rs @@ -3071,7 +3071,7 @@ mod tests { let (_, cx) = cx.add_window_view(|window, cx| { let page = cx.new(|cx| SettingsPage::new(store.clone(), window_state, window, cx)); page_handle = Some(page.clone()); - crate::overlay::OverlayHost::new(page, window, cx) + gpui_base::Root::new(page, window, cx) }); cx.update(|window, cx| { page_handle diff --git a/crates/ui/src/shortcut.rs b/crates/ui/src/shortcut.rs index 70e46cc3..4c3c4428 100644 --- a/crates/ui/src/shortcut.rs +++ b/crates/ui/src/shortcut.rs @@ -22,6 +22,23 @@ pub(crate) fn init(cx: &mut App) { cx.bind_keys([ KeyBinding::new("ctrl-tab", NavigateThread::Next, None), KeyBinding::new("ctrl-shift-tab", NavigateThread::Previous, None), + KeyBinding::new( + "shift-tab", + crate::composer::ToggleInteractionMode, + Some(crate::composer::CONTEXT), + ), + ]); + // Bindings dispatch before key-down listeners, and the terminal forwards + // raw keystrokes (Tab, Ctrl-C as an interrupt) from its listener, so the + // window root's focus traversal and selection copy must not claim them. + let terminal = Some(crate::terminal_drawer::CONTEXT); + cx.bind_keys([ + KeyBinding::new("tab", NoAction, terminal), + KeyBinding::new("shift-tab", NoAction, terminal), + #[cfg(target_os = "macos")] + KeyBinding::new("cmd-c", NoAction, terminal), + #[cfg(not(target_os = "macos"))] + KeyBinding::new("ctrl-c", NoAction, terminal), ]); } @@ -35,3 +52,87 @@ pub(crate) fn format_secondary_shortcut(key: &str) -> String { key_char: None, }) } + +#[cfg(test)] +mod tests { + use std::{cell::RefCell, rc::Rc}; + + use gpui::{ + AppContext as _, Context, Entity, FocusHandle, InteractiveElement as _, IntoElement, + KeyDownEvent, ParentElement as _, Render, TestAppContext, VisualTestContext, Window, div, + }; + + struct Harness { + terminal: FocusHandle, + composer: FocusHandle, + keys: Vec, + toggles: usize, + } + + impl Render for Harness { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .child( + div() + .key_context(crate::terminal_drawer::CONTEXT) + .track_focus(&self.terminal) + .on_key_down(cx.listener(|this, event: &KeyDownEvent, _, _| { + this.keys.push(event.keystroke.unparse()); + })), + ) + .child( + div() + .key_context(crate::composer::CONTEXT) + .on_action(cx.listener( + |this, _: &crate::composer::ToggleInteractionMode, _, _| { + this.toggles += 1; + }, + )) + .child(div().track_focus(&self.composer)), + ) + } + } + + fn mount(cx: &mut TestAppContext) -> (Entity, &mut VisualTestContext) { + cx.update(|cx| { + crate::theme::init(cx); + super::init(cx); + }); + let built = Rc::new(RefCell::new(None)); + let capture = built.clone(); + let (_, cx) = cx.add_window_view(move |window, cx| { + let harness = cx.new(|cx| Harness { + terminal: cx.focus_handle().tab_stop(true), + composer: cx.focus_handle().tab_stop(true), + keys: Vec::new(), + toggles: 0, + }); + *capture.borrow_mut() = Some(harness.clone()); + gpui_base::Root::new(harness, window, cx) + }); + let harness = built.borrow_mut().take().unwrap(); + cx.update(|window, cx| window.draw(cx).clear(cx)); + (harness, cx) + } + + /// The window root binds Tab to focus traversal, and bindings dispatch + /// before key-down listeners: the terminal still receives Tab as input, + /// and Shift-Tab in the composer switches the interaction mode. + #[gpui::test] + fn root_focus_traversal_leaves_terminal_and_composer_their_keys(cx: &mut TestAppContext) { + let (harness, cx) = mount(cx); + let terminal = harness.read_with(cx, |harness, _| harness.terminal.clone()); + cx.update(|window, cx| terminal.focus(window, cx)); + cx.simulate_keystrokes("tab shift-tab"); + harness.read_with(cx, |harness, _| { + assert_eq!(harness.keys, ["tab", "shift-tab"]); + }); + assert!(cx.update(|window, _| terminal.is_focused(window))); + + let composer = harness.read_with(cx, |harness, _| harness.composer.clone()); + cx.update(|window, cx| composer.focus(window, cx)); + cx.simulate_keystrokes("shift-tab"); + harness.read_with(cx, |harness, _| assert_eq!(harness.toggles, 1)); + assert!(cx.update(|window, _| composer.is_focused(window))); + } +} diff --git a/crates/ui/src/store/images.rs b/crates/ui/src/store/images.rs index 2b9e91cf..f96a118d 100644 --- a/crates/ui/src/store/images.rs +++ b/crates/ui/src/store/images.rs @@ -321,7 +321,7 @@ mod tests { cwd: cwd.clone(), }); message = Some(view.clone()); - crate::overlay::OverlayHost::new(view, window, cx) + gpui_base::Root::new(view, window, cx) }); let view = message.unwrap(); for inline in [false, true] { diff --git a/crates/ui/src/terminal_drawer.rs b/crates/ui/src/terminal_drawer.rs index 37025645..9b7a4494 100644 --- a/crates/ui/src/terminal_drawer.rs +++ b/crates/ui/src/terminal_drawer.rs @@ -187,6 +187,8 @@ impl SelectionDrag { } } +pub(crate) const CONTEXT: &str = "Terminal"; + #[derive(Action, Clone, PartialEq, Eq, serde::Deserialize)] #[action(namespace = tcode_terminal, no_json)] struct TerminalCopy(u64); @@ -1725,6 +1727,7 @@ impl Render for TerminalDrawer { crate::tr!("terminal.content"), cx, ) + .key_context(CONTEXT) .track_focus(&self.focus_handle) .on_key_down(cx.listener(Self::on_key_down)) // The accessibility focus ring is painted as a shadow behind this diff --git a/crates/ui/src/theme.rs b/crates/ui/src/theme.rs index f6b8b957..71ca2295 100644 --- a/crates/ui/src/theme.rs +++ b/crates/ui/src/theme.rs @@ -2,8 +2,8 @@ use std::{collections::HashMap, sync::Arc, sync::LazyLock}; use gpui::{App, Global, Hsla, Pixels, Rgba, SharedString, Window, WindowAppearance, px}; use gpui_base::{ - ColorTokens, RadiusTokens, ResizableTheme, ScrollbarMode, ScrollbarStyles, ScrollbarTheme, - SemanticThemeTokens, ThemeAppearance, TypographyTokens, + ColorTokens, PlotTheme, RadiusTokens, ResizableTheme, ScrollbarMode, ScrollbarStyles, + ScrollbarTheme, SemanticThemeTokens, ThemeAppearance, TypographyTokens, }; use serde::Deserialize; @@ -305,6 +305,7 @@ pub fn init(cx: &mut App) { /// the platform window is opaque. pub fn init_with_json(theme_json: &str, cx: &mut App) { gpui_base::init(cx); + crate::overlay::init(cx); crate::widgets::menu::init(cx); let themes = parse_theme_file(theme_json).expect("embedded themes/tcode.json must be valid"); @@ -344,6 +345,7 @@ pub fn change_mode(mode: ThemeMode, window: Option<&mut Window>, cx: &mut App) { handle: Some(theme.border), active_handle: Some(theme.ring), }, + plot: PlotTheme::default(), }; cx.set_global(base_theme); cx.set_global(theme); diff --git a/crates/ui/src/touch_selection/window_overlay.rs b/crates/ui/src/touch_selection/window_overlay.rs index cbf66952..d96715bf 100644 --- a/crates/ui/src/touch_selection/window_overlay.rs +++ b/crates/ui/src/touch_selection/window_overlay.rs @@ -10,7 +10,7 @@ use super::{EditMenuItem, SelectAllTouched, TouchSelectionOverlay}; /// Draws the edit menu of the window text selection — the one a /// [`crate::markdown::MarkdownView`] takes part in. -/// [`crate::overlay::OverlayHost`] mounts one per window, after the content, +/// [`crate::overlay`] mounts one per window, after the content, /// so the menu floats above whatever was selected. /// /// Read-only text offers Copy and Select All. @@ -84,7 +84,6 @@ mod tests { use crate::{ markdown::{MarkdownState, MarkdownView}, - overlay::OverlayHost, widgets::input::{Textarea, TextareaState}, }; @@ -169,7 +168,7 @@ mod tests { markdown, textarea: state, }); - OverlayHost::new(page, window, cx) + gpui_base::Root::new(page, window, cx) } }); let textarea = textarea.get().unwrap().clone(); @@ -236,7 +235,7 @@ mod tests { let state = cx.new(|cx| MarkdownState::new(&text, cx)); markdown.set(state.clone()).ok().unwrap(); let body = cx.new(|_| Body { markdown: state }); - OverlayHost::new(body, window, cx) + gpui_base::Root::new(body, window, cx) } }); let markdown = markdown.get().unwrap().clone(); diff --git a/crates/ui/src/widgets/menu.rs b/crates/ui/src/widgets/menu.rs index 3efcd0fc..31a7b42a 100644 --- a/crates/ui/src/widgets/menu.rs +++ b/crates/ui/src/widgets/menu.rs @@ -312,13 +312,16 @@ where } menu_state.update(cx, |state, _| state.menu = Some(menu.clone())); menu.focus_handle(cx).focus(window, cx); - let popover = cx.entity(); + // Weak: `menu_state` owns the menu this subscription lives as long + // as, so strong captures would keep both alive after the trigger + // unmounts, with the popover's deferred registration. + let popover = cx.entity().downgrade(); window .subscribe(&menu, cx, { - let menu_state = menu_state.clone(); + let menu_state = menu_state.downgrade(); move |_, _: &DismissEvent, window, cx| { - popover.update(cx, |state, cx| state.dismiss(window, cx)); - menu_state.update(cx, |state, _| state.menu = None); + let _ = popover.update(cx, |state, cx| state.dismiss(window, cx)); + let _ = menu_state.update(cx, |state, _| state.menu = None); } }) .detach(); @@ -407,10 +410,13 @@ impl Ren let previous_focus = window.focused(cx); let menu_focus = menu.focus_handle(cx); let deferred = gpui_base::GlobalState::register_deferred_popover(cx); + // Weak: the subscription is stored on `state`, so a strong + // capture would keep it, and its deferred registration, + // alive after the trigger unmounts. let subscription = window.subscribe(&menu, cx, { - let state = state.clone(); + let state = state.downgrade(); move |_, _: &DismissEvent, window, cx| { - state.update(cx, |state, _| { + let _ = state.update(cx, |state, _| { state.menu = None; state._subscription = None; state._deferred = None; diff --git a/crates/ui/src/widgets/popover.rs b/crates/ui/src/widgets/popover.rs index 964249d7..812caaff 100644 --- a/crates/ui/src/widgets/popover.rs +++ b/crates/ui/src/widgets/popover.rs @@ -101,8 +101,8 @@ impl Popover { T: gpui_base::Selectable + IntoElement + 'static, { self.trigger = Some(Box::new(move |open, _, _| { - let selected = trigger.is_selected(); - trigger.selected(selected || open).into_any_element() + let is_open = trigger.is_open(); + trigger.open(is_open || open).into_any_element() })); self } diff --git a/crates/web/Cargo.toml b/crates/web/Cargo.toml index a8ad512b..50735f9f 100644 --- a/crates/web/Cargo.toml +++ b/crates/web/Cargo.toml @@ -12,11 +12,11 @@ default = [] debug-exports = [] [target.'cfg(target_family = "wasm")'.dependencies] -gpui = { package = "gpui-pre", version = "=0.3.6", default-features = false } +gpui = { package = "gpui-pre", version = "=0.3.7", default-features = false } # The browser constructs its own platform (gpui::Application::with_platform) # rather than going through gpui-pre-platform, whose wasm arm enables # gpui-pre-web's nightly-only "multithreaded" default. -gpui_web = { package = "gpui-pre-web", version = "=0.3.6", default-features = false } +gpui_web = { package = "gpui-pre-web", version = "=0.3.7", default-features = false } console_error_panic_hook = "0.1" tcode-protocol = { path = "../protocol", default-features = false } tcode-client = { path = "../client" }