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