1use pixelcoords_core::session::MonitorRecord;
24use serde::Serialize;
25
26#[derive(Debug, Clone, PartialEq, Serialize)]
33pub struct Region {
34 pub offset_x: i32,
35 pub offset_y: i32,
36 pub width: i32,
37 pub height: i32,
38 pub scale: f64,
39 pub mapping_id: Option<String>,
42}
43
44impl Region {
45 fn logical_size_of(monitor: &MonitorRecord) -> (i32, i32) {
48 let scale = if monitor.scale > 0.0 {
49 monitor.scale
50 } else {
51 1.0
52 };
53 (
54 (f64::from(monitor.size_px.w) / scale).round() as i32,
55 (f64::from(monitor.size_px.h) / scale).round() as i32,
56 )
57 }
58
59 fn covers(&self, monitor: &MonitorRecord) -> bool {
66 Self::logical_size_of(monitor) == (self.width, self.height)
67 }
68
69 #[cfg(test)]
76 fn contains(&self, x: f64, y: f64) -> bool {
77 let left = f64::from(self.offset_x);
78 let top = f64::from(self.offset_y);
79 x >= left
80 && y >= top
81 && x < left + f64::from(self.width)
82 && y < top + f64::from(self.height)
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Serialize)]
88pub struct Placement {
89 pub region: usize,
91 pub x: f64,
92 pub y: f64,
93}
94
95#[derive(Debug, Clone, PartialEq)]
103pub enum PlaceError {
104 OutsideEveryMonitor { x: i32, y: i32 },
106 NoRegionForMonitor { monitor: usize },
109 AmbiguousRegion { monitor: usize, matches: usize },
112}
113
114impl std::fmt::Display for PlaceError {
115 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
116 match self {
117 Self::OutsideEveryMonitor { x, y } => write!(
118 f,
119 "the point ({x}, {y}) is not on any monitor the session describes"
120 ),
121 Self::NoRegionForMonitor { monitor } => write!(
122 f,
123 "no shared region matches monitor {monitor} — the grant covers a \
124 different screen, so re-run and share the screen the region is on"
125 ),
126 Self::AmbiguousRegion { monitor, matches } => write!(
127 f,
128 "{matches} shared regions match monitor {monitor}, so which one to \
129 aim at would be a guess — share a single screen instead"
130 ),
131 }
132 }
133}
134
135impl std::error::Error for PlaceError {}
136
137pub fn place(
146 monitors: &[MonitorRecord],
147 regions: &[Region],
148 global_x: i32,
149 global_y: i32,
150) -> Result<Placement, PlaceError> {
151 let Some(monitor) = crate::convert::monitor_at(monitors, global_x, global_y) else {
152 return Err(PlaceError::OutsideEveryMonitor {
153 x: global_x,
154 y: global_y,
155 });
156 };
157 let mut matching = regions
158 .iter()
159 .enumerate()
160 .filter(|(_, region)| region.covers(monitor));
161 let Some((index, region)) = matching.next() else {
162 return Err(PlaceError::NoRegionForMonitor {
163 monitor: monitor.index,
164 });
165 };
166 let extra = matching.count();
167 if extra > 0 {
168 return Err(PlaceError::AmbiguousRegion {
169 monitor: monitor.index,
170 matches: extra + 1,
171 });
172 }
173
174 let scale = if monitor.scale > 0.0 {
175 monitor.scale
176 } else {
177 1.0
178 };
179 let x = f64::from(region.offset_x) + f64::from(global_x - monitor.origin_px.x) / scale;
185 let y = f64::from(region.offset_y) + f64::from(global_y - monitor.origin_px.y) / scale;
186 Ok(Placement {
187 region: index,
188 x,
189 y,
190 })
191}
192
193pub fn unplace(
202 monitors: &[MonitorRecord],
203 regions: &[Region],
204 placement: Placement,
205) -> Option<(f64, f64)> {
206 let region = regions.get(placement.region)?;
207 let monitor = monitors.iter().find(|monitor| region.covers(monitor))?;
208 let scale = if monitor.scale > 0.0 {
209 monitor.scale
210 } else {
211 1.0
212 };
213 Some((
214 f64::from(monitor.origin_px.x) + (placement.x - f64::from(region.offset_x)) * scale,
215 f64::from(monitor.origin_px.y) + (placement.y - f64::from(region.offset_y)) * scale,
216 ))
217}
218
219#[cfg(test)]
220mod tests {
221 use pixelcoords_core::geometry::{Point, Size};
222
223 use super::*;
224
225 fn monitor(index: usize, origin: (i32, i32), size: (i32, i32), scale: f64) -> MonitorRecord {
226 MonitorRecord {
227 index,
228 name: format!("display {index}"),
229 primary: index == 0,
230 origin_px: Point::new(origin.0, origin.1),
231 size_px: Size::new(size.0, size.1),
232 scale,
233 }
234 }
235
236 fn region(offset: (i32, i32), size: (i32, i32), scale: f64) -> Region {
237 Region {
238 offset_x: offset.0,
239 offset_y: offset.1,
240 width: size.0,
241 height: size.1,
242 scale,
243 mapping_id: None,
244 }
245 }
246
247 #[test]
252 fn a_single_unscaled_monitor_maps_one_to_one() {
253 let monitors = vec![monitor(0, (0, 0), (1800, 1130), 1.0)];
254 let regions = vec![region((0, 0), (1800, 1130), 1.0)];
255 let placed = place(&monitors, ®ions, 900, 565).expect("centre of the only screen");
256 assert_eq!(placed.region, 0);
257 assert!((placed.x - 900.0).abs() < f64::EPSILON);
258 assert!((placed.y - 565.0).abs() < f64::EPSILON);
259 }
260
261 #[test]
262 fn a_scaled_monitor_divides_by_the_sessions_scale() {
263 let monitors = vec![monitor(0, (0, 0), (3600, 2260), 2.0)];
266 let regions = vec![region((0, 0), (1800, 1130), 2.0)];
267 let placed = place(&monitors, ®ions, 1624, 880).expect("inside");
268 assert!((placed.x - 812.0).abs() < f64::EPSILON);
269 assert!((placed.y - 440.0).abs() < f64::EPSILON);
270 }
271
272 #[test]
275 fn a_region_scale_of_one_on_a_retina_screen_does_not_move_the_click() {
276 let monitors = vec![monitor(0, (0, 0), (3600, 2260), 2.0)];
277 let honest = vec![region((0, 0), (1800, 1130), 2.0)];
278 let understated = vec![region((0, 0), (1800, 1130), 1.0)];
279 assert_eq!(
280 place(&monitors, &honest, 1624, 880).expect("honest"),
281 place(&monitors, &understated, 1624, 880).expect("understated")
282 );
283 }
284
285 #[test]
286 fn a_mixed_dpi_layout_uses_each_monitors_own_scale() {
287 let monitors = vec![
288 monitor(0, (0, 0), (3600, 2260), 2.0),
289 monitor(1, (3600, 0), (1920, 1080), 1.0),
290 ];
291 let regions = vec![
293 region((0, 0), (1800, 1130), 2.0),
294 region((1800, 0), (1920, 1080), 1.0),
295 ];
296 let on_retina = place(&monitors, ®ions, 1624, 880).expect("monitor 0");
297 assert_eq!(on_retina.region, 0);
298 assert!((on_retina.x - 812.0).abs() < f64::EPSILON);
299
300 let on_external = place(&monitors, ®ions, 4000, 500).expect("monitor 1");
301 assert_eq!(on_external.region, 1);
302 assert!((on_external.x - 2200.0).abs() < f64::EPSILON);
304 assert!((on_external.y - 500.0).abs() < f64::EPSILON);
305 }
306
307 #[test]
308 fn a_display_left_of_primary_carries_negative_origins() {
309 let monitors = vec![
310 monitor(0, (0, 0), (1920, 1080), 1.0),
311 monitor(1, (-2560, 0), (2560, 1440), 2.0),
312 ];
313 let regions = vec![
314 region((0, 0), (1920, 1080), 1.0),
315 region((-1280, 0), (1280, 720), 2.0),
316 ];
317 let placed = place(&monitors, ®ions, -1280, 720).expect("inside monitor 1");
318 assert_eq!(placed.region, 1);
319 assert!((placed.x - -640.0).abs() < f64::EPSILON);
321 assert!((placed.y - 360.0).abs() < f64::EPSILON);
322 }
323
324 #[test]
325 fn a_point_off_every_monitor_is_refused() {
326 let monitors = vec![monitor(0, (0, 0), (1800, 1130), 1.0)];
327 let regions = vec![region((0, 0), (1800, 1130), 1.0)];
328 assert_eq!(
329 place(&monitors, ®ions, 5000, 5000),
330 Err(PlaceError::OutsideEveryMonitor { x: 5000, y: 5000 })
331 );
332 }
333
334 #[test]
337 fn sharing_the_wrong_screen_is_refused_by_name() {
338 let monitors = vec![
339 monitor(0, (0, 0), (1920, 1080), 1.0),
340 monitor(1, (1920, 0), (2560, 1440), 1.0),
341 ];
342 let regions = vec![region((0, 0), (1920, 1080), 1.0)];
343 assert_eq!(
344 place(&monitors, ®ions, 3000, 700),
345 Err(PlaceError::NoRegionForMonitor { monitor: 1 })
346 );
347 }
348
349 #[test]
350 fn two_identical_screens_shared_at_once_are_ambiguous_not_guessed() {
351 let monitors = vec![
352 monitor(0, (0, 0), (1920, 1080), 1.0),
353 monitor(1, (1920, 0), (1920, 1080), 1.0),
354 ];
355 let regions = vec![
357 region((0, 0), (1920, 1080), 1.0),
358 region((1920, 0), (1920, 1080), 1.0),
359 ];
360 assert_eq!(
361 place(&monitors, ®ions, 100, 100),
362 Err(PlaceError::AmbiguousRegion {
363 monitor: 0,
364 matches: 2
365 })
366 );
367 }
368
369 #[test]
370 fn no_regions_at_all_is_refused() {
371 let monitors = vec![monitor(0, (0, 0), (1800, 1130), 1.0)];
372 assert_eq!(
373 place(&monitors, &[], 100, 100),
374 Err(PlaceError::NoRegionForMonitor { monitor: 0 })
375 );
376 }
377
378 #[test]
382 fn every_point_on_a_covered_monitor_lands_inside_its_region() {
383 let cases = [
384 ((0, 0), (1800, 1130), 1.0, (0, 0)),
385 ((0, 0), (3600, 2260), 2.0, (0, 0)),
386 ((-2560, -100), (2560, 1440), 2.0, (-1280, -50)),
387 ((0, 0), (1801, 1131), 2.0, (0, 0)),
389 ((0, 0), (1799, 1129), 2.0, (400, 300)),
390 ((0, 0), (1000, 800), 0.5, (0, 0)),
392 ];
393 for (origin, size, scale, offset) in cases {
394 let monitors = vec![monitor(0, origin, size, scale)];
395 let logical = Region::logical_size_of(&monitors[0]);
396 let regions = vec![region(offset, (logical.0, logical.1), scale)];
397 let corners = [
399 (origin.0, origin.1),
400 (origin.0 + size.0 - 1, origin.1),
401 (origin.0, origin.1 + size.1 - 1),
402 (origin.0 + size.0 - 1, origin.1 + size.1 - 1),
403 ];
404 for (x, y) in corners {
405 let placed = place(&monitors, ®ions, x, y)
406 .unwrap_or_else(|error| panic!("({x}, {y}) on {size:?}@{scale}: {error}"));
407 assert!(
408 regions[0].contains(placed.x, placed.y),
409 "({x}, {y}) on {size:?}@{scale} mapped to ({}, {}), outside {:?}",
410 placed.x,
411 placed.y,
412 regions[0]
413 );
414 }
415 }
416 }
417
418 #[test]
419 fn the_far_edge_belongs_to_the_region_but_one_past_it_does_not() {
420 let monitors = vec![monitor(0, (0, 0), (1800, 1130), 1.0)];
421 let regions = vec![region((0, 0), (1800, 1130), 1.0)];
422 assert!(place(&monitors, ®ions, 1799, 1129).is_ok());
423 assert_eq!(
425 place(&monitors, ®ions, 1800, 0),
426 Err(PlaceError::OutsideEveryMonitor { x: 1800, y: 0 })
427 );
428 }
429
430 #[test]
431 fn a_nonsense_scale_is_treated_as_one_rather_than_dividing_by_zero() {
432 let monitors = vec![monitor(0, (0, 0), (1800, 1130), 0.0)];
433 let regions = vec![region((0, 0), (1800, 1130), 1.0)];
434 let placed = place(&monitors, ®ions, 900, 565).expect("scale 0 falls back to 1");
435 assert!((placed.x - 900.0).abs() < f64::EPSILON);
436 }
437
438 #[test]
439 fn unplace_returns_the_physical_pixel_it_came_from() {
440 let monitors = vec![
441 monitor(0, (0, 0), (3600, 2260), 2.0),
442 monitor(1, (3600, 0), (1920, 1080), 1.0),
443 ];
444 let regions = vec![
445 region((0, 0), (1800, 1130), 2.0),
446 region((1800, 0), (1920, 1080), 1.0),
447 ];
448 for (x, y) in [(1624, 880), (4000, 500), (0, 0)] {
449 let placed = place(&monitors, ®ions, x, y).expect("inside");
450 let (back_x, back_y) = unplace(&monitors, ®ions, placed).expect("reversible");
451 assert!((back_x - f64::from(x)).abs() < 1e-9, "x for ({x}, {y})");
452 assert!((back_y - f64::from(y)).abs() < 1e-9, "y for ({x}, {y})");
453 }
454 }
455
456 #[test]
457 fn unplace_of_an_unknown_region_is_nothing_rather_than_a_panic() {
458 let monitors = vec![monitor(0, (0, 0), (1800, 1130), 1.0)];
459 let regions = vec![region((0, 0), (1800, 1130), 1.0)];
460 let bogus = Placement {
461 region: 7,
462 x: 0.0,
463 y: 0.0,
464 };
465 assert!(unplace(&monitors, ®ions, bogus).is_none());
466 }
467
468 #[test]
469 fn the_refusals_all_say_something_useful() {
470 let messages = [
471 PlaceError::OutsideEveryMonitor { x: 1, y: 2 }.to_string(),
472 PlaceError::NoRegionForMonitor { monitor: 1 }.to_string(),
473 PlaceError::AmbiguousRegion {
474 monitor: 0,
475 matches: 2,
476 }
477 .to_string(),
478 ];
479 for message in messages {
480 assert!(message.len() > 30, "too terse: {message}");
481 }
482 }
483}