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.
src/input/navigation.rs17.5 KB
1
//! Screenshot navigation in the visible workspace. Pure geometry only.2
//!3
//! Movement within a screen stays native. At an edge, its logical neighbour4
//! determines the destination, even when the two physical outputs are far apart.5
use windows::Win32::Foundation::{POINT, RECT};6
7
#[derive(Clone, Copy, Debug)]8
pub(super) struct Screen {9
pub(super) logical: RECT,10
pub(super) physical: RECT,11
}12
13
pub(super) struct Navigation {14
screens: Vec<Screen>,15
current: usize,16
held: u8,17
last_warp_time: Option<u32>,18
}19
20
#[derive(Clone, Copy)]21
enum Edge {22
Left,23
Right,24
Top,25
Bottom,26
}27
28
impl Navigation {29
pub(super) fn new(screens: Vec<Screen>, cursor: POINT, held: u8) -> Option<Self> {30
if screens31
.iter()32
.any(|s| !valid(s.logical) || !valid(s.physical))33
{34
return None;35
}36
for (i, a) in screens.iter().enumerate() {37
if screens[i + 1..]38
.iter()39
.any(|b| overlaps(a.logical, b.logical) || overlaps(a.physical, b.physical))40
{41
return None;42
}43
}44
// A new session starts in the normal logical workspace. Physical45
// lookup also permits an already completed handoff from another guard.46
let current = screens47
.iter()48
.position(|s| contains(s.logical, cursor))49
.or_else(|| screens.iter().position(|s| contains(s.physical, cursor)))?;50
Some(Self {51
screens,52
current,53
held,54
last_warp_time: None,55
})56
}57
58
pub(super) fn clip(&self) -> RECT {59
self.screens[self.current].physical60
}61
62
pub(super) fn handoff(&self, cursor: POINT) -> POINT {63
let screen = self.screens[self.current];64
let point = if contains(screen.logical, cursor) {65
map(cursor, screen.logical, screen.physical)66
} else {67
clamp(screen.physical, cursor)68
};69
// Enter one pixel inside the output. Even a clip-truncated first move70
// then has an outward direction at an outer desktop edge.71
inset(screen.physical, point)72
}73
74
pub(super) fn return_point(&self, cursor: POINT) -> POINT {75
self.screens76
.iter()77
.find(|s| contains(s.physical, cursor))78
.map(|s| map(cursor, s.physical, s.logical))79
.unwrap_or(cursor)80
}81
82
pub(super) fn button(&mut self, bit: u8, down: bool) {83
if down {84
self.held |= bit;85
} else {86
self.held &= !bit;87
}88
}89
90
pub(super) fn sync_buttons(&mut self, held: u8) {91
self.held = held;92
}93
94
pub(super) fn accepts_motion(&self, time: u32) -> bool {95
self.last_warp_time96
.is_none_or(|last| (time.wrapping_sub(last) as i32) > 0)97
}98
99
pub(super) fn warped_at(&mut self, time: u32) {100
self.last_warp_time = Some(time);101
}102
103
/// Raw deltas provide direction only, never speed/acceleration. They are104
/// needed when Windows has clipped both the old and new native positions105
/// to the same edge pixel (especially when changing direction at a corner).106
pub(super) fn raw_edge_move(&mut self, delta: POINT, cursor: POINT) -> POINT {107
let clip = self.clip();108
if !contains(clip, cursor) {109
return cursor;110
}111
let mut attempted = cursor;112
if (cursor.x == clip.left && delta.x < 0) || (cursor.x == clip.right - 1 && delta.x > 0) {113
attempted.x = cursor.x.saturating_add(delta.x.signum());114
}115
if (cursor.y == clip.top && delta.y < 0) || (cursor.y == clip.bottom - 1 && delta.y > 0) {116
attempted.y = cursor.y.saturating_add(delta.y.signum());117
}118
if attempted == cursor {119
cursor120
} else {121
self.move_to(attempted, cursor)122
}123
}124
125
pub(super) fn move_to(&mut self, attempted: POINT, previous: POINT) -> POINT {126
let screen = self.screens[self.current];127
let previous = clamp(screen.physical, previous);128
let constrained = clamp(screen.physical, attempted);129
let direction = POINT {130
x: attempted.x.saturating_sub(previous.x),131
y: attempted.y.saturating_sub(previous.y),132
};133
if self.held != 0 {134
return constrained;135
}136
let Some((edge, hit)) = exit_edge(screen.physical, previous, attempted, direction) else {137
return constrained;138
};139
let logical_hit = map(hit, screen.physical, screen.logical);140
let next = self.screens.iter().enumerate().find(|(i, candidate)| {141
*i != self.current && adjacent(screen.logical, candidate.logical, logical_hit, edge)142
});143
let Some((index, next)) = next else {144
return constrained;145
};146
// Preserve tangential position and the remainder of a fast move. Stop147
// within the first adjacent screen rather than jumping over a gap.148
let logical_end = map_unbounded(attempted, screen.physical, screen.logical);149
let mut destination = map(150
clamp(next.logical, logical_end),151
next.logical,152
next.physical,153
);154
match edge {155
Edge::Left | Edge::Right => {156
destination.x = inset_axis(destination.x, next.physical.left, next.physical.right)157
}158
Edge::Top | Edge::Bottom => {159
destination.y = inset_axis(destination.y, next.physical.top, next.physical.bottom)160
}161
}162
self.current = index;163
destination164
}165
}166
167
fn exit_edge(rect: RECT, from: POINT, to: POINT, direction: POINT) -> Option<(Edge, POINT)> {168
let dx = f64::from(to.x) - f64::from(from.x);169
let dy = f64::from(to.y) - f64::from(from.y);170
let mut first: Option<(f64, Edge)> = None;171
for (edge, outward, reaches, distance, delta) in [172
(173
Edge::Left,174
direction.x < 0,175
to.x <= rect.left,176
f64::from(rect.left) - f64::from(from.x),177
dx,178
),179
(180
Edge::Right,181
direction.x > 0,182
to.x >= rect.right - 1,183
f64::from(rect.right - 1) - f64::from(from.x),184
dx,185
),186
(187
Edge::Top,188
direction.y < 0,189
to.y <= rect.top,190
f64::from(rect.top) - f64::from(from.y),191
dy,192
),193
(194
Edge::Bottom,195
direction.y > 0,196
to.y >= rect.bottom - 1,197
f64::from(rect.bottom - 1) - f64::from(from.y),198
dy,199
),200
] {201
if !outward || !reaches {202
continue;203
}204
let fraction = if delta == 0.0 {205
0.0206
} else {207
(distance / delta).clamp(0.0, 1.0)208
};209
if first.is_none_or(|(time, _)| fraction < time) {210
first = Some((fraction, edge));211
}212
}213
first.map(|(time, edge)| {214
(215
edge,216
clamp(217
rect,218
POINT {219
x: (f64::from(from.x) + dx * time).round() as i32,220
y: (f64::from(from.y) + dy * time).round() as i32,221
},222
),223
)224
})225
}226
227
fn adjacent(from: RECT, to: RECT, hit: POINT, edge: Edge) -> bool {228
match edge {229
Edge::Left => from.left == to.right && hit.y >= to.top && hit.y < to.bottom,230
Edge::Right => from.right == to.left && hit.y >= to.top && hit.y < to.bottom,231
Edge::Top => from.top == to.bottom && hit.x >= to.left && hit.x < to.right,232
Edge::Bottom => from.bottom == to.top && hit.x >= to.left && hit.x < to.right,233
}234
}235
236
fn map(point: POINT, from: RECT, to: RECT) -> POINT {237
clamp(to, map_unbounded(clamp(from, point), from, to))238
}239
240
fn map_unbounded(point: POINT, from: RECT, to: RECT) -> POINT {241
fn axis(value: i32, start: i32, end: i32, target: i32, limit: i32) -> i32 {242
// Inclusive endpoints, rounded to the nearest pixel. Rounding avoids243
// cumulative tangential drift when repeatedly crossing scaled screens.244
// Widen before arithmetic to keep extreme desktop coordinates safe.245
let source_span = (i128::from(end) - i128::from(start) - 1).max(1);246
let target_span = i128::from(limit) - i128::from(target) - 1;247
let numerator = (i128::from(value) - i128::from(start)) * target_span;248
(i128::from(target) + (numerator + source_span / 2).div_euclid(source_span))249
.clamp(i128::from(i32::MIN), i128::from(i32::MAX)) as i32250
}251
POINT {252
x: axis(point.x, from.left, from.right, to.left, to.right),253
y: axis(point.y, from.top, from.bottom, to.top, to.bottom),254
}255
}256
257
fn inset_axis(value: i32, start: i32, end: i32) -> i32 {258
if i64::from(end) - i64::from(start) > 2 {259
value.clamp(start + 1, end - 2)260
} else {261
value.clamp(start, end - 1)262
}263
}264
265
fn inset(rect: RECT, point: POINT) -> POINT {266
POINT {267
x: inset_axis(point.x, rect.left, rect.right),268
y: inset_axis(point.y, rect.top, rect.bottom),269
}270
}271
272
fn valid(rect: RECT) -> bool {273
rect.left < rect.right && rect.top < rect.bottom274
}275
fn contains(rect: RECT, point: POINT) -> bool {276
point.x >= rect.left && point.x < rect.right && point.y >= rect.top && point.y < rect.bottom277
}278
fn clamp(rect: RECT, point: POINT) -> POINT {279
POINT {280
x: point.x.clamp(rect.left, rect.right - 1),281
y: point.y.clamp(rect.top, rect.bottom - 1),282
}283
}284
fn overlaps(a: RECT, b: RECT) -> bool {285
a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom286
}287
288
#[cfg(test)]289
mod tests {290
use super::*;291
fn r(left: i32, top: i32, right: i32, bottom: i32) -> RECT {292
RECT {293
left,294
top,295
right,296
bottom,297
}298
}299
fn p(x: i32, y: i32) -> POINT {300
POINT { x, y }301
}302
fn screens() -> Vec<Screen> {303
vec![304
Screen {305
logical: r(0, 0, 400, 200),306
physical: r(0, 0, 400, 200),307
},308
Screen {309
logical: r(20, 200, 380, 400),310
physical: r(-200, -100, 0, 0),311
},312
]313
}314
fn nav() -> Navigation {315
Navigation::new(screens(), p(200, 300), 0).unwrap()316
}317
318
#[test]319
fn handoff_and_up_follow_virtual_layout() {320
let mut nav = nav();321
let handoff = nav.handoff(p(200, 300));322
assert!(contains(r(-200, -100, 0, 0), handoff));323
let landed = nav.move_to(p(-100, -100), p(-100, -99));324
assert_eq!(nav.clip(), r(0, 0, 400, 200));325
assert!((198..=202).contains(&landed.x));326
assert_eq!(landed.y, 198);327
let returned = nav.move_to(p(landed.x, 200), landed);328
assert_eq!(nav.clip(), r(-200, -100, 0, 0));329
assert_eq!(returned.y, -99);330
}331
#[test]332
fn interior_native_motion_is_unchanged() {333
let mut nav = nav();334
for (a, b) in [(p(-80, -40), p(-100, -50)), (p(-120, -80), p(-80, -40))] {335
assert_eq!(nav.move_to(a, b), a);336
}337
}338
#[test]339
fn wrong_physical_neighbour_is_blocked() {340
let mut nav = nav();341
assert_eq!(nav.move_to(p(100, 50), p(-2, -50)), p(-1, -1));342
assert_eq!(nav.clip(), r(-200, -100, 0, 0));343
}344
#[test]345
fn drag_never_crosses_and_release_allows_navigation() {346
let mut nav = nav();347
nav.button(1, true);348
assert_eq!(nav.move_to(p(-100, -120), p(-100, -99)), p(-100, -100));349
nav.button(2, true);350
nav.button(1, false);351
assert_eq!(nav.move_to(p(-100, -120), p(-100, -100)), p(-100, -100));352
nav.button(2, false);353
assert!(contains(354
r(0, 0, 400, 200),355
nav.move_to(p(-100, -120), p(-100, -100))356
));357
}358
#[test]359
fn non_overlapping_edge_segment_is_a_wall() {360
let mut nav = Navigation::new(screens(), p(5, 100), 0).unwrap();361
assert_eq!(nav.move_to(p(5, 220), p(5, 190)), p(5, 199));362
assert_eq!(nav.clip(), r(0, 0, 400, 200));363
}364
#[test]365
fn fast_diagonal_uses_first_edge_intersection() {366
let mut nav = Navigation::new(screens(), p(5, 100), 0).unwrap();367
// The endpoint is over screen 3, but the path hits the unconnected368
// portion of screen 1's lower edge first. It must not teleport.369
assert_eq!(nav.move_to(p(200, 4000), p(5, 198)), p(200, 199));370
assert_eq!(nav.clip(), r(0, 0, 400, 200));371
}372
#[test]373
fn return_uses_current_display_not_starting_display() {374
let mut nav = nav();375
assert_eq!(nav.return_point(p(-200, -100)), p(20, 200));376
let landed = nav.move_to(p(-100, -110), p(-100, -90));377
assert_eq!(nav.return_point(landed), landed);378
}379
#[test]380
fn initial_edge_is_inset_and_stationary_point_does_not_teleport() {381
let mut nav = nav();382
assert_eq!(nav.handoff(p(20, 200)), p(-199, -99));383
assert_eq!(nav.move_to(p(-100, -100), p(-100, -100)), p(-100, -100));384
}385
#[test]386
fn no_ping_pong_after_transition() {387
let mut nav = nav();388
let a = nav.move_to(p(-100, -100), p(-100, -99));389
assert_eq!(nav.move_to(a, a), a);390
assert_eq!(nav.move_to(p(a.x, a.y - 1), a), p(a.x, a.y - 1));391
}392
393
#[test]394
fn repeated_scaled_crossings_do_not_drift_sideways() {395
let mut nav = nav();396
let mut current = p(-100, -99);397
for _ in 0..100 {398
let upper = nav.move_to(p(current.x, -100), current);399
current = nav.move_to(p(upper.x, 199), upper);400
assert_eq!(current.x, -100);401
}402
}403
404
#[test]405
fn left_and_right_transfer_between_two_mapped_outputs() {406
let mut layout = screens();407
layout.push(Screen {408
logical: r(-300, 200, 20, 400),409
physical: r(-700, 0, -400, 200),410
});411
let mut nav = Navigation::new(layout, p(200, 300), 0).unwrap();412
let left = nav.move_to(p(-200, -50), p(-199, -50));413
assert_eq!(nav.clip(), r(-700, 0, -400, 200));414
assert_eq!(left.x, -402);415
let back = nav.move_to(p(-401, left.y), left);416
assert_eq!(nav.clip(), r(-200, -100, 0, 0));417
assert_eq!(back.x, -199);418
assert_eq!(back.y, -50);419
}420
421
#[test]422
fn held_button_at_start_and_missed_release_are_reconciled() {423
let mut nav = Navigation::new(screens(), p(200, 300), 1).unwrap();424
assert_eq!(nav.move_to(p(-100, -100), p(-100, -99)), p(-100, -100));425
nav.sync_buttons(0);426
let upper = nav.raw_edge_move(p(0, -1), p(-100, -100));427
assert!(contains(r(0, 0, 400, 200), upper));428
}429
430
#[test]431
fn starting_on_ordinary_screen_can_enter_mapped_output() {432
let mut nav = Navigation::new(screens(), p(200, 100), 0).unwrap();433
let lower = nav.move_to(p(200, 199), p(200, 195));434
assert!(contains(r(-200, -100, 0, 0), lower));435
assert!(contains(r(20, 200, 380, 400), nav.return_point(lower)));436
}437
438
#[test]439
fn fast_crossing_keeps_remainder_inside_adjacent_screen() {440
let mut nav = nav();441
let upper = nav.move_to(p(-100, -110), p(-100, -90));442
assert!((175..=181).contains(&upper.y));443
assert!(contains(nav.clip(), upper));444
assert_eq!(nav.move_to(p(220, 0), p(200, 0)), p(220, 0));445
}446
447
#[test]448
fn clipped_corner_can_change_direction_without_guessing() {449
let mut nav = nav();450
let corner = nav.move_to(p(-250, -120), p(-190, -90));451
assert_eq!(corner, p(-200, -100));452
assert_eq!(nav.move_to(corner, corner), corner);453
assert_eq!(nav.raw_edge_move(p(-10, 0), corner), corner);454
let upper = nav.raw_edge_move(p(0, -10), corner);455
assert!(contains(r(0, 0, 400, 200), upper));456
}457
458
#[test]459
fn raw_input_never_adds_movement_inside_a_screen_or_during_drag() {460
let mut nav = nav();461
assert_eq!(nav.raw_edge_move(p(500, -500), p(-100, -50)), p(-100, -50));462
nav.button(1, true);463
assert_eq!(nav.raw_edge_move(p(0, -10), p(-100, -100)), p(-100, -100));464
}465
466
#[test]467
fn queued_motion_cannot_undo_a_warp_and_clock_wrap_is_supported() {468
let mut nav = nav();469
nav.warped_at(100);470
assert!(!nav.accepts_motion(99));471
assert!(!nav.accepts_motion(100));472
assert!(nav.accepts_motion(101));473
nav.warped_at(u32::MAX);474
assert!(nav.accepts_motion(0));475
assert!(!nav.accepts_motion(u32::MAX - 1));476
}477
#[test]478
fn gaps_and_corner_only_contacts_are_not_neighbours() {479
let mut layout = screens();480
layout[1].logical = r(400, 200, 600, 300);481
let mut nav = Navigation::new(layout, p(399, 198), 0).unwrap();482
assert_eq!(nav.move_to(p(450, 240), p(399, 198)), p(399, 199));483
let mut layout = screens();484
layout[1].logical.top = 201;485
let mut nav = Navigation::new(layout, p(200, 198), 0).unwrap();486
assert_eq!(nav.move_to(p(200, 205), p(200, 198)), p(200, 199));487
}488
#[test]489
fn invalid_or_overlapping_layouts_are_rejected() {490
assert!(Navigation::new(vec![], p(0, 0), 0).is_none());491
let mut layout = screens();492
layout[1].physical = layout[0].physical;493
assert!(Navigation::new(layout, p(0, 0), 0).is_none());494
let layout = vec![Screen {495
logical: r(0, 0, 0, 100),496
physical: r(1, 1, 2, 2),497
}];498
assert!(Navigation::new(layout, p(0, 0), 0).is_none());499
}500
#[test]501
fn tiny_and_extreme_rectangles_do_not_overflow() {502
let layout = vec![Screen {503
logical: r(i32::MIN, 0, i32::MAX, 1),504
physical: r(-1, -1, 0, 0),505
}];506
let nav = Navigation::new(layout, p(0, 0), 0).unwrap();507
assert_eq!(nav.handoff(p(0, 0)), p(-1, -1));508
}509
}510