diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index f4a23b921d..b5915f0a0d 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -44,14 +44,14 @@ use crate::{ Action, ActionBuildError, ActionRegistry, ActivityGuard, Any, AnyView, AnyWindowHandle, AppContext, Arena, ArenaBox, Asset, AssetSource, BackgroundExecutor, Bounds, ClipboardItem, ClipboardReadError, CursorStyle, DispatchPhase, DisplayId, EventEmitter, ExternalDragPayload, - FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext, Keymap, Keystroke, - LayoutId, Menu, MenuItem, MissingGlyph, OwnedMenu, PathPromptOptions, Pixels, Platform, - PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, Priority, - PromptBuilder, PromptButton, PromptHandle, PromptLevel, Render, RenderImage, + FocusHandle, FocusMap, ForegroundExecutor, Global, GlobalCursorState, KeyBinding, KeyContext, + Keymap, Keystroke, LayoutId, Menu, MenuItem, MissingGlyph, OwnedMenu, PathPromptOptions, + Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, + Priority, PromptBuilder, PromptButton, PromptHandle, PromptLevel, Render, RenderImage, RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet, Subscription, SvgRenderer, SystemNotification, SystemNotificationResponse, Task, - TextRenderingMode, TextSystem, ThermalState, Window, WindowAppearance, WindowButtonLayout, - WindowHandle, WindowId, WindowInvalidator, + TextRenderingMode, TextSystem, ThermalState, Window, WindowAppearance, WindowBounds, + WindowButtonLayout, WindowHandle, WindowId, WindowInvalidator, colors::{Colors, GlobalColors}, hash, init_app_menus, }; @@ -738,6 +738,10 @@ enum PlatformOwnedDragState { RestoredInSourceWindow, } +/// How often the global cursor state is sampled while an in-app drag is +/// active, to notice releases that happen outside of every window. +const DRAG_POLL_INTERVAL: Duration = Duration::from_millis(30); + /// Contains the state of the full application, and passed as a reference to a variety of callbacks. /// Other [Context] derefs to this type. /// You need a reference to an `App` to access the state of a [Entity]. @@ -749,6 +753,10 @@ pub struct App { pub(crate) actions: Rc, pub(crate) active_drag: Option, platform_owned_drag: Option, + pub(crate) drag_hover_window: Option>, + drag_poll_running: bool, + drag_poll_task: Option>, + drag_dropped_outside_handler: Option, &mut App)>>, pub(crate) background_executor: BackgroundExecutor, pub(crate) foreground_executor: ForegroundExecutor, #[cfg(feature = "profiler")] @@ -875,6 +883,10 @@ impl App { pending_updates: 0, active_drag: None, platform_owned_drag: None, + drag_hover_window: None, + drag_poll_running: false, + drag_poll_task: None, + drag_dropped_outside_handler: None, background_executor, foreground_executor, #[cfg(feature = "profiler")] @@ -2636,6 +2648,7 @@ impl App { pub fn stop_active_drag(&mut self, window: &mut Window) -> bool { if self.active_drag.is_some() { self.active_drag = None; + self.drag_hover_window = None; if self.platform_owned_drag.as_ref().is_some_and(|drag| { drag.source_window == window.window_handle().window_id() && matches!(&drag.state, PlatformOwnedDragState::RestoredInSourceWindow) @@ -2649,6 +2662,107 @@ impl App { } } + /// Registers a handler invoked when an in-app drag is released outside of + /// every application window, on platforms that report the global cursor + /// state. + pub fn set_drag_dropped_outside_handler( + &mut self, + handler: impl FnMut(&AnyDrag, Point, &mut App) + 'static, + ) { + self.drag_dropped_outside_handler = Some(Box::new(handler)); + } + + pub(crate) fn should_paint_drag_in(&self, window_id: WindowId) -> bool { + match self.drag_hover_window { + None => true, + Some(hovered_window) => hovered_window == Some(window_id), + } + } + + pub(crate) fn set_drag_hover_window(&mut self, hovered: Option>) { + if self.drag_hover_window == hovered { + return; + } + let previous = mem::replace(&mut self.drag_hover_window, hovered).flatten(); + let next = hovered.flatten(); + for window_id in [previous, next].into_iter().flatten() { + if let Some(handle) = self.window_handles.get(&window_id).copied() { + handle + .update(self, |_, window, _| window.refresh()) + .log_err(); + } + } + } + + pub(crate) fn ensure_drag_poll_running(&mut self) { + if self.drag_poll_running || self.active_drag.is_none() { + return; + } + if self.platform.global_cursor_state().is_none() { + return; + } + self.drag_poll_running = true; + self.drag_poll_task = Some(self.spawn(async move |cx| { + loop { + cx.background_executor.timer(DRAG_POLL_INTERVAL).await; + let Some(state) = cx.update(|cx: &mut App| cx.platform.global_cursor_state()) + else { + break; + }; + let finished = cx.update(|cx: &mut App| cx.poll_drag_state(state)); + if finished { + break; + } + } + cx.update(|cx: &mut App| cx.drag_poll_running = false); + })); + } + + fn poll_drag_state(&mut self, state: GlobalCursorState) -> bool { + if self.active_drag.is_none() { + self.set_drag_hover_window(None); + return true; + } + let hovered_window = self.window_under_point(state.position); + self.set_drag_hover_window(Some(hovered_window)); + if state.left_button_pressed { + return false; + } + if hovered_window.is_none() + && let Some(drag) = self.active_drag.take() + { + self.set_drag_hover_window(None); + let mut handler = self.drag_dropped_outside_handler.take(); + if let Some(handler) = handler.as_mut() { + handler(&drag, state.position, self); + } + self.drag_dropped_outside_handler = handler; + self.refresh_windows(); + } + true + } + + fn window_under_point(&mut self, position: Point) -> Option { + let ordered_handles = self + .platform + .window_stack() + .unwrap_or_else(|| self.window_handles.values().copied().collect()); + for handle in ordered_handles { + let Ok(bounds) = handle.update(self, |_, window, _| window.window_bounds()) else { + continue; + }; + let bounds = match bounds { + WindowBounds::Windowed(bounds) + | WindowBounds::Maximized(bounds) + | WindowBounds::Fullscreen(bounds) => bounds, + }; + if bounds.contains(&position) { + return Some(handle.window_id()); + } + } + None + } + pub(crate) fn hand_active_drag_to_platform(&mut self, source_window: WindowId) -> bool { let Some(drag) = self.active_drag.take() else { return false; diff --git a/crates/gpui/src/app/test_context.rs b/crates/gpui/src/app/test_context.rs index b2b8179e8f..64ce47de7d 100644 --- a/crates/gpui/src/app/test_context.rs +++ b/crates/gpui/src/app/test_context.rs @@ -1,12 +1,13 @@ use crate::{ Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AsyncApp, AvailableSpace, BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DrawPhase, Drawable, - Element, Empty, EntityId, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke, - Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, - Pixels, Platform, PlatformTextSystem, Point, Render, Result, SharedString, Size, - SystemNotification, SystemNotificationResponse, Task, TestDispatcher, TestPlatform, - TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds, - WindowHandle, WindowOptions, WindowVisibility, app::GpuiMode, window::ElementArenaScope, + Element, Empty, EntityId, EventEmitter, ForegroundExecutor, Global, GlobalCursorState, + InputEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, + MouseMoveEvent, MouseUpEvent, Pixels, Platform, PlatformTextSystem, Point, Render, Result, + SharedString, Size, SystemNotification, SystemNotificationResponse, Task, TestDispatcher, + TestPlatform, TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, + WindowBounds, WindowHandle, WindowOptions, WindowVisibility, app::GpuiMode, + window::ElementArenaScope, }; use anyhow::{anyhow, bail}; use futures::{Stream, StreamExt, channel::oneshot}; @@ -500,6 +501,12 @@ impl TestAppContext { self.test_platform.set_screen_capture_sources(sources); } + /// Sets the global cursor state reported by the test platform, for + /// exercising drags that leave every window. + pub fn set_global_cursor_state(&self, state: Option) { + self.test_platform.set_global_cursor_state(state); + } + /// Returns all windows open in the test. pub fn windows(&self) -> Vec { self.app.borrow().windows() diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index c03653ed94..178c833705 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -182,6 +182,16 @@ pub fn guess_compositor() -> &'static str { } } +/// The global cursor position and mouse button state, used to follow +/// in-app drags once the cursor leaves every application window. +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct GlobalCursorState { + /// The cursor position in global display coordinates. + pub position: Point, + /// Whether the left mouse button is currently pressed. + pub left_button_pressed: bool, +} + #[expect(missing_docs)] pub trait Platform: 'static { fn background_executor(&self) -> BackgroundExecutor; @@ -203,6 +213,13 @@ pub trait Platform: 'static { None } + /// The global cursor position and button state, for platforms that can + /// query it without an active pointer grab. Returning `None` disables + /// following drags outside of the application's windows. + fn global_cursor_state(&self) -> Option { + None + } + fn is_screen_capture_supported(&self) -> bool { false } diff --git a/crates/gpui/src/platform/test/platform.rs b/crates/gpui/src/platform/test/platform.rs index 77a644114f..a68c06cc0d 100644 --- a/crates/gpui/src/platform/test/platform.rs +++ b/crates/gpui/src/platform/test/platform.rs @@ -4,11 +4,11 @@ use crate::NoopTextSystem; use crate::PathPromptOptions; use crate::{ ActivityGuard, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels, - DummyKeyboardMapper, ForegroundExecutor, Keymap, OwnedMenu, Platform, PlatformDisplay, - PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, - PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SharedString, - SourceMetadata, SystemNotification, SystemNotificationResponse, Task, TestDisplay, TestWindow, - ThermalState, WindowAppearance, WindowParams, size, + DummyKeyboardMapper, ForegroundExecutor, GlobalCursorState, Keymap, OwnedMenu, Platform, + PlatformDisplay, PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, + PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, + SharedString, SourceMetadata, SystemNotification, SystemNotificationResponse, Task, + TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, size, }; use anyhow::Result; #[cfg(any(test, feature = "test-support"))] @@ -42,6 +42,7 @@ pub(crate) struct TestPlatform { #[cfg(any(test, feature = "test-support"))] pub(crate) prompts: RefCell, screen_capture_sources: RefCell>, + pub(crate) global_cursor_state: RefCell>, pub opened_url: RefCell>, pub(crate) system_notifications: RefCell, pub text_system: Arc, @@ -153,6 +154,7 @@ impl TestPlatform { #[cfg(any(test, feature = "test-support"))] prompts: Default::default(), screen_capture_sources: Default::default(), + global_cursor_state: Default::default(), active_cursor: Default::default(), active_display: Rc::new(TestDisplay::new()), active_window: Default::default(), @@ -174,6 +176,11 @@ impl TestPlatform { }) } + /// Sets the global cursor state reported by [`Platform::global_cursor_state`]. + pub fn set_global_cursor_state(&self, state: Option) { + *self.global_cursor_state.borrow_mut() = state; + } + #[cfg(any(test, feature = "test-support"))] pub(crate) fn simulate_new_path_selection( &self, @@ -489,6 +496,10 @@ impl Platform for TestPlatform { .map(|window| window.0.lock().handle) } + fn global_cursor_state(&self) -> Option { + *self.global_cursor_state.borrow() + } + fn open_window( &self, handle: AnyWindowHandle, diff --git a/crates/gpui/src/text_system.rs b/crates/gpui/src/text_system.rs index 5c2e61ba7a..5e625458ec 100644 --- a/crates/gpui/src/text_system.rs +++ b/crates/gpui/src/text_system.rs @@ -1442,7 +1442,9 @@ pub fn font_name_with_fallbacks_shared<'a>( // IBM Plex/Lilex, and so retained here for backward compatibility. match name.as_str() { ".SystemUIFont" => system, - ".GwenSans" | ".ZedSans" | "Zed Plex Sans" => const { &SharedString::new_static("IBM Plex Sans") }, + ".GwenSans" | ".ZedSans" | "Zed Plex Sans" => { + const { &SharedString::new_static("IBM Plex Sans") } + } ".GwenMono" | ".ZedMono" | "Zed Plex Mono" => const { &SharedString::new_static("Lilex") }, _ => name, } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 636cb6da21..7edc1c448c 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -3563,7 +3563,9 @@ impl Window { element.prepaint_as_root(Point::default(), root_size.into(), self, cx); prompt_element = Some(element); self.prompt = Some(prompt); - } else if let Some(active_drag) = cx.active_drag.take() { + } else if cx.should_paint_drag_in(self.handle.window_id()) + && let Some(active_drag) = cx.active_drag.take() + { let mut element = active_drag.view.clone().into_any_element(); let offset = self.mouse_position() - active_drag.cursor_offset; element.prepaint_as_root(offset, AvailableSpace::min_size(), self, cx); @@ -5789,14 +5791,17 @@ impl Window { self.rendered_frame.mouse_listeners = mouse_listeners; if cx.has_active_drag() { + cx.ensure_drag_poll_running(); if event.is::() { // If this was a mouse move event, redraw the window so that the // active drag can follow the mouse cursor. + cx.set_drag_hover_window(Some(Some(self.handle.window_id()))); self.refresh(); } else if event.is::() { // If this was a mouse up event, cancel the active drag and redraw // the window. cx.active_drag = None; + cx.set_drag_hover_window(None); self.refresh(); } } @@ -7640,15 +7645,16 @@ mod tests { cell::{Cell, RefCell}, path::PathBuf, rc::Rc, + sync::Arc, time::Duration, }; use crate::{ - AnyWindowHandle, AppContext as _, Bounds, ContentMask, Context, DispatchPhase, + AnyDrag, AnyWindowHandle, AppContext as _, Bounds, ContentMask, Context, DispatchPhase, DragMoveEvent, Empty, ExternalDragPayload, ExternalPaths, FileDragPaths, FileDropEvent, - FocusHandle, InputEvent as _, InteractiveElement as _, IntoElement, KeyDownEvent, - Keystroke, LongPressEvent, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement, - Pixels, PlatformInput, Point, Render, RequestFrameOptions, ScaledPixels, + FocusHandle, GlobalCursorState, InputEvent as _, InteractiveElement as _, IntoElement, + KeyDownEvent, Keystroke, LongPressEvent, MouseButton, MouseDownEvent, MouseMoveEvent, + ParentElement, Pixels, PlatformInput, Point, Render, RequestFrameOptions, ScaledPixels, StatefulInteractiveElement as _, Styled, TestAppContext, TouchDragEvent, TouchEvent, TouchId, TouchPhase, Underline, UnderlineStyle, Window, WindowAppearance, WindowOptions, canvas, div, hsla, point, px, size, @@ -9109,6 +9115,65 @@ mod tests { }) .unwrap(); } + + #[gpui::test] + fn test_drag_release_outside_windows_notifies_handler(cx: &mut TestAppContext) { + let dropped_position = Rc::new(RefCell::new(None)); + let observed_position = dropped_position.clone(); + cx.update(|cx| { + cx.set_drag_dropped_outside_handler(move |_drag, position, _cx| { + *observed_position.borrow_mut() = Some(position); + }); + }); + + let window: AnyWindowHandle = cx.add_window(|_, _| EmptyView).into(); + cx.set_global_cursor_state(Some(GlobalCursorState { + position: point(px(10.), px(10.)), + left_button_pressed: true, + })); + cx.update_window(window, |_, window, cx| { + cx.active_drag = Some(AnyDrag { + value: Arc::new("payload"), + view: cx.new(|_| Empty).into(), + cursor_offset: point(px(0.), px(0.)), + cursor_style: None, + external_payload_source: None, + }); + cx.ensure_drag_poll_running(); + window.refresh(); + }) + .unwrap(); + cx.executor().advance_clock(Duration::from_millis(50)); + cx.run_until_parked(); + assert_eq!( + cx.read(|cx| cx.drag_hover_window), + Some(Some(window.window_id())) + ); + + // While the button stays pressed outside every window, the drag lives + // on but no window is asked to paint the ghost. + cx.set_global_cursor_state(Some(GlobalCursorState { + position: point(px(10_000.), px(10_000.)), + left_button_pressed: true, + })); + cx.executor().advance_clock(Duration::from_millis(50)); + cx.run_until_parked(); + assert_eq!(cx.read(|cx| cx.drag_hover_window), Some(None)); + assert!(cx.read(|cx| cx.active_drag.is_some())); + + // Releasing outside every window hands the drag to the handler. + cx.set_global_cursor_state(Some(GlobalCursorState { + position: point(px(10_000.), px(10_000.)), + left_button_pressed: false, + })); + cx.executor().advance_clock(Duration::from_millis(50)); + cx.run_until_parked(); + assert_eq!( + *dropped_position.borrow(), + Some(point(px(10_000.), px(10_000.))) + ); + assert!(cx.read(|cx| cx.active_drag.is_none())); + } } #[cfg(all(test, any(feature = "inspector", debug_assertions)))] diff --git a/crates/gpui_linux/src/linux/headless/client.rs b/crates/gpui_linux/src/linux/headless/client.rs index 0e94a06007..cb78b587c9 100644 --- a/crates/gpui_linux/src/linux/headless/client.rs +++ b/crates/gpui_linux/src/linux/headless/client.rs @@ -7,8 +7,8 @@ use gpui_util::ResultExt; use crate::linux::headless::window::{HeadlessDisplay, HeadlessWindow}; use crate::linux::{LinuxClient, LinuxCommon, LinuxKeyboardLayout}; use gpui::{ - AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, PlatformKeyboardLayout, - PlatformWindow, WindowParams, + AnyWindowHandle, CursorStyle, DisplayId, GlobalCursorState, PlatformDisplay, + PlatformKeyboardLayout, PlatformWindow, WindowParams, }; pub struct HeadlessClientState { @@ -97,6 +97,10 @@ impl LinuxClient for HeadlessClient { None } + fn global_cursor_state(&self) -> Option { + None + } + fn open_window( &self, _handle: AnyWindowHandle, diff --git a/crates/gpui_linux/src/linux/platform.rs b/crates/gpui_linux/src/linux/platform.rs index 7bf77a1bc3..d7cd38ee17 100644 --- a/crates/gpui_linux/src/linux/platform.rs +++ b/crates/gpui_linux/src/linux/platform.rs @@ -33,10 +33,10 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State}; use crate::linux::{LinuxDispatcher, PriorityQueueCalloopReceiver}; use gpui::{ Action, ActivityGuard, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, - DisplayId, ForegroundExecutor, Keymap, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform, - PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem, - PlatformWindow, Result, RunnableVariant, Task, ThermalState, WindowAppearance, - WindowButtonLayout, WindowParams, + DisplayId, ForegroundExecutor, GlobalCursorState, Keymap, Menu, MenuItem, OwnedMenu, + PathPromptOptions, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, + PlatformTextSystem, PlatformWindow, Result, RunnableVariant, Task, ThermalState, + WindowAppearance, WindowButtonLayout, WindowParams, }; #[cfg(any(feature = "wayland", feature = "x11"))] use gpui::{Pixels, Point, px}; @@ -101,6 +101,7 @@ pub(crate) trait LinuxClient { fn read_from_clipboard(&self) -> Option; fn active_window(&self) -> Option; fn window_stack(&self) -> Option>; + fn global_cursor_state(&self) -> Option; fn run(&self); #[cfg(any(feature = "wayland", feature = "x11"))] @@ -436,6 +437,10 @@ impl Platform for LinuxPlatform

