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}