diff --git a/crates/gpui/examples/README.md b/crates/gpui/examples/README.md index dab1c302c9135a..5b02754baef3d7 100644 --- a/crates/gpui/examples/README.md +++ b/crates/gpui/examples/README.md @@ -67,6 +67,9 @@ best starting point for new applications: - `active_state_bug` is a focused active-state reproduction. - `layer_shell` demonstrates Linux layer-shell windows. - `list_example` demonstrates bottom-aligned list state and scrollbar behavior. +- `native_webview` demonstrates the macOS native-surface overlay API with a + directly hosted `WKWebView`, without depending on wry. It also demonstrates + click-anywhere overlay dismissal and native-focus handoff back to GPUI. - `ownership_post` supports the ownership and data-flow documentation. - `paths_bench` is a path rendering benchmark. - `tree` renders a deep tree of nested elements. diff --git a/crates/gpui/examples/native_webview.html b/crates/gpui/examples/native_webview.html new file mode 100644 index 00000000000000..74d6c5ffeb3d96 --- /dev/null +++ b/crates/gpui/examples/native_webview.html @@ -0,0 +1,117 @@ + + + + + + + +
+
+
Native surface / live
+

A real browser inside GPUI’s scene.

+

+ This page is rendered by WKWebView. GPUI owns the surfaces + below and above it, while AppKit composes all three. +

