From 058f01fa93503491a735bfded53e77bfaa276148 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Fri, 17 Jul 2026 01:15:21 +0530 Subject: [PATCH] gpui: Fix clicks on window prompts dismissing popovers behind them (#61136) I noticed that when Zed renders prompts in-window (Linux, or `use_system_prompts: false`), clicking a prompt button also dismisses the popover behind it. Native macOS dialogs don't have this problem since the click goes to the OS dialog and the window never sees it. For example, in the worktree picker: 1. Delete a dirty worktree. 2. Confirm "Force Delete". 3. Mouse down dismissed the picker, which aborted the confirmed deletion. This PR makes GPUI-rendered prompts behave the same way as native. If someone actually wants to observe mouse downs during a prompt, the raw `window.on_mouse_event` API still sees every event. macOS: https://github.com/user-attachments/assets/9f85622f-c0d1-44f2-83f9-8378d9d7d13b Before GPUI prompt: https://github.com/user-attachments/assets/d1845ba1-9886-4733-8cf6-ab32424416e1 After GPUI prompt: https://github.com/user-attachments/assets/27936aca-8de5-4da8-88be-d88dbc1259d8 Release Notes: - Fixed confirmation dialog buttons dismissing the popover underneath them on click when using Zed-rendered prompts. --- crates/gpui/src/elements/div.rs | 86 ++++++++++++++++++++++++++++++++- crates/gpui/src/window.rs | 8 +++ 2 files changed, 93 insertions(+), 1 deletion(-) diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index c53c76e7871..c02bd16b77e 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -262,7 +262,10 @@ impl Interactivity { ) { self.mouse_down_listeners .push(Box::new(move |event, phase, hitbox, window, cx| { - if phase == DispatchPhase::Capture && !hitbox.contains(&window.mouse_position()) { + if phase == DispatchPhase::Capture + && !window.has_active_prompt() + && !hitbox.contains(&window.mouse_position()) + { (listener)(event, window, cx) } })); @@ -4523,6 +4526,87 @@ mod tests { assert!(active_tooltip.borrow().is_none()); } + struct MouseDownOutOwner { + mouse_down_out_count: Rc>, + } + + impl Render for MouseDownOutOwner { + fn render(&mut self, _window: &mut Window, _cx: &mut Context) -> impl IntoElement { + let mouse_down_out_count = self.mouse_down_out_count.clone(); + div() + .size_full() + .child(div().id("target").w(px(50.)).h(px(50.)).on_mouse_down_out( + move |_, _, _| { + *mouse_down_out_count.borrow_mut() += 1; + }, + )) + } + } + + #[test] + fn mouse_down_out_is_suppressed_while_window_prompt_is_active() { + let mut test_app = TestAppContext::single(); + let mouse_down_out_count = Rc::new(RefCell::new(0)); + let window = test_app.add_window({ + let mouse_down_out_count = mouse_down_out_count.clone(); + move |_, _| MouseDownOutOwner { + mouse_down_out_count, + } + }); + let any_window: AnyWindowHandle = window.into(); + + fn dispatch_mouse_down_outside_target( + test_app: &mut TestAppContext, + any_window: AnyWindowHandle, + ) { + test_app + .update_window(any_window, |_, window, cx| { + window.dispatch_event( + MouseDownEvent { + position: point(px(75.), px(75.)), + button: MouseButton::Left, + modifiers: Default::default(), + click_count: 1, + first_mouse: false, + } + .to_platform_input(), + cx, + ); + }) + .unwrap(); + } + + test_app + .update_window(any_window, |_, window, cx| { + window.draw(cx).clear(); + }) + .unwrap(); + + dispatch_mouse_down_outside_target(&mut test_app, any_window); + assert_eq!( + *mouse_down_out_count.borrow(), + 1, + "mouse down outside the element should fire mouse-down-out listeners" + ); + + test_app + .update_window(any_window, |_, window, cx| { + cx.set_prompt_builder(crate::fallback_prompt_renderer); + let _receiver = + window.prompt(crate::PromptLevel::Warning, "message", None, &["Ok"], cx); + assert!(window.has_active_prompt()); + window.draw(cx).clear(); + }) + .unwrap(); + + dispatch_mouse_down_outside_target(&mut test_app, any_window); + assert_eq!( + *mouse_down_out_count.borrow(), + 1, + "mouse down over an active prompt should not fire mouse-down-out listeners" + ); + } + #[test] fn test_write_a11y_info_string_and_numeric_properties() { let mut interactivity = Interactivity::default(); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 4c8b4651c8c..6a2bbf7874b 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -5343,6 +5343,14 @@ impl Window { receiver } + /// Returns whether a prompt rendered by GPUI is currently active in this window. + /// + /// This is only true for prompts rendered in the window (see + /// [`App::set_prompt_builder`]), not for platform-native prompt dialogs. + pub fn has_active_prompt(&self) -> bool { + self.prompt.is_some() + } + /// Returns the current context stack. pub fn context_stack(&self) -> Vec { let node_id = self.focus_node_id_in_rendered_frame(self.focus);