display.rs

  1use itertools::Itertools;
  2use smallvec::SmallVec;
  3use std::rc::Rc;
  4use util::ResultExt;
  5use uuid::Uuid;
  6use windows::{
  7    Win32::{
  8        Foundation::*,
  9        Graphics::Gdi::*,
 10        UI::{
 11            HiDpi::{GetDpiForMonitor, MDT_EFFECTIVE_DPI},
 12            WindowsAndMessaging::USER_DEFAULT_SCREEN_DPI,
 13        },
 14    },
 15    core::*,
 16};
 17
 18use crate::{Bounds, DevicePixels, DisplayId, Pixels, PlatformDisplay, logical_point, point, size};
 19
 20#[derive(Debug, Clone, Copy)]
 21pub(crate) struct WindowsDisplay {
 22    pub handle: HMONITOR,
 23    pub display_id: DisplayId,
 24    scale_factor: f32,
 25    bounds: Bounds<Pixels>,
 26    visible_bounds: Bounds<Pixels>,
 27    physical_bounds: Bounds<DevicePixels>,
 28    uuid: Uuid,
 29}
 30
 31// The `HMONITOR` is thread-safe.
 32unsafe impl Send for WindowsDisplay {}
 33unsafe impl Sync for WindowsDisplay {}
 34
 35impl WindowsDisplay {
 36    pub(crate) fn new(display_id: DisplayId) -> Option<Self> {
 37        let screen = available_monitors().into_iter().nth(display_id.0 as _)?;
 38        let info = get_monitor_info(screen).log_err()?;
 39        let monitor_size = info.monitorInfo.rcMonitor;
 40        let work_area = info.monitorInfo.rcWork;
 41        let uuid = generate_uuid(&info.szDevice);
 42        let scale_factor = get_scale_factor_for_monitor(screen).log_err()?;
 43        let physical_size = size(
 44            (monitor_size.right - monitor_size.left).into(),
 45            (monitor_size.bottom - monitor_size.top).into(),
 46        );
 47
 48        Some(WindowsDisplay {
 49            handle: screen,
 50            display_id,
 51            scale_factor,
 52            bounds: Bounds {
 53                origin: logical_point(
 54                    monitor_size.left as f32,
 55                    monitor_size.top as f32,
 56                    scale_factor,
 57                ),
 58                size: physical_size.to_pixels(scale_factor),
 59            },
 60            visible_bounds: Bounds {
 61                origin: logical_point(work_area.left as f32, work_area.top as f32, scale_factor),
 62                size: size(
 63                    (work_area.right - work_area.left) as f32 / scale_factor,
 64                    (work_area.bottom - work_area.top) as f32 / scale_factor,
 65                )
 66                .map(crate::px),
 67            },
 68            physical_bounds: Bounds {
 69                origin: point(monitor_size.left.into(), monitor_size.top.into()),
 70                size: physical_size,
 71            },
 72            uuid,
 73        })
 74    }
 75
 76    pub fn new_with_handle(monitor: HMONITOR) -> anyhow::Result<Self> {
 77        let info = get_monitor_info(monitor)?;
 78        let monitor_size = info.monitorInfo.rcMonitor;
 79        let work_area = info.monitorInfo.rcWork;
 80        let uuid = generate_uuid(&info.szDevice);
 81        let display_id = available_monitors()
 82            .iter()
 83            .position(|handle| handle.0 == monitor.0)
 84            .unwrap();
 85        let scale_factor = get_scale_factor_for_monitor(monitor)?;
 86        let physical_size = size(
 87            (monitor_size.right - monitor_size.left).into(),
 88            (monitor_size.bottom - monitor_size.top).into(),
 89        );
 90
 91        Ok(WindowsDisplay {
 92            handle: monitor,
 93            display_id: DisplayId(display_id as _),
 94            scale_factor,
 95            bounds: Bounds {
 96                origin: logical_point(
 97                    monitor_size.left as f32,
 98                    monitor_size.top as f32,
 99                    scale_factor,
100                ),
101                size: physical_size.to_pixels(scale_factor),
102            },
103            visible_bounds: Bounds {
104                origin: logical_point(work_area.left as f32, work_area.top as f32, scale_factor),
105                size: size(
106                    (work_area.right - work_area.left) as f32 / scale_factor,
107                    (work_area.bottom - work_area.top) as f32 / scale_factor,
108                )
109                .map(crate::px),
110            },
111            physical_bounds: Bounds {
112                origin: point(monitor_size.left.into(), monitor_size.top.into()),
113                size: physical_size,
114            },
115            uuid,
116        })
117    }
118
119    fn new_with_handle_and_id(handle: HMONITOR, display_id: DisplayId) -> anyhow::Result<Self> {
120        let info = get_monitor_info(handle)?;
121        let monitor_size = info.monitorInfo.rcMonitor;
122        let work_area = info.monitorInfo.rcWork;
123        let uuid = generate_uuid(&info.szDevice);
124        let scale_factor = get_scale_factor_for_monitor(handle)?;
125        let physical_size = size(
126            (monitor_size.right - monitor_size.left).into(),
127            (monitor_size.bottom - monitor_size.top).into(),
128        );
129
130        Ok(WindowsDisplay {
131            handle,
132            display_id,
133            scale_factor,
134            bounds: Bounds {
135                origin: logical_point(
136                    monitor_size.left as f32,
137                    monitor_size.top as f32,
138                    scale_factor,
139                ),
140                size: physical_size.to_pixels(scale_factor),
141            },
142            visible_bounds: Bounds {
143                origin: logical_point(work_area.left as f32, work_area.top as f32, scale_factor),
144                size: size(
145                    (work_area.right - work_area.left) as f32 / scale_factor,
146                    (work_area.bottom - work_area.top) as f32 / scale_factor,
147                )
148                .map(crate::px),
149            },
150            physical_bounds: Bounds {
151                origin: point(monitor_size.left.into(), monitor_size.top.into()),
152                size: physical_size,
153            },
154            uuid,
155        })
156    }
157
158    pub fn primary_monitor() -> Option<Self> {
159        // https://devblogs.microsoft.com/oldnewthing/20070809-00/?p=25643
160        const POINT_ZERO: POINT = POINT { x: 0, y: 0 };
161        let monitor = unsafe { MonitorFromPoint(POINT_ZERO, MONITOR_DEFAULTTOPRIMARY) };
162        if monitor.is_invalid() {
163            log::error!(
164                "can not find the primary monitor: {}",
165                std::io::Error::last_os_error()
166            );
167            return None;
168        }
169        WindowsDisplay::new_with_handle(monitor).log_err()
170    }
171
172    /// Check if the center point of given bounds is inside this monitor
173    pub fn check_given_bounds(&self, bounds: Bounds<Pixels>) -> bool {
174        let center = bounds.center();
175        let center = POINT {
176            x: (center.x.0 * self.scale_factor) as i32,
177            y: (center.y.0 * self.scale_factor) as i32,
178        };
179        let monitor = unsafe { MonitorFromPoint(center, MONITOR_DEFAULTTONULL) };
180        if monitor.is_invalid() {
181            false
182        } else {
183            let Ok(display) = WindowsDisplay::new_with_handle(monitor) else {
184                return false;
185            };
186            display.uuid == self.uuid
187        }
188    }
189
190    pub fn displays() -> Vec<Rc<dyn PlatformDisplay>> {
191        available_monitors()
192            .into_iter()
193            .enumerate()
194            .filter_map(|(id, handle)| {
195                Some(Rc::new(
196                    WindowsDisplay::new_with_handle_and_id(handle, DisplayId(id as _)).ok()?,
197                ) as Rc<dyn PlatformDisplay>)
198            })
199            .collect()
200    }
201
202    /// Check if this monitor is still online
203    pub fn is_connected(hmonitor: HMONITOR) -> bool {
204        available_monitors().iter().contains(&hmonitor)
205    }
206
207    pub fn physical_bounds(&self) -> Bounds<DevicePixels> {
208        self.physical_bounds
209    }
210}
211
212impl PlatformDisplay for WindowsDisplay {
213    fn id(&self) -> DisplayId {
214        self.display_id
215    }
216
217    fn uuid(&self) -> anyhow::Result<Uuid> {
218        Ok(self.uuid)
219    }
220
221    fn bounds(&self) -> Bounds<Pixels> {
222        self.bounds
223    }
224
225    fn visible_bounds(&self) -> Bounds<Pixels> {
226        self.visible_bounds
227    }
228}
229
230fn available_monitors() -> SmallVec<[HMONITOR; 4]> {
231    let mut monitors: SmallVec<[HMONITOR; 4]> = SmallVec::new();
232    unsafe {
233        EnumDisplayMonitors(
234            None,
235            None,
236            Some(monitor_enum_proc),
237            LPARAM(&mut monitors as *mut _ as _),
238        )
239        .ok()
240        .log_err();
241    }
242    monitors
243}
244
245unsafe extern "system" fn monitor_enum_proc(
246    hmonitor: HMONITOR,
247    _hdc: HDC,
248    _place: *mut RECT,
249    data: LPARAM,
250) -> BOOL {
251    let monitors = data.0 as *mut SmallVec<[HMONITOR; 4]>;
252    unsafe { (*monitors).push(hmonitor) };
253    BOOL(1)
254}
255
256fn get_monitor_info(hmonitor: HMONITOR) -> anyhow::Result<MONITORINFOEXW> {
257    let mut monitor_info: MONITORINFOEXW = unsafe { std::mem::zeroed() };
258    monitor_info.monitorInfo.cbSize = std::mem::size_of::<MONITORINFOEXW>() as u32;
259    let status = unsafe {
260        GetMonitorInfoW(
261            hmonitor,
262            &mut monitor_info as *mut MONITORINFOEXW as *mut MONITORINFO,
263        )
264    };
265    if status.as_bool() {
266        Ok(monitor_info)
267    } else {
268        Err(anyhow::anyhow!(std::io::Error::last_os_error()))
269    }
270}
271
272fn generate_uuid(device_name: &[u16]) -> Uuid {
273    let name = device_name
274        .iter()
275        .flat_map(|&a| a.to_be_bytes())
276        .collect_vec();
277    Uuid::new_v5(&Uuid::NAMESPACE_DNS, &name)
278}
279
280fn get_scale_factor_for_monitor(monitor: HMONITOR) -> Result<f32> {
281    let mut dpi_x = 0;
282    let mut dpi_y = 0;
283    unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) }?;
284    assert_eq!(dpi_x, dpi_y);
285    Ok(dpi_x as f32 / USER_DEFAULT_SCREEN_DPI as f32)
286}