1use pixelcoords_core::session::MonitorRecord;
17use serde::{Deserialize, Serialize};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(rename_all = "lowercase")]
22pub enum Space {
23 Auto,
26 Physical,
28 Logical,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
35pub struct ResolvedPoint {
36 pub x: f64,
37 pub y: f64,
38 pub space: Space,
39 pub monitor: usize,
40 pub scale: f64,
41}
42
43pub const fn native_space() -> Space {
46 if cfg!(target_os = "macos") {
47 return Space::Logical;
48 }
49 Space::Physical
50}
51
52pub fn monitor_at(
58 monitors: &[MonitorRecord],
59 global_x: i32,
60 global_y: i32,
61) -> Option<&MonitorRecord> {
62 monitors.iter().find(|m| {
63 let right = m.origin_px.x + m.size_px.w;
64 let bottom = m.origin_px.y + m.size_px.h;
65 global_x >= m.origin_px.x
66 && global_x < right
67 && global_y >= m.origin_px.y
68 && global_y < bottom
69 })
70}
71
72pub fn to_space(
78 monitors: &[MonitorRecord],
79 global_x: i32,
80 global_y: i32,
81 space: Space,
82) -> Option<ResolvedPoint> {
83 let monitor = monitor_at(monitors, global_x, global_y)?;
84 let resolved = match space {
85 Space::Auto => native_space(),
86 other => other,
87 };
88 let divisor = match resolved {
89 Space::Logical => monitor.scale,
90 _ => 1.0,
91 };
92 Some(ResolvedPoint {
93 x: f64::from(global_x) / divisor,
94 y: f64::from(global_y) / divisor,
95 space: resolved,
96 monitor: monitor.index,
97 scale: monitor.scale,
98 })
99}
100
101pub fn near_screen_corner(
119 monitors: &[MonitorRecord],
120 space: Space,
121 x: f64,
122 y: f64,
123 margin: f64,
124) -> bool {
125 corners(monitors, space)
126 .iter()
127 .any(|corner| (corner.x - x).abs() <= margin && (corner.y - y).abs() <= margin)
128}
129
130pub fn corners(monitors: &[MonitorRecord], space: Space) -> Vec<ResolvedPoint> {
136 let mut corners = Vec::with_capacity(monitors.len() * 4);
137 for monitor in monitors {
138 let left = monitor.origin_px.x;
139 let top = monitor.origin_px.y;
140 let right = left + monitor.size_px.w - 1;
141 let bottom = top + monitor.size_px.h - 1;
142 for (x, y) in [(left, top), (right, top), (left, bottom), (right, bottom)] {
143 if let Some(point) = to_space(monitors, x, y, space) {
144 corners.push(point);
145 }
146 }
147 }
148 corners
149}
150
151#[cfg(test)]
152mod tests {
153 use pixelcoords_core::geometry::{Point, Size};
154
155 use super::*;
156
157 fn monitor(index: usize, origin: (i32, i32), size: (i32, i32), scale: f64) -> MonitorRecord {
158 MonitorRecord {
159 index,
160 name: format!("display {index}"),
161 primary: index == 0,
162 origin_px: Point::new(origin.0, origin.1),
163 size_px: Size::new(size.0, size.1),
164 scale,
165 }
166 }
167
168 #[test]
169 fn every_monitor_contributes_four_corners() {
170 let monitors = retina_plus_external();
171 assert_eq!(
172 corners(&monitors, Space::Physical).len(),
173 monitors.len() * 4
174 );
175 }
176
177 #[test]
178 fn a_cursor_slammed_into_a_corner_trips_the_kill_switch() {
179 let monitors = vec![monitor(0, (0, 0), (3024, 1964), 2.0)];
180 assert!(near_screen_corner(
183 &monitors,
184 Space::Logical,
185 1511.5,
186 981.5,
187 10.0
188 ));
189 assert!(near_screen_corner(
190 &monitors,
191 Space::Logical,
192 0.0,
193 0.0,
194 10.0
195 ));
196 assert!(near_screen_corner(
198 &monitors,
199 Space::Logical,
200 1503.0,
201 8.0,
202 10.0
203 ));
204 }
205
206 #[test]
207 fn the_middle_of_the_screen_is_not_a_corner() {
208 let monitors = vec![monitor(0, (0, 0), (3024, 1964), 2.0)];
209 assert!(!near_screen_corner(
210 &monitors,
211 Space::Logical,
212 700.0,
213 500.0,
214 10.0
215 ));
216 assert!(!near_screen_corner(
218 &monitors,
219 Space::Logical,
220 700.0,
221 0.0,
222 10.0
223 ));
224 }
225
226 #[test]
227 fn a_corner_of_any_monitor_counts_not_just_the_primary() {
228 let monitors = retina_plus_external();
229 let external = &monitors[1];
230 let corner = to_space(
231 &monitors,
232 external.origin_px.x + external.size_px.w - 1,
233 external.origin_px.y + external.size_px.h - 1,
234 Space::Physical,
235 )
236 .expect("a monitor contains its own corner");
237 assert!(near_screen_corner(
238 &monitors,
239 Space::Physical,
240 corner.x,
241 corner.y,
242 2.0
243 ));
244 }
245
246 fn retina_plus_external() -> Vec<MonitorRecord> {
247 vec![
248 monitor(0, (0, 0), (3024, 1964), 2.0),
249 monitor(1, (3024, 0), (1920, 1080), 1.0),
250 ]
251 }
252
253 #[test]
254 fn logical_conversion_divides_by_the_containing_monitors_scale() {
255 let monitors = retina_plus_external();
256 let point = to_space(&monitors, 1624, 880, Space::Logical).expect("inside monitor 0");
257 assert_eq!(point.monitor, 0);
258 assert!((point.x - 812.0).abs() < f64::EPSILON);
259 assert!((point.y - 440.0).abs() < f64::EPSILON);
260 }
261
262 #[test]
263 fn a_mixed_dpi_layout_converts_per_monitor_not_per_desktop() {
264 let monitors = retina_plus_external();
265 let point = to_space(&monitors, 4000, 500, Space::Logical).expect("inside monitor 1");
267 assert_eq!(point.monitor, 1);
268 assert!((point.x - 4000.0).abs() < f64::EPSILON);
269 assert!((point.y - 500.0).abs() < f64::EPSILON);
270 }
271
272 #[test]
273 fn physical_space_is_the_identity() {
274 let monitors = retina_plus_external();
275 let point = to_space(&monitors, 1624, 880, Space::Physical).expect("inside monitor 0");
276 assert!((point.x - 1624.0).abs() < f64::EPSILON);
277 assert!((point.y - 880.0).abs() < f64::EPSILON);
278 assert_eq!(point.space, Space::Physical);
279 }
280
281 #[test]
282 fn a_point_in_a_gap_between_monitors_resolves_to_nothing() {
283 let monitors = vec![
286 monitor(0, (0, 0), (1920, 1080), 1.0),
287 monitor(1, (2000, 0), (1920, 1080), 1.0),
288 ];
289 assert!(monitor_at(&monitors, 1960, 500).is_none());
290 assert!(to_space(&monitors, 1960, 500, Space::Auto).is_none());
291 }
292
293 #[test]
294 fn monitor_bounds_are_half_open_so_edges_belong_to_exactly_one() {
295 let monitors = retina_plus_external();
296 assert_eq!(monitor_at(&monitors, 3023, 0).expect("m0").index, 0);
298 assert_eq!(monitor_at(&monitors, 3024, 0).expect("m1").index, 1);
299 }
300
301 #[test]
302 fn auto_resolves_to_the_platforms_own_space() {
303 let monitors = retina_plus_external();
304 let point = to_space(&monitors, 1624, 880, Space::Auto).expect("inside monitor 0");
305 assert_eq!(point.space, native_space());
306 assert_ne!(point.space, Space::Auto);
308 }
309
310 #[test]
311 fn negative_origins_are_handled() {
312 let monitors = vec![
314 monitor(0, (0, 0), (1920, 1080), 1.0),
315 monitor(1, (-1920, 0), (1920, 1080), 2.0),
316 ];
317 let point = to_space(&monitors, -960, 500, Space::Logical).expect("inside monitor 1");
318 assert_eq!(point.monitor, 1);
319 assert!((point.x - -480.0).abs() < f64::EPSILON);
320 }
321}