1#![deny(unsafe_op_in_unsafe_fn)]
2
3use std::{
4 any::Any,
5 cell::{Cell, RefCell},
6 iter::once,
7 num::NonZeroIsize,
8 path::PathBuf,
9 rc::{Rc, Weak},
10 str::FromStr,
11 sync::{Arc, Once},
12 time::{Duration, Instant},
13};
14
15use ::util::ResultExt;
16use anyhow::Context;
17use blade_graphics as gpu;
18use futures::channel::oneshot::{self, Receiver};
19use itertools::Itertools;
20use raw_window_handle as rwh;
21use smallvec::SmallVec;
22use std::result::Result;
23use windows::{
24 core::*,
25 Win32::{
26 Foundation::*,
27 Graphics::Gdi::*,
28 System::{Com::*, LibraryLoader::*, Ole::*, SystemServices::*},
29 UI::{
30 Controls::*,
31 HiDpi::*,
32 Input::{Ime::*, KeyboardAndMouse::*},
33 Shell::*,
34 WindowsAndMessaging::*,
35 },
36 },
37};
38
39use crate::platform::blade::BladeRenderer;
40use crate::*;
41
42pub(crate) struct WindowsWindowInner {
43 hwnd: HWND,
44 origin: Cell<Point<DevicePixels>>,
45 physical_size: Cell<Size<DevicePixels>>,
46 scale_factor: Cell<f32>,
47 input_handler: Cell<Option<PlatformInputHandler>>,
48 renderer: RefCell<BladeRenderer>,
49 callbacks: RefCell<Callbacks>,
50 platform_inner: Rc<WindowsPlatformInner>,
51 pub(crate) handle: AnyWindowHandle,
52 hide_title_bar: bool,
53 display: RefCell<Rc<WindowsDisplay>>,
54 click_state: RefCell<ClickState>,
55 fullscreen: Cell<Option<StyleAndBounds>>,
56}
57
58impl WindowsWindowInner {
59 fn new(
60 hwnd: HWND,
61 cs: &CREATESTRUCTW,
62 platform_inner: Rc<WindowsPlatformInner>,
63 handle: AnyWindowHandle,
64 hide_title_bar: bool,
65 display: Rc<WindowsDisplay>,
66 ) -> Self {
67 let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32;
68 let origin = Cell::new(Point {
69 x: DevicePixels(cs.x),
70 y: DevicePixels(cs.y),
71 });
72 let physical_size = Cell::new(Size {
73 width: DevicePixels(cs.cx),
74 height: DevicePixels(cs.cy),
75 });
76 let scale_factor = Cell::new(monitor_dpi / USER_DEFAULT_SCREEN_DPI as f32);
77 let input_handler = Cell::new(None);
78 struct RawWindow {
79 hwnd: isize,
80 }
81 impl rwh::HasWindowHandle for RawWindow {
82 fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
83 Ok(unsafe {
84 let hwnd = NonZeroIsize::new_unchecked(self.hwnd);
85 let mut handle = rwh::Win32WindowHandle::new(hwnd);
86 let hinstance = get_window_long(HWND(self.hwnd), GWLP_HINSTANCE);
87 handle.hinstance = NonZeroIsize::new(hinstance);
88 rwh::WindowHandle::borrow_raw(handle.into())
89 })
90 }
91 }
92 impl rwh::HasDisplayHandle for RawWindow {
93 fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
94 let handle = rwh::WindowsDisplayHandle::new();
95 Ok(unsafe { rwh::DisplayHandle::borrow_raw(handle.into()) })
96 }
97 }
98
99 let raw = RawWindow { hwnd: hwnd.0 as _ };
100 let gpu = Arc::new(
101 unsafe {
102 gpu::Context::init_windowed(
103 &raw,
104 gpu::ContextDesc {
105 validation: false,
106 capture: false,
107 overlay: false,
108 },
109 )
110 }
111 .unwrap(),
112 );
113 let extent = gpu::Extent {
114 width: 1,
115 height: 1,
116 depth: 1,
117 };
118 let renderer = RefCell::new(BladeRenderer::new(gpu, extent));
119 let callbacks = RefCell::new(Callbacks::default());
120 let display = RefCell::new(display);
121 let click_state = RefCell::new(ClickState::new());
122 let fullscreen = Cell::new(None);
123 Self {
124 hwnd,
125 origin,
126 physical_size,
127 scale_factor,
128 input_handler,
129 renderer,
130 callbacks,
131 platform_inner,
132 handle,
133 hide_title_bar,
134 display,
135 click_state,
136 fullscreen,
137 }
138 }
139
140 fn is_maximized(&self) -> bool {
141 !self.is_fullscreen() && unsafe { IsZoomed(self.hwnd) }.as_bool()
142 }
143
144 fn is_minimized(&self) -> bool {
145 unsafe { IsIconic(self.hwnd) }.as_bool()
146 }
147
148 fn is_fullscreen(&self) -> bool {
149 let fullscreen = self.fullscreen.take();
150 let is_fullscreen = fullscreen.is_some();
151 self.fullscreen.set(fullscreen);
152 is_fullscreen
153 }
154
155 async fn toggle_fullscreen(self: Rc<Self>) {
156 let StyleAndBounds {
157 style,
158 x,
159 y,
160 cx,
161 cy,
162 } = if let Some(state) = self.fullscreen.take() {
163 state
164 } else {
165 let style = WINDOW_STYLE(unsafe { get_window_long(self.hwnd, GWL_STYLE) } as _);
166 let mut rc = RECT::default();
167 unsafe { GetWindowRect(self.hwnd, &mut rc) }.log_err();
168 self.fullscreen.set(Some(StyleAndBounds {
169 style,
170 x: rc.left,
171 y: rc.top,
172 cx: rc.right - rc.left,
173 cy: rc.bottom - rc.top,
174 }));
175 let style = style
176 & !(WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX | WS_CAPTION);
177 let bounds = self.display.borrow().clone().bounds();
178 StyleAndBounds {
179 style,
180 x: bounds.left().0,
181 y: bounds.top().0,
182 cx: bounds.size.width.0,
183 cy: bounds.size.height.0,
184 }
185 };
186 unsafe { set_window_long(self.hwnd, GWL_STYLE, style.0 as isize) };
187 unsafe {
188 SetWindowPos(
189 self.hwnd,
190 HWND::default(),
191 x,
192 y,
193 cx,
194 cy,
195 SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOZORDER,
196 )
197 }
198 .log_err();
199 }
200
201 pub(crate) fn title_bar_padding(&self) -> Pixels {
202 // using USER_DEFAULT_SCREEN_DPI because GPUI handles the scale with the scale factor
203 let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, USER_DEFAULT_SCREEN_DPI) };
204 px(padding as f32)
205 }
206
207 pub(crate) fn title_bar_top_offset(&self) -> Pixels {
208 if self.is_maximized() {
209 self.title_bar_padding() * 2
210 } else {
211 px(0.)
212 }
213 }
214
215 pub(crate) fn title_bar_height(&self) -> Pixels {
216 // todo(windows) this is hard set to match the ui title bar
217 // in the future the ui title bar component will report the size
218 px(32.) + self.title_bar_top_offset()
219 }
220
221 pub(crate) fn caption_button_width(&self) -> Pixels {
222 // todo(windows) this is hard set to match the ui title bar
223 // in the future the ui title bar component will report the size
224 px(36.)
225 }
226
227 fn get_titlebar_rect(&self) -> anyhow::Result<RECT> {
228 let height = self.title_bar_height();
229 let mut rect = RECT::default();
230 unsafe { GetClientRect(self.hwnd, &mut rect) }?;
231 rect.bottom = rect.top + ((height.0 * self.scale_factor.get()).round() as i32);
232 Ok(rect)
233 }
234
235 fn is_virtual_key_pressed(&self, vkey: VIRTUAL_KEY) -> bool {
236 unsafe { GetKeyState(vkey.0 as i32) < 0 }
237 }
238
239 fn current_modifiers(&self) -> Modifiers {
240 Modifiers {
241 control: self.is_virtual_key_pressed(VK_CONTROL),
242 alt: self.is_virtual_key_pressed(VK_MENU),
243 shift: self.is_virtual_key_pressed(VK_SHIFT),
244 platform: self.is_virtual_key_pressed(VK_LWIN) || self.is_virtual_key_pressed(VK_RWIN),
245 function: false,
246 }
247 }
248
249 /// mark window client rect to be re-drawn
250 /// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-invalidaterect
251 pub(crate) fn invalidate_client_area(&self) {
252 unsafe { InvalidateRect(self.hwnd, None, FALSE) };
253 }
254
255 fn handle_msg(&self, msg: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
256 let handled = match msg {
257 WM_ACTIVATE => self.handle_activate_msg(wparam),
258 WM_CREATE => self.handle_create_msg(lparam),
259 WM_MOVE => self.handle_move_msg(lparam),
260 WM_SIZE => self.handle_size_msg(lparam),
261 WM_ENTERSIZEMOVE | WM_ENTERMENULOOP => self.handle_size_move_loop(),
262 WM_EXITSIZEMOVE | WM_EXITMENULOOP => self.handle_size_move_loop_exit(),
263 WM_TIMER => self.handle_timer_msg(wparam),
264 WM_NCCALCSIZE => self.handle_calc_client_size(wparam, lparam),
265 WM_DPICHANGED => self.handle_dpi_changed_msg(wparam, lparam),
266 WM_NCHITTEST => self.handle_hit_test_msg(msg, wparam, lparam),
267 WM_PAINT => self.handle_paint_msg(),
268 WM_CLOSE => self.handle_close_msg(),
269 WM_DESTROY => self.handle_destroy_msg(),
270 WM_MOUSEMOVE => self.handle_mouse_move_msg(lparam, wparam),
271 WM_NCMOUSEMOVE => self.handle_nc_mouse_move_msg(lparam),
272 WM_NCLBUTTONDOWN => self.handle_nc_mouse_down_msg(MouseButton::Left, wparam, lparam),
273 WM_NCRBUTTONDOWN => self.handle_nc_mouse_down_msg(MouseButton::Right, wparam, lparam),
274 WM_NCMBUTTONDOWN => self.handle_nc_mouse_down_msg(MouseButton::Middle, wparam, lparam),
275 WM_NCLBUTTONUP => self.handle_nc_mouse_up_msg(MouseButton::Left, wparam, lparam),
276 WM_NCRBUTTONUP => self.handle_nc_mouse_up_msg(MouseButton::Right, wparam, lparam),
277 WM_NCMBUTTONUP => self.handle_nc_mouse_up_msg(MouseButton::Middle, wparam, lparam),
278 WM_LBUTTONDOWN => self.handle_mouse_down_msg(MouseButton::Left, lparam),
279 WM_RBUTTONDOWN => self.handle_mouse_down_msg(MouseButton::Right, lparam),
280 WM_MBUTTONDOWN => self.handle_mouse_down_msg(MouseButton::Middle, lparam),
281 WM_XBUTTONDOWN => self.handle_xbutton_msg(wparam, lparam, Self::handle_mouse_down_msg),
282 WM_LBUTTONUP => self.handle_mouse_up_msg(MouseButton::Left, lparam),
283 WM_RBUTTONUP => self.handle_mouse_up_msg(MouseButton::Right, lparam),
284 WM_MBUTTONUP => self.handle_mouse_up_msg(MouseButton::Middle, lparam),
285 WM_XBUTTONUP => self.handle_xbutton_msg(wparam, lparam, Self::handle_mouse_up_msg),
286 WM_MOUSEWHEEL => self.handle_mouse_wheel_msg(wparam, lparam),
287 WM_MOUSEHWHEEL => self.handle_mouse_horizontal_wheel_msg(wparam, lparam),
288 WM_SYSKEYDOWN => self.handle_syskeydown_msg(wparam, lparam),
289 WM_SYSKEYUP => self.handle_syskeyup_msg(wparam),
290 WM_KEYDOWN => self.handle_keydown_msg(msg, wparam, lparam),
291 WM_KEYUP => self.handle_keyup_msg(msg, wparam),
292 WM_CHAR => self.handle_char_msg(msg, wparam, lparam),
293 WM_IME_STARTCOMPOSITION => self.handle_ime_position(),
294 WM_IME_COMPOSITION => self.handle_ime_composition(lparam),
295 WM_SETCURSOR => self.handle_set_cursor(lparam),
296 _ => None,
297 };
298 if let Some(n) = handled {
299 LRESULT(n)
300 } else {
301 unsafe { DefWindowProcW(self.hwnd, msg, wparam, lparam) }
302 }
303 }
304
305 fn handle_move_msg(&self, lparam: LPARAM) -> Option<isize> {
306 let x = lparam.signed_loword() as i32;
307 let y = lparam.signed_hiword() as i32;
308 self.origin.set(Point {
309 x: DevicePixels(x),
310 y: DevicePixels(y),
311 });
312 let size = self.physical_size.get();
313 let center_x = x + size.width.0 / 2;
314 let center_y = y + size.height.0 / 2;
315 let monitor_bounds = self.display.borrow().bounds();
316 if center_x < monitor_bounds.left().0
317 || center_x > monitor_bounds.right().0
318 || center_y < monitor_bounds.top().0
319 || center_y > monitor_bounds.bottom().0
320 {
321 // center of the window may have moved to another monitor
322 let monitor = unsafe { MonitorFromWindow(self.hwnd, MONITOR_DEFAULTTONULL) };
323 if !monitor.is_invalid() && self.display.borrow().handle != monitor {
324 // we will get the same monitor if we only have one
325 (*self.display.borrow_mut()) = Rc::new(WindowsDisplay::new_with_handle(monitor));
326 }
327 }
328 let mut callbacks = self.callbacks.borrow_mut();
329 if let Some(callback) = callbacks.moved.as_mut() {
330 callback()
331 }
332 Some(0)
333 }
334
335 fn handle_size_msg(&self, lparam: LPARAM) -> Option<isize> {
336 let width = lparam.loword().max(1) as i32;
337 let height = lparam.hiword().max(1) as i32;
338 let scale_factor = self.scale_factor.get();
339 let new_physical_size = Size {
340 width: DevicePixels(width),
341 height: DevicePixels(height),
342 };
343 self.physical_size.set(new_physical_size);
344 self.renderer.borrow_mut().update_drawable_size(Size {
345 width: width as f64,
346 height: height as f64,
347 });
348 let mut callbacks = self.callbacks.borrow_mut();
349 if let Some(callback) = callbacks.resize.as_mut() {
350 let logical_size = logical_size(new_physical_size, scale_factor);
351 callback(logical_size, scale_factor);
352 }
353 Some(0)
354 }
355
356 fn handle_size_move_loop(&self) -> Option<isize> {
357 unsafe {
358 let ret = SetTimer(self.hwnd, SIZE_MOVE_LOOP_TIMER_ID, USER_TIMER_MINIMUM, None);
359 if ret == 0 {
360 log::error!(
361 "unable to create timer: {}",
362 std::io::Error::last_os_error()
363 );
364 }
365 }
366 None
367 }
368
369 fn handle_size_move_loop_exit(&self) -> Option<isize> {
370 unsafe {
371 KillTimer(self.hwnd, SIZE_MOVE_LOOP_TIMER_ID).log_err();
372 }
373 None
374 }
375
376 fn handle_timer_msg(&self, wparam: WPARAM) -> Option<isize> {
377 if wparam.0 == SIZE_MOVE_LOOP_TIMER_ID {
378 self.platform_inner.run_foreground_tasks();
379 self.handle_paint_msg();
380 return Some(0);
381 }
382 None
383 }
384
385 fn handle_paint_msg(&self) -> Option<isize> {
386 let mut paint_struct = PAINTSTRUCT::default();
387 let _hdc = unsafe { BeginPaint(self.hwnd, &mut paint_struct) };
388 let mut callbacks = self.callbacks.borrow_mut();
389 if let Some(request_frame) = callbacks.request_frame.as_mut() {
390 request_frame();
391 }
392 unsafe { EndPaint(self.hwnd, &paint_struct) };
393 Some(0)
394 }
395
396 fn handle_close_msg(&self) -> Option<isize> {
397 let mut callbacks = self.callbacks.borrow_mut();
398 if let Some(callback) = callbacks.should_close.as_mut() {
399 if callback() {
400 return Some(0);
401 }
402 }
403 None
404 }
405
406 fn handle_destroy_msg(&self) -> Option<isize> {
407 let mut callbacks = self.callbacks.borrow_mut();
408 if let Some(callback) = callbacks.close.take() {
409 callback()
410 }
411 let index = self
412 .platform_inner
413 .raw_window_handles
414 .read()
415 .iter()
416 .position(|handle| *handle == self.hwnd)
417 .unwrap();
418 self.platform_inner.raw_window_handles.write().remove(index);
419 if self.platform_inner.raw_window_handles.read().is_empty() {
420 self.platform_inner
421 .foreground_executor
422 .spawn(async {
423 unsafe { PostQuitMessage(0) };
424 })
425 .detach();
426 }
427 Some(1)
428 }
429
430 fn handle_mouse_move_msg(&self, lparam: LPARAM, wparam: WPARAM) -> Option<isize> {
431 let mut callbacks = self.callbacks.borrow_mut();
432 if let Some(callback) = callbacks.input.as_mut() {
433 let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) {
434 flags if flags.contains(MK_LBUTTON) => Some(MouseButton::Left),
435 flags if flags.contains(MK_RBUTTON) => Some(MouseButton::Right),
436 flags if flags.contains(MK_MBUTTON) => Some(MouseButton::Middle),
437 flags if flags.contains(MK_XBUTTON1) => {
438 Some(MouseButton::Navigate(NavigationDirection::Back))
439 }
440 flags if flags.contains(MK_XBUTTON2) => {
441 Some(MouseButton::Navigate(NavigationDirection::Forward))
442 }
443 _ => None,
444 };
445 let x = lparam.signed_loword() as f32;
446 let y = lparam.signed_hiword() as f32;
447 let scale_factor = self.scale_factor.get();
448 let event = MouseMoveEvent {
449 position: logical_point(x, y, scale_factor),
450 pressed_button,
451 modifiers: self.current_modifiers(),
452 };
453 if callback(PlatformInput::MouseMove(event)).default_prevented {
454 return Some(0);
455 }
456 }
457 Some(1)
458 }
459
460 fn parse_syskeydown_msg_keystroke(&self, wparam: WPARAM) -> Option<Keystroke> {
461 let modifiers = self.current_modifiers();
462 if !modifiers.alt {
463 // on Windows, F10 can trigger this event, not just the alt key
464 // and we just don't care about F10
465 return None;
466 }
467
468 let vk_code = wparam.loword();
469 let basic_key = basic_vkcode_to_string(vk_code, modifiers);
470 if basic_key.is_some() {
471 return basic_key;
472 }
473
474 let key = match VIRTUAL_KEY(vk_code) {
475 VK_BACK => Some("backspace"),
476 VK_RETURN => Some("enter"),
477 VK_TAB => Some("tab"),
478 VK_UP => Some("up"),
479 VK_DOWN => Some("down"),
480 VK_RIGHT => Some("right"),
481 VK_LEFT => Some("left"),
482 VK_HOME => Some("home"),
483 VK_END => Some("end"),
484 VK_PRIOR => Some("pageup"),
485 VK_NEXT => Some("pagedown"),
486 VK_ESCAPE => Some("escape"),
487 VK_INSERT => Some("insert"),
488 _ => None,
489 };
490
491 if let Some(key) = key {
492 Some(Keystroke {
493 modifiers,
494 key: key.to_string(),
495 ime_key: None,
496 })
497 } else {
498 None
499 }
500 }
501
502 fn parse_keydown_msg_keystroke(&self, wparam: WPARAM) -> Option<Keystroke> {
503 let vk_code = wparam.loword();
504
505 let modifiers = self.current_modifiers();
506 if modifiers.control || modifiers.alt {
507 let basic_key = basic_vkcode_to_string(vk_code, modifiers);
508 if basic_key.is_some() {
509 return basic_key;
510 }
511 }
512
513 if vk_code >= VK_F1.0 && vk_code <= VK_F24.0 {
514 let offset = vk_code - VK_F1.0;
515 return Some(Keystroke {
516 modifiers,
517 key: format!("f{}", offset + 1),
518 ime_key: None,
519 });
520 }
521
522 let key = match VIRTUAL_KEY(vk_code) {
523 VK_BACK => Some("backspace"),
524 VK_RETURN => Some("enter"),
525 VK_TAB => Some("tab"),
526 VK_UP => Some("up"),
527 VK_DOWN => Some("down"),
528 VK_RIGHT => Some("right"),
529 VK_LEFT => Some("left"),
530 VK_HOME => Some("home"),
531 VK_END => Some("end"),
532 VK_PRIOR => Some("pageup"),
533 VK_NEXT => Some("pagedown"),
534 VK_ESCAPE => Some("escape"),
535 VK_INSERT => Some("insert"),
536 VK_DELETE => Some("delete"),
537 _ => None,
538 };
539
540 if let Some(key) = key {
541 Some(Keystroke {
542 modifiers,
543 key: key.to_string(),
544 ime_key: None,
545 })
546 } else {
547 None
548 }
549 }
550
551 fn parse_char_msg_keystroke(&self, wparam: WPARAM) -> Option<Keystroke> {
552 let src = [wparam.0 as u16];
553 let Ok(first_char) = char::decode_utf16(src).collect::<Vec<_>>()[0] else {
554 return None;
555 };
556 if first_char.is_control() {
557 None
558 } else {
559 let mut modifiers = self.current_modifiers();
560 // for characters that use 'shift' to type it is expected that the
561 // shift is not reported if the uppercase/lowercase are the same and instead only the key is reported
562 if first_char.to_lowercase().to_string() == first_char.to_uppercase().to_string() {
563 modifiers.shift = false;
564 }
565 let key = match first_char {
566 ' ' => "space".to_string(),
567 first_char => first_char.to_lowercase().to_string(),
568 };
569 Some(Keystroke {
570 modifiers,
571 key,
572 ime_key: Some(first_char.to_string()),
573 })
574 }
575 }
576
577 fn handle_syskeydown_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
578 // we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}`
579 // shortcuts.
580 let Some(keystroke) = self.parse_syskeydown_msg_keystroke(wparam) else {
581 return None;
582 };
583 let Some(ref mut func) = self.callbacks.borrow_mut().input else {
584 return None;
585 };
586 let event = KeyDownEvent {
587 keystroke,
588 is_held: lparam.0 & (0x1 << 30) > 0,
589 };
590 if func(PlatformInput::KeyDown(event)).default_prevented {
591 self.invalidate_client_area();
592 return Some(0);
593 }
594 None
595 }
596
597 fn handle_syskeyup_msg(&self, wparam: WPARAM) -> Option<isize> {
598 // we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}`
599 // shortcuts.
600 let Some(keystroke) = self.parse_syskeydown_msg_keystroke(wparam) else {
601 return None;
602 };
603 let Some(ref mut func) = self.callbacks.borrow_mut().input else {
604 return None;
605 };
606 let event = KeyUpEvent { keystroke };
607 if func(PlatformInput::KeyUp(event)).default_prevented {
608 self.invalidate_client_area();
609 return Some(0);
610 }
611 None
612 }
613
614 fn handle_keydown_msg(&self, _msg: u32, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
615 let Some(keystroke) = self.parse_keydown_msg_keystroke(wparam) else {
616 return Some(1);
617 };
618 let Some(ref mut func) = self.callbacks.borrow_mut().input else {
619 return Some(1);
620 };
621 let event = KeyDownEvent {
622 keystroke,
623 is_held: lparam.0 & (0x1 << 30) > 0,
624 };
625 if func(PlatformInput::KeyDown(event)).default_prevented {
626 self.invalidate_client_area();
627 return Some(0);
628 }
629 Some(1)
630 }
631
632 fn handle_keyup_msg(&self, _msg: u32, wparam: WPARAM) -> Option<isize> {
633 let Some(keystroke) = self.parse_keydown_msg_keystroke(wparam) else {
634 return Some(1);
635 };
636 let Some(ref mut func) = self.callbacks.borrow_mut().input else {
637 return Some(1);
638 };
639 let event = KeyUpEvent { keystroke };
640 if func(PlatformInput::KeyUp(event)).default_prevented {
641 self.invalidate_client_area();
642 return Some(0);
643 }
644 Some(1)
645 }
646
647 fn handle_char_msg(&self, _msg: u32, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
648 let Some(keystroke) = self.parse_char_msg_keystroke(wparam) else {
649 return Some(1);
650 };
651 let mut callbacks = self.callbacks.borrow_mut();
652 let Some(ref mut func) = callbacks.input else {
653 return Some(1);
654 };
655 let ime_key = keystroke.ime_key.clone();
656 let event = KeyDownEvent {
657 keystroke,
658 is_held: lparam.0 & (0x1 << 30) > 0,
659 };
660
661 let dispatch_event_result = func(PlatformInput::KeyDown(event));
662 if dispatch_event_result.default_prevented || !dispatch_event_result.propagate {
663 self.invalidate_client_area();
664 return Some(0);
665 }
666 drop(callbacks);
667 let Some(ime_char) = ime_key else {
668 return Some(1);
669 };
670 let Some(mut input_handler) = self.input_handler.take() else {
671 return Some(1);
672 };
673 input_handler.replace_text_in_range(None, &ime_char);
674 self.input_handler.set(Some(input_handler));
675 self.invalidate_client_area();
676 Some(0)
677 }
678
679 fn handle_mouse_down_msg(&self, button: MouseButton, lparam: LPARAM) -> Option<isize> {
680 let mut callbacks = self.callbacks.borrow_mut();
681 if let Some(callback) = callbacks.input.as_mut() {
682 let x = lparam.signed_loword() as f32;
683 let y = lparam.signed_hiword() as f32;
684 let physical_point = point(DevicePixels(x as i32), DevicePixels(y as i32));
685 let click_count = self.click_state.borrow_mut().update(button, physical_point);
686 let scale_factor = self.scale_factor.get();
687 let event = MouseDownEvent {
688 button,
689 position: logical_point(x, y, scale_factor),
690 modifiers: self.current_modifiers(),
691 click_count,
692 first_mouse: false,
693 };
694 if callback(PlatformInput::MouseDown(event)).default_prevented {
695 return Some(0);
696 }
697 }
698 Some(1)
699 }
700
701 fn handle_mouse_up_msg(&self, button: MouseButton, lparam: LPARAM) -> Option<isize> {
702 let mut callbacks = self.callbacks.borrow_mut();
703 if let Some(callback) = callbacks.input.as_mut() {
704 let x = lparam.signed_loword() as f32;
705 let y = lparam.signed_hiword() as f32;
706 let click_count = self.click_state.borrow().current_count;
707 let scale_factor = self.scale_factor.get();
708 let event = MouseUpEvent {
709 button,
710 position: logical_point(x, y, scale_factor),
711 modifiers: self.current_modifiers(),
712 click_count,
713 };
714 if callback(PlatformInput::MouseUp(event)).default_prevented {
715 return Some(0);
716 }
717 }
718 Some(1)
719 }
720
721 fn handle_xbutton_msg(
722 &self,
723 wparam: WPARAM,
724 lparam: LPARAM,
725 handler: impl Fn(&Self, MouseButton, LPARAM) -> Option<isize>,
726 ) -> Option<isize> {
727 let nav_dir = match wparam.hiword() {
728 XBUTTON1 => NavigationDirection::Back,
729 XBUTTON2 => NavigationDirection::Forward,
730 _ => return Some(1),
731 };
732 handler(self, MouseButton::Navigate(nav_dir), lparam)
733 }
734
735 fn handle_mouse_wheel_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
736 let mut callbacks = self.callbacks.borrow_mut();
737 if let Some(callback) = callbacks.input.as_mut() {
738 let wheel_distance = (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32)
739 * self.platform_inner.settings.borrow().wheel_scroll_lines as f32;
740 let mut cursor_point = POINT {
741 x: lparam.signed_loword().into(),
742 y: lparam.signed_hiword().into(),
743 };
744 unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
745 let scale_factor = self.scale_factor.get();
746 let event = crate::ScrollWheelEvent {
747 position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
748 delta: ScrollDelta::Lines(Point {
749 x: 0.0,
750 y: wheel_distance,
751 }),
752 modifiers: self.current_modifiers(),
753 touch_phase: TouchPhase::Moved,
754 };
755 callback(PlatformInput::ScrollWheel(event));
756 return Some(0);
757 }
758 Some(1)
759 }
760
761 fn handle_mouse_horizontal_wheel_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
762 let mut callbacks = self.callbacks.borrow_mut();
763 if let Some(callback) = callbacks.input.as_mut() {
764 let wheel_distance = (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32)
765 * self.platform_inner.settings.borrow().wheel_scroll_chars as f32;
766 let mut cursor_point = POINT {
767 x: lparam.signed_loword().into(),
768 y: lparam.signed_hiword().into(),
769 };
770 unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
771 let scale_factor = self.scale_factor.get();
772 let event = crate::ScrollWheelEvent {
773 position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
774 delta: ScrollDelta::Lines(Point {
775 x: wheel_distance,
776 y: 0.0,
777 }),
778 modifiers: self.current_modifiers(),
779 touch_phase: TouchPhase::Moved,
780 };
781 if callback(PlatformInput::ScrollWheel(event)).default_prevented {
782 return Some(0);
783 }
784 }
785 Some(1)
786 }
787
788 fn handle_ime_position(&self) -> Option<isize> {
789 unsafe {
790 let ctx = ImmGetContext(self.hwnd);
791 let Some(mut input_handler) = self.input_handler.take() else {
792 return Some(1);
793 };
794 // we are composing, this should never fail
795 let caret_range = input_handler.selected_text_range().unwrap();
796 let caret_position = input_handler.bounds_for_range(caret_range).unwrap();
797 self.input_handler.set(Some(input_handler));
798 let scale_factor = self.scale_factor.get();
799 let config = CANDIDATEFORM {
800 dwStyle: CFS_CANDIDATEPOS,
801 // logical to physical
802 ptCurrentPos: POINT {
803 x: (caret_position.origin.x.0 * scale_factor) as i32,
804 y: (caret_position.origin.y.0 * scale_factor) as i32
805 + ((caret_position.size.height.0 * scale_factor) as i32 / 2),
806 },
807 ..Default::default()
808 };
809 ImmSetCandidateWindow(ctx, &config as _);
810 ImmReleaseContext(self.hwnd, ctx);
811 Some(0)
812 }
813 }
814
815 fn parse_ime_compostion_string(&self) -> Option<(String, usize)> {
816 unsafe {
817 let ctx = ImmGetContext(self.hwnd);
818 let string_len = ImmGetCompositionStringW(ctx, GCS_COMPSTR, None, 0);
819 let result = if string_len >= 0 {
820 let mut buffer = vec![0u8; string_len as usize + 2];
821 ImmGetCompositionStringW(
822 ctx,
823 GCS_COMPSTR,
824 Some(buffer.as_mut_ptr() as _),
825 string_len as _,
826 );
827 let wstring = std::slice::from_raw_parts::<u16>(
828 buffer.as_mut_ptr().cast::<u16>(),
829 string_len as usize / 2,
830 );
831 let string = String::from_utf16_lossy(wstring);
832 Some((string, string_len as usize / 2))
833 } else {
834 None
835 };
836 ImmReleaseContext(self.hwnd, ctx);
837 result
838 }
839 }
840
841 fn retrieve_composition_cursor_position(&self) -> usize {
842 unsafe {
843 let ctx = ImmGetContext(self.hwnd);
844 let ret = ImmGetCompositionStringW(ctx, GCS_CURSORPOS, None, 0);
845 ImmReleaseContext(self.hwnd, ctx);
846 ret as usize
847 }
848 }
849
850 fn parse_ime_compostion_result(&self) -> Option<String> {
851 unsafe {
852 let ctx = ImmGetContext(self.hwnd);
853 let string_len = ImmGetCompositionStringW(ctx, GCS_RESULTSTR, None, 0);
854 let result = if string_len >= 0 {
855 let mut buffer = vec![0u8; string_len as usize + 2];
856 ImmGetCompositionStringW(
857 ctx,
858 GCS_RESULTSTR,
859 Some(buffer.as_mut_ptr() as _),
860 string_len as _,
861 );
862 let wstring = std::slice::from_raw_parts::<u16>(
863 buffer.as_mut_ptr().cast::<u16>(),
864 string_len as usize / 2,
865 );
866 let string = String::from_utf16_lossy(wstring);
867 Some(string)
868 } else {
869 None
870 };
871 ImmReleaseContext(self.hwnd, ctx);
872 result
873 }
874 }
875
876 fn handle_ime_composition(&self, lparam: LPARAM) -> Option<isize> {
877 let mut ime_input = None;
878 if lparam.0 as u32 & GCS_COMPSTR.0 > 0 {
879 let Some((string, string_len)) = self.parse_ime_compostion_string() else {
880 return None;
881 };
882 let Some(mut input_handler) = self.input_handler.take() else {
883 return None;
884 };
885 input_handler.replace_and_mark_text_in_range(
886 None,
887 string.as_str(),
888 Some(0..string_len),
889 );
890 self.input_handler.set(Some(input_handler));
891 ime_input = Some(string);
892 }
893 if lparam.0 as u32 & GCS_CURSORPOS.0 > 0 {
894 let Some(ref comp_string) = ime_input else {
895 return None;
896 };
897 let caret_pos = self.retrieve_composition_cursor_position();
898 let Some(mut input_handler) = self.input_handler.take() else {
899 return None;
900 };
901 input_handler.replace_and_mark_text_in_range(None, comp_string, Some(0..caret_pos));
902 self.input_handler.set(Some(input_handler));
903 }
904 if lparam.0 as u32 & GCS_RESULTSTR.0 > 0 {
905 let Some(comp_result) = self.parse_ime_compostion_result() else {
906 return None;
907 };
908 let Some(mut input_handler) = self.input_handler.take() else {
909 return Some(1);
910 };
911 input_handler.replace_text_in_range(None, &comp_result);
912 self.input_handler.set(Some(input_handler));
913 self.invalidate_client_area();
914 return Some(0);
915 }
916 // currently, we don't care other stuff
917 None
918 }
919
920 fn handle_drag_drop(&self, input: PlatformInput) {
921 let mut callbacks = self.callbacks.borrow_mut();
922 let Some(ref mut func) = callbacks.input else {
923 return;
924 };
925 func(input);
926 }
927
928 /// SEE: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize
929 fn handle_calc_client_size(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
930 if !self.hide_title_bar || self.is_fullscreen() {
931 return None;
932 }
933
934 if wparam.0 == 0 {
935 return None;
936 }
937
938 let dpi = unsafe { GetDpiForWindow(self.hwnd) };
939
940 let frame_x = unsafe { GetSystemMetricsForDpi(SM_CXFRAME, dpi) };
941 let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) };
942 let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) };
943
944 // wparam is TRUE so lparam points to an NCCALCSIZE_PARAMS structure
945 let mut params = lparam.0 as *mut NCCALCSIZE_PARAMS;
946 let mut requested_client_rect = unsafe { &mut ((*params).rgrc) };
947
948 requested_client_rect[0].right -= frame_x + padding;
949 requested_client_rect[0].left += frame_x + padding;
950 requested_client_rect[0].bottom -= frame_y + padding;
951
952 Some(0)
953 }
954
955 fn handle_activate_msg(&self, wparam: WPARAM) -> Option<isize> {
956 if self.hide_title_bar {
957 if let Some(titlebar_rect) = self.get_titlebar_rect().log_err() {
958 unsafe { InvalidateRect(self.hwnd, Some(&titlebar_rect), FALSE) };
959 }
960 }
961 let activated = wparam.loword() > 0;
962 let mut callbacks = self.callbacks.borrow_mut();
963 if let Some(mut cb) = callbacks.active_status_change.as_mut() {
964 cb(activated);
965 }
966 None
967 }
968
969 fn handle_create_msg(&self, _lparam: LPARAM) -> Option<isize> {
970 let mut size_rect = RECT::default();
971 unsafe { GetWindowRect(self.hwnd, &mut size_rect).log_err() };
972
973 let width = size_rect.right - size_rect.left;
974 let height = size_rect.bottom - size_rect.top;
975
976 self.physical_size.set(Size {
977 width: DevicePixels(width),
978 height: DevicePixels(height),
979 });
980
981 if self.hide_title_bar {
982 // Inform the application of the frame change to force redrawing with the new
983 // client area that is extended into the title bar
984 unsafe {
985 SetWindowPos(
986 self.hwnd,
987 HWND::default(),
988 size_rect.left,
989 size_rect.top,
990 width,
991 height,
992 SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE,
993 )
994 .log_err()
995 };
996 }
997
998 Some(0)
999 }
1000
1001 fn handle_dpi_changed_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
1002 let new_dpi = wparam.loword() as f32;
1003 let scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32;
1004 self.scale_factor.set(scale_factor);
1005 let rect = unsafe { &*(lparam.0 as *const RECT) };
1006 let width = rect.right - rect.left;
1007 let height = rect.bottom - rect.top;
1008 // this will emit `WM_SIZE` and `WM_MOVE` right here
1009 // even before this function returns
1010 // the new size is handled in `WM_SIZE`
1011 unsafe {
1012 SetWindowPos(
1013 self.hwnd,
1014 None,
1015 rect.left,
1016 rect.top,
1017 width,
1018 height,
1019 SWP_NOZORDER | SWP_NOACTIVATE,
1020 )
1021 .context("unable to set window position after dpi has changed")
1022 .log_err();
1023 }
1024 self.invalidate_client_area();
1025 Some(0)
1026 }
1027
1028 fn handle_hit_test_msg(&self, msg: u32, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
1029 if !self.hide_title_bar {
1030 return None;
1031 }
1032
1033 // default handler for resize areas
1034 let hit = unsafe { DefWindowProcW(self.hwnd, msg, wparam, lparam) };
1035 if matches!(
1036 hit.0 as u32,
1037 HTNOWHERE
1038 | HTRIGHT
1039 | HTLEFT
1040 | HTTOPLEFT
1041 | HTTOP
1042 | HTTOPRIGHT
1043 | HTBOTTOMRIGHT
1044 | HTBOTTOM
1045 | HTBOTTOMLEFT
1046 ) {
1047 return Some(hit.0);
1048 }
1049
1050 if self.is_fullscreen() {
1051 return Some(HTCLIENT as _);
1052 }
1053
1054 let dpi = unsafe { GetDpiForWindow(self.hwnd) };
1055 let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) };
1056 let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) };
1057
1058 let mut cursor_point = POINT {
1059 x: lparam.signed_loword().into(),
1060 y: lparam.signed_hiword().into(),
1061 };
1062 unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
1063 if cursor_point.y > 0 && cursor_point.y < frame_y + padding {
1064 return Some(HTTOP as _);
1065 }
1066
1067 let titlebar_rect = self.get_titlebar_rect();
1068 if let Ok(titlebar_rect) = titlebar_rect {
1069 if cursor_point.y < titlebar_rect.bottom {
1070 let caption_btn_width =
1071 (self.caption_button_width().0 * self.scale_factor.get()) as i32;
1072 if cursor_point.x >= titlebar_rect.right - caption_btn_width {
1073 return Some(HTCLOSE as _);
1074 } else if cursor_point.x >= titlebar_rect.right - caption_btn_width * 2 {
1075 return Some(HTMAXBUTTON as _);
1076 } else if cursor_point.x >= titlebar_rect.right - caption_btn_width * 3 {
1077 return Some(HTMINBUTTON as _);
1078 }
1079
1080 return Some(HTCAPTION as _);
1081 }
1082 }
1083
1084 Some(HTCLIENT as _)
1085 }
1086
1087 fn handle_nc_mouse_move_msg(&self, lparam: LPARAM) -> Option<isize> {
1088 if !self.hide_title_bar {
1089 return None;
1090 }
1091
1092 let mut callbacks = self.callbacks.borrow_mut();
1093 if let Some(callback) = callbacks.input.as_mut() {
1094 let mut cursor_point = POINT {
1095 x: lparam.signed_loword().into(),
1096 y: lparam.signed_hiword().into(),
1097 };
1098 unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
1099 let scale_factor = self.scale_factor.get();
1100 let event = MouseMoveEvent {
1101 position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
1102 pressed_button: None,
1103 modifiers: self.current_modifiers(),
1104 };
1105 if callback(PlatformInput::MouseMove(event)).default_prevented {
1106 return Some(0);
1107 }
1108 }
1109 None
1110 }
1111
1112 fn handle_nc_mouse_down_msg(
1113 &self,
1114 button: MouseButton,
1115 wparam: WPARAM,
1116 lparam: LPARAM,
1117 ) -> Option<isize> {
1118 if !self.hide_title_bar {
1119 return None;
1120 }
1121
1122 let mut callbacks = self.callbacks.borrow_mut();
1123 if let Some(callback) = callbacks.input.as_mut() {
1124 let mut cursor_point = POINT {
1125 x: lparam.signed_loword().into(),
1126 y: lparam.signed_hiword().into(),
1127 };
1128 unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
1129 let physical_point = point(DevicePixels(cursor_point.x), DevicePixels(cursor_point.y));
1130 let click_count = self.click_state.borrow_mut().update(button, physical_point);
1131 let scale_factor = self.scale_factor.get();
1132 let event = MouseDownEvent {
1133 button,
1134 position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
1135 modifiers: self.current_modifiers(),
1136 click_count,
1137 first_mouse: false,
1138 };
1139 if callback(PlatformInput::MouseDown(event)).default_prevented {
1140 return Some(0);
1141 }
1142 }
1143
1144 // Since these are handled in handle_nc_mouse_up_msg we must prevent the default window proc
1145 matches!(wparam.0 as u32, HTMINBUTTON | HTMAXBUTTON | HTCLOSE).then_some(0)
1146 }
1147
1148 fn handle_nc_mouse_up_msg(
1149 &self,
1150 button: MouseButton,
1151 wparam: WPARAM,
1152 lparam: LPARAM,
1153 ) -> Option<isize> {
1154 if !self.hide_title_bar {
1155 return None;
1156 }
1157
1158 let mut callbacks = self.callbacks.borrow_mut();
1159 if let Some(callback) = callbacks.input.as_mut() {
1160 let mut cursor_point = POINT {
1161 x: lparam.signed_loword().into(),
1162 y: lparam.signed_hiword().into(),
1163 };
1164 unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
1165 let scale_factor = self.scale_factor.get();
1166 let event = MouseUpEvent {
1167 button,
1168 position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
1169 modifiers: self.current_modifiers(),
1170 click_count: 1,
1171 };
1172 if callback(PlatformInput::MouseUp(event)).default_prevented {
1173 return Some(0);
1174 }
1175 }
1176 drop(callbacks);
1177
1178 if button == MouseButton::Left {
1179 match wparam.0 as u32 {
1180 HTMINBUTTON => unsafe {
1181 ShowWindowAsync(self.hwnd, SW_MINIMIZE);
1182 },
1183 HTMAXBUTTON => unsafe {
1184 if self.is_maximized() {
1185 ShowWindowAsync(self.hwnd, SW_NORMAL);
1186 } else {
1187 ShowWindowAsync(self.hwnd, SW_MAXIMIZE);
1188 }
1189 },
1190 HTCLOSE => unsafe {
1191 PostMessageW(self.hwnd, WM_CLOSE, WPARAM::default(), LPARAM::default())
1192 .log_err();
1193 },
1194 _ => return None,
1195 };
1196 return Some(0);
1197 }
1198
1199 None
1200 }
1201
1202 fn handle_set_cursor(&self, lparam: LPARAM) -> Option<isize> {
1203 if matches!(
1204 lparam.loword() as u32,
1205 HTLEFT
1206 | HTRIGHT
1207 | HTTOP
1208 | HTTOPLEFT
1209 | HTTOPRIGHT
1210 | HTBOTTOM
1211 | HTBOTTOMLEFT
1212 | HTBOTTOMRIGHT
1213 ) {
1214 return None;
1215 }
1216 unsafe { SetCursor(self.platform_inner.current_cursor.get()) };
1217 Some(1)
1218 }
1219}
1220
1221#[derive(Default)]
1222struct Callbacks {
1223 request_frame: Option<Box<dyn FnMut()>>,
1224 input: Option<Box<dyn FnMut(crate::PlatformInput) -> DispatchEventResult>>,
1225 active_status_change: Option<Box<dyn FnMut(bool)>>,
1226 resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
1227 fullscreen: Option<Box<dyn FnMut(bool)>>,
1228 moved: Option<Box<dyn FnMut()>>,
1229 should_close: Option<Box<dyn FnMut() -> bool>>,
1230 close: Option<Box<dyn FnOnce()>>,
1231 appearance_changed: Option<Box<dyn FnMut()>>,
1232}
1233
1234pub(crate) struct WindowsWindow {
1235 inner: Rc<WindowsWindowInner>,
1236 drag_drop_handler: IDropTarget,
1237}
1238
1239struct WindowCreateContext {
1240 inner: Option<Rc<WindowsWindowInner>>,
1241 platform_inner: Rc<WindowsPlatformInner>,
1242 handle: AnyWindowHandle,
1243 hide_title_bar: bool,
1244 display: Rc<WindowsDisplay>,
1245}
1246
1247impl WindowsWindow {
1248 pub(crate) fn new(
1249 platform_inner: Rc<WindowsPlatformInner>,
1250 handle: AnyWindowHandle,
1251 options: WindowParams,
1252 ) -> Self {
1253 let classname = register_wnd_class(platform_inner.icon);
1254 let hide_title_bar = options
1255 .titlebar
1256 .as_ref()
1257 .map(|titlebar| titlebar.appears_transparent)
1258 .unwrap_or(false);
1259 let windowname = HSTRING::from(
1260 options
1261 .titlebar
1262 .as_ref()
1263 .and_then(|titlebar| titlebar.title.as_ref())
1264 .map(|title| title.as_ref())
1265 .unwrap_or(""),
1266 );
1267 let dwstyle = WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX;
1268 let x = options.bounds.origin.x.0;
1269 let y = options.bounds.origin.y.0;
1270 let nwidth = options.bounds.size.width.0;
1271 let nheight = options.bounds.size.height.0;
1272 let hwndparent = HWND::default();
1273 let hmenu = HMENU::default();
1274 let hinstance = get_module_handle();
1275 let mut context = WindowCreateContext {
1276 inner: None,
1277 platform_inner: platform_inner.clone(),
1278 handle,
1279 hide_title_bar,
1280 // todo(windows) move window to target monitor
1281 // options.display_id
1282 display: Rc::new(WindowsDisplay::primary_monitor().unwrap()),
1283 };
1284 let lpparam = Some(&context as *const _ as *const _);
1285 unsafe {
1286 CreateWindowExW(
1287 WS_EX_APPWINDOW,
1288 classname,
1289 &windowname,
1290 dwstyle,
1291 x,
1292 y,
1293 nwidth,
1294 nheight,
1295 hwndparent,
1296 hmenu,
1297 hinstance,
1298 lpparam,
1299 )
1300 };
1301 let drag_drop_handler = {
1302 let inner = context.inner.as_ref().unwrap();
1303 let handler = WindowsDragDropHandler(Rc::clone(inner));
1304 let drag_drop_handler: IDropTarget = handler.into();
1305 unsafe {
1306 RegisterDragDrop(inner.hwnd, &drag_drop_handler)
1307 .expect("unable to register drag-drop event")
1308 };
1309 drag_drop_handler
1310 };
1311 let wnd = Self {
1312 inner: context.inner.unwrap(),
1313 drag_drop_handler,
1314 };
1315 platform_inner
1316 .raw_window_handles
1317 .write()
1318 .push(wnd.inner.hwnd);
1319
1320 unsafe { ShowWindow(wnd.inner.hwnd, SW_SHOW) };
1321 wnd
1322 }
1323}
1324
1325impl rwh::HasWindowHandle for WindowsWindow {
1326 fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
1327 let raw =
1328 rwh::Win32WindowHandle::new(unsafe { NonZeroIsize::new_unchecked(self.inner.hwnd.0) })
1329 .into();
1330 Ok(unsafe { rwh::WindowHandle::borrow_raw(raw) })
1331 }
1332}
1333
1334// todo(windows)
1335impl rwh::HasDisplayHandle for WindowsWindow {
1336 fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
1337 unimplemented!()
1338 }
1339}
1340
1341impl Drop for WindowsWindow {
1342 fn drop(&mut self) {
1343 unsafe {
1344 let _ = RevokeDragDrop(self.inner.hwnd);
1345 self.inner.renderer.borrow_mut().destroy();
1346 }
1347 }
1348}
1349
1350impl PlatformWindow for WindowsWindow {
1351 fn bounds(&self) -> Bounds<DevicePixels> {
1352 Bounds {
1353 origin: self.inner.origin.get(),
1354 size: self.inner.physical_size.get(),
1355 }
1356 }
1357
1358 fn is_maximized(&self) -> bool {
1359 self.inner.is_maximized()
1360 }
1361
1362 fn is_minimized(&self) -> bool {
1363 self.inner.is_minimized()
1364 }
1365
1366 /// get the logical size of the app's drawable area.
1367 ///
1368 /// Currently, GPUI uses logical size of the app to handle mouse interactions (such as
1369 /// whether the mouse collides with other elements of GPUI).
1370 fn content_size(&self) -> Size<Pixels> {
1371 logical_size(
1372 self.inner.physical_size.get(),
1373 self.inner.scale_factor.get(),
1374 )
1375 }
1376
1377 fn scale_factor(&self) -> f32 {
1378 self.inner.scale_factor.get()
1379 }
1380
1381 // todo(windows)
1382 fn appearance(&self) -> WindowAppearance {
1383 WindowAppearance::Dark
1384 }
1385
1386 fn display(&self) -> Rc<dyn PlatformDisplay> {
1387 self.inner.display.borrow().clone()
1388 }
1389
1390 fn mouse_position(&self) -> Point<Pixels> {
1391 let point = unsafe {
1392 let mut point: POINT = std::mem::zeroed();
1393 GetCursorPos(&mut point)
1394 .context("unable to get cursor position")
1395 .log_err();
1396 ScreenToClient(self.inner.hwnd, &mut point);
1397 point
1398 };
1399 logical_point(
1400 point.x as f32,
1401 point.y as f32,
1402 self.inner.scale_factor.get(),
1403 )
1404 }
1405
1406 // todo(windows)
1407 fn modifiers(&self) -> Modifiers {
1408 Modifiers::none()
1409 }
1410
1411 fn as_any_mut(&mut self) -> &mut dyn Any {
1412 self
1413 }
1414
1415 // todo(windows)
1416 fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
1417 self.inner.input_handler.set(Some(input_handler));
1418 }
1419
1420 // todo(windows)
1421 fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
1422 self.inner.input_handler.take()
1423 }
1424
1425 fn prompt(
1426 &self,
1427 level: PromptLevel,
1428 msg: &str,
1429 detail: Option<&str>,
1430 answers: &[&str],
1431 ) -> Option<Receiver<usize>> {
1432 let (done_tx, done_rx) = oneshot::channel();
1433 let msg = msg.to_string();
1434 let detail_string = match detail {
1435 Some(info) => Some(info.to_string()),
1436 None => None,
1437 };
1438 let answers = answers.iter().map(|s| s.to_string()).collect::<Vec<_>>();
1439 let handle = self.inner.hwnd;
1440 self.inner
1441 .platform_inner
1442 .foreground_executor
1443 .spawn(async move {
1444 unsafe {
1445 let mut config;
1446 config = std::mem::zeroed::<TASKDIALOGCONFIG>();
1447 config.cbSize = std::mem::size_of::<TASKDIALOGCONFIG>() as _;
1448 config.hwndParent = handle;
1449 let title;
1450 let main_icon;
1451 match level {
1452 crate::PromptLevel::Info => {
1453 title = windows::core::w!("Info");
1454 main_icon = TD_INFORMATION_ICON;
1455 }
1456 crate::PromptLevel::Warning => {
1457 title = windows::core::w!("Warning");
1458 main_icon = TD_WARNING_ICON;
1459 }
1460 crate::PromptLevel::Critical | crate::PromptLevel::Destructive => {
1461 title = windows::core::w!("Critical");
1462 main_icon = TD_ERROR_ICON;
1463 }
1464 };
1465 config.pszWindowTitle = title;
1466 config.Anonymous1.pszMainIcon = main_icon;
1467 let instruction = msg.encode_utf16().chain(once(0)).collect_vec();
1468 config.pszMainInstruction = PCWSTR::from_raw(instruction.as_ptr());
1469 let hints_encoded;
1470 if let Some(ref hints) = detail_string {
1471 hints_encoded = hints.encode_utf16().chain(once(0)).collect_vec();
1472 config.pszContent = PCWSTR::from_raw(hints_encoded.as_ptr());
1473 };
1474 let mut buttons = Vec::new();
1475 let mut btn_encoded = Vec::new();
1476 for (index, btn_string) in answers.iter().enumerate() {
1477 let encoded = btn_string.encode_utf16().chain(once(0)).collect_vec();
1478 buttons.push(TASKDIALOG_BUTTON {
1479 nButtonID: index as _,
1480 pszButtonText: PCWSTR::from_raw(encoded.as_ptr()),
1481 });
1482 btn_encoded.push(encoded);
1483 }
1484 config.cButtons = buttons.len() as _;
1485 config.pButtons = buttons.as_ptr();
1486
1487 config.pfCallback = None;
1488 let mut res = std::mem::zeroed();
1489 let _ = TaskDialogIndirect(&config, Some(&mut res), None, None)
1490 .inspect_err(|e| log::error!("unable to create task dialog: {}", e));
1491
1492 let _ = done_tx.send(res as usize);
1493 }
1494 })
1495 .detach();
1496
1497 Some(done_rx)
1498 }
1499
1500 fn activate(&self) {
1501 unsafe { SetActiveWindow(self.inner.hwnd) };
1502 unsafe { SetFocus(self.inner.hwnd) };
1503 unsafe { SetForegroundWindow(self.inner.hwnd) };
1504 }
1505
1506 fn is_active(&self) -> bool {
1507 self.inner.hwnd == unsafe { GetActiveWindow() }
1508 }
1509
1510 // todo(windows)
1511 fn set_title(&mut self, title: &str) {
1512 unsafe { SetWindowTextW(self.inner.hwnd, &HSTRING::from(title)) }
1513 .inspect_err(|e| log::error!("Set title failed: {e}"))
1514 .ok();
1515 }
1516
1517 fn set_app_id(&mut self, _app_id: &str) {}
1518
1519 fn set_background_appearance(&mut self, _background_appearance: WindowBackgroundAppearance) {
1520 // todo(windows)
1521 }
1522
1523 // todo(windows)
1524 fn set_edited(&mut self, _edited: bool) {}
1525
1526 // todo(windows)
1527 fn show_character_palette(&self) {}
1528
1529 fn minimize(&self) {
1530 unsafe { ShowWindowAsync(self.inner.hwnd, SW_MINIMIZE) };
1531 }
1532
1533 fn zoom(&self) {
1534 unsafe { ShowWindowAsync(self.inner.hwnd, SW_MAXIMIZE) };
1535 }
1536
1537 fn toggle_fullscreen(&self) {
1538 self.inner
1539 .platform_inner
1540 .foreground_executor
1541 .spawn(self.inner.clone().toggle_fullscreen())
1542 .detach();
1543 }
1544
1545 fn is_fullscreen(&self) -> bool {
1546 self.inner.is_fullscreen()
1547 }
1548
1549 // todo(windows)
1550 fn on_request_frame(&self, callback: Box<dyn FnMut()>) {
1551 self.inner.callbacks.borrow_mut().request_frame = Some(callback);
1552 }
1553
1554 // todo(windows)
1555 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>) {
1556 self.inner.callbacks.borrow_mut().input = Some(callback);
1557 }
1558
1559 // todo(windows)
1560 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
1561 self.inner.callbacks.borrow_mut().active_status_change = Some(callback);
1562 }
1563
1564 // todo(windows)
1565 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
1566 self.inner.callbacks.borrow_mut().resize = Some(callback);
1567 }
1568
1569 // todo(windows)
1570 fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>) {
1571 self.inner.callbacks.borrow_mut().fullscreen = Some(callback);
1572 }
1573
1574 // todo(windows)
1575 fn on_moved(&self, callback: Box<dyn FnMut()>) {
1576 self.inner.callbacks.borrow_mut().moved = Some(callback);
1577 }
1578
1579 // todo(windows)
1580 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
1581 self.inner.callbacks.borrow_mut().should_close = Some(callback);
1582 }
1583
1584 // todo(windows)
1585 fn on_close(&self, callback: Box<dyn FnOnce()>) {
1586 self.inner.callbacks.borrow_mut().close = Some(callback);
1587 }
1588
1589 // todo(windows)
1590 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
1591 self.inner.callbacks.borrow_mut().appearance_changed = Some(callback);
1592 }
1593
1594 // todo(windows)
1595 fn is_topmost_for_position(&self, _position: Point<Pixels>) -> bool {
1596 true
1597 }
1598
1599 // todo(windows)
1600 fn draw(&self, scene: &Scene) {
1601 self.inner.renderer.borrow_mut().draw(scene)
1602 }
1603
1604 // todo(windows)
1605 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
1606 self.inner.renderer.borrow().sprite_atlas().clone()
1607 }
1608
1609 fn get_raw_handle(&self) -> HWND {
1610 self.inner.hwnd
1611 }
1612}
1613
1614#[implement(IDropTarget)]
1615struct WindowsDragDropHandler(pub Rc<WindowsWindowInner>);
1616
1617#[allow(non_snake_case)]
1618impl IDropTarget_Impl for WindowsDragDropHandler {
1619 fn DragEnter(
1620 &self,
1621 pdataobj: Option<&IDataObject>,
1622 _grfkeystate: MODIFIERKEYS_FLAGS,
1623 pt: &POINTL,
1624 pdweffect: *mut DROPEFFECT,
1625 ) -> windows::core::Result<()> {
1626 unsafe {
1627 let Some(idata_obj) = pdataobj else {
1628 log::info!("no dragging file or directory detected");
1629 return Ok(());
1630 };
1631 let config = FORMATETC {
1632 cfFormat: CF_HDROP.0,
1633 ptd: std::ptr::null_mut() as _,
1634 dwAspect: DVASPECT_CONTENT.0,
1635 lindex: -1,
1636 tymed: TYMED_HGLOBAL.0 as _,
1637 };
1638 let mut paths = SmallVec::<[PathBuf; 2]>::new();
1639 if idata_obj.QueryGetData(&config as _) == S_OK {
1640 *pdweffect = DROPEFFECT_LINK;
1641 let Ok(mut idata) = idata_obj.GetData(&config as _) else {
1642 return Ok(());
1643 };
1644 if idata.u.hGlobal.is_invalid() {
1645 return Ok(());
1646 }
1647 let hdrop = idata.u.hGlobal.0 as *mut HDROP;
1648 let file_count = DragQueryFileW(*hdrop, DRAGDROP_GET_FILES_COUNT, None);
1649 for file_index in 0..file_count {
1650 let filename_length = DragQueryFileW(*hdrop, file_index, None) as usize;
1651 let mut buffer = vec![0u16; filename_length + 1];
1652 let ret = DragQueryFileW(*hdrop, file_index, Some(buffer.as_mut_slice()));
1653 if ret == 0 {
1654 log::error!("unable to read file name");
1655 continue;
1656 }
1657 if let Ok(file_name) = String::from_utf16(&buffer[0..filename_length]) {
1658 if let Ok(path) = PathBuf::from_str(&file_name) {
1659 paths.push(path);
1660 }
1661 }
1662 }
1663 ReleaseStgMedium(&mut idata);
1664 let input = PlatformInput::FileDrop(crate::FileDropEvent::Entered {
1665 position: Point {
1666 x: Pixels(pt.x as _),
1667 y: Pixels(pt.y as _),
1668 },
1669 paths: crate::ExternalPaths(paths),
1670 });
1671 self.0.handle_drag_drop(input);
1672 } else {
1673 *pdweffect = DROPEFFECT_NONE;
1674 }
1675 }
1676 Ok(())
1677 }
1678
1679 fn DragOver(
1680 &self,
1681 _grfkeystate: MODIFIERKEYS_FLAGS,
1682 pt: &POINTL,
1683 _pdweffect: *mut DROPEFFECT,
1684 ) -> windows::core::Result<()> {
1685 let input = PlatformInput::FileDrop(crate::FileDropEvent::Pending {
1686 position: Point {
1687 x: Pixels(pt.x as _),
1688 y: Pixels(pt.y as _),
1689 },
1690 });
1691 self.0.handle_drag_drop(input);
1692
1693 Ok(())
1694 }
1695
1696 fn DragLeave(&self) -> windows::core::Result<()> {
1697 let input = PlatformInput::FileDrop(crate::FileDropEvent::Exited);
1698 self.0.handle_drag_drop(input);
1699
1700 Ok(())
1701 }
1702
1703 fn Drop(
1704 &self,
1705 _pdataobj: Option<&IDataObject>,
1706 _grfkeystate: MODIFIERKEYS_FLAGS,
1707 pt: &POINTL,
1708 _pdweffect: *mut DROPEFFECT,
1709 ) -> windows::core::Result<()> {
1710 let input = PlatformInput::FileDrop(crate::FileDropEvent::Submit {
1711 position: Point {
1712 x: Pixels(pt.x as _),
1713 y: Pixels(pt.y as _),
1714 },
1715 });
1716 self.0.handle_drag_drop(input);
1717
1718 Ok(())
1719 }
1720}
1721
1722#[derive(Debug)]
1723struct ClickState {
1724 button: MouseButton,
1725 last_click: Instant,
1726 last_position: Point<DevicePixels>,
1727 current_count: usize,
1728}
1729
1730impl ClickState {
1731 pub fn new() -> Self {
1732 ClickState {
1733 button: MouseButton::Left,
1734 last_click: Instant::now(),
1735 last_position: Point::default(),
1736 current_count: 0,
1737 }
1738 }
1739
1740 /// update self and return the needed click count
1741 pub fn update(&mut self, button: MouseButton, new_position: Point<DevicePixels>) -> usize {
1742 if self.button == button && self.is_double_click(new_position) {
1743 self.current_count += 1;
1744 } else {
1745 self.current_count = 1;
1746 }
1747 self.last_click = Instant::now();
1748 self.last_position = new_position;
1749 self.button = button;
1750
1751 self.current_count
1752 }
1753
1754 #[inline]
1755 fn is_double_click(&self, new_position: Point<DevicePixels>) -> bool {
1756 let diff = self.last_position - new_position;
1757
1758 self.last_click.elapsed() < DOUBLE_CLICK_INTERVAL
1759 && diff.x.0.abs() <= DOUBLE_CLICK_SPATIAL_TOLERANCE
1760 && diff.y.0.abs() <= DOUBLE_CLICK_SPATIAL_TOLERANCE
1761 }
1762}
1763
1764fn register_wnd_class(icon_handle: HICON) -> PCWSTR {
1765 const CLASS_NAME: PCWSTR = w!("Zed::Window");
1766
1767 static ONCE: Once = Once::new();
1768 ONCE.call_once(|| {
1769 let wc = WNDCLASSW {
1770 lpfnWndProc: Some(wnd_proc),
1771 hIcon: icon_handle,
1772 lpszClassName: PCWSTR(CLASS_NAME.as_ptr()),
1773 style: CS_HREDRAW | CS_VREDRAW,
1774 hInstance: get_module_handle().into(),
1775 ..Default::default()
1776 };
1777 unsafe { RegisterClassW(&wc) };
1778 });
1779
1780 CLASS_NAME
1781}
1782
1783unsafe extern "system" fn wnd_proc(
1784 hwnd: HWND,
1785 msg: u32,
1786 wparam: WPARAM,
1787 lparam: LPARAM,
1788) -> LRESULT {
1789 if msg == WM_NCCREATE {
1790 let cs = lparam.0 as *const CREATESTRUCTW;
1791 let cs = unsafe { &*cs };
1792 let ctx = cs.lpCreateParams as *mut WindowCreateContext;
1793 let ctx = unsafe { &mut *ctx };
1794 let inner = Rc::new(WindowsWindowInner::new(
1795 hwnd,
1796 cs,
1797 ctx.platform_inner.clone(),
1798 ctx.handle,
1799 ctx.hide_title_bar,
1800 ctx.display.clone(),
1801 ));
1802 let weak = Box::new(Rc::downgrade(&inner));
1803 unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) };
1804 ctx.inner = Some(inner);
1805 return LRESULT(1);
1806 }
1807 let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak<WindowsWindowInner>;
1808 if ptr.is_null() {
1809 return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
1810 }
1811 let inner = unsafe { &*ptr };
1812 let r = if let Some(inner) = inner.upgrade() {
1813 inner.handle_msg(msg, wparam, lparam)
1814 } else {
1815 unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
1816 };
1817 if msg == WM_NCDESTROY {
1818 unsafe { set_window_long(hwnd, GWLP_USERDATA, 0) };
1819 unsafe { drop(Box::from_raw(ptr)) };
1820 }
1821 r
1822}
1823
1824pub(crate) fn try_get_window_inner(hwnd: HWND) -> Option<Rc<WindowsWindowInner>> {
1825 if hwnd == HWND(0) {
1826 return None;
1827 }
1828
1829 let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak<WindowsWindowInner>;
1830 if !ptr.is_null() {
1831 let inner = unsafe { &*ptr };
1832 inner.upgrade()
1833 } else {
1834 None
1835 }
1836}
1837
1838fn basic_vkcode_to_string(code: u16, modifiers: Modifiers) -> Option<Keystroke> {
1839 match code {
1840 // VK_0 - VK_9
1841 48..=57 => Some(Keystroke {
1842 modifiers,
1843 key: format!("{}", code - VK_0.0),
1844 ime_key: None,
1845 }),
1846 // VK_A - VK_Z
1847 65..=90 => Some(Keystroke {
1848 modifiers,
1849 key: format!("{}", (b'a' + code as u8 - VK_A.0 as u8) as char),
1850 ime_key: None,
1851 }),
1852 // VK_F1 - VK_F24
1853 112..=135 => Some(Keystroke {
1854 modifiers,
1855 key: format!("f{}", code - VK_F1.0 + 1),
1856 ime_key: None,
1857 }),
1858 // OEM3: `/~, OEM_MINUS: -/_, OEM_PLUS: =/+, ...
1859 _ => {
1860 if let Some(key) = oemkey_vkcode_to_string(code) {
1861 Some(Keystroke {
1862 modifiers,
1863 key,
1864 ime_key: None,
1865 })
1866 } else {
1867 None
1868 }
1869 }
1870 }
1871}
1872
1873fn oemkey_vkcode_to_string(code: u16) -> Option<String> {
1874 match code {
1875 186 => Some(";".to_string()), // VK_OEM_1
1876 187 => Some("=".to_string()), // VK_OEM_PLUS
1877 188 => Some(",".to_string()), // VK_OEM_COMMA
1878 189 => Some("-".to_string()), // VK_OEM_MINUS
1879 190 => Some(".".to_string()), // VK_OEM_PERIOD
1880 // https://kbdlayout.info/features/virtualkeys/VK_ABNT_C1
1881 191 | 193 => Some("/".to_string()), // VK_OEM_2 VK_ABNT_C1
1882 192 => Some("`".to_string()), // VK_OEM_3
1883 219 => Some("[".to_string()), // VK_OEM_4
1884 220 => Some("\\".to_string()), // VK_OEM_5
1885 221 => Some("]".to_string()), // VK_OEM_6
1886 222 => Some("'".to_string()), // VK_OEM_7
1887 _ => None,
1888 }
1889}
1890
1891#[inline]
1892fn logical_size(physical_size: Size<DevicePixels>, scale_factor: f32) -> Size<Pixels> {
1893 Size {
1894 width: px(physical_size.width.0 as f32 / scale_factor),
1895 height: px(physical_size.height.0 as f32 / scale_factor),
1896 }
1897}
1898
1899#[inline]
1900fn logical_point(x: f32, y: f32, scale_factor: f32) -> Point<Pixels> {
1901 Point {
1902 x: px(x / scale_factor),
1903 y: px(y / scale_factor),
1904 }
1905}
1906
1907struct StyleAndBounds {
1908 style: WINDOW_STYLE,
1909 x: i32,
1910 y: i32,
1911 cx: i32,
1912 cy: i32,
1913}
1914
1915fn get_module_handle() -> HMODULE {
1916 unsafe {
1917 let mut h_module = std::mem::zeroed();
1918 GetModuleHandleExW(
1919 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
1920 windows::core::w!("ZedModule"),
1921 &mut h_module,
1922 )
1923 .expect("Unable to get module handle"); // this should never fail
1924
1925 h_module
1926 }
1927}
1928
1929// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
1930const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
1931// https://learn.microsoft.com/en-us/windows/win32/controls/ttm-setdelaytime?redirectedfrom=MSDN
1932const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(500);
1933// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics
1934const DOUBLE_CLICK_SPATIAL_TOLERANCE: i32 = 4;
1935const SIZE_MOVE_LOOP_TIMER_ID: usize = 1;
1936
1937#[cfg(test)]
1938mod tests {
1939 use super::ClickState;
1940 use crate::{point, DevicePixels, MouseButton};
1941 use std::time::Duration;
1942
1943 #[test]
1944 fn test_double_click_interval() {
1945 let mut state = ClickState::new();
1946 assert_eq!(
1947 state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))),
1948 1
1949 );
1950 assert_eq!(
1951 state.update(MouseButton::Right, point(DevicePixels(0), DevicePixels(0))),
1952 1
1953 );
1954 assert_eq!(
1955 state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))),
1956 1
1957 );
1958 assert_eq!(
1959 state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))),
1960 2
1961 );
1962 state.last_click -= Duration::from_millis(700);
1963 assert_eq!(
1964 state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))),
1965 1
1966 );
1967 }
1968
1969 #[test]
1970 fn test_double_click_spatial_tolerance() {
1971 let mut state = ClickState::new();
1972 assert_eq!(
1973 state.update(MouseButton::Left, point(DevicePixels(-3), DevicePixels(0))),
1974 1
1975 );
1976 assert_eq!(
1977 state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(3))),
1978 2
1979 );
1980 assert_eq!(
1981 state.update(MouseButton::Right, point(DevicePixels(3), DevicePixels(2))),
1982 1
1983 );
1984 assert_eq!(
1985 state.update(MouseButton::Right, point(DevicePixels(10), DevicePixels(0))),
1986 1
1987 );
1988 }
1989}