rs_teststand_sys/window/raise.rs
1//! Puts a window in front of everything else on the desktop.
2//!
3//! A message-popup step is not a fault: it is the sequence asking an operator a
4//! question. Killing the process to escape it throws away the answer and the
5//! run. What a host actually needs is for the question to be *seen*, including
6//! when an external front end, a kiosk shell, or a monitoring overlay is sitting
7//! on top of it.
8//!
9//! Windows sorts windows into two bands, ordinary and always-on-top. Moving a
10//! window into the always-on-top band puts it above every ordinary window
11//! permanently, and re-asserting the request moves it to the front of that band
12//! as well, so it also clears other always-on-top windows. A peer that
13//! re-asserts just as often would keep contending; nothing in Win32 grants a
14//! permanent win over one.
15//!
16//! Focus is a separate, weaker thing. Windows only lets a process steal the
17//! foreground under conditions it decides (roughly, that the process already
18//! owns the foreground or served the last input), so the request is made and
19//! its refusal reported rather than papered over. Z-order is the guarantee;
20//! focus is best effort.
21
22use windows::Win32::Foundation::HWND;
23use windows::Win32::UI::WindowsAndMessaging::{
24 GWL_EXSTYLE, GetWindowLongW, HWND_TOPMOST, SWP_ASYNCWINDOWPOS, SWP_NOMOVE, SWP_NOSIZE,
25 SetForegroundWindow, SetWindowPos, WS_EX_TOPMOST,
26};
27
28/// What raising a window actually achieved.
29///
30/// Reported rather than assumed, because the two halves have different
31/// strengths: the Z-order move is a guarantee, and the focus change is a
32/// request the system may refuse.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub struct Raised {
35 /// The window is in the always-on-top band, so no ordinary window covers
36 /// it. Read back from the window itself, not inferred from the request.
37 pub topmost: bool,
38 /// Windows granted the foreground, so the window also has keyboard focus.
39 /// `false` is normal and harmless for a background host: the dialog is
40 /// still fully visible and clickable, it just is not focused.
41 pub foreground: bool,
42}
43
44/// Moves `handle` to the front of the always-on-top band and asks for focus.
45///
46/// Both steps are attempted regardless of whether either succeeds, and the
47/// outcome is measured afterwards.
48pub(crate) fn bring_to_front(handle: HWND) -> Raised {
49 request_topmost(handle);
50 let foreground = request_foreground(handle);
51 Raised {
52 topmost: confirm_topmost(handle),
53 foreground,
54 }
55}
56
57/// How long to wait for the posted reorder to be carried out before reporting
58/// it as unconfirmed. Only spent on the first raise of a given window; once the
59/// style is set, the first read succeeds.
60const CONFIRM_ATTEMPTS: u8 = 10;
61const CONFIRM_INTERVAL: core::time::Duration = core::time::Duration::from_millis(10);
62
63/// Reads back whether the reorder took effect, allowing for it being posted.
64///
65/// The request is posted rather than sent, so it is carried out by the window's
66/// own thread a moment later; reading immediately would report "not on top" for
67/// a window that is about to be. Polling the style is safe from any thread
68/// because it sends no message and cannot block.
69fn confirm_topmost(handle: HWND) -> bool {
70 for _ in 0..CONFIRM_ATTEMPTS {
71 if is_topmost(handle) {
72 return true;
73 }
74 std::thread::sleep(CONFIRM_INTERVAL);
75 }
76 false
77}
78
79/// Asks for the always-on-top band, without waiting for the answer.
80///
81/// Posted rather than sent (`SWP_ASYNCWINDOWPOS`). A synchronous
82/// [`SetWindowPos`] would block on the window's own thread, and this runs on a
83/// guard thread whose entire purpose is to stay responsive while another thread
84/// is stuck. The position/size arguments are ignored because the window is only
85/// being reordered.
86fn request_topmost(handle: HWND) {
87 // SAFETY: `handle` came from a window enumeration in this process and is
88 // checked for visibility before use. The call reorders that window and
89 // writes nothing back; a failure means the window closed in the meantime,
90 // which is the ordinary race and needs no handling.
91 let _ = unsafe {
92 SetWindowPos(
93 handle,
94 Some(HWND_TOPMOST),
95 0,
96 0,
97 0,
98 0,
99 SWP_NOMOVE | SWP_NOSIZE | SWP_ASYNCWINDOWPOS,
100 )
101 };
102}
103
104/// Asks for keyboard focus. Returns whether the system agreed.
105fn request_foreground(handle: HWND) -> bool {
106 // SAFETY: `handle` is a live window in this process. The BOOL result is the
107 // documented refusal signal, not an error to propagate.
108 unsafe { SetForegroundWindow(handle) }.as_bool()
109}
110
111/// Whether the window currently sits in the always-on-top band.
112///
113/// Reads the window's extended style directly, which does not send a message,
114/// so it cannot block on the owning thread.
115fn is_topmost(handle: HWND) -> bool {
116 // SAFETY: `handle` is a live window in this process; the call only reads a
117 // style word. Zero is returned both for "no bits" and for a failure, and
118 // both mean "not known to be topmost", which is the honest answer.
119 let style = unsafe { GetWindowLongW(handle, GWL_EXSTYLE) };
120 u32::try_from(style).is_ok_and(|bits| bits & WS_EX_TOPMOST.0 != 0)
121}