From 6694a3bd14bba5d52c375cc4c3ce9681445cbc22 Mon Sep 17 00:00:00 2001 From: MostlyK <135974627+MostlyKIGuess@users.noreply.github.com> Date: Sun, 29 Mar 2026 04:11:33 +0530 Subject: [PATCH] gpui: Implement pinch event support for X11 and Windows (#51354) Closes #51312 - Remove platform-specific #[cfg] gates from PinchEvent, event listeners, and dispatch logic in GPUI - Windows: Intercept Ctrl+ScrollWheel (emitted by precision trackpads for pinch gestures) and convert them to GPUI PinchEvents - Image Viewer: remove redundant platform-specific blocks - X11: Bump XInput version to 2.4 and implement handlers for XinputGesturePinch events - [x] Added a solid test coverage and/or screenshots from doing manual testing - [x] Done a self-review taking into account security and performance aspects - [x] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Release Notes: - Pinching gestures now available on all devices. --------- Co-authored-by: John Tur --- Cargo.toml | 2 + crates/gpui/src/elements/div.rs | 26 -- crates/gpui/src/interactive.rs | 11 - crates/gpui/src/window.rs | 1 - crates/gpui_linux/src/linux/x11/client.rs | 67 +++- crates/gpui_linux/src/linux/x11/window.rs | 8 +- .../gpui_windows/src/direct_manipulation.rs | 359 ++++++++++++++++++ crates/gpui_windows/src/events.rs | 22 ++ crates/gpui_windows/src/gpui_windows.rs | 1 + crates/gpui_windows/src/window.rs | 6 + crates/image_viewer/src/image_viewer.rs | 26 +- 11 files changed, 463 insertions(+), 66 deletions(-) create mode 100644 crates/gpui_windows/src/direct_manipulation.rs diff --git a/Cargo.toml b/Cargo.toml index 998a4705f28c82160b7124a98c1eb23c22360125..29b4494503a8e05017b2badee31416849a89c634 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -812,6 +812,7 @@ features = [ "Win32_Graphics_Direct3D_Fxc", "Win32_Graphics_DirectComposition", "Win32_Graphics_DirectWrite", + "Win32_Graphics_DirectManipulation", "Win32_Graphics_Dwm", "Win32_Graphics_Dxgi", "Win32_Graphics_Dxgi_Common", @@ -843,6 +844,7 @@ features = [ "Win32_UI_HiDpi", "Win32_UI_Input_Ime", "Win32_UI_Input_KeyboardAndMouse", + "Win32_UI_Input_Pointer", "Win32_UI_Shell", "Win32_UI_Shell_Common", "Win32_UI_Shell_PropertiesSystem", diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index c1bb2011d0bdff432fc5bd0da12b63a79cb9ef5a..cc4f586a3dce937c310e177eefaff1c81c6a4b89 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -15,7 +15,6 @@ //! and Tailwind-like styling that you can use to build your own custom elements. Div is //! constructed by combining these two systems into an all-in-one element. -#[cfg(any(target_os = "linux", target_os = "macos"))] use crate::PinchEvent; use crate::{ AbsoluteLength, Action, AnyDrag, AnyElement, AnyTooltip, AnyView, App, Bounds, ClickEvent, @@ -357,11 +356,7 @@ impl Interactivity { /// Bind the given callback to pinch gesture events during the bubble phase. /// - /// Note: This event is only available on macOS and Wayland (Linux). - /// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held. - /// /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - #[cfg(any(target_os = "linux", target_os = "macos"))] pub fn on_pinch(&mut self, listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static) { self.pinch_listeners .push(Box::new(move |event, phase, hitbox, window, cx| { @@ -373,11 +368,7 @@ impl Interactivity { /// Bind the given callback to pinch gesture events during the capture phase. /// - /// Note: This event is only available on macOS and Wayland (Linux). - /// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held. - /// /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - #[cfg(any(target_os = "linux", target_os = "macos"))] pub fn capture_pinch( &mut self, listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static, @@ -675,15 +666,9 @@ impl Interactivity { self.hitbox_behavior = HitboxBehavior::BlockMouseExceptScroll; } - #[cfg(any(target_os = "linux", target_os = "macos"))] fn has_pinch_listeners(&self) -> bool { !self.pinch_listeners.is_empty() } - - #[cfg(not(any(target_os = "linux", target_os = "macos")))] - fn has_pinch_listeners(&self) -> bool { - false - } } /// A trait for elements that want to use the standard GPUI event handlers that don't @@ -957,11 +942,7 @@ pub trait InteractiveElement: Sized { /// Bind the given callback to pinch gesture events during the bubble phase. /// The fluent API equivalent to [`Interactivity::on_pinch`]. /// - /// Note: This event is only available on macOS and Wayland (Linux). - /// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held. - /// /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - #[cfg(any(target_os = "linux", target_os = "macos"))] fn on_pinch(mut self, listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static) -> Self { self.interactivity().on_pinch(listener); self @@ -970,11 +951,7 @@ pub trait InteractiveElement: Sized { /// Bind the given callback to pinch gesture events during the capture phase. /// The fluent API equivalent to [`Interactivity::capture_pinch`]. /// - /// Note: This event is only available on macOS and Wayland (Linux). - /// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held. - /// /// See [`Context::listener`](crate::Context::listener) to get access to a view's state from this callback. - #[cfg(any(target_os = "linux", target_os = "macos"))] fn capture_pinch( mut self, listener: impl Fn(&PinchEvent, &mut Window, &mut App) + 'static, @@ -1367,7 +1344,6 @@ pub(crate) type MouseMoveListener = pub(crate) type ScrollWheelListener = Box; -#[cfg(any(target_os = "linux", target_os = "macos"))] pub(crate) type PinchListener = Box; @@ -1725,7 +1701,6 @@ pub struct Interactivity { pub(crate) mouse_pressure_listeners: Vec, pub(crate) mouse_move_listeners: Vec, pub(crate) scroll_wheel_listeners: Vec, - #[cfg(any(target_os = "linux", target_os = "macos"))] pub(crate) pinch_listeners: Vec, pub(crate) key_down_listeners: Vec, pub(crate) key_up_listeners: Vec, @@ -2297,7 +2272,6 @@ impl Interactivity { }) } - #[cfg(any(target_os = "linux", target_os = "macos"))] for listener in self.pinch_listeners.drain(..) { let hitbox = hitbox.clone(); window.on_mouse_event(move |event: &PinchEvent, phase, window, cx| { diff --git a/crates/gpui/src/interactive.rs b/crates/gpui/src/interactive.rs index 3d3ddb49f70b2f96772627d085c93ce31b6dc0b5..0c7f2f9c97c59f90f8e037f069357dcc3c60c9cd 100644 --- a/crates/gpui/src/interactive.rs +++ b/crates/gpui/src/interactive.rs @@ -473,10 +473,7 @@ impl Default for ScrollDelta { /// A pinch gesture event from the platform, generated when the user performs /// a pinch-to-zoom gesture (typically on a trackpad). /// -/// Note: This event is only available on macOS and Wayland (Linux). -/// On Windows, pinch gestures are simulated as scroll wheel events with Ctrl held. #[derive(Clone, Debug, Default)] -#[cfg(any(target_os = "linux", target_os = "macos"))] pub struct PinchEvent { /// The position of the pinch center on the window. pub position: Point, @@ -493,20 +490,15 @@ pub struct PinchEvent { pub phase: TouchPhase, } -#[cfg(any(target_os = "linux", target_os = "macos"))] impl Sealed for PinchEvent {} -#[cfg(any(target_os = "linux", target_os = "macos"))] impl InputEvent for PinchEvent { fn to_platform_input(self) -> PlatformInput { PlatformInput::Pinch(self) } } -#[cfg(any(target_os = "linux", target_os = "macos"))] impl GestureEvent for PinchEvent {} -#[cfg(any(target_os = "linux", target_os = "macos"))] impl MouseEvent for PinchEvent {} -#[cfg(any(target_os = "linux", target_os = "macos"))] impl Deref for PinchEvent { type Target = Modifiers; @@ -675,7 +667,6 @@ pub enum PlatformInput { /// The scroll wheel was used. ScrollWheel(ScrollWheelEvent), /// A pinch gesture was performed. - #[cfg(any(target_os = "linux", target_os = "macos"))] Pinch(PinchEvent), /// Files were dragged and dropped onto the window. FileDrop(FileDropEvent), @@ -693,7 +684,6 @@ impl PlatformInput { PlatformInput::MousePressure(event) => Some(event), PlatformInput::MouseExited(event) => Some(event), PlatformInput::ScrollWheel(event) => Some(event), - #[cfg(any(target_os = "linux", target_os = "macos"))] PlatformInput::Pinch(event) => Some(event), PlatformInput::FileDrop(event) => Some(event), } @@ -710,7 +700,6 @@ impl PlatformInput { PlatformInput::MousePressure(_) => None, PlatformInput::MouseExited(_) => None, PlatformInput::ScrollWheel(_) => None, - #[cfg(any(target_os = "linux", target_os = "macos"))] PlatformInput::Pinch(_) => None, PlatformInput::FileDrop(_) => None, } diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 088dabb3c0cefa2fc6b25c984e46e8e2f6a6b081..48c381e5275e950bd6754541fedbab03ae3d64c2 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -4146,7 +4146,6 @@ impl Window { self.modifiers = scroll_wheel.modifiers; PlatformInput::ScrollWheel(scroll_wheel) } - #[cfg(any(target_os = "linux", target_os = "macos"))] PlatformInput::Pinch(pinch) => { self.mouse_position = pinch.position; self.modifiers = pinch.modifiers; diff --git a/crates/gpui_linux/src/linux/x11/client.rs b/crates/gpui_linux/src/linux/x11/client.rs index 05d4ebd1bacce7e955b48a218e93b432182b75d4..57871e6ef32b937a7a47662f8022293a57bc3fe2 100644 --- a/crates/gpui_linux/src/linux/x11/client.rs +++ b/crates/gpui_linux/src/linux/x11/client.rs @@ -176,6 +176,7 @@ pub struct X11ClientState { pub(crate) last_mouse_button: Option, pub(crate) last_location: Point, pub(crate) current_count: usize, + pub(crate) pinch_scale: f32, pub(crate) gpu_context: GpuContext, pub(crate) compositor_gpu: Option, @@ -342,11 +343,12 @@ impl X11Client { xcb_connection.prefetch_extension_information(render::X11_EXTENSION_NAME)?; xcb_connection.prefetch_extension_information(xinput::X11_EXTENSION_NAME)?; - // Announce to X server that XInput up to 2.1 is supported. To increase this to 2.2 and - // beyond, support for touch events would need to be added. + // Announce to X server that XInput up to 2.4 is supported. + // Version 2.4 is needed for gesture events (GesturePinchBegin/Update/End). + // If the server only supports an older version, gesture events simply won't be delivered. let xinput_version = get_reply( || "XInput XiQueryVersion failed", - xcb_connection.xinput_xi_query_version(2, 1), + xcb_connection.xinput_xi_query_version(2, 4), )?; assert!( xinput_version.major_version >= 2, @@ -502,6 +504,7 @@ impl X11Client { last_mouse_button: None, last_location: Point::new(px(0.0), px(0.0)), current_count: 0, + pinch_scale: 1.0, gpu_context: Rc::new(RefCell::new(None)), compositor_gpu, scale_factor, @@ -1324,6 +1327,64 @@ impl X11Client { reset_pointer_device_scroll_positions(pointer); } } + Event::XinputGesturePinchBegin(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + state.pinch_scale = 1.0; + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + drop(state); + window.handle_input(PlatformInput::Pinch(gpui::PinchEvent { + position, + delta: 0.0, + modifiers, + phase: gpui::TouchPhase::Started, + })); + } + Event::XinputGesturePinchUpdate(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + // scale is in FP16.16 format: divide by 65536 to get the float value + let new_absolute_scale = event.scale as f32 / 65536.0; + let previous_scale = state.pinch_scale; + let zoom_delta = new_absolute_scale - previous_scale; + state.pinch_scale = new_absolute_scale; + drop(state); + window.handle_input(PlatformInput::Pinch(gpui::PinchEvent { + position, + delta: zoom_delta, + modifiers, + phase: gpui::TouchPhase::Moved, + })); + } + Event::XinputGesturePinchEnd(event) => { + let window = self.get_window(event.event)?; + let mut state = self.0.borrow_mut(); + state.pinch_scale = 1.0; + let modifiers = modifiers_from_xinput_info(event.mods); + state.modifiers = modifiers; + let position = point( + px(event.event_x as f32 / u16::MAX as f32 / state.scale_factor), + px(event.event_y as f32 / u16::MAX as f32 / state.scale_factor), + ); + drop(state); + window.handle_input(PlatformInput::Pinch(gpui::PinchEvent { + position, + delta: 0.0, + modifiers, + phase: gpui::TouchPhase::Ended, + })); + } _ => {} }; diff --git a/crates/gpui_linux/src/linux/x11/window.rs b/crates/gpui_linux/src/linux/x11/window.rs index 31d1dbacb114f2a9f760f94a898ba37e582cf12e..79bd7666e0eca36459c925be1628f542a30162f5 100644 --- a/crates/gpui_linux/src/linux/x11/window.rs +++ b/crates/gpui_linux/src/linux/x11/window.rs @@ -671,7 +671,13 @@ impl X11WindowState { | xinput::XIEventMask::BUTTON_PRESS | xinput::XIEventMask::BUTTON_RELEASE | xinput::XIEventMask::ENTER - | xinput::XIEventMask::LEAVE, + | xinput::XIEventMask::LEAVE + // x11rb 0.13 doesn't define XIEventMask constants for gesture + // events, so we construct them from the event opcodes (each + // XInput event type N maps to mask bit N). + | xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_BEGIN_EVENT) + | xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_UPDATE_EVENT) + | xinput::XIEventMask::from(1u32 << xinput::GESTURE_PINCH_END_EVENT), ], }], ), diff --git a/crates/gpui_windows/src/direct_manipulation.rs b/crates/gpui_windows/src/direct_manipulation.rs new file mode 100644 index 0000000000000000000000000000000000000000..08a1e5243e19e1ea6464ceb224754bee93573ea2 --- /dev/null +++ b/crates/gpui_windows/src/direct_manipulation.rs @@ -0,0 +1,359 @@ +use std::cell::{Cell, RefCell}; +use std::rc::Rc; + +use ::util::ResultExt; +use anyhow::Result; +use gpui::*; +use windows::Win32::{ + Foundation::*, + Graphics::{DirectManipulation::*, Gdi::*}, + System::Com::*, + UI::{Input::Pointer::*, WindowsAndMessaging::*}, +}; + +use crate::*; + +/// Default viewport size in pixels. The actual content size doesn't matter +/// because we're using the viewport only for gesture recognition, not for +/// visual output. +const DEFAULT_VIEWPORT_SIZE: i32 = 1000; + +pub(crate) struct DirectManipulationHandler { + manager: IDirectManipulationManager, + update_manager: IDirectManipulationUpdateManager, + viewport: IDirectManipulationViewport, + _handler_cookie: u32, + window: HWND, + scale_factor: Rc>, + pending_events: Rc>>, +} + +impl DirectManipulationHandler { + pub fn new(window: HWND, scale_factor: f32) -> Result { + unsafe { + let manager: IDirectManipulationManager = + CoCreateInstance(&DirectManipulationManager, None, CLSCTX_INPROC_SERVER)?; + + let update_manager: IDirectManipulationUpdateManager = manager.GetUpdateManager()?; + + let viewport: IDirectManipulationViewport = manager.CreateViewport(None, window)?; + + let configuration = DIRECTMANIPULATION_CONFIGURATION_INTERACTION + | DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_X + | DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_Y + | DIRECTMANIPULATION_CONFIGURATION_TRANSLATION_INERTIA + | DIRECTMANIPULATION_CONFIGURATION_RAILS_X + | DIRECTMANIPULATION_CONFIGURATION_RAILS_Y + | DIRECTMANIPULATION_CONFIGURATION_SCALING; + viewport.ActivateConfiguration(configuration)?; + + viewport.SetViewportOptions( + DIRECTMANIPULATION_VIEWPORT_OPTIONS_MANUALUPDATE + | DIRECTMANIPULATION_VIEWPORT_OPTIONS_DISABLEPIXELSNAPPING, + )?; + + let mut rect = RECT { + left: 0, + top: 0, + right: DEFAULT_VIEWPORT_SIZE, + bottom: DEFAULT_VIEWPORT_SIZE, + }; + viewport.SetViewportRect(&mut rect)?; + + manager.Activate(window)?; + viewport.Enable()?; + + let scale_factor = Rc::new(Cell::new(scale_factor)); + let pending_events = Rc::new(RefCell::new(Vec::new())); + + let event_handler: IDirectManipulationViewportEventHandler = + DirectManipulationEventHandler::new( + window, + Rc::clone(&scale_factor), + Rc::clone(&pending_events), + ) + .into(); + + let handler_cookie = viewport.AddEventHandler(Some(window), &event_handler)?; + + update_manager.Update(None)?; + + Ok(Self { + manager, + update_manager, + viewport, + _handler_cookie: handler_cookie, + window, + scale_factor, + pending_events, + }) + } + } + + pub fn set_scale_factor(&self, scale_factor: f32) { + self.scale_factor.set(scale_factor); + } + + pub fn on_pointer_hit_test(&self, wparam: WPARAM) { + unsafe { + let pointer_id = wparam.loword() as u32; + let mut pointer_type = POINTER_INPUT_TYPE::default(); + if GetPointerType(pointer_id, &mut pointer_type).is_ok() && pointer_type == PT_TOUCHPAD + { + self.viewport.SetContact(pointer_id).log_err(); + } + } + } + + pub fn update(&self) { + unsafe { + self.update_manager.Update(None).log_err(); + } + } + + pub fn drain_events(&self) -> Vec { + std::mem::take(&mut *self.pending_events.borrow_mut()) + } +} + +impl Drop for DirectManipulationHandler { + fn drop(&mut self) { + unsafe { + self.viewport.Stop().log_err(); + self.viewport.Abandon().log_err(); + self.manager.Deactivate(self.window).log_err(); + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum GestureKind { + None, + Scroll, + Pinch, +} + +#[windows_core::implement(IDirectManipulationViewportEventHandler)] +struct DirectManipulationEventHandler { + window: HWND, + scale_factor: Rc>, + gesture_kind: Cell, + last_scale: Cell, + last_x_offset: Cell, + last_y_offset: Cell, + scroll_phase: Cell, + pending_events: Rc>>, +} + +impl DirectManipulationEventHandler { + fn new( + window: HWND, + scale_factor: Rc>, + pending_events: Rc>>, + ) -> Self { + Self { + window, + scale_factor, + gesture_kind: Cell::new(GestureKind::None), + last_scale: Cell::new(1.0), + last_x_offset: Cell::new(0.0), + last_y_offset: Cell::new(0.0), + scroll_phase: Cell::new(TouchPhase::Started), + pending_events, + } + } + + fn end_gesture(&self) { + let position = self.mouse_position(); + let modifiers = current_modifiers(); + match self.gesture_kind.get() { + GestureKind::Scroll => { + self.pending_events + .borrow_mut() + .push(PlatformInput::ScrollWheel(ScrollWheelEvent { + position, + delta: ScrollDelta::Pixels(point(px(0.0), px(0.0))), + modifiers, + touch_phase: TouchPhase::Ended, + })); + } + GestureKind::Pinch => { + self.pending_events + .borrow_mut() + .push(PlatformInput::Pinch(PinchEvent { + position, + delta: 0.0, + modifiers, + phase: TouchPhase::Ended, + })); + } + GestureKind::None => {} + } + self.gesture_kind.set(GestureKind::None); + } + + fn mouse_position(&self) -> Point { + let scale_factor = self.scale_factor.get(); + unsafe { + let mut point: POINT = std::mem::zeroed(); + let _ = GetCursorPos(&mut point); + let _ = ScreenToClient(self.window, &mut point); + logical_point(point.x as f32, point.y as f32, scale_factor) + } + } +} + +impl IDirectManipulationViewportEventHandler_Impl for DirectManipulationEventHandler_Impl { + fn OnViewportStatusChanged( + &self, + viewport: windows_core::Ref<'_, IDirectManipulationViewport>, + current: DIRECTMANIPULATION_STATUS, + previous: DIRECTMANIPULATION_STATUS, + ) -> windows_core::Result<()> { + if current == previous { + return Ok(()); + } + + // A new gesture interrupted inertia, so end the old sequence. + if current == DIRECTMANIPULATION_RUNNING && previous == DIRECTMANIPULATION_INERTIA { + self.end_gesture(); + } + + if current == DIRECTMANIPULATION_READY { + self.end_gesture(); + + // Reset the content transform so the viewport is ready for the next gesture. + // ZoomToRect triggers a second RUNNING -> READY cycle, so prevent an infinite loop here. + if self.last_scale.get() != 1.0 + || self.last_x_offset.get() != 0.0 + || self.last_y_offset.get() != 0.0 + { + if let Some(viewport) = viewport.as_ref() { + unsafe { + viewport + .ZoomToRect( + 0.0, + 0.0, + DEFAULT_VIEWPORT_SIZE as f32, + DEFAULT_VIEWPORT_SIZE as f32, + false, + ) + .log_err(); + } + } + } + + self.last_scale.set(1.0); + self.last_x_offset.set(0.0); + self.last_y_offset.set(0.0); + } + + Ok(()) + } + + fn OnViewportUpdated( + &self, + _viewport: windows_core::Ref<'_, IDirectManipulationViewport>, + ) -> windows_core::Result<()> { + Ok(()) + } + + fn OnContentUpdated( + &self, + _viewport: windows_core::Ref<'_, IDirectManipulationViewport>, + content: windows_core::Ref<'_, IDirectManipulationContent>, + ) -> windows_core::Result<()> { + let content = content.as_ref().ok_or(E_POINTER)?; + + // Get the 6-element content transform: [scale, 0, 0, scale, tx, ty] + let mut xform = [0.0f32; 6]; + unsafe { + content.GetContentTransform(&mut xform)?; + } + + let scale = xform[0]; + let scale_factor = self.scale_factor.get(); + let x_offset = xform[4] / scale_factor; + let y_offset = xform[5] / scale_factor; + + if scale == 0.0 { + return Ok(()); + } + + let last_scale = self.last_scale.get(); + let last_x = self.last_x_offset.get(); + let last_y = self.last_y_offset.get(); + + if float_equals(scale, last_scale) + && float_equals(x_offset, last_x) + && float_equals(y_offset, last_y) + { + return Ok(()); + } + + let position = self.mouse_position(); + let modifiers = current_modifiers(); + + // Direct Manipulation reports both translation and scale in every content update. + // Translation values can shift during a pinch due to the zoom center shifting. + // We classify each gesture as either scroll or pinch and only emit one type of event. + // We allow Scroll -> Pinch (a pinch can start with a small pan) but not the reverse. + if !float_equals(scale, 1.0) { + if self.gesture_kind.get() != GestureKind::Pinch { + self.end_gesture(); + self.gesture_kind.set(GestureKind::Pinch); + self.pending_events + .borrow_mut() + .push(PlatformInput::Pinch(PinchEvent { + position, + delta: 0.0, + modifiers, + phase: TouchPhase::Started, + })); + } + } else if self.gesture_kind.get() == GestureKind::None { + self.gesture_kind.set(GestureKind::Scroll); + self.scroll_phase.set(TouchPhase::Started); + } + + match self.gesture_kind.get() { + GestureKind::Scroll => { + let dx = x_offset - last_x; + let dy = y_offset - last_y; + let touch_phase = self.scroll_phase.get(); + self.scroll_phase.set(TouchPhase::Moved); + self.pending_events + .borrow_mut() + .push(PlatformInput::ScrollWheel(ScrollWheelEvent { + position, + delta: ScrollDelta::Pixels(point(px(dx), px(dy))), + modifiers, + touch_phase, + })); + } + GestureKind::Pinch => { + let scale_delta = scale / last_scale; + self.pending_events + .borrow_mut() + .push(PlatformInput::Pinch(PinchEvent { + position, + delta: scale_delta - 1.0, + modifiers, + phase: TouchPhase::Moved, + })); + } + GestureKind::None => {} + } + + self.last_scale.set(scale); + self.last_x_offset.set(x_offset); + self.last_y_offset.set(y_offset); + + Ok(()) + } +} + +fn float_equals(f1: f32, f2: f32) -> bool { + const EPSILON_SCALE: f32 = 0.00001; + (f1 - f2).abs() < EPSILON_SCALE * f1.abs().max(f2.abs()).max(EPSILON_SCALE) +} diff --git a/crates/gpui_windows/src/events.rs b/crates/gpui_windows/src/events.rs index 985989a4c98dcaafa35661b0a496dcadf42665d3..21eb6bed899687e1c639efdc40788c229fdc4728 100644 --- a/crates/gpui_windows/src/events.rs +++ b/crates/gpui_windows/src/events.rs @@ -111,6 +111,7 @@ impl WindowsWindowInner { WM_GPUI_CURSOR_STYLE_CHANGED => self.handle_cursor_changed(lparam), WM_GPUI_FORCE_UPDATE_WINDOW => self.draw_window(handle, true), WM_GPUI_GPU_DEVICE_LOST => self.handle_device_lost(lparam), + DM_POINTERHITTEST => self.handle_dm_pointer_hit_test(wparam), _ => None, }; if let Some(n) = handled { @@ -758,6 +759,10 @@ impl WindowsWindowInner { self.state.scale_factor.set(new_scale_factor); self.state.border_offset.update(handle).log_err(); + self.state + .direct_manipulation + .set_scale_factor(new_scale_factor); + if is_maximized { // Get the monitor and its work area at the new DPI let monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONEAREST) }; @@ -1139,10 +1144,27 @@ impl WindowsWindowInner { Some(0) } + fn handle_dm_pointer_hit_test(&self, wparam: WPARAM) -> Option { + self.state.direct_manipulation.on_pointer_hit_test(wparam); + None + } + #[inline] fn draw_window(&self, handle: HWND, force_render: bool) -> Option { let mut request_frame = self.state.callbacks.request_frame.take()?; + self.state.direct_manipulation.update(); + + let events = self.state.direct_manipulation.drain_events(); + if !events.is_empty() { + if let Some(mut func) = self.state.callbacks.input.take() { + for event in events { + func(event); + } + self.state.callbacks.input.set(Some(func)); + } + } + if force_render { // Re-enable drawing after a device loss recovery. The forced render // will rebuild the scene with fresh atlas textures. diff --git a/crates/gpui_windows/src/gpui_windows.rs b/crates/gpui_windows/src/gpui_windows.rs index af7408569ab1c88fc5f433795da99354942d89f2..0af5411d20e4fbb9d326e833641a2d4e5369dcb2 100644 --- a/crates/gpui_windows/src/gpui_windows.rs +++ b/crates/gpui_windows/src/gpui_windows.rs @@ -2,6 +2,7 @@ mod clipboard; mod destination_list; +mod direct_manipulation; mod direct_write; mod directx_atlas; mod directx_devices; diff --git a/crates/gpui_windows/src/window.rs b/crates/gpui_windows/src/window.rs index 62e88c47dfc10fedf6d636e2c6d6cbdcdc2e37c5..3a55100dfb75e961f57b977297bfcd2dc2ae2701 100644 --- a/crates/gpui_windows/src/window.rs +++ b/crates/gpui_windows/src/window.rs @@ -26,6 +26,7 @@ use windows::{ core::*, }; +use crate::direct_manipulation::DirectManipulationHandler; use crate::*; use gpui::*; @@ -57,6 +58,7 @@ pub struct WindowsWindowState { pub last_reported_modifiers: Cell>, pub last_reported_capslock: Cell>, pub hovered: Cell, + pub direct_manipulation: DirectManipulationHandler, pub renderer: RefCell, @@ -131,6 +133,9 @@ impl WindowsWindowState { let fullscreen = None; let initial_placement = None; + let direct_manipulation = DirectManipulationHandler::new(hwnd, scale_factor) + .context("initializing Direct Manipulation")?; + Ok(Self { origin: Cell::new(origin), logical_size: Cell::new(logical_size), @@ -157,6 +162,7 @@ impl WindowsWindowState { initial_placement: Cell::new(initial_placement), hwnd, invalidate_devices, + direct_manipulation, }) } diff --git a/crates/image_viewer/src/image_viewer.rs b/crates/image_viewer/src/image_viewer.rs index 93729559035437f58f30abae0e5a22a7a514967a..dc8d22b67270a58155c05eaf25cb450166e8eb51 100644 --- a/crates/image_viewer/src/image_viewer.rs +++ b/crates/image_viewer/src/image_viewer.rs @@ -6,14 +6,12 @@ use std::path::Path; use anyhow::Context as _; use editor::{EditorSettings, items::entry_git_aware_label_color}; use file_icons::FileIcons; -#[cfg(any(target_os = "linux", target_os = "macos"))] -use gpui::PinchEvent; use gpui::{ AnyElement, App, Bounds, Context, DispatchPhase, Element, ElementId, Entity, EventEmitter, FocusHandle, Focusable, Font, GlobalElementId, InspectorElementId, InteractiveElement, IntoElement, LayoutId, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, - ParentElement, Pixels, Point, Render, ScrollDelta, ScrollWheelEvent, Style, Styled, Task, - WeakEntity, Window, actions, checkerboard, div, img, point, px, size, + ParentElement, PinchEvent, Pixels, Point, Render, ScrollDelta, ScrollWheelEvent, Style, Styled, + Task, WeakEntity, Window, actions, checkerboard, div, img, point, px, size, }; use language::File as _; use persistence::ImageViewerDb; @@ -263,7 +261,6 @@ impl ImageView { } } - #[cfg(any(target_os = "linux", target_os = "macos"))] fn handle_pinch(&mut self, event: &PinchEvent, _window: &mut Window, cx: &mut Context) { let zoom_factor = 1.0 + event.delta; self.set_zoom(self.zoom_level * zoom_factor, Some(event.position), cx); @@ -685,7 +682,6 @@ impl Render for ImageView { .relative() .bg(cx.theme().colors().editor_background) .child({ - #[cfg(any(target_os = "linux", target_os = "macos"))] let container = div() .id("image-container") .size_full() @@ -704,24 +700,6 @@ impl Render for ImageView { .on_mouse_move(cx.listener(Self::handle_mouse_move)) .child(ImageContentElement::new(cx.entity())); - #[cfg(not(any(target_os = "linux", target_os = "macos")))] - let container = div() - .id("image-container") - .size_full() - .overflow_hidden() - .cursor(if self.is_dragging() { - gpui::CursorStyle::ClosedHand - } else { - gpui::CursorStyle::OpenHand - }) - .on_scroll_wheel(cx.listener(Self::handle_scroll_wheel)) - .on_mouse_down(MouseButton::Left, cx.listener(Self::handle_mouse_down)) - .on_mouse_down(MouseButton::Middle, cx.listener(Self::handle_mouse_down)) - .on_mouse_up(MouseButton::Left, cx.listener(Self::handle_mouse_up)) - .on_mouse_up(MouseButton::Middle, cx.listener(Self::handle_mouse_up)) - .on_mouse_move(cx.listener(Self::handle_mouse_move)) - .child(ImageContentElement::new(cx.entity())); - container }) }