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 bd87fa1e。

main/ root / src/input
bd87fa1e· 固定源码快照
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 neighbour
4//! determines the destination, even when the two physical outputs are far apart.
5use windows::Win32::Foundation::{POINT, RECT};
6
7#[derive(Clone, Copy, Debug)]
8pub(super) struct Screen {
9 pub(super) logical: RECT,
10 pub(super) physical: RECT,
11}
12
13pub(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)]
21enum Edge {
22 Left,
23 Right,
24 Top,
25 Bottom,
26}
27
28impl Navigation {
29 pub(super) fn new(screens: Vec<Screen>, cursor: POINT, held: u8) -> Option<Self> {
30 if screens
31 .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. Physical
45 // lookup also permits an already completed handoff from another guard.
46 let current = screens
47 .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].physical
60 }
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 move
70 // 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.screens
76 .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_time
96 .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 are
104 /// needed when Windows has clipped both the old and new native positions
105 /// 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 cursor
120 } 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. Stop
147 // 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 destination
164 }
165}
166
167fn 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.0
206 } 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
227fn 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
236fn map(point: POINT, from: RECT, to: RECT) -> POINT {
237 clamp(to, map_unbounded(clamp(from, point), from, to))
238}
239
240fn 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 avoids
243 // 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 i32
250 }
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
257fn 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
265fn 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
272fn valid(rect: RECT) -> bool {
273 rect.left < rect.right && rect.top < rect.bottom
274}
275fn contains(rect: RECT, point: POINT) -> bool {
276 point.x >= rect.left && point.x < rect.right && point.y >= rect.top && point.y < rect.bottom
277}
278fn 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}
284fn overlaps(a: RECT, b: RECT) -> bool {
285 a.left < b.right && b.left < a.right && a.top < b.bottom && b.top < a.bottom
286}
287
288#[cfg(test)]
289mod 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 unconnected
368 // 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