use core_graphics::display::{CGDirectDisplayID, CGDisplay};
use serde::{Deserialize, Serialize};
use crate::error::{AXError, AXResult};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct Rect {
pub x: f64,
pub y: f64,
pub width: f64,
pub height: f64,
}
impl Rect {
#[must_use]
pub fn contains_point(&self, px: f64, py: f64) -> bool {
px >= self.x && px < self.x + self.width && py >= self.y && py < self.y + self.height
}
#[must_use]
pub fn union(self, other: Rect) -> Rect {
let x = self.x.min(other.x);
let y = self.y.min(other.y);
let right = (self.x + self.width).max(other.x + other.width);
let bottom = (self.y + self.height).max(other.y + other.height);
Rect {
x,
y,
width: right - x,
height: bottom - y,
}
}
#[must_use]
pub fn intersects(&self, other: &Rect) -> bool {
self.x < other.x + other.width
&& self.x + self.width > other.x
&& self.y < other.y + other.height
&& self.y + self.height > other.y
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Display {
pub id: u32,
pub bounds: Rect,
pub scale_factor: f64,
pub is_primary: bool,
}
pub fn list_displays() -> AXResult<Vec<Display>> {
let ids = CGDisplay::active_displays()
.map_err(|code| AXError::SystemError(format!("CGGetActiveDisplayList failed: {code}")))?;
ids.into_iter().map(build_display).collect()
}
#[must_use]
pub fn display_for_point(x: f64, y: f64, displays: &[Display]) -> Option<&Display> {
displays.iter().find(|d| d.bounds.contains_point(x, y))
}
#[must_use]
pub fn displays_for_rect<'d>(window_bounds: &Rect, displays: &'d [Display]) -> Vec<&'d Display> {
displays
.iter()
.filter(|d| d.bounds.intersects(window_bounds))
.collect()
}
#[must_use]
pub fn global_to_local(x: f64, y: f64, displays: &[Display]) -> Option<(f64, f64)> {
display_for_point(x, y, displays).map(|d| (x - d.bounds.x, y - d.bounds.y))
}
#[must_use]
pub fn local_to_global(
display_id: u32,
local_x: f64,
local_y: f64,
displays: &[Display],
) -> Option<(f64, f64)> {
displays
.iter()
.find(|d| d.id == display_id)
.map(|d| (local_x + d.bounds.x, local_y + d.bounds.y))
}
#[allow(clippy::unnecessary_wraps)] fn build_display(id: CGDirectDisplayID) -> AXResult<Display> {
let cg = CGDisplay::new(id);
let cg_bounds = cg.bounds();
let bounds = Rect {
x: cg_bounds.origin.x,
y: cg_bounds.origin.y,
width: cg_bounds.size.width,
height: cg_bounds.size.height,
};
let scale_factor = compute_scale_factor(cg, &bounds);
Ok(Display {
id,
bounds,
scale_factor,
is_primary: cg.is_main(),
})
}
fn compute_scale_factor(cg: CGDisplay, logical_bounds: &Rect) -> f64 {
if logical_bounds.width == 0.0 {
return 1.0;
}
#[allow(clippy::cast_precision_loss)]
let physical_width = cg.pixels_wide() as f64;
(physical_width / logical_bounds.width).max(1.0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rect_contains_point_origin() {
let r = Rect {
x: 0.0,
y: 0.0,
width: 1920.0,
height: 1080.0,
};
assert!(r.contains_point(0.0, 0.0));
}
#[test]
fn rect_contains_point_negative_origin() {
let r = Rect {
x: -2560.0,
y: 0.0,
width: 2560.0,
height: 1440.0,
};
assert!(r.contains_point(-1280.0, 720.0));
assert!(!r.contains_point(100.0, 100.0));
}
#[test]
fn rect_contains_point_half_open_right_edge_is_exclusive() {
let r = Rect {
x: 0.0,
y: 0.0,
width: 1920.0,
height: 1080.0,
};
assert!(!r.contains_point(1920.0, 0.0));
assert!(r.contains_point(1919.999, 0.0));
}
#[test]
fn rect_union_two_adjacent_displays() {
let primary = Rect {
x: 0.0,
y: 0.0,
width: 1920.0,
height: 1080.0,
};
let secondary = Rect {
x: -2560.0,
y: 0.0,
width: 2560.0,
height: 1440.0,
};
let u = primary.union(secondary);
assert_eq!(u.x, -2560.0);
assert_eq!(u.y, 0.0);
assert_eq!(u.width, 4480.0);
assert_eq!(u.height, 1440.0);
}
#[test]
fn rect_intersects_overlapping_rects() {
let a = Rect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let b = Rect {
x: 50.0,
y: 50.0,
width: 100.0,
height: 100.0,
};
assert!(a.intersects(&b));
}
#[test]
fn rect_intersects_non_overlapping_rects() {
let a = Rect {
x: 0.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
let b = Rect {
x: 200.0,
y: 0.0,
width: 100.0,
height: 100.0,
};
assert!(!a.intersects(&b));
}
#[test]
fn rect_intersects_touching_edge_is_non_overlapping() {
let a = Rect {
x: 0.0,
y: 0.0,
width: 1920.0,
height: 1080.0,
};
let b = Rect {
x: 1920.0,
y: 0.0,
width: 2560.0,
height: 1440.0,
};
assert!(!a.intersects(&b));
}
#[test]
fn display_for_point_primary_at_origin() {
let displays = synthetic_two_display_layout();
let d = display_for_point(0.0, 0.0, &displays);
assert!(d.is_some());
assert!(d.unwrap().is_primary);
}
#[test]
fn display_for_point_secondary_negative_x() {
let displays = synthetic_two_display_layout();
let d = display_for_point(-1000.0, 500.0, &displays);
assert!(d.is_some());
assert!(!d.unwrap().is_primary);
}
#[test]
fn display_for_point_returns_none_for_gap() {
let displays = vec![
make_display(1, 0.0, 0.0, 1920.0, 1080.0, true),
make_display(2, 2000.0, 0.0, 1920.0, 1080.0, false), ];
let d = display_for_point(1950.0, 100.0, &displays);
assert!(d.is_none());
}
#[test]
fn displays_for_rect_window_on_primary_only() {
let displays = synthetic_two_display_layout();
let window = Rect {
x: 100.0,
y: 100.0,
width: 800.0,
height: 600.0,
};
let found = displays_for_rect(&window, &displays);
assert_eq!(found.len(), 1);
assert!(found[0].is_primary);
}
#[test]
fn displays_for_rect_window_spans_both_displays() {
let displays = synthetic_two_display_layout();
let window = Rect {
x: -100.0,
y: 0.0,
width: 500.0,
height: 400.0,
};
let found = displays_for_rect(&window, &displays);
assert_eq!(found.len(), 2);
}
#[test]
fn global_to_local_primary_origin_maps_to_zero() {
let displays = synthetic_two_display_layout();
let result = global_to_local(0.0, 0.0, &displays);
assert_eq!(result, Some((0.0, 0.0)));
}
#[test]
fn global_to_local_secondary_negative_x() {
let displays = synthetic_two_display_layout();
let result = global_to_local(-2000.0, 100.0, &displays);
assert!(result.is_some());
let (lx, ly) = result.unwrap();
assert!((lx - 560.0).abs() < f64::EPSILON);
assert!((ly - 100.0).abs() < f64::EPSILON);
}
#[test]
fn local_to_global_round_trips_primary() {
let displays = synthetic_two_display_layout();
let primary_id = displays.iter().find(|d| d.is_primary).unwrap().id;
let (gx, gy) = local_to_global(primary_id, 300.0, 400.0, &displays).unwrap();
let (lx, ly) = global_to_local(gx, gy, &displays).unwrap();
assert!((lx - 300.0).abs() < f64::EPSILON);
assert!((ly - 400.0).abs() < f64::EPSILON);
}
#[test]
fn local_to_global_unknown_display_returns_none() {
let displays = synthetic_two_display_layout();
assert!(local_to_global(9999, 0.0, 0.0, &displays).is_none());
}
#[test]
fn compute_scale_factor_degenerate_zero_width_returns_one() {
let cg = CGDisplay::new(0); let bounds = Rect {
x: 0.0,
y: 0.0,
width: 0.0,
height: 0.0,
};
let sf = compute_scale_factor(cg, &bounds);
assert_eq!(sf, 1.0);
}
#[test]
fn list_displays_returns_at_least_one() {
let displays = list_displays().expect("CGGetActiveDisplayList must succeed");
assert!(!displays.is_empty(), "at least one display must be active");
}
#[test]
fn list_displays_has_exactly_one_primary() {
let displays = list_displays().expect("must enumerate displays");
let primaries: Vec<_> = displays.iter().filter(|d| d.is_primary).collect();
assert_eq!(primaries.len(), 1, "exactly one primary display");
}
#[test]
fn list_displays_scale_factor_at_least_one() {
let displays = list_displays().expect("must enumerate displays");
for d in &displays {
assert!(d.scale_factor >= 1.0, "scale factor must be >= 1.0");
}
}
#[test]
fn list_displays_primary_bounds_width_positive() {
let displays = list_displays().expect("must enumerate displays");
let primary = displays.iter().find(|d| d.is_primary).unwrap();
assert!(primary.bounds.width > 0.0);
assert!(primary.bounds.height > 0.0);
}
fn make_display(id: u32, x: f64, y: f64, w: f64, h: f64, primary: bool) -> Display {
Display {
id,
bounds: Rect {
x,
y,
width: w,
height: h,
},
scale_factor: 1.0,
is_primary: primary,
}
}
fn synthetic_two_display_layout() -> Vec<Display> {
vec![
make_display(1, 0.0, 0.0, 1920.0, 1080.0, true),
make_display(2, -2560.0, 0.0, 2560.0, 1440.0, false),
]
}
}