+
+ + +
Keyboard event: waiting for input
+
+ + + diff --git a/crates/gpui/examples/native_webview.rs b/crates/gpui/examples/native_webview.rs new file mode 100644 index 00000000000000..26156dff3a07f0 --- /dev/null +++ b/crates/gpui/examples/native_webview.rs @@ -0,0 +1,659 @@ +#[cfg(not(target_os = "macos"))] +fn main() { + eprintln!("The native_webview example is only available on macOS."); +} + +#[cfg(target_os = "macos")] +mod macos { + use std::rc::Rc; + + use cocoa::{ + appkit::NSView, + base::{YES, id, nil}, + foundation::{NSPoint, NSRect, NSSize, NSString}, + }; + use gpui::{ + App, Bounds, Context, Div, Element, ElementId, GlobalElementId, IntoElement, LayoutId, + MouseButton, Pixels, Stateful, Style, Window, WindowBounds, WindowOptions, deferred, div, + prelude::*, px, relative, rgb, size, + }; + use gpui_platform::application; + use objc::{class, msg_send, sel, sel_impl}; + use raw_window_handle::{HasWindowHandle, RawWindowHandle}; + + #[link(name = "WebKit", kind = "framework")] + unsafe extern "C" {} + + const PAGE: &str = include_str!("native_webview.html"); + + struct NativeWebView { + parent: id, + view: id, + } + + impl NativeWebView { + fn new(window: &Window) -> anyhow::Result { + let window_handle = HasWindowHandle::window_handle(window).map_err(|error| { + anyhow::anyhow!("failed to get AppKit window handle: {error:?}") + })?; + let parent = match window_handle.as_raw() { + RawWindowHandle::AppKit(handle) => handle.ns_view.as_ptr() as id, + _ => anyhow::bail!("native_webview requires an AppKit window"), + }; + + unsafe { + let configuration: id = msg_send![class!(WKWebViewConfiguration), new]; + let view: id = msg_send![class!(WKWebView), alloc]; + let view: id = msg_send![ + view, + initWithFrame: NSRect::new( + NSPoint::new(0., 0.), + NSSize::new(0., 0.), + ) + configuration: configuration + ]; + if view.is_null() { + let _: () = msg_send![configuration, release]; + anyhow::bail!("failed to create WKWebView"); + } + + let _: () = msg_send![view, setWantsLayer: YES]; + let layer: id = msg_send![view, layer]; + let border_color: id = msg_send![ + class!(NSColor), + colorWithSRGBRed: 63. / 255. + green: 64. / 255. + blue: 67. / 255. + alpha: 1. + ]; + let border_color: id = msg_send![border_color, CGColor]; + let _: () = msg_send![layer, setMasksToBounds: YES]; + let _: () = msg_send![layer, setCornerRadius: 8.]; + let _: () = msg_send![layer, setBorderWidth: 1.]; + let _: () = msg_send![layer, setBorderColor: border_color]; + + let html = NSString::alloc(nil).init_str(PAGE); + let _: id = msg_send![view, loadHTMLString: html baseURL: nil]; + parent.addSubview_(view); + + let _: () = msg_send![html, release]; + let _: () = msg_send![configuration, release]; + + Ok(Self { parent, view }) + } + } + + fn set_bounds(&self, bounds: Bounds) { + unsafe { + let parent_bounds = NSView::bounds(self.parent); + let frame = NSRect::new( + NSPoint::new( + f64::from(bounds.origin.x), + parent_bounds.size.height + - f64::from(bounds.origin.y) + - f64::from(bounds.size.height), + ), + NSSize::new(f64::from(bounds.size.width), f64::from(bounds.size.height)), + ); + let _: () = msg_send![self.view, setFrame: frame]; + } + } + + fn focus_parent(&self) { + unsafe { + let window: id = msg_send![self.view, window]; + if !window.is_null() { + let _: bool = msg_send![window, makeFirstResponder: self.parent]; + } + } + } + } + + impl Drop for NativeWebView { + fn drop(&mut self) { + unsafe { + NSView::removeFromSuperview(self.view); + let _: () = msg_send![self.view, release]; + } + } + } + + struct NativeWebViewElement { + webview: Rc, + } + + impl IntoElement for NativeWebViewElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } + } + + impl Element for NativeWebViewElement { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + let mut style = Style::default(); + style.size.width = relative(1.).into(); + style.size.height = relative(1.).into(); + (window.request_layout(style, [], cx), ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, + bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) -> Self::PrepaintState { + self.webview.set_bounds(bounds); + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + _window: &mut Window, + _cx: &mut App, + ) { + } + } + + struct NativeWebViewExample { + webview: Rc, + about_active: bool, + dialog_open: bool, + menu_open: bool, + popover_open: bool, + } + + fn button(id: &'static str, label: &'static str) -> Stateful
{ + div() + .id(id) + .px_4() + .py_2() + .rounded_md() + .border_1() + .border_color(rgb(0x3f4043)) + .bg(rgb(0x1f2127)) + .text_color(rgb(0xbfbdb6)) + .text_sm() + .cursor_pointer() + .hover(|style| style.bg(rgb(0x2d2f34)).border_color(rgb(0x3e4043))) + .child(label) + } + + fn tab(id: &'static str, label: &'static str, active: bool) -> Stateful
{ + div() + .id(id) + .px_3() + .py_2() + .border_b_2() + .border_color(if active { rgb(0x5ac1fe) } else { rgb(0x313337) }) + .text_color(if active { rgb(0xbfbdb6) } else { rgb(0x8a8986) }) + .text_sm() + .cursor_pointer() + .hover(|style| style.text_color(rgb(0xbfbdb6))) + .child(label) + } + + fn menu_item(id: &'static str, label: &'static str) -> Stateful
{ + div() + .id(id) + .px_2() + .py_1() + .rounded_md() + .text_xs() + .text_color(rgb(0xbfbdb6)) + .cursor_pointer() + .hover(|style| style.bg(rgb(0x2d2f34))) + .child(label) + } + + fn layer_row(index: &'static str, label: &'static str, color: gpui::Rgba) -> Div { + div() + .flex() + .items_center() + .gap_2() + .py_2() + .border_b_1() + .border_color(rgb(0x3f4043)) + .child(div().w_1().h_5().rounded_sm().bg(color)) + .child(div().text_color(rgb(0x696a6a)).w(px(18.)).child(index)) + .child(div().text_color(rgb(0xbfbdb6)).child(label)) + } + + impl Render for NativeWebViewExample { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .id("native-webview-example") + .relative() + .flex() + .gap_6() + .size_full() + .p_7() + .bg(rgb(0x313337)) + .text_color(rgb(0xbfbdb6)) + // While a deferred overlay is visible, the transparent GPUI + // NSView captures the whole window. This handler therefore + // also dismisses clicks geometrically over the WKWebView. + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, _, _, cx| { + this.webview.focus_parent(); + this.popover_open = false; + this.dialog_open = false; + this.menu_open = false; + cx.notify(); + }), + ) + .child( + div() + .flex() + .flex_col() + .justify_between() + .w(px(180.)) + .child( + div() + .child( + div() + .mt(px(-2.)) + .text_lg() + .font_weight(gpui::FontWeight::SEMIBOLD) + .text_color(rgb(0xfeb454)) + .child("GPUI NATIVE WEBVIEW"), + ) + .child(div().mt_2().text_sm().text_color(rgb(0x8a8986)).child( + "Native composition.\nThree surfaces, one visual stack.", + )), + ) + .child( + div() + .flex() + .flex_col() + .gap_1() + .text_xs() + .child(layer_row("03", "GPUI overlay", rgb(0xfeb454))) + .child(layer_row("02", "WKWebView", rgb(0x5ac1fe))) + .child(layer_row("01", "GPUI base", rgb(0x8a8986))), + ), + ) + .child( + div() + .flex() + .flex_col() + .flex_1() + .min_w_0() + .h_full() + .gap_4() + .child( + div() + .flex() + .items_center() + .justify_between() + .child( + div() + .child( + div() + .text_xs() + .text_color(rgb(0x8a8986)) + .child("COMPOSITION TARGET"), + ) + .child( + div().mt_1().text_lg().child("WebView overlay proof"), + ), + ) + .child( + div() + .flex() + .gap_2() + .child( + button("toggle-popover", "Show popover").on_mouse_down( + MouseButton::Left, + cx.listener(|this, _, _, cx| { + cx.stop_propagation(); + this.webview.focus_parent(); + this.popover_open = !this.popover_open; + this.menu_open = false; + cx.notify(); + }), + ), + ) + .child(button("open-dialog", "Open dialog").on_mouse_down( + MouseButton::Left, + cx.listener(|this, _, _, cx| { + cx.stop_propagation(); + this.webview.focus_parent(); + this.dialog_open = true; + this.menu_open = false; + cx.notify(); + }), + )), + ), + ) + .child( + div() + .relative() + .flex() + .items_center() + .justify_between() + .border_b_1() + .border_color(rgb(0x3f4043)) + .child( + div() + .flex() + .child( + tab("webview-tab", "WebView", !self.about_active) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, _, _, cx| { + cx.stop_propagation(); + this.about_active = false; + this.popover_open = false; + this.dialog_open = false; + this.menu_open = false; + cx.notify(); + }), + ), + ) + .child( + tab("about-tab", "About", self.about_active) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, _, _, cx| { + cx.stop_propagation(); + this.webview.focus_parent(); + this.about_active = true; + this.popover_open = false; + this.dialog_open = false; + this.menu_open = false; + cx.notify(); + }), + ), + ), + ) + .child( + div() + .id("popup-menu-trigger") + .flex() + .items_center() + .justify_center() + .w(px(28.)) + .h(px(28.)) + .rounded_md() + .text_base() + .text_color(rgb(0x8a8986)) + .cursor_pointer() + .hover(|style| { + style.bg(rgb(0x2d2f34)).text_color(rgb(0xbfbdb6)) + }) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, _, _, cx| { + cx.stop_propagation(); + this.webview.focus_parent(); + this.menu_open = !this.menu_open; + cx.notify(); + }), + ) + .child("…"), + ) + .when(self.menu_open, |tab_bar| { + tab_bar.child( + deferred( + div() + .absolute() + .top(px(34.)) + .right_0() + .w(px(180.)) + .p_1() + .rounded_lg() + .border_1() + .border_color(rgb(0x3f4043)) + .bg(rgb(0x1f2127)) + .shadow_xl() + .child(menu_item( + "popup-menu-reload", + "Reload WebView", + )) + .child(menu_item( + "popup-menu-inspect", + "Inspect native surface", + )) + .child(div().my_1().h(px(1.)).bg(rgb(0x3f4043))) + .child(menu_item( + "popup-menu-about", + "About this example", + )) + .on_mouse_down( + MouseButton::Left, + cx.listener(|this, _, _, cx| { + cx.stop_propagation(); + this.menu_open = false; + cx.notify(); + }), + ), + ) + .priority(2), + ) + }), + ) + .child( + div() + .relative() + .flex_1() + .min_h_0() + .child(NativeWebViewElement { + webview: self.webview.clone(), + }) + .when(self.about_active, |content| { + content.child( + deferred( + div() + .absolute() + .inset_0() + .flex() + .flex_col() + .justify_center() + .p_10() + .rounded_lg() + .bg(rgb(0x0d1016)) + .text_color(rgb(0xbfbdb6)) + .child( + div() + .text_xs() + .text_color(rgb(0x5ac1fe)) + .child("GPUI OVERLAY CONTENT"), + ) + .child( + div() + .mt_3() + .text_3xl() + .font_weight(gpui::FontWeight::SEMIBOLD) + .child("A regular rendered view"), + ) + .child( + div() + .mt_4() + .max_w(px(520.)) + .text_color(rgb(0x8a8986)) + .line_height(relative(1.6)) + .child( + "This tab is rendered by GPUI above \ + the native WebView. It verifies that \ + non-popup content can replace and \ + fully occlude a native surface.", + ), + ), + ) + .priority(1), + ) + }), + ), + ) + .when(self.popover_open, |root| { + root.child( + deferred( + div() + .absolute() + .top(px(92.)) + .right(px(42.)) + .w(px(300.)) + .p_4() + .rounded_lg() + .shadow_xl() + .border_1() + .border_color(rgb(0x3f4043)) + .bg(rgb(0x1f2127)) + .text_color(rgb(0xbfbdb6)) + .child( + div() + .text_xs() + .text_color(rgb(0xfeb454)) + .child("SURFACE 03"), + ) + .child( + div() + .mt_2() + .font_weight(gpui::FontWeight::SEMIBOLD) + .child("Deferred GPUI popover"), + ) + .child(div().mt_2().text_sm().text_color(rgb(0x8a8986)).child( + "Painted after the native WebView without changing its \ + AppKit z-order.", + )), + ) + .priority(3), + ) + }) + .when(self.dialog_open, |root| { + root.child( + deferred( + div() + .absolute() + .inset_0() + .flex() + .items_center() + .justify_center() + .bg(rgb(0x0d1016).opacity(0.72)) + .child( + div() + .w(px(500.)) + .p_7() + .rounded_xl() + .shadow_xl() + .border_1() + .border_color(rgb(0x3f4043)) + .bg(rgb(0x1f2127)) + .text_color(rgb(0xbfbdb6)) + .child( + div() + .flex() + .items_center() + .justify_between() + .child( + div() + .text_xs() + .text_color(rgb(0xfeb454)) + .child("SURFACE 03 / GPUI OVERLAY"), + ) + .child( + div() + .px_2() + .py_1() + .rounded_md() + .bg(rgb(0x2d2f34)) + .text_xs() + .text_color(rgb(0x8a8986)) + .child("LIVE"), + ), + ) + .child( + div() + .mt_5() + .text_2xl() + .font_weight(gpui::FontWeight::SEMIBOLD) + .child( + "The native layer stays exactly where it is.", + ), + ) + .child(div().mt_3().text_color(rgb(0x8a8986)).child( + "GPUI splits its scene before deferred draws. \ + AppKit places WKWebView between the base and \ + this transparent overlay surface.", + )) + .child(div().mt_5().h(px(1.)).w_full().bg(rgb(0x3f4043))) + .child(div().mt_5().flex().child( + button("close-dialog", "Close").on_mouse_down( + MouseButton::Left, + cx.listener(|this, _, _, cx| { + cx.stop_propagation(); + this.webview.focus_parent(); + this.dialog_open = false; + cx.notify(); + }), + ), + )), + ), + ) + .priority(4), + ) + }) + } + } + + pub fn run() { + application().run(|cx: &mut App| { + let bounds = Bounds::centered(None, size(px(900.), px(640.)), cx); + cx.open_window( + WindowOptions { + window_bounds: Some(WindowBounds::Windowed(bounds)), + ..Default::default() + }, + |window, cx| { + let webview = Rc::new(NativeWebView::new(window).unwrap()); + + // Insert the transparent GPUI overlay after the native + // WebView so AppKit places it above the browser view. + window.enable_scene_overlay().unwrap(); + + cx.new(|_| NativeWebViewExample { + webview, + about_active: false, + dialog_open: false, + menu_open: false, + popover_open: false, + }) + }, + ) + .unwrap(); + cx.activate(true); + }); + } +} + +#[cfg(target_os = "macos")] +fn main() { + macos::run(); +} diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 5f8c63f93266cb..b47fdac3471faf 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -64,12 +64,25 @@ use std::io::Cursor; use std::ops; use std::time::Duration; use std::{ + any::Any, fmt::{self, Debug}, ops::Range, path::{Path, PathBuf}, rc::Rc, sync::Arc, }; + +/// A platform-native surface inserted between GPUI's base and overlay scene +/// planes. +pub trait PlatformNativeSurface { + /// Updates the native surface geometry in device pixels. + fn set_bounds(&self, bounds: Bounds) -> Result<()>; + /// Updates whether the native surface participates in composition. + fn set_visible(&self, visible: bool) -> Result<()>; + /// Returns the platform attachment object, such as an + /// `IDCompositionVisual` on Windows. + fn platform_handle(&self) -> Box; +} use strum::EnumIter; use uuid::Uuid; @@ -846,6 +859,25 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn on_appearance_changed(&self, callback: Box); fn on_button_layout_changed(&self, _callback: Box) {} fn draw(&self, scene: &Scene); + /// Draws a scene with primitives at and after `overlay_start` on a platform + /// overlay surface when one has been enabled. + /// + /// Platforms without layered scene support fall back to drawing the complete + /// scene on their primary surface. + fn draw_layered(&self, scene: &Scene, _overlay_start: usize) { + self.draw(scene); + } + /// Enables a transparent GPUI surface above native child views. + /// + /// This is currently an experimental capability for embedding native + /// surfaces between GPUI's base and deferred-overlay paint planes. + fn enable_scene_overlay(&self) -> anyhow::Result<()> { + anyhow::bail!("layered GPUI scenes are not supported on this platform") + } + /// Creates a native surface slot between GPUI's base and overlay planes. + fn create_native_surface(&self) -> Result> { + anyhow::bail!("native surface portals are not supported on this platform") + } fn completed_frame(&self) {} fn sprite_atlas(&self) -> Arc; fn is_subpixel_rendering_supported(&self) -> bool; diff --git a/crates/gpui/src/scene.rs b/crates/gpui/src/scene.rs index ea0f5d7e31af43..50b422ee304955 100644 --- a/crates/gpui/src/scene.rs +++ b/crates/gpui/src/scene.rs @@ -72,6 +72,22 @@ impl Scene { self.paint_operations.len() } + /// Returns whether the scene contains no drawable primitives. + /// + /// A scene may have paint operations that only open and close empty layers, + /// so `len() == 0` is not equivalent to having no visible/input-relevant + /// overlay content. + pub fn is_empty(&self) -> bool { + self.shadows.is_empty() + && self.quads.is_empty() + && self.paths.is_empty() + && self.underlines.is_empty() + && self.monochrome_sprites.is_empty() + && self.subpixel_sprites.is_empty() + && self.polychrome_sprites.is_empty() + && self.surfaces.is_empty() + } + pub fn push_layer(&mut self, bounds: Bounds) { let order = self.primitive_bounds.insert(bounds); self.layer_stack.push(order); @@ -191,6 +207,68 @@ impl Scene { } } +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn empty_layers_do_not_make_a_scene_drawable() { + let mut scene = Scene::default(); + let bounds = Bounds { + origin: Point::default(), + size: Size { + width: ScaledPixels::from(100.), + height: ScaledPixels::from(100.), + }, + }; + + scene.push_layer(bounds); + scene.pop_layer(); + + assert_ne!(scene.len(), 0); + assert!(scene.is_empty()); + } + + #[test] + fn drawable_primitives_make_a_scene_non_empty() { + let mut scene = Scene::default(); + let bounds = Bounds { + origin: Point::default(), + size: Size { + width: ScaledPixels::from(100.), + height: ScaledPixels::from(100.), + }, + }; + + scene.insert_primitive(Quad { + bounds, + content_mask: ContentMask { bounds }, + ..Default::default() + }); + + assert!(!scene.is_empty()); + } + + #[test] + fn replay_preserves_scene_emptiness() { + let mut source = Scene::default(); + let bounds = Bounds { + origin: Point::default(), + size: Size { + width: ScaledPixels::from(100.), + height: ScaledPixels::from(100.), + }, + }; + source.push_layer(bounds); + source.pop_layer(); + + let mut replayed = Scene::default(); + replayed.replay(0..source.len(), &source); + + assert!(replayed.is_empty()); + } +} + #[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd, Default)] #[cfg_attr( all( diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index de525d4545e2d6..4ab7fc338bfca8 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -912,6 +912,8 @@ pub(crate) struct Frame { pub(crate) mouse_listeners: Vec>, pub(crate) dispatch_tree: DispatchTree, pub(crate) scene: Scene, + /// First paint operation that belongs on the GPUI overlay surface. + pub(crate) overlay_scene_start: usize, pub(crate) hitboxes: Vec, pub(crate) window_control_hitboxes: Vec<(WindowControlArea, Hitbox)>, pub(crate) deferred_draws: Vec, @@ -958,6 +960,7 @@ impl Frame { mouse_listeners: Vec::new(), dispatch_tree, scene: Scene::default(), + overlay_scene_start: 0, hitboxes: Vec::new(), window_control_hitboxes: Vec::new(), deferred_draws: Vec::new(), @@ -983,6 +986,7 @@ impl Frame { self.mouse_listeners.clear(); self.dispatch_tree.clear(); self.scene.clear(); + self.overlay_scene_start = 0; self.input_handlers.clear(); self.tooltip_requests.clear(); self.cursor_styles.clear(); @@ -2012,6 +2016,21 @@ impl Window { self.handle } + /// Enables an experimental transparent GPUI scene plane above native child + /// views hosted by this window. + /// + /// The root scene remains on the primary surface. Deferred elements and + /// window-level overlays are rendered on the transparent plane. + pub fn enable_scene_overlay(&self) -> anyhow::Result<()> { + self.platform_window.enable_scene_overlay() + } + + /// Creates a native surface slot between the root scene and GPUI's + /// deferred/window-level overlay scene. + pub fn create_native_surface(&self) -> anyhow::Result> { + self.platform_window.create_native_surface() + } + /// Mark the window as dirty, scheduling it to be redrawn on the next frame. pub fn refresh(&mut self) { if self.invalidator.not_drawing() { @@ -2959,7 +2978,10 @@ impl Window { #[profiling::function] fn present(&mut self) { - self.platform_window.draw(&self.rendered_frame.scene); + self.platform_window.draw_layered( + &self.rendered_frame.scene, + self.rendered_frame.overlay_scene_start, + ); #[cfg(feature = "input-latency-histogram")] self.input_latency_tracker.record_frame_presented(); self.needs_present.set(false); @@ -3060,6 +3082,12 @@ impl Window { #[cfg(any(feature = "inspector", debug_assertions))] self.paint_inspector(inspector_element, cx); + // Native surfaces are composited after the root scene and before all + // deferred/window-level overlays. Platform backends with layered scene + // support use this boundary to render the remainder on a transparent + // surface above native children such as WebViews. + self.next_frame.overlay_scene_start = self.next_frame.scene.len(); + self.paint_deferred_draws(cx); if let Some(mut prompt_element) = prompt_element { diff --git a/crates/gpui_macos/src/metal_renderer.rs b/crates/gpui_macos/src/metal_renderer.rs index 5a72e23e140715..00762abdf3d9ee 100644 --- a/crates/gpui_macos/src/metal_renderer.rs +++ b/crates/gpui_macos/src/metal_renderer.rs @@ -53,6 +53,10 @@ pub(crate) unsafe fn new_renderer( MetalRenderer::new(context, transparent) } +pub(crate) fn new_overlay_renderer(context: self::Context, base: &Renderer) -> Renderer { + base.new_sharing_atlas(context, true) +} + pub(crate) struct InstanceBufferPool { buffer_size: usize, buffers: Vec, @@ -151,15 +155,25 @@ impl MetalRenderer { /// Creates a new MetalRenderer with a CAMetalLayer for window-based rendering. pub fn new(instance_buffer_pool: Arc>, transparent: bool) -> Self { let device = Self::create_device(); + // Support direct-to-display rendering if the window is not transparent + // https://developer.apple.com/documentation/metal/managing-your-game-window-for-metal-in-macos + let layer = Self::new_layer(&device, transparent); + + Self::new_internal( + device, + Some(layer), + !transparent, + instance_buffer_pool, + None, + ) + } + fn new_layer(device: &metal::Device, transparent: bool) -> metal::MetalLayer { let layer = metal::MetalLayer::new(); - layer.set_device(&device); + layer.set_device(device); layer.set_pixel_format(MTLPixelFormat::BGRA8Unorm); - // Support direct-to-display rendering if the window is not transparent - // https://developer.apple.com/documentation/metal/managing-your-game-window-for-metal-in-macos layer.set_opaque(!transparent); layer.set_maximum_drawable_count(3); - // Allow texture reading for visual tests (captures screenshots without ScreenCaptureKit) #[cfg(any(test, feature = "test-support"))] layer.set_framebuffer_only(false); unsafe { @@ -171,8 +185,23 @@ impl MetalRenderer { | AutoresizingMask::HEIGHT_SIZABLE ]; } + layer + } - Self::new_internal(device, Some(layer), !transparent, instance_buffer_pool) + fn new_sharing_atlas( + &self, + instance_buffer_pool: Arc>, + transparent: bool, + ) -> Self { + let device = self.device.clone(); + let layer = Self::new_layer(&device, transparent); + Self::new_internal( + device, + Some(layer), + !transparent, + instance_buffer_pool, + Some(self.sprite_atlas.clone()), + ) } /// Creates a new headless MetalRenderer for offscreen rendering without a window. @@ -182,7 +211,7 @@ impl MetalRenderer { #[cfg(any(test, feature = "test-support"))] pub fn new_headless(instance_buffer_pool: Arc>) -> Self { let device = Self::create_device(); - Self::new_internal(device, None, true, instance_buffer_pool) + Self::new_internal(device, None, true, instance_buffer_pool, None) } fn create_device() -> metal::Device { @@ -212,6 +241,7 @@ impl MetalRenderer { layer: Option, opaque: bool, instance_buffer_pool: Arc>, + shared_sprite_atlas: Option>, ) -> Self { #[cfg(feature = "runtime_shaders")] let library = device @@ -324,7 +354,8 @@ impl MetalRenderer { ); let command_queue = device.new_command_queue(); - let sprite_atlas = Arc::new(MetalAtlas::new(device.clone(), is_apple_gpu)); + let sprite_atlas = shared_sprite_atlas + .unwrap_or_else(|| Arc::new(MetalAtlas::new(device.clone(), is_apple_gpu))); let core_video_texture_cache = CVMetalTextureCache::new(None, device.clone(), None).unwrap(); diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index cb4e3cb7528e3d..679243d347eb4c 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -75,10 +75,12 @@ use std::{ }; const WINDOW_STATE_IVAR: &str = "windowState"; +const OVERLAY_INPUT_IVAR: &str = "overlayInputActive"; static mut WINDOW_CLASS: *const Class = ptr::null(); static mut PANEL_CLASS: *const Class = ptr::null(); static mut VIEW_CLASS: *const Class = ptr::null(); +static mut OVERLAY_VIEW_CLASS: *const Class = ptr::null(); static mut BLURRED_VIEW_CLASS: *const Class = ptr::null(); #[allow(non_upper_case_globals)] @@ -113,6 +115,27 @@ const NSDragOperationCopy: NSDragOperation = 1; const NSDragOperationMove: NSDragOperation = 16; const NSDRAGGING_CONTEXT_OUTSIDE_APPLICATION: NSInteger = 0; const NSDRAGGING_CONTEXT_WITHIN_APPLICATION: NSInteger = 1; + +extern "C" fn overlay_hit_test(this: &Object, _: Sel, _: NSPoint) -> id { + let active = unsafe { + let raw: *mut c_void = *this.get_ivar(OVERLAY_INPUT_IVAR); + &*(raw as *const AtomicBool) + }; + if active.load(Ordering::Acquire) { + this as *const Object as id + } else { + nil + } +} + +extern "C" fn handle_overlay_event(this: &Object, selector: Sel, native_event: id) { + let window_state = unsafe { get_window_state(this) }; + let native_view = window_state.lock().native_view; + unsafe { + handle_view_event(native_view.as_ref(), selector, native_event); + } +} + #[derive(PartialEq)] pub enum UserTabbingPreference { Never, @@ -300,6 +323,42 @@ unsafe fn build_classes() { ); decl.register() }; + OVERLAY_VIEW_CLASS = { + let mut decl = ClassDecl::new("GPUIOverlayView", class!(NSView)).unwrap(); + decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR); + decl.add_ivar::<*mut c_void>(OVERLAY_INPUT_IVAR); + decl.add_method( + sel!(dealloc), + dealloc_overlay_view as extern "C" fn(&Object, Sel), + ); + decl.add_method( + sel!(hitTest:), + overlay_hit_test as extern "C" fn(&Object, Sel, NSPoint) -> id, + ); + for selector in [ + sel!(mouseDown:), + sel!(mouseUp:), + sel!(rightMouseDown:), + sel!(rightMouseUp:), + sel!(otherMouseDown:), + sel!(otherMouseUp:), + sel!(mouseMoved:), + sel!(mouseExited:), + sel!(mouseDragged:), + sel!(rightMouseDragged:), + sel!(otherMouseDragged:), + sel!(scrollWheel:), + sel!(magnifyWithEvent:), + sel!(swipeWithEvent:), + sel!(pressureChangeWithEvent:), + ] { + decl.add_method( + selector, + handle_overlay_event as extern "C" fn(&Object, Sel, id), + ); + } + decl.register() + }; BLURRED_VIEW_CLASS = { let mut decl = ClassDecl::new("BlurredView", class!(NSVisualEffectView)).unwrap(); decl.add_method( @@ -503,12 +562,16 @@ struct MacWindowState { background_executor: BackgroundExecutor, native_window: id, native_view: NonNull, + overlay_view: Option>, + overlay_input_active: Arc, blurred_view: Option, background_appearance: WindowBackgroundAppearance, cursor_style: CursorStyle, cursor_visible: Arc, frame_source: Option, renderer: renderer::Renderer, + overlay_renderer: Option, + renderer_context: renderer::Context, request_frame_callback: Option>, event_callback: Option gpui::DispatchEventResult>>, activate_callback: Option>, @@ -549,6 +612,13 @@ struct MacWindowState { } impl MacWindowState { + fn set_presents_with_transaction(&mut self, enabled: bool) { + self.renderer.set_presents_with_transaction(enabled); + if let Some(renderer) = self.overlay_renderer.as_mut() { + renderer.set_presents_with_transaction(enabled); + } + } + fn move_traffic_light(&mut self) { if let Some(traffic_light_position) = self.traffic_light_position { if self.is_fullscreen() { @@ -899,18 +969,22 @@ impl MacWindow { background_executor, native_window, native_view: NonNull::new_unchecked(native_view), + overlay_view: None, + overlay_input_active: Arc::new(AtomicBool::new(false)), blurred_view: None, background_appearance: WindowBackgroundAppearance::Opaque, cursor_style: CursorStyle::Arrow, cursor_visible, frame_source: None, renderer: renderer::new_renderer( - renderer_context, + renderer_context.clone(), native_window as *mut _, native_view as *mut _, bounds.size.map(|pixels| pixels.as_f32()), false, ), + overlay_renderer: None, + renderer_context, request_frame_callback: None, event_callback: None, activate_callback: None, @@ -1760,6 +1834,84 @@ impl PlatformWindow for MacWindow { this.renderer.draw(scene); } + fn draw_layered(&self, scene: &gpui::Scene, overlay_start: usize) { + let mut this = self.0.lock(); + if this.overlay_renderer.is_none() { + this.renderer.draw(scene); + return; + } + + let split = overlay_start.min(scene.len()); + let mut base_scene = gpui::Scene::default(); + base_scene.replay(0..split, scene); + base_scene.finish(); + + let mut overlay_scene = gpui::Scene::default(); + overlay_scene.replay(split..scene.len(), scene); + overlay_scene.finish(); + + this.overlay_input_active + .store(!overlay_scene.is_empty(), Ordering::Release); + this.renderer.draw(&base_scene); + this.overlay_renderer + .as_mut() + .expect("overlay renderer checked above") + .draw(&overlay_scene); + } + + fn enable_scene_overlay(&self) -> anyhow::Result<()> { + let window_state = self.0.clone(); + let mut this = self.0.lock(); + if this.overlay_renderer.is_some() { + return Ok(()); + } + + unsafe { + let native_view = this.native_view.as_ptr() as id; + let frame = NSView::bounds(native_view); + let overlay_view: id = msg_send![OVERLAY_VIEW_CLASS, alloc]; + let overlay_view = NSView::initWithFrame_(overlay_view, frame); + anyhow::ensure!( + !overlay_view.is_null(), + "failed to create GPUI overlay NSView" + ); + (*overlay_view).set_ivar( + WINDOW_STATE_IVAR, + Arc::into_raw(window_state) as *const c_void, + ); + (*overlay_view).set_ivar( + OVERLAY_INPUT_IVAR, + Arc::into_raw(this.overlay_input_active.clone()) as *const c_void, + ); + + let mut overlay_renderer = + renderer::new_overlay_renderer(this.renderer_context.clone(), &this.renderer); + let scale_factor = this.scale_factor(); + overlay_renderer + .update_drawable_size(this.content_size().to_device_pixels(scale_factor)); + if let Some(layer) = overlay_renderer.layer() { + let _: () = msg_send![layer, setContentsScale: scale_factor as f64]; + } + + overlay_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); + overlay_view.setWantsLayer(YES); + let _: () = msg_send![overlay_view, setLayer: overlay_renderer.layer_ptr()]; + let _: () = msg_send![ + overlay_view, + setLayerContentsRedrawPolicy: NSViewLayerContentsRedrawDuringViewResize + ]; + + // wry has already inserted WKWebView by the time this API is called. + // Adding the overlay last places it above the WebView in AppKit's + // back-to-front subview order. + native_view.addSubview_(overlay_view.autorelease()); + this.overlay_view = NonNull::new(overlay_view); + this.overlay_renderer = Some(overlay_renderer); + } + + Ok(()) + } + fn sprite_atlas(&self) -> Arc { self.0.lock().renderer.sprite_atlas().clone() } @@ -2115,6 +2267,15 @@ extern "C" fn dealloc_view(this: &Object, _: Sel) { } } +extern "C" fn dealloc_overlay_view(this: &Object, _: Sel) { + unsafe { + drop_window_state(this); + let raw: *mut c_void = *this.get_ivar(OVERLAY_INPUT_IVAR); + drop(Arc::from_raw(raw as *const AtomicBool)); + let _: () = msg_send![super(this, class!(NSView)), dealloc]; + } +} + extern "C" fn reset_cursor_rects(this: &Object, _: Sel) { // SAFETY: AppKit invokes cursor-rect updates on the main thread for GPUIView instances, // whose WINDOW_STATE_IVAR is initialized when the view is created. The cursor registered @@ -2628,6 +2789,14 @@ fn update_window_scale_factor(window_state: &Arc>) { } lock.renderer.update_drawable_size(drawable_size); + if let Some(renderer) = lock.overlay_renderer.as_mut() { + if let Some(layer) = renderer.layer() { + unsafe { + let _: () = msg_send![layer, setContentsScale: scale_factor as f64]; + } + } + renderer.update_drawable_size(drawable_size); + } if let Some(mut callback) = lock.resize_callback.take() { let content_size = lock.content_size(); @@ -2697,14 +2866,14 @@ extern "C" fn window_did_change_key_status(this: &Object, selector: Sel, _: id) if lock.activated_least_once { if let Some(mut callback) = lock.request_frame_callback.take() { - lock.renderer.set_presents_with_transaction(true); + lock.set_presents_with_transaction(true); lock.stop_display_link(); drop(lock); callback(Default::default()); let mut lock = window_state.lock(); lock.request_frame_callback = Some(callback); - lock.renderer.set_presents_with_transaction(false); + lock.set_presents_with_transaction(false); lock.start_display_link(); } } else { @@ -2797,6 +2966,9 @@ extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) { let scale_factor = lock.scale_factor(); let drawable_size = new_size.to_device_pixels(scale_factor); lock.renderer.update_drawable_size(drawable_size); + if let Some(renderer) = lock.overlay_renderer.as_mut() { + renderer.update_drawable_size(drawable_size); + } if let Some(mut callback) = lock.resize_callback.take() { let content_size = lock.content_size(); @@ -2811,14 +2983,14 @@ extern "C" fn display_layer(this: &Object, _: Sel, _: id) { let window_state = unsafe { get_window_state(this) }; let mut lock = window_state.lock(); if let Some(mut callback) = lock.request_frame_callback.take() { - lock.renderer.set_presents_with_transaction(true); + lock.set_presents_with_transaction(true); lock.stop_display_link(); drop(lock); callback(Default::default()); let mut lock = window_state.lock(); lock.request_frame_callback = Some(callback); - lock.renderer.set_presents_with_transaction(false); + lock.set_presents_with_transaction(false); lock.start_display_link(); } } diff --git a/crates/gpui_windows/src/directx_renderer.rs b/crates/gpui_windows/src/directx_renderer.rs index 8a350682e239a7..eb604b05a3f615 100644 --- a/crates/gpui_windows/src/directx_renderer.rs +++ b/crates/gpui_windows/src/directx_renderer.rs @@ -1,4 +1,6 @@ use std::{ + cell::Cell, + rc::Rc, slice, sync::{Arc, OnceLock}, }; @@ -40,6 +42,7 @@ pub(crate) struct DirectXRenderer { atlas: Arc, devices: Option, resources: Option, + overlay_resources: Option, globals: DirectXGlobalElements, pipelines: DirectXRenderPipelines, direct_composition: Option, @@ -82,6 +85,12 @@ struct DirectXResources { viewport: D3D11_VIEWPORT, } +struct OverlayResources { + swap_chain: IDXGISwapChain1, + render_target: Option, + render_target_view: Option, +} + struct DirectXRenderPipelines { shadow_pipeline: PipelineState, quad_pipeline: PipelineState, @@ -115,8 +124,23 @@ impl Drop for Annotation<'_> { struct DirectComposition { comp_device: IDCompositionDevice, + // Keep these COM objects alive for the lifetime of the visual tree. They + // are not otherwise read after the tree is attached to the target. + #[allow(dead_code)] comp_target: IDCompositionTarget, - comp_visual: IDCompositionVisual, + #[allow(dead_code)] + root_visual: IDCompositionVisual, + base_visual: IDCompositionVisual, + portal_container: IDCompositionVisual, + overlay_visual: IDCompositionVisual, +} + +struct DirectCompositionPortal { + comp_device: IDCompositionDevice, + container: IDCompositionVisual, + visual: IDCompositionVisual, + clip: IDCompositionRectangleClip, + visible: Cell, } impl DirectXRendererDevices { @@ -185,6 +209,7 @@ impl DirectXRenderer { atlas, devices: Some(devices), resources: Some(resources), + overlay_resources: None, globals, pipelines, direct_composition, @@ -199,7 +224,11 @@ impl DirectXRenderer { self.atlas.clone() } - fn pre_draw(&self, clear_color: &[f32; 4]) -> Result<()> { + fn pre_draw( + &self, + render_target_view: &Option, + clear_color: &[f32; 4], + ) -> Result<()> { let resources = self.resources.as_ref().expect("resources missing"); let device_context = &self .devices @@ -220,14 +249,12 @@ impl DirectXRenderer { )?; unsafe { device_context.ClearRenderTargetView( - resources - .render_target_view + render_target_view .as_ref() .context("missing render target view")?, clear_color, ); - device_context - .OMSetRenderTargets(Some(slice::from_ref(&resources.render_target_view)), None); + device_context.OMSetRenderTargets(Some(slice::from_ref(render_target_view)), None); device_context.RSSetViewports(Some(slice::from_ref(&resources.viewport))); } Ok(()) @@ -254,6 +281,7 @@ impl DirectXRenderer { fn handle_device_lost_impl(&mut self, directx_devices: &DirectXDevices) -> Result<()> { let disable_direct_composition = self.direct_composition.is_none(); + let overlay_enabled = self.overlay_resources.is_some(); unsafe { #[cfg(debug_assertions)] @@ -264,6 +292,7 @@ impl DirectXRenderer { } self.resources.take(); + self.overlay_resources.take(); if let Some(devices) = &self.devices { devices.device_context.OMSetRenderTargets(None, None); devices.device_context.ClearState(); @@ -301,6 +330,16 @@ impl DirectXRenderer { composition.set_swap_chain(&resources.swap_chain)?; Some(composition) }; + let overlay_resources = if overlay_enabled { + let overlay = OverlayResources::new(&devices, self.width, self.height)?; + direct_composition + .as_ref() + .context("DirectComposition missing for overlay")? + .set_overlay_swap_chain(&overlay.swap_chain)?; + Some(overlay) + } else { + None + }; self.atlas .handle_device_lost(&devices.device, &devices.device_context); @@ -312,6 +351,7 @@ impl DirectXRenderer { } self.devices = Some(devices); self.resources = Some(resources); + self.overlay_resources = overlay_resources; self.globals = globals; self.pipelines = pipelines; self.direct_composition = direct_composition; @@ -329,13 +369,113 @@ impl DirectXRenderer { // and so likely do not have the textures anymore that are required for drawing return Ok(()); } - self.pre_draw(&match background_appearance { - WindowBackgroundAppearance::Opaque => [1.0f32; 4], - _ => [0.0f32; 4], - })?; + let render_target_view = self + .resources + .as_ref() + .context("resources missing")? + .render_target_view + .clone(); + self.pre_draw( + &render_target_view, + &match background_appearance { + WindowBackgroundAppearance::Opaque => [1.0f32; 4], + _ => [0.0f32; 4], + }, + )?; + self.draw_scene(scene)?; + self.present() + } - self.upload_scene_buffers(scene)?; + pub(crate) fn draw_layered( + &mut self, + scene: &Scene, + overlay_start: usize, + background_appearance: WindowBackgroundAppearance, + ) -> Result<()> { + if self.overlay_resources.is_none() { + return self.draw(scene, background_appearance); + } + if self.skip_draws { + return Ok(()); + } + + let split = overlay_start.min(scene.len()); + let mut base_scene = Scene::default(); + base_scene.replay(0..split, scene); + base_scene.finish(); + let mut overlay_scene = Scene::default(); + overlay_scene.replay(split..scene.len(), scene); + overlay_scene.finish(); + let base_view = self + .resources + .as_ref() + .context("resources missing")? + .render_target_view + .clone(); + self.pre_draw( + &base_view, + &match background_appearance { + WindowBackgroundAppearance::Opaque => [1.0; 4], + _ => [0.0; 4], + }, + )?; + self.draw_scene(&base_scene)?; + + let overlay_view = self + .overlay_resources + .as_ref() + .context("overlay resources missing")? + .render_target_view + .clone(); + self.pre_draw(&overlay_view, &[0.0; 4])?; + self.draw_scene(&overlay_scene)?; + + unsafe { + self.resources + .as_ref() + .context("resources missing")? + .swap_chain + .Present(0, DXGI_PRESENT(0)) + .ok() + .context("presenting base swap chain")?; + self.overlay_resources + .as_ref() + .context("overlay resources missing")? + .swap_chain + .Present(0, DXGI_PRESENT(0)) + .ok() + .context("presenting overlay swap chain")?; + } + Ok(()) + } + + pub(crate) fn enable_scene_overlay(&mut self) -> Result<()> { + if self.overlay_resources.is_some() { + return Ok(()); + } + let devices = self.devices.as_ref().context("devices missing")?; + let overlay = OverlayResources::new(devices, self.width, self.height)?; + self.direct_composition + .as_ref() + .context("DirectComposition is disabled")? + .set_overlay_swap_chain(&overlay.swap_chain)?; + self.overlay_resources = Some(overlay); + Ok(()) + } + + pub(crate) fn create_native_surface(&mut self) -> Result> { + self.enable_scene_overlay()?; + Ok(Rc::new( + self.direct_composition + .as_ref() + .context("DirectComposition is disabled")? + .create_portal()?, + )) + } + + fn draw_scene(&mut self, scene: &Scene) -> Result<()> { + self.upload_scene_buffers(scene)?; let annotation = self .devices .as_ref() @@ -380,7 +520,7 @@ impl DirectXRenderer { ) })?; } - self.present() + Ok(()) } pub(crate) fn resize(&mut self, new_size: Size) -> Result<()> { @@ -418,6 +558,10 @@ impl DirectXRenderer { resources.recreate_resources(devices, width, height)?; + if let Some(overlay) = self.overlay_resources.as_mut() { + overlay.resize(devices, width, height)?; + } + unsafe { devices .device_context @@ -927,23 +1071,149 @@ impl DirectComposition { pub fn new(dxgi_device: &IDXGIDevice, hwnd: HWND) -> Result { let comp_device = get_comp_device(dxgi_device)?; let comp_target = unsafe { comp_device.CreateTargetForHwnd(hwnd, true) }?; - let comp_visual = unsafe { comp_device.CreateVisual() }?; + let root_visual = unsafe { comp_device.CreateVisual() }?; + let base_visual = unsafe { comp_device.CreateVisual() }?; + let portal_container = unsafe { comp_device.CreateVisual() }?; + let overlay_visual = unsafe { comp_device.CreateVisual() }?; + + unsafe { + root_visual.AddVisual(&base_visual, false, None)?; + root_visual.AddVisual(&portal_container, true, &base_visual)?; + root_visual.AddVisual(&overlay_visual, true, &portal_container)?; + comp_target.SetRoot(&root_visual)?; + comp_device.Commit()?; + } Ok(Self { comp_device, comp_target, - comp_visual, + root_visual, + base_visual, + portal_container, + overlay_visual, }) } pub fn set_swap_chain(&self, swap_chain: &IDXGISwapChain1) -> Result<()> { unsafe { - self.comp_visual.SetContent(swap_chain)?; - self.comp_target.SetRoot(&self.comp_visual)?; + self.base_visual.SetContent(swap_chain)?; + self.comp_device.Commit()?; + } + Ok(()) + } + + pub fn set_overlay_swap_chain(&self, swap_chain: &IDXGISwapChain1) -> Result<()> { + unsafe { + self.overlay_visual.SetContent(swap_chain)?; self.comp_device.Commit()?; } Ok(()) } + + fn create_portal(&self) -> Result { + let visual = unsafe { self.comp_device.CreateVisual() }?; + let clip = unsafe { self.comp_device.CreateRectangleClip() }?; + unsafe { + visual.SetClip(&clip)?; + self.portal_container.AddVisual(&visual, true, None)?; + self.comp_device.Commit()?; + } + Ok(DirectCompositionPortal { + comp_device: self.comp_device.clone(), + container: self.portal_container.clone(), + visual, + clip, + visible: Cell::new(true), + }) + } +} + +impl PlatformNativeSurface for DirectCompositionPortal { + fn set_bounds(&self, bounds: Bounds) -> Result<()> { + let x = bounds.origin.x.0 as f32; + let y = bounds.origin.y.0 as f32; + let width = bounds.size.width.0.max(0) as f32; + let height = bounds.size.height.0.max(0) as f32; + unsafe { + self.visual.SetOffsetX2(x)?; + self.visual.SetOffsetY2(y)?; + self.clip.SetLeft2(0.0)?; + self.clip.SetTop2(0.0)?; + self.clip.SetRight2(width)?; + self.clip.SetBottom2(height)?; + self.comp_device.Commit()?; + } + Ok(()) + } + + fn set_visible(&self, visible: bool) -> Result<()> { + if self.visible.get() != visible { + unsafe { + if visible { + self.container.AddVisual(&self.visual, true, None)?; + } else { + self.container.RemoveVisual(&self.visual)?; + } + self.comp_device.Commit()?; + } + self.visible.set(visible); + } + Ok(()) + } + + fn platform_handle(&self) -> Box { + Box::new( + self.visual + .cast::() + .expect("IDCompositionVisual must implement IUnknown"), + ) + } +} + +impl Drop for DirectCompositionPortal { + fn drop(&mut self) { + unsafe { + self.container.RemoveVisual(&self.visual).ok(); + self.comp_device.Commit().ok(); + } + } +} + +impl OverlayResources { + fn new(devices: &DirectXRendererDevices, width: u32, height: u32) -> Result { + let swap_chain = create_swap_chain_for_composition( + &devices.dxgi_factory, + &devices.device, + width, + height, + )?; + let (render_target, render_target_view) = + create_render_target_and_its_view(&swap_chain, &devices.device)?; + Ok(Self { + swap_chain, + render_target: Some(render_target), + render_target_view, + }) + } + + fn resize(&mut self, devices: &DirectXRendererDevices, width: u32, height: u32) -> Result<()> { + self.render_target.take(); + self.render_target_view.take(); + unsafe { + self.swap_chain.ResizeBuffers( + BUFFER_COUNT as u32, + width, + height, + RENDER_TARGET_FORMAT, + DXGI_SWAP_CHAIN_FLAG(0), + )?; + } + let (render_target, render_target_view) = + create_render_target_and_its_view(&self.swap_chain, &devices.device)?; + self.render_target = Some(render_target); + self.render_target_view = render_target_view; + Ok(()) + } } impl DirectXGlobalElements { diff --git a/crates/gpui_windows/src/platform.rs b/crates/gpui_windows/src/platform.rs index d3bb150407e665..b1329fdd4ac87c 100644 --- a/crates/gpui_windows/src/platform.rs +++ b/crates/gpui_windows/src/platform.rs @@ -355,10 +355,13 @@ impl WindowsPlatform { } } -fn translate_accelerator(msg: &MSG) -> Option<()> { +fn translate_accelerator(msg: &MSG, is_gpui_window: impl FnOnce() -> bool) -> Option<()> { if msg.message != WM_KEYDOWN && msg.message != WM_SYSKEYDOWN { return None; } + if !is_gpui_window() { + return None; + } let result = unsafe { SendMessageW( @@ -419,7 +422,14 @@ impl Platform for WindowsPlatform { let mut msg = MSG::default(); unsafe { while GetMessageW(&mut msg, None, 0, 0).as_bool() { - if translate_accelerator(&msg).is_none() { + if translate_accelerator(&msg, || { + self.raw_window_handles + .read() + .iter() + .any(|handle| handle.as_raw() == msg.hwnd) + }) + .is_none() + { _ = TranslateMessage(&msg); DispatchMessageW(&msg); } @@ -1010,8 +1020,17 @@ impl WindowsPlatformInner { // then quit out of foreground work to allow us to process other gpui events first before returning back to foreground task work // if we don't we might not for example process window quit events let mut msg = MSG::default(); - let process_message = |msg: &_| { - if translate_accelerator(msg).is_none() { + let process_message = |msg: &MSG| { + if translate_accelerator(msg, || { + self.raw_window_handles.upgrade().is_some_and(|handles| { + handles + .read() + .iter() + .any(|handle| handle.as_raw() == msg.hwnd) + }) + }) + .is_none() + { _ = unsafe { TranslateMessage(msg) }; unsafe { DispatchMessageW(msg) }; } diff --git a/crates/gpui_windows/src/window.rs b/crates/gpui_windows/src/window.rs index af240716c90af9..7c194a8feeaa23 100644 --- a/crates/gpui_windows/src/window.rs +++ b/crates/gpui_windows/src/window.rs @@ -996,6 +996,22 @@ impl PlatformWindow for WindowsWindow { .log_err(); } + fn draw_layered(&self, scene: &Scene, overlay_start: usize) { + self.state + .renderer + .borrow_mut() + .draw_layered(scene, overlay_start, self.state.background_appearance.get()) + .log_err(); + } + + fn enable_scene_overlay(&self) -> anyhow::Result<()> { + self.state.renderer.borrow_mut().enable_scene_overlay() + } + + fn create_native_surface(&self) -> anyhow::Result> { + self.state.renderer.borrow_mut().create_native_surface() + } + fn sprite_atlas(&self) -> Arc { self.state.renderer.borrow().sprite_atlas() }