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/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 or4
//! `ClipCursor`): it only classifies screen points against the logical monitor5
//! layout and computes correction targets, so every rule is unit-testable.6
7
use std::sync::Arc;8
9
use windows::Win32::Foundation::{POINT, RECT};10
11
/// Physical target and virtual source rectangles of one active input guard.12
#[derive(Clone, Copy, Debug)]13
pub(super) struct GuardRects {14
pub(super) target: RECT,15
pub(super) source: RECT,16
}17
18
impl 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.25
pub(super) struct Boundary {26
allowed: Vec<RECT>,27
forbidden: Vec<RECT>,28
guards: Vec<GuardRects>,29
clip: RECT,30
}31
32
impl Boundary {33
/// Build boundary geometry from the logical monitor layout and the target34
/// and source rectangles registered by the active input guards.35
///36
/// Returns `None` (fail open: no boundary at all) when the registration is37
/// invalid or stale: empty layout or guards, empty rectangles, registered38
/// 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> = layout63
.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 allowed80
/// monitors. It may contain harmless gaps (the cursor cannot rest in81
/// monitor-less space). When it covers no forbidden target rectangle it is82
/// a complete hard boundary; otherwise the low-level hook corrects the83
/// remaining forbidden regions (and covers the loss of the global clip).84
pub(super) fn clip_rect(&self) -> RECT {85
self.clip86
}87
88
/// The logical workspace with each virtual source projected onto its89
/// physical output. Hidden source desktops never become screenshot targets.90
pub(super) fn screenshot_screens(&self) -> Vec<super::navigation::Screen> {91
self.allowed92
.iter()93
.map(|logical| super::navigation::Screen {94
logical: *logical,95
physical: self96
.guards97
.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.forbidden106
.iter()107
.any(|rect| contains_point(*rect, point))108
}109
110
/// Correct a forbidden movement attempt: clamp the attempted coordinates to111
/// the previous allowed monitor (preserving the tangent along its edge) and112
/// fall back to the nearest allowed monitor when the previous one is113
/// 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.allowed117
.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, pause127
/// 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 = self133
.allowed134
.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)]145
pub(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
156
impl 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 native169
/// boundary; the native boundary applies when no capture and no pause are170
/// active. `boundary` is the boundary clip rectangle while the boundary is171
/// live. `has_baseline` tracks whether the external baseline was preserved.172
pub(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).188
pub(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
196
impl 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; the225
/// 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 > 0242
}243
244
/// Current boundary geometry, ignoring the screenshot pause (callers fold245
/// `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
None256
};257
}258
}259
260
fn nonempty(rect: RECT) -> bool {261
rect.right > rect.left && rect.bottom > rect.top262
}263
264
fn rect_eq(left: RECT, right: RECT) -> bool {265
left.left == right.left266
&& left.top == right.top267
&& left.right == right.right268
&& left.bottom == right.bottom269
}270
271
/// Nonzero-area intersection (rectangles are half-open).272
fn overlaps(left: RECT, right: RECT) -> bool {273
left.left < right.right274
&& right.left < left.right275
&& left.top < right.bottom276
&& right.top < left.bottom277
}278
279
/// Half-open containment: `[left, right) x [top, bottom)`.280
fn contains_point(rect: RECT, point: POINT) -> bool {281
point.x >= rect.left && point.x < rect.right && point.y >= rect.top && point.y < rect.bottom282
}283
284
fn 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
291
fn 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_rect302
}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 squared306
/// distance accumulates in `u128`, so extreme coordinates cannot overflow.307
fn 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 - x316
} else if x > right_edge {317
x - right_edge318
} else {319
0320
};321
let dy = if y < top {322
top - y323
} else if y > bottom_edge {324
y - bottom_edge325
} else {326
0327
};328
let dx = dx as u128;329
let dy = dy as u128;330
dx * dx + dy * dy331
}332
333
#[cfg(test)]334
mod 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
!boundary355
.forbidden356
.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 mapped361
/// 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 sits477
// 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 bounding517
// 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 previous560
// 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::RestoreBaseline777
);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::RestoreBaseline793
);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::RestoreBaseline811
);812
}813
}814