diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 7ad023bdcc..2bb855bb8a 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -790,6 +790,23 @@ pub enum TextInputStateChange { ContentChanged, } +/// Raw composition handles for embedding native surfaces (Windows: +/// `IDCompositionDevice*` and an `IDCompositionVisual*` layer; `None` on +/// other platforms). Both are borrowed: never release them, and treat them +/// as valid only while `generation` is unchanged — GPU device-lost recovery +/// releases them and creates new ones. Take a reference (`AddRef`) on +/// anything kept beyond the current frame, and remount native children when +/// the generation changes. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct NativeComposition { + /// The device that owns the layer. + pub device: *mut std::ffi::c_void, + /// The container beneath GPUI's overlay plane and above its base plane. + pub layer: *mut std::ffi::c_void, + /// Changes when GPU recovery recreates the device; remount children. + pub generation: u64, +} + #[expect(missing_docs)] pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle { fn bounds(&self) -> Bounds; @@ -855,6 +872,12 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle { anyhow::bail!("Native scene overlays are unavailable on this platform") } + /// Where native children composite between the base and overlay planes. + /// Available on Windows once [`Self::enable_scene_overlay`] succeeded. + fn native_composition(&self) -> Option { + None + } + fn completed_frame(&self) {} fn sprite_atlas(&self) -> Arc; fn is_subpixel_rendering_supported(&self) -> bool; diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index f129525bd0..3c9f85d76f 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -2915,6 +2915,11 @@ impl Window { self.platform_window.enable_scene_overlay() } + /// The composition layer native children mount into, when supported. + pub fn native_composition(&self) -> Option { + self.platform_window.native_composition() + } + /// Schedule a native-view update from an element's paint pass. Painting /// can run solely to refresh input hit testing, without presenting a frame. /// These updates run only when the corresponding scene reaches the screen, diff --git a/crates/gpui_windows/src/directx_renderer.rs b/crates/gpui_windows/src/directx_renderer.rs index 7a7ed674d0..45ed63b0e6 100644 --- a/crates/gpui_windows/src/directx_renderer.rs +++ b/crates/gpui_windows/src/directx_renderer.rs @@ -49,6 +49,16 @@ pub(crate) struct DirectXRenderer { globals: DirectXGlobalElements, pipelines: DirectXRenderPipelines, direct_composition: Option, + /// Deferred GPUI content above native children, once enabled. + overlay: Option, + /// Set once [`Self::enable_overlay`] succeeds. Device-lost recovery + /// restores the overlay from this, not from `overlay` — a failed + /// recovery attempt has already dropped it before the retry. + overlay_requested: bool, + overlay_generation: u64, + /// While drawing the overlay: the base frame its backdrop blurs sample, + /// so frosted popovers blur the UI beneath rather than an empty plane. + backdrop_source: Option, font_info: &'static FontInfo, width: u32, @@ -127,6 +137,60 @@ struct DirectComposition { comp_visual: IDCompositionVisual, } +/// The base swap chain's visual hosts two children: `layer`, where native +/// surfaces (WebView2) mount, and above it `visual`, a second swap chain for +/// deferred GPUI content. An empty overlay detaches its content and draws +/// nothing, so frames without menus cost exactly what they did before. +struct OverlayPlane { + resources: DirectXResources, + layer: IDCompositionVisual, + visual: IDCompositionVisual, + attached: bool, +} + +impl OverlayPlane { + fn new( + devices: &DirectXRendererDevices, + composition: &DirectComposition, + width: u32, + height: u32, + hwnd: HWND, + ) -> Result { + let resources = DirectXResources::new(devices, width, height, hwnd, false) + .context("Creating overlay resources")?; + unsafe { + let layer = composition.comp_device.CreateVisual()?; + let visual = composition.comp_device.CreateVisual()?; + visual.SetContent(&resources.swap_chain)?; + composition + .comp_visual + .AddVisual(&layer, false, None::<&IDCompositionVisual>)?; + composition.comp_visual.AddVisual(&visual, true, &layer)?; + composition.comp_device.Commit()?; + Ok(Self { + resources, + layer, + visual, + attached: true, + }) + } + } + + fn set_attached(&mut self, attached: bool) -> Result<()> { + if self.attached != attached { + unsafe { + if attached { + self.visual.SetContent(&self.resources.swap_chain)?; + } else { + self.visual.SetContent(None::<&windows::core::IUnknown>)?; + } + } + self.attached = attached; + } + Ok(()) + } +} + impl DirectXRendererDevices { pub(crate) fn new( directx_devices: &DirectXDevices, @@ -196,6 +260,10 @@ impl DirectXRenderer { globals, pipelines, direct_composition, + overlay: None, + overlay_requested: false, + overlay_generation: 0, + backdrop_source: None, font_info: Self::get_font_info(), width: 1, height: 1, @@ -272,6 +340,7 @@ impl DirectXRenderer { } self.resources.take(); + self.overlay.take(); if let Some(devices) = &self.devices { devices.device_context.OMSetRenderTargets(None, None); devices.device_context.ClearState(); @@ -324,6 +393,12 @@ impl DirectXRenderer { self.pipelines = pipelines; self.direct_composition = direct_composition; self.skip_draws = true; + if self.overlay_requested { + // Native children were mounted on the lost device; the new + // generation tells their owners to remount. + self.enable_overlay() + .context("Re-enabling the scene overlay")?; + } Ok(()) } @@ -339,6 +414,105 @@ impl DirectXRenderer { self.present() } + /// Create the native layer and overlay plane (idempotent). + pub(crate) fn enable_overlay(&mut self) -> Result<()> { + if self.overlay.is_some() { + return Ok(()); + } + let composition = self + .direct_composition + .as_ref() + .context("Native scene overlays require DirectComposition")?; + let devices = self.devices.as_ref().context("devices missing")?; + self.overlay = Some(OverlayPlane::new( + devices, + composition, + self.width, + self.height, + self.hwnd, + )?); + self.overlay_requested = true; + self.overlay_generation += 1; + Ok(()) + } + + pub(crate) fn has_overlay(&self) -> bool { + self.overlay.is_some() + } + + pub(crate) fn native_composition(&self) -> Option { + let composition = self.direct_composition.as_ref()?; + let overlay = self.overlay.as_ref()?; + Some(NativeComposition { + device: composition.comp_device.as_raw(), + layer: overlay.layer.as_raw(), + generation: self.overlay_generation, + }) + } + + /// Draw `scene[..overlay_start]` beneath native children and the rest + /// above them. Without deferred content the scene is drawn as-is (no + /// copy) and the overlay stays detached. Commits native-layer geometry + /// that presentation callbacks changed for this frame. + pub(crate) fn draw_layered( + &mut self, + scene: &Scene, + overlay_start: usize, + background_appearance: WindowBackgroundAppearance, + ) -> Result<()> { + if self.overlay.is_none() { + return self.draw(scene, background_appearance); + } + if self.skip_draws { + return Ok(()); + } + let split = overlay_start.min(scene.len()); + if split == scene.len() { + self.draw_scene(scene, background_appearance)?; + self.present()?; + self.overlay.as_mut().unwrap().set_attached(false)?; + } else { + let mut base = Scene::default(); + base.replay(0..split, scene); + base.finish(); + let mut overlay = Scene::default(); + overlay.replay(split..scene.len(), scene); + overlay.finish(); + self.draw_scene(&base, background_appearance)?; + // Present the base after the overlay is drawn: its back buffer + // is the overlay's blur source until then. + if !overlay.backdrop_blurs.is_empty() { + self.backdrop_source = self + .resources + .as_ref() + .and_then(|resources| resources.render_target.clone()); + } + // The overlay shares devices, pipelines and the atlas; only its + // swap chain and render targets differ. + self.swap_overlay_resources(); + let drawn = self + .draw_scene(&overlay, WindowBackgroundAppearance::Transparent) + .and_then(|_| self.present()); + self.swap_overlay_resources(); + self.backdrop_source = None; + drawn?; + self.present()?; + self.overlay.as_mut().unwrap().set_attached(true)?; + } + if let Some(composition) = &self.direct_composition { + unsafe { composition.comp_device.Commit()? }; + } + Ok(()) + } + + fn swap_overlay_resources(&mut self) { + let overlay = self.overlay.as_mut().expect("overlay missing"); + std::mem::swap( + self.resources.as_mut().expect("resources missing"), + &mut overlay.resources, + ); + } + // Kept separate from presentation so WARP tests can read the actual frame. fn draw_scene( &mut self, @@ -441,7 +615,27 @@ impl DirectXRenderer { // Clear the render target before resizing let devices = self.devices.as_ref().context("devices missing")?; unsafe { devices.device_context.OMSetRenderTargets(None, None) }; + if let Some(overlay) = self.overlay.as_mut() { + Self::resize_resources(devices, &mut overlay.resources, width, height)?; + } let resources = self.resources.as_mut().context("resources missing")?; + Self::resize_resources(devices, resources, width, height)?; + + unsafe { + devices + .device_context + .OMSetRenderTargets(Some(slice::from_ref(&resources.render_target_view)), None); + } + + Ok(()) + } + + fn resize_resources( + devices: &DirectXRendererDevices, + resources: &mut DirectXResources, + width: u32, + height: u32, + ) -> Result<()> { resources.backdrop = None; resources.render_target.take(); resources.render_target_view.take(); @@ -463,15 +657,7 @@ impl DirectXRenderer { .context("Failed to resize swap chain")?; } - resources.recreate_resources(devices, width, height)?; - - unsafe { - devices - .device_context - .OMSetRenderTargets(Some(slice::from_ref(&resources.render_target_view)), None); - } - - Ok(()) + resources.recreate_resources(devices, width, height) } fn upload_scene_buffers(&mut self, scene: &Scene) -> Result<()> { diff --git a/crates/gpui_windows/src/directx_renderer/backdrop.rs b/crates/gpui_windows/src/directx_renderer/backdrop.rs index 6c66c6c580..d9b0a79c4e 100644 --- a/crates/gpui_windows/src/directx_renderer/backdrop.rs +++ b/crates/gpui_windows/src/directx_renderer/backdrop.rs @@ -273,6 +273,7 @@ impl DirectXRenderer { } let downsample = ((sigma / 8.) as u32).clamp(1, 4); let devices = self.devices.as_ref().context("devices missing")?; + let source = self.backdrop_source.clone(); let resources = self.resources.as_mut().context("resources missing")?; if resources.backdrop.is_none() { resources.backdrop = Some(BackdropResources::new(&devices.device)?); @@ -306,9 +307,9 @@ impl DirectXRenderer { 0, 0, 0, - resources - .render_target + source .as_ref() + .or(resources.render_target.as_ref()) .context("missing render target")?, 0, Some(&D3D11_BOX { diff --git a/crates/gpui_windows/src/events.rs b/crates/gpui_windows/src/events.rs index b293763bd9..31ec14784b 100644 --- a/crates/gpui_windows/src/events.rs +++ b/crates/gpui_windows/src/events.rs @@ -421,6 +421,9 @@ impl WindowsWindowInner { lparam: LPARAM, ) -> Option { unsafe { SetCapture(handle) }; + // Pressing GPUI content takes the keyboard from an embedded native + // child; a press the app forwards to that child refocuses it. + reclaim_focus_from_native_child(handle); let Some(mut func) = self.state.callbacks.input.take() else { return Some(1); diff --git a/crates/gpui_windows/src/window.rs b/crates/gpui_windows/src/window.rs index 49a935bafe..a2ef0d33ac 100644 --- a/crates/gpui_windows/src/window.rs +++ b/crates/gpui_windows/src/window.rs @@ -63,6 +63,9 @@ pub struct WindowsWindowState { pub direct_manipulation: DirectManipulationHandler, pub renderer: RefCell, + /// Whether interactive overlay content (menus, prompts, drags) was + /// visible above native children last frame. + pub overlay_capture_input: Cell, /// Set after a GPU device-lost recovery so the next `draw_window` call is /// treated as a forced render. This guarantees the next frame both /// re-enables drawing (via `mark_drawable`) and bypasses the GPUI view @@ -168,6 +171,7 @@ impl WindowsWindowState { last_reported_capslock: Cell::new(last_reported_capslock), hovered: Cell::new(hovered), renderer: RefCell::new(renderer), + overlay_capture_input: Cell::new(false), force_render_after_recovery: Cell::new(false), click_state, current_cursor: Cell::new(current_cursor), @@ -982,6 +986,36 @@ impl PlatformWindow for WindowsWindow { .log_err(); } + fn draw_layered(&self, scene: &Scene, overlay_start: usize, capture_input: bool) { + let mut renderer = self.state.renderer.borrow_mut(); + if !renderer.has_overlay() { + renderer + .draw(scene, self.state.background_appearance.get()) + .log_err(); + return; + } + renderer + .draw_layered(scene, overlay_start, self.state.background_appearance.get()) + .log_err(); + drop(renderer); + // A menu opening over a focused native child takes the keyboard, as + // the macOS overlay does; passive tooltips leave it alone. + let active = capture_input && overlay_start < scene.len(); + if active && !self.state.overlay_capture_input.replace(active) { + reclaim_focus_from_native_child(self.0.hwnd); + } else if !active { + self.state.overlay_capture_input.set(false); + } + } + + fn enable_scene_overlay(&self) -> anyhow::Result<()> { + self.state.renderer.borrow_mut().enable_overlay() + } + + fn native_composition(&self) -> Option { + self.state.renderer.borrow().native_composition() + } + fn sprite_atlas(&self) -> Arc { self.state.renderer.borrow().sprite_atlas() } @@ -1662,3 +1696,14 @@ mod tests { ); } } + +/// Move keyboard focus back to the GPUI window when one of its native +/// children (an embedded WebView2) holds it. +pub(crate) fn reclaim_focus_from_native_child(hwnd: HWND) { + unsafe { + let focus = GetFocus(); + if !focus.is_invalid() && focus != hwnd && IsChild(hwnd, focus).as_bool() { + let _ = SetFocus(Some(hwnd)); + } + } +}