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}