1#![deny(unsafe_op_in_unsafe_fn)]
2
3use std::{
4 cell::RefCell,
5 num::NonZeroIsize,
6 path::PathBuf,
7 rc::{Rc, Weak},
8 str::FromStr,
9 sync::{Arc, Once, atomic::AtomicBool},
10 time::{Duration, Instant},
11};
12
13use ::util::ResultExt;
14use anyhow::{Context as _, Result};
15use futures::channel::oneshot::{self, Receiver};
16use raw_window_handle as rwh;
17use smallvec::SmallVec;
18use windows::{
19 Win32::{
20 Foundation::*,
21 Graphics::Gdi::*,
22 System::{Com::*, LibraryLoader::*, Ole::*, SystemServices::*},
23 UI::{Controls::*, HiDpi::*, Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
24 },
25 core::*,
26};
27
28use crate::*;
29
30pub(crate) struct WindowsWindow(pub Rc<WindowsWindowInner>);
31
32pub struct WindowsWindowState {
33 pub origin: Point<Pixels>,
34 pub logical_size: Size<Pixels>,
35 pub min_size: Option<Size<Pixels>>,
36 pub fullscreen_restore_bounds: Bounds<Pixels>,
37 pub border_offset: WindowBorderOffset,
38 pub appearance: WindowAppearance,
39 pub scale_factor: f32,
40 pub restore_from_minimized: Option<Box<dyn FnMut(RequestFrameOptions)>>,
41
42 pub callbacks: Callbacks,
43 pub input_handler: Option<PlatformInputHandler>,
44 pub pending_surrogate: Option<u16>,
45 pub last_reported_modifiers: Option<Modifiers>,
46 pub last_reported_capslock: Option<Capslock>,
47 pub hovered: bool,
48
49 pub renderer: DirectXRenderer,
50
51 pub click_state: ClickState,
52 pub current_cursor: Option<HCURSOR>,
53 pub nc_button_pressed: Option<u32>,
54
55 pub display: WindowsDisplay,
56 /// Flag to instruct the `VSyncProvider` thread to invalidate the directx devices
57 /// as resizing them has failed, causing us to have lost at least the render target.
58 pub invalidate_devices: Arc<AtomicBool>,
59 fullscreen: Option<StyleAndBounds>,
60 initial_placement: Option<WindowOpenStatus>,
61 hwnd: HWND,
62}
63
64pub(crate) struct WindowsWindowInner {
65 hwnd: HWND,
66 drop_target_helper: IDropTargetHelper,
67 pub(crate) state: RefCell<WindowsWindowState>,
68 system_settings: RefCell<WindowsSystemSettings>,
69 pub(crate) handle: AnyWindowHandle,
70 pub(crate) hide_title_bar: bool,
71 pub(crate) is_movable: bool,
72 pub(crate) executor: ForegroundExecutor,
73 pub(crate) windows_version: WindowsVersion,
74 pub(crate) validation_number: usize,
75 pub(crate) main_receiver: flume::Receiver<RunnableVariant>,
76 pub(crate) platform_window_handle: HWND,
77}
78
79impl WindowsWindowState {
80 fn new(
81 hwnd: HWND,
82 directx_devices: &DirectXDevices,
83 window_params: &CREATESTRUCTW,
84 current_cursor: Option<HCURSOR>,
85 display: WindowsDisplay,
86 min_size: Option<Size<Pixels>>,
87 appearance: WindowAppearance,
88 disable_direct_composition: bool,
89 invalidate_devices: Arc<AtomicBool>,
90 ) -> Result<Self> {
91 let scale_factor = {
92 let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32;
93 monitor_dpi / USER_DEFAULT_SCREEN_DPI as f32
94 };
95 let origin = logical_point(window_params.x as f32, window_params.y as f32, scale_factor);
96 let logical_size = {
97 let physical_size = size(
98 DevicePixels(window_params.cx),
99 DevicePixels(window_params.cy),
100 );
101 physical_size.to_pixels(scale_factor)
102 };
103 let fullscreen_restore_bounds = Bounds {
104 origin,
105 size: logical_size,
106 };
107 let border_offset = WindowBorderOffset::default();
108 let restore_from_minimized = None;
109 let renderer = DirectXRenderer::new(hwnd, directx_devices, disable_direct_composition)
110 .context("Creating DirectX renderer")?;
111 let callbacks = Callbacks::default();
112 let input_handler = None;
113 let pending_surrogate = None;
114 let last_reported_modifiers = None;
115 let last_reported_capslock = None;
116 let hovered = false;
117 let click_state = ClickState::new();
118 let nc_button_pressed = None;
119 let fullscreen = None;
120 let initial_placement = None;
121
122 Ok(Self {
123 origin,
124 logical_size,
125 fullscreen_restore_bounds,
126 border_offset,
127 appearance,
128 scale_factor,
129 restore_from_minimized,
130 min_size,
131 callbacks,
132 input_handler,
133 pending_surrogate,
134 last_reported_modifiers,
135 last_reported_capslock,
136 hovered,
137 renderer,
138 click_state,
139 current_cursor,
140 nc_button_pressed,
141 display,
142 fullscreen,
143 initial_placement,
144 hwnd,
145 invalidate_devices,
146 })
147 }
148
149 #[inline]
150 pub(crate) fn is_fullscreen(&self) -> bool {
151 self.fullscreen.is_some()
152 }
153
154 pub(crate) fn is_maximized(&self) -> bool {
155 !self.is_fullscreen() && unsafe { IsZoomed(self.hwnd) }.as_bool()
156 }
157
158 fn bounds(&self) -> Bounds<Pixels> {
159 Bounds {
160 origin: self.origin,
161 size: self.logical_size,
162 }
163 }
164
165 // Calculate the bounds used for saving and whether the window is maximized.
166 fn calculate_window_bounds(&self) -> (Bounds<Pixels>, bool) {
167 let placement = unsafe {
168 let mut placement = WINDOWPLACEMENT {
169 length: std::mem::size_of::<WINDOWPLACEMENT>() as u32,
170 ..Default::default()
171 };
172 GetWindowPlacement(self.hwnd, &mut placement)
173 .context("failed to get window placement")
174 .log_err();
175 placement
176 };
177 (
178 calculate_client_rect(
179 placement.rcNormalPosition,
180 self.border_offset,
181 self.scale_factor,
182 ),
183 placement.showCmd == SW_SHOWMAXIMIZED.0 as u32,
184 )
185 }
186
187 fn window_bounds(&self) -> WindowBounds {
188 let (bounds, maximized) = self.calculate_window_bounds();
189
190 if self.is_fullscreen() {
191 WindowBounds::Fullscreen(self.fullscreen_restore_bounds)
192 } else if maximized {
193 WindowBounds::Maximized(bounds)
194 } else {
195 WindowBounds::Windowed(bounds)
196 }
197 }
198
199 /// get the logical size of the app's drawable area.
200 ///
201 /// Currently, GPUI uses the logical size of the app to handle mouse interactions (such as
202 /// whether the mouse collides with other elements of GPUI).
203 fn content_size(&self) -> Size<Pixels> {
204 self.logical_size
205 }
206}
207
208impl WindowsWindowInner {
209 fn new(context: &mut WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result<Rc<Self>> {
210 let state = RefCell::new(WindowsWindowState::new(
211 hwnd,
212 &context.directx_devices,
213 cs,
214 context.current_cursor,
215 context.display,
216 context.min_size,
217 context.appearance,
218 context.disable_direct_composition,
219 context.invalidate_devices.clone(),
220 )?);
221
222 Ok(Rc::new(Self {
223 hwnd,
224 drop_target_helper: context.drop_target_helper.clone(),
225 state,
226 handle: context.handle,
227 hide_title_bar: context.hide_title_bar,
228 is_movable: context.is_movable,
229 executor: context.executor.clone(),
230 windows_version: context.windows_version,
231 validation_number: context.validation_number,
232 main_receiver: context.main_receiver.clone(),
233 platform_window_handle: context.platform_window_handle,
234 system_settings: RefCell::new(WindowsSystemSettings::new(context.display)),
235 }))
236 }
237
238 fn toggle_fullscreen(self: &Rc<Self>) {
239 let this = self.clone();
240 self.executor
241 .spawn(async move {
242 let mut lock = this.state.borrow_mut();
243 let StyleAndBounds {
244 style,
245 x,
246 y,
247 cx,
248 cy,
249 } = match lock.fullscreen.take() {
250 Some(state) => state,
251 None => {
252 let (window_bounds, _) = lock.calculate_window_bounds();
253 lock.fullscreen_restore_bounds = window_bounds;
254 drop(lock);
255
256 let style =
257 WINDOW_STYLE(unsafe { get_window_long(this.hwnd, GWL_STYLE) } as _);
258 let mut rc = RECT::default();
259 unsafe { GetWindowRect(this.hwnd, &mut rc) }
260 .context("failed to get window rect")
261 .log_err();
262
263 lock = this.state.borrow_mut();
264 let _ = lock.fullscreen.insert(StyleAndBounds {
265 style,
266 x: rc.left,
267 y: rc.top,
268 cx: rc.right - rc.left,
269 cy: rc.bottom - rc.top,
270 });
271 let style = style
272 & !(WS_THICKFRAME
273 | WS_SYSMENU
274 | WS_MAXIMIZEBOX
275 | WS_MINIMIZEBOX
276 | WS_CAPTION);
277 let physical_bounds = lock.display.physical_bounds();
278 StyleAndBounds {
279 style,
280 x: physical_bounds.left().0,
281 y: physical_bounds.top().0,
282 cx: physical_bounds.size.width.0,
283 cy: physical_bounds.size.height.0,
284 }
285 }
286 };
287 drop(lock);
288 unsafe { set_window_long(this.hwnd, GWL_STYLE, style.0 as isize) };
289 unsafe {
290 SetWindowPos(
291 this.hwnd,
292 None,
293 x,
294 y,
295 cx,
296 cy,
297 SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOZORDER,
298 )
299 }
300 .log_err();
301 })
302 .detach();
303 }
304
305 fn set_window_placement(self: &Rc<Self>) -> Result<()> {
306 let Some(open_status) = self.state.borrow_mut().initial_placement.take() else {
307 return Ok(());
308 };
309 match open_status.state {
310 WindowOpenState::Maximized => unsafe {
311 SetWindowPlacement(self.hwnd, &open_status.placement)
312 .context("failed to set window placement")?;
313 ShowWindowAsync(self.hwnd, SW_MAXIMIZE).ok()?;
314 },
315 WindowOpenState::Fullscreen => {
316 unsafe {
317 SetWindowPlacement(self.hwnd, &open_status.placement)
318 .context("failed to set window placement")?
319 };
320 self.toggle_fullscreen();
321 }
322 WindowOpenState::Windowed => unsafe {
323 SetWindowPlacement(self.hwnd, &open_status.placement)
324 .context("failed to set window placement")?;
325 },
326 }
327 Ok(())
328 }
329
330 pub(crate) fn system_settings(&self) -> std::cell::Ref<'_, WindowsSystemSettings> {
331 self.system_settings.borrow()
332 }
333
334 pub(crate) fn system_settings_mut(&self) -> std::cell::RefMut<'_, WindowsSystemSettings> {
335 self.system_settings.borrow_mut()
336 }
337}
338
339#[derive(Default)]
340pub(crate) struct Callbacks {
341 pub(crate) request_frame: Option<Box<dyn FnMut(RequestFrameOptions)>>,
342 pub(crate) input: Option<Box<dyn FnMut(crate::PlatformInput) -> DispatchEventResult>>,
343 pub(crate) active_status_change: Option<Box<dyn FnMut(bool)>>,
344 pub(crate) hovered_status_change: Option<Box<dyn FnMut(bool)>>,
345 pub(crate) resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
346 pub(crate) moved: Option<Box<dyn FnMut()>>,
347 pub(crate) should_close: Option<Box<dyn FnMut() -> bool>>,
348 pub(crate) close: Option<Box<dyn FnOnce()>>,
349 pub(crate) hit_test_window_control: Option<Box<dyn FnMut() -> Option<WindowControlArea>>>,
350 pub(crate) appearance_changed: Option<Box<dyn FnMut()>>,
351}
352
353struct WindowCreateContext {
354 inner: Option<Result<Rc<WindowsWindowInner>>>,
355 handle: AnyWindowHandle,
356 hide_title_bar: bool,
357 display: WindowsDisplay,
358 is_movable: bool,
359 min_size: Option<Size<Pixels>>,
360 executor: ForegroundExecutor,
361 current_cursor: Option<HCURSOR>,
362 windows_version: WindowsVersion,
363 drop_target_helper: IDropTargetHelper,
364 validation_number: usize,
365 main_receiver: flume::Receiver<RunnableVariant>,
366 platform_window_handle: HWND,
367 appearance: WindowAppearance,
368 disable_direct_composition: bool,
369 directx_devices: DirectXDevices,
370 invalidate_devices: Arc<AtomicBool>,
371}
372
373impl WindowsWindow {
374 pub(crate) fn new(
375 handle: AnyWindowHandle,
376 params: WindowParams,
377 creation_info: WindowCreationInfo,
378 ) -> Result<Self> {
379 let WindowCreationInfo {
380 icon,
381 executor,
382 current_cursor,
383 windows_version,
384 drop_target_helper,
385 validation_number,
386 main_receiver,
387 platform_window_handle,
388 disable_direct_composition,
389 directx_devices,
390 invalidate_devices,
391 } = creation_info;
392 register_window_class(icon);
393 let hide_title_bar = params
394 .titlebar
395 .as_ref()
396 .map(|titlebar| titlebar.appears_transparent)
397 .unwrap_or(true);
398 let window_name = HSTRING::from(
399 params
400 .titlebar
401 .as_ref()
402 .and_then(|titlebar| titlebar.title.as_ref())
403 .map(|title| title.as_ref())
404 .unwrap_or(""),
405 );
406
407 let (mut dwexstyle, dwstyle) = if params.kind == WindowKind::PopUp {
408 (WS_EX_TOOLWINDOW, WINDOW_STYLE(0x0))
409 } else {
410 let mut dwstyle = WS_SYSMENU;
411
412 if params.is_resizable {
413 dwstyle |= WS_THICKFRAME | WS_MAXIMIZEBOX;
414 }
415
416 if params.is_minimizable {
417 dwstyle |= WS_MINIMIZEBOX;
418 }
419
420 (WS_EX_APPWINDOW, dwstyle)
421 };
422 if !disable_direct_composition {
423 dwexstyle |= WS_EX_NOREDIRECTIONBITMAP;
424 }
425
426 let hinstance = get_module_handle();
427 let display = if let Some(display_id) = params.display_id {
428 // if we obtain a display_id, then this ID must be valid.
429 WindowsDisplay::new(display_id).unwrap()
430 } else {
431 WindowsDisplay::primary_monitor().unwrap()
432 };
433 let appearance = system_appearance().unwrap_or_default();
434 let mut context = WindowCreateContext {
435 inner: None,
436 handle,
437 hide_title_bar,
438 display,
439 is_movable: params.is_movable,
440 min_size: params.window_min_size,
441 executor,
442 current_cursor,
443 windows_version,
444 drop_target_helper,
445 validation_number,
446 main_receiver,
447 platform_window_handle,
448 appearance,
449 disable_direct_composition,
450 directx_devices,
451 invalidate_devices,
452 };
453 let creation_result = unsafe {
454 CreateWindowExW(
455 dwexstyle,
456 WINDOW_CLASS_NAME,
457 &window_name,
458 dwstyle,
459 CW_USEDEFAULT,
460 CW_USEDEFAULT,
461 CW_USEDEFAULT,
462 CW_USEDEFAULT,
463 None,
464 None,
465 Some(hinstance.into()),
466 Some(&context as *const _ as *const _),
467 )
468 };
469
470 // Failure to create a `WindowsWindowState` can cause window creation to fail,
471 // so check the inner result first.
472 let this = context.inner.take().transpose()?;
473 let hwnd = creation_result?;
474 let this = this.unwrap();
475
476 register_drag_drop(&this)?;
477 configure_dwm_dark_mode(hwnd, appearance);
478 this.state.borrow_mut().border_offset.update(hwnd)?;
479 let placement = retrieve_window_placement(
480 hwnd,
481 display,
482 params.bounds,
483 this.state.borrow().scale_factor,
484 this.state.borrow().border_offset,
485 )?;
486 if params.show {
487 unsafe { SetWindowPlacement(hwnd, &placement)? };
488 } else {
489 this.state.borrow_mut().initial_placement = Some(WindowOpenStatus {
490 placement,
491 state: WindowOpenState::Windowed,
492 });
493 }
494
495 Ok(Self(this))
496 }
497}
498
499impl rwh::HasWindowHandle for WindowsWindow {
500 fn window_handle(&self) -> std::result::Result<rwh::WindowHandle<'_>, rwh::HandleError> {
501 let raw = rwh::Win32WindowHandle::new(unsafe {
502 NonZeroIsize::new_unchecked(self.0.hwnd.0 as isize)
503 })
504 .into();
505 Ok(unsafe { rwh::WindowHandle::borrow_raw(raw) })
506 }
507}
508
509// todo(windows)
510impl rwh::HasDisplayHandle for WindowsWindow {
511 fn display_handle(&self) -> std::result::Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
512 unimplemented!()
513 }
514}
515
516impl Drop for WindowsWindow {
517 fn drop(&mut self) {
518 // clone this `Rc` to prevent early release of the pointer
519 let this = self.0.clone();
520 self.0
521 .executor
522 .spawn(async move {
523 let handle = this.hwnd;
524 unsafe {
525 RevokeDragDrop(handle).log_err();
526 DestroyWindow(handle).log_err();
527 }
528 })
529 .detach();
530 }
531}
532
533impl PlatformWindow for WindowsWindow {
534 fn bounds(&self) -> Bounds<Pixels> {
535 self.0.state.borrow().bounds()
536 }
537
538 fn is_maximized(&self) -> bool {
539 self.0.state.borrow().is_maximized()
540 }
541
542 fn window_bounds(&self) -> WindowBounds {
543 self.0.state.borrow().window_bounds()
544 }
545
546 /// get the logical size of the app's drawable area.
547 ///
548 /// Currently, GPUI uses the logical size of the app to handle mouse interactions (such as
549 /// whether the mouse collides with other elements of GPUI).
550 fn content_size(&self) -> Size<Pixels> {
551 self.0.state.borrow().content_size()
552 }
553
554 fn resize(&mut self, size: Size<Pixels>) {
555 let hwnd = self.0.hwnd;
556 let bounds =
557 crate::bounds(self.bounds().origin, size).to_device_pixels(self.scale_factor());
558 let rect = calculate_window_rect(bounds, self.0.state.borrow().border_offset);
559
560 self.0
561 .executor
562 .spawn(async move {
563 unsafe {
564 SetWindowPos(
565 hwnd,
566 None,
567 bounds.origin.x.0,
568 bounds.origin.y.0,
569 rect.right - rect.left,
570 rect.bottom - rect.top,
571 SWP_NOMOVE,
572 )
573 .context("unable to set window content size")
574 .log_err();
575 }
576 })
577 .detach();
578 }
579
580 fn scale_factor(&self) -> f32 {
581 self.0.state.borrow().scale_factor
582 }
583
584 fn appearance(&self) -> WindowAppearance {
585 self.0.state.borrow().appearance
586 }
587
588 fn display(&self) -> Option<Rc<dyn PlatformDisplay>> {
589 Some(Rc::new(self.0.state.borrow().display))
590 }
591
592 fn mouse_position(&self) -> Point<Pixels> {
593 let scale_factor = self.scale_factor();
594 let point = unsafe {
595 let mut point: POINT = std::mem::zeroed();
596 GetCursorPos(&mut point)
597 .context("unable to get cursor position")
598 .log_err();
599 ScreenToClient(self.0.hwnd, &mut point).ok().log_err();
600 point
601 };
602 logical_point(point.x as f32, point.y as f32, scale_factor)
603 }
604
605 fn modifiers(&self) -> Modifiers {
606 current_modifiers()
607 }
608
609 fn capslock(&self) -> Capslock {
610 current_capslock()
611 }
612
613 fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
614 self.0.state.borrow_mut().input_handler = Some(input_handler);
615 }
616
617 fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
618 self.0.state.borrow_mut().input_handler.take()
619 }
620
621 fn prompt(
622 &self,
623 level: PromptLevel,
624 msg: &str,
625 detail: Option<&str>,
626 answers: &[PromptButton],
627 ) -> Option<Receiver<usize>> {
628 let (done_tx, done_rx) = oneshot::channel();
629 let msg = msg.to_string();
630 let detail_string = detail.map(|detail| detail.to_string());
631 let handle = self.0.hwnd;
632 let answers = answers.to_vec();
633 self.0
634 .executor
635 .spawn(async move {
636 unsafe {
637 let mut config = TASKDIALOGCONFIG::default();
638 config.cbSize = std::mem::size_of::<TASKDIALOGCONFIG>() as _;
639 config.hwndParent = handle;
640 let title;
641 let main_icon;
642 match level {
643 crate::PromptLevel::Info => {
644 title = windows::core::w!("Info");
645 main_icon = TD_INFORMATION_ICON;
646 }
647 crate::PromptLevel::Warning => {
648 title = windows::core::w!("Warning");
649 main_icon = TD_WARNING_ICON;
650 }
651 crate::PromptLevel::Critical => {
652 title = windows::core::w!("Critical");
653 main_icon = TD_ERROR_ICON;
654 }
655 };
656 config.pszWindowTitle = title;
657 config.Anonymous1.pszMainIcon = main_icon;
658 let instruction = HSTRING::from(msg);
659 config.pszMainInstruction = PCWSTR::from_raw(instruction.as_ptr());
660 let hints_encoded;
661 if let Some(ref hints) = detail_string {
662 hints_encoded = HSTRING::from(hints);
663 config.pszContent = PCWSTR::from_raw(hints_encoded.as_ptr());
664 };
665 let mut button_id_map = Vec::with_capacity(answers.len());
666 let mut buttons = Vec::new();
667 let mut btn_encoded = Vec::new();
668 for (index, btn) in answers.iter().enumerate() {
669 let encoded = HSTRING::from(btn.label().as_ref());
670 let button_id = match btn {
671 PromptButton::Ok(_) => IDOK.0,
672 PromptButton::Cancel(_) => IDCANCEL.0,
673 // the first few low integer values are reserved for known buttons
674 // so for simplicity we just go backwards from -1
675 PromptButton::Other(_) => -(index as i32) - 1,
676 };
677 button_id_map.push(button_id);
678 buttons.push(TASKDIALOG_BUTTON {
679 nButtonID: button_id,
680 pszButtonText: PCWSTR::from_raw(encoded.as_ptr()),
681 });
682 btn_encoded.push(encoded);
683 }
684 config.cButtons = buttons.len() as _;
685 config.pButtons = buttons.as_ptr();
686
687 config.pfCallback = None;
688 let mut res = std::mem::zeroed();
689 let _ = TaskDialogIndirect(&config, Some(&mut res), None, None)
690 .context("unable to create task dialog")
691 .log_err();
692
693 if let Some(clicked) =
694 button_id_map.iter().position(|&button_id| button_id == res)
695 {
696 let _ = done_tx.send(clicked);
697 }
698 }
699 })
700 .detach();
701
702 Some(done_rx)
703 }
704
705 fn activate(&self) {
706 let hwnd = self.0.hwnd;
707 let this = self.0.clone();
708 self.0
709 .executor
710 .spawn(async move {
711 this.set_window_placement().log_err();
712
713 unsafe {
714 // If the window is minimized, restore it.
715 if IsIconic(hwnd).as_bool() {
716 ShowWindowAsync(hwnd, SW_RESTORE).ok().log_err();
717 }
718
719 SetActiveWindow(hwnd).log_err();
720 SetFocus(Some(hwnd)).log_err();
721 }
722
723 // premium ragebait by windows, this is needed because the window
724 // must have received an input event to be able to set itself to foreground
725 // so let's just simulate user input as that seems to be the most reliable way
726 // some more info: https://gist.github.com/Aetopia/1581b40f00cc0cadc93a0e8ccb65dc8c
727 // bonus: this bug also doesn't manifest if you have vs attached to the process
728 let inputs = [
729 INPUT {
730 r#type: INPUT_KEYBOARD,
731 Anonymous: INPUT_0 {
732 ki: KEYBDINPUT {
733 wVk: VK_MENU,
734 dwFlags: KEYBD_EVENT_FLAGS(0),
735 ..Default::default()
736 },
737 },
738 },
739 INPUT {
740 r#type: INPUT_KEYBOARD,
741 Anonymous: INPUT_0 {
742 ki: KEYBDINPUT {
743 wVk: VK_MENU,
744 dwFlags: KEYEVENTF_KEYUP,
745 ..Default::default()
746 },
747 },
748 },
749 ];
750 unsafe { SendInput(&inputs, std::mem::size_of::<INPUT>() as i32) };
751
752 // todo(windows)
753 // crate `windows 0.56` reports true as Err
754 unsafe { SetForegroundWindow(hwnd).as_bool() };
755 })
756 .detach();
757 }
758
759 fn is_active(&self) -> bool {
760 self.0.hwnd == unsafe { GetActiveWindow() }
761 }
762
763 fn is_hovered(&self) -> bool {
764 self.0.state.borrow().hovered
765 }
766
767 fn set_title(&mut self, title: &str) {
768 unsafe { SetWindowTextW(self.0.hwnd, &HSTRING::from(title)) }
769 .inspect_err(|e| log::error!("Set title failed: {e}"))
770 .ok();
771 }
772
773 fn set_background_appearance(&self, background_appearance: WindowBackgroundAppearance) {
774 let hwnd = self.0.hwnd;
775
776 match background_appearance {
777 WindowBackgroundAppearance::Opaque => {
778 // ACCENT_DISABLED
779 set_window_composition_attribute(hwnd, None, 0);
780 }
781 WindowBackgroundAppearance::Transparent => {
782 // Use ACCENT_ENABLE_TRANSPARENTGRADIENT for transparent background
783 set_window_composition_attribute(hwnd, None, 2);
784 }
785 WindowBackgroundAppearance::Blurred => {
786 // Enable acrylic blur
787 // ACCENT_ENABLE_ACRYLICBLURBEHIND
788 set_window_composition_attribute(hwnd, Some((0, 0, 0, 0)), 4);
789 }
790 }
791 }
792
793 fn minimize(&self) {
794 unsafe { ShowWindowAsync(self.0.hwnd, SW_MINIMIZE).ok().log_err() };
795 }
796
797 fn zoom(&self) {
798 unsafe {
799 if IsWindowVisible(self.0.hwnd).as_bool() {
800 ShowWindowAsync(self.0.hwnd, SW_MAXIMIZE).ok().log_err();
801 } else if let Some(status) = self.0.state.borrow_mut().initial_placement.as_mut() {
802 status.state = WindowOpenState::Maximized;
803 }
804 }
805 }
806
807 fn toggle_fullscreen(&self) {
808 if unsafe { IsWindowVisible(self.0.hwnd).as_bool() } {
809 self.0.toggle_fullscreen();
810 } else if let Some(status) = self.0.state.borrow_mut().initial_placement.as_mut() {
811 status.state = WindowOpenState::Fullscreen;
812 }
813 }
814
815 fn is_fullscreen(&self) -> bool {
816 self.0.state.borrow().is_fullscreen()
817 }
818
819 fn on_request_frame(&self, callback: Box<dyn FnMut(RequestFrameOptions)>) {
820 self.0.state.borrow_mut().callbacks.request_frame = Some(callback);
821 }
822
823 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>) {
824 self.0.state.borrow_mut().callbacks.input = Some(callback);
825 }
826
827 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
828 self.0.state.borrow_mut().callbacks.active_status_change = Some(callback);
829 }
830
831 fn on_hover_status_change(&self, callback: Box<dyn FnMut(bool)>) {
832 self.0.state.borrow_mut().callbacks.hovered_status_change = Some(callback);
833 }
834
835 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
836 self.0.state.borrow_mut().callbacks.resize = Some(callback);
837 }
838
839 fn on_moved(&self, callback: Box<dyn FnMut()>) {
840 self.0.state.borrow_mut().callbacks.moved = Some(callback);
841 }
842
843 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
844 self.0.state.borrow_mut().callbacks.should_close = Some(callback);
845 }
846
847 fn on_close(&self, callback: Box<dyn FnOnce()>) {
848 self.0.state.borrow_mut().callbacks.close = Some(callback);
849 }
850
851 fn on_hit_test_window_control(&self, callback: Box<dyn FnMut() -> Option<WindowControlArea>>) {
852 self.0.state.borrow_mut().callbacks.hit_test_window_control = Some(callback);
853 }
854
855 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
856 self.0.state.borrow_mut().callbacks.appearance_changed = Some(callback);
857 }
858
859 fn draw(&self, scene: &Scene) {
860 self.0.state.borrow_mut().renderer.draw(scene).log_err();
861 }
862
863 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
864 self.0.state.borrow().renderer.sprite_atlas()
865 }
866
867 fn get_raw_handle(&self) -> HWND {
868 self.0.hwnd
869 }
870
871 fn gpu_specs(&self) -> Option<GpuSpecs> {
872 self.0.state.borrow().renderer.gpu_specs().log_err()
873 }
874
875 fn update_ime_position(&self, _bounds: Bounds<Pixels>) {
876 // There is no such thing on Windows.
877 }
878}
879
880#[implement(IDropTarget)]
881struct WindowsDragDropHandler(pub Rc<WindowsWindowInner>);
882
883impl WindowsDragDropHandler {
884 fn handle_drag_drop(&self, input: PlatformInput) {
885 let mut lock = self.0.state.borrow_mut();
886 if let Some(mut func) = lock.callbacks.input.take() {
887 drop(lock);
888 func(input);
889 self.0.state.borrow_mut().callbacks.input = Some(func);
890 }
891 }
892}
893
894#[allow(non_snake_case)]
895impl IDropTarget_Impl for WindowsDragDropHandler_Impl {
896 fn DragEnter(
897 &self,
898 pdataobj: windows::core::Ref<IDataObject>,
899 _grfkeystate: MODIFIERKEYS_FLAGS,
900 pt: &POINTL,
901 pdweffect: *mut DROPEFFECT,
902 ) -> windows::core::Result<()> {
903 unsafe {
904 let idata_obj = pdataobj.ok()?;
905 let config = FORMATETC {
906 cfFormat: CF_HDROP.0,
907 ptd: std::ptr::null_mut() as _,
908 dwAspect: DVASPECT_CONTENT.0,
909 lindex: -1,
910 tymed: TYMED_HGLOBAL.0 as _,
911 };
912 let cursor_position = POINT { x: pt.x, y: pt.y };
913 if idata_obj.QueryGetData(&config as _) == S_OK {
914 *pdweffect = DROPEFFECT_COPY;
915 let Some(mut idata) = idata_obj.GetData(&config as _).log_err() else {
916 return Ok(());
917 };
918 if idata.u.hGlobal.is_invalid() {
919 return Ok(());
920 }
921 let hdrop = HDROP(idata.u.hGlobal.0);
922 let mut paths = SmallVec::<[PathBuf; 2]>::new();
923 with_file_names(hdrop, |file_name| {
924 if let Some(path) = PathBuf::from_str(&file_name).log_err() {
925 paths.push(path);
926 }
927 });
928 ReleaseStgMedium(&mut idata);
929 let mut cursor_position = cursor_position;
930 ScreenToClient(self.0.hwnd, &mut cursor_position)
931 .ok()
932 .log_err();
933 let scale_factor = self.0.state.borrow().scale_factor;
934 let input = PlatformInput::FileDrop(FileDropEvent::Entered {
935 position: logical_point(
936 cursor_position.x as f32,
937 cursor_position.y as f32,
938 scale_factor,
939 ),
940 paths: ExternalPaths(paths),
941 });
942 self.handle_drag_drop(input);
943 } else {
944 *pdweffect = DROPEFFECT_NONE;
945 }
946 self.0
947 .drop_target_helper
948 .DragEnter(self.0.hwnd, idata_obj, &cursor_position, *pdweffect)
949 .log_err();
950 }
951 Ok(())
952 }
953
954 fn DragOver(
955 &self,
956 _grfkeystate: MODIFIERKEYS_FLAGS,
957 pt: &POINTL,
958 pdweffect: *mut DROPEFFECT,
959 ) -> windows::core::Result<()> {
960 let mut cursor_position = POINT { x: pt.x, y: pt.y };
961 unsafe {
962 *pdweffect = DROPEFFECT_COPY;
963 self.0
964 .drop_target_helper
965 .DragOver(&cursor_position, *pdweffect)
966 .log_err();
967 ScreenToClient(self.0.hwnd, &mut cursor_position)
968 .ok()
969 .log_err();
970 }
971 let scale_factor = self.0.state.borrow().scale_factor;
972 let input = PlatformInput::FileDrop(FileDropEvent::Pending {
973 position: logical_point(
974 cursor_position.x as f32,
975 cursor_position.y as f32,
976 scale_factor,
977 ),
978 });
979 self.handle_drag_drop(input);
980
981 Ok(())
982 }
983
984 fn DragLeave(&self) -> windows::core::Result<()> {
985 unsafe {
986 self.0.drop_target_helper.DragLeave().log_err();
987 }
988 let input = PlatformInput::FileDrop(FileDropEvent::Exited);
989 self.handle_drag_drop(input);
990
991 Ok(())
992 }
993
994 fn Drop(
995 &self,
996 pdataobj: windows::core::Ref<IDataObject>,
997 _grfkeystate: MODIFIERKEYS_FLAGS,
998 pt: &POINTL,
999 pdweffect: *mut DROPEFFECT,
1000 ) -> windows::core::Result<()> {
1001 let idata_obj = pdataobj.ok()?;
1002 let mut cursor_position = POINT { x: pt.x, y: pt.y };
1003 unsafe {
1004 *pdweffect = DROPEFFECT_COPY;
1005 self.0
1006 .drop_target_helper
1007 .Drop(idata_obj, &cursor_position, *pdweffect)
1008 .log_err();
1009 ScreenToClient(self.0.hwnd, &mut cursor_position)
1010 .ok()
1011 .log_err();
1012 }
1013 let scale_factor = self.0.state.borrow().scale_factor;
1014 let input = PlatformInput::FileDrop(FileDropEvent::Submit {
1015 position: logical_point(
1016 cursor_position.x as f32,
1017 cursor_position.y as f32,
1018 scale_factor,
1019 ),
1020 });
1021 self.handle_drag_drop(input);
1022
1023 Ok(())
1024 }
1025}
1026
1027#[derive(Debug, Clone, Copy)]
1028pub(crate) struct ClickState {
1029 button: MouseButton,
1030 last_click: Instant,
1031 last_position: Point<DevicePixels>,
1032 double_click_spatial_tolerance_width: i32,
1033 double_click_spatial_tolerance_height: i32,
1034 double_click_interval: Duration,
1035 pub(crate) current_count: usize,
1036}
1037
1038impl ClickState {
1039 pub fn new() -> Self {
1040 let double_click_spatial_tolerance_width = unsafe { GetSystemMetrics(SM_CXDOUBLECLK) };
1041 let double_click_spatial_tolerance_height = unsafe { GetSystemMetrics(SM_CYDOUBLECLK) };
1042 let double_click_interval = Duration::from_millis(unsafe { GetDoubleClickTime() } as u64);
1043
1044 ClickState {
1045 button: MouseButton::Left,
1046 last_click: Instant::now(),
1047 last_position: Point::default(),
1048 double_click_spatial_tolerance_width,
1049 double_click_spatial_tolerance_height,
1050 double_click_interval,
1051 current_count: 0,
1052 }
1053 }
1054
1055 /// update self and return the needed click count
1056 pub fn update(&mut self, button: MouseButton, new_position: Point<DevicePixels>) -> usize {
1057 if self.button == button && self.is_double_click(new_position) {
1058 self.current_count += 1;
1059 } else {
1060 self.current_count = 1;
1061 }
1062 self.last_click = Instant::now();
1063 self.last_position = new_position;
1064 self.button = button;
1065
1066 self.current_count
1067 }
1068
1069 pub fn system_update(&mut self, wparam: usize) {
1070 match wparam {
1071 // SPI_SETDOUBLECLKWIDTH
1072 29 => {
1073 self.double_click_spatial_tolerance_width =
1074 unsafe { GetSystemMetrics(SM_CXDOUBLECLK) }
1075 }
1076 // SPI_SETDOUBLECLKHEIGHT
1077 30 => {
1078 self.double_click_spatial_tolerance_height =
1079 unsafe { GetSystemMetrics(SM_CYDOUBLECLK) }
1080 }
1081 // SPI_SETDOUBLECLICKTIME
1082 32 => {
1083 self.double_click_interval =
1084 Duration::from_millis(unsafe { GetDoubleClickTime() } as u64)
1085 }
1086 _ => {}
1087 }
1088 }
1089
1090 #[inline]
1091 fn is_double_click(&self, new_position: Point<DevicePixels>) -> bool {
1092 let diff = self.last_position - new_position;
1093
1094 self.last_click.elapsed() < self.double_click_interval
1095 && diff.x.0.abs() <= self.double_click_spatial_tolerance_width
1096 && diff.y.0.abs() <= self.double_click_spatial_tolerance_height
1097 }
1098}
1099
1100struct StyleAndBounds {
1101 style: WINDOW_STYLE,
1102 x: i32,
1103 y: i32,
1104 cx: i32,
1105 cy: i32,
1106}
1107
1108#[repr(C)]
1109struct WINDOWCOMPOSITIONATTRIBDATA {
1110 attrib: u32,
1111 pv_data: *mut std::ffi::c_void,
1112 cb_data: usize,
1113}
1114
1115#[repr(C)]
1116struct AccentPolicy {
1117 accent_state: u32,
1118 accent_flags: u32,
1119 gradient_color: u32,
1120 animation_id: u32,
1121}
1122
1123type Color = (u8, u8, u8, u8);
1124
1125#[derive(Debug, Default, Clone, Copy)]
1126pub(crate) struct WindowBorderOffset {
1127 pub(crate) width_offset: i32,
1128 pub(crate) height_offset: i32,
1129}
1130
1131impl WindowBorderOffset {
1132 pub(crate) fn update(&mut self, hwnd: HWND) -> anyhow::Result<()> {
1133 let window_rect = unsafe {
1134 let mut rect = std::mem::zeroed();
1135 GetWindowRect(hwnd, &mut rect)?;
1136 rect
1137 };
1138 let client_rect = unsafe {
1139 let mut rect = std::mem::zeroed();
1140 GetClientRect(hwnd, &mut rect)?;
1141 rect
1142 };
1143 self.width_offset =
1144 (window_rect.right - window_rect.left) - (client_rect.right - client_rect.left);
1145 self.height_offset =
1146 (window_rect.bottom - window_rect.top) - (client_rect.bottom - client_rect.top);
1147 Ok(())
1148 }
1149}
1150
1151struct WindowOpenStatus {
1152 placement: WINDOWPLACEMENT,
1153 state: WindowOpenState,
1154}
1155
1156enum WindowOpenState {
1157 Maximized,
1158 Fullscreen,
1159 Windowed,
1160}
1161
1162const WINDOW_CLASS_NAME: PCWSTR = w!("Zed::Window");
1163
1164fn register_window_class(icon_handle: HICON) {
1165 static ONCE: Once = Once::new();
1166 ONCE.call_once(|| {
1167 let wc = WNDCLASSW {
1168 lpfnWndProc: Some(window_procedure),
1169 hIcon: icon_handle,
1170 lpszClassName: PCWSTR(WINDOW_CLASS_NAME.as_ptr()),
1171 style: CS_HREDRAW | CS_VREDRAW,
1172 hInstance: get_module_handle().into(),
1173 hbrBackground: unsafe { CreateSolidBrush(COLORREF(0x00000000)) },
1174 ..Default::default()
1175 };
1176 unsafe { RegisterClassW(&wc) };
1177 });
1178}
1179
1180unsafe extern "system" fn window_procedure(
1181 hwnd: HWND,
1182 msg: u32,
1183 wparam: WPARAM,
1184 lparam: LPARAM,
1185) -> LRESULT {
1186 if msg == WM_NCCREATE {
1187 let window_params = unsafe { &*(lparam.0 as *const CREATESTRUCTW) };
1188 let window_creation_context = window_params.lpCreateParams as *mut WindowCreateContext;
1189 let window_creation_context = unsafe { &mut *window_creation_context };
1190 return match WindowsWindowInner::new(window_creation_context, hwnd, window_params) {
1191 Ok(window_state) => {
1192 let weak = Box::new(Rc::downgrade(&window_state));
1193 unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) };
1194 window_creation_context.inner = Some(Ok(window_state));
1195 unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
1196 }
1197 Err(error) => {
1198 window_creation_context.inner = Some(Err(error));
1199 LRESULT(0)
1200 }
1201 };
1202 }
1203
1204 let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak<WindowsWindowInner>;
1205 if ptr.is_null() {
1206 return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
1207 }
1208 let inner = unsafe { &*ptr };
1209 let result = if let Some(inner) = inner.upgrade() {
1210 inner.handle_msg(hwnd, msg, wparam, lparam)
1211 } else {
1212 unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
1213 };
1214
1215 if msg == WM_NCDESTROY {
1216 unsafe { set_window_long(hwnd, GWLP_USERDATA, 0) };
1217 unsafe { drop(Box::from_raw(ptr)) };
1218 }
1219
1220 result
1221}
1222
1223pub(crate) fn window_from_hwnd(hwnd: HWND) -> Option<Rc<WindowsWindowInner>> {
1224 if hwnd.is_invalid() {
1225 return None;
1226 }
1227
1228 let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak<WindowsWindowInner>;
1229 if !ptr.is_null() {
1230 let inner = unsafe { &*ptr };
1231 inner.upgrade()
1232 } else {
1233 None
1234 }
1235}
1236
1237fn get_module_handle() -> HMODULE {
1238 unsafe {
1239 let mut h_module = std::mem::zeroed();
1240 GetModuleHandleExW(
1241 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
1242 windows::core::w!("ZedModule"),
1243 &mut h_module,
1244 )
1245 .expect("Unable to get module handle"); // this should never fail
1246
1247 h_module
1248 }
1249}
1250
1251fn register_drag_drop(window: &Rc<WindowsWindowInner>) -> Result<()> {
1252 let window_handle = window.hwnd;
1253 let handler = WindowsDragDropHandler(window.clone());
1254 // The lifetime of `IDropTarget` is handled by Windows, it won't release until
1255 // we call `RevokeDragDrop`.
1256 // So, it's safe to drop it here.
1257 let drag_drop_handler: IDropTarget = handler.into();
1258 unsafe {
1259 RegisterDragDrop(window_handle, &drag_drop_handler)
1260 .context("unable to register drag-drop event")?;
1261 }
1262 Ok(())
1263}
1264
1265fn calculate_window_rect(bounds: Bounds<DevicePixels>, border_offset: WindowBorderOffset) -> RECT {
1266 // NOTE:
1267 // The reason we're not using `AdjustWindowRectEx()` here is
1268 // that the size reported by this function is incorrect.
1269 // You can test it, and there are similar discussions online.
1270 // See: https://stackoverflow.com/questions/12423584/how-to-set-exact-client-size-for-overlapped-window-winapi
1271 //
1272 // So we manually calculate these values here.
1273 let mut rect = RECT {
1274 left: bounds.left().0,
1275 top: bounds.top().0,
1276 right: bounds.right().0,
1277 bottom: bounds.bottom().0,
1278 };
1279 let left_offset = border_offset.width_offset / 2;
1280 let top_offset = border_offset.height_offset / 2;
1281 let right_offset = border_offset.width_offset - left_offset;
1282 let bottom_offset = border_offset.height_offset - top_offset;
1283 rect.left -= left_offset;
1284 rect.top -= top_offset;
1285 rect.right += right_offset;
1286 rect.bottom += bottom_offset;
1287 rect
1288}
1289
1290fn calculate_client_rect(
1291 rect: RECT,
1292 border_offset: WindowBorderOffset,
1293 scale_factor: f32,
1294) -> Bounds<Pixels> {
1295 let left_offset = border_offset.width_offset / 2;
1296 let top_offset = border_offset.height_offset / 2;
1297 let right_offset = border_offset.width_offset - left_offset;
1298 let bottom_offset = border_offset.height_offset - top_offset;
1299 let left = rect.left + left_offset;
1300 let top = rect.top + top_offset;
1301 let right = rect.right - right_offset;
1302 let bottom = rect.bottom - bottom_offset;
1303 let physical_size = size(DevicePixels(right - left), DevicePixels(bottom - top));
1304 Bounds {
1305 origin: logical_point(left as f32, top as f32, scale_factor),
1306 size: physical_size.to_pixels(scale_factor),
1307 }
1308}
1309
1310fn retrieve_window_placement(
1311 hwnd: HWND,
1312 display: WindowsDisplay,
1313 initial_bounds: Bounds<Pixels>,
1314 scale_factor: f32,
1315 border_offset: WindowBorderOffset,
1316) -> Result<WINDOWPLACEMENT> {
1317 let mut placement = WINDOWPLACEMENT {
1318 length: std::mem::size_of::<WINDOWPLACEMENT>() as u32,
1319 ..Default::default()
1320 };
1321 unsafe { GetWindowPlacement(hwnd, &mut placement)? };
1322 // the bounds may be not inside the display
1323 let bounds = if display.check_given_bounds(initial_bounds) {
1324 initial_bounds
1325 } else {
1326 display.default_bounds()
1327 };
1328 let bounds = bounds.to_device_pixels(scale_factor);
1329 placement.rcNormalPosition = calculate_window_rect(bounds, border_offset);
1330 Ok(placement)
1331}
1332
1333fn set_window_composition_attribute(hwnd: HWND, color: Option<Color>, state: u32) {
1334 let mut version = unsafe { std::mem::zeroed() };
1335 let status = unsafe { windows::Wdk::System::SystemServices::RtlGetVersion(&mut version) };
1336 if !status.is_ok() || version.dwBuildNumber < 17763 {
1337 return;
1338 }
1339
1340 unsafe {
1341 type SetWindowCompositionAttributeType =
1342 unsafe extern "system" fn(HWND, *mut WINDOWCOMPOSITIONATTRIBDATA) -> BOOL;
1343 let module_name = PCSTR::from_raw(c"user32.dll".as_ptr() as *const u8);
1344 if let Some(user32) = GetModuleHandleA(module_name)
1345 .context("Unable to get user32.dll handle")
1346 .log_err()
1347 {
1348 let func_name = PCSTR::from_raw(c"SetWindowCompositionAttribute".as_ptr() as *const u8);
1349 let set_window_composition_attribute: SetWindowCompositionAttributeType =
1350 std::mem::transmute(GetProcAddress(user32, func_name));
1351 let mut color = color.unwrap_or_default();
1352 let is_acrylic = state == 4;
1353 if is_acrylic && color.3 == 0 {
1354 color.3 = 1;
1355 }
1356 let accent = AccentPolicy {
1357 accent_state: state,
1358 accent_flags: if is_acrylic { 0 } else { 2 },
1359 gradient_color: (color.0 as u32)
1360 | ((color.1 as u32) << 8)
1361 | ((color.2 as u32) << 16)
1362 | ((color.3 as u32) << 24),
1363 animation_id: 0,
1364 };
1365 let mut data = WINDOWCOMPOSITIONATTRIBDATA {
1366 attrib: 0x13,
1367 pv_data: &accent as *const _ as *mut _,
1368 cb_data: std::mem::size_of::<AccentPolicy>(),
1369 };
1370 let _ = set_window_composition_attribute(hwnd, &mut data as *mut _ as _);
1371 }
1372 }
1373}
1374
1375#[cfg(test)]
1376mod tests {
1377 use super::ClickState;
1378 use crate::{DevicePixels, MouseButton, point};
1379 use std::time::Duration;
1380
1381 #[test]
1382 fn test_double_click_interval() {
1383 let mut state = ClickState::new();
1384 assert_eq!(
1385 state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))),
1386 1
1387 );
1388 assert_eq!(
1389 state.update(MouseButton::Right, point(DevicePixels(0), DevicePixels(0))),
1390 1
1391 );
1392 assert_eq!(
1393 state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))),
1394 1
1395 );
1396 assert_eq!(
1397 state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))),
1398 2
1399 );
1400 state.last_click -= Duration::from_millis(700);
1401 assert_eq!(
1402 state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))),
1403 1
1404 );
1405 }
1406
1407 #[test]
1408 fn test_double_click_spatial_tolerance() {
1409 let mut state = ClickState::new();
1410 assert_eq!(
1411 state.update(MouseButton::Left, point(DevicePixels(-3), DevicePixels(0))),
1412 1
1413 );
1414 assert_eq!(
1415 state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(3))),
1416 2
1417 );
1418 assert_eq!(
1419 state.update(MouseButton::Right, point(DevicePixels(3), DevicePixels(2))),
1420 1
1421 );
1422 assert_eq!(
1423 state.update(MouseButton::Right, point(DevicePixels(10), DevicePixels(0))),
1424 1
1425 );
1426 }
1427}