Skip to main content

display_config/
windows.rs

1//! This module contains the Windows-specific implementation of the display configuration.
2
3use std::{
4    collections::HashMap,
5    ffi::{OsStr, OsString, c_void},
6    os::windows::ffi::OsStringExt,
7    sync::{Arc, Mutex},
8};
9
10use dpi::{LogicalPosition, LogicalSize};
11use smallvec::SmallVec;
12use windows::{
13    Win32::{
14        Devices::Display::*,
15        Foundation::*,
16        Graphics::Gdi::*,
17        System::LibraryLoader::*,
18        UI::{HiDpi::*, WindowsAndMessaging::*},
19    },
20    core::{BOOL, w},
21};
22
23use crate::{Display, DisplayEventCallback, Event};
24
25/// The error type for Windows-specific operations.
26/// This is a type alias for [`windows::core::Error`][windows::core::Error].
27///
28/// [windows::core::Error]: https://docs.rs/windows/latest/windows/core/struct.Error.html
29pub type WindowsError = windows::core::Error;
30
31/// Sets the current process as DPI aware (Per Monitor).
32///
33/// This function calls `SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE)`.
34/// It is recommended to call this function at the very beginning of the application
35/// to ensure that the display information (especially `scale_factor`) is correctly reported.
36///
37/// **Important**: This setting cannot be changed once set for a process.
38/// If you are integrating this crate with a GUI framework (e.g., Winit, Tauri, or others),
39/// it is likely that the framework already handles DPI awareness. Calling this function
40/// in such a scenario might conflict with the framework's own DPI management,
41/// potentially leading to unexpected behavior or crashes. In most cases, it's best to
42/// defer DPI awareness management to your chosen GUI framework or manage it at the
43/// application level very early in the process lifecycle.
44///
45/// # Errors
46/// Returns a [`WindowsError`] if `SetProcessDpiAwareness` fails.
47pub fn set_process_per_monitor_dpi_aware() -> Result<(), WindowsError> {
48    unsafe { SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE) }
49}
50
51/// A Windows-specific unique identifier for a display.
52///
53/// This ID is based on the [device path][device path] of the display.
54///
55/// [device path]: https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#dos-device-paths
56#[derive(Debug, Clone)]
57pub struct WindowsDisplayId {
58    name: Arc<OsString>,
59}
60
61impl std::hash::Hash for WindowsDisplayId {
62    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
63        self.name.hash(state)
64    }
65}
66
67impl PartialEq for WindowsDisplayId {
68    fn eq(&self, other: &Self) -> bool {
69        self.name.eq(&other.name)
70    }
71}
72
73impl Eq for WindowsDisplayId {}
74
75impl WindowsDisplayId {
76    /// Creates a new `WindowsDisplayId` from a device name string.
77    pub fn new(name: OsString) -> Self {
78        Self {
79            name: Arc::new(name),
80        }
81    }
82
83    /// Creates a `WindowsDisplayId` from a Windows `HMONITOR` handle.
84    ///
85    /// # Errors
86    /// Returns a [`WindowsError`] if `GetMonitorInfoW` fails.
87    pub fn from_handle(handle: HMONITOR) -> Result<Self, WindowsError> {
88        let mut monitor_info = MONITORINFOEXW::default();
89        monitor_info.monitorInfo.cbSize = std::mem::size_of::<MONITORINFOEXW>() as _;
90
91        unsafe { GetMonitorInfoW(handle, &raw mut monitor_info as _).ok()? };
92
93        let name_slice = &monitor_info.szDevice;
94        let len = name_slice
95            .iter()
96            .position(|&c| c == 0)
97            .unwrap_or(name_slice.len());
98        let name = OsString::from_wide(&monitor_info.szDevice[..len]);
99
100        Ok(Self {
101            name: Arc::new(name),
102        })
103    }
104
105    /// Get device identification string. This is also called device path.
106    /// e.g. `\\?\DISPLAY1..."
107    ///
108    /// See [Microsoft's documentation][docs] for more details.
109    ///
110    /// [docs]: https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#dos-device-paths
111    pub fn device_name(&self) -> &OsStr {
112        &self.name
113    }
114}
115
116fn is_display_mirrored(device_name: &OsStr) -> Result<bool, WindowsError> {
117    let mut path_count = 0;
118    let mut mode_count = 0;
119
120    unsafe {
121        GetDisplayConfigBufferSizes(QDC_ONLY_ACTIVE_PATHS, &mut path_count, &mut mode_count)
122            .ok()?;
123    }
124
125    let mut paths = vec![DISPLAYCONFIG_PATH_INFO::default(); path_count as usize];
126    let mut modes = vec![DISPLAYCONFIG_MODE_INFO::default(); mode_count as usize];
127
128    unsafe {
129        QueryDisplayConfig(
130            QDC_ONLY_ACTIVE_PATHS,
131            &mut path_count,
132            paths.as_mut_ptr(),
133            &mut mode_count,
134            modes.as_mut_ptr(),
135            None,
136        )
137        .ok()?;
138    }
139
140    let mut match_count = 0;
141    for path in paths.iter().take(path_count as usize) {
142        let mut source_name = DISPLAYCONFIG_SOURCE_DEVICE_NAME::default();
143
144        source_name.header.r#type = DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME;
145        source_name.header.size = std::mem::size_of::<DISPLAYCONFIG_SOURCE_DEVICE_NAME>() as u32;
146        source_name.header.adapterId = path.sourceInfo.adapterId;
147        source_name.header.id = path.sourceInfo.id;
148
149        if unsafe { DisplayConfigGetDeviceInfo(&mut source_name.header as *mut _) }
150            == ERROR_SUCCESS.0 as i32
151        {
152            let name_slice = &source_name.viewGdiDeviceName;
153            let len = name_slice
154                .iter()
155                .position(|&c| c == 0)
156                .unwrap_or(name_slice.len());
157            let name = OsString::from_wide(&name_slice[..len]);
158
159            if name == device_name {
160                match_count += 1;
161            }
162        }
163    }
164
165    Ok(match_count > 1)
166}
167
168fn get_scale_factor(hdc: HDC, h_monitor: HMONITOR) -> f64 {
169    // NOTE: https://learn.microsoft.com/ja-jp/windows/win32/learnwin32/dpi-and-device-independent-pixels#converting-physical-pixels-to-dips
170    const USER_DEFAULT_SCREEN_DPI: u32 = 96;
171
172    let mut dpi_x = 0;
173    let mut dpi_y = 0;
174    let result = unsafe { GetDpiForMonitor(h_monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) };
175
176    if result.is_err() {
177        dpi_x = if unsafe { IsProcessDPIAware().as_bool() } {
178            unsafe { GetDeviceCaps(Some(hdc), LOGPIXELSX) as _ }
179        } else {
180            USER_DEFAULT_SCREEN_DPI
181        };
182    };
183
184    dpi_x as f64 / USER_DEFAULT_SCREEN_DPI as f64
185}
186
187struct EnumDisplayMonitorsUserData {
188    displays: Vec<Display>,
189    result: Result<(), WindowsError>,
190}
191
192unsafe extern "system" fn monitor_enum_proc(
193    h_monitor: HMONITOR,
194    hdc: HDC,
195    _rect: *mut RECT,
196    user_data: LPARAM,
197) -> BOOL {
198    let monitors_ptr = user_data.0 as *mut EnumDisplayMonitorsUserData;
199    if monitors_ptr.is_null() {
200        return false.into();
201    }
202
203    let user_data = unsafe { &mut *monitors_ptr };
204
205    // Get full monitor info
206    let mut monitor_info = MONITORINFOEXW::default();
207    monitor_info.monitorInfo.cbSize = std::mem::size_of::<MONITORINFOEXW>() as _;
208
209    if let Err(e) = unsafe { GetMonitorInfoW(h_monitor, &raw mut monitor_info as _) }.ok() {
210        user_data.result = Err(e);
211        return true.into(); // Skip this monitor but continue enumeration
212    }
213
214    let name_slice = &monitor_info.szDevice;
215    let len = name_slice
216        .iter()
217        .position(|&c| c == 0)
218        .unwrap_or(name_slice.len());
219    let device_name = OsString::from_wide(&monitor_info.szDevice[..len]);
220    let id = WindowsDisplayId::new(device_name);
221
222    let origin = LogicalPosition::new(
223        monitor_info.monitorInfo.rcMonitor.left,
224        monitor_info.monitorInfo.rcMonitor.top,
225    );
226    let size = LogicalSize::new(
227        (monitor_info.monitorInfo.rcMonitor.right - monitor_info.monitorInfo.rcMonitor.left) as u32,
228        (monitor_info.monitorInfo.rcMonitor.bottom - monitor_info.monitorInfo.rcMonitor.top) as u32,
229    );
230    let is_primary = (monitor_info.monitorInfo.dwFlags & MONITORINFOF_PRIMARY) != 0;
231
232    let is_mirrored = match is_display_mirrored(id.device_name()) {
233        Ok(value) => value,
234        Err(e) => {
235            user_data.result = Err(e);
236            return false.into();
237        }
238    };
239    let scale_factor = get_scale_factor(hdc, h_monitor);
240
241    user_data.displays.push(Display {
242        id: id.into(),
243        origin,
244        size,
245        scale_factor,
246        is_primary,
247        is_mirrored,
248    });
249
250    true.into()
251}
252
253/// Get a list of all currently active Windows displays.
254pub(crate) fn get_windows_displays() -> Result<Vec<Display>, WindowsError> {
255    let mut user_data: EnumDisplayMonitorsUserData = EnumDisplayMonitorsUserData {
256        displays: Vec::new(),
257        result: Ok(()),
258    };
259
260    unsafe {
261        EnumDisplayMonitors(
262            None,
263            None,
264            Some(monitor_enum_proc),
265            LPARAM(&raw mut user_data as isize),
266        )
267        .ok()?;
268    };
269
270    user_data.result.map(|_| user_data.displays)
271}
272
273struct EventTracker {
274    cached_displays: HashMap<WindowsDisplayId, Display>,
275}
276
277impl EventTracker {
278    fn new() -> Result<Self, WindowsError> {
279        let mut tracker = Self {
280            cached_displays: HashMap::new(),
281        };
282        tracker.cached_displays = tracker.collect_new_cached_state()?;
283
284        Ok(tracker)
285    }
286
287    fn collect_new_cached_state(&self) -> Result<HashMap<WindowsDisplayId, Display>, WindowsError> {
288        let displays = get_windows_displays()?;
289        let mut cached_state = HashMap::new();
290
291        for display in displays {
292            let win_id = display.id.windows_id();
293            cached_state.insert(win_id.clone(), display);
294        }
295
296        Ok(cached_state)
297    }
298
299    fn track_events(&mut self) -> Result<SmallVec<[Event; 10]>, WindowsError> {
300        let new_cached_state = self.collect_new_cached_state()?;
301        let before = std::mem::replace(&mut self.cached_displays, new_cached_state);
302        let mut events = SmallVec::new();
303
304        for (id, before_display) in before.iter() {
305            if let Some(after_display) = self.cached_displays.get(id) {
306                if before_display.size != after_display.size {
307                    events.push(Event::SizeChanged {
308                        display: (*after_display).clone(),
309                        before: before_display.size,
310                        after: after_display.size,
311                    });
312                };
313
314                if before_display.origin != after_display.origin {
315                    events.push(Event::OriginChanged {
316                        display: (*after_display).clone(),
317                        before: before_display.origin,
318                        after: after_display.origin,
319                    });
320                }
321
322                if before_display.is_mirrored != after_display.is_mirrored {
323                    let event = if after_display.is_mirrored {
324                        Event::Mirrored((*after_display).clone())
325                    } else {
326                        Event::UnMirrored((*after_display).clone())
327                    };
328
329                    events.push(event);
330                }
331            } else {
332                events.push(Event::Removed(id.clone().into()));
333            }
334        }
335
336        for (id, after_display) in &self.cached_displays {
337            if !before.contains_key(id) {
338                events.push(Event::Added((*after_display).clone()));
339            }
340        }
341
342        Ok(events)
343    }
344}
345
346struct ObserverContext {
347    callback: Option<DisplayEventCallback>,
348    tracker: EventTracker,
349}
350
351/// A Windows-specific display observer that monitors changes to the display configuration.
352///
353/// This observer creates a hidden window to receive `WM_DISPLAYCHANGE` messages
354/// and uses device notification APIs to track display events.
355pub(crate) struct WindowsDisplayObserver {
356    hwnd: HWND,
357    h_notify: HDEVNOTIFY,
358    ctx: Arc<Mutex<ObserverContext>>,
359}
360
361impl WindowsDisplayObserver {
362    /// Creates a new `WindowsDisplayObserver`.
363    ///
364    /// This function sets up a hidden window and registers for device notifications
365    /// to begin observing display configuration changes.
366    ///
367    /// # Errors
368    /// Returns a [`WindowsError`] if there is an issue creating the window,
369    /// registering for notifications, or collecting initial display information.
370    pub fn new() -> Result<Self, WindowsError> {
371        let h_instance = unsafe { GetModuleHandleW(None)? };
372        let window_class_name = w!("DisplayMonitorClass");
373        let window_class = WNDCLASSW {
374            style: CS_HREDRAW | CS_VREDRAW,
375            lpfnWndProc: Some(wnd_proc),
376            hInstance: h_instance.into(),
377            lpszClassName: window_class_name,
378            ..Default::default()
379        };
380
381        unsafe {
382            RegisterClassW(&window_class);
383        }
384
385        let ctx = Arc::new(Mutex::new(ObserverContext {
386            callback: None,
387            tracker: EventTracker::new()?,
388        }));
389        let state_ptr = Arc::as_ptr(&ctx) as *mut c_void;
390
391        let hwnd = unsafe {
392            CreateWindowExW(
393                WINDOW_EX_STYLE::default(),
394                window_class_name,
395                w!("DisplayMonitorWindow"),
396                WS_OVERLAPPEDWINDOW,
397                0,
398                0,
399                0,
400                0,
401                None,
402                None,
403                Some(h_instance.into()),
404                Some(state_ptr),
405            )?
406        };
407
408        let mut filter = DEV_BROADCAST_DEVICEINTERFACE_W {
409            dbcc_size: std::mem::size_of::<DEV_BROADCAST_DEVICEINTERFACE_W>() as u32,
410            dbcc_devicetype: DBT_DEVTYP_DEVICEINTERFACE.0,
411            dbcc_classguid: GUID_DEVINTERFACE_MONITOR,
412            ..Default::default()
413        };
414
415        let h_notify = unsafe {
416            match RegisterDeviceNotificationW(
417                hwnd.into(),
418                &mut filter as *mut _ as *const c_void,
419                DEVICE_NOTIFY_WINDOW_HANDLE,
420            ) {
421                Ok(handle) => handle,
422                Err(e) => {
423                    _ = DestroyWindow(hwnd);
424                    return Err(e);
425                }
426            }
427        };
428
429        // Store the state pointer in the window user data so WndProc can access it.
430        // NOTE: We passed it in CreateWindowExW, but we also set it here to be sure or if we missed WM_CREATE handling.
431        unsafe {
432            SetWindowLongPtrW(hwnd, GWLP_USERDATA, state_ptr as isize);
433        }
434
435        Ok(Self {
436            hwnd,
437            h_notify,
438            ctx,
439        })
440    }
441
442    /// Sets the callback function to be invoked when a display event occurs.
443    ///
444    /// The provided callback will receive a [`Event`] enum,
445    /// indicating the nature of the display change.
446    pub fn set_callback(&self, callback: DisplayEventCallback) {
447        let mut state = self.ctx.lock().unwrap();
448        state.callback = Some(callback);
449    }
450
451    /// Removes the currently set callback function.
452    /// After calling this, no display events will be dispatched.
453    pub fn remove_callback(&self) {
454        let mut state = self.ctx.lock().unwrap();
455        state.callback = None;
456    }
457
458    /// Runs the Windows message loop to start handling display events.
459    ///
460    /// This function will block the current thread and dispatch messages.
461    ///
462    /// # Errors
463    /// Returns a [`WindowsError`] if `GetMessageW` fails.
464    pub fn run(&self) -> Result<(), WindowsError> {
465        unsafe {
466            let mut msg = MSG::default();
467            loop {
468                let message_state = GetMessageW(&mut msg, None, 0, 0).0;
469                if message_state == -1 {
470                    return Err(WindowsError::from_thread());
471                }
472                if message_state == 0 {
473                    break;
474                }
475
476                _ = TranslateMessage(&msg);
477                DispatchMessageW(&msg);
478            }
479        }
480
481        Ok(())
482    }
483}
484
485impl Drop for WindowsDisplayObserver {
486    fn drop(&mut self) {
487        unsafe {
488            if !self.h_notify.is_invalid() {
489                _ = UnregisterDeviceNotification(self.h_notify);
490            }
491            _ = DestroyWindow(self.hwnd);
492        }
493    }
494}
495
496#[inline]
497fn process_window_message(
498    msg: u32,
499    _wparam: WPARAM,
500    _lparam: LPARAM,
501    ctx: &mut ObserverContext,
502) -> Result<Option<SmallVec<[Event; 10]>>, WindowsError> {
503    Ok(match msg {
504        WM_DISPLAYCHANGE => Some(ctx.tracker.track_events()?),
505        _ => None,
506    })
507}
508
509unsafe extern "system" fn wnd_proc(
510    hwnd: HWND,
511    msg: u32,
512    wparam: WPARAM,
513    lparam: LPARAM,
514) -> LRESULT {
515    let default_window_proc = || unsafe { DefWindowProcW(hwnd, msg, wparam, lparam) };
516
517    let ctx = unsafe {
518        let user_data = GetWindowLongPtrW(hwnd, GWLP_USERDATA);
519        let user_data_ptr = user_data as *const Mutex<ObserverContext>;
520
521        if user_data_ptr.is_null() {
522            return default_window_proc();
523        }
524
525        &*(user_data_ptr)
526    };
527
528    if let Ok(mut ctx) = ctx.lock()
529        && let Ok(Some(events)) = process_window_message(msg, wparam, lparam, &mut ctx)
530        && let Some(callback) = ctx.callback.as_mut()
531    {
532        for event in events {
533            (callback)(event);
534        }
535    }
536
537    default_window_proc()
538}