use crate::error::{Error, Result};
use crate::geometry::{Fit, Point, Quad, Rect};
#[derive(Clone, Debug, PartialEq)]
pub struct Monitor {
pub name: String,
pub bounds: Rect,
pub scale_factor: f64,
pub refresh_millihertz: Option<u32>,
pub is_primary: bool,
pub persistent_id: String,
}
impl Monitor {
pub fn new(name: impl Into<String>, bounds: Rect) -> Self {
let name_str = name.into();
let persistent_id = monitor_id(
&name_str,
bounds.x.round() as i32,
bounds.y.round() as i32,
1.0,
);
Monitor {
name: name_str,
bounds,
scale_factor: 1.0,
refresh_millihertz: None,
is_primary: false,
persistent_id,
}
}
pub fn refresh_hz(&self) -> Option<f64> {
self.refresh_millihertz
.map(|millihertz| f64::from(millihertz) / 1000.0)
}
pub fn frame_interval(&self) -> Option<std::time::Duration> {
self.refresh_hz()
.filter(|hz| *hz > 0.0)
.map(|hz| std::time::Duration::from_secs_f64(1.0 / hz))
}
}
pub fn monitor_id(name: &str, x: i32, y: i32, scale_factor: f64) -> String {
if cfg!(target_os = "linux") && !name.is_empty() {
return format!("xrandr_{name}");
}
const OFFSET: u64 = 0xcbf2_9ce4_8422_2325;
const PRIME: u64 = 0x0000_0100_0000_01b3;
let mut hash = OFFSET;
for part in [
name.as_bytes(),
&[0xff],
&x.to_le_bytes(),
&y.to_le_bytes(),
&scale_factor.to_bits().to_le_bytes(),
] {
for byte in part {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(PRIME);
}
}
format!("monitor_{hash:016x}")
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MonitorSelector {
Primary,
Index(usize),
Name(String),
ContainingPoint(i32, i32),
}
impl std::fmt::Display for MonitorSelector {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MonitorSelector::Primary => formatter.write_str("the primary monitor"),
MonitorSelector::Index(index) => write!(formatter, "monitor {index}"),
MonitorSelector::Name(name) => write!(formatter, "the monitor named {name:?}"),
MonitorSelector::ContainingPoint(x, y) => {
write!(formatter, "the monitor containing ({x}, {y})")
}
}
}
}
impl MonitorSelector {
pub fn find<'a>(&self, monitors: &'a [Monitor]) -> Option<&'a Monitor> {
match self {
MonitorSelector::Primary => monitors
.iter()
.find(|monitor| monitor.is_primary)
.or_else(|| monitors.first()),
MonitorSelector::Index(index) => monitors.get(*index),
MonitorSelector::Name(wanted) => {
let wanted = wanted.to_lowercase();
monitors
.iter()
.find(|monitor| monitor.name.to_lowercase().contains(&wanted))
}
MonitorSelector::ContainingPoint(x, y) => {
let point = Point::new(f64::from(*x), f64::from(*y));
monitors
.iter()
.find(|monitor| monitor.bounds.contains(point))
}
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Area {
FullScreen,
Rect(Rect),
Corners(Quad),
NormalizedCorners(Quad),
}
#[derive(Clone, Debug, PartialEq)]
pub struct Placement {
pub monitor: MonitorSelector,
pub area: Area,
pub fit: Fit,
pub opacity: f32,
pub always_on_top: bool,
pub fall_back_to_primary: bool,
}
impl Placement {
pub fn new(monitor: MonitorSelector) -> Self {
Placement {
monitor,
area: Area::FullScreen,
fit: Fit::Contain,
opacity: 1.0,
always_on_top: false,
fall_back_to_primary: true,
}
}
pub fn area(mut self, rect: Rect) -> Self {
self.area = Area::Rect(rect);
self
}
pub fn corners(mut self, quad: Quad) -> Self {
self.area = Area::Corners(quad);
self
}
pub fn corners_normalized(mut self, quad: Quad) -> Self {
self.area = Area::NormalizedCorners(quad);
self
}
pub fn keystone(self, inset: f64) -> Self {
let inset = inset.clamp(0.0, 0.49);
self.corners_normalized(Quad::new([
Point::new(inset, 0.0),
Point::new(1.0 - inset, 0.0),
Point::new(1.0, 1.0),
Point::new(0.0, 1.0),
]))
}
pub fn fit(mut self, fit: Fit) -> Self {
self.fit = fit;
self
}
pub fn opacity(mut self, opacity: f32) -> Self {
self.opacity = opacity;
self
}
pub fn always_on_top(mut self, on_top: bool) -> Self {
self.always_on_top = on_top;
self
}
pub fn require_monitor(mut self, required: bool) -> Self {
self.fall_back_to_primary = !required;
self
}
pub fn resolve(
&self,
source_width: u32,
source_height: u32,
monitors: &[Monitor],
) -> Result<ResolvedPlacement> {
if !(0.0..=1.0).contains(&self.opacity) {
return Err(Error::placement(format!(
"opacity is {} — it has to be between 0 and 1",
self.opacity
)));
}
if monitors.is_empty() {
return Err(Error::NoSuchMonitor {
selector: self.monitor.to_string(),
available: 0,
});
}
let (monitor, fell_back) = match self.monitor.find(monitors) {
Some(monitor) => (monitor, false),
None if self.fall_back_to_primary => {
let primary = MonitorSelector::Primary
.find(monitors)
.expect("the list is not empty");
(primary, true)
}
None => {
return Err(Error::NoSuchMonitor {
selector: self.monitor.to_string(),
available: monitors.len(),
})
}
};
let origin = Point::new(monitor.bounds.x, monitor.bounds.y);
let quad = match &self.area {
Area::FullScreen => {
let area = Rect::from_size(monitor.bounds.width, monitor.bounds.height);
let placed =
self.fit
.apply(f64::from(source_width), f64::from(source_height), area);
Quad::from_rect(offset(placed, origin))
}
Area::Rect(rect) => {
if rect.is_empty() {
return Err(Error::placement(format!(
"the area is {}×{} — there is nothing to draw into",
rect.width, rect.height
)));
}
let placed =
self.fit
.apply(f64::from(source_width), f64::from(source_height), *rect);
Quad::from_rect(offset(placed, origin))
}
Area::NormalizedCorners(quad) => {
let scaled = Quad::new(quad.corners.map(|corner| {
Point::new(
origin.x + corner.x * monitor.bounds.width,
origin.y + corner.y * monitor.bounds.height,
)
}));
scaled.homography()?;
scaled
}
Area::Corners(quad) => {
let moved = Quad::new(
quad.corners
.map(|corner| Point::new(corner.x + origin.x, corner.y + origin.y)),
);
moved.homography()?;
moved
}
};
Ok(ResolvedPlacement {
monitor: monitor.clone(),
quad,
opacity: self.opacity,
always_on_top: self.always_on_top,
fell_back,
})
}
}
fn offset(rect: Rect, origin: Point) -> Rect {
Rect::new(
rect.x + origin.x,
rect.y + origin.y,
rect.width,
rect.height,
)
}
#[derive(Clone, Debug, PartialEq)]
pub struct ResolvedPlacement {
pub monitor: Monitor,
pub quad: Quad,
pub opacity: f32,
pub always_on_top: bool,
pub fell_back: bool,
}
impl ResolvedPlacement {
pub fn window_rect(&self) -> Rect {
self.quad.bounds()
}
pub fn quad_in_window(&self) -> Quad {
let bounds = self.window_rect();
Quad::new(
self.quad
.corners
.map(|corner| Point::new(corner.x - bounds.x, corner.y - bounds.y)),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn desktop() -> Vec<Monitor> {
vec![
Monitor {
is_primary: true,
..Monitor::new(
"Built-in Retina Display",
Rect::new(0.0, 0.0, 3024.0, 1964.0),
)
},
Monitor {
scale_factor: 1.0,
refresh_millihertz: Some(59_940),
..Monitor::new("DELL U2720Q", Rect::new(3024.0, 0.0, 3840.0, 2160.0))
},
Monitor::new("EPSON Projector", Rect::new(-1920.0, 0.0, 1920.0, 1080.0)),
]
}
#[test]
fn monitor_ids_are_pinned_and_tell_monitors_apart() {
if cfg!(target_os = "linux") {
assert_eq!(monitor_id("HDMI-1", 0, 0, 1.0), "xrandr_HDMI-1");
return;
}
assert_eq!(monitor_id("DELL U2720Q", 3024, 0, 1.0), "monitor_342b0e446031e910");
assert_eq!(
monitor_id("Built-in Retina Display", 0, 0, 2.0),
"monitor_e16def3726bc150b"
);
assert_ne!(
monitor_id("DELL U2720Q", 3024, 0, 1.0),
monitor_id("DELL U2720Q", 0, 0, 1.0),
"two identical models side by side are two monitors"
);
assert_eq!(
Monitor::new("DELL U2720Q", Rect::new(3024.0, 0.0, 3840.0, 2160.0)).persistent_id,
monitor_id("DELL U2720Q", 3024, 0, 1.0)
);
}
#[test]
fn selectors_find_what_they_name() {
let monitors = desktop();
assert_eq!(
MonitorSelector::Primary.find(&monitors).unwrap().name,
"Built-in Retina Display"
);
assert_eq!(
MonitorSelector::Index(1).find(&monitors).unwrap().name,
"DELL U2720Q"
);
assert_eq!(
MonitorSelector::Name("dell".to_string())
.find(&monitors)
.unwrap()
.name,
"DELL U2720Q",
"name matching should not care about case"
);
assert_eq!(
MonitorSelector::ContainingPoint(-1000, 500)
.find(&monitors)
.unwrap()
.name,
"EPSON Projector",
"a monitor to the left of the primary has negative coordinates"
);
}
#[test]
fn an_unplugged_monitor_falls_back_and_admits_it() {
let monitors = vec![desktop()[0].clone()];
let placement = Placement::new(MonitorSelector::Name("EPSON".to_string()));
let resolved = placement.resolve(1920, 1080, &monitors).unwrap();
assert!(resolved.fell_back);
assert_eq!(resolved.monitor.name, "Built-in Retina Display");
}
#[test]
fn a_required_monitor_is_an_error_when_it_is_missing() {
let monitors = vec![desktop()[0].clone()];
let placement = Placement::new(MonitorSelector::Index(7)).require_monitor(true);
let Err(Error::NoSuchMonitor {
selector,
available,
}) = placement.resolve(1920, 1080, &monitors)
else {
panic!("monitor 7 is not attached");
};
assert_eq!(available, 1);
assert!(selector.contains('7'), "{selector}");
}
#[test]
fn no_monitors_at_all_is_an_error_however_forgiving_the_placement() {
let placement = Placement::new(MonitorSelector::Primary);
assert!(matches!(
placement.resolve(1920, 1080, &[]),
Err(Error::NoSuchMonitor { available: 0, .. })
));
}
#[test]
fn an_area_is_relative_to_its_monitor_and_resolves_to_the_desktop() {
let monitors = desktop();
let placement = Placement::new(MonitorSelector::Index(1))
.area(Rect::new(100.0, 100.0, 1920.0, 1080.0))
.fit(Fit::Stretch);
let resolved = placement.resolve(1920, 1080, &monitors).unwrap();
let rect = resolved.window_rect();
assert_eq!((rect.x, rect.y), (3024.0 + 100.0, 100.0));
assert_eq!((rect.width, rect.height), (1920.0, 1080.0));
assert!(!resolved.fell_back);
}
#[test]
fn full_screen_with_contain_letterboxes_within_the_monitor() {
let monitors = desktop();
let placement = Placement::new(MonitorSelector::Primary);
let resolved = placement.resolve(1920, 1080, &monitors).unwrap();
let rect = resolved.window_rect();
assert_eq!(rect.width, 3024.0);
assert!((rect.height - 1701.0).abs() < 1.0, "{rect:?}");
assert!(
rect.y > 0.0,
"the picture should be centred, not at the top"
);
}
#[test]
fn corner_pinning_moves_the_corners_and_keeps_their_shape() {
let monitors = desktop();
let quad = Quad::keystone(Rect::from_size(1920.0, 1080.0), 200.0);
let placement = Placement::new(MonitorSelector::Index(1)).corners(quad);
let resolved = placement.resolve(1920, 1080, &monitors).unwrap();
assert_eq!(resolved.quad.corners[0].x, 3024.0 + 200.0);
assert_eq!(resolved.quad_in_window().corners[3].x, 0.0);
assert!(resolved.quad.homography().is_ok());
}
#[test]
fn normalized_corners_scale_to_whichever_monitor_they_land_on() {
let monitors = desktop();
let placement = Placement::new(MonitorSelector::Index(1)).keystone(0.1);
let resolved = placement.resolve(1920, 1080, &monitors).unwrap();
assert_eq!(resolved.quad.corners[0], Point::new(3024.0 + 384.0, 0.0));
assert_eq!(resolved.quad.corners[3], Point::new(3024.0, 2160.0));
assert!(
!resolved.quad.is_affine(),
"a keystone is not a parallelogram"
);
let elsewhere = Placement::new(MonitorSelector::Name("EPSON".to_string()))
.keystone(0.1)
.resolve(1920, 1080, &monitors)
.unwrap();
assert_eq!(elsewhere.quad.corners[0], Point::new(-1920.0 + 192.0, 0.0));
}
#[test]
fn a_folded_quad_is_refused_while_it_is_still_configuration() {
let monitors = desktop();
let bowtie = Quad::new([
Point::new(0.0, 0.0),
Point::new(100.0, 0.0),
Point::new(0.0, 100.0),
Point::new(100.0, 100.0),
]);
let placement = Placement::new(MonitorSelector::Primary).corners(bowtie);
assert!(matches!(
placement.resolve(1920, 1080, &monitors),
Err(Error::Placement { .. })
));
}
#[test]
fn an_impossible_opacity_or_an_empty_area_is_refused() {
let monitors = desktop();
let too_opaque = Placement::new(MonitorSelector::Primary).opacity(1.5);
assert!(matches!(
too_opaque.resolve(64, 64, &monitors),
Err(Error::Placement { .. })
));
let nothing = Placement::new(MonitorSelector::Primary).area(Rect::from_size(0.0, 100.0));
assert!(matches!(
nothing.resolve(64, 64, &monitors),
Err(Error::Placement { .. })
));
}
#[test]
fn refresh_rates_keep_their_fractions() {
let monitors = desktop();
let dell = &monitors[1];
assert_eq!(dell.refresh_hz(), Some(59.94));
let interval = dell.frame_interval().unwrap();
assert!((interval.as_secs_f64() - 1.0 / 59.94).abs() < 1e-9);
assert!(monitors[0].frame_interval().is_none());
}
}