From d49a8e04e63605bdee52520d67318bee12a209a2 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 23 Dec 2025 12:29:05 -0700 Subject: [PATCH] Add pointer capture API for stable drag handling Add minimal pointer capture API to gpui::Window: - capture_pointer(hitbox_id): starts capture for the given hitbox - release_pointer(): releases capture - captured_hitbox(): returns the captured hitbox, if any When captured, HitboxId::is_hovered() returns true for the captured hitbox regardless of actual hit testing. Capture is automatically released on MouseUpEvent. This enables drag operations (like scrollbar thumb dragging) to continue working even when the pointer moves outside the element's bounds during the drag. --- crates/gpui/src/window.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index d0a6ff7dec602b27dd0dc2ab13d05f470d1187b2..e990dd31ea36800ad183e06bb6c91c489162ed2a 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -506,6 +506,10 @@ impl HitboxId { /// /// See [`Hitbox::is_hovered`] for details. pub fn is_hovered(self, window: &Window) -> bool { + // If this hitbox has captured the pointer, it's always considered hovered + if window.captured_hitbox == Some(self) { + return true; + } let hit_test = &window.mouse_hit_test; for id in hit_test.ids.iter().take(hit_test.hover_hitbox_count) { if self == *id { @@ -892,6 +896,9 @@ pub struct Window { pub(crate) pending_input_observers: SubscriberSet<(), AnyObserver>, prompt: Option, pub(crate) client_inset: Option, + /// The hitbox that has captured the pointer, if any. + /// While captured, mouse events route to this hitbox regardless of hit testing. + captured_hitbox: Option, #[cfg(any(feature = "inspector", debug_assertions))] inspector: Option>, } @@ -1316,6 +1323,7 @@ impl Window { prompt: None, client_inset: None, image_cache_stack: Vec::new(), + captured_hitbox: None, #[cfg(any(feature = "inspector", debug_assertions))] inspector: None, }) @@ -1999,6 +2007,26 @@ impl Window { self.mouse_position } + /// Captures the pointer for the given hitbox. While captured, all mouse move and mouse up + /// events will be routed to listeners that check this hitbox's `is_hovered` status, + /// regardless of actual hit testing. This enables drag operations that continue + /// even when the pointer moves outside the element's bounds. + /// + /// The capture is automatically released on mouse up. + pub fn capture_pointer(&mut self, hitbox_id: HitboxId) { + self.captured_hitbox = Some(hitbox_id); + } + + /// Releases any active pointer capture. + pub fn release_pointer(&mut self) { + self.captured_hitbox = None; + } + + /// Returns the hitbox that has captured the pointer, if any. + pub fn captured_hitbox(&self) -> Option { + self.captured_hitbox + } + /// The current state of the keyboard's modifiers pub fn modifiers(&self) -> Modifiers { self.modifiers @@ -3890,6 +3918,11 @@ impl Window { self.refresh(); } } + + // Auto-release pointer capture on mouse up + if event.is::() && self.captured_hitbox.is_some() { + self.captured_hitbox = None; + } } fn dispatch_key_event(&mut self, event: &dyn Any, cx: &mut App) {