use image::RgbaImage;
use xcap::{Monitor, XCapError};
use zyris::WireError;
use zyris_caps::{display_scale, no_such_display, Display};
use super::internal;
fn monitor_id(monitor: &Monitor, index: usize) -> String {
match monitor.id() {
Ok(id) => id.to_string(),
Err(_) => format!("display-{index}"),
}
}
fn monitor_name(monitor: &Monitor) -> String {
monitor
.friendly_name()
.or_else(|_| monitor.name())
.unwrap_or_default()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Reported {
AsCaptured,
ScaledDown,
Xwayland,
}
fn looks_like_wayland(session_type: &str, wayland_display: &str) -> bool {
session_type == "wayland" || wayland_display.to_lowercase().contains("wayland")
}
fn wayland_session() -> bool {
fn var(name: &str) -> String {
std::env::var(name).unwrap_or_default()
}
looks_like_wayland(&var("XDG_SESSION_TYPE"), &var("WAYLAND_DISPLAY"))
}
fn rule_for(target_os: &str, wayland: bool) -> Reported {
match target_os {
"windows" => Reported::AsCaptured,
"linux" if wayland => Reported::Xwayland,
_ => Reported::ScaledDown,
}
}
fn reported() -> Reported {
rule_for(std::env::consts::OS, wayland_session())
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Geometry {
x: i32,
y: i32,
width: u32,
height: u32,
scale_factor: f32,
}
fn geometry_of(name: &str, measured: &[Display]) -> Option<Geometry> {
if name.is_empty() {
return None;
}
measured
.iter()
.find(|m| m.id == name)
.map(|m| Geometry {
x: m.x,
y: m.y,
width: m.width,
height: m.height,
scale_factor: m.scale_factor,
})
}
fn correct(reported: Reported, display: Display, measured: Option<Geometry>) -> Display {
let factor = display_scale(display.scale_factor);
match (reported, measured) {
(Reported::AsCaptured, _) => Display { scale_factor: factor, ..display },
(Reported::Xwayland, Some(g)) => Display {
x: g.x,
y: g.y,
width: g.width,
height: g.height,
scale_factor: display_scale(g.scale_factor),
..display
},
_ => Display {
x: (display.x as f32 * factor).round() as i32,
y: (display.y as f32 * factor).round() as i32,
width: (display.width as f32 * factor).round() as u32,
height: (display.height as f32 * factor).round() as u32,
scale_factor: factor,
..display
},
}
}
#[cfg(target_os = "linux")]
fn measured_geometry(reported: Reported) -> Vec<Display> {
if reported != Reported::Xwayland {
return Vec::new();
}
match super::wayland_backend::displays() {
Ok(outputs) => outputs,
Err(err) => {
tracing::warn!(
%err,
"could not read wl_output for display sizes; falling back to xcap's geometry, \
which is the compositor's logical layout scaled by Xwayland and is not what a \
capture of these displays will be"
);
Vec::new()
}
}
}
#[cfg(not(target_os = "linux"))]
fn measured_geometry(_reported: Reported) -> Vec<Display> {
Vec::new()
}
fn describe(monitor: &Monitor, index: usize, reported: Reported, measured: &[Display]) -> Display {
let name = monitor_name(monitor);
let geometry = geometry_of(&name, measured);
correct(
reported,
Display {
id: monitor_id(monitor, index),
name,
x: monitor.x().unwrap_or(0),
y: monitor.y().unwrap_or(0),
width: monitor.width().unwrap_or(0),
height: monitor.height().unwrap_or(0),
scale_factor: monitor.scale_factor().unwrap_or(1.0),
primary: monitor.is_primary().unwrap_or(false),
},
geometry,
)
}
fn select<'a>(monitors: &'a [Monitor], wanted: Option<&str>) -> zyris::Result<(&'a Monitor, usize)> {
if let Some(wanted) = wanted {
let found = monitors
.iter()
.enumerate()
.find(|(index, m)| monitor_id(m, *index) == wanted)
.or_else(|| {
monitors
.iter()
.enumerate()
.find(|(_, m)| monitor_name(m) == wanted)
});
return match found {
Some((index, monitor)) => Ok((monitor, index)),
None => Err(no_such_display(wanted)),
};
}
let index = monitors
.iter()
.position(|m| m.is_primary().unwrap_or(false))
.unwrap_or(0);
Ok((&monitors[index], index))
}
pub(super) fn displays() -> zyris::Result<Vec<Display>> {
let monitors = Monitor::all().map_err(internal)?;
let reported = reported();
let measured = measured_geometry(reported);
Ok(monitors
.iter()
.enumerate()
.map(|(index, monitor)| describe(monitor, index, reported, &measured))
.collect())
}
pub(super) fn capture(display: Option<&str>) -> zyris::Result<(String, RgbaImage)> {
let monitors = Monitor::all().map_err(internal)?;
if monitors.is_empty() {
return Err(internal("no displays are attached"));
}
let (monitor, index) = select(&monitors, display)?;
let id = monitor_id(monitor, index);
let image = monitor.capture_image().map_err(|err| match err {
XCapError::InvalidCaptureRegion(msg) => WireError::invalid_params(msg),
other => internal(other),
})?;
Ok((id, image))
}
#[cfg(test)]
mod tests {
use super::*;
fn reports(width: u32, height: u32, scale_factor: f32) -> Display {
Display {
id: "33".into(),
name: "eDP-1".into(),
x: 1920,
y: -360,
width,
height,
scale_factor,
primary: false,
}
}
fn measures(width: u32, height: u32, scale_factor: f32) -> Vec<Display> {
vec![Display {
id: "eDP-1".into(),
name: "Built-in".into(),
x: 0,
y: 0,
width,
height,
scale_factor,
primary: true,
}]
}
#[test]
fn every_platform_gets_the_rule_its_own_apis_call_for() {
assert_eq!(rule_for("windows", false), Reported::AsCaptured);
assert_eq!(rule_for("windows", true), Reported::AsCaptured);
assert_eq!(rule_for("macos", false), Reported::ScaledDown);
assert_eq!(rule_for("macos", true), Reported::ScaledDown);
assert_eq!(rule_for("linux", false), Reported::ScaledDown);
assert_eq!(rule_for("linux", true), Reported::Xwayland);
}
#[test]
fn a_wayland_session_is_recognised_the_way_xcap_recognises_one() {
assert!(looks_like_wayland("wayland", ""));
assert!(looks_like_wayland("", "wayland-0"));
assert!(looks_like_wayland("", "WAYLAND-1"));
assert!(!looks_like_wayland("Wayland", ""));
assert!(!looks_like_wayland("x11", ":0"));
assert!(!looks_like_wayland("", ""));
}
#[test]
fn an_x11_display_is_reported_in_the_pixels_it_captures_in() {
let display = correct(Reported::ScaledDown, reports(1920, 1080, 2.0), None);
assert_eq!((display.width, display.height), (3840, 2160));
assert_eq!((display.x, display.y), (3840, -720));
assert_eq!(display.scale_factor, 2.0);
}
#[test]
fn a_mac_reports_points_and_captures_its_backing_store() {
let display = correct(Reported::ScaledDown, reports(1680, 1050, 2.0), None);
assert_eq!((display.width, display.height), (3360, 2100));
}
#[test]
fn windows_geometry_is_already_physical() {
let display = correct(Reported::AsCaptured, reports(1920, 1080, 2.0), None);
assert_eq!((display.width, display.height), (1920, 1080));
assert_eq!((display.x, display.y), (1920, -360));
assert_eq!(display.scale_factor, 2.0);
}
#[test]
fn an_xwayland_display_is_the_size_wl_output_says_it_is() {
let measured = measures(1920, 1080, 1.25);
let display = correct(
Reported::Xwayland,
reports(2457, 1382, 1.25),
geometry_of("eDP-1", &measured),
);
assert_eq!((display.width, display.height), (1920, 1080));
assert_eq!((display.x, display.y), (0, 0));
assert_eq!(display.scale_factor, 1.25);
assert_eq!(display.id, "33");
}
#[test]
fn an_xwayland_session_with_nothing_to_measure_keeps_the_old_arithmetic() {
let display = correct(Reported::Xwayland, reports(2457, 1382, 1.25), None);
assert_eq!((display.width, display.height), (3071, 1728));
}
#[test]
fn a_display_that_is_not_scaled_is_left_exactly_where_it_is() {
let unscaled = reports(1920, 1080, 1.0);
for rule in [Reported::AsCaptured, Reported::ScaledDown, Reported::Xwayland] {
assert_eq!(
correct(rule, unscaled.clone(), None),
unscaled,
"{rule:?} moved a display that is not scaled"
);
}
let measured = measures(1920, 1080, 1.0);
let corrected = correct(
Reported::Xwayland,
unscaled.clone(),
geometry_of("eDP-1", &measured),
);
assert_eq!((corrected.width, corrected.height), (1920, 1080));
assert_eq!(corrected.scale_factor, 1.0);
}
#[test]
fn a_scale_of_zero_is_a_scale_of_one() {
let expected = reports(1920, 1080, 1.0);
assert_eq!(correct(Reported::ScaledDown, reports(1920, 1080, 0.0), None), expected);
assert_eq!(correct(Reported::ScaledDown, reports(1920, 1080, f32::NAN), None), expected);
assert_eq!(correct(Reported::AsCaptured, reports(1920, 1080, 0.0), None), expected);
let measured = measures(1920, 1080, 0.0);
let corrected = correct(
Reported::Xwayland,
reports(2457, 1382, 1.25),
geometry_of("eDP-1", &measured),
);
assert_eq!(corrected.scale_factor, 1.0);
}
#[test]
fn a_measurement_is_found_by_connector_name_and_not_otherwise() {
let measured = measures(1920, 1080, 1.25);
assert!(geometry_of("eDP-1", &measured).is_some());
assert!(geometry_of("33", &measured).is_none());
assert!(geometry_of("Built-in", &measured).is_none());
assert!(geometry_of("HDMI-1", &measured).is_none());
assert!(geometry_of("", &measures(1920, 1080, 1.25)).is_none());
assert!(geometry_of("eDP-1", &[]).is_none());
}
}