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.
+
+
+
+ ENGINE WEBKIT
+ HOST NSVIEW
+ STATE INTERACTIVE
+
+
+ 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()
}