{ self.inner.window_stack() } + fn global_cursor_state(&self) -> Option { + self.inner.global_cursor_state() + } + fn open_window( &self, handle: AnyWindowHandle, diff --git a/crates/gpui_linux/src/linux/wayland/client.rs b/crates/gpui_linux/src/linux/wayland/client.rs index 469eb44471..0bbf7ecd9a 100644 --- a/crates/gpui_linux/src/linux/wayland/client.rs +++ b/crates/gpui_linux/src/linux/wayland/client.rs @@ -97,9 +97,9 @@ use crate::linux::{ }; use gpui::{ AnyWindowHandle, Bounds, Capslock, CursorStyle, DevicePixels, DisplayId, ExternalDragPayload, - FileDragPaths, FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke, - Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, - MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, PlatformInput, + FileDragPaths, FileDropEvent, ForegroundExecutor, GlobalCursorState, KeyDownEvent, KeyUpEvent, + Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, + MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, PlatformInput, PlatformKeyboardLayout, PlatformWindow, Point, ScrollDelta, ScrollWheelEvent, SharedString, Size, TouchPhase, WindowButtonLayout, WindowKind, WindowParams, point, profiler, px, size, }; @@ -1368,6 +1368,10 @@ impl LinuxClient for WaylandClient { None } + fn global_cursor_state(&self) -> Option { + None + } + fn compositor_name(&self) -> &'static str { "Wayland" } diff --git a/crates/gpui_linux/src/linux/x11/client.rs b/crates/gpui_linux/src/linux/x11/client.rs index 23a9a91a97..e2350d489f 100644 --- a/crates/gpui_linux/src/linux/x11/client.rs +++ b/crates/gpui_linux/src/linux/x11/client.rs @@ -60,10 +60,11 @@ use crate::linux::{ use crate::linux::{LinuxCommon, LinuxKeyboardLayout, X11Window, modifiers_from_xinput_info}; use gpui::{ - AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke, - Modifiers, ModifiersChangedEvent, MouseButton, Pixels, PlatformDisplay, PlatformInput, - PlatformKeyboardLayout, PlatformWindow, Point, RequestFrameOptions, ScrollDelta, Size, - TouchPhase, WindowButtonLayout, WindowParams, WindowVisibility, point, px, + AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, + GlobalCursorState, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Pixels, + PlatformDisplay, PlatformInput, PlatformKeyboardLayout, PlatformWindow, Point, + RequestFrameOptions, ScrollDelta, Size, TouchPhase, WindowButtonLayout, WindowParams, + WindowVisibility, point, px, }; use gpui_wgpu::{CompositorGpuHint, GpuContext}; @@ -1878,6 +1879,24 @@ impl LinuxClient for X11Client { Some(handles) } + fn global_cursor_state(&self) -> Option { + let state = self.0.borrow(); + let root = state.xcb_connection.setup().roots[state.x_root_index].root; + let scale_factor = state.scale_factor; + let reply = get_reply( + || "Failed to query pointer position", + state.xcb_connection.query_pointer(root), + ) + .log_err()?; + Some(GlobalCursorState { + position: point( + px(reply.root_x as f32 / scale_factor), + px(reply.root_y as f32 / scale_factor), + ), + left_button_pressed: reply.mask.contains(xproto::KeyButMask::BUTTON1), + }) + } + fn window_identifier(&self) -> impl Future> + Send + 'static { let state = self.0.borrow(); state