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/screenshot.rs25.1 KB
1
use std::cell::Cell;2
use std::mem::size_of;3
use std::time::{Duration, Instant};4
5
use windows::Win32::Foundation::{CloseHandle, HANDLE, HWND, LPARAM, POINT, RECT};6
use windows::Win32::Graphics::Dwm::{DWMWA_CLOAKED, DwmGetWindowAttribute};7
use windows::Win32::System::StationsAndDesktops::{8
CloseDesktop, DESKTOP_CONTROL_FLAGS, DESKTOP_READOBJECTS, GetThreadDesktop,9
GetUserObjectInformationW, HDESK, OpenInputDesktop, UOI_NAME,10
};11
use windows::Win32::System::Threading::{12
GetCurrentThreadId, OpenProcess, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION,13
QueryFullProcessImageNameW,14
};15
use windows::Win32::UI::WindowsAndMessaging::{16
EnumChildWindows, EnumWindows, GetClassNameW, GetWindowThreadProcessId, IsWindow,17
IsWindowVisible,18
};19
use windows::core::{BOOL, PWSTR};20
21
const START_TIMEOUT: Duration = Duration::from_secs(5);22
const CLOSED_SETTLE: Duration = Duration::from_millis(100);23
24
/// Enlarge an owned clip just enough for a cursor transfer. Both the current25
/// cursor (inside `clip`) and destination stay legal until placement completes.26
pub(super) fn return_clip(clip: RECT, destination: POINT) -> Option<RECT> {27
Some(RECT {28
left: clip.left.min(destination.x),29
top: clip.top.min(destination.y),30
right: clip.right.max(destination.x.checked_add(1)?),31
bottom: clip.bottom.max(destination.y.checked_add(1)?),32
})33
}34
35
/// Discard only events from at/before the transfer, not new motion during a36
/// fixed quiet period. Expiration prevents the 32-bit message clock from being37
/// mistaken for an old event after a long idle or a clock wrap.38
pub(super) fn stale_return_motion(time: u32, encoded_tick: u64, now: u64) -> bool {39
let Some(tick) = encoded_tick.checked_sub(1) else {40
return false;41
};42
now.saturating_sub(tick) <= 1_000 && (time.wrapping_sub(tick as u32) as i32) <= 043
}44
45
#[derive(Clone, Copy, Debug, Eq, PartialEq)]46
pub(super) enum Progress {47
Waiting,48
Finished,49
TimedOut,50
}51
52
pub(super) struct Session {53
started: Instant,54
observed: bool,55
absent_since: Option<Instant>,56
completed: Option<Progress>,57
}58
59
impl Session {60
pub(super) fn new(now: Instant) -> Self {61
Self {62
started: now,63
observed: false,64
absent_since: None,65
completed: None,66
}67
}68
69
pub(super) fn poll(&mut self, now: Instant, overlay_visible: Option<bool>) -> Progress {70
if let Some(completed) = self.completed {71
return completed;72
}73
match overlay_visible {74
Some(true) => {75
self.observed = true;76
self.absent_since = None;77
}78
Some(false) if self.observed => {79
let absent_since = self.absent_since.get_or_insert(now);80
if now.saturating_duration_since(*absent_since) >= CLOSED_SETTLE {81
self.completed = Some(Progress::Finished);82
return Progress::Finished;83
}84
}85
// A failed query cannot contribute to a continuous absence interval.86
_ => self.absent_since = None,87
}88
if !self.observed89
&& overlay_visible == Some(false)90
&& now.saturating_duration_since(self.started) >= START_TIMEOUT91
{92
self.completed = Some(Progress::TimedOut);93
return Progress::TimedOut;94
}95
Progress::Waiting96
}97
}98
99
/// Pure per-scan state for one `EnumWindows` traversal. `observe` returns100
/// `false` to stop the enumeration as soon as a real overlay is confirmed;101
/// `outcome` folds the traversal result into the established `Option<bool>`102
/// semantics where only a fully clean scan may confirm absence.103
#[derive(Default)]104
struct OverlayScan {105
visible: bool,106
uncertain: bool,107
found: Option<isize>,108
}109
110
impl OverlayScan {111
/// Feed one classified window. `false` means "stop the enumeration"; an112
/// uncertain classification keeps scanning but never confirms absence.113
fn observe(&mut self, handle: isize, overlay: Option<bool>) -> bool {114
match overlay {115
Some(true) => {116
self.visible = true;117
self.found = Some(handle);118
false119
}120
Some(false) => true,121
None => {122
self.uncertain = true;123
true124
}125
}126
}127
128
/// `enumeration_failed` is `EnumWindows`'s own error return. It is only129
/// consulted when no overlay was found, so the FALSE produced by the early130
/// stop (the callback declining further windows after a find) is never131
/// mistaken for a failed enumeration.132
fn outcome(&self, enumeration_failed: bool) -> Option<bool> {133
if self.visible {134
Some(true)135
} else if enumeration_failed || self.uncertain {136
None137
} else {138
Some(false)139
}140
}141
}142
143
/// Thread-local cache for the last confirmed overlay handle. The handle is a144
/// plain `isize` key so the policy is pure and unit-testable; the caller always145
/// re-validates it through the full `window_is_overlay` identity check146
/// (class/process/visibility), so a closed, destroyed or reused handle falls147
/// back to the full search instead of being trusted.148
#[derive(Clone, Copy)]149
struct OverlayCache {150
candidate: Option<isize>,151
}152
153
impl OverlayCache {154
const fn new() -> Self {155
Self { candidate: None }156
}157
158
fn visibility(159
&mut self,160
revalidate: impl FnOnce(isize) -> Option<bool>,161
search: impl FnOnce() -> (Option<bool>, Option<isize>),162
) -> Option<bool> {163
if let Some(candidate) = self.candidate {164
match revalidate(candidate) {165
// Still the live overlay: skip the enumeration entirely.166
Some(true) => return Some(true),167
// Closed, hidden or reused by another window: search again.168
Some(false) => self.candidate = None,169
// A failed re-validation proves nothing. Keep the candidate and170
// the uncertainty: never report a confirmed close.171
None => return None,172
}173
}174
let (visible, found) = search();175
self.candidate = if visible == Some(true) { found } else { None };176
visible177
}178
}179
180
thread_local! {181
static OVERLAY_CACHE: Cell<OverlayCache> = const { Cell::new(OverlayCache::new()) };182
}183
184
pub(super) fn overlay_visible() -> Option<bool> {185
// While a session is armed, any input-desktop switch must defer restoration.186
// The renderer cannot safely move or recapture the cursor on another desktop.187
// The desktop check runs first on every poll, before the cache.188
if !current_desktop_is_input()? {189
return Some(true);190
}191
OVERLAY_CACHE.with(|cache| {192
// Keep no RefCell borrow alive across Win32 calls: input callbacks can193
// re-enter this thread while a window is being inspected.194
let mut candidate = cache.get();195
let visible = candidate.visibility(196
|handle| window_is_overlay(hwnd_from_key(handle)),197
search_overlay,198
);199
cache.set(candidate);200
visible201
})202
}203
204
/// Full enumeration for cache misses: stops at the first confirmed overlay.205
fn search_overlay() -> (Option<bool>, Option<isize>) {206
let mut scan = OverlayScan::default();207
let result = unsafe {208
EnumWindows(209
Some(enumerate_window),210
LPARAM((&mut scan as *mut OverlayScan) as isize),211
)212
};213
(scan.outcome(result.is_err()), scan.found)214
}215
216
fn hwnd_from_key(handle: isize) -> HWND {217
HWND(handle as *mut _)218
}219
220
fn current_desktop_is_input() -> Option<bool> {221
// GetThreadDesktop returns a borrowed handle; only close OpenInputDesktop's handle.222
let current = unsafe { GetThreadDesktop(GetCurrentThreadId()) }.ok()?;223
let current_name = desktop_name(current)?;224
let input =225
unsafe { OpenInputDesktop(DESKTOP_CONTROL_FLAGS(0), false, DESKTOP_READOBJECTS) }.ok()?;226
let input_name = desktop_name(input);227
unsafe {228
let _ = CloseDesktop(input);229
}230
Some(current_name == input_name?)231
}232
233
fn desktop_name(desktop: HDESK) -> Option<Vec<u16>> {234
let mut name = [0u16; 256];235
unsafe {236
GetUserObjectInformationW(237
HANDLE(desktop.0),238
UOI_NAME,239
Some(name.as_mut_ptr().cast()),240
size_of::<[u16; 256]>() as u32,241
None,242
)243
}244
.ok()?;245
let length = name.iter().position(|value| *value == 0)?;246
(length != 0).then(|| name[..length].to_vec())247
}248
249
unsafe extern "system" fn enumerate_window(window: HWND, parameter: LPARAM) -> BOOL {250
// A confirmed find below returns BOOL(0) and stops this top-level scan too.251
if unsafe { enumerate_overlay(window, parameter) }.0 == 0 {252
return BOOL(0);253
}254
if unsafe { IsWindowVisible(window) }.as_bool() {255
// The precise overlay root can be hosted beneath a framework window.256
// EnumChildWindows already traverses descendants; do not recurse in its callback.257
// Its FALSE return after a find is the stop signal, not an error.258
unsafe {259
let _ = EnumChildWindows(Some(window), Some(enumerate_overlay), parameter);260
}261
}262
// Propagate a find from the child scan to the outer EnumWindows loop.263
let found = unsafe { &*(parameter.0 as *const OverlayScan) }.visible;264
BOOL(if found { 0 } else { 1 })265
}266
267
unsafe extern "system" fn enumerate_overlay(window: HWND, parameter: LPARAM) -> BOOL {268
let overlay = window_is_overlay(window);269
let scan = unsafe { &mut *(parameter.0 as *mut OverlayScan) };270
// BOOL(0) stops EnumChildWindows/EnumWindows as soon as a real overlay is271
// confirmed. The found handle is recorded before returning, so the early272
// stop's FALSE can never be mistaken for an enumeration failure.273
BOOL(if scan.observe(window.0 as isize, overlay) {274
1275
} else {276
0277
})278
}279
280
fn window_is_overlay(window: HWND) -> Option<bool> {281
if !unsafe { IsWindowVisible(window) }.as_bool() {282
return Some(false);283
}284
let mut class = [0u16; 256];285
let length = unsafe { GetClassNameW(window, &mut class) };286
if length == 0 {287
return if unsafe { IsWindow(Some(window)) }.as_bool() {288
None289
} else {290
Some(false)291
};292
}293
let class = String::from_utf16_lossy(&class[..length as usize]);294
if !is_overlay_class(&class) {295
return Some(false);296
}297
let mut pid = 0;298
if unsafe { GetWindowThreadProcessId(window, Some(&mut pid)) } == 0 {299
return None;300
}301
let image = process_image(pid)?;302
let Some(kind) = overlay_kind(&class, &image) else {303
return Some(false);304
};305
// Snipping Tool's root owns the visible Xaml overlays while remaining cloaked.306
// Its precise class and process identity distinguish it from the image editor.307
if kind == OverlayKind::SnippingToolRoot {308
return kind.visibility(true, None);309
}310
let mut cloaked = 0u32;311
let query = unsafe {312
DwmGetWindowAttribute(313
window,314
DWMWA_CLOAKED,315
(&mut cloaked as *mut u32).cast(),316
size_of::<u32>() as u32,317
)318
};319
kind.visibility(true, query.ok().map(|()| cloaked))320
}321
322
fn process_image(pid: u32) -> Option<String> {323
let process = unsafe { OpenProcess(PROCESS_QUERY_LIMITED_INFORMATION, false, pid) }.ok()?;324
let mut image = vec![0u16; 32_768];325
let mut length = image.len() as u32;326
let result = unsafe {327
QueryFullProcessImageNameW(328
process,329
PROCESS_NAME_WIN32,330
PWSTR(image.as_mut_ptr()),331
&mut length,332
)333
};334
unsafe {335
let _ = CloseHandle(process);336
}337
result.ok()?;338
Some(String::from_utf16_lossy(&image[..length as usize]))339
}340
341
fn is_overlay_class(class: &str) -> bool {342
matches!(343
class,344
"SnipOverlayRootWindow" | "Windows.UI.Core.CoreWindow"345
)346
}347
348
#[derive(Clone, Copy, Debug, Eq, PartialEq)]349
enum OverlayKind {350
SnippingToolRoot,351
ScreenClippingHost,352
}353
354
impl OverlayKind {355
fn visibility(self, visible: bool, cloaked: Option<u32>) -> Option<bool> {356
if !visible {357
Some(false)358
} else if self == Self::SnippingToolRoot {359
Some(true)360
} else {361
cloaked.map(|cloaked| cloaked == 0)362
}363
}364
}365
366
fn overlay_kind(class: &str, image: &str) -> Option<OverlayKind> {367
let executable = image.rsplit(['\\', '/']).next().unwrap_or(image);368
match class {369
"SnipOverlayRootWindow" if executable.eq_ignore_ascii_case("SnippingTool.exe") => {370
Some(OverlayKind::SnippingToolRoot)371
}372
"Windows.UI.Core.CoreWindow"373
if executable.eq_ignore_ascii_case("ScreenClippingHost.exe") =>374
{375
Some(OverlayKind::ScreenClippingHost)376
}377
_ => None,378
}379
}380
381
#[cfg(test)]382
mod tests {383
use super::{384
OverlayCache, OverlayKind, OverlayScan, Progress, Session, overlay_kind, return_clip,385
stale_return_motion,386
};387
use std::cell::Cell;388
use std::time::{Duration, Instant};389
use windows::Win32::Foundation::{POINT, RECT};390
391
#[test]392
fn return_keeps_both_positions_legal_until_final_placement() {393
let physical = RECT {394
left: -200,395
top: -100,396
right: 0,397
bottom: 0,398
};399
let logical = RECT {400
left: 0,401
top: 0,402
right: 400,403
bottom: 400,404
};405
let start = POINT { x: -100, y: -50 };406
let destination = POINT { x: 200, y: 300 };407
let clamp = |rect: RECT, p: POINT| POINT {408
x: p.x.clamp(rect.left, rect.right - 1),409
y: p.y.clamp(rect.top, rect.bottom - 1),410
};411
// The old order creates a visible, unrelated intermediate edge point.412
assert_eq!(clamp(logical, start), POINT { x: 0, y: 0 });413
let bridge = return_clip(physical, destination).unwrap();414
assert_eq!(clamp(bridge, start), start);415
assert_eq!(clamp(bridge, destination), destination);416
assert_eq!(clamp(logical, destination), destination);417
// An ordinary display needs no repositioning or unnecessary expansion.418
assert_eq!(return_clip(logical, destination), Some(logical));419
// Half-open bounds must include a destination exactly at the old edge.420
let edge = POINT {421
x: physical.right,422
y: physical.bottom,423
};424
assert_eq!(clamp(return_clip(physical, edge).unwrap(), edge), edge);425
assert!(return_clip(physical, POINT { x: i32::MAX, y: 0 }).is_none());426
}427
428
#[test]429
fn return_fence_rejects_delayed_motion_but_never_waits_for_a_quiet_mouse() {430
let encoded_tick = 1_001;431
for time in [950, 999, 1_000] {432
assert!(stale_return_motion(time, encoded_tick, 1_010));433
}434
// Keep accepting fresh motion even while an older raw packet is late.435
assert!(!stale_return_motion(1_001, encoded_tick, 1_010));436
assert!(stale_return_motion(999, encoded_tick, 1_011));437
assert!(!stale_return_motion(1_010, encoded_tick, 1_011));438
assert!(!stale_return_motion(999, 0, 1_010));439
assert!(!stale_return_motion(999, encoded_tick, 2_001));440
}441
442
#[test]443
fn return_fence_handles_zero_and_32_bit_message_clock_wrap() {444
assert!(stale_return_motion(0, 1, 0));445
assert!(!stale_return_motion(1, 1, 1));446
let tick = u32::MAX as u64 + 2;447
assert!(stale_return_motion(u32::MAX, tick + 1, tick + 1));448
assert!(stale_return_motion(1, tick + 1, tick + 1));449
assert!(!stale_return_motion(2, tick + 1, tick + 1));450
assert!(!stale_return_motion(0, tick + 1, tick + (1 << 32)));451
}452
453
#[test]454
fn a_long_selection_has_no_forced_completion() {455
let now = Instant::now();456
let mut session = Session::new(now);457
assert_eq!(session.poll(now, Some(true)), Progress::Waiting);458
assert_eq!(459
session.poll(now + Duration::from_secs(600), Some(true)),460
Progress::Waiting461
);462
}463
464
#[test]465
fn completion_or_cancel_waits_for_all_overlays_to_stay_absent() {466
let now = Instant::now();467
let mut session = Session::new(now);468
session.poll(now, Some(true));469
assert_eq!(470
session.poll(now + Duration::from_millis(50), Some(false)),471
Progress::Waiting472
);473
assert_eq!(474
session.poll(now + Duration::from_millis(149), Some(false)),475
Progress::Waiting476
);477
assert_eq!(478
session.poll(now + Duration::from_millis(150), Some(false)),479
Progress::Finished480
);481
assert_eq!(482
session.poll(now + Duration::from_millis(200), Some(false)),483
Progress::Finished484
);485
}486
487
#[test]488
fn a_short_hide_or_failed_query_restarts_the_absence_interval() {489
for interruption in [Some(true), None] {490
let now = Instant::now();491
let mut session = Session::new(now);492
session.poll(now, Some(true));493
session.poll(now + Duration::from_millis(50), Some(false));494
assert_eq!(495
session.poll(now + Duration::from_millis(100), interruption),496
Progress::Waiting497
);498
assert_eq!(499
session.poll(now + Duration::from_millis(150), Some(false)),500
Progress::Waiting501
);502
assert_eq!(503
session.poll(now + Duration::from_millis(250), Some(false)),504
Progress::Finished505
);506
}507
}508
509
#[test]510
fn startup_timeout_is_distinct_from_observed_completion() {511
let now = Instant::now();512
let mut session = Session::new(now);513
assert_eq!(514
session.poll(now + Duration::from_millis(4_999), Some(false)),515
Progress::Waiting516
);517
assert_eq!(518
session.poll(now + Duration::from_secs(5), None),519
Progress::Waiting520
);521
assert_eq!(522
session.poll(now + Duration::from_secs(5), Some(false)),523
Progress::TimedOut524
);525
assert_eq!(526
session.poll(now + Duration::from_secs(6), Some(false)),527
Progress::TimedOut528
);529
let mut visible_at_deadline = Session::new(now);530
assert_eq!(531
visible_at_deadline.poll(now + Duration::from_secs(5), Some(true)),532
Progress::Waiting533
);534
}535
536
#[test]537
fn selector_rejects_editors_and_unrelated_core_windows() {538
assert_eq!(539
overlay_kind(540
"Windows.UI.Core.CoreWindow",541
r"C:\Windows\SystemApps\ScreenClippingHost.exe"542
),543
Some(OverlayKind::ScreenClippingHost)544
);545
assert_eq!(546
overlay_kind(547
"SnipOverlayRootWindow",548
r"C:\Program Files\WindowsApps\Microsoft.ScreenSketch\SnippingTool.exe"549
),550
Some(OverlayKind::SnippingToolRoot)551
);552
for (class, image) in [553
("ApplicationFrameWindow", "SnippingTool.exe"),554
("WinUIDesktopWin32WindowClass", "SnippingTool.exe"),555
("Microsoft-Windows-SnipperEditor", "SnippingTool.exe"),556
("Windows.UI.Core.CoreWindow", "SnippingTool.exe"),557
("Windows.UI.Core.CoreWindow", "SomeOtherApp.exe"),558
("SnipOverlayRootWindow", "SomeOtherApp.exe"),559
] {560
assert_eq!(overlay_kind(class, image), None);561
}562
}563
564
#[test]565
fn modern_overlay_root_can_be_visible_and_cloaked() {566
assert_eq!(567
OverlayKind::SnippingToolRoot.visibility(true, Some(1)),568
Some(true)569
);570
assert_eq!(571
OverlayKind::SnippingToolRoot.visibility(false, Some(1)),572
Some(false)573
);574
assert_eq!(575
OverlayKind::ScreenClippingHost.visibility(true, Some(1)),576
Some(false)577
);578
assert_eq!(OverlayKind::ScreenClippingHost.visibility(true, None), None);579
}580
581
#[test]582
fn cache_hit_skips_the_full_enumeration() {583
let mut cache = OverlayCache::new();584
assert_eq!(585
cache.visibility(|_| unreachable!(), || (Some(true), Some(7))),586
Some(true)587
);588
let searched = Cell::new(false);589
assert_eq!(590
cache.visibility(591
|handle| {592
assert_eq!(handle, 7);593
Some(true)594
},595
|| {596
searched.set(true);597
(Some(false), None)598
}599
),600
Some(true)601
);602
assert!(603
!searched.get(),604
"a confirmed cache hit must not scan the desktop"605
);606
}607
608
#[test]609
fn rejected_cache_falls_back_to_search_and_replaces_the_candidate() {610
let mut cache = OverlayCache::new();611
assert_eq!(612
cache.visibility(|_| unreachable!(), || (Some(true), Some(7))),613
Some(true)614
);615
// Window 7 closed mid-poll: the re-validation rejects it and the full616
// search runs again, caching the overlay it actually found.617
assert_eq!(618
cache.visibility(|_| Some(false), || (Some(true), Some(9))),619
Some(true)620
);621
let searched = Cell::new(false);622
assert_eq!(623
cache.visibility(624
|handle| {625
assert_eq!(handle, 9, "the replacement candidate must be re-validated");626
Some(true)627
},628
|| {629
searched.set(true);630
(Some(false), None)631
}632
),633
Some(true)634
);635
assert!(!searched.get());636
}637
638
#[test]639
fn reused_handle_is_not_mistaken_for_the_overlay() {640
let mut cache = OverlayCache::new();641
assert_eq!(642
cache.visibility(|_| unreachable!(), || (Some(true), Some(7))),643
Some(true)644
);645
// Handle 7 was destroyed and reused by a foreign window: the full646
// class/process/visibility re-validation rejects it, so the outcome is647
// the search's own answer, never a false "overlay visible".648
assert_eq!(649
cache.visibility(|_| Some(false), || (Some(false), None)),650
Some(false)651
);652
// The cache is empty again: the next poll searches without a probe.653
assert_eq!(654
cache.visibility(|_| unreachable!(), || (Some(true), Some(8))),655
Some(true)656
);657
}658
659
#[test]660
fn failed_revalidation_keeps_the_candidate_and_never_confirms_closure() {661
let mut cache = OverlayCache::new();662
assert_eq!(663
cache.visibility(|_| unreachable!(), || (Some(true), Some(7))),664
Some(true)665
);666
let searched = Cell::new(false);667
// A failed identity query is uncertainty: it must not become a668
// confirmed close, and no search may overrule it behind the candidate.669
assert_eq!(670
cache.visibility(671
|_| None,672
|| {673
searched.set(true);674
(Some(false), None)675
}676
),677
None678
);679
assert!(!searched.get());680
// The candidate stays cached: the next poll re-validates the same handle.681
assert_eq!(682
cache.visibility(683
|handle| {684
assert_eq!(handle, 7);685
Some(true)686
},687
|| unreachable!()688
),689
Some(true)690
);691
}692
693
#[test]694
fn search_stops_at_the_first_confirmed_overlay() {695
let mut scan = OverlayScan::default();696
assert!(scan.observe(1, Some(false)));697
// An uncertain classification keeps scanning; it must not stop the698
// search before a real overlay is found.699
assert!(scan.observe(2, None));700
// The confirmed overlay stops the enumeration immediately.701
assert!(!scan.observe(3, Some(true)));702
assert_eq!(scan.found, Some(3));703
// Windows after the stop are never classified: the find already won.704
assert_eq!(scan.outcome(false), Some(true));705
}706
707
#[test]708
fn uncertain_results_are_never_confirmed_closed() {709
// A failed window query keeps the scan uncertain even if the rest is clean.710
let mut scan = OverlayScan::default();711
assert!(scan.observe(1, Some(false)));712
assert!(scan.observe(2, None));713
assert_eq!(scan.outcome(false), None);714
// An unexplained EnumWindows failure without a find is uncertainty too.715
let mut scan = OverlayScan::default();716
assert!(scan.observe(1, Some(false)));717
assert_eq!(scan.outcome(true), None);718
// Only a fully clean scan confirms absence.719
let mut scan = OverlayScan::default();720
assert!(scan.observe(1, Some(false)));721
assert_eq!(scan.outcome(false), Some(false));722
}723
724
#[test]725
fn an_early_stop_find_is_not_an_enumeration_failure() {726
// EnumWindows reports FALSE when the callback stops it after a find;727
// the recorded find must win over that error return.728
let mut scan = OverlayScan::default();729
assert!(!scan.observe(4, Some(true)));730
assert_eq!(scan.outcome(true), Some(true));731
assert_eq!(scan.found, Some(4));732
}733
}734