1use pixelcoords_core::geometry::Point;
17use pixelcoords_core::session::MonitorRecord;
18use pixelcoords_core::space::{Platform, Resolved, Units, logical_of};
19use serde::{Deserialize, Serialize};
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
23#[serde(rename_all = "lowercase")]
24pub enum Space {
25 Auto,
28 Physical,
30 Logical,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
37pub struct ResolvedPoint {
38 pub x: f64,
39 pub y: f64,
40 pub space: Space,
41 pub monitor: usize,
42 pub scale: f64,
43}
44
45const fn native_platform() -> Platform {
47 if cfg!(target_os = "macos") {
48 return Platform::MacOs;
49 }
50 if cfg!(target_os = "windows") {
51 return Platform::Windows;
52 }
53 Platform::Linux
54}
55
56pub const fn native_space() -> Space {
65 match Units::Auto.resolve(native_platform()) {
66 Resolved::Logical => Space::Logical,
67 Resolved::Physical => Space::Physical,
68 }
69}
70
71pub fn monitor_at(
77 monitors: &[MonitorRecord],
78 global_x: i32,
79 global_y: i32,
80) -> Option<&MonitorRecord> {
81 monitors.iter().find(|m| {
82 let right = m.origin_px.x + m.size_px.w;
83 let bottom = m.origin_px.y + m.size_px.h;
84 global_x >= m.origin_px.x
85 && global_x < right
86 && global_y >= m.origin_px.y
87 && global_y < bottom
88 })
89}
90
91pub fn to_space(
110 monitors: &[MonitorRecord],
111 global_x: i32,
112 global_y: i32,
113 space: Space,
114) -> Option<ResolvedPoint> {
115 let monitor = monitor_at(monitors, global_x, global_y)?;
116 let resolved = match space {
117 Space::Auto => native_space(),
118 other => other,
119 };
120 let physical = Point::new(global_x, global_y);
121 let point = match resolved {
122 Space::Logical => logical_of(physical, monitor.scale),
123 _ => physical,
124 };
125 Some(ResolvedPoint {
126 x: f64::from(point.x),
127 y: f64::from(point.y),
128 space: resolved,
129 monitor: monitor.index,
130 scale: monitor.scale,
131 })
132}
133
134pub fn near_screen_corner(
152 monitors: &[MonitorRecord],
153 space: Space,
154 x: f64,
155 y: f64,
156 margin: f64,
157) -> bool {
158 corners(monitors, space)
159 .iter()
160 .any(|corner| (corner.x - x).abs() <= margin && (corner.y - y).abs() <= margin)
161}
162
163pub fn corners(monitors: &[MonitorRecord], space: Space) -> Vec<ResolvedPoint> {
169 let mut corners = Vec::with_capacity(monitors.len() * 4);
170 for monitor in monitors {
171 let left = monitor.origin_px.x;
172 let top = monitor.origin_px.y;
173 let right = left + monitor.size_px.w - 1;
174 let bottom = top + monitor.size_px.h - 1;
175 for (x, y) in [(left, top), (right, top), (left, bottom), (right, bottom)] {
176 if let Some(point) = to_space(monitors, x, y, space) {
177 corners.push(point);
178 }
179 }
180 }
181 corners
182}
183
184#[cfg(test)]
185mod tests {
186 use pixelcoords_core::geometry::{Point, Size};
187
188 use super::*;
189
190 fn monitor(index: usize, origin: (i32, i32), size: (i32, i32), scale: f64) -> MonitorRecord {
191 MonitorRecord {
192 index,
193 name: format!("display {index}"),
194 primary: index == 0,
195 origin_px: Point::new(origin.0, origin.1),
196 size_px: Size::new(size.0, size.1),
197 scale,
198 }
199 }
200
201 #[test]
202 fn every_monitor_contributes_four_corners() {
203 let monitors = retina_plus_external();
204 assert_eq!(
205 corners(&monitors, Space::Physical).len(),
206 monitors.len() * 4
207 );
208 }
209
210 #[test]
211 fn a_cursor_slammed_into_a_corner_trips_the_kill_switch() {
212 let monitors = vec![monitor(0, (0, 0), (3024, 1964), 2.0)];
213 assert!(near_screen_corner(
216 &monitors,
217 Space::Logical,
218 1511.5,
219 981.5,
220 10.0
221 ));
222 assert!(near_screen_corner(
223 &monitors,
224 Space::Logical,
225 0.0,
226 0.0,
227 10.0
228 ));
229 assert!(near_screen_corner(
231 &monitors,
232 Space::Logical,
233 1503.0,
234 8.0,
235 10.0
236 ));
237 }
238
239 #[test]
240 fn the_middle_of_the_screen_is_not_a_corner() {
241 let monitors = vec![monitor(0, (0, 0), (3024, 1964), 2.0)];
242 assert!(!near_screen_corner(
243 &monitors,
244 Space::Logical,
245 700.0,
246 500.0,
247 10.0
248 ));
249 assert!(!near_screen_corner(
251 &monitors,
252 Space::Logical,
253 700.0,
254 0.0,
255 10.0
256 ));
257 }
258
259 #[test]
260 fn a_corner_of_any_monitor_counts_not_just_the_primary() {
261 let monitors = retina_plus_external();
262 let external = &monitors[1];
263 let corner = to_space(
264 &monitors,
265 external.origin_px.x + external.size_px.w - 1,
266 external.origin_px.y + external.size_px.h - 1,
267 Space::Physical,
268 )
269 .expect("a monitor contains its own corner");
270 assert!(near_screen_corner(
271 &monitors,
272 Space::Physical,
273 corner.x,
274 corner.y,
275 2.0
276 ));
277 }
278
279 fn retina_plus_external() -> Vec<MonitorRecord> {
280 vec![
281 monitor(0, (0, 0), (3024, 1964), 2.0),
282 monitor(1, (3024, 0), (1920, 1080), 1.0),
283 ]
284 }
285
286 #[test]
287 fn logical_conversion_divides_by_the_containing_monitors_scale() {
288 let monitors = retina_plus_external();
289 let point = to_space(&monitors, 1624, 880, Space::Logical).expect("inside monitor 0");
290 assert_eq!(point.monitor, 0);
291 assert!((point.x - 812.0).abs() < f64::EPSILON);
292 assert!((point.y - 440.0).abs() < f64::EPSILON);
293 }
294
295 #[test]
296 fn a_mixed_dpi_layout_converts_per_monitor_not_per_desktop() {
297 let monitors = retina_plus_external();
298 let point = to_space(&monitors, 4000, 500, Space::Logical).expect("inside monitor 1");
300 assert_eq!(point.monitor, 1);
301 assert!((point.x - 4000.0).abs() < f64::EPSILON);
302 assert!((point.y - 500.0).abs() < f64::EPSILON);
303 }
304
305 #[test]
306 fn physical_space_is_the_identity() {
307 let monitors = retina_plus_external();
308 let point = to_space(&monitors, 1624, 880, Space::Physical).expect("inside monitor 0");
309 assert!((point.x - 1624.0).abs() < f64::EPSILON);
310 assert!((point.y - 880.0).abs() < f64::EPSILON);
311 assert_eq!(point.space, Space::Physical);
312 }
313
314 #[test]
315 fn a_point_in_a_gap_between_monitors_resolves_to_nothing() {
316 let monitors = vec![
319 monitor(0, (0, 0), (1920, 1080), 1.0),
320 monitor(1, (2000, 0), (1920, 1080), 1.0),
321 ];
322 assert!(monitor_at(&monitors, 1960, 500).is_none());
323 assert!(to_space(&monitors, 1960, 500, Space::Auto).is_none());
324 }
325
326 #[test]
327 fn monitor_bounds_are_half_open_so_edges_belong_to_exactly_one() {
328 let monitors = retina_plus_external();
329 assert_eq!(monitor_at(&monitors, 3023, 0).expect("m0").index, 0);
331 assert_eq!(monitor_at(&monitors, 3024, 0).expect("m1").index, 1);
332 }
333
334 #[test]
335 fn auto_resolves_to_the_platforms_own_space() {
336 let monitors = retina_plus_external();
337 let point = to_space(&monitors, 1624, 880, Space::Auto).expect("inside monitor 0");
338 assert_eq!(point.space, native_space());
339 assert_ne!(point.space, Space::Auto);
341 }
342
343 #[test]
344 fn negative_origins_are_handled() {
345 let monitors = vec![
347 monitor(0, (0, 0), (1920, 1080), 1.0),
348 monitor(1, (-1920, 0), (1920, 1080), 2.0),
349 ];
350 let point = to_space(&monitors, -960, 500, Space::Logical).expect("inside monitor 1");
351 assert_eq!(point.monitor, 1);
352 assert!((point.x - -480.0).abs() < f64::EPSILON);
353 }
354}