diff --git a/Cargo.lock b/Cargo.lock index c821b9942..97591af46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -846,7 +846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" dependencies = [ "once_cell", - "proc-macro-crate 3.5.0", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 2.0.117", @@ -1197,7 +1197,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "gpui_util", "indexmap", @@ -1753,7 +1753,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "proc-macro2", "quote", @@ -2900,7 +2900,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "accesskit", "anyhow", @@ -3141,14 +3141,20 @@ name = "gpui-wry" version = "0.5.0" dependencies = [ "anyhow", + "block2 0.6.2", "gpui", "lb-wry", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "webview2-com", + "windows 0.61.3", + "windows-core 0.61.2", ] [[package]] name = "gpui_linux" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "accesskit", "accesskit_unix", @@ -3170,6 +3176,7 @@ dependencies = [ "itertools 0.14.0", "libc", "log", + "notify-rust", "oo7", "open", "parking_lot", @@ -3199,13 +3206,14 @@ dependencies = [ [[package]] name = "gpui_macos" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "accesskit", "accesskit_macos", "anyhow", "async-task", "block", + "block2 0.6.2", "cbindgen", "cocoa 0.26.0", "collections", @@ -3233,6 +3241,7 @@ dependencies = [ "objc2 0.6.4", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", + "objc2-user-notifications", "parking_lot", "pathfinder_geometry", "raw-window-handle", @@ -3246,7 +3255,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3257,7 +3266,7 @@ dependencies = [ [[package]] name = "gpui_platform" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "console_error_panic_hook", "gpui", @@ -3270,7 +3279,7 @@ dependencies = [ [[package]] name = "gpui_shared_string" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "schemars", "serde", @@ -3280,7 +3289,7 @@ dependencies = [ [[package]] name = "gpui_util" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "anyhow", "log", @@ -3290,7 +3299,7 @@ dependencies = [ [[package]] name = "gpui_web" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "anyhow", "console_error_panic_hook", @@ -3302,7 +3311,6 @@ dependencies = [ "log", "parking_lot", "raw-window-handle", - "smallvec", "uuid", "wasm-bindgen", "wasm-bindgen-futures", @@ -3314,7 +3322,7 @@ dependencies = [ [[package]] name = "gpui_wgpu" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "anyhow", "bytemuck", @@ -3332,6 +3340,7 @@ dependencies = [ "raw-window-handle", "smallvec", "swash", + "unicode-bidi", "unicode-segmentation", "wasm-bindgen", "wasm-bindgen-futures", @@ -3343,7 +3352,7 @@ dependencies = [ [[package]] name = "gpui_windows" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "accesskit", "accesskit_windows", @@ -3659,7 +3668,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "anyhow", "async-compression", @@ -3679,7 +3688,7 @@ dependencies = [ [[package]] name = "http_client_tls" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "rustls", "rustls-platform-verifier", @@ -4294,8 +4303,7 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "lb-wry" version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9cfe72bff8acf9af0d6d276569be5b9cb3f313f9882761ada5a50d3044214d4" +source = "git+https://github.com/longbridge/wry?branch=gpui-composition-controller#2d0b3e26f152ab82ca0bae5a7c365a68ca4349fe" dependencies = [ "base64", "block2 0.6.2", @@ -4574,6 +4582,20 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "mac-notification-sys" +version = "0.6.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd604973958ddcc11b561193c0fb96ba146506ef2f231ef2e7c35fd2cbc9beca" +dependencies = [ + "cc", + "log", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "time", + "uuid", +] + [[package]] name = "mach" version = "0.3.2" @@ -4700,7 +4722,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "anyhow", "bindgen", @@ -4950,6 +4972,20 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "notify-rust" +version = "4.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b4c1b4f2aa9f25f63a7a49d3dd0ed567b3670da15330a66b29434be899b891" +dependencies = [ + "futures-lite", + "log", + "mac-notification-sys", + "serde", + "tauri-winrt-notification", + "zbus", +] + [[package]] name = "notify-types" version = "1.0.1" @@ -5281,6 +5317,16 @@ dependencies = [ "objc2-foundation 0.3.2", ] +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-core-text" version = "0.3.2" @@ -5429,6 +5475,19 @@ dependencies = [ "objc2-foundation 0.3.2", ] +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "bitflags 2.11.1", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-location", + "objc2-foundation 0.3.2", +] + [[package]] name = "objc2-web-kit" version = "0.3.2" @@ -5513,9 +5572,9 @@ dependencies = [ [[package]] name = "open" -version = "5.3.4" +version = "5.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3bab717c29a857abf75fcef718d441ec7cb2725f937343c734740a985d37fd" +checksum = "43bb73a7fa3799b198970490a51174027ba0d4ec504b03cd08caf513d40024bc" dependencies = [ "is-wsl", "libc", @@ -5536,9 +5595,9 @@ checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" [[package]] name = "ordered-float" -version = "5.3.0" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" dependencies = [ "num-traits", ] @@ -5669,7 +5728,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "collections", "serde", @@ -5941,9 +6000,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.33" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" [[package]] name = "plain" @@ -6111,11 +6170,11 @@ dependencies = [ [[package]] name = "proc-macro-crate" -version = "3.5.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" dependencies = [ - "toml_edit 0.25.11+spec-1.1.0", + "toml_edit 0.23.10+spec-1.0.0", ] [[package]] @@ -6667,7 +6726,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "derive_refineable", ] @@ -6719,7 +6778,7 @@ checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" [[package]] name = "reqwest_client" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "anyhow", "bytes", @@ -7156,7 +7215,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "async-task", "backtrace", @@ -7777,7 +7836,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "heapless", "log", @@ -8042,9 +8101,9 @@ dependencies = [ [[package]] name = "taffy" -version = "0.12.1" +version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73afc801dd6bd47529eaa7c7e90557f107527d1b7c9c7ed7d7803c7b8d0c357f" +checksum = "340a09581f29809fc0df82a3955501dc7f2a21f887e5d1c13dbe288fe1c0bef4" dependencies = [ "arrayvec", "grid", @@ -8087,6 +8146,17 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "tauri-winrt-notification" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed071c670382e85fc2f48ae706492d8c338f4f89bf72520d32f8abfe880aade" +dependencies = [ + "thiserror 2.0.18", + "windows 0.61.3", + "windows-version", +] + [[package]] name = "tempfile" version = "3.27.0" @@ -8391,15 +8461,6 @@ dependencies = [ "serde_core", ] -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - [[package]] name = "toml_edit" version = "0.19.15" @@ -8438,14 +8499,14 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.11+spec-1.1.0" +version = "0.23.10+spec-1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +checksum = "84c8b9f757e028cee9fa244aea147aab2a9ec09d5325a9b01e0a49730c2b5269" dependencies = [ "indexmap", - "toml_datetime 1.1.1+spec-1.1.0", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", - "winnow 1.0.2", + "winnow 0.7.15", ] [[package]] @@ -9247,7 +9308,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "perf", "quote", @@ -9510,8 +9571,7 @@ dependencies = [ [[package]] name = "wasm_thread" version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7516db7f32decdadb1c3b8deb1b7d78b9df7606c5cc2f6241737c2ab3a0258e" +source = "git+https://github.com/zed-industries/wasm_thread?rev=0cf96c7708dfb97ccf3da50347e25edcf75d6937#0cf96c7708dfb97ccf3da50347e25edcf75d6937" dependencies = [ "futures", "js-sys", @@ -10993,7 +11053,7 @@ version = "5.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897e79616e84aac4b2c46e9132a4f63b93105d54fe8c0e8f6bffc21fa8d49222" dependencies = [ - "proc-macro-crate 3.5.0", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 2.0.117", @@ -11247,7 +11307,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "anyhow", "chrono", @@ -11264,7 +11324,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" dependencies = [ "tracing", "tracing-subscriber", @@ -11275,7 +11335,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/zed-industries/zed#1a246efd7e1b83ab568ec5e3e6c1a43a42e1abba" +source = "git+https://github.com/huacnlee/zed?branch=gpui-webview-overlay#20a699acac7b5bceea8e8fe6ba257a61ad47fb09" [[package]] name = "zune-core" @@ -11337,7 +11397,7 @@ version = "5.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" dependencies = [ - "proc-macro-crate 3.5.0", + "proc-macro-crate 3.4.0", "proc-macro2", "quote", "syn 2.0.117", diff --git a/Cargo.toml b/Cargo.toml index 8eadcc879..19f2dddce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,11 +33,11 @@ gpui-component-macros = { path = "crates/macros", version = "0.5.1" } gpui-component-assets = { path = "crates/assets", version = "0.5.1" } story = { path = "crates/story" } -gpui = { git = "https://github.com/zed-industries/zed" } -gpui_platform = { git = "https://github.com/zed-industries/zed", features = ["font-kit", "x11", "wayland", "runtime_shaders"] } -gpui_web = { git = "https://github.com/zed-industries/zed" } -gpui_macros = { git = "https://github.com/zed-industries/zed" } -reqwest_client = { git = "https://github.com/zed-industries/zed" } +gpui = { git = "https://github.com/huacnlee/zed", branch = "gpui-webview-overlay" } +gpui_platform = { git = "https://github.com/huacnlee/zed", branch = "gpui-webview-overlay", features = ["font-kit", "x11", "wayland", "runtime_shaders"] } +gpui_web = { git = "https://github.com/huacnlee/zed", branch = "gpui-webview-overlay" } +gpui_macros = { git = "https://github.com/huacnlee/zed", branch = "gpui-webview-overlay" } +reqwest_client = { git = "https://github.com/huacnlee/zed", branch = "gpui-webview-overlay" } sum-tree = { version = "0.2.0", package = "zed-sum-tree" } # reqwest = { version = "0.12.15-zed", package = "zed-reqwest" } reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [ diff --git a/crates/ui/src/root.rs b/crates/ui/src/root.rs index 7d531b883..0d1d1a2db 100644 --- a/crates/ui/src/root.rs +++ b/crates/ui/src/root.rs @@ -14,7 +14,7 @@ use gpui::{ Anchor, AnyView, App, AppContext, Bounds, ClipboardItem, Context, DefiniteLength, ElementId, Entity, EntityId, FocusHandle, Hitbox, InteractiveElement, IntoElement, KeyBinding, ParentElement as _, Pixels, Render, StyleRefinement, Styled, WeakEntity, WeakFocusHandle, - Window, actions, div, prelude::FluentBuilder as _, + Window, actions, deferred, div, prelude::FluentBuilder as _, }; use std::{any::TypeId, collections::HashMap, rc::Rc}; @@ -159,6 +159,15 @@ impl Root { cx: &mut App, ) -> Option> { let root = window.root::()??; + if root + .read(cx) + .notification + .read(cx) + .notifications() + .is_empty() + { + return None; + } let active_sheet_placement = root.read(cx).active_sheet.clone().map(|d| d.placement); @@ -173,7 +182,7 @@ impl Root { let placement = cx.theme().notification.placement; - Some( + Some(deferred( div() .absolute() .when(matches!(placement, Anchor::TopRight), |this| { @@ -199,7 +208,7 @@ impl Root { .when_some(mb, |this, offset| this.mb(offset)) .when_some(ml, |this, offset| this.ml(offset)) .child(root.read(cx).notification.clone()), - ) + )) } /// Render the Sheet layer. @@ -217,12 +226,9 @@ impl Root { let size = sheet.size; - return Some( - div() - .relative() - .child(sheet) - .on_prepaint(move |_, _, cx| root.update(cx, |r, _| r.sheet_size = Some(size))), - ); + return Some(deferred(div().relative().child(sheet).on_prepaint( + move |_, _, cx| root.update(cx, |r, _| r.sheet_size = Some(size)), + ))); } None @@ -273,7 +279,7 @@ impl Root { } } - Some(div().children(dialogs)) + Some(deferred(div().children(dialogs))) } pub fn open_dialog(&mut self, build: F, window: &mut Window, cx: &mut Context<'_, Root>) diff --git a/crates/webview/Cargo.toml b/crates/webview/Cargo.toml index 684325a5f..b8e350e5c 100644 --- a/crates/webview/Cargo.toml +++ b/crates/webview/Cargo.toml @@ -20,4 +20,17 @@ doctest = false [dependencies] anyhow.workspace = true gpui.workspace = true -wry = { version = "0.53.3", package = "lb-wry" } +wry = { git = "https://github.com/longbridge/wry", branch = "gpui-composition-controller", package = "lb-wry" } + +[target.'cfg(target_os = "macos")'.dependencies] +block2 = "0.6" +objc2 = "0.6" +objc2-app-kit = { version = "0.3", features = ["block2", "NSEvent", "NSView", "NSWindow"] } + +[target.'cfg(target_os = "windows")'.dependencies] +webview2-com = "0.38" +windows-core = "0.61" + +[target.'cfg(target_os = "windows")'.dependencies.windows] +version = "0.61" +features = ["Win32_Foundation"] diff --git a/crates/webview/README.md b/crates/webview/README.md index be0c95089..d71c1d1d2 100644 --- a/crates/webview/README.md +++ b/crates/webview/README.md @@ -1,13 +1,28 @@ # Wry for GPUI -A webview supports for GPUI, based on [Wry](https://github.com/tauri-apps/wry). - -This still a experimental with limited features, please file issues for any bugs or missing features. - -- The WebView will render on top of the GPUI window, any GPUI elements behind the WebView bounds will be covered. -- Only supports macOS and Windows currently. - -So, we recommend using the webview in a separate window or in a Popup layer. +A WebView implementation for GPUI, based on [Wry](https://github.com/tauri-apps/wry). + +This is still experimental with limited features. Please file issues for bugs or missing features. + +- On macOS, GPUI Popovers, Dialogs, Notifications, and Popup Menus can render + above the WebView when using the layered-scene GPUI branch configured by + this repository. +- The WebView owns native keyboard focus after it is clicked. `gpui-wry` + clears GPUI's logical focus at that boundary so an Input does not continue + showing a stale blinking caret. +- On macOS, applications must provide standard Edit-menu actions for Cut, + Copy, Paste, and Select All. AppKit routes their keyboard shortcuts through + the responder chain to either the focused WebView or GPUI. +- This overlay work targets macOS and Windows. Windows support uses WebView2 + composition hosting and is still experimental. +- Linux overlay support is intentionally deferred. wry uses different X11 and + Wayland integration paths and does not expose a surface that can be inserted + directly into GPUI's existing composition tree. Matching macOS and Windows + would require substantial integration with GPUI's Linux GTK/GDK window + backend. + +See [`WEBVIEW_OVERLAY_RESEARCH.md`](WEBVIEW_OVERLAY_RESEARCH.md) for the +composition, focus, input, and platform details. ## Run Example diff --git a/crates/webview/WEBVIEW_OVERLAY_RESEARCH.md b/crates/webview/WEBVIEW_OVERLAY_RESEARCH.md new file mode 100644 index 000000000..2841e3153 --- /dev/null +++ b/crates/webview/WEBVIEW_OVERLAY_RESEARCH.md @@ -0,0 +1,805 @@ +# Rendering GPUI Elements Above Native WebViews: Platform Composition Research + +> Research date: 2026-07-30 +> Baseline: `gpui-wry` uses `lb-wry 0.53.3`; GPUI is based on Zed commit +> `huacnlee/zed:gpui-webview-overlay` at `ad82de04a2`. This document records the +> native composition research, the implemented macOS layered-scene spike, and +> the Windows architecture. + +## Platform Scope + +This implementation targets **macOS and Windows**. Linux overlay support is +intentionally deferred and is not a completion requirement for this work. +wry's Linux hosting differs between X11 child windows and Wayland GTK widgets; +neither route exposes a compositable native surface that can be inserted +between GPUI's base and overlay scenes. Supporting the same behavior would +require substantial integration with GPUI's GTK/GDK window backend rather than +a local WebView adapter change. + +The Linux analysis below is retained to explain that boundary and prevent an +unsupported child-window or snapshot fallback from being mistaken for feature +parity. + +## Conclusion + +Adding a `z-index` to an existing wry WebView cannot solve this problem across +platforms. GPUI elements are composed into one GPU surface, while wry creates a +platform-native child view or window by default. Paint order inside GPUI cannot +cross the native window hierarchy. + +The recommended end state is a GPUI **native surface slot**: + +1. After prepaint, GPUI submits the native surface bounds, clip, visibility, + and stacking information every frame. +2. The platform backend inserts the WebView into that platform's compositable + visual or view tree. +3. GPUI continues to draw its overlays, but the platform backend guarantees + that their GPU visual is above the WebView visual. Input is routed to the + overlay or WebView according to GPUI hit testing. +4. Validate the design on macOS first. Windows must use WebView2 composition + hosting. Linux is outside the current implementation scope. + +GPUI already has a better natural split point than an arbitrary `z-index`. +`Window::draw_roots` paints the root, calls `paint_deferred_draws`, and finally +paints the window prompt, active drag, or core tooltip +([GPUI `Window::draw_roots`](https://github.com/zed-industries/zed/blob/66d95fb1945b7a7be671427f11ebfb42c339bdb4/crates/gpui/src/window.rs#L2839-L2923)). +The preferred model is therefore: + +```text +base scene paint + | +native portal (WebView visual) + | +deferred overlay scene + | +prompt / active drag / core tooltip scene +``` + +This follows the existing GPUI paint pipeline more closely than hard-coding +component names as overlays. + +| Platform | Default wry representation | Reordering siblings directly | Recommendation | +| --- | --- | --- | --- | +| macOS | `WKWebView` as an `NSView` subview | **Promising; validate first** | Put WKWebView and the GPUI Metal views in an explicit sibling view/layer hierarchy; validate transparent overlays, input, and resize | +| Windows | WebView2 controller hosted in a separate `WS_CHILD` HWND | **Unreliable; not a final design** | Fork or extend wry to use WebView2 Composition Controller and attach the WebView visual to GPUI's DirectComposition tree | +| Linux | X11 child window or GTK `WebKitWebView` widget | **Deferred; not supported by this overlay work** | Correct support requires GPUI's Linux window backend to join the GTK/GDK hierarchy; child-window and snapshot fallbacks do not provide feature parity | + +## Why the Current Implementation Always Covers GPUI + +`WebViewElement::paint` does not draw the WebView; it only registers a hitbox. +wry's native object displays the page, while `prepaint` only calls +`set_bounds`. Later GPUI paint ordering for Popover, Dialog, or Notification +can only reorder pixels inside the same GPUI scene. It cannot reorder an +external `NSView`, child `HWND`, or GTK widget. + +wry defines `build_as_child` in native platform terms: a child window on +Windows, an `NSView` under the content view on macOS, and an X11-only child +window on Linux. Wayland requires a GTK container. These are explicit wry API +semantics, not a GPUI-specific effect +([official wry `WebViewBuilder::build_as_child` API](https://docs.rs/wry/0.53.3/wry/struct.WebViewBuilder.html#method.build_as_child), +[official wry child WebView example](https://github.com/tauri-apps/wry/tree/v0.53.3#child-webviews)). + +The pinned wry Windows source creates `WRY_WEBVIEW` with +`WS_CHILD | WS_CLIPCHILDREN`, calls `SetWindowPos(..., HWND_TOP, ...)`, and +then creates an ordinary `ICoreWebView2Controller`. It does not use a +composition controller +([wry 0.53.3 child HWND creation](https://github.com/tauri-apps/wry/blob/v0.53.3/src/webview2/mod.rs#L310-L376), +[wry 0.53.3 ordinary WebView2 controller creation](https://github.com/tauri-apps/wry/blob/v0.53.3/src/webview2/mod.rs#L545-L594)). +This directly explains why the WebView remains in front of the GPUI scene on +Windows. + +## What `Application::run_embedded` Solves + +Zed PR [#60574](https://github.com/zed-industries/zed/pull/60574) added +`Application::run_embedded` and `ApplicationHandle`. They invert ownership of +the event loop and GPUI application: when an external host owns the run loop +and `Platform::run` returns immediately after its launch callback, +`ApplicationHandle` keeps the application alive and lets the host re-enter +GPUI through `update()` and `to_async()`. The PR did not add an embeddable +`NSView`, `HWND`, `CAMetalLayer`, GPU texture, or scene-stacking API. + +Its first consumer, `embedded_gpui`, lets guest UI participate like ordinary +GPUI elements because the guest outputs a retained display list that the host +replays as **the host's own GPUI primitives**. It does not insert a guest +native window or surface into the GPUI scene +([`embedded_gpui` README](https://github.com/zed-industries/embedded_gpui#readme)). +WKWebView, WebView2, and WebKitGTK do not expose a browser display list that an +application can replay every frame, so this mechanism cannot be applied +directly to a wry WebView. + +It can still be reused indirectly: + +- If the macOS spike uses an external native shell that owns the run loop and + embeds a GPUI overlay renderer in a selected `NSView` or `CAMetalLayer`, + `run_embedded` can manage application lifetime. +- If GPUI later gains a truly embedded `PlatformWindow`, it can be the entry + point through which the host drives GPUI. +- It does not create the `base GPUI < WebView < overlay GPUI` layers or handle + scene separation, alpha, hit testing, focus, IME, or WebView input + forwarding. + +This problem still requires a native surface slot and overlay plane. +`run_embedded` is a reusable startup and lifetime building block, not the +composition solution. + +## macOS: One NSView / CALayer Tree + +### Primary-source facts + +- AppKit's `addSubview(_:positioned:relativeTo:)` inserts a view immediately + above or below a sibling. A `nil` relative view places it above or below all + siblings + ([Apple `NSView.addSubview` ordering API](https://developer.apple.com/documentation/appkit/nsview/addsubview%28_%3Apositioned%3Arelativeto%3A%29)). +- Apple's view hierarchy guide says that a view which must draw in front of + another should be a subview or descendant of the rear view, and that the + positioned API controls ordering + ([Apple, Working with the View Hierarchy](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/CocoaViewsGuide/WorkingWithAViewHierarchy/WorkingWithAViewHierarchy.html)). +- A layer-backed view caches drawing in its `CALayer`; enabling `wantsLayer` + on a parent makes the subtree layer-backed. Apple also warns against adding + ordinary subviews to a layer-hosting view, which must be managed entirely + through Core Animation + ([Apple `NSView.wantsLayer`](https://developer.apple.com/documentation/appkit/nsview/wantslayer)). +- wry 0.53.3 inserts `WKWebView` into its parent with `addSubview` and exposes + no relative sibling-order policy + ([wry WKWebView insertion source](https://github.com/tauri-apps/wry/blob/v0.53.3/src/wkwebview/mod.rs#L615-L674)). +- GPUI's macOS backend creates `GPUIView`, explicitly enables `wantsLayer`, + and adds it to `NSWindow.contentView` + ([GPUI macOS window source](https://github.com/zed-industries/zed/blob/66d95fb1945b7a7be671427f11ebfb42c339bdb4/crates/gpui_macos/src/window.rs#L880-L984)). + Its renderer uses `CAMetalLayer` + ([GPUI Metal renderer source](https://github.com/zed-industries/zed/blob/66d95fb1945b7a7be671427f11ebfb42c339bdb4/crates/gpui_macos/src/metal_renderer.rs#L151-L177)). + +### Viable design + +The smallest prototype is not a new transparent GPUI window. It explicitly +rebuilds sibling order below the same content view: + +```text +NSWindow.contentView ++-- GPUI container NSView + +-- GPUI base CAMetalLayer / view + +-- WKWebView NSView + +-- GPUI overlay CAMetalLayer / view (transparent) +``` + +GPUI currently has one complete scene and one `CAMetalLayer`. Putting that +entire view above WKWebView also puts the GPUI background above the page. +There are two options: + +1. **Two GPUI surfaces:** render base and overlay scenes separately, with the + WebView between them. +2. **Transparent hole punching:** keep one GPUI surface above the WebView, + clear base pixels inside WebView bounds to transparent, and retain only + overlay pixels in that region. + +The second option touches less code but must validate a non-opaque +`CAMetalLayer`, clear alpha, window background, and text antialiasing together. +The current renderer derives `CAMetalLayer.opaque` from the window's +`transparent` option, so an ordinary opaque window cannot be assumed to +support local transparency. The first option has clearer, more durable +boundaries, but requires routing the GPUI scene by stacking plane. + +### macOS prototype acceptance criteria + +- Popovers, dialogs, and notifications cross WebView bounds with correct + shadows and translucency. +- The WebView outside an overlay still receives mouse, scroll, keyboard, and + drag/drop input. +- GPUI hit testing intercepts input inside the overlay and immediately returns + it to the WebView after dismissal. +- Retina scale, live resize, fullscreen, multiple WebViews, WebView focus, and + IME all work. +- Native bounds and clip remain consistent with the GPUI content mask when the + WebView enters or leaves scrolling and clipping containers. + +## Windows: Join the DirectComposition Visual Tree + +### Why HWND z-order is insufficient + +An ordinary wry WebView is a child HWND. Moving it behind the GPUI HWND has two +structural problems: + +1. All GPUI pixels remain in one surface, which cannot represent + `GPUI base < WebView < GPUI overlay`. +2. A child HWND is a rectangular native window. HWND z-order cannot express + GPUI clipping, rounded corners, translucency, or per-element hit testing. + +`SetWindowPos(HWND_BOTTOM)`, temporarily hiding the WebView, or creating a +transparent top-level window for each popover are only workarounds. The last +also creates cross-window focus, IME, movement, DPI, taskbar, and accessibility +consistency problems. + +### Primary-source facts + +- Microsoft provides `CreateCoreWebView2CompositionControllerAsync` for visual + hosting. The host must set `RootVisualTarget` and forward mouse/pointer input + to WebView + ([Microsoft CreateCoreWebView2CompositionControllerAsync](https://learn.microsoft.com/en-us/dotnet/api/microsoft.web.webview2.core.corewebview2environment.createcorewebview2compositioncontrollerasync)). +- `RootVisualTarget` may be an `IDCompositionVisual` or + `Windows::UI::Composition::ContainerVisual`. WebView attaches its visual + tree there; the application controls position, commit, and input + ([Microsoft `ICoreWebView2CompositionController`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2compositioncontroller)). +- Microsoft's API overview also states that a WebView2 composition tree can + attach to `IDCompositionVisual`, `IDCompositionTarget`, or `ContainerVisual` + ([Microsoft WebView2 API overview: Rendering using Composition](https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/overview-features-apis#rendering-webview2-using-composition)). +- GPUI's Windows renderer already creates an `IDCompositionDevice`, + `IDCompositionTarget`, and root `IDCompositionVisual`, then sets the GPUI + swap chain as the visual content + ([GPUI `DirectComposition` source](https://github.com/zed-industries/zed/blob/66d95fb1945b7a7be671427f11ebfb42c339bdb4/crates/gpui_windows/src/directx_renderer.rs#L898-L917)). +- GPUI's composition swap chain uses `DXGI_ALPHA_MODE_PREMULTIPLIED` + ([GPUI composition swap-chain source](https://github.com/zed-industries/zed/blob/66d95fb1945b7a7be671427f11ebfb42c339bdb4/crates/gpui_windows/src/directx_renderer.rs#L1180-L1203)). + This provides the alpha mode needed for transparent visual composition, but + does not prove that the current scene produces correct hole or overlay + alpha. Rendering must still be split and tested. + +### Recommended visual tree + +```text +IDCompositionTarget (GPUI HWND) ++-- root + +-- GPUI base visual (swap chain) + +-- WebView slot visual + | +-- WebView2 composition tree + +-- GPUI overlay visual (transparent swap chain) +``` + +This requires changing wry or implementing a Windows-specific WebView2 host. +wry 0.53.3 does not expose a composition controller. Beyond pixels, the host +must implement `SendMouseInput` / `SendPointerInput`, cursor, drag/drop, focus, +IME, accessibility provider, and DPI coordinate conversion. Visual output +without input is not a complete implementation. + +### Windows implementation note + +#### `lb-wry 0.53.3` capability boundary + +Inspection of the pinned source gives a precise result: **it exposes an +ordinary controller to callers, but does not create, store, or expose a +Composition Controller**. + +- `WebViewBuilderExtWindows` offers additional browser arguments, data + directory, browser accelerator keys, theme, HTTPS scheme, environment, and + incognito options, but no composition-hosting option + ([wry 0.53.3 Windows builder extension](https://github.com/tauri-apps/wry/blob/v0.53.3/src/lib.rs#L1668-L1810)). +- `WebViewExtWindows::controller()` returns `ICoreWebView2Controller` + ([wry 0.53.3 Windows WebView extension](https://github.com/tauri-apps/wry/blob/v0.53.3/src/lib.rs#L2224-L2263)). + An already-created ordinary controller cannot be upgraded to a composition + controller. The environment must create one through + `CreateCoreWebView2CompositionController[WithOptions]`. +- The internal `WebView` stores `ICoreWebView2Controller`, but its construction + path only calls `CreateCoreWebView2Controller` or + `CreateCoreWebView2ControllerWithOptions` + ([wry 0.53.3 controller construction](https://github.com/tauri-apps/wry/blob/v0.53.3/src/webview2/mod.rs#L549-L643)). +- Its drag/drop helper registers `IDropTarget` around wry's child HWND. It is + not composition-controller drag forwarding + ([wry 0.53.3 drag/drop controller](https://github.com/tauri-apps/wry/blob/v0.53.3/src/webview2/drag_drop.rs#L39-L97)). + +It is therefore impossible to obtain `webview.controller()` outside +`gpui-wry` and add a `RootVisualTarget` afterward. The smallest correct change +must occur before wry constructs the controller. Maintain a narrow wry fork +rather than copying all initialization, protocol, navigation, IPC, and +lifecycle logic into `crates/webview`. + +#### Composition Controller host responsibilities + +Microsoft defines the following composition-hosting contract: + +- `RootVisualTarget` accepts `IDCompositionVisual`, `IDCompositionTarget`, or + `Windows::UI::Composition::ContainerVisual` + ([WebView2 composition overview](https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/overview-features-apis#connecting-to-the-visual-tree)). + Since GPUI already uses DirectComposition, pass a slot + `IDCompositionVisual` created by **the same GPUI `IDCompositionDevice`**. + Do not create an independent target tree for the same HWND. +- After `put_RootVisualTarget` connects the WebView tree, the host must still + set the ordinary controller's `Bounds` and commit visual-tree changes on its + device + ([Microsoft `ICoreWebView2CompositionController`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2compositioncontroller)). +- Mouse, touch, and pen first reach the parent HWND. The host converts them to + WebView client coordinates and calls `SendMouseInput` or + `SendPointerInput`. WebView reports cursor changes through `CursorChanged`; + the host updates `WM_SETCURSOR` or the parent HWND. It must also send mouse + leave events or cursor state becomes incorrect + ([Microsoft composition input contract](https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/overview-features-apis#forwarding-input), + [Microsoft cursor contract](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2compositioncontroller)). +- External drops do not enter WebView automatically. The host registers + `IDropTarget`, forwards `DragEnter`, `DragOver`, `DragLeave`, and `Drop` to + `ICoreWebView2CompositionController3`, and converts points to WebView client + coordinates + ([Microsoft `ICoreWebView2CompositionController3`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2compositioncontroller3)). +- WebView remains a child of the parent HWND in the accessibility tree by + default. Correct spatial and hierarchical integration with GPUI AccessKit + may use the `IRawElementProviderSimple` provider returned by + `ICoreWebView2CompositionController2::get_AutomationProvider` + ([Microsoft `ICoreWebView2CompositionController2`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2compositioncontroller2), + [WebView2 composition accessibility overview](https://learn.microsoft.com/en-us/microsoft-edge/webview2/concepts/overview-features-apis#accessibility)). +- Focus, visibility, bounds, and keyboard behavior use the ordinary + `ICoreWebView2Controller` that the composition controller also implements: + `MoveFocus`, Got/LostFocus, MoveFocusRequested, AcceleratorKeyPressed, and + related APIs. The host must call `NotifyParentWindowPositionChanged` when + the WebView parent or ancestor HWND moves; Microsoft says this is required + for accessibility and WebView dialogs + ([Microsoft `ICoreWebView2Controller`](https://learn.microsoft.com/en-us/microsoft-edge/webview2/reference/win32/icorewebview2controller)). +- Microsoft does not require forwarding `WM_IME_*` through a + composition-specific API, because the interface has no IME forwarding + method. Keyboard and IME still depend on the controller's internal HWND and + focus. **GPUI must not continue consuming or rewriting these messages while + WebView has focus.** Runtime validation with CJK input methods is required; + successful COM compilation alone is not proof. + +The last two points mean the input boundary is not simply "when GPUI hit +testing misses, call SendMouseInput." Pointer input can be forwarded +explicitly, but keyboard and IME require real controller focus. When Tab +leaves the WebView, the host must handle `MoveFocusRequested` and restore GPUI +focus. + +#### Concrete changes to GPUI Windows DirectComposition + +`DirectComposition` currently contains: + +```text +IDCompositionTarget ++-- comp_visual(content = GPUI swap chain) +``` + +Its constructor creates one target and one visual, and `set_swap_chain` makes +that visual the root +([current GPUI DirectComposition wrapper](https://github.com/zed-industries/zed/blob/66d95fb1945b7a7be671427f11ebfb42c339bdb4/crates/gpui_windows/src/directx_renderer.rs#L898-L917)). +It must become: + +```text +IDCompositionTarget ++-- root_visual + +-- base_visual(content = base GPUI swap chain) + +-- portal_container + | +-- webview_slot_visual(s) + +-- overlay_visual(content = transparent overlay GPUI swap chain) +``` + +Concrete change points: + +1. Add `root_visual`, `base_visual`, `portal_container`, and `overlay_visual` + to `DirectComposition`. Call `comp_target.SetRoot` only during construction + and device recovery; `set_swap_chain` must no longer replace the root. +2. Add an overlay swap chain, texture, and RTV to `DirectXResources`. The + existing composition swap chain already uses + `DXGI_ALPHA_MODE_PREMULTIPLIED`, which the overlay can reuse + ([GPUI composition swap chain](https://github.com/zed-industries/zed/blob/66d95fb1945b7a7be671427f11ebfb42c339bdb4/crates/gpui_windows/src/directx_renderer.rs#L1180-L1203)). +3. Render the base scene and + `paint_deferred_draws + prompt/drag/tooltip` scene to separate RTVs. The + overlay clear must be `[0, 0, 0, 0]`, not `[1, 1, 1, 1]` as used by the + current opaque-window path + ([current GPUI `draw` clear selection](https://github.com/zed-industries/zed/blob/66d95fb1945b7a7be671427f11ebfb42c339bdb4/crates/gpui_windows/src/directx_renderer.rs#L301-L355)). +4. In one frame, portal update sets slot visual offset, clip, visibility, and + order, then commits them with GPUI visual changes. `RootVisualTarget` + attaches the tree; WebView `Bounds` still controls raster size. +5. Device-loss recovery currently drops and rebuilds all DirectComposition + state and the swap chain + ([GPUI renderer recovery path](https://github.com/zed-industries/zed/blob/66d95fb1945b7a7be671427f11ebfb42c339bdb4/crates/gpui_windows/src/directx_renderer.rs#L232-L300)). + A portal handle must carry a generation. After recovery, the old slot visual + is invalid and the WebView host must rebind `RootVisualTarget`; it cannot + retain a raw COM pointer forever. +6. `GPUI_DISABLE_DIRECT_COMPOSITION` falls back to an HWND swap chain. This + mode has no visual slot and must report that capability as unavailable. It + must not silently fall back to the old child WebView while claiming overlay + support. + +`crates/webview` must not call `CreateTargetForHwnd` again for the GPUI HWND. +Microsoft permits at most one composition target per layer for an HWND, and +`topmost` decides whether the visual tree is above or below child windows +([Microsoft `CreateTargetForHwnd`](https://learn.microsoft.com/en-us/windows/desktop/api/dcomp/nf-dcomp-idcompositiondevice-createtargetforhwnd)). +GPUI already owns the topmost target; extend its root tree. + +#### Transparent child HWND overlay versus Composition Controller + +Keeping the wry child HWND and creating a transparent GPUI child or owned HWND +above it is suitable for a short spike, not a product architecture: + +| Dimension | Transparent overlay HWND | Composition Controller | +| --- | --- | --- | +| Visual order | Depends on two HWNDs' z-order; the GPUI scene still needs splitting or duplication | Natively expresses base / WebView / overlay visual order | +| Clip, corners, animation | HWND region, layered window, and GPUI clip need duplicate synchronization | Visual offset, clip, and opacity commit in one transaction | +| Input | An extra WndProc must decide hit testing and pass-through | Pointer forwarding is an official WebView2 contract; still complex, but bounded | +| Focus, IME, accessibility | Cross-HWND focus, IME, and UIA spatial relationships are fragile | The controller retains official focus and UIA APIs | +| Resize and DPI | Two native windows require tear-free synchronization | Visual tree and swap chains commit together | +| Multiple WebViews and nested overlays | HWND count and z-order state grow rapidly | A portal container manages multiple slot visuals | + +Windows 8 and later permit child `WS_EX_LAYERED` windows +([Microsoft extended window styles](https://learn.microsoft.com/en-us/windows/win32/winmsg/extended-window-styles)), +but this proves only that the API can create one, not that it satisfies GPUI +per-element hit testing, focus, and atomic composition. +`WS_EX_TRANSPARENT` is not general input pass-through; Microsoft limits its +classic use to same-thread child hierarchies, while top-level use also requires +a layered style +([Microsoft DWM best practices](https://learn.microsoft.com/en-us/windows/win32/dwm/bestpractices-ovw)). + +**Decision: use Composition Controller for the final implementation.** +A transparent overlay HWND is allowed only for early visual validation or an +"open a separate popup window" fallback when DirectComposition is disabled. +It must not implement the claim that arbitrary GPUI elements can cover a +WebView in the same window. + +#### Minimum compilable API and module boundaries + +Keep WebView2 initialization in a narrow wry fork and visual-tree ownership in +GPUI: + +```rust +// gpui: cross-platform, no COM types +pub struct NativeSurfacePortal { /* opaque id + generation */ } + +impl Window { + pub fn create_native_surface_portal( + &mut self, + cx: &mut App, + ) -> Result; +} + +impl NativeSurfacePortal { + pub fn capabilities(&self) -> NativeSurfaceCapabilities; + pub fn set_geometry(&self, bounds: Bounds, clip: Bounds); + pub fn set_visible(&self, visible: bool); +} + +// gpui_windows: Windows-only extension trait exposing the COM attachment point +pub trait NativeSurfacePortalExtWindows { + fn parent_hwnd(&self) -> HWND; + fn root_visual_target(&self) -> IUnknown; + fn generation(&self) -> u64; +} + +// wry fork: Windows builder extension +pub trait WebViewBuilderExtWindowsComposition { + fn build_as_composition_child( + self, + parent: HWND, + root_visual_target: IUnknown, + ) -> Result; +} + +pub trait WebViewExtWindowsComposition { + fn composition_controller( + &self, + ) -> Option; +} +``` + +Responsibilities: + +- `gpui` / `gpui_windows`: visual creation and ordering, geometry and clip, + overlay swap chain, commit, and device-loss generation. +- wry fork: environment and controller creation, existing WebView handlers and + lifecycle, `RootVisualTarget` binding, and composition-controller exposure. +- `crates/webview/src/platform/windows.rs`: adapt GPUI mouse, pointer, focus, + and drag events to the composition controller; observe cursor and focus; and + rebind when the portal generation changes. +- `WebViewElement`: submit only declarative bounds, clip, visibility, and a + GPUI hitbox; never manipulate the DComp tree directly. + +`root_visual_target()` exposes a COM object rather than +`IDCompositionDevice`, preventing wry from modifying the full GPUI tree. The +portal handle guarantees the slot visual's lifetime. + +#### Phased Windows implementation checklist + +1. **Compilation gate:** add a composition construction path to the wry fork + while reusing all existing WebView initialization. Type checks prove the + ordinary path is unchanged and the composition path returns + `ICoreWebView2CompositionController`. +2. **Visual-only gate:** GPUI creates the `base / slot / overlay` tree. Use a + solid-color test visual instead of WebView to prove order, clip, resize, + DPI, and device recovery. +3. **WebView picture gate:** bind WebView `RootVisualTarget` to the slot. + Validate multiple WebViews, move/resize/visibility, scrolling clip, + transparent overlay, and absence of one-frame z-order flashes. +4. **Mouse/cursor gate:** cover move, down, up, double-click, wheel, + horizontal wheel, and leave; coordinate conversion, capture, and + CursorChanged. Do not forward when overlay hit testing succeeds. +5. **Touch/pen/drag gate:** implement `SendPointerInput` and + `ICoreWebView2CompositionController3` drag/drop. Validate internal WebView + dragging and drag-in from other applications. +6. **Focus/IME gate:** validate click-to-focus, Tab/Shift-Tab, + `MoveFocusRequested`, accelerators, CJK IME composition and candidate + windows, and WebView dialogs. Call `NotifyParentWindowPositionChanged` when + the window moves. +7. **Accessibility gate:** attach the WebView automation provider at the + correct GPUI/AccessKit parent and screen bounds; validate Narrator order. +8. **Recovery/capability gate:** recreate and rebind slots after GPU device + loss. Return explicit unsupported status when DComp is disabled and use a + separate WebView window or popup fallback. + +The first three stages prove only that WebView can display with an overlay. +Interactive, accessible, recoverable product support requires stages 4-8 too. + +## Linux (Deferred): GTK/WebKitGTK and Native Child-Window Constraints + +### Primary-source facts + +- wry explicitly states that `build_as_child` supports only X11 on Linux. + Wayland should use `WebViewBuilderExtUnix::new_gtk` with `gtk::Fixed` + ([wry build_as_child platform notes](https://docs.rs/wry/0.53.3/wry/struct.WebViewBuilder.html#method.build_as_child)). +- `GtkFixed` positions child widgets in pixels and performs no automatic + layout + ([GTK 3 `GtkFixed`](https://docs.gtk.org/gtk3/class.Fixed.html)). +- `GtkOverlay` places overlay widgets above its main child, and an overlay + child's index determines draw order when children overlap + ([GTK 3 `GtkOverlay`](https://docs.gtk.org/gtk3/class.Overlay.html), + [GTK 3 `reorder_overlay`](https://docs.gtk.org/gtk3/method.Overlay.reorder_overlay.html)). +- Normal GTK 3 drawing propagates from the toplevel through the widget + hierarchy in back-to-front order. The documentation also recognizes that a + toplevel may contain multiple native subwindows. Only content in the same + GTK/GDK hierarchy is naturally governed by GTK overlay ordering + ([GTK 3 Drawing Model](https://docs.gtk.org/gtk3/drawing-model.html)). +- WebKitGTK's public snapshot API asynchronously captures a visible region or + full document and returns a static snapshot. It does not expose WebKit's + live GPU surface to GPUI + ([WebKitGTK `WebView.get_snapshot`](https://webkitgtk.org/reference/webkit2gtk/stable/method.WebView.get_snapshot.html)). +- GTK explicitly limits `OffscreenWindow` to snapshots of widgets outside a + normal widget hierarchy. It is itself a toplevel and cannot be embedded in + another toplevel + ([GTK 3 `OffscreenWindow` index entry](https://docs.gtk.org/gtk3/index.html#classes)). + +### Feasibility + +On X11, treating the WebKitGTK/X11 child window and GPUI surface as two +rectangular native windows and changing stacking still cannot express +arbitrary overlays inside GPUI. wry explicitly does not support this child +window route on Wayland. + +In theory, WebKitWebView can be the main child of `GtkOverlay`, with a GTK +widget capable of presenting the GPUI overlay surface above it. That requires +the GPUI Linux window backend itself to integrate with GTK/GDK surface +lifetime and coordinate renderer surfaces, event loops, input, and Wayland +subsurfaces. It is not a local `crates/webview` change. + +The snapshot route can draw a WebView image as a GPUI texture and obtain +arbitrary GPUI z-order. The official API is asynchronous, however, with no +evidence that it supports real-time scrolling, video, animation, or +low-latency input. It is only a preview or frozen-state fallback, not the final +interactive WebView implementation. + +## Recommended GPUI Core Abstraction + +Do not expose platform details as a vague `z_index(i32)`. Introduce a limited, +testable stacking plane: + +```rust +enum NativeSurfacePlane { + BelowGpui, + BetweenBaseAndOverlay, + AboveGpui, +} +``` + +`BetweenBaseAndOverlay` is the main path. GPUI also needs to: + +- Collect window-space bounds, content mask or clip, and visibility for each + native slot every frame. +- Define the native portal insertion point between the end of + `root_element.paint` and the start of `paint_deferred_draws`. `Deferred` + already promises to paint after its ancestors, with higher priority closer + to the viewer + ([GPUI `Deferred` source](https://github.com/zed-industries/zed/blob/66d95fb1945b7a7be671427f11ebfb42c339bdb4/crates/gpui/src/elements/deferred.rs#L7-L96)). +- Continue collecting the window prompt, active drag, and core tooltip after + the deferred overlay scene; the current pipeline paints them later. +- Avoid submitting an additional surface when the overlay plane is empty. +- Hit-test before native input forwarding, so a transparent overlay view does + not consume all WebView input. +- Create, update, and destroy native slots on the platform UI thread in sync + with the GPUI frame lifecycle. + +The platform trait should describe capabilities instead of pretending all +platforms are identical: + +```rust +struct NativeSurfaceCapabilities { + can_embed_between_gpui_planes: bool, + can_clip_non_rectangular: bool, + requires_forwarded_pointer_input: bool, +} +``` + +Linux can then report missing capabilities instead of silently degrading to +"WebView is always on top." + +### Existing overlay coverage audit + +The current split directly covers: + +- `Popover`, `ContextMenu`, `PopupMenu`, and fallback native menu. +- Popups for `Select`, `Combobox`, and `DatePicker`. +- Input completion, code-action, and hover popups. +- `gpui-component::Tooltip` and plot tooltip. + +These implementations use `gpui::deferred`; examples include +[`Popover`](https://github.com/longbridge/gpui-component/blob/be3c8413766cafc736a0c1c80306ff0f293e04f3/crates/ui/src/popover.rs#L335-L350), +[`PopupMenu`](https://github.com/longbridge/gpui-component/blob/be3c8413766cafc736a0c1c80306ff0f293e04f3/crates/ui/src/menu/popup_menu.rs#L1330-L1345), +and +[`Tooltip`](https://github.com/longbridge/gpui-component/blob/be3c8413766cafc736a0c1c80306ff0f293e04f3/crates/ui/src/tooltip.rs#L489-L515). + +Not every visually overlay-like component was originally in the deferred +plane: + +- The GPUI window prompt, active drag, and core tooltip are not in + `deferred_draws`, but `draw_roots` paints them separately afterward. The + final phase must also target the top surface. +- `Root::render_dialog_layer`, `render_sheet_layer`, and + `render_notification_layer` return ordinary absolute or relative elements + and do not wrap themselves in `deferred` + ([Root layer source](https://github.com/longbridge/gpui-component/blob/be3c8413766cafc736a0c1c80306ff0f293e04f3/crates/ui/src/root.rs#L157-L277)). + If a caller adds them as ordinary later root children, they paint after the + WebView element but remain in the base scene and cannot cross the native + portal automatically. +- Dialog, Sheet, and Notification therefore need explicit migration to + deferred or a more direct GPUI `OverlayPlane` element. Migration must test + content masks, modal hitboxes, focus traps, animation, and nested popovers. + Splitting the renderer without migrating these Root layers is incomplete. + +The current spike wraps all three Root layers in `deferred`. This is required +to validate the architecture, but sheet animation, notification placement, +nested popups, and modal focus traps still need dedicated regression testing +before upstreaming. + +`draw_roots` calculates `mouse_hit_test` only after prepainting all deferred +elements, prompts, drags, and tooltips. This is useful for native input routing: +run the existing GPUI hit test first and forward an unhandled event to the +composition WebView. Platform events still reach the GPUI window first, so +Windows must explicitly convert unconsumed events to `SendMouseInput` or +`SendPointerInput`. + +## Implementation Order and Decision Gates + +1. **macOS spike:** prove + `GPUI base / WKWebView / GPUI overlay` pixels and input in one window. Hole + punching may validate the API if stable, but still compare it with two + surfaces. +2. **GPUI scene separation:** use + `root paint / native portal / deferred paint / prompt-drag-tooltip paint` + as the existing skeleton. Promote the overlay plane from a platform hack + into a renderer/window abstraction, move Dialog, Sheet, and Notification + into the top plane, and validate alpha, resize, and hit testing with a + non-WebView test visual. +3. **Windows composition host:** reuse GPUI's `IDCompositionDevice` and root + tree, create a WebView2 composition controller, and complete pointer, + drag/drop, IME, and accessibility support. + +Linux is not part of this implementation order. A future, separately scoped +feasibility project would need to test a GTK-hosted WebKitWebView with a GPUI +overlay widget on X11 and Wayland independently; snapshots would not qualify +as live WebView support. + +Re-evaluate the unified native-embedding route if any of these occur: + +- macOS cannot produce stable local transparent GPUI overlays in an ordinary + opaque window. +- The Windows WebView2 composition visual cannot safely share the lifecycle of + GPUI's device and target. +- Wayland requires a complete GPUI event/window-backend rewrite with + unacceptable maintenance cost. + +Even if Linux cannot reach parity, macOS and Windows can share native-slot and +overlay-plane semantics while reporting different platform capabilities. + +## 2026-07-30 macOS Spike Results + +The current worktree implements and has run the first phase: + +- A GPUI frame records a scene-operation boundary after the root and inspector + but before deferred and window overlays. +- `PlatformWindow::draw_layered` preserves single-surface behavior by default. + The macOS backend replays operations on either side of the boundary into + base and transparent-overlay Metal renderers. +- Both renderers share one Metal device and sprite atlas so atlas tile IDs + remain valid in the second renderer. +- After WKWebView joins the view hierarchy, the macOS window creates a + transparent sibling overlay `NSView/CAMetalLayer`, producing + `base CAMetalLayer / WKWebView / overlay CAMetalLayer`. +- When the overlay has no drawable primitives, `hitTest:` returns `nil` and + WebView receives input. With Popover, Dialog, or similar content, input goes + to GPUI so outside clicks can dismiss the overlay. +- Dialog, Sheet, and Notification have moved into the deferred/top plane. +- The WebView example uses an 800x600 window and provides both Popover and + Dialog scenarios. + +Runtime validation covered: + +1. A Popover renders completely above a real `WKWebView`. +2. Clicking the WebView area outside the Popover closes it without a crash. +3. Dialog, translucent backdrop, text, and buttons render above WebView. +4. The Dialog close hitbox works, and WebView continues working afterward. +5. Notification renders completely above a running WebView page. +6. PopupMenu background, labels, separators, and link items render completely + above WebView. +7. WebView remains clickable while the overlay is empty. +8. Resize and drawable-size updates reach both renderers. + +### Focus ownership is part of the portal contract + +A native WebView and GPUI maintain separate focus systems. Giving native +keyboard focus to the WebView does not automatically clear GPUI's focused +element. Without an explicit handoff, a GPUI `Input` can keep drawing its +blinking caret after the user clicks the WebView, even though subsequent +typing belongs to the WebView. The result presents two apparent input targets +and makes keyboard ownership ambiguous. + +When GPUI receives a mouse-down inside WebView bounds, the WebView element +treats it as a focus boundary and clears GPUI window focus. This covers the +top-plane path where the event dismisses a Popover over the WebView region. +When the top plane is empty, AppKit sends the event directly to `WKWebView`. +`gpui-wry` therefore installs a lifecycle-scoped local `NSEvent` monitor for +mouse-down events. The monitor checks the event window, native-hit-tests the +content view, and clears this GPUI window's logical focus only when the hit +view is the managed `WKWebView` or one of its descendants. The monitor token +is removed when the `WebView` is dropped. + +Keeping this handoff in `gpui-wry` is deliberate. It avoids adding a +WebView-specific focus event, AppKit responder override, or native-descendant +classification to GPUI. The GPUI platform patch stays limited to scene +composition, while the component that owns the embedded native view also owns +its focus boundary. Returning from WebView to GPUI uses the normal GPUI focus +path. + +This is not example-only polish. Any native-surface-slot API must define: + +- which focus system owns keyboard and IME input at every point; +- how pointer, Tab, Shift-Tab, programmatic focus, and overlay dismissal + transfer that ownership; +- that the losing system immediately removes visible focus indicators, + including carets and focus rings; +- that only the owner receives text, composition, and accessibility focus + events. + +Regression validation must first focus the address `Input`, confirm its caret +is visible, and then click the WebView both with no overlay and while a Popover +is open. Both paths stop the GPUI caret as WebView receives focus. Runtime +validation confirmed that the address Input loses its focus indication after +a direct WebView click and that clicking the WebView region dismisses an open +Popover. The reverse transition must restore exactly one GPUI focus target. + +On macOS, `Cmd+C`, `Cmd+V`, `Cmd+X`, and `Cmd+A` are AppKit key equivalents +routed through standard Edit-menu selectors and the responder chain. A bare +GPUI example without those menu items can display and focus WKWebView while +copy and paste shortcuts still do nothing. The example therefore installs an +Edit menu whose `MenuItem::os_action` entries map to Cut, Copy, Paste, and +Select All. When WKWebView owns native focus, AppKit routes those selectors to +WebKit; when GPUI owns focus, the same items dispatch the corresponding GPUI +input actions. Native-surface examples and host applications must preserve +this responder-chain integration rather than implementing clipboard shortcuts +as WebView-specific JavaScript. Runtime validation copied a sentinel from a +GPUI Input into a WebView text field and another sentinel from the WebView back +into the GPUI Input using only `Cmd+C` and `Cmd+V`. + +The Notification layer creates a deferred element only when the list is +non-empty. This avoids a permanent empty top-plane node and unnecessary scene +operations. After opening a Dialog or Notification, the WebView example +explicitly invalidates itself because the example entity currently composes +these Root layers while the state lives in the `Root` entity. + +The spike found and fixed three non-obvious issues: + +- An independent renderer with an independent atlas fails because replayed + tile IDs do not exist in the second atlas. +- Re-locking window state inside Objective-C `hitTest:` causes a re-entrant + deadlock. +- Passing the overlay `NSView` itself to GPUI input processing violates + `NSTextInputContext` assumptions and causes a null-pointer abort. Events must + borrow the original GPUI native view's input context. + +To keep a transparent overlay from permanently intercepting input, +`Scene::is_empty` checks actual drawable primitives rather than paint-operation +count. Empty `StartLayer/EndLayer` operations still increase `Scene::len()`. +Unit tests cover empty layers, drawable primitives, and replay. + +### What the spike has not proved + +- The top plane is currently a window-level switch and one scene split point, + not a complete native-surface-slot API. Multiple WebViews in one window and + interleaved ordering between them are not expressible. +- If the overlay has any drawable primitive, the full transparent NSView takes + input. This supports modal and outside-click behavior, but the production + API should carry top-plane hit regions and explicit `capture`, + `dismiss-then-consume`, and `pass-through` policies. +- macOS still needs regression testing for IME, drag/drop, accessibility, + fullscreen, scale-factor changes, multiple windows, WebView focus, and live + resize. +- Windows CompositionController runtime behavior is not yet fully validated. + Linux GTK/WebKitGTK support is intentionally deferred and is not used to + judge macOS/Windows completion. diff --git a/crates/webview/src/lib.rs b/crates/webview/src/lib.rs index f747c3c0e..4183107f6 100644 --- a/crates/webview/src/lib.rs +++ b/crates/webview/src/lib.rs @@ -1,14 +1,32 @@ use std::{ops::Deref, rc::Rc}; +#[cfg(target_os = "macos")] +use block2::RcBlock; +#[cfg(target_os = "windows")] +use std::cell::Cell; use wry::{ Rect, dpi::{self, LogicalSize}, }; +#[cfg(target_os = "macos")] +use objc2::{rc::Retained, runtime::AnyObject}; +#[cfg(target_os = "macos")] +use objc2_app_kit::{NSEvent, NSEventMask}; +#[cfg(target_os = "windows")] +use webview2_com::Microsoft::Web::WebView2::Win32::*; +#[cfg(target_os = "windows")] +use windows::Win32::Foundation::POINT; + +use gpui::{ + App, Bounds, ContentMask, DismissEvent, DispatchPhase, Element, ElementId, Entity, + EventEmitter, FocusHandle, Focusable, GlobalElementId, Hitbox, InteractiveElement, IntoElement, + LayoutId, MouseDownEvent, ParentElement as _, Pixels, Render, Size, Style, Styled as _, Window, + canvas, div, +}; +#[cfg(target_os = "windows")] use gpui::{ - App, Bounds, ContentMask, DismissEvent, Element, ElementId, Entity, EventEmitter, FocusHandle, - Focusable, GlobalElementId, Hitbox, InteractiveElement, IntoElement, LayoutId, MouseDownEvent, - ParentElement as _, Pixels, Render, Size, Style, Styled as _, Window, canvas, div, + MouseButton, MouseExitEvent, MouseMoveEvent, MouseUpEvent, ScrollDelta, ScrollWheelEvent, }; /// A webview based on wry WebView. @@ -19,37 +37,121 @@ pub struct WebView { webview: Rc, visible: bool, bounds: Bounds, + #[cfg(target_os = "macos")] + event_monitor: Option>, + #[cfg(target_os = "windows")] + native_surface: Option>, } impl Drop for WebView { fn drop(&mut self) { + #[cfg(target_os = "macos")] + if let Some(event_monitor) = self.event_monitor.take() { + // SAFETY: The token was returned by addLocalMonitor and is removed + // exactly once while the application is still running. + unsafe { NSEvent::removeMonitor(&event_monitor) }; + } self.hide(); } } impl WebView { + /// Builds a child WebView using the platform's native-surface integration. + pub fn build_as_child( + builder: wry::WebViewBuilder<'_>, + parent: &impl wry::raw_window_handle::HasWindowHandle, + window: &Window, + cx: &mut App, + ) -> anyhow::Result { + #[cfg(target_os = "windows")] + { + use wry::WebViewBuilderExtWindows as _; + + let native_surface = window.create_native_surface()?; + let platform_handle = native_surface.platform_handle(); + let root_visual = platform_handle + .downcast::() + .map_err(|_| anyhow::anyhow!("GPUI returned an invalid Windows portal handle"))?; + let webview = builder + .with_composition_root_visual(*root_visual) + .build_as_child(parent)?; + return Ok(Self::new_with_native_surface( + webview, + native_surface, + window, + cx, + )); + } + + #[cfg(not(target_os = "windows"))] + { + Ok(Self::new(builder.build_as_child(parent)?, window, cx)) + } + } + /// Create a new WebView from a wry WebView. - pub fn new(webview: wry::WebView, _: &mut Window, cx: &mut App) -> Self { + pub fn new(webview: wry::WebView, _window: &Window, cx: &mut App) -> Self { let _ = webview.set_bounds(Rect::default()); + #[cfg(target_os = "macos")] + _window + .enable_scene_overlay() + .expect("macOS WebView requires GPUI layered scene support"); + + #[cfg(target_os = "macos")] + let event_monitor = install_focus_monitor(&webview, _window, cx); + + Self { + focus_handle: cx.focus_handle(), + visible: true, + bounds: Bounds::default(), + webview: Rc::new(webview), + #[cfg(target_os = "macos")] + event_monitor, + #[cfg(target_os = "windows")] + native_surface: None, + } + } + + #[cfg(target_os = "windows")] + fn new_with_native_surface( + webview: wry::WebView, + native_surface: Rc, + _window: &Window, + cx: &mut App, + ) -> Self { Self { focus_handle: cx.focus_handle(), visible: true, bounds: Bounds::default(), webview: Rc::new(webview), + native_surface: Some(native_surface), } } /// Show the webview. pub fn show(&mut self) { let _ = self.webview.set_visible(true); + #[cfg(target_os = "windows")] + if let Some(native_surface) = &self.native_surface { + let _ = native_surface.set_visible(true); + } self.visible = true; } /// Hide the webview. pub fn hide(&mut self) { - _ = self.webview.focus_parent(); + #[cfg(target_os = "windows")] + focus_parent(&self.webview); + #[cfg(not(target_os = "windows"))] + { + _ = self.webview.focus_parent(); + } _ = self.webview.set_visible(false); + #[cfg(target_os = "windows")] + if let Some(native_surface) = &self.native_surface { + let _ = native_surface.set_visible(false); + } self.visible = false; } @@ -79,6 +181,55 @@ impl WebView { } } +#[cfg(target_os = "macos")] +fn install_focus_monitor( + webview: &wry::WebView, + window: &Window, + cx: &App, +) -> Option> { + use wry::WebViewExtMacOS as _; + + let native_webview = webview.webview(); + let native_window = webview.ns_window(); + let async_window = window.to_async(cx); + let foreground_executor = cx.foreground_executor().clone(); + let handler = RcBlock::new(move |event: std::ptr::NonNull| { + let event = unsafe { event.as_ref() }; + let clicked_webview = event + .window(objc2::MainThreadMarker::new().expect("NSEvent runs on the main thread")) + .filter(|event_window| std::ptr::eq(&**event_window, &*native_window)) + .and_then(|event_window| event_window.contentView()) + .and_then(|content_view| { + let point = content_view.convertPoint_fromView(event.locationInWindow(), None); + content_view.hitTest(point) + }) + .is_some_and(|hit_view| { + std::ptr::eq(&*hit_view, &***native_webview) + || hit_view.isDescendantOf(&native_webview) + }); + + if clicked_webview { + let mut async_window = async_window.clone(); + foreground_executor + .spawn(async move { + let _ = async_window.update(|window, _| window.blur()); + }) + .detach(); + } + + event as *const NSEvent as *mut NSEvent + }); + + // SAFETY: The block returns the same live NSEvent it receives. The retained + // monitor token is stored on WebView and removed in Drop. + unsafe { + NSEvent::addLocalMonitorForEventsMatchingMask_handler( + NSEventMask::LeftMouseDown | NSEventMask::RightMouseDown | NSEventMask::OtherMouseDown, + &handler, + ) + } +} + impl Deref for WebView { type Target = wry::WebView; @@ -199,6 +350,11 @@ impl Element for WebViewElement { )), }); + #[cfg(target_os = "windows")] + if let Some(native_surface) = &self.parent.read(cx).native_surface { + let _ = native_surface.set_bounds(bounds.to_device_pixels(window.scale_factor())); + } + // Create a hitbox to handle mouse event Some(window.insert_hitbox(bounds, gpui::HitboxBehavior::Normal)) } @@ -215,13 +371,260 @@ impl Element for WebViewElement { ) { let bounds = hitbox.clone().map(|h| h.bounds).unwrap_or(bounds); window.with_content_mask(Some(ContentMask { bounds }), |window| { - let webview = self.view.clone(); - window.on_mouse_event(move |event: &MouseDownEvent, _, _, _| { - if !bounds.contains(&event.position) { - // Click white space to blur the input focus - let _ = webview.focus_parent(); - } - }); + #[cfg(target_os = "windows")] + { + let webview = self.view.clone(); + let was_hovered = Rc::new(Cell::new(false)); + window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, _| { + if phase != DispatchPhase::Bubble { + return; + } + if bounds.contains(&event.position) { + was_hovered.set(true); + send_mouse_input(&webview, MouseInput::Move(event), bounds, window); + } else if was_hovered.replace(false) { + send_mouse_leave(&webview); + } + }); + + let webview = self.view.clone(); + let down_hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| { + // GPUI has updated its hit test before dispatching the + // event. Use it here rather than only checking bounds so + // an overlay control above the WebView remains the real + // target. + let hovered = down_hitbox + .as_ref() + .is_some_and(|hitbox| hitbox.is_hovered(window)); + if phase == DispatchPhase::Capture { + if hovered { + // Move native focus before any bubble listener can + // stop propagation. Composition WebView keyboard + // input is delivered through the controller's + // focused native window, not through GPUI's input + // handler. + window.blur(); + let _ = webview.focus(); + } else { + // Overlay controls may stop propagation during + // bubble, so hand native focus back during capture. + focus_parent(&webview); + } + return; + } + if phase != DispatchPhase::Bubble || !hovered { + return; + } + + { + if let Some(hitbox) = down_hitbox.as_ref() { + window.capture_pointer(hitbox.id); + } + send_mouse_input(&webview, MouseInput::Down(event), bounds, window); + } + }); + + let webview = self.view.clone(); + let up_hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &MouseUpEvent, phase, window, _| { + if phase != DispatchPhase::Bubble { + return; + } + if up_hitbox + .as_ref() + .is_some_and(|hitbox| hitbox.is_hovered(window)) + { + send_mouse_input(&webview, MouseInput::Up(event), bounds, window); + } + }); + + let webview = self.view.clone(); + let wheel_hitbox = hitbox.clone(); + window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, _| { + if phase != DispatchPhase::Bubble { + return; + } + if wheel_hitbox + .as_ref() + .is_some_and(|hitbox| hitbox.should_handle_scroll(window)) + { + send_mouse_input(&webview, MouseInput::Wheel(event), bounds, window); + } + }); + + let webview = self.view.clone(); + window.on_mouse_event(move |_: &MouseExitEvent, phase, _, _| { + if phase != DispatchPhase::Bubble { + return; + } + send_mouse_leave(&webview); + }); + } + + #[cfg(not(target_os = "windows"))] + { + let webview = self.view.clone(); + window.on_mouse_event(move |event: &MouseDownEvent, phase, window, _| { + if phase != DispatchPhase::Bubble { + return; + } + if bounds.contains(&event.position) { + // Native WebView focus is outside GPUI's focus tree. Clear + // GPUI focus so text inputs stop showing a stale caret. + window.blur(); + } else { + // Return native focus to the GPUI parent when clicking + // elsewhere in the GPUI window. + let _ = webview.focus_parent(); + } + }); + } }); } } + +#[cfg(target_os = "windows")] +enum MouseInput<'a> { + Move(&'a MouseMoveEvent), + Down(&'a MouseDownEvent), + Up(&'a MouseUpEvent), + Wheel(&'a ScrollWheelEvent), +} + +#[cfg(target_os = "windows")] +fn composition_controller(webview: &wry::WebView) -> Option { + use wry::WebViewExtWindows as _; + webview.composition_controller() +} + +#[cfg(target_os = "windows")] +fn focus_parent(webview: &wry::WebView) { + let _ = webview.focus_parent(); +} + +#[cfg(target_os = "windows")] +fn send_mouse_leave(webview: &wry::WebView) { + if let Some(controller) = composition_controller(webview) { + let _ = unsafe { + controller.SendMouseInput( + COREWEBVIEW2_MOUSE_EVENT_KIND_LEAVE, + COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_NONE, + 0, + POINT { x: 0, y: 0 }, + ) + }; + } +} + +#[cfg(target_os = "windows")] +fn send_mouse_input( + webview: &wry::WebView, + input: MouseInput<'_>, + bounds: Bounds, + window: &Window, +) { + let Some(controller) = composition_controller(webview) else { + return; + }; + + let (position, kind, virtual_keys, mouse_data) = match input { + MouseInput::Move(event) => ( + event.position, + COREWEBVIEW2_MOUSE_EVENT_KIND_MOVE, + virtual_keys(event.modifiers, event.pressed_button), + 0, + ), + MouseInput::Down(event) => ( + event.position, + button_kind(event.button, true, event.click_count), + virtual_keys(event.modifiers, Some(event.button)), + 0, + ), + MouseInput::Up(event) => ( + event.position, + button_kind(event.button, false, event.click_count), + virtual_keys(event.modifiers, None), + 0, + ), + MouseInput::Wheel(event) => { + let (kind, delta) = match event.delta { + ScrollDelta::Pixels(delta) => { + if delta.x.as_f32() != 0. { + ( + COREWEBVIEW2_MOUSE_EVENT_KIND_HORIZONTAL_WHEEL, + delta.x.as_f32() as i32, + ) + } else { + (COREWEBVIEW2_MOUSE_EVENT_KIND_WHEEL, delta.y.as_f32() as i32) + } + } + ScrollDelta::Lines(delta) => { + if delta.x != 0. { + ( + COREWEBVIEW2_MOUSE_EVENT_KIND_HORIZONTAL_WHEEL, + (delta.x * 120.) as i32, + ) + } else { + (COREWEBVIEW2_MOUSE_EVENT_KIND_WHEEL, (delta.y * 120.) as i32) + } + } + }; + ( + event.position, + kind, + virtual_keys(event.modifiers, None), + delta, + ) + } + }; + + let scale_factor = window.scale_factor(); + let point = POINT { + x: ((position.x - bounds.origin.x).as_f32() * scale_factor) as i32, + y: ((position.y - bounds.origin.y).as_f32() * scale_factor) as i32, + }; + let _ = unsafe { controller.SendMouseInput(kind, virtual_keys, mouse_data as u32, point) }; +} + +#[cfg(target_os = "windows")] +fn virtual_keys( + modifiers: gpui::Modifiers, + pressed_button: Option, +) -> COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS { + let mut keys = COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_NONE; + if modifiers.control { + keys = keys | COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_CONTROL; + } + if modifiers.shift { + keys = keys | COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_SHIFT; + } + keys | match pressed_button { + Some(MouseButton::Left) => COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_LEFT_BUTTON, + Some(MouseButton::Right) => COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_RIGHT_BUTTON, + Some(MouseButton::Middle) => COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_MIDDLE_BUTTON, + _ => COREWEBVIEW2_MOUSE_EVENT_VIRTUAL_KEYS_NONE, + } +} + +#[cfg(target_os = "windows")] +fn button_kind( + button: MouseButton, + down: bool, + click_count: usize, +) -> COREWEBVIEW2_MOUSE_EVENT_KIND { + match (button, down, click_count > 1) { + (MouseButton::Left, true, true) => COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOUBLE_CLICK, + (MouseButton::Left, true, false) => COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_DOWN, + (MouseButton::Left, false, _) => COREWEBVIEW2_MOUSE_EVENT_KIND_LEFT_BUTTON_UP, + (MouseButton::Right, true, true) => COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_DOUBLE_CLICK, + (MouseButton::Right, true, false) => COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_DOWN, + (MouseButton::Right, false, _) => COREWEBVIEW2_MOUSE_EVENT_KIND_RIGHT_BUTTON_UP, + (MouseButton::Middle, true, true) => { + COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_DOUBLE_CLICK + } + (MouseButton::Middle, true, false) => COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_DOWN, + (MouseButton::Middle, false, _) => COREWEBVIEW2_MOUSE_EVENT_KIND_MIDDLE_BUTTON_UP, + _ => COREWEBVIEW2_MOUSE_EVENT_KIND_MOVE, + } +} diff --git a/examples/webview/Cargo.toml b/examples/webview/Cargo.toml index eb792facb..fc3cf7673 100644 --- a/examples/webview/Cargo.toml +++ b/examples/webview/Cargo.toml @@ -16,7 +16,7 @@ gpui-component = { workspace = true } # WebView dependencies gpui-wry = { path = "../../crates/webview" } -wry = { version = "0.53.3", package = "lb-wry" } +wry = { git = "https://github.com/longbridge/wry", branch = "gpui-composition-controller", package = "lb-wry" } raw-window-handle = { version = "0.6", features = ["std"] } [target."cfg(target_os = \"linux\")".dependencies] diff --git a/examples/webview/src/main.rs b/examples/webview/src/main.rs index b47e7f27b..3602cc143 100644 --- a/examples/webview/src/main.rs +++ b/examples/webview/src/main.rs @@ -1,11 +1,83 @@ use gpui::*; use gpui_component::{ - ActiveTheme as _, Root, h_flex, - input::{Input, InputEvent, InputState}, + ActiveTheme as _, Root, WindowExt as _, + button::{Button, ButtonVariants as _}, + h_flex, + input::{Copy, Cut, Input, InputEvent, InputState, Paste, SelectAll}, + menu::DropdownMenu as _, + popover::Popover, v_flex, }; use gpui_wry::WebView; +const TEST_HTML: &str = r#" + + +Local WebView input test + +
+

Local WebView input test

+

Click each field and type: abc XYZ 123

+ + + + +
Focus: none
+

Keyboard events

+
Click the page, then press keys...
+
+ +"#; + pub struct Example { focus_handle: FocusHandle, webview: Entity, @@ -15,7 +87,7 @@ pub struct Example { impl Example { pub fn new(window: &mut Window, cx: &mut App) -> Entity { let webview = cx.new(|cx| { - let builder = wry::WebViewBuilder::new(); + let builder = wry::WebViewBuilder::new().with_html(TEST_HTML); #[cfg(any(debug_assertions, feature = "inspector"))] let builder = builder.with_devtools(true); @@ -25,7 +97,7 @@ impl Example { target_os = "ios", target_os = "android" )))] - let webview = { + { use gtk::prelude::*; use wry::WebViewBuilderExtUnix; // borrowed from https://github.com/tauri-apps/wry/blob/dev/examples/gtk_multiwebview.rs @@ -33,33 +105,27 @@ impl Example { // TODO: How to initialize this fixed? let fixed = gtk::Fixed::builder().build(); fixed.show_all(); - builder.build_gtk(&fixed).unwrap() - }; + let webview = builder.build_gtk(&fixed).unwrap(); + return WebView::new(webview, window, cx); + } #[cfg(any( target_os = "windows", target_os = "macos", target_os = "ios", target_os = "android" ))] - let webview = { + { use raw_window_handle::HasWindowHandle; let window_handle = window.window_handle().expect("No window handle"); - builder.build_as_child(&window_handle).unwrap() - }; - - WebView::new(webview, window, cx) + WebView::build_as_child(builder, &window_handle, window, cx).unwrap() + } }); let address_input = cx.new(|cx| { InputState::new(window, cx).default_value("https://longbridge.github.io/gpui-component") }); - let url = address_input.read(cx).value().clone(); - webview.update(cx, |view, _| { - view.load_url(&url); - }); - cx.new(|cx| { let this = Self { focus_handle: cx.focus_handle(), @@ -95,6 +161,43 @@ impl Example { webview.back().unwrap(); }); } + + fn show_dialog(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { + window.open_dialog(cx, |dialog, _, _| { + dialog.title("WebView overlay dialog").child( + v_flex() + .gap_3() + .w(px(520.)) + .child("This modal, its translucent backdrop, text, and controls are all GPUI.") + .child( + div() + .p_4() + .rounded_lg() + .border_1() + .child("The native WKWebView must remain visible behind the backdrop."), + ) + .child( + h_flex() + .gap_2() + .child(Button::new("dialog-secondary").label("Secondary action")) + .child( + Button::new("dialog-primary") + .primary() + .label("Primary action"), + ), + ), + ) + }); + cx.notify(); + } + + fn show_notification(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context) { + window.push_notification( + "This GPUI notification is rendered above the native WebView.", + cx, + ); + cx.notify(); + } } impl Focusable for Example { @@ -104,7 +207,7 @@ impl Focusable for Example { } impl Render for Example { - fn render(&mut self, _: &mut Window, cx: &mut Context) -> impl IntoElement { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let webview = self.webview.clone(); v_flex() @@ -115,7 +218,33 @@ impl Render for Example { h_flex() .gap_2() .items_center() - .child(Input::new(&self.address_input)), + .child(Input::new(&self.address_input)) + .child( + Popover::new("webview-overlay-proof") + .trigger(Button::new("overlay-trigger").label("Overlay proof")) + .w(px(320.)) + .child("This GPUI popover is rendered above the native WebView."), + ) + .child( + Button::new("dialog-trigger") + .label("Open dialog") + .on_click(cx.listener(Self::show_dialog)), + ) + .child( + Button::new("notification-trigger") + .label("Notify") + .on_click(cx.listener(Self::show_notification)), + ) + .child( + Button::new("popup-menu-trigger") + .label("Popup menu") + .dropdown_menu(|menu, _, _| { + menu.label("GPUI above WebView").separator().link( + "Open GPUI Component", + "https://longbridge.github.io/gpui-component/", + ) + }), + ), ) .child( div() @@ -125,25 +254,34 @@ impl Render for Example { .border_color(cx.theme().border) .child(webview.clone()), ) + .children(Root::render_dialog_layer(window, cx)) + .children(Root::render_notification_layer(window, cx)) } } fn main() { - // Required this for Windows to render the WebView. - #[cfg(target_os = "windows")] - unsafe { - std::env::set_var("GPUI_DISABLE_DIRECT_COMPOSITION", "true"); - } - gpui_platform::application().run(move |cx| { // This must be called before using any GPUI Component features. gpui_component::init(cx); + cx.set_menus([Menu::new("Edit").items([ + MenuItem::os_action("Cut", Cut, OsAction::Cut), + MenuItem::os_action("Copy", Copy, OsAction::Copy), + MenuItem::os_action("Paste", Paste, OsAction::Paste), + MenuItem::os_action("Select All", SelectAll, OsAction::SelectAll), + ])]); + let window_bounds = WindowBounds::centered(size(px(800.), px(600.)), cx); cx.spawn(async move |cx| { - cx.open_window(WindowOptions::default(), |window, cx| { - let view = Example::new(window, cx); - cx.new(|cx| Root::new(view, window, cx)) - }) + cx.open_window( + WindowOptions { + window_bounds: Some(window_bounds), + ..Default::default() + }, + |window, cx| { + let view = Example::new(window, cx); + cx.new(|cx| Root::new(view, window, cx)) + }, + ) .expect("Failed to open window"); }) .detach();