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/screenshot.rs25.1 KB
1use std::cell::Cell;
2use std::mem::size_of;
3use std::time::{Duration, Instant};
4
5use windows::Win32::Foundation::{CloseHandle, HANDLE, HWND, LPARAM, POINT, RECT};
6use windows::Win32::Graphics::Dwm::{DWMWA_CLOAKED, DwmGetWindowAttribute};
7use windows::Win32::System::StationsAndDesktops::{
8 CloseDesktop, DESKTOP_CONTROL_FLAGS, DESKTOP_READOBJECTS, GetThreadDesktop,
9 GetUserObjectInformationW, HDESK, OpenInputDesktop, UOI_NAME,
10};
11use windows::Win32::System::Threading::{
12 GetCurrentThreadId, OpenProcess, PROCESS_NAME_WIN32, PROCESS_QUERY_LIMITED_INFORMATION,
13 QueryFullProcessImageNameW,
14};
15use windows::Win32::UI::WindowsAndMessaging::{
16 EnumChildWindows, EnumWindows, GetClassNameW, GetWindowThreadProcessId, IsWindow,
17 IsWindowVisible,
18};
19use windows::core::{BOOL, PWSTR};
20
21const START_TIMEOUT: Duration = Duration::from_secs(5);
22const CLOSED_SETTLE: Duration = Duration::from_millis(100);
23
24/// Enlarge an owned clip just enough for a cursor transfer. Both the current
25/// cursor (inside `clip`) and destination stay legal until placement completes.
26pub(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 a
36/// fixed quiet period. Expiration prevents the 32-bit message clock from being
37/// mistaken for an old event after a long idle or a clock wrap.
38pub(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) <= 0
43}
44
45#[derive(Clone, Copy, Debug, Eq, PartialEq)]
46pub(super) enum Progress {
47 Waiting,
48 Finished,
49 TimedOut,
50}
51
52pub(super) struct Session {
53 started: Instant,
54 observed: bool,
55 absent_since: Option<Instant>,
56 completed: Option<Progress>,
57}
58
59impl 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.observed
89 && overlay_visible == Some(false)
90 && now.saturating_duration_since(self.started) >= START_TIMEOUT
91 {
92 self.completed = Some(Progress::TimedOut);
93 return Progress::TimedOut;
94 }
95 Progress::Waiting
96 }
97}
98
99/// Pure per-scan state for one `EnumWindows` traversal. `observe` returns
100/// `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)]
104struct OverlayScan {
105 visible: bool,
106 uncertain: bool,
107 found: Option<isize>,
108}
109
110impl OverlayScan {
111 /// Feed one classified window. `false` means "stop the enumeration"; an
112 /// 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 false
119 }
120 Some(false) => true,
121 None => {
122 self.uncertain = true;
123 true
124 }
125 }
126 }
127
128 /// `enumeration_failed` is `EnumWindows`'s own error return. It is only
129 /// consulted when no overlay was found, so the FALSE produced by the early
130 /// stop (the callback declining further windows after a find) is never
131 /// 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 None
137 } else {
138 Some(false)
139 }
140 }
141}
142
143/// Thread-local cache for the last confirmed overlay handle. The handle is a
144/// plain `isize` key so the policy is pure and unit-testable; the caller always
145/// re-validates it through the full `window_is_overlay` identity check
146/// (class/process/visibility), so a closed, destroyed or reused handle falls
147/// back to the full search instead of being trusted.
148#[derive(Clone, Copy)]
149struct OverlayCache {
150 candidate: Option<isize>,
151}
152
153impl 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 and
170 // 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 visible
177 }
178}
179
180thread_local! {
181 static OVERLAY_CACHE: Cell<OverlayCache> = const { Cell::new(OverlayCache::new()) };
182}
183
184pub(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 can
193 // 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 visible
201 })
202}
203
204/// Full enumeration for cache misses: stops at the first confirmed overlay.
205fn 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
216fn hwnd_from_key(handle: isize) -> HWND {
217 HWND(handle as *mut _)
218}
219
220fn 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
233fn 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
249unsafe 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
267unsafe 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 is
271 // confirmed. The found handle is recorded before returning, so the early
272 // stop's FALSE can never be mistaken for an enumeration failure.
273 BOOL(if scan.observe(window.0 as isize, overlay) {
274 1
275 } else {
276 0
277 })
278}
279
280fn 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 None
289 } 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
322fn 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
341fn 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)]
349enum OverlayKind {
350 SnippingToolRoot,
351 ScreenClippingHost,
352}
353
354impl 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
366fn 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)]
382mod 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::Waiting
461 );
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::Waiting
472 );
473 assert_eq!(
474 session.poll(now + Duration::from_millis(149), Some(false)),
475 Progress::Waiting
476 );
477 assert_eq!(
478 session.poll(now + Duration::from_millis(150), Some(false)),
479 Progress::Finished
480 );
481 assert_eq!(
482 session.poll(now + Duration::from_millis(200), Some(false)),
483 Progress::Finished
484 );
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::Waiting
497 );
498 assert_eq!(
499 session.poll(now + Duration::from_millis(150), Some(false)),
500 Progress::Waiting
501 );
502 assert_eq!(
503 session.poll(now + Duration::from_millis(250), Some(false)),
504 Progress::Finished
505 );
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::Waiting
516 );
517 assert_eq!(
518 session.poll(now + Duration::from_secs(5), None),
519 Progress::Waiting
520 );
521 assert_eq!(
522 session.poll(now + Duration::from_secs(5), Some(false)),
523 Progress::TimedOut
524 );
525 assert_eq!(
526 session.poll(now + Duration::from_secs(6), Some(false)),
527 Progress::TimedOut
528 );
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::Waiting
533 );
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 full
616 // 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 full
646 // class/process/visibility re-validation rejects it, so the outcome is
647 // 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 a
668 // 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 None
678 );
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 the
698 // 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