Evan's Blog
EvanZhu0721 / 公开仓库

SBMS

镜像已同步

SBMS complete the calculation rules for the default logical desktop topology relationship and the physical dimensions of the displays in the Windows multi-screen collaboration system.

上游 GitHub
READ-ONLY MIRROR

README、源码与 Release 已保存到本站;源码视图固定到 commit a37d6c4b。

main/ root / src
a37d6c4b· 固定源码快照
src/input.rs79.7 KB
1use std::cell::RefCell;
2use std::mem::{MaybeUninit, size_of};
3use std::sync::Mutex;
4use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
5use std::time::Instant;
6
7use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, POINT, RECT, WPARAM};
8use windows::Win32::Security::Cryptography::{BCRYPT_USE_SYSTEM_PREFERRED_RNG, BCryptGenRandom};
9use windows::Win32::System::SystemInformation::GetTickCount64;
10use windows::Win32::UI::Input::KeyboardAndMouse::{
11 GetAsyncKeyState, INPUT, INPUT_0, INPUT_MOUSE, MOUSE_EVENT_FLAGS, MOUSEEVENTF_ABSOLUTE,
12 MOUSEEVENTF_HWHEEL, MOUSEEVENTF_LEFTDOWN, MOUSEEVENTF_LEFTUP, MOUSEEVENTF_MIDDLEDOWN,
13 MOUSEEVENTF_MIDDLEUP, MOUSEEVENTF_MOVE, MOUSEEVENTF_RIGHTDOWN, MOUSEEVENTF_RIGHTUP,
14 MOUSEEVENTF_VIRTUALDESK, MOUSEEVENTF_WHEEL, MOUSEEVENTF_XDOWN, MOUSEEVENTF_XUP, MOUSEINPUT,
15 SendInput, VK_LBUTTON, VK_LSHIFT, VK_LWIN, VK_MBUTTON, VK_RBUTTON, VK_RSHIFT, VK_RWIN,
16 VK_SNAPSHOT, VK_XBUTTON1, VK_XBUTTON2,
17};
18use windows::Win32::UI::Input::{
19 GetRawInputData, HRAWINPUT, MOUSE_MOVE_ABSOLUTE, RAWINPUT, RAWINPUTDEVICE, RID_INPUT,
20 RIDEV_INPUTSINK, RIDEV_REMOVE, RIM_TYPEMOUSE, RegisterRawInputDevices,
21};
22use windows::Win32::UI::WindowsAndMessaging::{
23 CallNextHookEx, ClipCursor, GA_ROOT, GetAncestor, GetClipCursor, GetCursorPos, GetMessageTime,
24 GetSystemMetrics, HC_ACTION, HHOOK, KBDLLHOOKSTRUCT, KillTimer, LLMHF_INJECTED, MSLLHOOKSTRUCT,
25 PostMessageW, SM_CXVIRTUALSCREEN, SM_CYVIRTUALSCREEN, SM_XVIRTUALSCREEN, SM_YVIRTUALSCREEN,
26 SetCursorPos, SetTimer, SetWindowsHookExW, UnhookWindowsHookEx, WH_KEYBOARD_LL, WH_MOUSE_LL,
27 WM_APP, WM_DISPLAYCHANGE, WM_INPUT, WM_KEYDOWN, WM_LBUTTONDOWN, WM_LBUTTONUP, WM_MBUTTONDOWN,
28 WM_MBUTTONUP, WM_MOUSEHWHEEL, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONDOWN, WM_RBUTTONUP,
29 WM_SYSKEYDOWN, WM_TIMER, WM_XBUTTONDOWN, WM_XBUTTONUP, WindowFromPoint,
30};
31
32use crate::display::active_display_topology;
33use crate::geometry::{CoordinateTransform, PixelPoint, PixelRect, Rotation};
34
35mod boundary;
36mod navigation;
37mod screenshot;
38
39const SCREENSHOT_TIMER_ID: usize = 0x5342_4d53;
40const SCREENSHOT_POLL_MS: u32 = 50;
41
42const WM_RELEASE_CAPTURE: u32 = WM_APP + 1;
43const RELEASE_NORMAL: isize = 0;
44const RELEASE_INJECTION_FAILURE: isize = 1;
45const RELEASE_EXTERNAL_INJECTION: isize = 2;
46const RELEASE_ABSOLUTE_INPUT: isize = 3;
47
48const BUTTON_LEFT: u8 = 1 << 0;
49const BUTTON_RIGHT: u8 = 1 << 1;
50const BUTTON_MIDDLE: u8 = 1 << 2;
51const BUTTON_X1: u8 = 1 << 3;
52const BUTTON_X2: u8 = 1 << 4;
53
54const NO_INPUT_ENDPOINT: usize = 0;
55const INPUT_TAG_MASK: usize = if usize::BITS >= 64 {
56 0xffff_ffff_0000_0000_u64 as usize
57} else {
58 0xffff_ffff_u64 as usize
59};
60const INPUT_TAG_SIGNATURE: usize = if usize::BITS >= 64 {
61 0x5342_4d53_0000_0000_u64 as usize
62} else {
63 0x5342_4d53_u64 as usize
64};
65
66static ACTIVE_INPUT_ENDPOINT: ActiveEndpoint = ActiveEndpoint::new();
67static INPUT_COORDINATION: Mutex<InputCoordination> = Mutex::new(InputCoordination::new());
68static INPUT_CAPTURE_GENERATION: AtomicUsize = AtomicUsize::new(0);
69static BOUNDARY_WARP_DEPTH: AtomicUsize = AtomicUsize::new(0);
70// Encoded as tick + 1 so a transfer at system tick zero is representable.
71static SCREENSHOT_RETURN_TICK: AtomicU64 = AtomicU64::new(0);
72
73thread_local! {
74 static INPUT_STATE: RefCell<Option<InputMapper>> = const { RefCell::new(None) };
75}
76
77struct ActiveEndpoint {
78 window: AtomicUsize,
79}
80
81impl ActiveEndpoint {
82 const fn new() -> Self {
83 Self {
84 window: AtomicUsize::new(NO_INPUT_ENDPOINT),
85 }
86 }
87
88 fn replace(&self, window: usize) -> usize {
89 self.window.swap(window, Ordering::AcqRel)
90 }
91
92 fn restore(&self, expected: usize, previous: usize) -> bool {
93 self.window
94 .compare_exchange(expected, previous, Ordering::AcqRel, Ordering::Acquire)
95 .is_ok()
96 }
97
98 fn release(&self, window: usize) -> bool {
99 self.restore(window, NO_INPUT_ENDPOINT)
100 }
101
102 fn owns(&self, window: usize) -> bool {
103 window != NO_INPUT_ENDPOINT && self.window.load(Ordering::Acquire) == window
104 }
105}
106
107struct InputCoordination {
108 clip_baseline: Option<ClipBaseline>,
109 applied_clip: Option<RECT>,
110 restoration_owner: Option<usize>,
111 capture_source: Option<RECT>,
112 boundary: boundary::BoundaryRegistry,
113 screenshot_navigation: Option<navigation::Navigation>,
114 screenshot_raw_owner: Option<usize>,
115}
116
117impl InputCoordination {
118 const fn new() -> Self {
119 Self {
120 clip_baseline: None,
121 applied_clip: None,
122 restoration_owner: None,
123 capture_source: None,
124 boundary: boundary::BoundaryRegistry::new(),
125 screenshot_navigation: None,
126 screenshot_raw_owner: None,
127 }
128 }
129
130 fn activation_allowed(&self) -> bool {
131 self.restoration_owner.is_none()
132 }
133
134 fn observe_clip(&mut self, current: ClipBaseline) {
135 if self
136 .applied_clip
137 .is_some_and(|ours| !rect_eq(ours, current.rect))
138 {
139 // Another application replaced our clip. Its current setting is
140 // now authoritative; the old baseline no longer belongs to us.
141 self.applied_clip = None;
142 self.clip_baseline = None;
143 }
144 }
145
146 fn native_clip_allowed(&self, current: ClipBaseline) -> bool {
147 self.clip_baseline.unwrap_or(current).was_full_desktop
148 }
149
150 fn reserve_restoration(
151 &mut self,
152 window: usize,
153 endpoint: usize,
154 current_generation: usize,
155 expected_generation: usize,
156 ) -> bool {
157 if !self.activation_allowed()
158 || !screenshot_endpoint_available(endpoint, current_generation, expected_generation)
159 {
160 return false;
161 }
162 self.restoration_owner = Some(window);
163 true
164 }
165
166 fn release_restoration(&mut self, window: usize) {
167 if self.restoration_owner == Some(window) {
168 self.restoration_owner = None;
169 }
170 }
171}
172
173struct ScreenshotRestorationLease {
174 window: usize,
175}
176
177impl ScreenshotRestorationLease {
178 fn acquire(window: HWND, capture_generation: usize) -> Option<Self> {
179 let window = window_key(window);
180 let mut coordination = lock_coordination();
181 if coordination.reserve_restoration(
182 window,
183 ACTIVE_INPUT_ENDPOINT.window.load(Ordering::Acquire),
184 INPUT_CAPTURE_GENERATION.load(Ordering::Acquire),
185 capture_generation,
186 ) {
187 Some(Self { window })
188 } else {
189 None
190 }
191 }
192
193 fn activate(&self, source: RECT) -> Result<(), String> {
194 register_raw_mouse(window_from_key(self.window), RIDEV_INPUTSINK)?;
195 let clip_result = {
196 let mut coordination = lock_coordination();
197 debug_assert_eq!(coordination.restoration_owner, Some(self.window));
198 debug_assert_eq!(
199 ACTIVE_INPUT_ENDPOINT.window.load(Ordering::Acquire),
200 NO_INPUT_ENDPOINT
201 );
202 coordination.capture_source = Some(source);
203 ACTIVE_INPUT_ENDPOINT.replace(self.window);
204 INPUT_CAPTURE_GENERATION.fetch_add(1, Ordering::AcqRel);
205 apply_clip_state_locked(&mut coordination)
206 };
207 if let Err(error) = clip_result {
208 self.rollback_activation();
209 return Err(error);
210 }
211 Ok(())
212 }
213
214 fn rollback_activation(&self) {
215 let clip_result = {
216 let mut coordination = lock_coordination();
217 if !ACTIVE_INPUT_ENDPOINT.release(self.window) {
218 return;
219 }
220 coordination.capture_source = None;
221 apply_clip_state_locked(&mut coordination)
222 };
223 if let Err(error) = unregister_raw_mouse() {
224 eprintln!("warning: screenshot raw input cleanup failed: {error}");
225 }
226 if let Err(error) = clip_result {
227 eprintln!("warning: screenshot clip rollback failed: {error}");
228 }
229 }
230}
231
232impl Drop for ScreenshotRestorationLease {
233 fn drop(&mut self) {
234 lock_coordination().release_restoration(self.window);
235 }
236}
237
238#[derive(Clone, Copy)]
239struct ClipBaseline {
240 rect: RECT,
241 was_full_desktop: bool,
242}
243
244struct InputMapper {
245 target: RECT,
246 source: RECT,
247 transform: CoordinateTransform,
248 source_to_target: CoordinateTransform,
249 screenshot_restore: Option<ScreenshotRestore>,
250 cursor: POINT,
251 move_pending: bool,
252 captured: bool,
253 pressed: u8,
254 tag: usize,
255 window: HWND,
256 mouse_hook: HHOOK,
257 keyboard_hook: HHOOK,
258}
259
260pub struct InputGuard;
261
262impl InputGuard {
263 pub fn start(
264 window: HWND,
265 target: RECT,
266 source: RECT,
267 instance: HINSTANCE,
268 ) -> Result<Self, String> {
269 let transform = CoordinateTransform::stretch(
270 PixelRect {
271 left: 0,
272 top: 0,
273 width: rect_extent(target.left, target.right)?,
274 height: rect_extent(target.top, target.bottom)?,
275 },
276 PixelRect {
277 left: source.left,
278 top: source.top,
279 width: rect_extent(source.left, source.right)?,
280 height: rect_extent(source.top, source.bottom)?,
281 },
282 Rotation::Deg0,
283 )
284 .map_err(|error| format!("input geometry is invalid: {error}"))?;
285 let source_to_target = CoordinateTransform::stretch(
286 transform.source,
287 PixelRect {
288 left: target.left,
289 top: target.top,
290 ..transform.target
291 },
292 Rotation::Deg0,
293 )
294 .map_err(|error| format!("input geometry is invalid: {error}"))?;
295 let mut nonce = [0u8; size_of::<usize>()];
296 let status = unsafe { BCryptGenRandom(None, &mut nonce, BCRYPT_USE_SYSTEM_PREFERRED_RNG) };
297 if status.0 < 0 {
298 return Err(format!("BCryptGenRandom failed: 0x{:08x}", status.0 as u32));
299 }
300 let tag = input_tag(usize::from_le_bytes(nonce));
301
302 let mouse_hook =
303 unsafe { SetWindowsHookExW(WH_MOUSE_LL, Some(mouse_hook), Some(instance), 0) }
304 .map_err(|error| format!("SetWindowsHookExW(mouse) failed: {error}"))?;
305 let keyboard_hook = match unsafe {
306 SetWindowsHookExW(WH_KEYBOARD_LL, Some(keyboard_hook), Some(instance), 0)
307 } {
308 Ok(hook) => hook,
309 Err(error) => {
310 unsafe {
311 let _ = UnhookWindowsHookEx(mouse_hook);
312 }
313 return Err(format!("SetWindowsHookExW(keyboard) failed: {error}"));
314 }
315 };
316
317 INPUT_STATE.with(|cell| {
318 *cell.borrow_mut() = Some(InputMapper {
319 target,
320 source,
321 transform,
322 source_to_target,
323 screenshot_restore: None,
324 cursor: POINT {
325 x: (target.right - target.left).max(1) / 2,
326 y: (target.bottom - target.top).max(1) / 2,
327 },
328 move_pending: false,
329 captured: false,
330 pressed: 0,
331 tag,
332 window,
333 mouse_hook,
334 keyboard_hook,
335 });
336 });
337 register_input_guard(window, target, source);
338 Ok(Self)
339 }
340}
341
342impl Drop for InputGuard {
343 fn drop(&mut self) {
344 cleanup();
345 }
346}
347
348pub fn handle_message(message: u32, wparam: WPARAM, lparam: LPARAM) -> Option<LRESULT> {
349 match message {
350 WM_LBUTTONDOWN => {
351 // The overlay may not yet cover the mirror during its launch.
352 // Do not reacquire ordinary mapping capture in that interval.
353 if lock_coordination().boundary.has_pause() {
354 return Some(LRESULT(0));
355 }
356 let x = low_word_signed(lparam.0);
357 let y = high_word_signed(lparam.0);
358 if let Err(error) = capture_at(x, y) {
359 eprintln!("warning: input capture failed: {error}");
360 }
361 Some(LRESULT(0))
362 }
363 WM_INPUT => {
364 if let Err(error) = handle_raw_input(HRAWINPUT(lparam.0 as *mut _)) {
365 eprintln!("warning: raw mouse input failed: {error}");
366 release_capture();
367 }
368 Some(LRESULT(0))
369 }
370 WM_RELEASE_CAPTURE => {
371 cancel_screenshot_restore();
372 release_capture();
373 match lparam.0 {
374 RELEASE_INJECTION_FAILURE => {
375 eprintln!(
376 "warning: input capture released because SendInput was rejected (possibly UIPI)"
377 )
378 }
379 RELEASE_EXTERNAL_INJECTION => {
380 eprintln!("warning: input capture released after foreign injected mouse input")
381 }
382 RELEASE_ABSOLUTE_INPUT => {
383 eprintln!(
384 "warning: absolute mouse/touch/pen input is not supported during capture"
385 )
386 }
387 _ => {}
388 }
389 Some(LRESULT(0))
390 }
391 WM_TIMER if wparam.0 == SCREENSHOT_TIMER_ID => {
392 poll_screenshot_restore();
393 Some(LRESULT(0))
394 }
395 WM_DISPLAYCHANGE => {
396 handle_display_change();
397 cancel_screenshot_restore();
398 Some(LRESULT(0))
399 }
400 _ => None,
401 }
402}
403
404fn capture_at(x: i32, y: i32) -> Result<(), String> {
405 cancel_screenshot_restore();
406 let (source, point, tag, window) = INPUT_STATE.with(|cell| {
407 let mut state = cell.borrow_mut();
408 let state = state
409 .as_mut()
410 .ok_or_else(|| "input mapper is not initialized".to_string())?;
411 let width = (state.target.right - state.target.left).max(1);
412 let height = (state.target.bottom - state.target.top).max(1);
413 state.cursor.x = x.clamp(0, width - 1);
414 state.cursor.y = y.clamp(0, height - 1);
415 Ok::<_, String>((state.source, source_point(state), state.tag, state.window))
416 })?;
417
418 let previous = activate_endpoint(window, source)?;
419 if previous != NO_INPUT_ENDPOINT && previous != window_key(window) {
420 post_release(window_from_key(previous), RELEASE_NORMAL);
421 }
422 if !send_mouse(point, MOUSEEVENTF_MOVE, 0, tag)
423 || !send_mouse(point, MOUSEEVENTF_LEFTDOWN, 0, tag)
424 {
425 deactivate_endpoint(window);
426 return Err(format!(
427 "SendInput failed: {} (the source may run at a higher integrity level)",
428 windows::core::Error::from_thread()
429 ));
430 }
431 INPUT_STATE.with(|cell| {
432 if let Some(state) = cell.borrow_mut().as_mut() {
433 state.captured = true;
434 state.pressed |= BUTTON_LEFT;
435 }
436 });
437 Ok(())
438}
439
440fn handle_raw_input(handle: HRAWINPUT) -> Result<(), String> {
441 let mut raw = MaybeUninit::<RAWINPUT>::zeroed();
442 let mut bytes = size_of::<RAWINPUT>() as u32;
443 let read = unsafe {
444 GetRawInputData(
445 handle,
446 RID_INPUT,
447 Some(raw.as_mut_ptr().cast()),
448 &mut bytes,
449 size_of::<windows::Win32::UI::Input::RAWINPUTHEADER>() as u32,
450 )
451 };
452 if read == u32::MAX || read < size_of::<RAWINPUT>() as u32 {
453 return Err(format!("GetRawInputData returned {read} bytes"));
454 }
455 let raw = unsafe { raw.assume_init() };
456 if raw.header.dwType != RIM_TYPEMOUSE.0 {
457 return Ok(());
458 }
459 let mouse = unsafe { raw.data.mouse };
460 // Registration changes do not remove WM_INPUT already in this window's
461 // queue. Those deltas are already represented by the return-position sample.
462 if stale_screenshot_motion(unsafe { GetMessageTime() } as u32) {
463 return Ok(());
464 }
465 if mouse.usFlags.0 & MOUSE_MOVE_ABSOLUTE.0 == 0
466 && unsafe { mouse.Anonymous.Anonymous.usButtonFlags } == 0
467 {
468 handle_screenshot_raw_motion(
469 POINT {
470 x: mouse.lLastX,
471 y: mouse.lLastY,
472 },
473 unsafe { GetMessageTime() } as u32,
474 );
475 }
476 INPUT_STATE.with(|cell| {
477 let mut state = cell.borrow_mut();
478 let Some(state) = state.as_mut() else {
479 return;
480 };
481 if !state.captured || !ACTIVE_INPUT_ENDPOINT.owns(window_key(state.window)) {
482 return;
483 }
484 if mouse.usFlags.0 & MOUSE_MOVE_ABSOLUTE.0 != 0 {
485 post_release(state.window, RELEASE_ABSOLUTE_INPUT);
486 return;
487 }
488 let width = (state.target.right - state.target.left).max(1);
489 let height = (state.target.bottom - state.target.top).max(1);
490 state.cursor.x = state
491 .cursor
492 .x
493 .saturating_add(mouse.lLastX)
494 .clamp(0, width - 1);
495 state.cursor.y = state
496 .cursor
497 .y
498 .saturating_add(mouse.lLastY)
499 .clamp(0, height - 1);
500 state.move_pending = true;
501 });
502 Ok(())
503}
504
505unsafe extern "system" fn mouse_hook(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
506 if code < HC_ACTION as i32 {
507 return unsafe { CallNextHookEx(None, code, wparam, lparam) };
508 }
509 let event = unsafe { &*(lparam.0 as *const MSLLHOOKSTRUCT) };
510 let stale_return_move = wparam.0 as u32 == WM_MOUSEMOVE
511 && BOUNDARY_WARP_DEPTH.load(Ordering::Acquire) == 0
512 && stale_screenshot_motion(event.time);
513 if event.flags & LLMHF_INJECTED != 0 && is_managed_input_tag(event.dwExtraInfo) {
514 // SendInput inserts events into the input stream; a queued move may
515 // reach this hook after a screenshot has released capture and moved
516 // the cursor to the physical display. Do not let that old move undo
517 // the handoff. Button-up events must still clear any held buttons.
518 let stale = INPUT_STATE.with(|cell| {
519 cell.borrow().as_ref().is_some_and(|state| {
520 (event.dwExtraInfo == state.tag && stale_return_move)
521 || stale_managed_move(
522 wparam.0 as u32,
523 event.dwExtraInfo,
524 state.tag,
525 ACTIVE_INPUT_ENDPOINT.window.load(Ordering::Acquire),
526 )
527 })
528 });
529 if stale {
530 return LRESULT(1);
531 }
532 // Our own injected events must not flush another mapped move: doing
533 // so adds work (and potentially another injection) to the hook chain.
534 return unsafe { CallNextHookEx(None, code, wparam, lparam) };
535 }
536 // Preserve buttons and foreign injected input. Only old movement from our
537 // physical workspace is invalid after returning to logical coordinates.
538 if stale_return_move && event.flags & LLMHF_INJECTED == 0 {
539 return LRESULT(1);
540 }
541 flush_movement();
542 let snapshot = INPUT_STATE.with(|cell| {
543 cell.borrow().as_ref().map(|state| {
544 (
545 state.captured && ACTIVE_INPUT_ENDPOINT.owns(window_key(state.window)),
546 state.tag,
547 state.window,
548 source_point(state),
549 )
550 })
551 });
552 let Some((captured, tag, window, point)) = snapshot else {
553 return unsafe { CallNextHookEx(None, code, wparam, lparam) };
554 };
555 if event.flags & LLMHF_INJECTED != 0 {
556 if captured {
557 post_release(window, RELEASE_EXTERNAL_INJECTION);
558 }
559 return unsafe { CallNextHookEx(None, code, wparam, lparam) };
560 }
561 if !captured {
562 return native_boundary_hook(code, wparam, lparam, event);
563 }
564
565 let (flags, data, pressed_bit, is_down) = match wparam.0 as u32 {
566 WM_MOUSEMOVE => return LRESULT(1),
567 WM_LBUTTONDOWN => (MOUSEEVENTF_LEFTDOWN, 0, BUTTON_LEFT, true),
568 WM_LBUTTONUP => (MOUSEEVENTF_LEFTUP, 0, BUTTON_LEFT, false),
569 WM_RBUTTONDOWN => (MOUSEEVENTF_RIGHTDOWN, 0, BUTTON_RIGHT, true),
570 WM_RBUTTONUP => (MOUSEEVENTF_RIGHTUP, 0, BUTTON_RIGHT, false),
571 WM_MBUTTONDOWN => (MOUSEEVENTF_MIDDLEDOWN, 0, BUTTON_MIDDLE, true),
572 WM_MBUTTONUP => (MOUSEEVENTF_MIDDLEUP, 0, BUTTON_MIDDLE, false),
573 WM_MOUSEWHEEL => (
574 MOUSEEVENTF_WHEEL,
575 ((event.mouseData >> 16) as u16 as i16 as i32) as u32,
576 0,
577 false,
578 ),
579 WM_MOUSEHWHEEL => (
580 MOUSEEVENTF_HWHEEL,
581 ((event.mouseData >> 16) as u16 as i16 as i32) as u32,
582 0,
583 false,
584 ),
585 WM_XBUTTONDOWN => {
586 let data = (event.mouseData >> 16) & 0xffff;
587 (MOUSEEVENTF_XDOWN, data, x_button_bit(data), true)
588 }
589 WM_XBUTTONUP => {
590 let data = (event.mouseData >> 16) & 0xffff;
591 (MOUSEEVENTF_XUP, data, x_button_bit(data), false)
592 }
593 _ => return unsafe { CallNextHookEx(None, code, wparam, lparam) },
594 };
595 if send_mouse(point, flags, data, tag) {
596 if pressed_bit != 0 {
597 INPUT_STATE.with(|cell| {
598 if let Some(state) = cell.borrow_mut().as_mut() {
599 if is_down {
600 state.pressed |= pressed_bit;
601 } else {
602 state.pressed &= !pressed_bit;
603 }
604 }
605 });
606 }
607 } else {
608 post_release(window, RELEASE_INJECTION_FAILURE);
609 }
610 LRESULT(1)
611}
612
613unsafe extern "system" fn keyboard_hook(code: i32, wparam: WPARAM, lparam: LPARAM) -> LRESULT {
614 if code < HC_ACTION as i32 {
615 return unsafe { CallNextHookEx(None, code, wparam, lparam) };
616 }
617 let message = wparam.0 as u32;
618 let event = unsafe { &*(lparam.0 as *const KBDLLHOOKSTRUCT) };
619 if matches!(message, WM_KEYDOWN | WM_SYSKEYDOWN)
620 && (event.vkCode == VK_SNAPSHOT.0 as u32 || is_snipping_shortcut(event.vkCode))
621 {
622 // Complete the handoff before the shortcut can open the capture overlay.
623 handoff_screenshot_cursor(event.time);
624 }
625 unsafe { CallNextHookEx(None, code, wparam, lparam) }
626}
627
628#[derive(Clone, Copy, Debug, Eq, PartialEq)]
629enum ScreenshotHandoff {
630 ReleaseCapture,
631 MoveCursor(PixelPoint),
632 Ignore,
633}
634
635#[derive(Clone, Copy, Debug, Eq, PartialEq)]
636enum ScreenshotMode {
637 Native,
638 Captured,
639 /// Boundary-only session: the overlay may drag across mapped targets while
640 /// the cursor is not on a source; no cursor handoff and no cursor restore.
641 PauseOnly,
642}
643
644impl ScreenshotMode {
645 fn with_handoff(self, incoming: Self) -> Self {
646 match (self, incoming) {
647 (Self::Captured, _) | (_, Self::Captured) => Self::Captured,
648 (Self::Native, _) | (_, Self::Native) => Self::Native,
649 _ => Self::PauseOnly,
650 }
651 }
652}
653
654struct ScreenshotRestore {
655 session: screenshot::Session,
656 mode: ScreenshotMode,
657 capture_generation: usize,
658}
659
660#[derive(Clone, Copy, Debug, Eq, PartialEq)]
661enum ScreenshotRestoreDecision {
662 Wait,
663 Restore,
664 Cancel,
665}
666
667fn screenshot_handoff(
668 captured: bool,
669 owns_endpoint: bool,
670 source_to_target: CoordinateTransform,
671 cursor: Option<PixelPoint>,
672) -> ScreenshotHandoff {
673 if captured {
674 return if owns_endpoint {
675 ScreenshotHandoff::ReleaseCapture
676 } else {
677 ScreenshotHandoff::Ignore
678 };
679 }
680 cursor
681 .and_then(|point| source_to_target.map_target_point(point))
682 .map(ScreenshotHandoff::MoveCursor)
683 .unwrap_or(ScreenshotHandoff::Ignore)
684}
685
686fn handoff_screenshot_cursor(time: u32) {
687 let snapshot = INPUT_STATE.with(|cell| {
688 cell.borrow().as_ref().map(|state| {
689 (
690 state.captured,
691 ACTIVE_INPUT_ENDPOINT.owns(window_key(state.window)),
692 state.source_to_target,
693 )
694 })
695 });
696 let Some((captured, owns_endpoint, source_to_target)) = snapshot else {
697 return;
698 };
699 let cursor = if captured {
700 None
701 } else {
702 let mut point = POINT::default();
703 if let Err(error) = unsafe { GetCursorPos(&mut point) } {
704 eprintln!("warning: screenshot cursor position failed: {error}");
705 return;
706 }
707 Some(PixelPoint {
708 x: point.x,
709 y: point.y,
710 })
711 };
712 let handoff = screenshot_handoff(captured, owns_endpoint, source_to_target, cursor);
713 if handoff == ScreenshotHandoff::Ignore {
714 // All guards share one navigation session, including screenshots
715 // started on an ordinary display and later moved onto a mapped output.
716 if arm_screenshot_restore(ScreenshotMode::PauseOnly) {
717 start_screenshot_navigation(time);
718 }
719 return;
720 }
721 let mode = if captured {
722 ScreenshotMode::Captured
723 } else {
724 ScreenshotMode::Native
725 };
726 if !arm_screenshot_restore(mode) {
727 return;
728 }
729 // Win32 input calls can re-enter our hooks; keep the RefCell unborrowed.
730 let moved = match handoff {
731 ScreenshotHandoff::ReleaseCapture => release_capture(),
732 ScreenshotHandoff::MoveCursor(point) => set_cursor_position(POINT {
733 x: point.x,
734 y: point.y,
735 }),
736 ScreenshotHandoff::Ignore => false,
737 };
738 if !moved {
739 cancel_screenshot_restore();
740 } else {
741 start_screenshot_navigation(time);
742 }
743}
744
745fn arm_screenshot_restore(mode: ScreenshotMode) -> bool {
746 let snapshot = INPUT_STATE.with(|cell| {
747 let mut state = cell.borrow_mut();
748 let state = state.as_mut()?;
749 if let Some(restore) = state.screenshot_restore.as_mut() {
750 // A repeated shortcut belongs to the same temporary handoff.
751 restore.session = screenshot::Session::new(Instant::now());
752 restore.mode = restore.mode.with_handoff(mode);
753 return Some((state.window, false));
754 }
755 Some((state.window, true))
756 });
757 let Some((window, needs_timer)) = snapshot else {
758 return false;
759 };
760 if !needs_timer {
761 return true;
762 }
763 let capture_generation = INPUT_CAPTURE_GENERATION.load(Ordering::Acquire);
764 if unsafe { SetTimer(Some(window), SCREENSHOT_TIMER_ID, SCREENSHOT_POLL_MS, None) } == 0 {
765 eprintln!(
766 "warning: screenshot recovery timer failed: {}",
767 windows::core::Error::from_thread()
768 );
769 return false;
770 }
771 // Pause the normal source-space boundary before handoff. Screenshot
772 // navigation installs a physical-screen fence as soon as capture releases.
773 // A live ScreenshotRestore owns exactly one pause.
774 acquire_screenshot_pause();
775 INPUT_STATE.with(|cell| {
776 if let Some(state) = cell.borrow_mut().as_mut() {
777 state.screenshot_restore = Some(ScreenshotRestore {
778 session: screenshot::Session::new(Instant::now()),
779 mode,
780 capture_generation,
781 });
782 }
783 });
784 true
785}
786
787/// Take the running screenshot session (killing its timer) without ending the
788/// global pause; callers must pair this with `end_screenshot_pause` after their
789/// cursor work so the boundary resumes only once the cursor is back in place.
790fn take_screenshot_restore() -> Option<ScreenshotRestore> {
791 let taken = INPUT_STATE.with(|cell| {
792 let mut state = cell.borrow_mut();
793 let state = state.as_mut()?;
794 let window = state.window;
795 let restore = state.screenshot_restore.take();
796 restore.map(|restore| (window, restore))
797 });
798 let (window, restore) = taken?;
799 let _ = unsafe { KillTimer(Some(window), SCREENSHOT_TIMER_ID) };
800 Some(restore)
801}
802
803fn cancel_screenshot_restore() {
804 let restore = take_screenshot_restore();
805 end_screenshot_pause(restore.is_some());
806}
807
808fn screenshot_restore_decision(
809 progress: screenshot::Progress,
810 overlay_visible: Option<bool>,
811 endpoint_available: bool,
812 cursor_in_target: bool,
813 buttons_down: bool,
814 mirror_receives_input: bool,
815) -> ScreenshotRestoreDecision {
816 if !endpoint_available {
817 return ScreenshotRestoreDecision::Cancel;
818 }
819 if progress == screenshot::Progress::Waiting {
820 return ScreenshotRestoreDecision::Wait;
821 }
822 if overlay_visible != Some(false) {
823 return if progress == screenshot::Progress::TimedOut {
824 ScreenshotRestoreDecision::Cancel
825 } else {
826 ScreenshotRestoreDecision::Wait
827 };
828 }
829 if !cursor_in_target {
830 return ScreenshotRestoreDecision::Cancel;
831 }
832 if buttons_down {
833 return ScreenshotRestoreDecision::Wait;
834 }
835 if progress == screenshot::Progress::TimedOut && !mirror_receives_input {
836 return ScreenshotRestoreDecision::Cancel;
837 }
838 ScreenshotRestoreDecision::Restore
839}
840
841fn screenshot_return_point(
842 target: RECT,
843 transform: CoordinateTransform,
844 cursor: POINT,
845) -> Option<(PixelPoint, PixelPoint)> {
846 let local = PixelPoint {
847 x: cursor.x.checked_sub(target.left)?,
848 y: cursor.y.checked_sub(target.top)?,
849 };
850 transform
851 .map_target_point(local)
852 .map(|source| (local, source))
853}
854
855fn screenshot_endpoint_available(
856 endpoint: usize,
857 current_generation: usize,
858 expected_generation: usize,
859) -> bool {
860 endpoint == NO_INPUT_ENDPOINT && current_generation == expected_generation
861}
862
863fn poll_screenshot_restore() {
864 let snapshot = INPUT_STATE.with(|cell| {
865 let state = cell.borrow();
866 let state = state.as_ref()?;
867 let restore = state.screenshot_restore.as_ref()?;
868 Some((
869 state.window,
870 state.target,
871 state.transform,
872 restore.capture_generation,
873 ))
874 });
875 let Some((window, target, transform, capture_generation)) = snapshot else {
876 return;
877 };
878 let endpoint_available = || {
879 screenshot_endpoint_available(
880 ACTIVE_INPUT_ENDPOINT.window.load(Ordering::Acquire),
881 INPUT_CAPTURE_GENERATION.load(Ordering::Acquire),
882 capture_generation,
883 )
884 };
885 if !endpoint_available() {
886 cancel_screenshot_restore();
887 return;
888 }
889 let overlay_visible = screenshot::overlay_visible();
890 if overlay_visible.is_some() {
891 // Reconcile missed/synthetic button releases off the hook path. Never
892 // treat an inaccessible input desktop as an all-buttons-up signal.
893 let held = physical_buttons_down();
894 if let Some(navigation) = lock_coordination().screenshot_navigation.as_mut() {
895 navigation.sync_buttons(held);
896 }
897 }
898 let progress = INPUT_STATE.with(|cell| {
899 cell.borrow_mut()
900 .as_mut()?
901 .screenshot_restore
902 .as_mut()
903 .map(|restore| restore.session.poll(Instant::now(), overlay_visible))
904 });
905 let Some(progress) = progress else {
906 return;
907 };
908 if progress == screenshot::Progress::Waiting {
909 return;
910 }
911 // A desktop switch can also make GetCursorPos temporarily unavailable.
912 // Wait for a confirmed end before touching the input desktop's cursor.
913 if overlay_visible != Some(false) {
914 if progress == screenshot::Progress::TimedOut {
915 cancel_screenshot_restore();
916 }
917 return;
918 }
919 let mut cursor = POINT::default();
920 if let Err(error) = unsafe { GetCursorPos(&mut cursor) } {
921 eprintln!("warning: screenshot return position failed: {error}");
922 cancel_screenshot_restore();
923 return;
924 }
925 let return_point = screenshot_return_point(target, transform, cursor);
926 let buttons_down = [VK_LBUTTON, VK_RBUTTON, VK_MBUTTON, VK_XBUTTON1, VK_XBUTTON2]
927 .iter()
928 .any(|key| key_down(key.0 as i32) < 0);
929 let mirror_receives_input = progress == screenshot::Progress::TimedOut
930 && return_point.is_some()
931 && unsafe { GetAncestor(WindowFromPoint(cursor), GA_ROOT) } == window;
932 match screenshot_restore_decision(
933 progress,
934 overlay_visible,
935 endpoint_available(),
936 return_point.is_some(),
937 buttons_down,
938 mirror_receives_input,
939 ) {
940 ScreenshotRestoreDecision::Wait => {}
941 ScreenshotRestoreDecision::Cancel => cancel_screenshot_restore(),
942 ScreenshotRestoreDecision::Restore => {
943 if let Some((local, source)) = return_point {
944 restore_screenshot_cursor(local, source, cursor);
945 }
946 }
947 }
948}
949
950fn restore_screenshot_cursor(local: PixelPoint, source: PixelPoint, physical: POINT) {
951 let snapshot: Option<(HWND, RECT, ScreenshotMode, usize)> = INPUT_STATE.with(|cell| {
952 let state = cell.borrow();
953 let state = state.as_ref()?;
954 let restore = state.screenshot_restore.as_ref()?;
955 Some((
956 state.window,
957 state.source,
958 restore.mode,
959 restore.capture_generation,
960 ))
961 });
962 let Some(snapshot) = snapshot else {
963 return;
964 };
965 let restore = take_screenshot_restore();
966 // A pause-only session never owns cursor placement: other guards must not
967 // push the screenshot cursor anywhere.
968 if snapshot.2 != ScreenshotMode::PauseOnly {
969 run_screenshot_cursor_restore(local, source, physical, snapshot);
970 }
971 // Restore the source cursor first; only then does the boundary resume.
972 end_screenshot_pause(restore.is_some());
973}
974
975fn run_screenshot_cursor_restore(
976 local: PixelPoint,
977 source: PixelPoint,
978 physical: POINT,
979 snapshot: (HWND, RECT, ScreenshotMode, usize),
980) {
981 let (window, source_rect, mode, capture_generation) = snapshot;
982 let point = POINT {
983 x: source.x,
984 y: source.y,
985 };
986 let Some(lease) = ScreenshotRestorationLease::acquire(window, capture_generation) else {
987 return;
988 };
989 let _warp = CursorWarpGuard::new();
990 if mode == ScreenshotMode::Native {
991 // The shared navigator restores the current logical destination after
992 // the final guard finishes. Its physical clip is still active here.
993 if lock_coordination().screenshot_navigation.is_none() {
994 let prepared = prepare_screenshot_return(&mut lock_coordination(), point);
995 if prepared.is_ok() {
996 place_screenshot_return(point);
997 }
998 let _ = apply_clip_state_locked(&mut lock_coordination());
999 }
1000 return;
1001 }
1002 // Keep the old position legal until the single return warp. Activating
1003 // capture first would constrain the physical cursor to a source edge.
1004 if let Err(error) = prepare_screenshot_return(&mut lock_coordination(), point) {
1005 eprintln!("warning: screenshot cursor recovery preparation failed: {error}");
1006 return;
1007 }
1008 if !place_screenshot_return(point) {
1009 let _ = apply_clip_state_locked(&mut lock_coordination());
1010 return;
1011 }
1012 if let Err(error) = lease.activate(source_rect) {
1013 eprintln!("warning: screenshot input recovery failed: {error}");
1014 let _ = apply_clip_state_locked(&mut lock_coordination());
1015 set_cursor_position(physical);
1016 return;
1017 }
1018 INPUT_STATE.with(|cell| {
1019 if let Some(state) = cell.borrow_mut().as_mut() {
1020 state.cursor = POINT {
1021 x: local.x,
1022 y: local.y,
1023 };
1024 state.move_pending = false;
1025 state.pressed = 0;
1026 }
1027 });
1028 // Placement is synchronous above. Do not enqueue a SendInput return move
1029 // that could overtake raw input received after capture has resumed.
1030 INPUT_STATE.with(|cell| {
1031 if let Some(state) = cell.borrow_mut().as_mut()
1032 && ACTIVE_INPUT_ENDPOINT.owns(window_key(window))
1033 {
1034 state.captured = true;
1035 }
1036 });
1037 mark_screenshot_return();
1038}
1039
1040fn is_snipping_shortcut(vk_code: u32) -> bool {
1041 const VK_S: u32 = b'S' as u32;
1042 vk_code == VK_S
1043 && (key_down(VK_LWIN.0 as i32) < 0 || key_down(VK_RWIN.0 as i32) < 0)
1044 && (key_down(VK_LSHIFT.0 as i32) < 0 || key_down(VK_RSHIFT.0 as i32) < 0)
1045}
1046
1047fn key_down(key: i32) -> i16 {
1048 unsafe { GetAsyncKeyState(key) }
1049}
1050
1051fn send_mouse(point: POINT, flags: MOUSE_EVENT_FLAGS, data: u32, tag: usize) -> bool {
1052 let desktop = virtual_desktop_rect();
1053 let dx = normalize(point.x - desktop.left, desktop.right - desktop.left);
1054 let dy = normalize(point.y - desktop.top, desktop.bottom - desktop.top);
1055 let input = INPUT {
1056 r#type: INPUT_MOUSE,
1057 Anonymous: INPUT_0 {
1058 mi: MOUSEINPUT {
1059 dx,
1060 dy,
1061 mouseData: data,
1062 dwFlags: MOUSEEVENTF_ABSOLUTE | MOUSEEVENTF_VIRTUALDESK | flags,
1063 time: 0,
1064 dwExtraInfo: tag,
1065 },
1066 },
1067 };
1068 (unsafe { SendInput(&[input], size_of::<INPUT>() as i32) }) == 1
1069}
1070
1071pub(crate) fn flush_movement() {
1072 let pending = INPUT_STATE.with(|cell| {
1073 let mut state = cell.borrow_mut();
1074 let state = state.as_mut()?;
1075 if !state.captured
1076 || !state.move_pending
1077 || !ACTIVE_INPUT_ENDPOINT.owns(window_key(state.window))
1078 {
1079 return None;
1080 }
1081 state.move_pending = false;
1082 Some((source_point(state), state.tag, state.window))
1083 });
1084 if let Some((point, tag, window)) = pending
1085 && !send_mouse(point, MOUSEEVENTF_MOVE, 0, tag)
1086 {
1087 post_release(window, RELEASE_INJECTION_FAILURE);
1088 }
1089}
1090
1091fn release_capture() -> bool {
1092 let release = INPUT_STATE.with(|cell| {
1093 let mut state = cell.borrow_mut();
1094 let state = state.as_mut()?;
1095 if !state.captured {
1096 return None;
1097 }
1098 state.captured = false;
1099 state.move_pending = false;
1100 let pressed = state.pressed;
1101 state.pressed = 0;
1102 Some((
1103 source_point(state),
1104 POINT {
1105 x: state.target.left + state.cursor.x,
1106 y: state.target.top + state.cursor.y,
1107 },
1108 state.tag,
1109 pressed,
1110 state.window,
1111 ))
1112 });
1113 if let Some((source, target, tag, pressed, window)) = release {
1114 let owned_endpoint = deactivate_endpoint(window);
1115 release_pressed_buttons(source, tag, pressed);
1116 if owned_endpoint {
1117 // Never land the released cursor inside a mapped target display;
1118 // with the boundary paused (screenshot handoff) the raw mirror
1119 // position is kept.
1120 return set_cursor_position(boundary_adjusted_warp(target));
1121 }
1122 }
1123 false
1124}
1125
1126fn set_cursor_position(point: POINT) -> bool {
1127 if let Err(error) = unsafe { SetCursorPos(point.x, point.y) } {
1128 eprintln!("warning: cursor handoff failed: {error}");
1129 return false;
1130 }
1131 true
1132}
1133
1134fn release_pressed_buttons(point: POINT, tag: usize, pressed: u8) {
1135 for (bit, flags, data) in [
1136 (BUTTON_LEFT, MOUSEEVENTF_LEFTUP, 0),
1137 (BUTTON_RIGHT, MOUSEEVENTF_RIGHTUP, 0),
1138 (BUTTON_MIDDLE, MOUSEEVENTF_MIDDLEUP, 0),
1139 (BUTTON_X1, MOUSEEVENTF_XUP, 1),
1140 (BUTTON_X2, MOUSEEVENTF_XUP, 2),
1141 ] {
1142 if pressed & bit != 0 {
1143 let _ = send_mouse(point, flags, data, tag);
1144 }
1145 }
1146}
1147
1148fn cleanup() {
1149 cancel_screenshot_restore();
1150 release_capture();
1151 let hooks = INPUT_STATE.with(|cell| {
1152 cell.borrow_mut()
1153 .take()
1154 .map(|state| (state.window, state.mouse_hook, state.keyboard_hook))
1155 });
1156 if let Some((window, mouse, keyboard)) = hooks {
1157 unregister_input_guard(window);
1158 unsafe {
1159 let _ = UnhookWindowsHookEx(mouse);
1160 let _ = UnhookWindowsHookEx(keyboard);
1161 }
1162 }
1163}
1164
1165fn activate_endpoint(window: HWND, source: RECT) -> Result<usize, String> {
1166 let mut coordination = lock_coordination();
1167 if !coordination.activation_allowed() {
1168 return Err("mouse capture is busy restoring the screenshot cursor".into());
1169 }
1170 activate_endpoint_locked(&mut coordination, window_key(window), source)
1171}
1172
1173fn activate_endpoint_locked(
1174 coordination: &mut InputCoordination,
1175 window: usize,
1176 source: RECT,
1177) -> Result<usize, String> {
1178 let current = ACTIVE_INPUT_ENDPOINT.window.load(Ordering::Acquire);
1179 if current == window {
1180 coordination.capture_source = Some(source);
1181 apply_clip_state_locked(coordination)?;
1182 INPUT_CAPTURE_GENERATION.fetch_add(1, Ordering::AcqRel);
1183 return Ok(current);
1184 }
1185
1186 let previous_source = coordination.capture_source;
1187 let previous = ACTIVE_INPUT_ENDPOINT.replace(window);
1188 debug_assert_eq!(previous, current);
1189 coordination.capture_source = Some(source);
1190 if let Err(error) = register_raw_mouse(window_from_key(window), RIDEV_INPUTSINK) {
1191 let _ = ACTIVE_INPUT_ENDPOINT.restore(window, previous);
1192 coordination.capture_source = previous_source;
1193 return Err(error);
1194 }
1195 if let Err(error) = apply_clip_state_locked(coordination) {
1196 let registration = if previous == NO_INPUT_ENDPOINT {
1197 unregister_raw_mouse()
1198 } else {
1199 register_raw_mouse(window_from_key(previous), RIDEV_INPUTSINK)
1200 };
1201 let _ = ACTIVE_INPUT_ENDPOINT.restore(window, previous);
1202 coordination.capture_source = previous_source;
1203 let rollback = registration
1204 .err()
1205 .map(|rollback| format!("raw input endpoint rollback failed: {rollback}"))
1206 .or_else(|| apply_clip_state_locked(coordination).err());
1207 return Err(match rollback {
1208 Some(rollback) => format!("{error}; {rollback}"),
1209 None => error,
1210 });
1211 }
1212 INPUT_CAPTURE_GENERATION.fetch_add(1, Ordering::AcqRel);
1213 Ok(previous)
1214}
1215
1216fn deactivate_endpoint(window: HWND) -> bool {
1217 let window = window_key(window);
1218 let mut coordination = lock_coordination();
1219 if !ACTIVE_INPUT_ENDPOINT.release(window) {
1220 return false;
1221 }
1222 if let Err(error) = unregister_raw_mouse() {
1223 eprintln!("warning: raw mouse cleanup failed: {error}");
1224 }
1225 // Release only the capture clip: an active native boundary (or the external
1226 // baseline when none is active) must survive capture teardown.
1227 coordination.capture_source = None;
1228 if let Err(error) = apply_clip_state_locked(&mut coordination) {
1229 eprintln!("warning: cursor clip restore failed: {error}");
1230 }
1231 true
1232}
1233
1234fn read_clip_baseline() -> Result<ClipBaseline, String> {
1235 let mut rect = RECT::default();
1236 unsafe { GetClipCursor(&mut rect) }
1237 .map_err(|error| format!("GetClipCursor failed: {error}"))?;
1238 Ok(ClipBaseline {
1239 rect,
1240 was_full_desktop: rect_eq(rect, virtual_desktop_rect()),
1241 })
1242}
1243
1244fn restore_clip_baseline(baseline: ClipBaseline) -> Result<(), String> {
1245 let result = unsafe {
1246 if baseline.was_full_desktop {
1247 ClipCursor(None)
1248 } else {
1249 ClipCursor(Some(&baseline.rect))
1250 }
1251 };
1252 result.map_err(|error| format!("ClipCursor restore failed: {error}"))
1253}
1254
1255/// Temporarily widen only a clip we still own. Narrowing to the destination
1256/// workspace happens after placement, so Windows never clamps to an unrelated
1257/// edge in the middle of the return. An external clip remains authoritative.
1258fn prepare_screenshot_return(
1259 coordination: &mut InputCoordination,
1260 destination: POINT,
1261) -> Result<(), String> {
1262 let current = read_clip_baseline()?;
1263 coordination.observe_clip(current);
1264 if coordination.applied_clip.is_none() {
1265 return if current.was_full_desktop {
1266 Ok(())
1267 } else {
1268 Err("cursor clip is controlled by another application".into())
1269 };
1270 }
1271 let rect = screenshot::return_clip(current.rect, destination)
1272 .ok_or_else(|| "screenshot return point is out of range".to_string())?;
1273 if !rect_eq(current.rect, rect) {
1274 unsafe { ClipCursor(Some(&rect)) }
1275 .map_err(|error| format!("ClipCursor return preparation failed: {error}"))?;
1276 coordination.applied_clip = Some(rect);
1277 }
1278 Ok(())
1279}
1280
1281fn mark_screenshot_return() {
1282 SCREENSHOT_RETURN_TICK.fetch_max(unsafe { GetTickCount64() } + 1, Ordering::AcqRel);
1283}
1284
1285fn place_screenshot_return(point: POINT) -> bool {
1286 mark_screenshot_return();
1287 let placed = set_cursor_position(point);
1288 mark_screenshot_return();
1289 placed
1290}
1291
1292fn stale_screenshot_motion(time: u32) -> bool {
1293 let tick = SCREENSHOT_RETURN_TICK.load(Ordering::Acquire);
1294 tick != 0 && screenshot::stale_return_motion(time, tick, unsafe { GetTickCount64() })
1295}
1296
1297/// The single `ClipCursor` authority. Priority: the captured source clip, then
1298/// the native boundary while no screenshot pause is active. The external
1299/// baseline is preserved exactly once across capture/boundary swaps and is
1300/// restored only while the current clip still matches our last write. Native
1301/// mapping yields to another application's clip; explicit capture may claim
1302/// it temporarily. Hooks use try_lock and explicit cursor warps stay outside
1303/// the coordination lock.
1304fn apply_clip_state_locked(coordination: &mut InputCoordination) -> Result<(), String> {
1305 let current = read_clip_baseline()?;
1306 coordination.observe_clip(current);
1307 let native_allowed = coordination.native_clip_allowed(current);
1308 let screenshot_rect = native_allowed
1309 .then(|| {
1310 coordination
1311 .screenshot_navigation
1312 .as_ref()
1313 .map(navigation::Navigation::clip)
1314 })
1315 .flatten();
1316 let boundary_rect = native_allowed
1317 .then(|| {
1318 coordination
1319 .boundary
1320 .snapshot()
1321 .map(|boundary| boundary.clip_rect())
1322 })
1323 .flatten();
1324 match boundary::clip_action(
1325 coordination.capture_source.or(screenshot_rect),
1326 coordination.boundary.has_pause(),
1327 boundary_rect,
1328 coordination.clip_baseline.is_some(),
1329 ) {
1330 boundary::ClipAction::SaveBaselineThenSet(rect) => {
1331 unsafe { ClipCursor(Some(&rect)) }
1332 .map_err(|error| format!("ClipCursor failed: {error}"))?;
1333 coordination.clip_baseline = Some(current);
1334 coordination.applied_clip = Some(rect);
1335 Ok(())
1336 }
1337 boundary::ClipAction::Set(rect) => {
1338 if !rect_eq(current.rect, rect) {
1339 unsafe { ClipCursor(Some(&rect)) }
1340 .map_err(|error| format!("ClipCursor failed: {error}"))?;
1341 }
1342 coordination.applied_clip = Some(rect);
1343 Ok(())
1344 }
1345 boundary::ClipAction::RestoreBaseline => {
1346 if let Some(baseline) = coordination.clip_baseline {
1347 restore_clip_baseline(baseline)?;
1348 }
1349 // Keep both fields on failure, so a later attempt never mistakes
1350 // our still-active restriction for an external baseline.
1351 coordination.clip_baseline = None;
1352 coordination.applied_clip = None;
1353 Ok(())
1354 }
1355 boundary::ClipAction::None => Ok(()),
1356 }
1357}
1358
1359/// Clamp a discrete warp destination out of every mapped target display while
1360/// the boundary is live; paused or failed-open states keep the raw point.
1361fn boundary_adjusted_warp(point: POINT) -> POINT {
1362 let boundary = {
1363 let coordination = lock_coordination();
1364 if coordination.boundary.has_pause() {
1365 None
1366 } else {
1367 coordination.boundary.snapshot()
1368 }
1369 };
1370 match boundary {
1371 Some(boundary) => boundary.warp_to_allowed(point),
1372 None => point,
1373 }
1374}
1375
1376/// `SetCursorPos` re-enters the low-level hooks; the bypass flag keeps our own
1377/// corrective warp from triggering another correction.
1378struct CursorWarpGuard;
1379
1380impl CursorWarpGuard {
1381 fn new() -> Self {
1382 BOUNDARY_WARP_DEPTH.fetch_add(1, Ordering::AcqRel);
1383 Self
1384 }
1385}
1386
1387impl Drop for CursorWarpGuard {
1388 fn drop(&mut self) {
1389 BOUNDARY_WARP_DEPTH.fetch_sub(1, Ordering::AcqRel);
1390 }
1391}
1392
1393fn warp_cursor_with_bypass(point: POINT) {
1394 let _warp = CursorWarpGuard::new();
1395 set_cursor_position(point);
1396}
1397
1398fn boundary_consumable_message(message: u32) -> bool {
1399 matches!(
1400 message,
1401 WM_MOUSEMOVE
1402 | WM_LBUTTONDOWN
1403 | WM_LBUTTONUP
1404 | WM_RBUTTONDOWN
1405 | WM_RBUTTONUP
1406 | WM_MBUTTONDOWN
1407 | WM_MBUTTONUP
1408 | WM_XBUTTONDOWN
1409 | WM_XBUTTONUP
1410 | WM_MOUSEWHEEL
1411 | WM_MOUSEHWHEEL
1412 )
1413}
1414
1415/// Native-mode boundary fallback: consume movement/button deliveries aimed at
1416/// a mapped target display and correct the cursor back to the previous allowed
1417/// monitor edge immediately. Runs with no coordination lock held while the
1418/// cursor moves (try-lock snapshot only) and issues no topology queries.
1419fn native_boundary_hook(
1420 code: i32,
1421 wparam: WPARAM,
1422 lparam: LPARAM,
1423 event: &MSLLHOOKSTRUCT,
1424) -> LRESULT {
1425 if BOUNDARY_WARP_DEPTH.load(Ordering::Acquire) != 0 {
1426 return unsafe { CallNextHookEx(None, code, wparam, lparam) };
1427 }
1428 if !boundary_consumable_message(wparam.0 as u32) {
1429 return unsafe { CallNextHookEx(None, code, wparam, lparam) };
1430 }
1431 // An active capture endpoint or a global screenshot pause disables
1432 // corrections; only plain native mapping applies the boundary hook.
1433 if ACTIVE_INPUT_ENDPOINT.window.load(Ordering::Acquire) != NO_INPUT_ENDPOINT {
1434 return unsafe { CallNextHookEx(None, code, wparam, lparam) };
1435 }
1436 if screenshot_navigation_hook(wparam.0 as u32, event) {
1437 return LRESULT(1);
1438 }
1439 let boundary = INPUT_COORDINATION.try_lock().ok().and_then(|coordination| {
1440 if coordination.boundary.has_pause() {
1441 None
1442 } else {
1443 coordination.boundary.snapshot()
1444 }
1445 });
1446 let Some(boundary) = boundary else {
1447 return unsafe { CallNextHookEx(None, code, wparam, lparam) };
1448 };
1449 if !boundary.is_forbidden(event.pt) {
1450 return unsafe { CallNextHookEx(None, code, wparam, lparam) };
1451 }
1452 // Forbidden delivery: consume it and correct the position from this
1453 // callback. In a ClipOnly layout this path should normally never run (the
1454 // clip already blocks the move); it is the cheap soft fallback for when
1455 // another application has cleared or replaced the global clip. Win32 gives
1456 // no frame-level guarantee about callback positioning.
1457 let mut previous = POINT::default();
1458 if unsafe { GetCursorPos(&mut previous) }.is_err() {
1459 previous = event.pt;
1460 }
1461 let corrected = boundary.correct_movement(event.pt, Some(previous));
1462 warp_cursor_with_bypass(corrected);
1463 LRESULT(1)
1464}
1465
1466/// Register one active input guard's rectangles globally and refresh the
1467/// logical monitor layout off the hook path.
1468fn register_input_guard(window: HWND, target: RECT, source: RECT) {
1469 refresh_boundary_layout(Some((
1470 window_key(window),
1471 boundary::GuardRects { target, source },
1472 )));
1473}
1474
1475fn unregister_input_guard(window: HWND) {
1476 let mut coordination = lock_coordination();
1477 clear_screenshot_navigation(&mut coordination);
1478 coordination.boundary.unregister(window_key(window));
1479 if let Err(error) = apply_clip_state_locked(&mut coordination) {
1480 eprintln!("warning: cursor boundary release failed: {error}");
1481 }
1482}
1483
1484fn monitor_layout() -> Vec<RECT> {
1485 active_display_topology()
1486 .map(|displays| {
1487 displays
1488 .into_iter()
1489 .map(|display| display.rect)
1490 .collect::<Vec<_>>()
1491 })
1492 .unwrap_or_default()
1493}
1494
1495fn refresh_boundary_layout(register: Option<(usize, boundary::GuardRects)>) {
1496 let layout = monitor_layout();
1497 let mut coordination = lock_coordination();
1498 clear_screenshot_navigation(&mut coordination);
1499 if layout.is_empty() {
1500 coordination.boundary.invalidate();
1501 } else {
1502 coordination.boundary.set_layout(layout);
1503 }
1504 if let Some((window, rects)) = register {
1505 coordination.boundary.register(window, rects);
1506 }
1507 if let Err(error) = apply_clip_state_locked(&mut coordination) {
1508 eprintln!("warning: cursor boundary refresh failed: {error}");
1509 }
1510}
1511
1512/// A display-layout mutation releases the boundary across all guards until a
1513/// fresh layout passes validation again; no stale corrective warp may survive.
1514fn handle_display_change() {
1515 {
1516 let mut coordination = lock_coordination();
1517 clear_screenshot_navigation(&mut coordination);
1518 coordination.boundary.invalidate();
1519 if let Err(error) = apply_clip_state_locked(&mut coordination) {
1520 eprintln!("warning: cursor boundary release failed: {error}");
1521 }
1522 }
1523 refresh_boundary_layout(None);
1524}
1525
1526/// Pause the native boundary for one screenshot session owner (global across
1527/// mappers). Every acquisition is balanced by `end_screenshot_pause`.
1528fn acquire_screenshot_pause() {
1529 let mut coordination = lock_coordination();
1530 coordination.boundary.pause();
1531 if let Err(error) = apply_clip_state_locked(&mut coordination) {
1532 eprintln!("warning: screenshot boundary pause failed: {error}");
1533 }
1534}
1535
1536fn end_screenshot_pause(held: bool) {
1537 if !held {
1538 return;
1539 }
1540 let _warp = CursorWarpGuard::new();
1541 let (boundary, return_point) = {
1542 let mut coordination = lock_coordination();
1543 coordination.boundary.resume();
1544 // Read the final physical position before restoring the normal clip:
1545 // ClipCursor itself can move a cursor outside its new rectangle.
1546 let mut return_point = if !coordination.boundary.has_pause() {
1547 stop_screenshot_raw_input(&mut coordination);
1548 coordination
1549 .screenshot_navigation
1550 .take()
1551 .and_then(|navigation| {
1552 let mut cursor = POINT::default();
1553 (ACTIVE_INPUT_ENDPOINT.window.load(Ordering::Acquire) == NO_INPUT_ENDPOINT
1554 && unsafe { GetCursorPos(&mut cursor) }.is_ok())
1555 .then(|| navigation.return_point(cursor))
1556 })
1557 } else {
1558 None
1559 };
1560 let boundary = if coordination.boundary.has_pause() {
1561 None
1562 } else {
1563 coordination.boundary.snapshot()
1564 };
1565 if let Some(point) = return_point
1566 && let Err(error) = prepare_screenshot_return(&mut coordination, point)
1567 {
1568 eprintln!("warning: screenshot return preparation failed: {error}");
1569 return_point = None;
1570 }
1571 // With a return pending, keep the widened clip until after placement.
1572 if return_point.is_none()
1573 && let Err(error) = apply_clip_state_locked(&mut coordination)
1574 {
1575 eprintln!("warning: screenshot boundary resume failed: {error}");
1576 }
1577 (boundary, return_point)
1578 };
1579 if let Some(point) = return_point {
1580 place_screenshot_return(point);
1581 if let Err(error) = apply_clip_state_locked(&mut lock_coordination()) {
1582 eprintln!("warning: screenshot boundary resume failed: {error}");
1583 }
1584 }
1585 // The boundary resumes after the cursor is back in place; a cursor left on
1586 // a forbidden target (cancelled session) is corrected outside every lock.
1587 if let Some(boundary) = boundary {
1588 let mut point = POINT::default();
1589 if unsafe { GetCursorPos(&mut point) }.is_ok() && boundary.is_forbidden(point) {
1590 warp_cursor_with_bypass(boundary.warp_to_allowed(point));
1591 }
1592 }
1593}
1594
1595fn physical_buttons_down() -> u8 {
1596 [
1597 (VK_LBUTTON, BUTTON_LEFT),
1598 (VK_RBUTTON, BUTTON_RIGHT),
1599 (VK_MBUTTON, BUTTON_MIDDLE),
1600 (VK_XBUTTON1, BUTTON_X1),
1601 (VK_XBUTTON2, BUTTON_X2),
1602 ]
1603 .into_iter()
1604 .fold(0, |bits, (key, bit)| {
1605 bits | if key_down(key.0 as i32) < 0 { bit } else { 0 }
1606 })
1607}
1608
1609fn start_screenshot_navigation(time: u32) {
1610 if ACTIVE_INPUT_ENDPOINT.window.load(Ordering::Acquire) != NO_INPUT_ENDPOINT {
1611 return;
1612 }
1613 let mut cursor = POINT::default();
1614 if unsafe { GetCursorPos(&mut cursor) }.is_err() {
1615 return;
1616 }
1617 let held = physical_buttons_down();
1618 let Some(window) = INPUT_STATE.with(|cell| cell.borrow().as_ref().map(|state| state.window))
1619 else {
1620 return;
1621 };
1622 let _warp = CursorWarpGuard::new();
1623 let destination = {
1624 let mut coordination = lock_coordination();
1625 if !coordination.boundary.has_pause() || coordination.screenshot_navigation.is_some() {
1626 return;
1627 }
1628 let Some(boundary) = coordination.boundary.snapshot() else {
1629 return;
1630 };
1631 let Some(mut navigation) =
1632 navigation::Navigation::new(boundary.screenshot_screens(), cursor, held)
1633 else {
1634 return;
1635 };
1636 let destination = navigation.handoff(cursor);
1637 navigation.warped_at(time);
1638 coordination.screenshot_navigation = Some(navigation);
1639 if let Err(error) = apply_clip_state_locked(&mut coordination) {
1640 eprintln!("warning: screenshot navigation clip failed: {error}");
1641 clear_screenshot_navigation(&mut coordination);
1642 let _ = apply_clip_state_locked(&mut coordination);
1643 return;
1644 }
1645 // Honour a foreign clip instead of fighting it with corrective warps.
1646 if coordination.applied_clip
1647 != coordination
1648 .screenshot_navigation
1649 .as_ref()
1650 .map(navigation::Navigation::clip)
1651 {
1652 clear_screenshot_navigation(&mut coordination);
1653 return;
1654 }
1655 match register_raw_mouse(window, RIDEV_INPUTSINK) {
1656 Ok(()) => coordination.screenshot_raw_owner = Some(window_key(window)),
1657 Err(error) => {
1658 eprintln!("warning: screenshot edge direction input unavailable: {error}")
1659 }
1660 }
1661 destination
1662 };
1663 set_cursor_position(destination);
1664}
1665
1666/// Return true only when a native move has been replaced. Buttons always
1667/// remain native so the snipping tool owns the complete down/up sequence.
1668fn screenshot_navigation_hook(message: u32, event: &MSLLHOOKSTRUCT) -> bool {
1669 let destination = {
1670 let Ok(mut coordination) = INPUT_COORDINATION.try_lock() else {
1671 return false;
1672 };
1673 let applied_clip = coordination.applied_clip;
1674 let Some(navigation) = coordination.screenshot_navigation.as_mut() else {
1675 return false;
1676 };
1677 if applied_clip != Some(navigation.clip()) {
1678 return false;
1679 }
1680 match message {
1681 WM_LBUTTONDOWN => navigation.button(BUTTON_LEFT, true),
1682 WM_LBUTTONUP => navigation.button(BUTTON_LEFT, false),
1683 WM_RBUTTONDOWN => navigation.button(BUTTON_RIGHT, true),
1684 WM_RBUTTONUP => navigation.button(BUTTON_RIGHT, false),
1685 WM_MBUTTONDOWN => navigation.button(BUTTON_MIDDLE, true),
1686 WM_MBUTTONUP => navigation.button(BUTTON_MIDDLE, false),
1687 WM_XBUTTONDOWN | WM_XBUTTONUP => navigation.button(
1688 x_button_bit((event.mouseData >> 16) & 0xffff),
1689 message == WM_XBUTTONDOWN,
1690 ),
1691 WM_MOUSEMOVE => {}
1692 _ => return false,
1693 }
1694 if message != WM_MOUSEMOVE {
1695 return false;
1696 }
1697 if !navigation.accepts_motion(event.time) {
1698 return true;
1699 }
1700 let mut previous = POINT::default();
1701 if unsafe { GetCursorPos(&mut previous) }.is_err() {
1702 return false;
1703 }
1704 let old_clip = navigation.clip();
1705 let destination = navigation.move_to(event.pt, previous);
1706 if destination != event.pt {
1707 navigation.warped_at(event.time);
1708 }
1709 if navigation.clip() != old_clip
1710 && let Err(error) = apply_clip_state_locked(&mut coordination)
1711 {
1712 eprintln!("warning: screenshot screen transition failed: {error}");
1713 clear_screenshot_navigation(&mut coordination);
1714 let _ = apply_clip_state_locked(&mut coordination);
1715 return false;
1716 }
1717 destination
1718 };
1719 if destination == event.pt {
1720 return false;
1721 }
1722 let _warp = CursorWarpGuard::new();
1723 set_cursor_position(destination)
1724}
1725
1726fn stop_screenshot_raw_input(coordination: &mut InputCoordination) {
1727 if coordination.screenshot_raw_owner.take().is_some()
1728 && ACTIVE_INPUT_ENDPOINT.window.load(Ordering::Acquire) == NO_INPUT_ENDPOINT
1729 && let Err(error) = unregister_raw_mouse()
1730 {
1731 eprintln!("warning: screenshot edge input cleanup failed: {error}");
1732 }
1733}
1734
1735fn clear_screenshot_navigation(coordination: &mut InputCoordination) {
1736 coordination.screenshot_navigation = None;
1737 stop_screenshot_raw_input(coordination);
1738}
1739
1740fn handle_screenshot_raw_motion(delta: POINT, time: u32) {
1741 if ACTIVE_INPUT_ENDPOINT.window.load(Ordering::Acquire) != NO_INPUT_ENDPOINT {
1742 return;
1743 }
1744 let owner =
1745 INPUT_STATE.with(|cell| cell.borrow().as_ref().map(|state| window_key(state.window)));
1746 let _warp = CursorWarpGuard::new();
1747 let destination = {
1748 let mut coordination = lock_coordination();
1749 if coordination.screenshot_raw_owner != owner || owner.is_none() {
1750 return;
1751 }
1752 let mut cursor = POINT::default();
1753 if unsafe { GetCursorPos(&mut cursor) }.is_err() {
1754 return;
1755 }
1756 let applied_clip = coordination.applied_clip;
1757 let Some(navigation) = coordination.screenshot_navigation.as_mut() else {
1758 return;
1759 };
1760 if applied_clip != Some(navigation.clip()) || !navigation.accepts_motion(time) {
1761 return;
1762 }
1763 let destination = navigation.raw_edge_move(delta, cursor);
1764 if destination == cursor {
1765 return;
1766 }
1767 navigation.warped_at(time);
1768 if let Err(error) = apply_clip_state_locked(&mut coordination) {
1769 eprintln!("warning: screenshot edge transition failed: {error}");
1770 clear_screenshot_navigation(&mut coordination);
1771 let _ = apply_clip_state_locked(&mut coordination);
1772 return;
1773 }
1774 destination
1775 };
1776 set_cursor_position(destination);
1777}
1778
1779fn lock_coordination() -> std::sync::MutexGuard<'static, InputCoordination> {
1780 INPUT_COORDINATION
1781 .lock()
1782 .unwrap_or_else(|poisoned| poisoned.into_inner())
1783}
1784
1785fn register_raw_mouse(
1786 window: HWND,
1787 flags: windows::Win32::UI::Input::RAWINPUTDEVICE_FLAGS,
1788) -> Result<(), String> {
1789 let mouse = RAWINPUTDEVICE {
1790 usUsagePage: 0x01,
1791 usUsage: 0x02,
1792 dwFlags: flags,
1793 hwndTarget: window,
1794 };
1795 unsafe { RegisterRawInputDevices(&[mouse], size_of::<RAWINPUTDEVICE>() as u32) }
1796 .map_err(|error| format!("RegisterRawInputDevices(mouse) failed: {error}"))
1797}
1798
1799fn unregister_raw_mouse() -> Result<(), String> {
1800 register_raw_mouse(HWND::default(), RIDEV_REMOVE)
1801}
1802
1803fn post_release(window: HWND, reason: isize) {
1804 unsafe {
1805 let _ = PostMessageW(Some(window), WM_RELEASE_CAPTURE, WPARAM(0), LPARAM(reason));
1806 }
1807}
1808
1809fn source_point(state: &InputMapper) -> POINT {
1810 let point = state
1811 .transform
1812 .map_target_point(PixelPoint {
1813 x: state.cursor.x,
1814 y: state.cursor.y,
1815 })
1816 .expect("captured cursor is clamped inside the target transform");
1817 POINT {
1818 x: point.x,
1819 y: point.y,
1820 }
1821}
1822
1823fn input_tag(nonce: usize) -> usize {
1824 (nonce & !INPUT_TAG_MASK) | INPUT_TAG_SIGNATURE
1825}
1826
1827fn is_managed_input_tag(tag: usize) -> bool {
1828 tag & INPUT_TAG_MASK == INPUT_TAG_SIGNATURE
1829}
1830
1831fn stale_managed_move(message: u32, tag: usize, owner_tag: usize, endpoint: usize) -> bool {
1832 // Only the injecting guard rejects its move. Other SBMS instances have
1833 // independent endpoint state and their tagged input must remain untouched.
1834 message == WM_MOUSEMOVE
1835 && tag == owner_tag
1836 && is_managed_input_tag(tag)
1837 && endpoint == NO_INPUT_ENDPOINT
1838}
1839
1840fn window_key(window: HWND) -> usize {
1841 window.0 as usize
1842}
1843
1844fn window_from_key(window: usize) -> HWND {
1845 HWND(window as *mut _)
1846}
1847
1848fn virtual_desktop_rect() -> RECT {
1849 let left = unsafe { GetSystemMetrics(SM_XVIRTUALSCREEN) };
1850 let top = unsafe { GetSystemMetrics(SM_YVIRTUALSCREEN) };
1851 RECT {
1852 left,
1853 top,
1854 right: left + unsafe { GetSystemMetrics(SM_CXVIRTUALSCREEN) }.max(1),
1855 bottom: top + unsafe { GetSystemMetrics(SM_CYVIRTUALSCREEN) }.max(1),
1856 }
1857}
1858
1859fn normalize(value: i32, extent: i32) -> i32 {
1860 ((value as i64 * 65_535) / extent.saturating_sub(1).max(1) as i64).clamp(0, 65_535) as i32
1861}
1862
1863fn rect_extent(start: i32, end: i32) -> Result<u32, String> {
1864 u32::try_from(end.saturating_sub(start))
1865 .ok()
1866 .filter(|extent| *extent > 0)
1867 .ok_or_else(|| format!("invalid rectangle extent {start}..{end}"))
1868}
1869
1870fn x_button_bit(data: u32) -> u8 {
1871 match data {
1872 1 => BUTTON_X1,
1873 2 => BUTTON_X2,
1874 _ => 0,
1875 }
1876}
1877
1878fn rect_eq(left: RECT, right: RECT) -> bool {
1879 left.left == right.left
1880 && left.top == right.top
1881 && left.right == right.right
1882 && left.bottom == right.bottom
1883}
1884
1885fn low_word_signed(value: isize) -> i32 {
1886 value as u16 as i16 as i32
1887}
1888
1889fn high_word_signed(value: isize) -> i32 {
1890 (value as usize >> 16) as u16 as i16 as i32
1891}
1892
1893#[cfg(test)]
1894mod tests {
1895 use super::boundary::{Boundary, GuardRects};
1896 use super::navigation::Navigation;
1897 use super::screenshot::Progress;
1898 use super::{
1899 ActiveEndpoint, ClipBaseline, CoordinateTransform, INPUT_TAG_SIGNATURE, InputCoordination,
1900 NO_INPUT_ENDPOINT, PixelPoint, PixelRect, Rotation, ScreenshotHandoff, ScreenshotMode,
1901 ScreenshotRestoreDecision, input_tag, is_managed_input_tag, rect_eq,
1902 screenshot_endpoint_available, screenshot_handoff, screenshot_restore_decision,
1903 screenshot_return_point, stale_managed_move,
1904 };
1905 use windows::Win32::Foundation::{POINT, RECT};
1906 use windows::Win32::UI::WindowsAndMessaging::{
1907 WM_LBUTTONUP, WM_MBUTTONUP, WM_MOUSEMOVE, WM_MOUSEWHEEL, WM_RBUTTONUP, WM_XBUTTONUP,
1908 };
1909
1910 #[test]
1911 fn late_physical_move_after_return_cannot_push_cursor_to_virtual_edge() {
1912 let target = RECT {
1913 left: -200,
1914 top: -100,
1915 right: 0,
1916 bottom: 0,
1917 };
1918 let source = RECT {
1919 left: 20,
1920 top: 200,
1921 right: 380,
1922 bottom: 400,
1923 };
1924 let ordinary = RECT {
1925 left: 0,
1926 top: 0,
1927 right: 400,
1928 bottom: 200,
1929 };
1930 let boundary = Boundary::build(
1931 &[target, source, ordinary],
1932 &[GuardRects { target, source }],
1933 )
1934 .unwrap();
1935 let physical = POINT { x: -100, y: -50 };
1936 let nav = Navigation::new(boundary.screenshot_screens(), physical, 0).unwrap();
1937 let returned = nav.return_point(physical);
1938 // Before the fix, a delayed physical-space move goes through the
1939 // ordinary boundary hook after navigation is removed, landing here.
1940 let old_move = POINT { x: -99, y: -49 };
1941 assert!(boundary.is_forbidden(old_move));
1942 assert_eq!(
1943 boundary.correct_movement(old_move, Some(returned)),
1944 POINT { x: 20, y: 200 }
1945 );
1946 let mut cursor = returned;
1947 for (time, point) in [
1948 (999, old_move),
1949 (
1950 1_001,
1951 POINT {
1952 x: returned.x + 1,
1953 y: returned.y + 1,
1954 },
1955 ),
1956 (1_000, old_move),
1957 ] {
1958 if !super::screenshot::stale_return_motion(time, 1_001, 1_010) {
1959 cursor = if boundary.is_forbidden(point) {
1960 boundary.correct_movement(point, Some(cursor))
1961 } else {
1962 point
1963 };
1964 }
1965 }
1966 assert_eq!(
1967 cursor,
1968 POINT {
1969 x: returned.x + 1,
1970 y: returned.y + 1
1971 }
1972 );
1973 }
1974
1975 #[test]
1976 fn queued_mapped_move_cannot_undo_screenshot_handoff() {
1977 let endpoint = ActiveEndpoint::new();
1978 let tag = input_tag(17);
1979 endpoint.replace(11);
1980 let rejects = |message, event_tag| {
1981 stale_managed_move(
1982 message,
1983 event_tag,
1984 tag,
1985 endpoint.window.load(std::sync::atomic::Ordering::Acquire),
1986 )
1987 };
1988 assert!(!rejects(WM_MOUSEMOVE, tag));
1989 assert!(endpoint.release(11));
1990 // The pending virtual move arrives after the physical cursor handoff.
1991 assert!(rejects(WM_MOUSEMOVE, tag));
1992 // Releasing synthetic buttons during handoff must remain possible.
1993 for message in [
1994 WM_LBUTTONUP,
1995 WM_RBUTTONUP,
1996 WM_MBUTTONUP,
1997 WM_XBUTTONUP,
1998 WM_MOUSEWHEEL,
1999 ] {
2000 assert!(!rejects(message, tag));
2001 }
2002 assert!(!rejects(WM_MOUSEMOVE, 0));
2003 assert!(!rejects(WM_MOUSEMOVE, input_tag(23)));
2004 // Screenshot restoration publishes the endpoint before injecting its
2005 // return move; every mapping hook must let that valid move through.
2006 endpoint.replace(22);
2007 assert!(!rejects(WM_MOUSEMOVE, tag));
2008 assert!(!rejects(WM_MOUSEMOVE, input_tag(23)));
2009 assert!(!endpoint.release(11));
2010 assert!(!rejects(WM_MOUSEMOVE, input_tag(23)));
2011 }
2012
2013 #[test]
2014 fn repeated_screenshot_shortcut_keeps_the_strongest_return_mode() {
2015 let mode = ScreenshotMode::PauseOnly.with_handoff(ScreenshotMode::Native);
2016 assert_eq!(mode, ScreenshotMode::Native);
2017 assert_eq!(
2018 mode.with_handoff(ScreenshotMode::PauseOnly),
2019 ScreenshotMode::Native
2020 );
2021 let mode = mode.with_handoff(ScreenshotMode::Captured);
2022 assert_eq!(mode, ScreenshotMode::Captured);
2023 assert_eq!(
2024 mode.with_handoff(ScreenshotMode::Native),
2025 ScreenshotMode::Captured
2026 );
2027 assert_eq!(
2028 mode.with_handoff(ScreenshotMode::PauseOnly),
2029 ScreenshotMode::Captured
2030 );
2031 }
2032
2033 #[test]
2034 fn foreign_clip_replacement_discards_our_stale_restore_authority() {
2035 let desktop = ClipBaseline {
2036 rect: RECT {
2037 left: -1920,
2038 top: 0,
2039 right: 1920,
2040 bottom: 1080,
2041 },
2042 was_full_desktop: true,
2043 };
2044 let ours = RECT {
2045 left: 0,
2046 top: 0,
2047 right: 1920,
2048 bottom: 1080,
2049 };
2050 let foreign = ClipBaseline {
2051 rect: RECT {
2052 left: 200,
2053 top: 200,
2054 right: 900,
2055 bottom: 700,
2056 },
2057 was_full_desktop: false,
2058 };
2059 let mut coordination = InputCoordination::new();
2060 coordination.clip_baseline = Some(desktop);
2061 coordination.applied_clip = Some(ours);
2062 coordination.observe_clip(foreign);
2063 assert!(coordination.clip_baseline.is_none());
2064 assert!(coordination.applied_clip.is_none());
2065 assert!(!coordination.native_clip_allowed(foreign));
2066 // Native clipping can resume once that application releases its clip.
2067 assert!(coordination.native_clip_allowed(desktop));
2068 }
2069
2070 #[test]
2071 fn unchanged_own_clip_keeps_original_baseline_for_restore_retry() {
2072 let original = ClipBaseline {
2073 rect: RECT {
2074 left: -1920,
2075 top: 0,
2076 right: 1920,
2077 bottom: 1080,
2078 },
2079 was_full_desktop: true,
2080 };
2081 let current = ClipBaseline {
2082 rect: RECT {
2083 left: 0,
2084 top: 0,
2085 right: 1920,
2086 bottom: 1080,
2087 },
2088 was_full_desktop: false,
2089 };
2090 let mut coordination = InputCoordination::new();
2091 coordination.clip_baseline = Some(original);
2092 coordination.applied_clip = Some(current.rect);
2093 coordination.observe_clip(current);
2094 assert!(rect_eq(
2095 coordination.clip_baseline.unwrap().rect,
2096 original.rect
2097 ));
2098 assert!(coordination.native_clip_allowed(current));
2099 // A capture that borrowed another application's clip must return it,
2100 // rather than replacing it with the native boundary at capture end.
2101 coordination.clip_baseline = Some(current);
2102 assert!(!coordination.native_clip_allowed(current));
2103 }
2104
2105 #[test]
2106 fn restoration_lease_blocks_activation_during_native_cursor_movement() {
2107 let mut coordination = InputCoordination::new();
2108 assert!(coordination.reserve_restoration(11, NO_INPUT_ENDPOINT, 7, 7));
2109 assert!(!coordination.activation_allowed());
2110 assert!(!coordination.reserve_restoration(22, NO_INPUT_ENDPOINT, 7, 7));
2111 coordination.release_restoration(22);
2112 assert!(!coordination.activation_allowed());
2113 coordination.release_restoration(11);
2114 assert!(coordination.activation_allowed());
2115 }
2116
2117 #[test]
2118 fn restoration_lease_blocks_transfer_after_captured_endpoint_commit() {
2119 let mut coordination = InputCoordination::new();
2120 let endpoints = ActiveEndpoint::new();
2121 assert!(coordination.reserve_restoration(11, NO_INPUT_ENDPOINT, 7, 7));
2122 endpoints.replace(11);
2123 // The endpoint is published before MOVE; another capture must still
2124 // wait until that movement and the captured state have been committed.
2125 assert!(!coordination.activation_allowed());
2126 assert!(endpoints.owns(11));
2127 coordination.release_restoration(11);
2128 assert!(coordination.activation_allowed());
2129 assert_eq!(endpoints.replace(22), 11);
2130 }
2131
2132 #[test]
2133 fn rejected_restoration_does_not_block_later_activation() {
2134 let mut coordination = InputCoordination::new();
2135 assert!(!coordination.reserve_restoration(11, 22, 7, 7));
2136 assert!(!coordination.reserve_restoration(11, NO_INPUT_ENDPOINT, 8, 7));
2137 assert!(coordination.activation_allowed());
2138 }
2139
2140 #[test]
2141 fn screenshot_return_does_not_reclaim_after_another_capture_already_ended() {
2142 assert!(screenshot_endpoint_available(NO_INPUT_ENDPOINT, 7, 7));
2143 assert!(!screenshot_endpoint_available(42, 7, 7));
2144 assert!(!screenshot_endpoint_available(NO_INPUT_ENDPOINT, 8, 7));
2145 }
2146
2147 #[test]
2148 fn screenshot_return_uses_current_physical_position_and_checks_its_monitor() {
2149 let target = RECT {
2150 left: 1920,
2151 top: 0,
2152 right: 3840,
2153 bottom: 1080,
2154 };
2155 let source_to_target = screenshot_transform();
2156 let transform = CoordinateTransform::stretch(
2157 PixelRect {
2158 left: 0,
2159 top: 0,
2160 ..source_to_target.source
2161 },
2162 source_to_target.target,
2163 Rotation::Deg0,
2164 )
2165 .unwrap();
2166 assert_eq!(
2167 screenshot_return_point(target, transform, POINT { x: 2880, y: 540 }),
2168 Some((
2169 PixelPoint { x: 960, y: 540 },
2170 PixelPoint { x: -1280, y: -720 }
2171 ))
2172 );
2173 assert_eq!(
2174 screenshot_return_point(target, transform, POINT { x: 3839, y: 1079 }),
2175 Some((PixelPoint { x: 1919, y: 1079 }, PixelPoint { x: -1, y: -1 }))
2176 );
2177 for cursor in [
2178 POINT { x: 1919, y: 540 },
2179 POINT { x: 3840, y: 540 },
2180 POINT { x: 2880, y: -1 },
2181 POINT { x: 2880, y: 1080 },
2182 POINT { x: -1280, y: -720 },
2183 ] {
2184 assert!(screenshot_return_point(target, transform, cursor).is_none());
2185 }
2186 }
2187
2188 #[test]
2189 fn screenshot_return_waits_for_overlay_and_drag_to_end() {
2190 assert_eq!(
2191 screenshot_restore_decision(Progress::Waiting, Some(true), true, true, false, true),
2192 ScreenshotRestoreDecision::Wait
2193 );
2194 for overlay in [None, Some(true)] {
2195 assert_eq!(
2196 screenshot_restore_decision(Progress::Finished, overlay, true, true, false, true),
2197 ScreenshotRestoreDecision::Wait
2198 );
2199 }
2200 for progress in [Progress::Finished, Progress::TimedOut] {
2201 assert_eq!(
2202 screenshot_restore_decision(progress, Some(false), true, true, true, true),
2203 ScreenshotRestoreDecision::Wait
2204 );
2205 assert_eq!(
2206 screenshot_restore_decision(progress, Some(false), true, true, false, true),
2207 ScreenshotRestoreDecision::Restore
2208 );
2209 }
2210 }
2211
2212 #[test]
2213 fn screenshot_return_cancels_after_other_input_or_leaving_the_target() {
2214 for progress in [Progress::Finished, Progress::TimedOut] {
2215 assert_eq!(
2216 screenshot_restore_decision(progress, Some(false), false, true, false, true),
2217 ScreenshotRestoreDecision::Cancel
2218 );
2219 assert_eq!(
2220 screenshot_restore_decision(progress, Some(false), true, false, false, true),
2221 ScreenshotRestoreDecision::Cancel
2222 );
2223 }
2224 for (overlay, receives_input) in [(None, true), (Some(true), true), (Some(false), false)] {
2225 assert_eq!(
2226 screenshot_restore_decision(
2227 Progress::TimedOut,
2228 overlay,
2229 true,
2230 true,
2231 false,
2232 receives_input
2233 ),
2234 ScreenshotRestoreDecision::Cancel
2235 );
2236 }
2237 }
2238
2239 fn screenshot_transform() -> CoordinateTransform {
2240 CoordinateTransform::stretch(
2241 PixelRect {
2242 left: -2560,
2243 top: -1440,
2244 width: 2560,
2245 height: 1440,
2246 },
2247 PixelRect {
2248 left: 1920,
2249 top: 0,
2250 width: 1920,
2251 height: 1080,
2252 },
2253 Rotation::Deg0,
2254 )
2255 .unwrap()
2256 }
2257
2258 #[test]
2259 fn screenshot_maps_live_uncaptured_cursor_to_its_physical_display() {
2260 let transform = screenshot_transform();
2261 for (cursor, expected) in [
2262 (
2263 PixelPoint { x: -1280, y: -720 },
2264 PixelPoint { x: 2879, y: 539 },
2265 ),
2266 (
2267 PixelPoint { x: -1920, y: -1080 },
2268 PixelPoint { x: 2399, y: 269 },
2269 ),
2270 (
2271 PixelPoint { x: -2560, y: -1440 },
2272 PixelPoint { x: 1920, y: 0 },
2273 ),
2274 (PixelPoint { x: -1, y: -1 }, PixelPoint { x: 3839, y: 1079 }),
2275 ] {
2276 assert_eq!(
2277 screenshot_handoff(false, false, transform, Some(cursor)),
2278 ScreenshotHandoff::MoveCursor(expected)
2279 );
2280 }
2281 }
2282
2283 #[test]
2284 fn screenshot_leaves_physical_displays_and_other_sources_alone() {
2285 let transform = screenshot_transform();
2286 for cursor in [
2287 PixelPoint { x: 2500, y: 500 },
2288 PixelPoint { x: 0, y: -720 },
2289 PixelPoint { x: -1280, y: 0 },
2290 PixelPoint { x: -2561, y: -720 },
2291 PixelPoint { x: -1280, y: -1441 },
2292 ] {
2293 assert_eq!(
2294 screenshot_handoff(false, false, transform, Some(cursor)),
2295 ScreenshotHandoff::Ignore
2296 );
2297 }
2298 assert_eq!(
2299 screenshot_handoff(false, false, transform, None),
2300 ScreenshotHandoff::Ignore
2301 );
2302 }
2303
2304 #[test]
2305 fn screenshot_releases_only_the_captured_endpoint_owner() {
2306 let transform = screenshot_transform();
2307 for cursor in [None, Some(PixelPoint { x: -1280, y: -720 })] {
2308 assert_eq!(
2309 screenshot_handoff(true, true, transform, cursor),
2310 ScreenshotHandoff::ReleaseCapture
2311 );
2312 assert_eq!(
2313 screenshot_handoff(true, false, transform, cursor),
2314 ScreenshotHandoff::Ignore
2315 );
2316 }
2317 }
2318
2319 #[test]
2320 fn stale_endpoint_cannot_release_new_owner() {
2321 let endpoints = ActiveEndpoint::new();
2322 assert_eq!(endpoints.replace(11), NO_INPUT_ENDPOINT);
2323 assert_eq!(endpoints.replace(22), 11);
2324 assert!(!endpoints.release(11));
2325 assert!(endpoints.owns(22));
2326 assert!(endpoints.release(22));
2327 assert!(!endpoints.owns(22));
2328 }
2329
2330 #[test]
2331 fn all_sbms_injection_tags_are_recognized() {
2332 let first = input_tag(1);
2333 let second = input_tag(usize::MAX);
2334 assert!(is_managed_input_tag(first));
2335 assert!(is_managed_input_tag(second));
2336 assert!(is_managed_input_tag(INPUT_TAG_SIGNATURE));
2337 assert!(!is_managed_input_tag(0));
2338 }
2339}
2340