1#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
22#[error("{0}")]
23pub struct NoDesktop(pub String);
24
25pub fn check() -> Result<(), NoDesktop> {
27 imp::check()
28}
29
30#[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 pub scale: f32,
40}
41
42pub const MAX_MONITORS: usize = 64;
44
45pub fn monitors() -> Vec<Monitor> {
48 screens::monitors()
49}
50
51pub 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 let Some(mtm) = MainThreadMarker::new() else { return Vec::new() };
67 let screens = NSScreen::screens(mtm);
68 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 const MONITORINFOF_PRIMARY: u32 = 1;
91
92 type Found = Vec<(super::Monitor, bool)>;
94
95 pub fn monitors() -> Vec<super::Monitor> {
96 let mut found: Found = Vec::new();
97 let _ = unsafe { EnumDisplayMonitors(None, None, Some(collect), LPARAM((&raw mut found) as isize)) };
100 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 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 if !unsafe { GetMonitorInfoW(monitor, &raw mut info) }.as_bool() {
116 return true.into();
117 }
118 let (mut dx, mut dy) = (0u32, 0u32);
119 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
140pub const WSF_VISIBLE: u32 = 0x0001;
142
143pub 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 let station = unsafe { GetProcessWindowStation() }.ok()?;
169 let mut info = USEROBJECTFLAGS::default();
170 let size = std::mem::size_of::<USEROBJECTFLAGS>() as u32;
171 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 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 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 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 if let Err(e) = check() {
228 assert!(!e.0.is_empty());
229 }
230 }
231}