1use std::rc::Rc;
2
3use ::util::ResultExt;
4use anyhow::Context as _;
5use windows::{
6 Win32::{
7 Foundation::*,
8 Graphics::Gdi::*,
9 System::SystemServices::*,
10 UI::{
11 Controls::*,
12 HiDpi::*,
13 Input::{Ime::*, KeyboardAndMouse::*},
14 WindowsAndMessaging::*,
15 },
16 },
17 core::PCWSTR,
18};
19
20use crate::*;
21
22pub(crate) const WM_GPUI_CURSOR_STYLE_CHANGED: u32 = WM_USER + 1;
23pub(crate) const WM_GPUI_CLOSE_ONE_WINDOW: u32 = WM_USER + 2;
24pub(crate) const WM_GPUI_TASK_DISPATCHED_ON_MAIN_THREAD: u32 = WM_USER + 3;
25pub(crate) const WM_GPUI_DOCK_MENU_ACTION: u32 = WM_USER + 4;
26
27const SIZE_MOVE_LOOP_TIMER_ID: usize = 1;
28const AUTO_HIDE_TASKBAR_THICKNESS_PX: i32 = 1;
29
30pub(crate) fn handle_msg(
31 handle: HWND,
32 msg: u32,
33 wparam: WPARAM,
34 lparam: LPARAM,
35 state_ptr: Rc<WindowsWindowStatePtr>,
36) -> LRESULT {
37 let handled = match msg {
38 WM_ACTIVATE => handle_activate_msg(wparam, state_ptr),
39 WM_CREATE => handle_create_msg(handle, state_ptr),
40 WM_MOVE => handle_move_msg(handle, lparam, state_ptr),
41 WM_SIZE => handle_size_msg(wparam, lparam, state_ptr),
42 WM_GETMINMAXINFO => handle_get_min_max_info_msg(lparam, state_ptr),
43 WM_ENTERSIZEMOVE | WM_ENTERMENULOOP => handle_size_move_loop(handle),
44 WM_EXITSIZEMOVE | WM_EXITMENULOOP => handle_size_move_loop_exit(handle),
45 WM_TIMER => handle_timer_msg(handle, wparam, state_ptr),
46 WM_NCCALCSIZE => handle_calc_client_size(handle, wparam, lparam, state_ptr),
47 WM_DPICHANGED => handle_dpi_changed_msg(handle, wparam, lparam, state_ptr),
48 WM_DISPLAYCHANGE => handle_display_change_msg(handle, state_ptr),
49 WM_NCHITTEST => handle_hit_test_msg(handle, msg, wparam, lparam, state_ptr),
50 WM_PAINT => handle_paint_msg(handle, state_ptr),
51 WM_CLOSE => handle_close_msg(handle, state_ptr),
52 WM_DESTROY => handle_destroy_msg(handle, state_ptr),
53 WM_MOUSEMOVE => handle_mouse_move_msg(handle, lparam, wparam, state_ptr),
54 WM_MOUSELEAVE | WM_NCMOUSELEAVE => handle_mouse_leave_msg(state_ptr),
55 WM_NCMOUSEMOVE => handle_nc_mouse_move_msg(handle, lparam, state_ptr),
56 WM_NCLBUTTONDOWN => {
57 handle_nc_mouse_down_msg(handle, MouseButton::Left, wparam, lparam, state_ptr)
58 }
59 WM_NCRBUTTONDOWN => {
60 handle_nc_mouse_down_msg(handle, MouseButton::Right, wparam, lparam, state_ptr)
61 }
62 WM_NCMBUTTONDOWN => {
63 handle_nc_mouse_down_msg(handle, MouseButton::Middle, wparam, lparam, state_ptr)
64 }
65 WM_NCLBUTTONUP => {
66 handle_nc_mouse_up_msg(handle, MouseButton::Left, wparam, lparam, state_ptr)
67 }
68 WM_NCRBUTTONUP => {
69 handle_nc_mouse_up_msg(handle, MouseButton::Right, wparam, lparam, state_ptr)
70 }
71 WM_NCMBUTTONUP => {
72 handle_nc_mouse_up_msg(handle, MouseButton::Middle, wparam, lparam, state_ptr)
73 }
74 WM_LBUTTONDOWN => handle_mouse_down_msg(handle, MouseButton::Left, lparam, state_ptr),
75 WM_RBUTTONDOWN => handle_mouse_down_msg(handle, MouseButton::Right, lparam, state_ptr),
76 WM_MBUTTONDOWN => handle_mouse_down_msg(handle, MouseButton::Middle, lparam, state_ptr),
77 WM_XBUTTONDOWN => {
78 handle_xbutton_msg(handle, wparam, lparam, handle_mouse_down_msg, state_ptr)
79 }
80 WM_LBUTTONUP => handle_mouse_up_msg(handle, MouseButton::Left, lparam, state_ptr),
81 WM_RBUTTONUP => handle_mouse_up_msg(handle, MouseButton::Right, lparam, state_ptr),
82 WM_MBUTTONUP => handle_mouse_up_msg(handle, MouseButton::Middle, lparam, state_ptr),
83 WM_XBUTTONUP => handle_xbutton_msg(handle, wparam, lparam, handle_mouse_up_msg, state_ptr),
84 WM_MOUSEWHEEL => handle_mouse_wheel_msg(handle, wparam, lparam, state_ptr),
85 WM_MOUSEHWHEEL => handle_mouse_horizontal_wheel_msg(handle, wparam, lparam, state_ptr),
86 WM_SYSKEYDOWN => handle_syskeydown_msg(handle, wparam, lparam, state_ptr),
87 WM_SYSKEYUP => handle_syskeyup_msg(handle, wparam, lparam, state_ptr),
88 WM_SYSCOMMAND => handle_system_command(wparam, state_ptr),
89 WM_KEYDOWN => handle_keydown_msg(handle, wparam, lparam, state_ptr),
90 WM_KEYUP => handle_keyup_msg(handle, wparam, lparam, state_ptr),
91 WM_CHAR => handle_char_msg(wparam, state_ptr),
92 WM_DEADCHAR => handle_dead_char_msg(wparam, state_ptr),
93 WM_IME_STARTCOMPOSITION => handle_ime_position(handle, state_ptr),
94 WM_IME_COMPOSITION => handle_ime_composition(handle, lparam, state_ptr),
95 WM_SETCURSOR => handle_set_cursor(handle, lparam, state_ptr),
96 WM_SETTINGCHANGE => handle_system_settings_changed(handle, wparam, lparam, state_ptr),
97 WM_INPUTLANGCHANGE => handle_input_language_changed(lparam, state_ptr),
98 WM_GPUI_CURSOR_STYLE_CHANGED => handle_cursor_changed(lparam, state_ptr),
99 _ => None,
100 };
101 if let Some(n) = handled {
102 LRESULT(n)
103 } else {
104 unsafe { DefWindowProcW(handle, msg, wparam, lparam) }
105 }
106}
107
108fn handle_move_msg(
109 handle: HWND,
110 lparam: LPARAM,
111 state_ptr: Rc<WindowsWindowStatePtr>,
112) -> Option<isize> {
113 let mut lock = state_ptr.state.borrow_mut();
114 let origin = logical_point(
115 lparam.signed_loword() as f32,
116 lparam.signed_hiword() as f32,
117 lock.scale_factor,
118 );
119 lock.origin = origin;
120 let size = lock.logical_size;
121 let center_x = origin.x.0 + size.width.0 / 2.;
122 let center_y = origin.y.0 + size.height.0 / 2.;
123 let monitor_bounds = lock.display.bounds();
124 if center_x < monitor_bounds.left().0
125 || center_x > monitor_bounds.right().0
126 || center_y < monitor_bounds.top().0
127 || center_y > monitor_bounds.bottom().0
128 {
129 // center of the window may have moved to another monitor
130 let monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) };
131 // minimize the window can trigger this event too, in this case,
132 // monitor is invalid, we do nothing.
133 if !monitor.is_invalid() && lock.display.handle != monitor {
134 // we will get the same monitor if we only have one
135 lock.display = WindowsDisplay::new_with_handle(monitor);
136 }
137 }
138 if let Some(mut callback) = lock.callbacks.moved.take() {
139 drop(lock);
140 callback();
141 state_ptr.state.borrow_mut().callbacks.moved = Some(callback);
142 }
143 Some(0)
144}
145
146fn handle_get_min_max_info_msg(
147 lparam: LPARAM,
148 state_ptr: Rc<WindowsWindowStatePtr>,
149) -> Option<isize> {
150 let lock = state_ptr.state.borrow();
151 let min_size = lock.min_size?;
152 let scale_factor = lock.scale_factor;
153 let boarder_offset = lock.border_offset;
154 drop(lock);
155 unsafe {
156 let minmax_info = &mut *(lparam.0 as *mut MINMAXINFO);
157 minmax_info.ptMinTrackSize.x =
158 min_size.width.scale(scale_factor).0 as i32 + boarder_offset.width_offset;
159 minmax_info.ptMinTrackSize.y =
160 min_size.height.scale(scale_factor).0 as i32 + boarder_offset.height_offset;
161 }
162 Some(0)
163}
164
165fn handle_size_msg(
166 wparam: WPARAM,
167 lparam: LPARAM,
168 state_ptr: Rc<WindowsWindowStatePtr>,
169) -> Option<isize> {
170 let mut lock = state_ptr.state.borrow_mut();
171
172 // Don't resize the renderer when the window is minimized, but record that it was minimized so
173 // that on restore the swap chain can be recreated via `update_drawable_size_even_if_unchanged`.
174 if wparam.0 == SIZE_MINIMIZED as usize {
175 lock.restore_from_minimized = lock.callbacks.request_frame.take();
176 return Some(0);
177 }
178
179 let width = lparam.loword().max(1) as i32;
180 let height = lparam.hiword().max(1) as i32;
181 let new_size = size(DevicePixels(width), DevicePixels(height));
182 let scale_factor = lock.scale_factor;
183 if lock.restore_from_minimized.is_some() {
184 lock.callbacks.request_frame = lock.restore_from_minimized.take();
185 } else {
186 lock.renderer.resize(new_size).log_err();
187 }
188 let new_size = new_size.to_pixels(scale_factor);
189 lock.logical_size = new_size;
190 if let Some(mut callback) = lock.callbacks.resize.take() {
191 drop(lock);
192 callback(new_size, scale_factor);
193 state_ptr.state.borrow_mut().callbacks.resize = Some(callback);
194 }
195 Some(0)
196}
197
198fn handle_size_move_loop(handle: HWND) -> Option<isize> {
199 unsafe {
200 let ret = SetTimer(
201 Some(handle),
202 SIZE_MOVE_LOOP_TIMER_ID,
203 USER_TIMER_MINIMUM,
204 None,
205 );
206 if ret == 0 {
207 log::error!(
208 "unable to create timer: {}",
209 std::io::Error::last_os_error()
210 );
211 }
212 }
213 None
214}
215
216fn handle_size_move_loop_exit(handle: HWND) -> Option<isize> {
217 unsafe {
218 KillTimer(Some(handle), SIZE_MOVE_LOOP_TIMER_ID).log_err();
219 }
220 None
221}
222
223fn handle_timer_msg(
224 handle: HWND,
225 wparam: WPARAM,
226 state_ptr: Rc<WindowsWindowStatePtr>,
227) -> Option<isize> {
228 if wparam.0 == SIZE_MOVE_LOOP_TIMER_ID {
229 for runnable in state_ptr.main_receiver.drain() {
230 runnable.run();
231 }
232 handle_paint_msg(handle, state_ptr)
233 } else {
234 None
235 }
236}
237
238fn handle_paint_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
239 let mut lock = state_ptr.state.borrow_mut();
240 let is_idle = if let Some(mut request_frame) = lock.callbacks.request_frame.take() {
241 drop(lock);
242 // request_frame(RequestFrameOptions {
243 // require_presentation: true,
244 // });
245 let is_idle = request_frame(Default::default());
246 state_ptr.state.borrow_mut().callbacks.request_frame = Some(request_frame);
247 is_idle
248 } else {
249 false
250 };
251 unsafe { ValidateRect(Some(handle), None).ok().log_err() };
252 if is_idle {
253 unsafe {
254 MsgWaitForMultipleObjects(None, false, 100, QS_ALLINPUT);
255 }
256 }
257 Some(0)
258}
259
260fn handle_close_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
261 let mut lock = state_ptr.state.borrow_mut();
262 let output = if let Some(mut callback) = lock.callbacks.should_close.take() {
263 drop(lock);
264 let should_close = callback();
265 state_ptr.state.borrow_mut().callbacks.should_close = Some(callback);
266 if should_close { None } else { Some(0) }
267 } else {
268 None
269 };
270
271 // Workaround as window close animation is not played with `WS_EX_LAYERED` enabled.
272 if output.is_none() {
273 unsafe {
274 let current_style = get_window_long(handle, GWL_EXSTYLE);
275 set_window_long(
276 handle,
277 GWL_EXSTYLE,
278 current_style & !WS_EX_LAYERED.0 as isize,
279 );
280 }
281 }
282
283 output
284}
285
286fn handle_destroy_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
287 let callback = {
288 let mut lock = state_ptr.state.borrow_mut();
289 lock.callbacks.close.take()
290 };
291 if let Some(callback) = callback {
292 callback();
293 }
294 unsafe {
295 PostThreadMessageW(
296 state_ptr.main_thread_id_win32,
297 WM_GPUI_CLOSE_ONE_WINDOW,
298 WPARAM(state_ptr.validation_number),
299 LPARAM(handle.0 as isize),
300 )
301 .log_err();
302 }
303 Some(0)
304}
305
306fn handle_mouse_move_msg(
307 handle: HWND,
308 lparam: LPARAM,
309 wparam: WPARAM,
310 state_ptr: Rc<WindowsWindowStatePtr>,
311) -> Option<isize> {
312 start_tracking_mouse(handle, &state_ptr, TME_LEAVE);
313
314 let mut lock = state_ptr.state.borrow_mut();
315 let Some(mut func) = lock.callbacks.input.take() else {
316 return Some(1);
317 };
318 let scale_factor = lock.scale_factor;
319 drop(lock);
320
321 let pressed_button = match MODIFIERKEYS_FLAGS(wparam.loword() as u32) {
322 flags if flags.contains(MK_LBUTTON) => Some(MouseButton::Left),
323 flags if flags.contains(MK_RBUTTON) => Some(MouseButton::Right),
324 flags if flags.contains(MK_MBUTTON) => Some(MouseButton::Middle),
325 flags if flags.contains(MK_XBUTTON1) => {
326 Some(MouseButton::Navigate(NavigationDirection::Back))
327 }
328 flags if flags.contains(MK_XBUTTON2) => {
329 Some(MouseButton::Navigate(NavigationDirection::Forward))
330 }
331 _ => None,
332 };
333 let x = lparam.signed_loword() as f32;
334 let y = lparam.signed_hiword() as f32;
335 let input = PlatformInput::MouseMove(MouseMoveEvent {
336 position: logical_point(x, y, scale_factor),
337 pressed_button,
338 modifiers: current_modifiers(),
339 });
340 let handled = !func(input).propagate;
341 state_ptr.state.borrow_mut().callbacks.input = Some(func);
342
343 if handled { Some(0) } else { Some(1) }
344}
345
346fn handle_mouse_leave_msg(state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
347 let mut lock = state_ptr.state.borrow_mut();
348 lock.hovered = false;
349 if let Some(mut callback) = lock.callbacks.hovered_status_change.take() {
350 drop(lock);
351 callback(false);
352 state_ptr.state.borrow_mut().callbacks.hovered_status_change = Some(callback);
353 }
354
355 Some(0)
356}
357
358fn handle_syskeydown_msg(
359 handle: HWND,
360 wparam: WPARAM,
361 lparam: LPARAM,
362 state_ptr: Rc<WindowsWindowStatePtr>,
363) -> Option<isize> {
364 let mut lock = state_ptr.state.borrow_mut();
365 let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
366 PlatformInput::KeyDown(KeyDownEvent {
367 keystroke,
368 is_held: lparam.0 & (0x1 << 30) > 0,
369 })
370 })?;
371 let mut func = lock.callbacks.input.take()?;
372 drop(lock);
373
374 let handled = !func(input).propagate;
375
376 let mut lock = state_ptr.state.borrow_mut();
377 lock.callbacks.input = Some(func);
378
379 if handled {
380 lock.system_key_handled = true;
381 Some(0)
382 } else {
383 // we need to call `DefWindowProcW`, or we will lose the system-wide `Alt+F4`, `Alt+{other keys}`
384 // shortcuts.
385 None
386 }
387}
388
389fn handle_syskeyup_msg(
390 handle: HWND,
391 wparam: WPARAM,
392 lparam: LPARAM,
393 state_ptr: Rc<WindowsWindowStatePtr>,
394) -> Option<isize> {
395 let mut lock = state_ptr.state.borrow_mut();
396 let input = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
397 PlatformInput::KeyUp(KeyUpEvent { keystroke })
398 })?;
399 let mut func = lock.callbacks.input.take()?;
400 drop(lock);
401 func(input);
402 state_ptr.state.borrow_mut().callbacks.input = Some(func);
403
404 // Always return 0 to indicate that the message was handled, so we could properly handle `ModifiersChanged` event.
405 Some(0)
406}
407
408// It's a known bug that you can't trigger `ctrl-shift-0`. See:
409// https://superuser.com/questions/1455762/ctrl-shift-number-key-combination-has-stopped-working-for-a-few-numbers
410fn handle_keydown_msg(
411 handle: HWND,
412 wparam: WPARAM,
413 lparam: LPARAM,
414 state_ptr: Rc<WindowsWindowStatePtr>,
415) -> Option<isize> {
416 let mut lock = state_ptr.state.borrow_mut();
417 let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
418 PlatformInput::KeyDown(KeyDownEvent {
419 keystroke,
420 is_held: lparam.0 & (0x1 << 30) > 0,
421 })
422 }) else {
423 return Some(1);
424 };
425 drop(lock);
426
427 let is_composing = with_input_handler(&state_ptr, |input_handler| {
428 input_handler.marked_text_range()
429 })
430 .flatten()
431 .is_some();
432 if is_composing {
433 translate_message(handle, wparam, lparam);
434 return Some(0);
435 }
436
437 let Some(mut func) = state_ptr.state.borrow_mut().callbacks.input.take() else {
438 return Some(1);
439 };
440
441 let handled = !func(input).propagate;
442
443 state_ptr.state.borrow_mut().callbacks.input = Some(func);
444
445 if handled {
446 Some(0)
447 } else {
448 translate_message(handle, wparam, lparam);
449 Some(1)
450 }
451}
452
453fn handle_keyup_msg(
454 handle: HWND,
455 wparam: WPARAM,
456 lparam: LPARAM,
457 state_ptr: Rc<WindowsWindowStatePtr>,
458) -> Option<isize> {
459 let mut lock = state_ptr.state.borrow_mut();
460 let Some(input) = handle_key_event(handle, wparam, lparam, &mut lock, |keystroke| {
461 PlatformInput::KeyUp(KeyUpEvent { keystroke })
462 }) else {
463 return Some(1);
464 };
465
466 let Some(mut func) = lock.callbacks.input.take() else {
467 return Some(1);
468 };
469 drop(lock);
470
471 let handled = !func(input).propagate;
472 state_ptr.state.borrow_mut().callbacks.input = Some(func);
473
474 if handled { Some(0) } else { Some(1) }
475}
476
477fn handle_char_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
478 let input = parse_char_message(wparam, &state_ptr)?;
479 with_input_handler(&state_ptr, |input_handler| {
480 input_handler.replace_text_in_range(None, &input);
481 });
482
483 Some(0)
484}
485
486fn handle_dead_char_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
487 let ch = char::from_u32(wparam.0 as u32)?.to_string();
488 with_input_handler(&state_ptr, |input_handler| {
489 input_handler.replace_and_mark_text_in_range(None, &ch, None);
490 });
491 None
492}
493
494fn handle_mouse_down_msg(
495 handle: HWND,
496 button: MouseButton,
497 lparam: LPARAM,
498 state_ptr: Rc<WindowsWindowStatePtr>,
499) -> Option<isize> {
500 unsafe { SetCapture(handle) };
501 let mut lock = state_ptr.state.borrow_mut();
502 let Some(mut func) = lock.callbacks.input.take() else {
503 return Some(1);
504 };
505 let x = lparam.signed_loword();
506 let y = lparam.signed_hiword();
507 let physical_point = point(DevicePixels(x as i32), DevicePixels(y as i32));
508 let click_count = lock.click_state.update(button, physical_point);
509 let scale_factor = lock.scale_factor;
510 drop(lock);
511
512 let input = PlatformInput::MouseDown(MouseDownEvent {
513 button,
514 position: logical_point(x as f32, y as f32, scale_factor),
515 modifiers: current_modifiers(),
516 click_count,
517 first_mouse: false,
518 });
519 let handled = !func(input).propagate;
520 state_ptr.state.borrow_mut().callbacks.input = Some(func);
521
522 if handled { Some(0) } else { Some(1) }
523}
524
525fn handle_mouse_up_msg(
526 _handle: HWND,
527 button: MouseButton,
528 lparam: LPARAM,
529 state_ptr: Rc<WindowsWindowStatePtr>,
530) -> Option<isize> {
531 unsafe { ReleaseCapture().log_err() };
532 let mut lock = state_ptr.state.borrow_mut();
533 let Some(mut func) = lock.callbacks.input.take() else {
534 return Some(1);
535 };
536 let x = lparam.signed_loword() as f32;
537 let y = lparam.signed_hiword() as f32;
538 let click_count = lock.click_state.current_count;
539 let scale_factor = lock.scale_factor;
540 drop(lock);
541
542 let input = PlatformInput::MouseUp(MouseUpEvent {
543 button,
544 position: logical_point(x, y, scale_factor),
545 modifiers: current_modifiers(),
546 click_count,
547 });
548 let handled = !func(input).propagate;
549 state_ptr.state.borrow_mut().callbacks.input = Some(func);
550
551 if handled { Some(0) } else { Some(1) }
552}
553
554fn handle_xbutton_msg(
555 handle: HWND,
556 wparam: WPARAM,
557 lparam: LPARAM,
558 handler: impl Fn(HWND, MouseButton, LPARAM, Rc<WindowsWindowStatePtr>) -> Option<isize>,
559 state_ptr: Rc<WindowsWindowStatePtr>,
560) -> Option<isize> {
561 let nav_dir = match wparam.hiword() {
562 XBUTTON1 => NavigationDirection::Back,
563 XBUTTON2 => NavigationDirection::Forward,
564 _ => return Some(1),
565 };
566 handler(handle, MouseButton::Navigate(nav_dir), lparam, state_ptr)
567}
568
569fn handle_mouse_wheel_msg(
570 handle: HWND,
571 wparam: WPARAM,
572 lparam: LPARAM,
573 state_ptr: Rc<WindowsWindowStatePtr>,
574) -> Option<isize> {
575 let modifiers = current_modifiers();
576 let mut lock = state_ptr.state.borrow_mut();
577 let Some(mut func) = lock.callbacks.input.take() else {
578 return Some(1);
579 };
580 let scale_factor = lock.scale_factor;
581 let wheel_scroll_amount = match modifiers.shift {
582 true => lock.system_settings.mouse_wheel_settings.wheel_scroll_chars,
583 false => lock.system_settings.mouse_wheel_settings.wheel_scroll_lines,
584 };
585 drop(lock);
586
587 let wheel_distance =
588 (wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_amount as f32;
589 let mut cursor_point = POINT {
590 x: lparam.signed_loword().into(),
591 y: lparam.signed_hiword().into(),
592 };
593 unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
594 let input = PlatformInput::ScrollWheel(ScrollWheelEvent {
595 position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
596 delta: ScrollDelta::Lines(match modifiers.shift {
597 true => Point {
598 x: wheel_distance,
599 y: 0.0,
600 },
601 false => Point {
602 y: wheel_distance,
603 x: 0.0,
604 },
605 }),
606 modifiers,
607 touch_phase: TouchPhase::Moved,
608 });
609 let handled = !func(input).propagate;
610 state_ptr.state.borrow_mut().callbacks.input = Some(func);
611
612 if handled { Some(0) } else { Some(1) }
613}
614
615fn handle_mouse_horizontal_wheel_msg(
616 handle: HWND,
617 wparam: WPARAM,
618 lparam: LPARAM,
619 state_ptr: Rc<WindowsWindowStatePtr>,
620) -> Option<isize> {
621 let mut lock = state_ptr.state.borrow_mut();
622 let Some(mut func) = lock.callbacks.input.take() else {
623 return Some(1);
624 };
625 let scale_factor = lock.scale_factor;
626 let wheel_scroll_chars = lock.system_settings.mouse_wheel_settings.wheel_scroll_chars;
627 drop(lock);
628
629 let wheel_distance =
630 (-wparam.signed_hiword() as f32 / WHEEL_DELTA as f32) * wheel_scroll_chars as f32;
631 let mut cursor_point = POINT {
632 x: lparam.signed_loword().into(),
633 y: lparam.signed_hiword().into(),
634 };
635 unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
636 let event = PlatformInput::ScrollWheel(ScrollWheelEvent {
637 position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
638 delta: ScrollDelta::Lines(Point {
639 x: wheel_distance,
640 y: 0.0,
641 }),
642 modifiers: current_modifiers(),
643 touch_phase: TouchPhase::Moved,
644 });
645 let handled = !func(event).propagate;
646 state_ptr.state.borrow_mut().callbacks.input = Some(func);
647
648 if handled { Some(0) } else { Some(1) }
649}
650
651fn retrieve_caret_position(state_ptr: &Rc<WindowsWindowStatePtr>) -> Option<POINT> {
652 with_input_handler_and_scale_factor(state_ptr, |input_handler, scale_factor| {
653 let caret_range = input_handler.selected_text_range(false)?;
654 let caret_position = input_handler.bounds_for_range(caret_range.range)?;
655 Some(POINT {
656 // logical to physical
657 x: (caret_position.origin.x.0 * scale_factor) as i32,
658 y: (caret_position.origin.y.0 * scale_factor) as i32
659 + ((caret_position.size.height.0 * scale_factor) as i32 / 2),
660 })
661 })
662}
663
664fn handle_ime_position(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
665 unsafe {
666 let ctx = ImmGetContext(handle);
667
668 let Some(caret_position) = retrieve_caret_position(&state_ptr) else {
669 return Some(0);
670 };
671 {
672 let config = COMPOSITIONFORM {
673 dwStyle: CFS_POINT,
674 ptCurrentPos: caret_position,
675 ..Default::default()
676 };
677 ImmSetCompositionWindow(ctx, &config as _).ok().log_err();
678 }
679 {
680 let config = CANDIDATEFORM {
681 dwStyle: CFS_CANDIDATEPOS,
682 ptCurrentPos: caret_position,
683 ..Default::default()
684 };
685 ImmSetCandidateWindow(ctx, &config as _).ok().log_err();
686 }
687 ImmReleaseContext(handle, ctx).ok().log_err();
688 Some(0)
689 }
690}
691
692fn handle_ime_composition(
693 handle: HWND,
694 lparam: LPARAM,
695 state_ptr: Rc<WindowsWindowStatePtr>,
696) -> Option<isize> {
697 let ctx = unsafe { ImmGetContext(handle) };
698 let result = handle_ime_composition_inner(ctx, lparam, state_ptr);
699 unsafe { ImmReleaseContext(handle, ctx).ok().log_err() };
700 result
701}
702
703fn handle_ime_composition_inner(
704 ctx: HIMC,
705 lparam: LPARAM,
706 state_ptr: Rc<WindowsWindowStatePtr>,
707) -> Option<isize> {
708 let lparam = lparam.0 as u32;
709 if lparam == 0 {
710 // Japanese IME may send this message with lparam = 0, which indicates that
711 // there is no composition string.
712 with_input_handler(&state_ptr, |input_handler| {
713 input_handler.replace_text_in_range(None, "");
714 })?;
715 Some(0)
716 } else {
717 if lparam & GCS_COMPSTR.0 > 0 {
718 let comp_string = parse_ime_composition_string(ctx, GCS_COMPSTR)?;
719 let caret_pos = (!comp_string.is_empty() && lparam & GCS_CURSORPOS.0 > 0).then(|| {
720 let pos = retrieve_composition_cursor_position(ctx);
721 pos..pos
722 });
723 with_input_handler(&state_ptr, |input_handler| {
724 input_handler.replace_and_mark_text_in_range(None, &comp_string, caret_pos);
725 })?;
726 }
727 if lparam & GCS_RESULTSTR.0 > 0 {
728 let comp_result = parse_ime_composition_string(ctx, GCS_RESULTSTR)?;
729 with_input_handler(&state_ptr, |input_handler| {
730 input_handler.replace_text_in_range(None, &comp_result);
731 })?;
732 return Some(0);
733 }
734
735 // currently, we don't care other stuff
736 None
737 }
738}
739
740/// SEE: https://learn.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize
741fn handle_calc_client_size(
742 handle: HWND,
743 wparam: WPARAM,
744 lparam: LPARAM,
745 state_ptr: Rc<WindowsWindowStatePtr>,
746) -> Option<isize> {
747 if !state_ptr.hide_title_bar || state_ptr.state.borrow().is_fullscreen() || wparam.0 == 0 {
748 return None;
749 }
750
751 let is_maximized = state_ptr.state.borrow().is_maximized();
752 let insets = get_client_area_insets(handle, is_maximized, state_ptr.windows_version);
753 // wparam is TRUE so lparam points to an NCCALCSIZE_PARAMS structure
754 let mut params = lparam.0 as *mut NCCALCSIZE_PARAMS;
755 let mut requested_client_rect = unsafe { &mut ((*params).rgrc) };
756
757 requested_client_rect[0].left += insets.left;
758 requested_client_rect[0].top += insets.top;
759 requested_client_rect[0].right -= insets.right;
760 requested_client_rect[0].bottom -= insets.bottom;
761
762 // Fix auto hide taskbar not showing. This solution is based on the approach
763 // used by Chrome. However, it may result in one row of pixels being obscured
764 // in our client area. But as Chrome says, "there seems to be no better solution."
765 if is_maximized {
766 if let Some(ref taskbar_position) = state_ptr
767 .state
768 .borrow()
769 .system_settings
770 .auto_hide_taskbar_position
771 {
772 // Fot the auto-hide taskbar, adjust in by 1 pixel on taskbar edge,
773 // so the window isn't treated as a "fullscreen app", which would cause
774 // the taskbar to disappear.
775 match taskbar_position {
776 AutoHideTaskbarPosition::Left => {
777 requested_client_rect[0].left += AUTO_HIDE_TASKBAR_THICKNESS_PX
778 }
779 AutoHideTaskbarPosition::Top => {
780 requested_client_rect[0].top += AUTO_HIDE_TASKBAR_THICKNESS_PX
781 }
782 AutoHideTaskbarPosition::Right => {
783 requested_client_rect[0].right -= AUTO_HIDE_TASKBAR_THICKNESS_PX
784 }
785 AutoHideTaskbarPosition::Bottom => {
786 requested_client_rect[0].bottom -= AUTO_HIDE_TASKBAR_THICKNESS_PX
787 }
788 }
789 }
790 }
791
792 Some(0)
793}
794
795fn handle_activate_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
796 let activated = wparam.loword() > 0;
797 let this = state_ptr.clone();
798 state_ptr
799 .executor
800 .spawn(async move {
801 let mut lock = this.state.borrow_mut();
802 if let Some(mut func) = lock.callbacks.active_status_change.take() {
803 drop(lock);
804 func(activated);
805 this.state.borrow_mut().callbacks.active_status_change = Some(func);
806 }
807 })
808 .detach();
809
810 None
811}
812
813fn handle_create_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
814 if state_ptr.hide_title_bar {
815 notify_frame_changed(handle);
816 Some(0)
817 } else {
818 None
819 }
820}
821
822fn handle_dpi_changed_msg(
823 handle: HWND,
824 wparam: WPARAM,
825 lparam: LPARAM,
826 state_ptr: Rc<WindowsWindowStatePtr>,
827) -> Option<isize> {
828 let new_dpi = wparam.loword() as f32;
829 let mut lock = state_ptr.state.borrow_mut();
830 lock.scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32;
831 lock.border_offset.update(handle).log_err();
832 drop(lock);
833
834 let rect = unsafe { &*(lparam.0 as *const RECT) };
835 let width = rect.right - rect.left;
836 let height = rect.bottom - rect.top;
837 // this will emit `WM_SIZE` and `WM_MOVE` right here
838 // even before this function returns
839 // the new size is handled in `WM_SIZE`
840 unsafe {
841 SetWindowPos(
842 handle,
843 None,
844 rect.left,
845 rect.top,
846 width,
847 height,
848 SWP_NOZORDER | SWP_NOACTIVATE,
849 )
850 .context("unable to set window position after dpi has changed")
851 .log_err();
852 }
853
854 Some(0)
855}
856
857/// The following conditions will trigger this event:
858/// 1. The monitor on which the window is located goes offline or changes resolution.
859/// 2. Another monitor goes offline, is plugged in, or changes resolution.
860///
861/// In either case, the window will only receive information from the monitor on which
862/// it is located.
863///
864/// For example, in the case of condition 2, where the monitor on which the window is
865/// located has actually changed nothing, it will still receive this event.
866fn handle_display_change_msg(handle: HWND, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
867 // NOTE:
868 // Even the `lParam` holds the resolution of the screen, we just ignore it.
869 // Because WM_DPICHANGED, WM_MOVE, WM_SIZE will come first, window reposition and resize
870 // are handled there.
871 // So we only care about if monitor is disconnected.
872 let previous_monitor = state_ptr.state.borrow().display;
873 if WindowsDisplay::is_connected(previous_monitor.handle) {
874 // we are fine, other display changed
875 return None;
876 }
877 // display disconnected
878 // in this case, the OS will move our window to another monitor, and minimize it.
879 // we deminimize the window and query the monitor after moving
880 unsafe {
881 let _ = ShowWindow(handle, SW_SHOWNORMAL);
882 };
883 let new_monitor = unsafe { MonitorFromWindow(handle, MONITOR_DEFAULTTONULL) };
884 // all monitors disconnected
885 if new_monitor.is_invalid() {
886 log::error!("No monitor detected!");
887 return None;
888 }
889 let new_display = WindowsDisplay::new_with_handle(new_monitor);
890 state_ptr.state.borrow_mut().display = new_display;
891 Some(0)
892}
893
894fn handle_hit_test_msg(
895 handle: HWND,
896 msg: u32,
897 wparam: WPARAM,
898 lparam: LPARAM,
899 state_ptr: Rc<WindowsWindowStatePtr>,
900) -> Option<isize> {
901 if !state_ptr.is_movable || state_ptr.state.borrow().is_fullscreen() {
902 return None;
903 }
904
905 let mut lock = state_ptr.state.borrow_mut();
906 if let Some(mut callback) = lock.callbacks.hit_test_window_control.take() {
907 drop(lock);
908 let area = callback();
909 state_ptr
910 .state
911 .borrow_mut()
912 .callbacks
913 .hit_test_window_control = Some(callback);
914 if let Some(area) = area {
915 return match area {
916 WindowControlArea::Drag => Some(HTCAPTION as _),
917 WindowControlArea::Close => Some(HTCLOSE as _),
918 WindowControlArea::Max => Some(HTMAXBUTTON as _),
919 WindowControlArea::Min => Some(HTMINBUTTON as _),
920 };
921 }
922 } else {
923 drop(lock);
924 }
925
926 if !state_ptr.hide_title_bar {
927 // If the OS draws the title bar, we don't need to handle hit test messages.
928 return None;
929 }
930
931 // default handler for resize areas
932 let hit = unsafe { DefWindowProcW(handle, msg, wparam, lparam) };
933 if matches!(
934 hit.0 as u32,
935 HTNOWHERE
936 | HTRIGHT
937 | HTLEFT
938 | HTTOPLEFT
939 | HTTOP
940 | HTTOPRIGHT
941 | HTBOTTOMRIGHT
942 | HTBOTTOM
943 | HTBOTTOMLEFT
944 ) {
945 return Some(hit.0);
946 }
947
948 if state_ptr.state.borrow().is_fullscreen() {
949 return Some(HTCLIENT as _);
950 }
951
952 let dpi = unsafe { GetDpiForWindow(handle) };
953 let frame_y = unsafe { GetSystemMetricsForDpi(SM_CYFRAME, dpi) };
954
955 let mut cursor_point = POINT {
956 x: lparam.signed_loword().into(),
957 y: lparam.signed_hiword().into(),
958 };
959 unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
960 if !state_ptr.state.borrow().is_maximized() && cursor_point.y >= 0 && cursor_point.y <= frame_y
961 {
962 return Some(HTTOP as _);
963 }
964
965 Some(HTCLIENT as _)
966}
967
968fn handle_nc_mouse_move_msg(
969 handle: HWND,
970 lparam: LPARAM,
971 state_ptr: Rc<WindowsWindowStatePtr>,
972) -> Option<isize> {
973 start_tracking_mouse(handle, &state_ptr, TME_LEAVE | TME_NONCLIENT);
974
975 let mut lock = state_ptr.state.borrow_mut();
976 let mut func = lock.callbacks.input.take()?;
977 let scale_factor = lock.scale_factor;
978 drop(lock);
979
980 let mut cursor_point = POINT {
981 x: lparam.signed_loword().into(),
982 y: lparam.signed_hiword().into(),
983 };
984 unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
985 let input = PlatformInput::MouseMove(MouseMoveEvent {
986 position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
987 pressed_button: None,
988 modifiers: current_modifiers(),
989 });
990 let handled = !func(input).propagate;
991 state_ptr.state.borrow_mut().callbacks.input = Some(func);
992
993 if handled { Some(0) } else { None }
994}
995
996fn handle_nc_mouse_down_msg(
997 handle: HWND,
998 button: MouseButton,
999 wparam: WPARAM,
1000 lparam: LPARAM,
1001 state_ptr: Rc<WindowsWindowStatePtr>,
1002) -> Option<isize> {
1003 let mut lock = state_ptr.state.borrow_mut();
1004 if let Some(mut func) = lock.callbacks.input.take() {
1005 let scale_factor = lock.scale_factor;
1006 let mut cursor_point = POINT {
1007 x: lparam.signed_loword().into(),
1008 y: lparam.signed_hiword().into(),
1009 };
1010 unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
1011 let physical_point = point(DevicePixels(cursor_point.x), DevicePixels(cursor_point.y));
1012 let click_count = lock.click_state.update(button, physical_point);
1013 drop(lock);
1014
1015 let input = PlatformInput::MouseDown(MouseDownEvent {
1016 button,
1017 position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
1018 modifiers: current_modifiers(),
1019 click_count,
1020 first_mouse: false,
1021 });
1022 let result = func(input.clone());
1023 let handled = !result.propagate || result.default_prevented;
1024 state_ptr.state.borrow_mut().callbacks.input = Some(func);
1025
1026 if handled {
1027 return Some(0);
1028 }
1029 } else {
1030 drop(lock);
1031 };
1032
1033 // Since these are handled in handle_nc_mouse_up_msg we must prevent the default window proc
1034 if button == MouseButton::Left {
1035 match wparam.0 as u32 {
1036 HTMINBUTTON => state_ptr.state.borrow_mut().nc_button_pressed = Some(HTMINBUTTON),
1037 HTMAXBUTTON => state_ptr.state.borrow_mut().nc_button_pressed = Some(HTMAXBUTTON),
1038 HTCLOSE => state_ptr.state.borrow_mut().nc_button_pressed = Some(HTCLOSE),
1039 _ => return None,
1040 };
1041 Some(0)
1042 } else {
1043 None
1044 }
1045}
1046
1047fn handle_nc_mouse_up_msg(
1048 handle: HWND,
1049 button: MouseButton,
1050 wparam: WPARAM,
1051 lparam: LPARAM,
1052 state_ptr: Rc<WindowsWindowStatePtr>,
1053) -> Option<isize> {
1054 let mut lock = state_ptr.state.borrow_mut();
1055 if let Some(mut func) = lock.callbacks.input.take() {
1056 let scale_factor = lock.scale_factor;
1057 drop(lock);
1058
1059 let mut cursor_point = POINT {
1060 x: lparam.signed_loword().into(),
1061 y: lparam.signed_hiword().into(),
1062 };
1063 unsafe { ScreenToClient(handle, &mut cursor_point).ok().log_err() };
1064 let input = PlatformInput::MouseUp(MouseUpEvent {
1065 button,
1066 position: logical_point(cursor_point.x as f32, cursor_point.y as f32, scale_factor),
1067 modifiers: current_modifiers(),
1068 click_count: 1,
1069 });
1070 let handled = !func(input).propagate;
1071 state_ptr.state.borrow_mut().callbacks.input = Some(func);
1072
1073 if handled {
1074 return Some(0);
1075 }
1076 } else {
1077 drop(lock);
1078 }
1079
1080 let last_pressed = state_ptr.state.borrow_mut().nc_button_pressed.take();
1081 if button == MouseButton::Left
1082 && let Some(last_pressed) = last_pressed
1083 {
1084 let handled = match (wparam.0 as u32, last_pressed) {
1085 (HTMINBUTTON, HTMINBUTTON) => {
1086 unsafe { ShowWindowAsync(handle, SW_MINIMIZE).ok().log_err() };
1087 true
1088 }
1089 (HTMAXBUTTON, HTMAXBUTTON) => {
1090 if state_ptr.state.borrow().is_maximized() {
1091 unsafe { ShowWindowAsync(handle, SW_NORMAL).ok().log_err() };
1092 } else {
1093 unsafe { ShowWindowAsync(handle, SW_MAXIMIZE).ok().log_err() };
1094 }
1095 true
1096 }
1097 (HTCLOSE, HTCLOSE) => {
1098 unsafe {
1099 PostMessageW(Some(handle), WM_CLOSE, WPARAM::default(), LPARAM::default())
1100 .log_err()
1101 };
1102 true
1103 }
1104 _ => false,
1105 };
1106 if handled {
1107 return Some(0);
1108 }
1109 }
1110
1111 None
1112}
1113
1114fn handle_cursor_changed(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
1115 let mut state = state_ptr.state.borrow_mut();
1116 let had_cursor = state.current_cursor.is_some();
1117
1118 state.current_cursor = if lparam.0 == 0 {
1119 None
1120 } else {
1121 Some(HCURSOR(lparam.0 as _))
1122 };
1123
1124 if had_cursor != state.current_cursor.is_some() {
1125 unsafe { SetCursor(state.current_cursor) };
1126 }
1127
1128 Some(0)
1129}
1130
1131fn handle_set_cursor(
1132 handle: HWND,
1133 lparam: LPARAM,
1134 state_ptr: Rc<WindowsWindowStatePtr>,
1135) -> Option<isize> {
1136 if unsafe { !IsWindowEnabled(handle).as_bool() }
1137 || matches!(
1138 lparam.loword() as u32,
1139 HTLEFT
1140 | HTRIGHT
1141 | HTTOP
1142 | HTTOPLEFT
1143 | HTTOPRIGHT
1144 | HTBOTTOM
1145 | HTBOTTOMLEFT
1146 | HTBOTTOMRIGHT
1147 )
1148 {
1149 return None;
1150 }
1151 unsafe {
1152 SetCursor(state_ptr.state.borrow().current_cursor);
1153 };
1154 Some(1)
1155}
1156
1157fn handle_system_settings_changed(
1158 handle: HWND,
1159 wparam: WPARAM,
1160 lparam: LPARAM,
1161 state_ptr: Rc<WindowsWindowStatePtr>,
1162) -> Option<isize> {
1163 if wparam.0 != 0 {
1164 let mut lock = state_ptr.state.borrow_mut();
1165 let display = lock.display;
1166 lock.system_settings.update(display, wparam.0);
1167 lock.click_state.system_update(wparam.0);
1168 lock.border_offset.update(handle).log_err();
1169 } else {
1170 handle_system_theme_changed(handle, lparam, state_ptr)?;
1171 };
1172 // Force to trigger WM_NCCALCSIZE event to ensure that we handle auto hide
1173 // taskbar correctly.
1174 notify_frame_changed(handle);
1175
1176 Some(0)
1177}
1178
1179fn handle_system_command(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
1180 if wparam.0 == SC_KEYMENU as usize {
1181 let mut lock = state_ptr.state.borrow_mut();
1182 if lock.system_key_handled {
1183 lock.system_key_handled = false;
1184 return Some(0);
1185 }
1186 }
1187 None
1188}
1189
1190fn handle_system_theme_changed(
1191 handle: HWND,
1192 lparam: LPARAM,
1193 state_ptr: Rc<WindowsWindowStatePtr>,
1194) -> Option<isize> {
1195 // lParam is a pointer to a string that indicates the area containing the system parameter
1196 // that was changed.
1197 let parameter = PCWSTR::from_raw(lparam.0 as _);
1198 if unsafe { !parameter.is_null() && !parameter.is_empty() } {
1199 if let Some(parameter_string) = unsafe { parameter.to_string() }.log_err() {
1200 log::info!("System settings changed: {}", parameter_string);
1201 match parameter_string.as_str() {
1202 "ImmersiveColorSet" => {
1203 let new_appearance = system_appearance()
1204 .context("unable to get system appearance when handling ImmersiveColorSet")
1205 .log_err()?;
1206 let mut lock = state_ptr.state.borrow_mut();
1207 if new_appearance != lock.appearance {
1208 lock.appearance = new_appearance;
1209 let mut callback = lock.callbacks.appearance_changed.take()?;
1210 drop(lock);
1211 callback();
1212 state_ptr.state.borrow_mut().callbacks.appearance_changed = Some(callback);
1213 configure_dwm_dark_mode(handle, new_appearance);
1214 }
1215 }
1216 _ => {}
1217 }
1218 }
1219 }
1220 Some(0)
1221}
1222
1223fn handle_input_language_changed(
1224 lparam: LPARAM,
1225 state_ptr: Rc<WindowsWindowStatePtr>,
1226) -> Option<isize> {
1227 let thread = state_ptr.main_thread_id_win32;
1228 let validation = state_ptr.validation_number;
1229 unsafe {
1230 PostThreadMessageW(thread, WM_INPUTLANGCHANGE, WPARAM(validation), lparam).log_err();
1231 }
1232 Some(0)
1233}
1234
1235#[inline]
1236fn parse_char_message(wparam: WPARAM, state_ptr: &Rc<WindowsWindowStatePtr>) -> Option<String> {
1237 let code_point = wparam.loword();
1238 let mut lock = state_ptr.state.borrow_mut();
1239 // https://www.unicode.org/versions/Unicode16.0.0/core-spec/chapter-3/#G2630
1240 match code_point {
1241 0xD800..=0xDBFF => {
1242 // High surrogate, wait for low surrogate
1243 lock.pending_surrogate = Some(code_point);
1244 None
1245 }
1246 0xDC00..=0xDFFF => {
1247 if let Some(high_surrogate) = lock.pending_surrogate.take() {
1248 // Low surrogate, combine with pending high surrogate
1249 String::from_utf16(&[high_surrogate, code_point]).ok()
1250 } else {
1251 // Invalid low surrogate without a preceding high surrogate
1252 log::warn!(
1253 "Received low surrogate without a preceding high surrogate: {code_point:x}"
1254 );
1255 None
1256 }
1257 }
1258 _ => {
1259 lock.pending_surrogate = None;
1260 char::from_u32(code_point as u32)
1261 .filter(|c| !c.is_control())
1262 .map(|c| c.to_string())
1263 }
1264 }
1265}
1266
1267#[inline]
1268fn translate_message(handle: HWND, wparam: WPARAM, lparam: LPARAM) {
1269 let msg = MSG {
1270 hwnd: handle,
1271 message: WM_KEYDOWN,
1272 wParam: wparam,
1273 lParam: lparam,
1274 // It seems like leaving the following two parameters empty doesn't break key events, they still work as expected.
1275 // But if any bugs pop up after this PR, this is probably the place to look first.
1276 time: 0,
1277 pt: POINT::default(),
1278 };
1279 unsafe { TranslateMessage(&msg).ok().log_err() };
1280}
1281
1282fn handle_key_event<F>(
1283 handle: HWND,
1284 wparam: WPARAM,
1285 lparam: LPARAM,
1286 state: &mut WindowsWindowState,
1287 f: F,
1288) -> Option<PlatformInput>
1289where
1290 F: FnOnce(Keystroke) -> PlatformInput,
1291{
1292 let virtual_key = VIRTUAL_KEY(wparam.loword());
1293 let mut modifiers = current_modifiers();
1294
1295 match virtual_key {
1296 VK_SHIFT | VK_CONTROL | VK_MENU | VK_LWIN | VK_RWIN => {
1297 if state
1298 .last_reported_modifiers
1299 .is_some_and(|prev_modifiers| prev_modifiers == modifiers)
1300 {
1301 return None;
1302 }
1303 state.last_reported_modifiers = Some(modifiers);
1304 Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
1305 modifiers,
1306 capslock: current_capslock(),
1307 }))
1308 }
1309 VK_PACKET => {
1310 translate_message(handle, wparam, lparam);
1311 None
1312 }
1313 VK_CAPITAL => {
1314 let capslock = current_capslock();
1315 if state
1316 .last_reported_capslock
1317 .is_some_and(|prev_capslock| prev_capslock == capslock)
1318 {
1319 return None;
1320 }
1321 state.last_reported_capslock = Some(capslock);
1322 Some(PlatformInput::ModifiersChanged(ModifiersChangedEvent {
1323 modifiers,
1324 capslock,
1325 }))
1326 }
1327 vkey => {
1328 let vkey = if vkey == VK_PROCESSKEY {
1329 VIRTUAL_KEY(unsafe { ImmGetVirtualKey(handle) } as u16)
1330 } else {
1331 vkey
1332 };
1333 let keystroke = parse_normal_key(vkey, lparam, modifiers)?;
1334 Some(f(keystroke))
1335 }
1336 }
1337}
1338
1339fn parse_immutable(vkey: VIRTUAL_KEY) -> Option<String> {
1340 Some(
1341 match vkey {
1342 VK_SPACE => "space",
1343 VK_BACK => "backspace",
1344 VK_RETURN => "enter",
1345 VK_TAB => "tab",
1346 VK_UP => "up",
1347 VK_DOWN => "down",
1348 VK_RIGHT => "right",
1349 VK_LEFT => "left",
1350 VK_HOME => "home",
1351 VK_END => "end",
1352 VK_PRIOR => "pageup",
1353 VK_NEXT => "pagedown",
1354 VK_BROWSER_BACK => "back",
1355 VK_BROWSER_FORWARD => "forward",
1356 VK_ESCAPE => "escape",
1357 VK_INSERT => "insert",
1358 VK_DELETE => "delete",
1359 VK_APPS => "menu",
1360 VK_F1 => "f1",
1361 VK_F2 => "f2",
1362 VK_F3 => "f3",
1363 VK_F4 => "f4",
1364 VK_F5 => "f5",
1365 VK_F6 => "f6",
1366 VK_F7 => "f7",
1367 VK_F8 => "f8",
1368 VK_F9 => "f9",
1369 VK_F10 => "f10",
1370 VK_F11 => "f11",
1371 VK_F12 => "f12",
1372 VK_F13 => "f13",
1373 VK_F14 => "f14",
1374 VK_F15 => "f15",
1375 VK_F16 => "f16",
1376 VK_F17 => "f17",
1377 VK_F18 => "f18",
1378 VK_F19 => "f19",
1379 VK_F20 => "f20",
1380 VK_F21 => "f21",
1381 VK_F22 => "f22",
1382 VK_F23 => "f23",
1383 VK_F24 => "f24",
1384 _ => return None,
1385 }
1386 .to_string(),
1387 )
1388}
1389
1390fn parse_normal_key(
1391 vkey: VIRTUAL_KEY,
1392 lparam: LPARAM,
1393 mut modifiers: Modifiers,
1394) -> Option<Keystroke> {
1395 let mut key_char = None;
1396 let key = parse_immutable(vkey).or_else(|| {
1397 let scan_code = lparam.hiword() & 0xFF;
1398 key_char = generate_key_char(
1399 vkey,
1400 scan_code as u32,
1401 modifiers.control,
1402 modifiers.shift,
1403 modifiers.alt,
1404 );
1405 get_keystroke_key(vkey, scan_code as u32, &mut modifiers)
1406 })?;
1407 Some(Keystroke {
1408 modifiers,
1409 key,
1410 key_char,
1411 })
1412}
1413
1414fn parse_ime_composition_string(ctx: HIMC, comp_type: IME_COMPOSITION_STRING) -> Option<String> {
1415 unsafe {
1416 let string_len = ImmGetCompositionStringW(ctx, comp_type, None, 0);
1417 if string_len >= 0 {
1418 let mut buffer = vec![0u8; string_len as usize + 2];
1419 ImmGetCompositionStringW(
1420 ctx,
1421 comp_type,
1422 Some(buffer.as_mut_ptr() as _),
1423 string_len as _,
1424 );
1425 let wstring = std::slice::from_raw_parts::<u16>(
1426 buffer.as_mut_ptr().cast::<u16>(),
1427 string_len as usize / 2,
1428 );
1429 Some(String::from_utf16_lossy(wstring))
1430 } else {
1431 None
1432 }
1433 }
1434}
1435
1436#[inline]
1437fn retrieve_composition_cursor_position(ctx: HIMC) -> usize {
1438 unsafe { ImmGetCompositionStringW(ctx, GCS_CURSORPOS, None, 0) as usize }
1439}
1440
1441#[inline]
1442fn is_virtual_key_pressed(vkey: VIRTUAL_KEY) -> bool {
1443 unsafe { GetKeyState(vkey.0 as i32) < 0 }
1444}
1445
1446#[inline]
1447pub(crate) fn current_modifiers() -> Modifiers {
1448 Modifiers {
1449 control: is_virtual_key_pressed(VK_CONTROL),
1450 alt: is_virtual_key_pressed(VK_MENU),
1451 shift: is_virtual_key_pressed(VK_SHIFT),
1452 platform: is_virtual_key_pressed(VK_LWIN) || is_virtual_key_pressed(VK_RWIN),
1453 function: false,
1454 }
1455}
1456
1457#[inline]
1458pub(crate) fn current_capslock() -> Capslock {
1459 let on = unsafe { GetKeyState(VK_CAPITAL.0 as i32) & 1 } > 0;
1460 Capslock { on: on }
1461}
1462
1463fn get_client_area_insets(
1464 handle: HWND,
1465 is_maximized: bool,
1466 windows_version: WindowsVersion,
1467) -> RECT {
1468 // For maximized windows, Windows outdents the window rect from the screen's client rect
1469 // by `frame_thickness` on each edge, meaning `insets` must contain `frame_thickness`
1470 // on all sides (including the top) to avoid the client area extending onto adjacent
1471 // monitors.
1472 //
1473 // For non-maximized windows, things become complicated:
1474 //
1475 // - On Windows 10
1476 // The top inset must be zero, since if there is any nonclient area, Windows will draw
1477 // a full native titlebar outside the client area. (This doesn't occur in the maximized
1478 // case.)
1479 //
1480 // - On Windows 11
1481 // The top inset is calculated using an empirical formula that I derived through various
1482 // tests. Without this, the top 1-2 rows of pixels in our window would be obscured.
1483 let dpi = unsafe { GetDpiForWindow(handle) };
1484 let frame_thickness = get_frame_thickness(dpi);
1485 let top_insets = if is_maximized {
1486 frame_thickness
1487 } else {
1488 match windows_version {
1489 WindowsVersion::Win10 => 0,
1490 WindowsVersion::Win11 => (dpi as f32 / USER_DEFAULT_SCREEN_DPI as f32).round() as i32,
1491 }
1492 };
1493 RECT {
1494 left: frame_thickness,
1495 top: top_insets,
1496 right: frame_thickness,
1497 bottom: frame_thickness,
1498 }
1499}
1500
1501// there is some additional non-visible space when talking about window
1502// borders on Windows:
1503// - SM_CXSIZEFRAME: The resize handle.
1504// - SM_CXPADDEDBORDER: Additional border space that isn't part of the resize handle.
1505fn get_frame_thickness(dpi: u32) -> i32 {
1506 let resize_frame_thickness = unsafe { GetSystemMetricsForDpi(SM_CXSIZEFRAME, dpi) };
1507 let padding_thickness = unsafe { GetSystemMetricsForDpi(SM_CXPADDEDBORDER, dpi) };
1508 resize_frame_thickness + padding_thickness
1509}
1510
1511fn notify_frame_changed(handle: HWND) {
1512 unsafe {
1513 SetWindowPos(
1514 handle,
1515 None,
1516 0,
1517 0,
1518 0,
1519 0,
1520 SWP_FRAMECHANGED
1521 | SWP_NOACTIVATE
1522 | SWP_NOCOPYBITS
1523 | SWP_NOMOVE
1524 | SWP_NOOWNERZORDER
1525 | SWP_NOREPOSITION
1526 | SWP_NOSENDCHANGING
1527 | SWP_NOSIZE
1528 | SWP_NOZORDER,
1529 )
1530 .log_err();
1531 }
1532}
1533
1534fn start_tracking_mouse(
1535 handle: HWND,
1536 state_ptr: &Rc<WindowsWindowStatePtr>,
1537 flags: TRACKMOUSEEVENT_FLAGS,
1538) {
1539 let mut lock = state_ptr.state.borrow_mut();
1540 if !lock.hovered {
1541 lock.hovered = true;
1542 unsafe {
1543 TrackMouseEvent(&mut TRACKMOUSEEVENT {
1544 cbSize: std::mem::size_of::<TRACKMOUSEEVENT>() as u32,
1545 dwFlags: flags,
1546 hwndTrack: handle,
1547 dwHoverTime: HOVER_DEFAULT,
1548 })
1549 .log_err()
1550 };
1551 if let Some(mut callback) = lock.callbacks.hovered_status_change.take() {
1552 drop(lock);
1553 callback(true);
1554 state_ptr.state.borrow_mut().callbacks.hovered_status_change = Some(callback);
1555 }
1556 }
1557}
1558
1559fn with_input_handler<F, R>(state_ptr: &Rc<WindowsWindowStatePtr>, f: F) -> Option<R>
1560where
1561 F: FnOnce(&mut PlatformInputHandler) -> R,
1562{
1563 let mut input_handler = state_ptr.state.borrow_mut().input_handler.take()?;
1564 let result = f(&mut input_handler);
1565 state_ptr.state.borrow_mut().input_handler = Some(input_handler);
1566 Some(result)
1567}
1568
1569fn with_input_handler_and_scale_factor<F, R>(
1570 state_ptr: &Rc<WindowsWindowStatePtr>,
1571 f: F,
1572) -> Option<R>
1573where
1574 F: FnOnce(&mut PlatformInputHandler, f32) -> Option<R>,
1575{
1576 let mut lock = state_ptr.state.borrow_mut();
1577 let mut input_handler = lock.input_handler.take()?;
1578 let scale_factor = lock.scale_factor;
1579 drop(lock);
1580 let result = f(&mut input_handler, scale_factor);
1581 state_ptr.state.borrow_mut().input_handler = Some(input_handler);
1582 result
1583}