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