Skip to main content

ashlar_native/
display.rs

1//! Whether this process can show a window at all.
2//!
3//! On Windows a program started by a service, a scheduled task "whether the user is logged on or
4//! not", or a remote shell running as SYSTEM (SSH, a management agent) lives in session 0 or in a
5//! service's window station. Windows never shows that window station: a window can be created, but
6//! the graphics driver refuses it a swap chain, and wgpu treats that as fatal (it panics on
7//! `Surface::configure`, before the app can install an error handler). [`check`] tells apps so up
8//! front, so they can say why and stop cleanly instead of crashing.
9//!
10//! A disconnected or locked remote session is not this case: its window station is interactive,
11//! the app keeps running and draws again when the user reconnects.
12//!
13//! On every other platform [`check`] answers `Ok`: there the windowing library reports a missing
14//! display as an ordinary error.
15//!
16//! [`monitors`] lists the monitors before any window exists (macOS `NSScreen.screens`, Windows
17//! `EnumDisplayMonitors`; empty elsewhere), so a window placement saved last time can be checked
18//! against them before the window opens.
19
20/// Why no window can be shown here.
21#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
22#[error("{0}")]
23pub struct NoDesktop(pub String);
24
25/// `Ok` when this process can show windows (see the module docs).
26pub fn check() -> Result<(), NoDesktop> {
27    imp::check()
28}
29
30/// A monitor's area in points (logical pixels), in desktop coordinates: origin at the primary
31/// monitor's top left, y down.
32#[derive(Debug, Clone, Copy, PartialEq)]
33pub struct Monitor {
34    pub x: f32,
35    pub y: f32,
36    pub width: f32,
37    pub height: f32,
38    /// Pixels per point.
39    pub scale: f32,
40}
41
42/// At most this many monitors are listed.
43pub const MAX_MONITORS: usize = 64;
44
45/// The monitors, the primary first. Empty where the platform has no way to ask before a window
46/// exists (Linux, the web), when called off the main thread on macOS, or when the call fails.
47pub fn monitors() -> Vec<Monitor> {
48    screens::monitors()
49}
50
51/// A macOS screen frame (points, origin at the primary screen's bottom left, y up) in desktop
52/// coordinates (y down from the primary's top), given the primary's height.
53pub fn flip_y(x: f64, y: f64, width: f64, height: f64, primary_height: f64, scale: f64) -> Option<Monitor> {
54    let m = Monitor { x: x as f32, y: (primary_height - y - height) as f32, width: width as f32, height: height as f32, scale: scale as f32 };
55    let ok = [m.x, m.y, m.width, m.height, m.scale].iter().all(|v| v.is_finite()) && m.width >= 1.0 && m.height >= 1.0 && m.scale > 0.0;
56    ok.then_some(m)
57}
58
59#[cfg(target_os = "macos")]
60mod screens {
61    use objc2::MainThreadMarker;
62    use objc2_app_kit::NSScreen;
63
64    pub fn monitors() -> Vec<super::Monitor> {
65        // AppKit answers only on the main thread
66        let Some(mtm) = MainThreadMarker::new() else { return Vec::new() };
67        let screens = NSScreen::screens(mtm);
68        // the first screen is the one with the menu bar, whose bottom left is the origin
69        let Some(primary) = screens.iter().next().map(|s| s.frame().size.height) else { return Vec::new() };
70        screens
71            .iter()
72            .take(super::MAX_MONITORS)
73            .filter_map(|s| {
74                let f = s.frame();
75                super::flip_y(f.origin.x, f.origin.y, f.size.width, f.size.height, primary, s.backingScaleFactor())
76            })
77            .collect()
78    }
79}
80
81#[cfg(target_os = "windows")]
82#[allow(unsafe_code)]
83mod screens {
84    use windows::Win32::Foundation::{LPARAM, RECT};
85    use windows::Win32::Graphics::Gdi::{EnumDisplayMonitors, GetMonitorInfoW, HDC, HMONITOR, MONITORINFO};
86    use windows::Win32::UI::HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI};
87    use windows::core::BOOL;
88
89    /// `MONITORINFO::dwFlags` bit of the primary monitor (winuser.h).
90    const MONITORINFOF_PRIMARY: u32 = 1;
91
92    /// One monitor: its rectangle in pixels, its scale and whether it is the primary.
93    type Found = Vec<(super::Monitor, bool)>;
94
95    pub fn monitors() -> Vec<super::Monitor> {
96        let mut found: Found = Vec::new();
97        // SAFETY: `found` outlives the call, which runs `collect` synchronously on this thread
98        // and passes the LPARAM back unchanged; no clip rectangle and no device context.
99        let _ = unsafe { EnumDisplayMonitors(None, None, Some(collect), LPARAM((&raw mut found) as isize)) };
100        // the primary first, as the other platforms list it
101        found.sort_by_key(|(_, primary)| !primary);
102        found.into_iter().map(|(m, _)| m).collect()
103    }
104
105    unsafe extern "system" fn collect(monitor: HMONITOR, _: HDC, _: *mut RECT, data: LPARAM) -> BOOL {
106        // SAFETY: `data` is the `&mut Found` that `monitors` passed, alive and not otherwise
107        // borrowed while EnumDisplayMonitors runs.
108        let Some(found) = (unsafe { (data.0 as *mut Found).as_mut() }) else { return false.into() };
109        if found.len() >= super::MAX_MONITORS {
110            return false.into();
111        }
112        let mut info = MONITORINFO { cbSize: std::mem::size_of::<MONITORINFO>() as u32, ..Default::default() };
113        // SAFETY: `monitor` is the handle the enumeration passed; `info` is writable and its
114        // `cbSize` says its size.
115        if !unsafe { GetMonitorInfoW(monitor, &raw mut info) }.as_bool() {
116            return true.into();
117        }
118        let (mut dx, mut dy) = (0u32, 0u32);
119        // SAFETY: as above; both outputs are writable u32s. A process that is not DPI aware
120        // yet gets 96 and rectangles already in 96-DPI units, so points come out right either way.
121        let dpi = unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &raw mut dx, &raw mut dy) }.ok().map_or(96, |()| dx);
122        let scale = if dpi == 0 { 1.0 } else { dpi as f32 / 96.0 };
123        let r = info.rcMonitor;
124        let (w, h) = (r.right.saturating_sub(r.left) as f32, r.bottom.saturating_sub(r.top) as f32);
125        let m = super::Monitor { x: r.left as f32 / scale, y: r.top as f32 / scale, width: w / scale, height: h / scale, scale };
126        if m.width >= 1.0 && m.height >= 1.0 {
127            found.push((m, info.dwFlags & MONITORINFOF_PRIMARY != 0));
128        }
129        true.into()
130    }
131}
132
133#[cfg(not(any(target_os = "macos", target_os = "windows")))]
134mod screens {
135    pub fn monitors() -> Vec<super::Monitor> {
136        Vec::new()
137    }
138}
139
140/// `WSF_VISIBLE`: the window station has visible display surfaces (it is the interactive one).
141pub const WSF_VISIBLE: u32 = 0x0001;
142
143/// [`check`]'s decision from a window station's `UOI_FLAGS` (`None` when they could not be read,
144/// which is not treated as "no desktop": an app that can't tell tries to open its window).
145pub fn from_window_station_flags(flags: Option<u32>) -> Result<(), NoDesktop> {
146    match flags {
147        Some(f) if f & WSF_VISIBLE == 0 => Err(NoDesktop(
148            "this program was started outside the signed-in desktop (by a service, a scheduled task that runs whether or not the user is signed in, or a remote shell running as SYSTEM), where Windows shows no windows. Start it from the desktop, or from a task that runs only when the user is signed in"
149                .into(),
150        )),
151        _ => Ok(()),
152    }
153}
154
155#[cfg(target_os = "windows")]
156#[allow(unsafe_code)]
157mod imp {
158    use windows::Win32::Foundation::HANDLE;
159    use windows::Win32::System::StationsAndDesktops::{GetProcessWindowStation, GetUserObjectInformationW, UOI_FLAGS, USEROBJECTFLAGS};
160
161    pub fn check() -> Result<(), super::NoDesktop> {
162        super::from_window_station_flags(flags())
163    }
164
165    fn flags() -> Option<u32> {
166        // SAFETY: GetProcessWindowStation takes no arguments and returns a handle owned by the
167        // system, which must not be closed.
168        let station = unsafe { GetProcessWindowStation() }.ok()?;
169        let mut info = USEROBJECTFLAGS::default();
170        let size = std::mem::size_of::<USEROBJECTFLAGS>() as u32;
171        // SAFETY: `station` is the live window station handle from above; `info` is a writable
172        // USEROBJECTFLAGS and `size` is its exact size, which is what UOI_FLAGS fills.
173        unsafe { GetUserObjectInformationW(HANDLE(station.0), UOI_FLAGS, Some((&raw mut info).cast()), size, None) }.ok()?;
174        Some(info.dwFlags)
175    }
176}
177
178#[cfg(not(target_os = "windows"))]
179mod imp {
180    pub fn check() -> Result<(), super::NoDesktop> {
181        Ok(())
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn a_hidden_window_station_has_no_desktop() {
191        // the flags of a service's window station (Service-0x0-3e7$): no WSF_VISIBLE
192        let e = from_window_station_flags(Some(0)).err();
193        assert!(e.is_some_and(|e| e.0.contains("outside the signed-in desktop")));
194        assert!(from_window_station_flags(Some(0x0002)).is_err());
195    }
196
197    #[test]
198    fn the_interactive_window_station_and_unknown_flags_pass() {
199        assert_eq!(from_window_station_flags(Some(WSF_VISIBLE)), Ok(()));
200        assert_eq!(from_window_station_flags(Some(WSF_VISIBLE | 0x0100)), Ok(()));
201        assert_eq!(from_window_station_flags(None), Ok(()));
202    }
203
204    #[test]
205    fn mac_frames_flip_to_y_down() {
206        // primary 1440x900 at the origin; a second screen above-left of it
207        assert_eq!(flip_y(0.0, 0.0, 1440.0, 900.0, 900.0, 2.0), Some(Monitor { x: 0.0, y: 0.0, width: 1440.0, height: 900.0, scale: 2.0 }));
208        let above = flip_y(-1920.0, 900.0, 1920.0, 1080.0, 900.0, 1.0);
209        assert_eq!(above, Some(Monitor { x: -1920.0, y: -1080.0, width: 1920.0, height: 1080.0, scale: 1.0 }));
210        assert_eq!(flip_y(0.0, 0.0, 0.0, 900.0, 900.0, 1.0), None);
211        assert_eq!(flip_y(f64::NAN, 0.0, 10.0, 10.0, 900.0, 1.0), None);
212    }
213
214    #[test]
215    fn monitors_off_the_main_thread_are_empty_not_a_crash() {
216        // test threads are not the main thread: macOS answers nothing rather than touch AppKit
217        let off = std::thread::spawn(monitors).join().unwrap_or_default();
218        if cfg!(target_os = "macos") {
219            assert!(off.is_empty());
220        }
221        assert!(off.len() <= MAX_MONITORS);
222    }
223
224    #[test]
225    fn this_test_process_can_show_windows_or_says_why() {
226        // on a desktop and in CI this passes; it must never panic either way
227        if let Err(e) = check() {
228            assert!(!e.0.is_empty());
229        }
230    }
231}