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/boundary.rs26.4 KB
1//! Pure cursor-boundary geometry for native mapping.
2//!
3//! This module never calls Win32 input APIs (no cursor movement, hooks or
4//! `ClipCursor`): it only classifies screen points against the logical monitor
5//! layout and computes correction targets, so every rule is unit-testable.
6
7use std::sync::Arc;
8
9use windows::Win32::Foundation::{POINT, RECT};
10
11/// Physical target and virtual source rectangles of one active input guard.
12#[derive(Clone, Copy, Debug)]
13pub(super) struct GuardRects {
14 pub(super) target: RECT,
15 pub(super) source: RECT,
16}
17
18impl PartialEq for GuardRects {
19 fn eq(&self, other: &Self) -> bool {
20 rect_eq(self.target, other.target) && rect_eq(self.source, other.source)
21 }
22}
23
24/// Validated boundary geometry for one registration generation.
25pub(super) struct Boundary {
26 allowed: Vec<RECT>,
27 forbidden: Vec<RECT>,
28 guards: Vec<GuardRects>,
29 clip: RECT,
30}
31
32impl Boundary {
33 /// Build boundary geometry from the logical monitor layout and the target
34 /// and source rectangles registered by the active input guards.
35 ///
36 /// Returns `None` (fail open: no boundary at all) when the registration is
37 /// invalid or stale: empty layout or guards, empty rectangles, registered
38 /// rectangles missing from the layout, or overlapping registrations.
39 pub(super) fn build(layout: &[RECT], guards: &[GuardRects]) -> Option<Boundary> {
40 if layout.is_empty() || guards.is_empty() {
41 return None;
42 }
43 if layout.iter().any(|monitor| !nonempty(*monitor)) {
44 return None;
45 }
46 let mut registered: Vec<RECT> = Vec::new();
47 for guard in guards {
48 for rect in [guard.target, guard.source] {
49 if !nonempty(rect) {
50 return None;
51 }
52 if !layout.iter().any(|monitor| rect_eq(*monitor, rect)) {
53 return None;
54 }
55 if registered.iter().any(|other| overlaps(*other, rect)) {
56 return None;
57 }
58 registered.push(rect);
59 }
60 }
61 let forbidden: Vec<RECT> = guards.iter().map(|guard| guard.target).collect();
62 let allowed: Vec<RECT> = layout
63 .iter()
64 .copied()
65 .filter(|monitor| !forbidden.iter().any(|target| overlaps(*target, *monitor)))
66 .collect();
67 if allowed.is_empty() {
68 return None;
69 }
70 let clip = bounding_box(&allowed);
71 Some(Boundary {
72 allowed,
73 forbidden,
74 guards: guards.to_vec(),
75 clip,
76 })
77 }
78
79 /// The single `ClipCursor` rectangle: the bounding box of the allowed
80 /// monitors. It may contain harmless gaps (the cursor cannot rest in
81 /// monitor-less space). When it covers no forbidden target rectangle it is
82 /// a complete hard boundary; otherwise the low-level hook corrects the
83 /// remaining forbidden regions (and covers the loss of the global clip).
84 pub(super) fn clip_rect(&self) -> RECT {
85 self.clip
86 }
87
88 /// The logical workspace with each virtual source projected onto its
89 /// physical output. Hidden source desktops never become screenshot targets.
90 pub(super) fn screenshot_screens(&self) -> Vec<super::navigation::Screen> {
91 self.allowed
92 .iter()
93 .map(|logical| super::navigation::Screen {
94 logical: *logical,
95 physical: self
96 .guards
97 .iter()
98 .find(|guard| rect_eq(guard.source, *logical))
99 .map_or(*logical, |guard| guard.target),
100 })
101 .collect()
102 }
103
104 pub(super) fn is_forbidden(&self, point: POINT) -> bool {
105 self.forbidden
106 .iter()
107 .any(|rect| contains_point(*rect, point))
108 }
109
110 /// Correct a forbidden movement attempt: clamp the attempted coordinates to
111 /// the previous allowed monitor (preserving the tangent along its edge) and
112 /// fall back to the nearest allowed monitor when the previous one is
113 /// unknown. Never teleports to a center point.
114 pub(super) fn correct_movement(&self, attempted: POINT, previous: Option<POINT>) -> POINT {
115 if let Some(rect) = previous.and_then(|point| {
116 self.allowed
117 .iter()
118 .find(|monitor| contains_point(**monitor, point))
119 .copied()
120 }) {
121 return clamp_point(rect, attempted);
122 }
123 self.warp_to_allowed(attempted)
124 }
125
126 /// Nearest allowed destination for a discrete warp (capture release, pause
127 /// resume): points that are not forbidden are left untouched.
128 pub(super) fn warp_to_allowed(&self, point: POINT) -> POINT {
129 if !self.is_forbidden(point) {
130 return point;
131 }
132 let nearest = self
133 .allowed
134 .iter()
135 .enumerate()
136 .min_by_key(|(index, rect)| (distance_sq(**rect, point), rect.left, rect.top, *index))
137 .map(|(_, rect)| *rect)
138 .expect("allowed monitors are nonempty");
139 clamp_point(nearest, point)
140 }
141}
142
143/// Desired clip state for the single `ClipCursor` owner.
144#[derive(Clone, Copy, Debug)]
145pub(super) enum ClipAction {
146 /// First own clip: preserve the external baseline, then set our rectangle.
147 SaveBaselineThenSet(RECT),
148 /// Swap our own clip rectangle; the baseline is kept untouched.
149 Set(RECT),
150 /// Nothing of ours remains: restore the preserved external baseline.
151 RestoreBaseline,
152 /// Nothing of ours and no baseline to restore.
153 None,
154}
155
156impl PartialEq for ClipAction {
157 fn eq(&self, other: &Self) -> bool {
158 match (self, other) {
159 (ClipAction::SaveBaselineThenSet(left), ClipAction::SaveBaselineThenSet(right))
160 | (ClipAction::Set(left), ClipAction::Set(right)) => rect_eq(*left, *right),
161 (ClipAction::RestoreBaseline, ClipAction::RestoreBaseline)
162 | (ClipAction::None, ClipAction::None) => true,
163 _ => false,
164 }
165 }
166}
167
168/// Priority: captured source clip; the screenshot pause releases the native
169/// boundary; the native boundary applies when no capture and no pause are
170/// active. `boundary` is the boundary clip rectangle while the boundary is
171/// live. `has_baseline` tracks whether the external baseline was preserved.
172pub(super) fn clip_action(
173 capture: Option<RECT>,
174 paused: bool,
175 boundary: Option<RECT>,
176 has_baseline: bool,
177) -> ClipAction {
178 let desired = capture.or(if paused { None } else { boundary });
179 match (has_baseline, desired) {
180 (false, Some(rect)) => ClipAction::SaveBaselineThenSet(rect),
181 (true, Some(rect)) => ClipAction::Set(rect),
182 (true, None) => ClipAction::RestoreBaseline,
183 (false, None) => ClipAction::None,
184 }
185}
186
187/// Global registration state shared by all input guards (multiple mappers).
188pub(super) struct BoundaryRegistry {
189 guards: Vec<(usize, GuardRects)>,
190 layout: Vec<RECT>,
191 layout_valid: bool,
192 pause_owners: usize,
193 snapshot: Option<Arc<Boundary>>,
194}
195
196impl BoundaryRegistry {
197 pub(super) const fn new() -> Self {
198 Self {
199 guards: Vec::new(),
200 layout: Vec::new(),
201 layout_valid: false,
202 pause_owners: 0,
203 snapshot: None,
204 }
205 }
206
207 pub(super) fn register(&mut self, window: usize, rects: GuardRects) {
208 self.guards.retain(|(key, _)| *key != window);
209 self.guards.push((window, rects));
210 self.rebuild();
211 }
212
213 pub(super) fn unregister(&mut self, window: usize) {
214 self.guards.retain(|(key, _)| *key != window);
215 self.rebuild();
216 }
217
218 pub(super) fn set_layout(&mut self, layout: Vec<RECT>) {
219 self.layout = layout;
220 self.layout_valid = true;
221 self.rebuild();
222 }
223
224 /// A display-layout mutation invalidates every guard's geometry; the
225 /// boundary stays off until a fresh layout passes validation again.
226 pub(super) fn invalidate(&mut self) {
227 self.layout.clear();
228 self.layout_valid = false;
229 self.rebuild();
230 }
231
232 pub(super) fn pause(&mut self) {
233 self.pause_owners = self.pause_owners.saturating_add(1);
234 }
235
236 pub(super) fn resume(&mut self) {
237 self.pause_owners = self.pause_owners.saturating_sub(1);
238 }
239
240 pub(super) fn has_pause(&self) -> bool {
241 self.pause_owners > 0
242 }
243
244 /// Current boundary geometry, ignoring the screenshot pause (callers fold
245 /// `has_pause` into their own decisions).
246 pub(super) fn snapshot(&self) -> Option<Arc<Boundary>> {
247 self.snapshot.clone()
248 }
249
250 fn rebuild(&mut self) {
251 self.snapshot = if self.layout_valid {
252 let guards: Vec<GuardRects> = self.guards.iter().map(|(_, rects)| *rects).collect();
253 Boundary::build(&self.layout, &guards).map(Arc::new)
254 } else {
255 None
256 };
257 }
258}
259
260fn nonempty(rect: RECT) -> bool {
261 rect.right > rect.left && rect.bottom > rect.top
262}
263
264fn rect_eq(left: RECT, right: RECT) -> bool {
265 left.left == right.left
266 && left.top == right.top
267 && left.right == right.right
268 && left.bottom == right.bottom
269}
270
271/// Nonzero-area intersection (rectangles are half-open).
272fn overlaps(left: RECT, right: RECT) -> bool {
273 left.left < right.right
274 && right.left < left.right
275 && left.top < right.bottom
276 && right.top < left.bottom
277}
278
279/// Half-open containment: `[left, right) x [top, bottom)`.
280fn contains_point(rect: RECT, point: POINT) -> bool {
281 point.x >= rect.left && point.x < rect.right && point.y >= rect.top && point.y < rect.bottom
282}
283
284fn clamp_point(rect: RECT, point: POINT) -> POINT {
285 POINT {
286 x: point.x.clamp(rect.left, rect.right - 1),
287 y: point.y.clamp(rect.top, rect.bottom - 1),
288 }
289}
290
291fn bounding_box(rects: &[RECT]) -> RECT {
292 let mut iter = rects.iter().copied();
293 let first = iter.next().expect("bounding box of a nonempty list");
294 let mut box_rect = first;
295 for rect in iter {
296 box_rect.left = box_rect.left.min(rect.left);
297 box_rect.top = box_rect.top.min(rect.top);
298 box_rect.right = box_rect.right.max(rect.right);
299 box_rect.bottom = box_rect.bottom.max(rect.bottom);
300 }
301 box_rect
302}
303
304/// Squared distance from a point to the nearest pixel of a half-open rectangle.
305/// Endpoint arithmetic happens in `i64` before subtracting and the squared
306/// distance accumulates in `u128`, so extreme coordinates cannot overflow.
307fn distance_sq(rect: RECT, point: POINT) -> u128 {
308 let x = i64::from(point.x);
309 let y = i64::from(point.y);
310 let left = i64::from(rect.left);
311 let top = i64::from(rect.top);
312 let right_edge = i64::from(rect.right) - 1;
313 let bottom_edge = i64::from(rect.bottom) - 1;
314 let dx = if x < left {
315 left - x
316 } else if x > right_edge {
317 x - right_edge
318 } else {
319 0
320 };
321 let dy = if y < top {
322 top - y
323 } else if y > bottom_edge {
324 y - bottom_edge
325 } else {
326 0
327 };
328 let dx = dx as u128;
329 let dy = dy as u128;
330 dx * dx + dy * dy
331}
332
333#[cfg(test)]
334mod tests {
335 use super::{Boundary, BoundaryRegistry, ClipAction, GuardRects, clip_action, overlaps};
336 use windows::Win32::Foundation::{POINT, RECT};
337
338 fn rect(left: i32, top: i32, right: i32, bottom: i32) -> RECT {
339 RECT {
340 left,
341 top,
342 right,
343 bottom,
344 }
345 }
346
347 fn point(x: i32, y: i32) -> POINT {
348 POINT { x, y }
349 }
350
351 /// Whether the clip bounding box is free of forbidden target rectangles:
352 /// the geometry where a single ClipCursor rectangle is a complete boundary.
353 fn clip_is_disjoint_from_forbidden(boundary: &Boundary) -> bool {
354 !boundary
355 .forbidden
356 .iter()
357 .any(|target| overlaps(*target, boundary.clip))
358 }
359
360 /// Screenshot layout: 2 at the upper left of 1, 3 below 1; 2 is the mapped
361 /// physical target, 1 and 3 stay usable.
362 fn screenshot_layout() -> [RECT; 3] {
363 [
364 rect(0, 0, 1920, 1080),
365 rect(-1280, -720, 0, 0),
366 rect(0, 1080, 1920, 2160),
367 ]
368 }
369
370 #[test]
371 fn screenshot_layout_allowing_1_and_3_uses_clip_only() {
372 let layout = screenshot_layout();
373 let boundary = Boundary::build(
374 &layout,
375 &[GuardRects {
376 target: layout[1],
377 source: layout[0],
378 }],
379 )
380 .expect("valid registration");
381 assert!(clip_is_disjoint_from_forbidden(&boundary));
382 let clip = boundary.clip_rect();
383 assert_eq!(
384 (clip.left, clip.top, clip.right, clip.bottom),
385 (0, 0, 1920, 2160)
386 );
387 assert!(boundary.is_forbidden(point(-1, -1)));
388 assert!(boundary.is_forbidden(point(-1280, -720)));
389 assert!(!boundary.is_forbidden(point(0, 0)));
390 assert!(!boundary.is_forbidden(point(500, 1500)));
391 }
392
393 #[test]
394 fn forbidden_between_allowed_monitors_requires_hook_correction() {
395 let layout = [
396 rect(0, 0, 1920, 1080),
397 rect(640, 1080, 1280, 1380),
398 rect(0, 1380, 1920, 1980),
399 ];
400 let boundary = Boundary::build(
401 &layout,
402 &[GuardRects {
403 target: layout[1],
404 source: layout[2],
405 }],
406 )
407 .expect("valid registration");
408 assert!(!clip_is_disjoint_from_forbidden(&boundary));
409 let clip = boundary.clip_rect();
410 assert_eq!(
411 (clip.left, clip.top, clip.right, clip.bottom),
412 (0, 0, 1920, 1980)
413 );
414 }
415
416 #[test]
417 fn movement_correction_preserves_tangent_on_previous_monitor() {
418 let layout = [
419 rect(0, 0, 1920, 1080),
420 rect(640, 1080, 1280, 1380),
421 rect(0, 1380, 1920, 1980),
422 ];
423 let boundary = Boundary::build(
424 &layout,
425 &[GuardRects {
426 target: layout[1],
427 source: layout[2],
428 }],
429 )
430 .expect("valid registration");
431 // Exiting through the top edge of 3 keeps the x coordinate.
432 assert_eq!(
433 boundary.correct_movement(point(700, 1200), Some(point(700, 1500))),
434 point(700, 1380)
435 );
436 // Exiting through the bottom edge of 1 keeps the x coordinate.
437 assert_eq!(
438 boundary.correct_movement(point(700, 1200), Some(point(700, 500))),
439 point(700, 1079)
440 );
441 // A diagonal entry clamps to the touched corner of the previous monitor.
442 assert_eq!(
443 boundary.correct_movement(point(641, 1081), Some(point(641, 1079))),
444 point(641, 1079)
445 );
446 }
447
448 #[test]
449 fn correction_falls_back_to_nearest_allowed_monitor() {
450 let layout = [
451 rect(0, 0, 1920, 1080),
452 rect(640, 1080, 1280, 1380),
453 rect(0, 1380, 1920, 1980),
454 ];
455 let boundary = Boundary::build(
456 &layout,
457 &[GuardRects {
458 target: layout[1],
459 source: layout[2],
460 }],
461 )
462 .expect("valid registration");
463 // Without a usable previous monitor the nearest allowed monitor wins.
464 assert_eq!(
465 boundary.correct_movement(point(700, 1200), Some(point(700, 1250))),
466 point(700, 1079)
467 );
468 assert_eq!(
469 boundary.correct_movement(point(700, 1200), None),
470 point(700, 1079)
471 );
472 }
473
474 #[test]
475 fn negative_and_unequal_layouts_use_half_open_edges() {
476 // Unequal sizes and negative coordinates; the forbidden target sits
477 // between the two allowed monitors, so hook correction applies.
478 let layout = [
479 rect(-2560, -1440, 0, -480),
480 rect(-1600, -480, -320, 0),
481 rect(-2560, 0, 0, 720),
482 ];
483 let boundary = Boundary::build(
484 &layout,
485 &[GuardRects {
486 target: layout[1],
487 source: layout[0],
488 }],
489 )
490 .expect("valid registration");
491 assert!(!clip_is_disjoint_from_forbidden(&boundary));
492 // Tangent preservation with negative coordinates.
493 assert_eq!(
494 boundary.correct_movement(point(-1000, -200), Some(point(-1000, -1000))),
495 point(-1000, -481)
496 );
497 // The monitor below owns the exit and clamps to its first row.
498 assert_eq!(
499 boundary.correct_movement(point(-1000, -200), Some(point(-1000, 100))),
500 point(-1000, 0)
501 );
502 // Half-open clamp: the last pixel of the previous monitor is kept.
503 assert_eq!(
504 boundary.correct_movement(point(-1599, -479), Some(point(-1000, -1000))),
505 point(-1599, -481)
506 );
507 // Nearest allowed monitor fallback still lands on an edge pixel.
508 assert_eq!(
509 boundary.warp_to_allowed(point(-1000, -200)),
510 point(-1000, 0)
511 );
512 }
513
514 #[test]
515 fn clip_only_layout_with_negative_target_outside_bbox() {
516 // The forbidden target lies completely right of the allowed bounding
517 // box: the single clip rectangle is a complete boundary.
518 let layout = [
519 rect(-2560, -1440, 0, 0),
520 rect(0, 0, 1920, 1080),
521 rect(-640, 0, 0, 720),
522 ];
523 let boundary = Boundary::build(
524 &layout,
525 &[GuardRects {
526 target: layout[1],
527 source: layout[0],
528 }],
529 )
530 .expect("valid registration");
531 assert!(clip_is_disjoint_from_forbidden(&boundary));
532 let clip = boundary.clip_rect();
533 assert_eq!(
534 (clip.left, clip.top, clip.right, clip.bottom),
535 (-2560, -1440, 0, 720)
536 );
537 }
538
539 #[test]
540 fn edge_pixels_are_half_open_and_preserved() {
541 let layout = screenshot_layout();
542 let boundary = Boundary::build(
543 &layout,
544 &[GuardRects {
545 target: layout[1],
546 source: layout[0],
547 }],
548 )
549 .expect("valid registration");
550 // Half-open: the corner pixel (0, 0) is allowed, (-1, -1) is forbidden.
551 assert!(!boundary.is_forbidden(point(0, 0)));
552 assert!(!boundary.is_forbidden(point(0, 2159)));
553 assert!(boundary.is_forbidden(point(-1, -1)));
554 // The last allowed pixel is preserved and never warped.
555 assert_eq!(
556 boundary.warp_to_allowed(point(1919, 2159)),
557 point(1919, 2159)
558 );
559 // A forbidden attempt clamps to the adjacent edge pixel of the previous
560 // allowed monitor; unknown previous state falls back to the nearest one.
561 assert_eq!(
562 boundary.correct_movement(point(-5, -5), Some(point(5, 5))),
563 point(0, 0)
564 );
565 assert_eq!(
566 boundary.correct_movement(point(-5, -5), Some(point(5, 1500))),
567 point(0, 1080)
568 );
569 }
570
571 #[test]
572 fn unmapped_displays_remain_usable_and_never_forbidden() {
573 let mut layout = screenshot_layout().to_vec();
574 layout.push(rect(3000, 0, 4920, 1080));
575 let boundary = Boundary::build(
576 &layout,
577 &[GuardRects {
578 target: layout[1],
579 source: layout[0],
580 }],
581 )
582 .expect("valid registration");
583 assert!(!boundary.is_forbidden(point(3500, 500)));
584 let clip = boundary.clip_rect();
585 assert!((clip.left, clip.top, clip.right, clip.bottom) == (0, 0, 4920, 2160));
586 }
587
588 #[test]
589 fn warp_keeps_already_allowed_points_untouched() {
590 let layout = screenshot_layout();
591 let boundary = Boundary::build(
592 &layout,
593 &[GuardRects {
594 target: layout[1],
595 source: layout[0],
596 }],
597 )
598 .expect("valid registration");
599 assert_eq!(boundary.warp_to_allowed(point(5, 5)), point(5, 5));
600 assert_eq!(
601 boundary.warp_to_allowed(point(1919, 1079)),
602 point(1919, 1079)
603 );
604 }
605
606 #[test]
607 fn invalid_registrations_fail_open() {
608 let layout = screenshot_layout();
609 let guards = [GuardRects {
610 target: layout[1],
611 source: layout[0],
612 }];
613 // Empty layout or guard list.
614 assert!(Boundary::build(&[], &guards).is_none());
615 assert!(Boundary::build(&layout, &[]).is_none());
616 // Empty rectangle.
617 assert!(
618 Boundary::build(
619 &layout,
620 &[GuardRects {
621 target: rect(0, 0, 0, 0),
622 source: layout[0],
623 }],
624 )
625 .is_none()
626 );
627 // Registered rectangle missing from the layout (stale topology).
628 assert!(
629 Boundary::build(
630 &layout,
631 &[GuardRects {
632 target: rect(5, 5, 10, 10),
633 source: layout[0],
634 }],
635 )
636 .is_none()
637 );
638 // Overlapping registrations (target equals its own source).
639 assert!(
640 Boundary::build(
641 &layout,
642 &[GuardRects {
643 target: layout[0],
644 source: layout[0],
645 }],
646 )
647 .is_none()
648 );
649 // Two guards sharing one target display.
650 assert!(Boundary::build(&layout, &[guards[0], guards[0]]).is_none());
651 }
652
653 #[test]
654 fn registry_guard_add_remove_updates_boundary() {
655 let layout = screenshot_layout();
656 let mut registry = BoundaryRegistry::new();
657 assert!(registry.snapshot().is_none());
658 registry.set_layout(layout.to_vec());
659 assert!(registry.snapshot().is_none(), "no guards, no boundary");
660 registry.register(
661 11,
662 GuardRects {
663 target: layout[1],
664 source: layout[0],
665 },
666 );
667 let boundary = registry.snapshot().expect("one guard");
668 assert!(clip_is_disjoint_from_forbidden(&boundary));
669 // A second guard on the same target display is invalid: fail open.
670 registry.register(
671 22,
672 GuardRects {
673 target: layout[1],
674 source: layout[2],
675 },
676 );
677 assert!(registry.snapshot().is_none());
678 registry.unregister(22);
679 assert!(registry.snapshot().is_some());
680 registry.unregister(11);
681 assert!(
682 registry.snapshot().is_none(),
683 "empty guards restore baseline"
684 );
685 }
686
687 #[test]
688 fn registry_invalidates_until_layout_is_refreshed() {
689 let layout = screenshot_layout();
690 let mut registry = BoundaryRegistry::new();
691 registry.set_layout(layout.to_vec());
692 registry.register(
693 11,
694 GuardRects {
695 target: layout[1],
696 source: layout[0],
697 },
698 );
699 assert!(registry.snapshot().is_some());
700 registry.invalidate();
701 assert!(
702 registry.snapshot().is_none(),
703 "display change releases boundary"
704 );
705 // Re-registration without a fresh layout must not revive stale geometry.
706 registry.unregister(11);
707 registry.register(
708 12,
709 GuardRects {
710 target: layout[1],
711 source: layout[0],
712 },
713 );
714 assert!(registry.snapshot().is_none());
715 registry.set_layout(layout.to_vec());
716 assert!(
717 registry.snapshot().is_some(),
718 "refresh restores validated geometry"
719 );
720 }
721
722 #[test]
723 fn registry_pause_balances_across_mappers() {
724 let layout = screenshot_layout();
725 let mut registry = BoundaryRegistry::new();
726 registry.set_layout(layout.to_vec());
727 registry.register(
728 11,
729 GuardRects {
730 target: layout[1],
731 source: layout[0],
732 },
733 );
734 assert!(!registry.has_pause());
735 registry.pause();
736 registry.pause();
737 assert!(registry.has_pause());
738 registry.resume();
739 assert!(registry.has_pause(), "one owner still holds the pause");
740 registry.resume();
741 assert!(!registry.has_pause());
742 registry.resume();
743 assert!(
744 !registry.has_pause(),
745 "unbalanced resume must not underflow"
746 );
747 }
748
749 #[test]
750 fn clip_action_transitions_preserve_baseline_across_swaps() {
751 let source = rect(0, 0, 1920, 1080);
752 let clip = rect(-10, -10, 2000, 2000);
753 // First own clip preserves the external baseline.
754 assert_eq!(
755 clip_action(Some(source), false, Some(clip), false),
756 ClipAction::SaveBaselineThenSet(source)
757 );
758 // Capture -> boundary swap keeps the baseline untouched.
759 assert_eq!(
760 clip_action(None, false, Some(clip), true),
761 ClipAction::Set(clip)
762 );
763 assert_eq!(
764 clip_action(Some(source), false, Some(clip), true),
765 ClipAction::Set(source)
766 );
767 }
768
769 #[test]
770 fn clip_action_screenshot_pause_releases_only_the_native_boundary() {
771 let source = rect(0, 0, 1920, 1080);
772 let clip = rect(-10, -10, 2000, 2000);
773 // Pause releases the native boundary back to the baseline.
774 assert_eq!(
775 clip_action(None, true, Some(clip), true),
776 ClipAction::RestoreBaseline
777 );
778 // A captured source clip survives the pause.
779 assert_eq!(
780 clip_action(Some(source), true, Some(clip), true),
781 ClipAction::Set(source)
782 );
783 }
784
785 #[test]
786 fn clip_action_balances_down_to_the_baseline() {
787 let source = rect(0, 0, 1920, 1080);
788 let clip = rect(-10, -10, 2000, 2000);
789 // Empty guards restore the baseline exactly once.
790 assert_eq!(
791 clip_action(None, false, None, true),
792 ClipAction::RestoreBaseline
793 );
794 assert_eq!(clip_action(None, false, None, false), ClipAction::None);
795 // Full lifecycle: boundary -> capture -> boundary -> stop.
796 assert_eq!(
797 clip_action(None, false, Some(clip), false),
798 ClipAction::SaveBaselineThenSet(clip)
799 );
800 assert_eq!(
801 clip_action(Some(source), false, Some(clip), true),
802 ClipAction::Set(source)
803 );
804 assert_eq!(
805 clip_action(None, false, Some(clip), true),
806 ClipAction::Set(clip)
807 );
808 assert_eq!(
809 clip_action(None, false, None, true),
810 ClipAction::RestoreBaseline
811 );
812 }
813}
814