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.
文件
../ bininputuiconfig.rs81.2 KBcontrol.rs4.1 KBcontroller.rs43.6 KBdiagnostics.rs24.2 KBdisplay.rs29.8 KBgeometry.rs21.4 KBgpu_renderer.rs37.4 KBinput.rs79.7 KBlaunch_broker.rs10.3 KBlib.rs350 Blimits.rs714 Bmain.rs24.4 KBmapping.rs46.5 KBnetwork.rs9.2 KBrenderer.rs20.6 KBsession_gate.rs14.9 KBsunshine.rs12.2 KBui.rs68.5 KBvirtual_display.rs7.2 KBwin32_flyout.rs3.4 KBwin32_tray.rs22.3 KBwindow_migration.rs43.8 KBsrc/input.rs79.7 KB
1
use std::cell::RefCell;2
use std::mem::{MaybeUninit, size_of};3
use std::sync::Mutex;4
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};5
use std::time::Instant;6
7
use windows::Win32::Foundation::{HINSTANCE, HWND, LPARAM, LRESULT, POINT, RECT, WPARAM};8
use windows::Win32::Security::Cryptography::{BCRYPT_USE_SYSTEM_PREFERRED_RNG, BCryptGenRandom};9
use windows::Win32::System::SystemInformation::GetTickCount64;10
use 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
};18
use windows::Win32::UI::Input::{19
GetRawInputData, HRAWINPUT, MOUSE_MOVE_ABSOLUTE, RAWINPUT, RAWINPUTDEVICE, RID_INPUT,20
RIDEV_INPUTSINK, RIDEV_REMOVE, RIM_TYPEMOUSE, RegisterRawInputDevices,21
};22
use 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
32
use crate::display::active_display_topology;33
use crate::geometry::{CoordinateTransform, PixelPoint, PixelRect, Rotation};34
35
mod boundary;36
mod navigation;37
mod screenshot;38
39
const SCREENSHOT_TIMER_ID: usize = 0x5342_4d53;40
const SCREENSHOT_POLL_MS: u32 = 50;41
42
const WM_RELEASE_CAPTURE: u32 = WM_APP + 1;43
const RELEASE_NORMAL: isize = 0;44
const RELEASE_INJECTION_FAILURE: isize = 1;45
const RELEASE_EXTERNAL_INJECTION: isize = 2;46
const RELEASE_ABSOLUTE_INPUT: isize = 3;47
48
const BUTTON_LEFT: u8 = 1 << 0;49
const BUTTON_RIGHT: u8 = 1 << 1;50
const BUTTON_MIDDLE: u8 = 1 << 2;51
const BUTTON_X1: u8 = 1 << 3;52
const BUTTON_X2: u8 = 1 << 4;53
54
const NO_INPUT_ENDPOINT: usize = 0;55
const INPUT_TAG_MASK: usize = if usize::BITS >= 64 {56
0xffff_ffff_0000_0000_u64 as usize57
} else {58
0xffff_ffff_u64 as usize59
};60
const INPUT_TAG_SIGNATURE: usize = if usize::BITS >= 64 {61
0x5342_4d53_0000_0000_u64 as usize62
} else {63
0x5342_4d53_u64 as usize64
};65
66
static ACTIVE_INPUT_ENDPOINT: ActiveEndpoint = ActiveEndpoint::new();67
static INPUT_COORDINATION: Mutex<InputCoordination> = Mutex::new(InputCoordination::new());68
static INPUT_CAPTURE_GENERATION: AtomicUsize = AtomicUsize::new(0);69
static BOUNDARY_WARP_DEPTH: AtomicUsize = AtomicUsize::new(0);70
// Encoded as tick + 1 so a transfer at system tick zero is representable.71
static SCREENSHOT_RETURN_TICK: AtomicU64 = AtomicU64::new(0);72
73
thread_local! {74
static INPUT_STATE: RefCell<Option<InputMapper>> = const { RefCell::new(None) };75
}76
77
struct ActiveEndpoint {78
window: AtomicUsize,79
}80
81
impl 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.window94
.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) == window104
}105
}106
107
struct 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
117
impl 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 self136
.applied_clip137
.is_some_and(|ours| !rect_eq(ours, current.rect))138
{139
// Another application replaced our clip. Its current setting is140
// 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_desktop148
}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
true164
}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
173
struct ScreenshotRestorationLease {174
window: usize,175
}176
177
impl 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
None190
}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_ENDPOINT201
);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
232
impl Drop for ScreenshotRestorationLease {233
fn drop(&mut self) {234
lock_coordination().release_restoration(self.window);235
}236
}237
238
#[derive(Clone, Copy)]239
struct ClipBaseline {240
rect: RECT,241
was_full_desktop: bool,242
}243
244
struct 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
260
pub struct InputGuard;261
262
impl 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.target291
},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
342
impl Drop for InputGuard {343
fn drop(&mut self) {344
cleanup();345
}346
}347
348
pub 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
404
fn 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 = state409
.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
440
fn 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's461
// 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 == 0466
&& unsafe { mouse.Anonymous.Anonymous.usButtonFlags } == 0467
{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 = state491
.cursor492
.x493
.saturating_add(mouse.lLastX)494
.clamp(0, width - 1);495
state.cursor.y = state496
.cursor497
.y498
.saturating_add(mouse.lLastY)499
.clamp(0, height - 1);500
state.move_pending = true;501
});502
Ok(())503
}504
505
unsafe 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_MOUSEMOVE511
&& BOUNDARY_WARP_DEPTH.load(Ordering::Acquire) == 0512
&& 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 may515
// reach this hook after a screenshot has released capture and moved516
// the cursor to the physical display. Do not let that old move undo517
// 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: doing533
// 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 our537
// 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
613
unsafe 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)]629
enum ScreenshotHandoff {630
ReleaseCapture,631
MoveCursor(PixelPoint),632
Ignore,633
}634
635
#[derive(Clone, Copy, Debug, Eq, PartialEq)]636
enum ScreenshotMode {637
Native,638
Captured,639
/// Boundary-only session: the overlay may drag across mapped targets while640
/// the cursor is not on a source; no cursor handoff and no cursor restore.641
PauseOnly,642
}643
644
impl 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
654
struct ScreenshotRestore {655
session: screenshot::Session,656
mode: ScreenshotMode,657
capture_generation: usize,658
}659
660
#[derive(Clone, Copy, Debug, Eq, PartialEq)]661
enum ScreenshotRestoreDecision {662
Wait,663
Restore,664
Cancel,665
}666
667
fn 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::ReleaseCapture676
} else {677
ScreenshotHandoff::Ignore678
};679
}680
cursor681
.and_then(|point| source_to_target.map_target_point(point))682
.map(ScreenshotHandoff::MoveCursor)683
.unwrap_or(ScreenshotHandoff::Ignore)684
}685
686
fn 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
None701
} 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 screenshots715
// 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::Captured723
} else {724
ScreenshotMode::Native725
};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
745
fn 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. Screenshot772
// 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
true785
}786
787
/// Take the running screenshot session (killing its timer) without ending the788
/// global pause; callers must pair this with `end_screenshot_pause` after their789
/// cursor work so the boundary resumes only once the cursor is back in place.790
fn 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
803
fn cancel_screenshot_restore() {804
let restore = take_screenshot_restore();805
end_screenshot_pause(restore.is_some());806
}807
808
fn 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::Cancel825
} else {826
ScreenshotRestoreDecision::Wait827
};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::Restore839
}840
841
fn 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
transform851
.map_target_point(local)852
.map(|source| (local, source))853
}854
855
fn screenshot_endpoint_available(856
endpoint: usize,857
current_generation: usize,858
expected_generation: usize,859
) -> bool {860
endpoint == NO_INPUT_ENDPOINT && current_generation == expected_generation861
}862
863
fn 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. Never892
// 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_restore902
.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::TimedOut930
&& 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
950
fn 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 not967
// 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
975
fn 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 after992
// 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. Activating1003
// 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 move1029
// 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
1040
fn is_snipping_shortcut(vk_code: u32) -> bool {1041
const VK_S: u32 = b'S' as u32;1042
vk_code == VK_S1043
&& (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
1047
fn key_down(key: i32) -> i16 {1048
unsafe { GetAsyncKeyState(key) }1049
}1050
1051
fn 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) }) == 11069
}1070
1071
pub(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.captured1076
|| !state.move_pending1077
|| !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)) = pending1085
&& !send_mouse(point, MOUSEEVENTF_MOVE, 0, tag)1086
{1087
post_release(window, RELEASE_INJECTION_FAILURE);1088
}1089
}1090
1091
fn 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 mirror1119
// position is kept.1120
return set_cursor_position(boundary_adjusted_warp(target));1121
}1122
}1123
false1124
}1125
1126
fn 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
true1132
}1133
1134
fn 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
1148
fn 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
1165
fn 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
1173
fn 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 = registration1204
.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
1216
fn 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 external1226
// 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
true1232
}1233
1234
fn 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
1244
fn 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 destination1256
/// workspace happens after placement, so Windows never clamps to an unrelated1257
/// edge in the middle of the return. An external clip remains authoritative.1258
fn 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
1281
fn mark_screenshot_return() {1282
SCREENSHOT_RETURN_TICK.fetch_max(unsafe { GetTickCount64() } + 1, Ordering::AcqRel);1283
}1284
1285
fn place_screenshot_return(point: POINT) -> bool {1286
mark_screenshot_return();1287
let placed = set_cursor_position(point);1288
mark_screenshot_return();1289
placed1290
}1291
1292
fn 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, then1298
/// the native boundary while no screenshot pause is active. The external1299
/// baseline is preserved exactly once across capture/boundary swaps and is1300
/// restored only while the current clip still matches our last write. Native1301
/// mapping yields to another application's clip; explicit capture may claim1302
/// it temporarily. Hooks use try_lock and explicit cursor warps stay outside1303
/// the coordination lock.1304
fn 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_allowed1309
.then(|| {1310
coordination1311
.screenshot_navigation1312
.as_ref()1313
.map(navigation::Navigation::clip)1314
})1315
.flatten();1316
let boundary_rect = native_allowed1317
.then(|| {1318
coordination1319
.boundary1320
.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 mistakes1350
// 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 while1360
/// the boundary is live; paused or failed-open states keep the raw point.1361
fn boundary_adjusted_warp(point: POINT) -> POINT {1362
let boundary = {1363
let coordination = lock_coordination();1364
if coordination.boundary.has_pause() {1365
None1366
} 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 own1377
/// corrective warp from triggering another correction.1378
struct CursorWarpGuard;1379
1380
impl CursorWarpGuard {1381
fn new() -> Self {1382
BOUNDARY_WARP_DEPTH.fetch_add(1, Ordering::AcqRel);1383
Self1384
}1385
}1386
1387
impl Drop for CursorWarpGuard {1388
fn drop(&mut self) {1389
BOUNDARY_WARP_DEPTH.fetch_sub(1, Ordering::AcqRel);1390
}1391
}1392
1393
fn warp_cursor_with_bypass(point: POINT) {1394
let _warp = CursorWarpGuard::new();1395
set_cursor_position(point);1396
}1397
1398
fn boundary_consumable_message(message: u32) -> bool {1399
matches!(1400
message,1401
WM_MOUSEMOVE1402
| WM_LBUTTONDOWN1403
| WM_LBUTTONUP1404
| WM_RBUTTONDOWN1405
| WM_RBUTTONUP1406
| WM_MBUTTONDOWN1407
| WM_MBUTTONUP1408
| WM_XBUTTONDOWN1409
| WM_XBUTTONUP1410
| WM_MOUSEWHEEL1411
| WM_MOUSEHWHEEL1412
)1413
}1414
1415
/// Native-mode boundary fallback: consume movement/button deliveries aimed at1416
/// a mapped target display and correct the cursor back to the previous allowed1417
/// monitor edge immediately. Runs with no coordination lock held while the1418
/// cursor moves (try-lock snapshot only) and issues no topology queries.1419
fn 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 disables1432
// 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
None1442
} 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 this1453
// callback. In a ClipOnly layout this path should normally never run (the1454
// clip already blocks the move); it is the cheap soft fallback for when1455
// another application has cleared or replaced the global clip. Win32 gives1456
// 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 the1467
/// logical monitor layout off the hook path.1468
fn 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
1475
fn 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
1484
fn monitor_layout() -> Vec<RECT> {1485
active_display_topology()1486
.map(|displays| {1487
displays1488
.into_iter()1489
.map(|display| display.rect)1490
.collect::<Vec<_>>()1491
})1492
.unwrap_or_default()1493
}1494
1495
fn 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 a1513
/// fresh layout passes validation again; no stale corrective warp may survive.1514
fn 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 across1527
/// mappers). Every acquisition is balanced by `end_screenshot_pause`.1528
fn 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
1536
fn 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
coordination1549
.screenshot_navigation1550
.take()1551
.and_then(|navigation| {1552
let mut cursor = POINT::default();1553
(ACTIVE_INPUT_ENDPOINT.window.load(Ordering::Acquire) == NO_INPUT_ENDPOINT1554
&& unsafe { GetCursorPos(&mut cursor) }.is_ok())1555
.then(|| navigation.return_point(cursor))1556
})1557
} else {1558
None1559
};1560
let boundary = if coordination.boundary.has_pause() {1561
None1562
} else {1563
coordination.boundary.snapshot()1564
};1565
if let Some(point) = return_point1566
&& 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 on1586
// 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
1595
fn 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
1609
fn 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_clip1647
!= coordination1648
.screenshot_navigation1649
.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
destination1662
};1663
set_cursor_position(destination);1664
}1665
1666
/// Return true only when a native move has been replaced. Buttons always1667
/// remain native so the snipping tool owns the complete down/up sequence.1668
fn 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_clip1710
&& 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
destination1718
};1719
if destination == event.pt {1720
return false;1721
}1722
let _warp = CursorWarpGuard::new();1723
set_cursor_position(destination)1724
}1725
1726
fn 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_ENDPOINT1729
&& let Err(error) = unregister_raw_mouse()1730
{1731
eprintln!("warning: screenshot edge input cleanup failed: {error}");1732
}1733
}1734
1735
fn clear_screenshot_navigation(coordination: &mut InputCoordination) {1736
coordination.screenshot_navigation = None;1737
stop_screenshot_raw_input(coordination);1738
}1739
1740
fn 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
destination1775
};1776
set_cursor_position(destination);1777
}1778
1779
fn lock_coordination() -> std::sync::MutexGuard<'static, InputCoordination> {1780
INPUT_COORDINATION1781
.lock()1782
.unwrap_or_else(|poisoned| poisoned.into_inner())1783
}1784
1785
fn 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
1799
fn unregister_raw_mouse() -> Result<(), String> {1800
register_raw_mouse(HWND::default(), RIDEV_REMOVE)1801
}1802
1803
fn post_release(window: HWND, reason: isize) {1804
unsafe {1805
let _ = PostMessageW(Some(window), WM_RELEASE_CAPTURE, WPARAM(0), LPARAM(reason));1806
}1807
}1808
1809
fn source_point(state: &InputMapper) -> POINT {1810
let point = state1811
.transform1812
.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
1823
fn input_tag(nonce: usize) -> usize {1824
(nonce & !INPUT_TAG_MASK) | INPUT_TAG_SIGNATURE1825
}1826
1827
fn is_managed_input_tag(tag: usize) -> bool {1828
tag & INPUT_TAG_MASK == INPUT_TAG_SIGNATURE1829
}1830
1831
fn stale_managed_move(message: u32, tag: usize, owner_tag: usize, endpoint: usize) -> bool {1832
// Only the injecting guard rejects its move. Other SBMS instances have1833
// independent endpoint state and their tagged input must remain untouched.1834
message == WM_MOUSEMOVE1835
&& tag == owner_tag1836
&& is_managed_input_tag(tag)1837
&& endpoint == NO_INPUT_ENDPOINT1838
}1839
1840
fn window_key(window: HWND) -> usize {1841
window.0 as usize1842
}1843
1844
fn window_from_key(window: usize) -> HWND {1845
HWND(window as *mut _)1846
}1847
1848
fn 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
1859
fn 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 i321861
}1862
1863
fn 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
1870
fn x_button_bit(data: u32) -> u8 {1871
match data {1872
1 => BUTTON_X1,1873
2 => BUTTON_X2,1874
_ => 0,1875
}1876
}1877
1878
fn rect_eq(left: RECT, right: RECT) -> bool {1879
left.left == right.left1880
&& left.top == right.top1881
&& left.right == right.right1882
&& left.bottom == right.bottom1883
}1884
1885
fn low_word_signed(value: isize) -> i32 {1886
value as u16 as i16 as i321887
}1888
1889
fn high_word_signed(value: isize) -> i32 {1890
(value as usize >> 16) as u16 as i16 as i321891
}1892
1893
#[cfg(test)]1894
mod 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 the1939
// 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
point1963
};1964
}1965
}1966
assert_eq!(1967
cursor,1968
POINT {1969
x: returned.x + 1,1970
y: returned.y + 11971
}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 its2005
// 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::Native2020
);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::Captured2026
);2027
assert_eq!(2028
mode.with_handoff(ScreenshotMode::PauseOnly),2029
ScreenshotMode::Captured2030
);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.rect2097
));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 still2124
// 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.source2161
},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::Wait2193
);2194
for overlay in [None, Some(true)] {2195
assert_eq!(2196
screenshot_restore_decision(Progress::Finished, overlay, true, true, false, true),2197
ScreenshotRestoreDecision::Wait2198
);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::Wait2204
);2205
assert_eq!(2206
screenshot_restore_decision(progress, Some(false), true, true, false, true),2207
ScreenshotRestoreDecision::Restore2208
);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::Cancel2218
);2219
assert_eq!(2220
screenshot_restore_decision(progress, Some(false), true, false, false, true),2221
ScreenshotRestoreDecision::Cancel2222
);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_input2233
),2234
ScreenshotRestoreDecision::Cancel2235
);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::Ignore2296
);2297
}2298
assert_eq!(2299
screenshot_handoff(false, false, transform, None),2300
ScreenshotHandoff::Ignore2301
);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::ReleaseCapture2311
);2312
assert_eq!(2313
screenshot_handoff(true, false, transform, cursor),2314
ScreenshotHandoff::Ignore2315
);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