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 scale_factor = self.scale_factor.get();
705 let event = MouseUpEvent {
706 button,
707 position: logical_point(x, y, scale_factor),
708 modifiers: self.current_modifiers(),
709 click_count: 1,
710 };
711 if callback(PlatformInput::MouseUp(event)).default_prevented {
712 return Some(0);
713 }
714 }
715 Some(1)
716 }
717
718 fn handle_xbutton_msg(
719 &self,
720 wparam: WPARAM,
721 lparam: LPARAM,
722 handler: impl Fn(&Self, MouseButton, LPARAM) -> Option<isize>,
723 ) -> Option<isize> {
724 let nav_dir = match wparam.hiword() {
725 XBUTTON1 => NavigationDirection::Back,
726 XBUTTON2 => NavigationDirection::Forward,
727 _ => return Some(1),
728 };
729 handler(self, MouseButton::Navigate(nav_dir), lparam)
730 }
731
732 fn handle_mouse_wheel_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
733 let mut callbacks = self.callbacks.borrow_mut();
734 if let Some(callback) = callbacks.input.as_mut() {
735 let wheel_distance = (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32)
736 * self.platform_inner.settings.borrow().wheel_scroll_lines as f32;
737 let mut cursor_point = POINT {
738 x: lparam.signed_loword().into(),
739 y: lparam.signed_hiword().into(),
740 };
741 unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
742 let scale_factor = self.scale_factor.get();
743 let event = crate::ScrollWheelEvent {
744 position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
745 delta: ScrollDelta::Lines(Point {
746 x: 0.0,
747 y: wheel_distance,
748 }),
749 modifiers: self.current_modifiers(),
750 touch_phase: TouchPhase::Moved,
751 };
752 callback(PlatformInput::ScrollWheel(event));
753 return Some(0);
754 }
755 Some(1)
756 }
757
758 fn handle_mouse_horizontal_wheel_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
759 let mut callbacks = self.callbacks.borrow_mut();
760 if let Some(callback) = callbacks.input.as_mut() {
761 let wheel_distance = (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32)
762 * self.platform_inner.settings.borrow().wheel_scroll_chars as f32;
763 let mut cursor_point = POINT {
764 x: lparam.signed_loword().into(),
765 y: lparam.signed_hiword().into(),
766 };
767 unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
768 let scale_factor = self.scale_factor.get();
769 let event = crate::ScrollWheelEvent {
770 position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
771 delta: ScrollDelta::Lines(Point {
772 x: wheel_distance,
773 y: 0.0,
774 }),
775 modifiers: self.current_modifiers(),
776 touch_phase: TouchPhase::Moved,
777 };
778 if callback(PlatformInput::ScrollWheel(event)).default_prevented {
779 return Some(0);
780 }
781 }
782 Some(1)
783 }
784
785 fn handle_ime_position(&self) -> Option<isize> {
786 unsafe {
787 let ctx = ImmGetContext(self.hwnd);
788 let Some(mut input_handler) = self.input_handler.take() else {
789 return Some(1);
790 };
791 // we are composing, this should never fail
792 let caret_range = input_handler.selected_text_range().unwrap();
793 let caret_position = input_handler.bounds_for_range(caret_range).unwrap();
794 self.input_handler.set(Some(input_handler));
795 let scale_factor = self.scale_factor.get();
796 let config = CANDIDATEFORM {
797 dwStyle: CFS_CANDIDATEPOS,
798 // logical to physical
799 ptCurrentPos: POINT {
800 x: (caret_position.origin.x.0 * scale_factor) as i32,
801 y: (caret_position.origin.y.0 * scale_factor) as i32
802 + ((caret_position.size.height.0 * scale_factor) as i32 / 2),
803 },
804 ..Default::default()
805 };
806 ImmSetCandidateWindow(ctx, &config as _);
807 ImmReleaseContext(self.hwnd, ctx);
808 Some(0)
809 }
810 }
811
812 fn parse_ime_compostion_string(&self) -> Option<(String, usize)> {
813 unsafe {
814 let ctx = ImmGetContext(self.hwnd);
815 let string_len = ImmGetCompositionStringW(ctx, GCS_COMPSTR, None, 0);
816 let result = if string_len >= 0 {
817 let mut buffer = vec![0u8; string_len as usize + 2];
818 ImmGetCompositionStringW(
819 ctx,
820 GCS_COMPSTR,
821 Some(buffer.as_mut_ptr() as _),
822 string_len as _,
823 );
824 let wstring = std::slice::from_raw_parts::<u16>(
825 buffer.as_mut_ptr().cast::<u16>(),
826 string_len as usize / 2,
827 );
828 let string = String::from_utf16_lossy(wstring);
829 Some((string, string_len as usize / 2))
830 } else {
831 None
832 };
833 ImmReleaseContext(self.hwnd, ctx);
834 result
835 }
836 }
837
838 fn retrieve_composition_cursor_position(&self) -> usize {
839 unsafe {
840 let ctx = ImmGetContext(self.hwnd);
841 let ret = ImmGetCompositionStringW(ctx, GCS_CURSORPOS, None, 0);
842 ImmReleaseContext(self.hwnd, ctx);
843 ret as usize
844 }
845 }
846
847 fn parse_ime_compostion_result(&self) -> Option<String> {
848 unsafe {
849 let ctx = ImmGetContext(self.hwnd);
850 let string_len = ImmGetCompositionStringW(ctx, GCS_RESULTSTR, None, 0);
851 let result = if string_len >= 0 {
852 let mut buffer = vec![0u8; string_len as usize + 2];
853 ImmGetCompositionStringW(
854 ctx,
855 GCS_RESULTSTR,
856 Some(buffer.as_mut_ptr() as _),
857 string_len as _,
858 );
859 let wstring = std::slice::from_raw_parts::<u16>(
860 buffer.as_mut_ptr().cast::<u16>(),
861 string_len as usize / 2,
862 );
863 let string = String::from_utf16_lossy(wstring);
864 Some(string)
865 } else {
866 None
867 };
868 ImmReleaseContext(self.hwnd, ctx);
869 result
870 }
871 }
872
873 fn handle_ime_composition(&self, lparam: LPARAM) -> Option<isize> {
874 let mut ime_input = None;
875 if lparam.0 as u32 & GCS_COMPSTR.0 > 0 {
876 let Some((string, string_len)) = self.parse_ime_compostion_string() else {
877 return None;
878 };
879 let Some(mut input_handler) = self.input_handler.take() else {
880 return None;
881 };
882 input_handler.replace_and_mark_text_in_range(
883 None,
884 string.as_str(),
885 Some(0..string_len),
886 );
887 self.input_handler.set(Some(input_handler));
888 ime_input = Some(string);
889 }
890 if lparam.0 as u32 & GCS_CURSORPOS.0 > 0 {
891 let Some(ref comp_string) = ime_input else {
892 return None;
893 };
894 let caret_pos = self.retrieve_composition_cursor_position();
895 let Some(mut input_handler) = self.input_handler.take() else {
896 return None;
897 };
898 input_handler.replace_and_mark_text_in_range(None, comp_string, Some(0..caret_pos));
899 self.input_handler.set(Some(input_handler));
900 }
901 if lparam.0 as u32 & GCS_RESULTSTR.0 > 0 {
902 let Some(comp_result) = self.parse_ime_compostion_result() else {
903 return None;
904 };
905 let Some(mut input_handler) = self.input_handler.take() else {
906 return Some(1);
907 };
908 input_handler.replace_text_in_range(None, &comp_result);
909 self.input_handler.set(Some(input_handler));
910 self.invalidate_client_area();
911 return Some(0);
912 }
913 // currently, we don't care other stuff
914 None
915 }
916
917 fn handle_drag_drop(&self, input: PlatformInput) {
918 let mut callbacks = self.callbacks.borrow_mut();
919 let Some(ref mut func) = callbacks.input else {
920 return;
921 };
922 func(input);
923 }
924
925 /// SEE: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize
926 fn handle_calc_client_size(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
927 if !self.hide_title_bar || self.is_fullscreen() {
928 return None;
929 }
930
931 if wparam.0 == 0 {
932 return None;
933 }
934
935 let dpi = unsafe { GetDpiForWindow(self.hwnd) };
936
937 let frame_x = unsafe { GetSystemMetricsForDpi(SM_CXFRAME, dpi) };
938 let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) };
939 let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) };
940
941 // wparam is TRUE so lparam points to an NCCALCSIZE_PARAMS structure
942 let mut params = lparam.0 as *mut NCCALCSIZE_PARAMS;
943 let mut requested_client_rect = unsafe { &mut ((*params).rgrc) };
944
945 requested_client_rect[0].right -= frame_x + padding;
946 requested_client_rect[0].left += frame_x + padding;
947 requested_client_rect[0].bottom -= frame_y + padding;
948
949 Some(0)
950 }
951
952 fn handle_activate_msg(&self, wparam: WPARAM) -> Option<isize> {
953 if self.hide_title_bar {
954 if let Some(titlebar_rect) = self.get_titlebar_rect().log_err() {
955 unsafe { InvalidateRect(self.hwnd, Some(&titlebar_rect), FALSE) };
956 }
957 }
958 let activated = wparam.loword() > 0;
959 let mut callbacks = self.callbacks.borrow_mut();
960 if let Some(mut cb) = callbacks.active_status_change.as_mut() {
961 cb(activated);
962 }
963 None
964 }
965
966 fn handle_create_msg(&self, _lparam: LPARAM) -> Option<isize> {
967 let mut size_rect = RECT::default();
968 unsafe { GetWindowRect(self.hwnd, &mut size_rect).log_err() };
969
970 let width = size_rect.right - size_rect.left;
971 let height = size_rect.bottom - size_rect.top;
972
973 self.physical_size.set(Size {
974 width: DevicePixels(width),
975 height: DevicePixels(height),
976 });
977
978 if self.hide_title_bar {
979 // Inform the application of the frame change to force redrawing with the new
980 // client area that is extended into the title bar
981 unsafe {
982 SetWindowPos(
983 self.hwnd,
984 HWND::default(),
985 size_rect.left,
986 size_rect.top,
987 width,
988 height,
989 SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE,
990 )
991 .log_err()
992 };
993 }
994
995 Some(0)
996 }
997
998 fn handle_dpi_changed_msg(&self, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
999 let new_dpi = wparam.loword() as f32;
1000 let scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32;
1001 self.scale_factor.set(scale_factor);
1002 let rect = unsafe { &*(lparam.0 as *const RECT) };
1003 let width = rect.right - rect.left;
1004 let height = rect.bottom - rect.top;
1005 // this will emit `WM_SIZE` and `WM_MOVE` right here
1006 // even before this function returns
1007 // the new size is handled in `WM_SIZE`
1008 unsafe {
1009 SetWindowPos(
1010 self.hwnd,
1011 None,
1012 rect.left,
1013 rect.top,
1014 width,
1015 height,
1016 SWP_NOZORDER | SWP_NOACTIVATE,
1017 )
1018 .context("unable to set window position after dpi has changed")
1019 .log_err();
1020 }
1021 self.invalidate_client_area();
1022 Some(0)
1023 }
1024
1025 fn handle_hit_test_msg(&self, msg: u32, wparam: WPARAM, lparam: LPARAM) -> Option<isize> {
1026 if !self.hide_title_bar {
1027 return None;
1028 }
1029
1030 // default handler for resize areas
1031 let hit = unsafe { DefWindowProcW(self.hwnd, msg, wparam, lparam) };
1032 if matches!(
1033 hit.0 as u32,
1034 HTNOWHERE
1035 | HTRIGHT
1036 | HTLEFT
1037 | HTTOPLEFT
1038 | HTTOP
1039 | HTTOPRIGHT
1040 | HTBOTTOMRIGHT
1041 | HTBOTTOM
1042 | HTBOTTOMLEFT
1043 ) {
1044 return Some(hit.0);
1045 }
1046
1047 if self.is_fullscreen() {
1048 return Some(HTCLIENT as _);
1049 }
1050
1051 let dpi = unsafe { GetDpiForWindow(self.hwnd) };
1052 let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) };
1053 let padding = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) };
1054
1055 let mut cursor_point = POINT {
1056 x: lparam.signed_loword().into(),
1057 y: lparam.signed_hiword().into(),
1058 };
1059 unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
1060 if cursor_point.y > 0 && cursor_point.y < frame_y + padding {
1061 return Some(HTTOP as _);
1062 }
1063
1064 let titlebar_rect = self.get_titlebar_rect();
1065 if let Ok(titlebar_rect) = titlebar_rect {
1066 if cursor_point.y < titlebar_rect.bottom {
1067 let caption_btn_width =
1068 (self.caption_button_width().0 * self.scale_factor.get()) as i32;
1069 if cursor_point.x >= titlebar_rect.right - caption_btn_width {
1070 return Some(HTCLOSE as _);
1071 } else if cursor_point.x >= titlebar_rect.right - caption_btn_width * 2 {
1072 return Some(HTMAXBUTTON as _);
1073 } else if cursor_point.x >= titlebar_rect.right - caption_btn_width * 3 {
1074 return Some(HTMINBUTTON as _);
1075 }
1076
1077 return Some(HTCAPTION as _);
1078 }
1079 }
1080
1081 Some(HTCLIENT as _)
1082 }
1083
1084 fn handle_nc_mouse_move_msg(&self, lparam: LPARAM) -> Option<isize> {
1085 if !self.hide_title_bar {
1086 return None;
1087 }
1088
1089 let mut callbacks = self.callbacks.borrow_mut();
1090 if let Some(callback) = callbacks.input.as_mut() {
1091 let mut cursor_point = POINT {
1092 x: lparam.signed_loword().into(),
1093 y: lparam.signed_hiword().into(),
1094 };
1095 unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
1096 let scale_factor = self.scale_factor.get();
1097 let event = MouseMoveEvent {
1098 position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
1099 pressed_button: None,
1100 modifiers: self.current_modifiers(),
1101 };
1102 if callback(PlatformInput::MouseMove(event)).default_prevented {
1103 return Some(0);
1104 }
1105 }
1106 None
1107 }
1108
1109 fn handle_nc_mouse_down_msg(
1110 &self,
1111 button: MouseButton,
1112 wparam: WPARAM,
1113 lparam: LPARAM,
1114 ) -> Option<isize> {
1115 if !self.hide_title_bar {
1116 return None;
1117 }
1118
1119 let mut callbacks = self.callbacks.borrow_mut();
1120 if let Some(callback) = callbacks.input.as_mut() {
1121 let mut cursor_point = POINT {
1122 x: lparam.signed_loword().into(),
1123 y: lparam.signed_hiword().into(),
1124 };
1125 unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
1126 let physical_point = point(DevicePixels(cursor_point.x), DevicePixels(cursor_point.y));
1127 let click_count = self.click_state.borrow_mut().update(button, physical_point);
1128 let scale_factor = self.scale_factor.get();
1129 let event = MouseDownEvent {
1130 button,
1131 position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
1132 modifiers: self.current_modifiers(),
1133 click_count,
1134 first_mouse: false,
1135 };
1136 if callback(PlatformInput::MouseDown(event)).default_prevented {
1137 return Some(0);
1138 }
1139 }
1140
1141 // Since these are handled in handle_nc_mouse_up_msg we must prevent the default window proc
1142 matches!(wparam.0 as u32, HTMINBUTTON | HTMAXBUTTON | HTCLOSE).then_some(0)
1143 }
1144
1145 fn handle_nc_mouse_up_msg(
1146 &self,
1147 button: MouseButton,
1148 wparam: WPARAM,
1149 lparam: LPARAM,
1150 ) -> Option<isize> {
1151 if !self.hide_title_bar {
1152 return None;
1153 }
1154
1155 let mut callbacks = self.callbacks.borrow_mut();
1156 if let Some(callback) = callbacks.input.as_mut() {
1157 let mut cursor_point = POINT {
1158 x: lparam.signed_loword().into(),
1159 y: lparam.signed_hiword().into(),
1160 };
1161 unsafe { ScreenToClient(self.hwnd, &mut cursor_point) };
1162 let scale_factor = self.scale_factor.get();
1163 let event = MouseUpEvent {
1164 button,
1165 position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
1166 modifiers: self.current_modifiers(),
1167 click_count: 1,
1168 };
1169 if callback(PlatformInput::MouseUp(event)).default_prevented {
1170 return Some(0);
1171 }
1172 }
1173 drop(callbacks);
1174
1175 if button == MouseButton::Left {
1176 match wparam.0 as u32 {
1177 HTMINBUTTON => unsafe {
1178 ShowWindowAsync(self.hwnd, SW_MINIMIZE);
1179 },
1180 HTMAXBUTTON => unsafe {
1181 if self.is_maximized() {
1182 ShowWindowAsync(self.hwnd, SW_NORMAL);
1183 } else {
1184 ShowWindowAsync(self.hwnd, SW_MAXIMIZE);
1185 }
1186 },
1187 HTCLOSE => unsafe {
1188 PostMessageW(self.hwnd, WM_CLOSE, WPARAM::default(), LPARAM::default())
1189 .log_err();
1190 },
1191 _ => return None,
1192 };
1193 return Some(0);
1194 }
1195
1196 None
1197 }
1198
1199 fn handle_set_cursor(&self, lparam: LPARAM) -> Option<isize> {
1200 if matches!(
1201 lparam.loword() as u32,
1202 HTLEFT
1203 | HTRIGHT
1204 | HTTOP
1205 | HTTOPLEFT
1206 | HTTOPRIGHT
1207 | HTBOTTOM
1208 | HTBOTTOMLEFT
1209 | HTBOTTOMRIGHT
1210 ) {
1211 return None;
1212 }
1213 unsafe { SetCursor(self.platform_inner.current_cursor.get()) };
1214 Some(1)
1215 }
1216}
1217
1218#[derive(Default)]
1219struct Callbacks {
1220 request_frame: Option<Box<dyn FnMut()>>,
1221 input: Option<Box<dyn FnMut(crate::PlatformInput) -> DispatchEventResult>>,
1222 active_status_change: Option<Box<dyn FnMut(bool)>>,
1223 resize: Option<Box<dyn FnMut(Size<Pixels>, f32)>>,
1224 fullscreen: Option<Box<dyn FnMut(bool)>>,
1225 moved: Option<Box<dyn FnMut()>>,
1226 should_close: Option<Box<dyn FnMut() -> bool>>,
1227 close: Option<Box<dyn FnOnce()>>,
1228 appearance_changed: Option<Box<dyn FnMut()>>,
1229}
1230
1231pub(crate) struct WindowsWindow {
1232 inner: Rc<WindowsWindowInner>,
1233 drag_drop_handler: IDropTarget,
1234}
1235
1236struct WindowCreateContext {
1237 inner: Option<Rc<WindowsWindowInner>>,
1238 platform_inner: Rc<WindowsPlatformInner>,
1239 handle: AnyWindowHandle,
1240 hide_title_bar: bool,
1241 display: Rc<WindowsDisplay>,
1242}
1243
1244impl WindowsWindow {
1245 pub(crate) fn new(
1246 platform_inner: Rc<WindowsPlatformInner>,
1247 handle: AnyWindowHandle,
1248 options: WindowParams,
1249 ) -> Self {
1250 let classname = register_wnd_class(platform_inner.icon);
1251 let hide_title_bar = options
1252 .titlebar
1253 .as_ref()
1254 .map(|titlebar| titlebar.appears_transparent)
1255 .unwrap_or(false);
1256 let windowname = HSTRING::from(
1257 options
1258 .titlebar
1259 .as_ref()
1260 .and_then(|titlebar| titlebar.title.as_ref())
1261 .map(|title| title.as_ref())
1262 .unwrap_or(""),
1263 );
1264 let dwstyle = WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX;
1265 let x = options.bounds.origin.x.0;
1266 let y = options.bounds.origin.y.0;
1267 let nwidth = options.bounds.size.width.0;
1268 let nheight = options.bounds.size.height.0;
1269 let hwndparent = HWND::default();
1270 let hmenu = HMENU::default();
1271 let hinstance = HINSTANCE::default();
1272 let mut context = WindowCreateContext {
1273 inner: None,
1274 platform_inner: platform_inner.clone(),
1275 handle,
1276 hide_title_bar,
1277 // todo(windows) move window to target monitor
1278 // options.display_id
1279 display: Rc::new(WindowsDisplay::primary_monitor().unwrap()),
1280 };
1281 let lpparam = Some(&context as *const _ as *const _);
1282 unsafe {
1283 CreateWindowExW(
1284 WS_EX_APPWINDOW,
1285 classname,
1286 &windowname,
1287 dwstyle,
1288 x,
1289 y,
1290 nwidth,
1291 nheight,
1292 hwndparent,
1293 hmenu,
1294 hinstance,
1295 lpparam,
1296 )
1297 };
1298 let drag_drop_handler = {
1299 let inner = context.inner.as_ref().unwrap();
1300 let handler = WindowsDragDropHandler(Rc::clone(inner));
1301 let drag_drop_handler: IDropTarget = handler.into();
1302 unsafe {
1303 RegisterDragDrop(inner.hwnd, &drag_drop_handler)
1304 .expect("unable to register drag-drop event")
1305 };
1306 drag_drop_handler
1307 };
1308 let wnd = Self {
1309 inner: context.inner.unwrap(),
1310 drag_drop_handler,
1311 };
1312 platform_inner
1313 .raw_window_handles
1314 .write()
1315 .push(wnd.inner.hwnd);
1316
1317 unsafe { ShowWindow(wnd.inner.hwnd, SW_SHOW) };
1318 wnd
1319 }
1320}
1321
1322impl rwh::HasWindowHandle for WindowsWindow {
1323 fn window_handle(&self) -> Result<rwh::WindowHandle<'_>, rwh::HandleError> {
1324 let raw =
1325 rwh::Win32WindowHandle::new(unsafe { NonZeroIsize::new_unchecked(self.inner.hwnd.0) })
1326 .into();
1327 Ok(unsafe { rwh::WindowHandle::borrow_raw(raw) })
1328 }
1329}
1330
1331// todo(windows)
1332impl rwh::HasDisplayHandle for WindowsWindow {
1333 fn display_handle(&self) -> Result<rwh::DisplayHandle<'_>, rwh::HandleError> {
1334 unimplemented!()
1335 }
1336}
1337
1338impl Drop for WindowsWindow {
1339 fn drop(&mut self) {
1340 unsafe {
1341 let _ = RevokeDragDrop(self.inner.hwnd);
1342 self.inner.renderer.borrow_mut().destroy();
1343 }
1344 }
1345}
1346
1347impl PlatformWindow for WindowsWindow {
1348 fn bounds(&self) -> Bounds<DevicePixels> {
1349 Bounds {
1350 origin: self.inner.origin.get(),
1351 size: self.inner.physical_size.get(),
1352 }
1353 }
1354
1355 fn is_maximized(&self) -> bool {
1356 self.inner.is_maximized()
1357 }
1358
1359 fn is_minimized(&self) -> bool {
1360 self.inner.is_minimized()
1361 }
1362
1363 /// get the logical size of the app's drawable area.
1364 ///
1365 /// Currently, GPUI uses logical size of the app to handle mouse interactions (such as
1366 /// whether the mouse collides with other elements of GPUI).
1367 fn content_size(&self) -> Size<Pixels> {
1368 logical_size(
1369 self.inner.physical_size.get(),
1370 self.inner.scale_factor.get(),
1371 )
1372 }
1373
1374 fn scale_factor(&self) -> f32 {
1375 self.inner.scale_factor.get()
1376 }
1377
1378 // todo(windows)
1379 fn appearance(&self) -> WindowAppearance {
1380 WindowAppearance::Dark
1381 }
1382
1383 fn display(&self) -> Rc<dyn PlatformDisplay> {
1384 self.inner.display.borrow().clone()
1385 }
1386
1387 fn mouse_position(&self) -> Point<Pixels> {
1388 let point = unsafe {
1389 let mut point: POINT = std::mem::zeroed();
1390 GetCursorPos(&mut point)
1391 .context("unable to get cursor position")
1392 .log_err();
1393 ScreenToClient(self.inner.hwnd, &mut point);
1394 point
1395 };
1396 logical_point(
1397 point.x as f32,
1398 point.y as f32,
1399 self.inner.scale_factor.get(),
1400 )
1401 }
1402
1403 // todo(windows)
1404 fn modifiers(&self) -> Modifiers {
1405 Modifiers::none()
1406 }
1407
1408 fn as_any_mut(&mut self) -> &mut dyn Any {
1409 self
1410 }
1411
1412 // todo(windows)
1413 fn set_input_handler(&mut self, input_handler: PlatformInputHandler) {
1414 self.inner.input_handler.set(Some(input_handler));
1415 }
1416
1417 // todo(windows)
1418 fn take_input_handler(&mut self) -> Option<PlatformInputHandler> {
1419 self.inner.input_handler.take()
1420 }
1421
1422 fn prompt(
1423 &self,
1424 level: PromptLevel,
1425 msg: &str,
1426 detail: Option<&str>,
1427 answers: &[&str],
1428 ) -> Option<Receiver<usize>> {
1429 let (done_tx, done_rx) = oneshot::channel();
1430 let msg = msg.to_string();
1431 let detail_string = match detail {
1432 Some(info) => Some(info.to_string()),
1433 None => None,
1434 };
1435 let answers = answers.iter().map(|s| s.to_string()).collect::<Vec<_>>();
1436 let handle = self.inner.hwnd;
1437 self.inner
1438 .platform_inner
1439 .foreground_executor
1440 .spawn(async move {
1441 unsafe {
1442 let mut config;
1443 config = std::mem::zeroed::<TASKDIALOGCONFIG>();
1444 config.cbSize = std::mem::size_of::<TASKDIALOGCONFIG>() as _;
1445 config.hwndParent = handle;
1446 let title;
1447 let main_icon;
1448 match level {
1449 crate::PromptLevel::Info => {
1450 title = windows::core::w!("Info");
1451 main_icon = TD_INFORMATION_ICON;
1452 }
1453 crate::PromptLevel::Warning => {
1454 title = windows::core::w!("Warning");
1455 main_icon = TD_WARNING_ICON;
1456 }
1457 crate::PromptLevel::Critical => {
1458 title = windows::core::w!("Critical");
1459 main_icon = TD_ERROR_ICON;
1460 }
1461 };
1462 config.pszWindowTitle = title;
1463 config.Anonymous1.pszMainIcon = main_icon;
1464 let instruction = msg.encode_utf16().chain(once(0)).collect_vec();
1465 config.pszMainInstruction = PCWSTR::from_raw(instruction.as_ptr());
1466 let hints_encoded;
1467 if let Some(ref hints) = detail_string {
1468 hints_encoded = hints.encode_utf16().chain(once(0)).collect_vec();
1469 config.pszContent = PCWSTR::from_raw(hints_encoded.as_ptr());
1470 };
1471 let mut buttons = Vec::new();
1472 let mut btn_encoded = Vec::new();
1473 for (index, btn_string) in answers.iter().enumerate() {
1474 let encoded = btn_string.encode_utf16().chain(once(0)).collect_vec();
1475 buttons.push(TASKDIALOG_BUTTON {
1476 nButtonID: index as _,
1477 pszButtonText: PCWSTR::from_raw(encoded.as_ptr()),
1478 });
1479 btn_encoded.push(encoded);
1480 }
1481 config.cButtons = buttons.len() as _;
1482 config.pButtons = buttons.as_ptr();
1483
1484 config.pfCallback = None;
1485 let mut res = std::mem::zeroed();
1486 let _ = TaskDialogIndirect(&config, Some(&mut res), None, None)
1487 .inspect_err(|e| log::error!("unable to create task dialog: {}", e));
1488
1489 let _ = done_tx.send(res as usize);
1490 }
1491 })
1492 .detach();
1493
1494 Some(done_rx)
1495 }
1496
1497 fn activate(&self) {
1498 unsafe { SetActiveWindow(self.inner.hwnd) };
1499 unsafe { SetFocus(self.inner.hwnd) };
1500 unsafe { SetForegroundWindow(self.inner.hwnd) };
1501 }
1502
1503 fn is_active(&self) -> bool {
1504 self.inner.hwnd == unsafe { GetActiveWindow() }
1505 }
1506
1507 // todo(windows)
1508 fn set_title(&mut self, title: &str) {
1509 unsafe { SetWindowTextW(self.inner.hwnd, &HSTRING::from(title)) }
1510 .inspect_err(|e| log::error!("Set title failed: {e}"))
1511 .ok();
1512 }
1513
1514 fn set_background_appearance(&mut self, _background_appearance: WindowBackgroundAppearance) {
1515 // todo(windows)
1516 }
1517
1518 // todo(windows)
1519 fn set_edited(&mut self, _edited: bool) {}
1520
1521 // todo(windows)
1522 fn show_character_palette(&self) {}
1523
1524 fn minimize(&self) {
1525 unsafe { ShowWindowAsync(self.inner.hwnd, SW_MINIMIZE) };
1526 }
1527
1528 fn zoom(&self) {
1529 unsafe { ShowWindowAsync(self.inner.hwnd, SW_MAXIMIZE) };
1530 }
1531
1532 fn toggle_fullscreen(&self) {
1533 self.inner
1534 .platform_inner
1535 .foreground_executor
1536 .spawn(self.inner.clone().toggle_fullscreen())
1537 .detach();
1538 }
1539
1540 fn is_fullscreen(&self) -> bool {
1541 self.inner.is_fullscreen()
1542 }
1543
1544 // todo(windows)
1545 fn on_request_frame(&self, callback: Box<dyn FnMut()>) {
1546 self.inner.callbacks.borrow_mut().request_frame = Some(callback);
1547 }
1548
1549 // todo(windows)
1550 fn on_input(&self, callback: Box<dyn FnMut(PlatformInput) -> DispatchEventResult>) {
1551 self.inner.callbacks.borrow_mut().input = Some(callback);
1552 }
1553
1554 // todo(windows)
1555 fn on_active_status_change(&self, callback: Box<dyn FnMut(bool)>) {
1556 self.inner.callbacks.borrow_mut().active_status_change = Some(callback);
1557 }
1558
1559 // todo(windows)
1560 fn on_resize(&self, callback: Box<dyn FnMut(Size<Pixels>, f32)>) {
1561 self.inner.callbacks.borrow_mut().resize = Some(callback);
1562 }
1563
1564 // todo(windows)
1565 fn on_fullscreen(&self, callback: Box<dyn FnMut(bool)>) {
1566 self.inner.callbacks.borrow_mut().fullscreen = Some(callback);
1567 }
1568
1569 // todo(windows)
1570 fn on_moved(&self, callback: Box<dyn FnMut()>) {
1571 self.inner.callbacks.borrow_mut().moved = Some(callback);
1572 }
1573
1574 // todo(windows)
1575 fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>) {
1576 self.inner.callbacks.borrow_mut().should_close = Some(callback);
1577 }
1578
1579 // todo(windows)
1580 fn on_close(&self, callback: Box<dyn FnOnce()>) {
1581 self.inner.callbacks.borrow_mut().close = Some(callback);
1582 }
1583
1584 // todo(windows)
1585 fn on_appearance_changed(&self, callback: Box<dyn FnMut()>) {
1586 self.inner.callbacks.borrow_mut().appearance_changed = Some(callback);
1587 }
1588
1589 // todo(windows)
1590 fn is_topmost_for_position(&self, _position: Point<Pixels>) -> bool {
1591 true
1592 }
1593
1594 // todo(windows)
1595 fn draw(&self, scene: &Scene) {
1596 self.inner.renderer.borrow_mut().draw(scene)
1597 }
1598
1599 // todo(windows)
1600 fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas> {
1601 self.inner.renderer.borrow().sprite_atlas().clone()
1602 }
1603
1604 fn get_raw_handle(&self) -> HWND {
1605 self.inner.hwnd
1606 }
1607}
1608
1609#[implement(IDropTarget)]
1610struct WindowsDragDropHandler(pub Rc<WindowsWindowInner>);
1611
1612#[allow(non_snake_case)]
1613impl IDropTarget_Impl for WindowsDragDropHandler {
1614 fn DragEnter(
1615 &self,
1616 pdataobj: Option<&IDataObject>,
1617 _grfkeystate: MODIFIERKEYS_FLAGS,
1618 pt: &POINTL,
1619 pdweffect: *mut DROPEFFECT,
1620 ) -> windows::core::Result<()> {
1621 unsafe {
1622 let Some(idata_obj) = pdataobj else {
1623 log::info!("no dragging file or directory detected");
1624 return Ok(());
1625 };
1626 let config = FORMATETC {
1627 cfFormat: CF_HDROP.0,
1628 ptd: std::ptr::null_mut() as _,
1629 dwAspect: DVASPECT_CONTENT.0,
1630 lindex: -1,
1631 tymed: TYMED_HGLOBAL.0 as _,
1632 };
1633 let mut paths = SmallVec::<[PathBuf; 2]>::new();
1634 if idata_obj.QueryGetData(&config as _) == S_OK {
1635 *pdweffect = DROPEFFECT_LINK;
1636 let Ok(mut idata) = idata_obj.GetData(&config as _) else {
1637 return Ok(());
1638 };
1639 if idata.u.hGlobal.is_invalid() {
1640 return Ok(());
1641 }
1642 let hdrop = idata.u.hGlobal.0 as *mut HDROP;
1643 let file_count = DragQueryFileW(*hdrop, DRAGDROP_GET_FILES_COUNT, None);
1644 for file_index in 0..file_count {
1645 let filename_length = DragQueryFileW(*hdrop, file_index, None) as usize;
1646 let mut buffer = vec![0u16; filename_length + 1];
1647 let ret = DragQueryFileW(*hdrop, file_index, Some(buffer.as_mut_slice()));
1648 if ret == 0 {
1649 log::error!("unable to read file name");
1650 continue;
1651 }
1652 if let Ok(file_name) = String::from_utf16(&buffer[0..filename_length]) {
1653 if let Ok(path) = PathBuf::from_str(&file_name) {
1654 paths.push(path);
1655 }
1656 }
1657 }
1658 ReleaseStgMedium(&mut idata);
1659 let input = PlatformInput::FileDrop(crate::FileDropEvent::Entered {
1660 position: Point {
1661 x: Pixels(pt.x as _),
1662 y: Pixels(pt.y as _),
1663 },
1664 paths: crate::ExternalPaths(paths),
1665 });
1666 self.0.handle_drag_drop(input);
1667 } else {
1668 *pdweffect = DROPEFFECT_NONE;
1669 }
1670 }
1671 Ok(())
1672 }
1673
1674 fn DragOver(
1675 &self,
1676 _grfkeystate: MODIFIERKEYS_FLAGS,
1677 pt: &POINTL,
1678 _pdweffect: *mut DROPEFFECT,
1679 ) -> windows::core::Result<()> {
1680 let input = PlatformInput::FileDrop(crate::FileDropEvent::Pending {
1681 position: Point {
1682 x: Pixels(pt.x as _),
1683 y: Pixels(pt.y as _),
1684 },
1685 });
1686 self.0.handle_drag_drop(input);
1687
1688 Ok(())
1689 }
1690
1691 fn DragLeave(&self) -> windows::core::Result<()> {
1692 let input = PlatformInput::FileDrop(crate::FileDropEvent::Exited);
1693 self.0.handle_drag_drop(input);
1694
1695 Ok(())
1696 }
1697
1698 fn Drop(
1699 &self,
1700 _pdataobj: Option<&IDataObject>,
1701 _grfkeystate: MODIFIERKEYS_FLAGS,
1702 pt: &POINTL,
1703 _pdweffect: *mut DROPEFFECT,
1704 ) -> windows::core::Result<()> {
1705 let input = PlatformInput::FileDrop(crate::FileDropEvent::Submit {
1706 position: Point {
1707 x: Pixels(pt.x as _),
1708 y: Pixels(pt.y as _),
1709 },
1710 });
1711 self.0.handle_drag_drop(input);
1712
1713 Ok(())
1714 }
1715}
1716
1717#[derive(Debug)]
1718struct ClickState {
1719 button: MouseButton,
1720 last_click: Instant,
1721 last_position: Point<DevicePixels>,
1722 current_count: usize,
1723}
1724
1725impl ClickState {
1726 pub fn new() -> Self {
1727 ClickState {
1728 button: MouseButton::Left,
1729 last_click: Instant::now(),
1730 last_position: Point::default(),
1731 current_count: 0,
1732 }
1733 }
1734
1735 /// update self and return the needed click count
1736 pub fn update(&mut self, button: MouseButton, new_position: Point<DevicePixels>) -> usize {
1737 if self.button == button && self.is_double_click(new_position) {
1738 self.current_count += 1;
1739 } else {
1740 self.current_count = 1;
1741 }
1742 self.last_click = Instant::now();
1743 self.last_position = new_position;
1744 self.button = button;
1745
1746 self.current_count
1747 }
1748
1749 #[inline]
1750 fn is_double_click(&self, new_position: Point<DevicePixels>) -> bool {
1751 let diff = self.last_position - new_position;
1752
1753 self.last_click.elapsed() < DOUBLE_CLICK_INTERVAL
1754 && diff.x.0.abs() <= DOUBLE_CLICK_SPATIAL_TOLERANCE
1755 && diff.y.0.abs() <= DOUBLE_CLICK_SPATIAL_TOLERANCE
1756 }
1757}
1758
1759fn register_wnd_class(icon_handle: HICON) -> PCWSTR {
1760 const CLASS_NAME: PCWSTR = w!("Zed::Window");
1761
1762 static ONCE: Once = Once::new();
1763 ONCE.call_once(|| {
1764 let wc = WNDCLASSW {
1765 lpfnWndProc: Some(wnd_proc),
1766 hIcon: icon_handle,
1767 lpszClassName: PCWSTR(CLASS_NAME.as_ptr()),
1768 style: CS_HREDRAW | CS_VREDRAW,
1769 ..Default::default()
1770 };
1771 unsafe { RegisterClassW(&wc) };
1772 });
1773
1774 CLASS_NAME
1775}
1776
1777unsafe extern "system" fn wnd_proc(
1778 hwnd: HWND,
1779 msg: u32,
1780 wparam: WPARAM,
1781 lparam: LPARAM,
1782) -> LRESULT {
1783 if msg == WM_NCCREATE {
1784 let cs = lparam.0 as *const CREATESTRUCTW;
1785 let cs = unsafe { &*cs };
1786 let ctx = cs.lpCreateParams as *mut WindowCreateContext;
1787 let ctx = unsafe { &mut *ctx };
1788 let inner = Rc::new(WindowsWindowInner::new(
1789 hwnd,
1790 cs,
1791 ctx.platform_inner.clone(),
1792 ctx.handle,
1793 ctx.hide_title_bar,
1794 ctx.display.clone(),
1795 ));
1796 let weak = Box::new(Rc::downgrade(&inner));
1797 unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) };
1798 ctx.inner = Some(inner);
1799 return LRESULT(1);
1800 }
1801 let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak<WindowsWindowInner>;
1802 if ptr.is_null() {
1803 return unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
1804 }
1805 let inner = unsafe { &*ptr };
1806 let r = if let Some(inner) = inner.upgrade() {
1807 inner.handle_msg(msg, wparam, lparam)
1808 } else {
1809 unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) }
1810 };
1811 if msg == WM_NCDESTROY {
1812 unsafe { set_window_long(hwnd, GWLP_USERDATA, 0) };
1813 unsafe { drop(Box::from_raw(ptr)) };
1814 }
1815 r
1816}
1817
1818pub(crate) fn try_get_window_inner(hwnd: HWND) -> Option<Rc<WindowsWindowInner>> {
1819 if hwnd == HWND(0) {
1820 return None;
1821 }
1822
1823 let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak<WindowsWindowInner>;
1824 if !ptr.is_null() {
1825 let inner = unsafe { &*ptr };
1826 inner.upgrade()
1827 } else {
1828 None
1829 }
1830}
1831
1832fn basic_vkcode_to_string(code: u16, modifiers: Modifiers) -> Option<Keystroke> {
1833 match code {
1834 // VK_0 - VK_9
1835 48..=57 => Some(Keystroke {
1836 modifiers,
1837 key: format!("{}", code - VK_0.0),
1838 ime_key: None,
1839 }),
1840 // VK_A - VK_Z
1841 65..=90 => Some(Keystroke {
1842 modifiers,
1843 key: format!("{}", (b'a' + code as u8 - VK_A.0 as u8) as char),
1844 ime_key: None,
1845 }),
1846 // VK_F1 - VK_F24
1847 112..=135 => Some(Keystroke {
1848 modifiers,
1849 key: format!("f{}", code - VK_F1.0 + 1),
1850 ime_key: None,
1851 }),
1852 // OEM3: `/~, OEM_MINUS: -/_, OEM_PLUS: =/+, ...
1853 _ => {
1854 if let Some(key) = oemkey_vkcode_to_string(code) {
1855 Some(Keystroke {
1856 modifiers,
1857 key,
1858 ime_key: None,
1859 })
1860 } else {
1861 None
1862 }
1863 }
1864 }
1865}
1866
1867fn oemkey_vkcode_to_string(code: u16) -> Option<String> {
1868 match code {
1869 186 => Some(";".to_string()), // VK_OEM_1
1870 187 => Some("=".to_string()), // VK_OEM_PLUS
1871 188 => Some(",".to_string()), // VK_OEM_COMMA
1872 189 => Some("-".to_string()), // VK_OEM_MINUS
1873 190 => Some(".".to_string()), // VK_OEM_PERIOD
1874 // https://kbdlayout.info/features/virtualkeys/VK_ABNT_C1
1875 191 | 193 => Some("/".to_string()), // VK_OEM_2 VK_ABNT_C1
1876 192 => Some("`".to_string()), // VK_OEM_3
1877 219 => Some("[".to_string()), // VK_OEM_4
1878 220 => Some("\\".to_string()), // VK_OEM_5
1879 221 => Some("]".to_string()), // VK_OEM_6
1880 222 => Some("'".to_string()), // VK_OEM_7
1881 _ => None,
1882 }
1883}
1884
1885#[inline]
1886fn logical_size(physical_size: Size<DevicePixels>, scale_factor: f32) -> Size<Pixels> {
1887 Size {
1888 width: px(physical_size.width.0 as f32 / scale_factor),
1889 height: px(physical_size.height.0 as f32 / scale_factor),
1890 }
1891}
1892
1893#[inline]
1894fn logical_point(x: f32, y: f32, scale_factor: f32) -> Point<Pixels> {
1895 Point {
1896 x: px(x / scale_factor),
1897 y: px(y / scale_factor),
1898 }
1899}
1900
1901struct StyleAndBounds {
1902 style: WINDOW_STYLE,
1903 x: i32,
1904 y: i32,
1905 cx: i32,
1906 cy: i32,
1907}
1908
1909// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
1910const DRAGDROP_GET_FILES_COUNT: u32 = 0xFFFFFFFF;
1911// https://learn.microsoft.com/en-us/windows/win32/controls/ttm-setdelaytime?redirectedfrom=MSDN
1912const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(500);
1913// https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getsystemmetrics
1914const DOUBLE_CLICK_SPATIAL_TOLERANCE: i32 = 4;
1915const SIZE_MOVE_LOOP_TIMER_ID: usize = 1;
1916
1917#[cfg(test)]
1918mod tests {
1919 use super::ClickState;
1920 use crate::{point, DevicePixels, MouseButton};
1921 use std::time::Duration;
1922
1923 #[test]
1924 fn test_double_click_interval() {
1925 let mut state = ClickState::new();
1926 assert_eq!(
1927 state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))),
1928 1
1929 );
1930 assert_eq!(
1931 state.update(MouseButton::Right, point(DevicePixels(0), DevicePixels(0))),
1932 1
1933 );
1934 assert_eq!(
1935 state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))),
1936 1
1937 );
1938 assert_eq!(
1939 state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))),
1940 2
1941 );
1942 state.last_click -= Duration::from_millis(700);
1943 assert_eq!(
1944 state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(0))),
1945 1
1946 );
1947 }
1948
1949 #[test]
1950 fn test_double_click_spatial_tolerance() {
1951 let mut state = ClickState::new();
1952 assert_eq!(
1953 state.update(MouseButton::Left, point(DevicePixels(-3), DevicePixels(0))),
1954 1
1955 );
1956 assert_eq!(
1957 state.update(MouseButton::Left, point(DevicePixels(0), DevicePixels(3))),
1958 2
1959 );
1960 assert_eq!(
1961 state.update(MouseButton::Right, point(DevicePixels(3), DevicePixels(2))),
1962 1
1963 );
1964 assert_eq!(
1965 state.update(MouseButton::Right, point(DevicePixels(10), DevicePixels(0))),
1966 1
1967 );
1968 }
1969}