diff --git a/crates/gpui/src/platform/windows/window.rs b/crates/gpui/src/platform/windows/window.rs index fbc0b2efe5..7a093f481f 100644 --- a/crates/gpui/src/platform/windows/window.rs +++ b/crates/gpui/src/platform/windows/window.rs @@ -10,6 +10,7 @@ use std::{ rc::{Rc, Weak}, str::FromStr, sync::{Arc, Once}, + time::{Duration, Instant}, }; use ::util::ResultExt; @@ -52,6 +53,7 @@ pub(crate) struct WindowsWindowInner { hide_title_bar: bool, display: RefCell>, last_ime_input: RefCell>, + click_state: RefCell, } impl WindowsWindowInner { @@ -112,6 +114,7 @@ impl WindowsWindowInner { let callbacks = RefCell::new(Callbacks::default()); let display = RefCell::new(display); let last_ime_input = RefCell::new(None); + let click_state = RefCell::new(ClickState::new()); Self { hwnd, origin, @@ -125,6 +128,7 @@ impl WindowsWindowInner { hide_title_bar, display, last_ime_input, + click_state, } } @@ -588,12 +592,14 @@ impl WindowsWindowInner { if let Some(callback) = callbacks.input.as_mut() { let x = lparam.signed_loword() as f32; let y = lparam.signed_hiword() as f32; + let physical_point = point(GlobalPixels(x), GlobalPixels(y)); + let click_count = self.click_state.borrow_mut().update(button, physical_point); let scale_factor = self.scale_factor.get(); let event = MouseDownEvent { button, position: logical_point(x, y, scale_factor), modifiers: self.current_modifiers(), - click_count: 1, + click_count, first_mouse: false, }; if callback(PlatformInput::MouseDown(event)).default_prevented { @@ -1010,12 +1016,17 @@ impl WindowsWindowInner { y: lparam.signed_hiword().into(), }; unsafe { ScreenToClient(self.hwnd, &mut cursor_point) }; + let physical_point = point( + GlobalPixels(cursor_point.x as f32), + GlobalPixels(cursor_point.y as f32), + ); + let click_count = self.click_state.borrow_mut().update(button, physical_point); let scale_factor = self.scale_factor.get(); let event = MouseDownEvent { button, position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor), modifiers: self.current_modifiers(), - click_count: 1, + click_count, first_mouse: false, }; if callback(PlatformInput::MouseDown(event)).default_prevented { @@ -1595,6 +1606,48 @@ impl IDropTarget_Impl for WindowsDragDropHandler { } } +#[derive(Debug)] +struct ClickState { + button: MouseButton, + last_click: Instant, + last_position: Point, + current_count: usize, +} + +impl ClickState { + pub fn new() -> Self { + ClickState { + button: MouseButton::Left, + last_click: Instant::now(), + last_position: Point::default(), + current_count: 0, + } + } + + /// update self and return the needed click count + pub fn update(&mut self, button: MouseButton, new_position: Point) -> usize { + if self.button == button && self.is_double_click(new_position) { + self.current_count += 1; + } else { + self.current_count = 1; + } + self.last_click = Instant::now(); + self.last_position = new_position; + self.button = button; + + self.current_count + } + + #[inline] + fn is_double_click(&self, new_position: Point) -> bool { + let diff = self.last_position - new_position; + + self.last_click.elapsed() < DOUBLE_CLICK_INTERVAL + && diff.x.0.abs() <= DOUBLE_CLICK_SPATIAL_TOLERANCE + && diff.y.0.abs() <= DOUBLE_CLICK_SPATIAL_TOLERANCE + } +} + fn register_wnd_class(icon_handle: HICON) -> PCWSTR { const CLASS_NAME: PCWSTR = w!("Zed::Window"); @@ -1739,3 +1792,88 @@ fn logical_point(x: f32, y: f32, scale_factor: f32) -> Point { // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF; +// https://learn.microsoft.com/en-us/windows/win32/controls/ttm-setdelaytime?redirectedfrom=MSDN +const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(500); +// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics +const DOUBLE_CLICK_SPATIAL_TOLERANCE: f32 = 4.0; + +#[cfg(test)] +mod tests { + use super::ClickState; + use crate::{point, GlobalPixels, MouseButton}; + use std::time::Duration; + + #[test] + fn test_double_click_interval() { + let mut state = ClickState::new(); + assert_eq!( + state.update( + MouseButton::Left, + point(GlobalPixels(0.0), GlobalPixels(0.0)) + ), + 1 + ); + assert_eq!( + state.update( + MouseButton::Right, + point(GlobalPixels(0.0), GlobalPixels(0.0)) + ), + 1 + ); + assert_eq!( + state.update( + MouseButton::Left, + point(GlobalPixels(0.0), GlobalPixels(0.0)) + ), + 1 + ); + assert_eq!( + state.update( + MouseButton::Left, + point(GlobalPixels(0.0), GlobalPixels(0.0)) + ), + 2 + ); + state.last_click -= Duration::from_millis(700); + assert_eq!( + state.update( + MouseButton::Left, + point(GlobalPixels(0.0), GlobalPixels(0.0)) + ), + 1 + ); + } + + #[test] + fn test_double_click_spatial_tolerance() { + let mut state = ClickState::new(); + assert_eq!( + state.update( + MouseButton::Left, + point(GlobalPixels(-3.0), GlobalPixels(0.0)) + ), + 1 + ); + assert_eq!( + state.update( + MouseButton::Left, + point(GlobalPixels(0.0), GlobalPixels(3.0)) + ), + 2 + ); + assert_eq!( + state.update( + MouseButton::Right, + point(GlobalPixels(3.0), GlobalPixels(2.0)) + ), + 1 + ); + assert_eq!( + state.update( + MouseButton::Right, + point(GlobalPixels(10.0), GlobalPixels(0.0)) + ), + 1 + ); + } +}