Skip to main content

display_config/
lib.rs

1//! display-config is a Rust crate for retrieving display configurations and
2//! observing changes (monitor plug/unplug, resolution changes, etc.) on
3//! Windows and macOS.
4//!
5//! # Windows-Specific Notes on DPI Awareness
6//!
7//! On Windows, to correctly retrieve display `scale_factor` and other display
8//! metrics, your application needs to declare its
9//! [DPI Awareness](https://learn.microsoft.com/en-us/windows/win32/hidpi/high-dpi-desktop-application-development-on-windows).
10//! If your application does not declare DPI awareness, Windows may virtualize
11//! DPI settings for your process, leading to incorrect values (e.g., `scale_factor` always being `1.0`).
12//!
13//! You can set your process's DPI awareness programmatically using the
14//! `display_config::windows::set_process_per_monitor_dpi_aware()` function
15//! provided by this crate. It is recommended to call this function at the
16//! very beginning of your application's lifecycle, before any display-related
17//! operations are performed.
18//!
19//! ```no_run
20//! fn main() -> Result<(), Box<dyn std::error::Error>> {
21//!     // Set the process as DPI aware for correct display metric reporting
22//!     #[cfg(target_os = "windows")]
23//!     display_config::windows::set_process_per_monitor_dpi_aware()?;
24//!
25//!     let displays = display_config::get_displays()?;
26//!     for display in displays {
27//!         println!("Display ID: {:?}", display.id);
28//!         println!("  Scale Factor: {}", display.scale_factor);
29//!     }
30//!
31//!     Ok(())
32//! }
33//! ```
34//!
35//! **Important Considerations**:
36//! - DPI awareness cannot be changed once set for a process.
37//! - If you are using a GUI framework, it often handles
38//!   DPI awareness itself. Calling this function might conflict with the
39//!   framework's settings. In such cases, defer to the framework's DPI management.
40
41use dpi::{LogicalPosition, LogicalSize};
42
43#[cfg(target_os = "macos")]
44use macos::{
45    MacOSDisplayId as PlatformDisplayId, MacOSDisplayObserver as PlatformDisplayObserver,
46    MacOSError as PlatformError, get_macos_displays as get_platform_displays,
47};
48#[cfg(target_os = "windows")]
49use windows::{
50    WindowsDisplayId as PlatformDisplayId, WindowsDisplayObserver as PlatformDisplayObserver,
51    WindowsError as PlatformError, get_windows_displays as get_platform_displays,
52};
53
54#[cfg(target_os = "macos")]
55pub mod macos;
56#[cfg(target_os = "windows")]
57pub mod windows;
58
59/// The error type for this crate.
60#[derive(Debug, thiserror::Error)]
61pub enum Error {
62    /// An error occurred during initialization.
63    #[error("Initialization failed.")]
64    InitializationError(PlatformError),
65    /// An error occurred in the platform-specific implementation.
66    #[error("A platform-specific error has occurred.")]
67    PlatformError(PlatformError),
68}
69
70impl From<PlatformError> for Error {
71    fn from(value: PlatformError) -> Self {
72        Self::PlatformError(value)
73    }
74}
75
76/// Get all available displays.
77pub fn get_displays() -> Result<Vec<Display>, Error> {
78    Ok(get_platform_displays()?)
79}
80
81/// A unique identifier for a display.
82/// It is used to track displays across different platforms.
83///
84/// # Platform-specific
85/// - **Windows**: The id is [device path][device path].
86/// - **macOS**: The id is a value of [`CGDirectDisplayID`][CGDirectDisplayID].
87///
88/// [device path]: https://learn.microsoft.com/en-us/dotnet/standard/io/file-path-formats#dos-device-paths
89/// [CGDirectDisplayID]: https://developer.apple.com/documentation/coregraphics/cgdirectdisplayid?language=objc
90#[derive(Debug, Clone, PartialEq, Eq, Hash)]
91pub struct DisplayId(PlatformDisplayId);
92
93impl From<PlatformDisplayId> for DisplayId {
94    fn from(value: PlatformDisplayId) -> Self {
95        Self(value)
96    }
97}
98
99impl DisplayId {
100    /// Returns the Windows-specific display id.
101    #[cfg(target_os = "windows")]
102    pub fn windows_id(&self) -> &PlatformDisplayId {
103        &self.0
104    }
105
106    /// Returns the macOS-specific display id.
107    #[cfg(target_os = "macos")]
108    pub fn macos_id(&self) -> &PlatformDisplayId {
109        &self.0
110    }
111}
112
113/// A display.
114///
115/// This struct provides a cross-platform interface to interact with displays.
116/// You can get the display's id, origin, size, and check if it's mirrored.
117#[derive(Debug, Clone, PartialEq)]
118pub struct Display {
119    /// The unique identifier of the display.
120    pub id: DisplayId,
121    /// The origin of the display.
122    pub origin: LogicalPosition<i32>,
123    /// The size of the display.
124    pub size: LogicalSize<u32>,
125    /// The scale factor of the display.
126    pub scale_factor: f64,
127    /// Whether the display is the primary monitor.
128    pub is_primary: bool,
129    /// Whether the display is mirrored.
130    pub is_mirrored: bool,
131}
132
133/// An event that occurs when the display configuration changes.
134#[derive(Debug, Clone)]
135pub enum Event {
136    /// A display was added.
137    Added(Display),
138    /// A display was removed.
139    Removed(DisplayId),
140    /// The size of a display changed.
141    SizeChanged {
142        display: Display,
143        before: LogicalSize<u32>,
144        after: LogicalSize<u32>,
145    },
146    /// The origin of a display changed.
147    OriginChanged {
148        display: Display,
149        before: LogicalPosition<i32>,
150        after: LogicalPosition<i32>,
151    },
152    /// A display was mirrored.
153    Mirrored(Display),
154    /// A display was unmirrored.
155    UnMirrored(Display),
156}
157
158/// A callback function that is called when a display event occurs.
159pub type DisplayEventCallback = Box<dyn FnMut(Event) + Send + 'static>;
160
161/// A display observer that monitors changes to the display configuration.
162///
163/// # Platform-specifics
164/// - **Windows**: Internally creates an invisible window to receive `WM_DISPLAYCHANGE` events.
165/// - **macOS**: Uses `CGDisplayRegisterReconfigurationCallback` to track display configuration changes.
166pub struct DisplayObserver {
167    inner: PlatformDisplayObserver,
168}
169
170impl From<PlatformDisplayObserver> for DisplayObserver {
171    fn from(inner: PlatformDisplayObserver) -> Self {
172        Self { inner }
173    }
174}
175
176impl From<DisplayObserver> for PlatformDisplayObserver {
177    fn from(value: DisplayObserver) -> Self {
178        value.inner
179    }
180}
181
182impl DisplayObserver {
183    /// Create the display observer instance.
184    pub fn new() -> Result<Self, Error> {
185        Ok(Self {
186            inner: PlatformDisplayObserver::new()?,
187        })
188    }
189
190    /// Sets the callback function to be invoked when a display event occurs.
191    pub fn set_callback<F>(&self, callback: F)
192    where
193        F: FnMut(Event) + Send + 'static,
194    {
195        self.inner.set_callback(Box::new(callback));
196    }
197
198    /// Removes the currently set callback function. After calling this, no display events will be dispatched.
199    pub fn remove_callback(&self) {
200        self.inner.remove_callback();
201    }
202
203    /// Run the event loop.
204    /// Since macOS ui thread must be on main, this function must be called on main thread.
205    /// If you call this on non-main thread, this will panic on macOS.
206    pub fn run(&self) -> Result<(), Error> {
207        #[cfg(target_os = "windows")]
208        {
209            self.inner.run()?;
210            Ok(())
211        }
212        #[cfg(target_os = "macos")]
213        {
214            self.inner.run();
215            Ok(())
216        }
217    }
218}