Skip to main content

rdi_core/
monitor.rs

1//! Monitor / display enumeration types.
2//!
3//! Icon coordinates on Windows live in **virtual-screen space**: a
4//! single coordinate system that spans every attached monitor. The
5//! primary monitor's top-left is `(0, 0)`; secondary monitors can sit
6//! anywhere around it, including at negative offsets when arranged to
7//! the left of or above the primary.
8//!
9//! To place icons intelligently on specific monitors, callers need to
10//! know where each monitor lives in that shared coordinate space —
11//! that is what [`MonitorInfo`] carries.
12//!
13//! Populated by [`DesktopBackend::list_monitors`](crate::DesktopBackend::list_monitors).
14
15use crate::Point;
16
17/// A pixel-space axis-aligned rectangle.
18///
19/// Uses inclusive-left / exclusive-right / inclusive-top / exclusive-
20/// bottom conventions, matching Windows `RECT`. A monitor whose upper-
21/// left is `(0, 0)` and whose size is `1920 x 1080` therefore has
22/// `right = 1920` and `bottom = 1080`.
23#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
24pub struct Rect {
25    pub left: i32,
26    pub top: i32,
27    pub right: i32,
28    pub bottom: i32,
29}
30
31impl Rect {
32    /// Construct a `Rect` from its four edges.
33    #[inline]
34    pub const fn new(left: i32, top: i32, right: i32, bottom: i32) -> Self {
35        Self {
36            left,
37            top,
38            right,
39            bottom,
40        }
41    }
42
43    /// Construct a `Rect` from its top-left corner and its size.
44    #[inline]
45    pub const fn from_origin_size(left: i32, top: i32, width: i32, height: i32) -> Self {
46        Self {
47            left,
48            top,
49            right: left + width,
50            bottom: top + height,
51        }
52    }
53
54    #[inline]
55    pub const fn width(&self) -> i32 {
56        self.right - self.left
57    }
58
59    #[inline]
60    pub const fn height(&self) -> i32 {
61        self.bottom - self.top
62    }
63
64    /// True if `point` lies inside the rectangle (inclusive left/top,
65    /// exclusive right/bottom — matches Windows `PtInRect`).
66    #[inline]
67    pub const fn contains(&self, point: Point) -> bool {
68        point.x >= self.left
69            && point.x < self.right
70            && point.y >= self.top
71            && point.y < self.bottom
72    }
73}
74
75/// A single connected display in the virtual-screen coordinate system.
76#[derive(Clone, Debug, PartialEq)]
77pub struct MonitorInfo {
78    /// Stable-for-session identifier. On Windows this is the device
79    /// name reported by `GetMonitorInfoW` — for example `\\.\DISPLAY1`.
80    /// Stable across process runs on the same session; **not**
81    /// guaranteed to be stable across reboots or hardware changes.
82    pub id: String,
83
84    /// Human-readable display name (same as `id` on the current Windows
85    /// backend; kept as a distinct field so future backends can supply
86    /// a friendlier label).
87    pub name: String,
88
89    /// Full monitor bounds in virtual-screen coordinates. May contain
90    /// negative left/top when the monitor is arranged to the left of
91    /// or above the primary monitor.
92    pub bounds: Rect,
93
94    /// Bounds excluding the taskbar and any other reserved-space
95    /// widgets. This is what icons should honour when the user has a
96    /// visible taskbar.
97    pub work_area: Rect,
98
99    /// True for the monitor that owns virtual coordinate `(0, 0)`.
100    pub is_primary: bool,
101
102    /// Effective DPI scale factor for this monitor, expressed as a
103    /// multiplier (`1.0` = 96 DPI, `1.5` = 144 DPI, `2.0` = 192 DPI,
104    /// …).
105    ///
106    /// This value is reported in the **calling process's coordinate
107    /// space**. A DPI-unaware process on a HiDPI monitor sees
108    /// scale = 1.0 (Windows virtualises the coordinates so 96 DPI is
109    /// always accurate for what the process draws); a per-monitor-DPI-
110    /// aware process sees the true scale. Since icon coordinates use
111    /// the same space, callers can multiply/divide safely without a
112    /// second correction.
113    ///
114    /// `1.0` on backends that do not implement per-monitor DPI.
115    pub scale_factor: f32,
116}
117
118impl MonitorInfo {
119    pub fn resolution(&self) -> Point {
120        Point::new(self.bounds.width(), self.bounds.height())
121    }
122
123    pub fn dpi(&self) -> f32 {
124        self.scale_factor * 96.0
125    }
126
127    /// True if the given point (in virtual-screen coordinates) lies
128    /// inside this monitor's full bounds.
129    #[inline]
130    pub fn contains(&self, point: Point) -> bool {
131        self.bounds.contains(point)
132    }
133}
134
135#[cfg(test)]
136mod tests {
137    use super::*;
138
139    #[test]
140    fn rect_from_origin_size_matches_edges() {
141        let r = Rect::from_origin_size(-100, -50, 1920, 1080);
142        assert_eq!(r.left, -100);
143        assert_eq!(r.top, -50);
144        assert_eq!(r.right, 1820);
145        assert_eq!(r.bottom, 1030);
146        assert_eq!(r.width(), 1920);
147        assert_eq!(r.height(), 1080);
148    }
149
150    #[test]
151    fn rect_contains_half_open() {
152        let r = Rect::new(0, 0, 100, 100);
153        assert!(r.contains(Point::new(0, 0))); // top-left is inclusive
154        assert!(r.contains(Point::new(99, 99)));
155        assert!(!r.contains(Point::new(100, 50))); // right is exclusive
156        assert!(!r.contains(Point::new(50, 100))); // bottom is exclusive
157        assert!(!r.contains(Point::new(-1, 50)));
158    }
159
160    #[test]
161    fn monitor_contains_delegates_to_bounds() {
162        let m = MonitorInfo {
163            id: "\\.\\DISPLAY2".into(),
164            name: "\\.\\DISPLAY2".into(),
165            bounds: Rect::from_origin_size(-1920, 0, 1920, 1080),
166            work_area: Rect::from_origin_size(-1920, 0, 1920, 1040),
167            is_primary: false,
168            scale_factor: 1.0,
169        };
170        assert!(m.contains(Point::new(-100, 100)));
171        assert!(!m.contains(Point::new(100, 100)));
172    }
173}