#![cfg(target_os = "linux")]
use std::sync::OnceLock;
use wayland_client::protocol::{wl_output, wl_registry};
use wayland_client::{Connection, Dispatch, QueueHandle, WEnum};
use wayland_protocols::xdg::xdg_output::zv1::client::{zxdg_output_manager_v1, zxdg_output_v1};
use x11rb::connection::Connection as _;
use x11rb::protocol::xproto::{AtomEnum, ConnectionExt as _};
use x11rb::rust_connection::RustConnection;
const BASE_DPI: f64 = 96.0;
const MAX_SCALE: i64 = 8;
const INTEGER_EPS: f64 = 0.05;
pub fn coordinate_scale() -> f64 {
static SCALE: OnceLock<f64> = OnceLock::new();
*SCALE.get_or_init(detect_x11_scale)
}
pub fn screenshot_scale() -> f64 {
static SCALE: OnceLock<f64> = OnceLock::new();
*SCALE.get_or_init(|| {
match scale_source(
std::env::var_os("DISPLAY").is_some(),
std::env::var_os("WAYLAND_DISPLAY").is_some(),
) {
ScaleSource::X11 => coordinate_scale(),
ScaleSource::Wayland => wayland_output_scale().unwrap_or(1.0),
ScaleSource::Headless => 1.0,
}
})
}
#[derive(Debug, PartialEq, Eq)]
enum ScaleSource {
X11,
Wayland,
Headless,
}
fn scale_source(display_set: bool, wayland_set: bool) -> ScaleSource {
if display_set {
ScaleSource::X11
} else if wayland_set {
ScaleSource::Wayland
} else {
ScaleSource::Headless
}
}
fn detect_x11_scale() -> f64 {
if std::env::var_os("WAYLAND_DISPLAY").is_some() {
return 1.0;
}
if std::env::var_os("DISPLAY").is_none() {
return 1.0;
}
match read_xft_dpi() {
Some(dpi) => scale_from_dpi(dpi),
None => 1.0,
}
}
fn read_xft_dpi() -> Option<f64> {
let (conn, screen_num) = RustConnection::connect(None).ok()?;
let root = conn.setup().roots.get(screen_num)?.root;
let reply = conn
.get_property(
false,
root,
AtomEnum::RESOURCE_MANAGER,
AtomEnum::STRING,
0,
1 << 18,
)
.ok()?
.reply()
.ok()?;
let text = String::from_utf8_lossy(&reply.value);
parse_xft_dpi(&text)
}
fn parse_xft_dpi(resources: &str) -> Option<f64> {
for line in resources.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("Xft.dpi:") {
return rest.trim().parse::<f64>().ok().filter(|d| *d > 0.0);
}
}
None
}
fn scale_from_dpi(dpi: f64) -> f64 {
if !dpi.is_finite() || dpi <= 0.0 {
return 1.0;
}
let raw = dpi / BASE_DPI;
let nearest = raw.round();
if (raw - nearest).abs() <= INTEGER_EPS && nearest >= 1.0 && (nearest as i64) <= MAX_SCALE {
nearest
} else {
1.0
}
}
#[derive(Default)]
struct OutputInfo {
outputs: Vec<wl_output::WlOutput>,
xdg_manager: Option<zxdg_output_manager_v1::ZxdgOutputManagerV1>,
xdg_outputs: Vec<zxdg_output_v1::ZxdgOutputV1>,
output_count: usize,
current_mode_width: Option<i32>,
logical_width: Option<i32>,
max_int_scale: i32,
}
impl Dispatch<wl_registry::WlRegistry, ()> for OutputInfo {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_data: &(),
_conn: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_registry::Event::Global {
name,
interface,
version,
} = event
{
match interface.as_str() {
"wl_output" if version >= 2 => {
let output = registry.bind::<wl_output::WlOutput, (), Self>(name, 2, qh, ());
state.outputs.push(output);
state.output_count += 1;
}
"zxdg_output_manager_v1" => {
let mgr = registry
.bind::<zxdg_output_manager_v1::ZxdgOutputManagerV1, (), Self>(
name,
version.min(3),
qh,
(),
);
state.xdg_manager = Some(mgr);
}
_ => {}
}
}
}
}
impl Dispatch<wl_output::WlOutput, ()> for OutputInfo {
fn event(
state: &mut Self,
_output: &wl_output::WlOutput,
event: wl_output::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
match event {
wl_output::Event::Scale { factor } => {
state.max_int_scale = state.max_int_scale.max(factor);
}
wl_output::Event::Mode {
flags: WEnum::Value(m),
width,
..
} if m.contains(wl_output::Mode::Current) => {
state.current_mode_width = Some(width);
}
_ => {}
}
}
}
impl Dispatch<zxdg_output_manager_v1::ZxdgOutputManagerV1, ()> for OutputInfo {
fn event(
_state: &mut Self,
_mgr: &zxdg_output_manager_v1::ZxdgOutputManagerV1,
_event: zxdg_output_manager_v1::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
}
}
impl Dispatch<zxdg_output_v1::ZxdgOutputV1, ()> for OutputInfo {
fn event(
state: &mut Self,
_xdg_output: &zxdg_output_v1::ZxdgOutputV1,
event: zxdg_output_v1::Event,
_data: &(),
_conn: &Connection,
_qh: &QueueHandle<Self>,
) {
if let zxdg_output_v1::Event::LogicalSize { width, .. } = event {
state.logical_width = Some(width);
}
}
}
fn wayland_output_scale() -> Option<f64> {
let conn = Connection::connect_to_env().ok()?;
wayland_output_scale_from_conn(conn)
}
fn wayland_output_scale_from_conn(conn: Connection) -> Option<f64> {
let display = conn.display();
let mut queue = conn.new_event_queue::<OutputInfo>();
let qh = queue.handle();
display.get_registry(&qh, ());
let mut state = OutputInfo::default();
queue.roundtrip(&mut state).ok()?;
if let Some(mgr) = state.xdg_manager.clone() {
for output in state.outputs.clone() {
let xdg_output = mgr.get_xdg_output(&output, &qh, ());
state.xdg_outputs.push(xdg_output);
}
}
queue.roundtrip(&mut state).ok()?;
queue.roundtrip(&mut state).ok()?;
combine_wayland_scale(
state.output_count,
state.current_mode_width,
state.logical_width,
state.max_int_scale,
)
}
fn combine_wayland_scale(
output_count: usize,
mode_width: Option<i32>,
logical_width: Option<i32>,
max_int_scale: i32,
) -> Option<f64> {
if output_count == 1 {
if let (Some(m), Some(l)) = (mode_width, logical_width) {
if m > 0 && l > 0 {
return sane_scale(f64::from(m) / f64::from(l));
}
}
}
sane_scale(f64::from(max_int_scale))
}
fn sane_scale(s: f64) -> Option<f64> {
if s.is_finite() && s >= 1.0 && s <= MAX_SCALE as f64 {
Some(s)
} else {
None
}
}
#[cfg(test)]
mod tests {
use super::{combine_wayland_scale, parse_xft_dpi, scale_from_dpi, scale_source, ScaleSource};
#[test]
fn scale_source_mirrors_backend_selection() {
assert_eq!(scale_source(true, true), ScaleSource::X11);
assert_eq!(scale_source(true, false), ScaleSource::X11);
assert_eq!(scale_source(false, true), ScaleSource::Wayland);
assert_eq!(scale_source(false, false), ScaleSource::Headless);
}
#[test]
fn parse_extracts_dpi_line() {
let xrdb = "*customization:\t-color\nXft.dpi:\t192\nXft.antialias:\t1\n";
assert_eq!(parse_xft_dpi(xrdb), Some(192.0));
}
#[test]
fn parse_tolerates_spaces_and_position() {
assert_eq!(parse_xft_dpi("Xft.dpi: 96"), Some(96.0));
assert_eq!(parse_xft_dpi("a:1\n Xft.dpi:\t144 \nb:2"), Some(144.0));
}
#[test]
fn parse_missing_or_bad_is_none() {
assert_eq!(parse_xft_dpi(""), None);
assert_eq!(parse_xft_dpi("Xft.antialias:\t1"), None);
assert_eq!(parse_xft_dpi("Xft.dpi:\tnope"), None);
assert_eq!(parse_xft_dpi("Xft.dpi:\t-5"), None);
}
#[test]
fn integer_scales_are_honored() {
assert_eq!(scale_from_dpi(96.0), 1.0);
assert_eq!(scale_from_dpi(192.0), 2.0);
assert_eq!(scale_from_dpi(288.0), 3.0);
assert_eq!(scale_from_dpi(191.5), 2.0);
}
#[test]
fn fractional_scales_fall_back_to_one() {
assert_eq!(scale_from_dpi(120.0), 1.0);
assert_eq!(scale_from_dpi(144.0), 1.0);
}
#[test]
fn out_of_range_or_bad_dpi_is_one() {
assert_eq!(scale_from_dpi(0.0), 1.0);
assert_eq!(scale_from_dpi(-96.0), 1.0);
assert_eq!(scale_from_dpi(f64::NAN), 1.0);
assert_eq!(scale_from_dpi(864.0), 1.0);
}
#[test]
fn single_output_integer_scale_is_exact() {
assert_eq!(
combine_wayland_scale(1, Some(3840), Some(1920), 2),
Some(2.0)
);
}
#[test]
fn single_output_fractional_scale_is_exact() {
assert_eq!(
combine_wayland_scale(1, Some(1920), Some(1280), 2),
Some(1.5)
);
}
#[test]
fn single_output_missing_logical_falls_back_to_int_scale() {
assert_eq!(combine_wayland_scale(1, Some(1920), None, 2), Some(2.0));
assert_eq!(combine_wayland_scale(1, None, Some(1280), 3), Some(3.0));
}
#[test]
fn multi_output_uses_integer_scale_even_with_geometry() {
assert_eq!(
combine_wayland_scale(2, Some(1920), Some(1280), 2),
Some(2.0)
);
}
#[test]
fn no_geometry_and_no_scale_is_none() {
assert_eq!(combine_wayland_scale(0, None, None, 0), None);
assert_eq!(combine_wayland_scale(1, None, None, 0), None);
}
#[test]
fn out_of_range_ratio_is_rejected() {
assert_eq!(combine_wayland_scale(1, Some(1920), Some(1), 0), None);
}
}
#[cfg(test)]
mod wayland_mock_tests {
use std::os::unix::net::{UnixListener, UnixStream};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;
use wayland_client::Connection;
use wayland_protocols::xdg::xdg_output::zv1::server::{
zxdg_output_manager_v1::{self, ZxdgOutputManagerV1},
zxdg_output_v1::{self, ZxdgOutputV1},
};
use wayland_server::protocol::wl_output::{self, WlOutput};
use wayland_server::{backend::ClientData, Dispatch, Display, DisplayHandle, GlobalDispatch};
const PHYSICAL_WIDTH: i32 = 1920;
const LOGICAL_WIDTH: i32 = 1280;
struct MockServer;
struct ClientState;
impl ClientData for ClientState {}
impl GlobalDispatch<WlOutput, ()> for MockServer {
fn bind(
_state: &mut Self,
_handle: &DisplayHandle,
_client: &wayland_server::Client,
resource: wayland_server::New<WlOutput>,
_global_data: &(),
data_init: &mut wayland_server::DataInit<'_, Self>,
) {
let output = data_init.init(resource, ());
output.geometry(
0,
0,
340,
190,
wl_output::Subpixel::Unknown,
"xa11y".into(),
"mock".into(),
wl_output::Transform::Normal,
);
output.mode(wl_output::Mode::Current, PHYSICAL_WIDTH, 1080, 60_000);
output.scale(2);
output.done();
}
}
impl Dispatch<WlOutput, ()> for MockServer {
fn request(
_state: &mut Self,
_client: &wayland_server::Client,
_resource: &WlOutput,
_request: wl_output::Request,
_data: &(),
_dh: &DisplayHandle,
_init: &mut wayland_server::DataInit<'_, Self>,
) {
}
}
impl GlobalDispatch<ZxdgOutputManagerV1, ()> for MockServer {
fn bind(
_state: &mut Self,
_handle: &DisplayHandle,
_client: &wayland_server::Client,
resource: wayland_server::New<ZxdgOutputManagerV1>,
_global_data: &(),
data_init: &mut wayland_server::DataInit<'_, Self>,
) {
data_init.init(resource, ());
}
}
impl Dispatch<ZxdgOutputManagerV1, ()> for MockServer {
fn request(
_state: &mut Self,
_client: &wayland_server::Client,
_resource: &ZxdgOutputManagerV1,
request: zxdg_output_manager_v1::Request,
_data: &(),
_dh: &DisplayHandle,
data_init: &mut wayland_server::DataInit<'_, Self>,
) {
if let zxdg_output_manager_v1::Request::GetXdgOutput { id, .. } = request {
let xdg = data_init.init(id, ());
xdg.logical_position(0, 0);
xdg.logical_size(LOGICAL_WIDTH, 720);
xdg.done();
}
}
}
impl Dispatch<ZxdgOutputV1, ()> for MockServer {
fn request(
_state: &mut Self,
_client: &wayland_server::Client,
_resource: &ZxdgOutputV1,
_request: zxdg_output_v1::Request,
_data: &(),
_dh: &DisplayHandle,
_init: &mut wayland_server::DataInit<'_, Self>,
) {
}
}
#[test]
fn fractional_scale_from_mock_compositor() {
let sock_path =
std::env::temp_dir().join(format!("xa11y-mock-wl-{}.sock", std::process::id()));
let _ = std::fs::remove_file(&sock_path);
let listener = UnixListener::bind(&sock_path).expect("bind mock socket");
listener.set_nonblocking(true).expect("nonblocking");
let mut display: Display<MockServer> = Display::new().expect("server display");
let mut dh = display.handle();
dh.create_global::<MockServer, WlOutput, ()>(4, ());
dh.create_global::<MockServer, ZxdgOutputManagerV1, ()>(3, ());
let shutdown = Arc::new(AtomicBool::new(false));
let server_shutdown = Arc::clone(&shutdown);
let server = thread::spawn(move || {
let mut state = MockServer;
loop {
match listener.accept() {
Ok((stream, _)) => {
dh.insert_client(stream, Arc::new(ClientState))
.expect("insert client");
}
Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {}
Err(e) => panic!("mock accept: {e}"),
}
display.dispatch_clients(&mut state).expect("dispatch");
display.flush_clients().expect("flush");
if server_shutdown.load(Ordering::Relaxed) {
break;
}
thread::sleep(Duration::from_millis(1));
}
});
let stream = UnixStream::connect(&sock_path).expect("connect mock socket");
let conn = Connection::from_socket(stream).expect("client connection");
let scale = super::wayland_output_scale_from_conn(conn);
shutdown.store(true, Ordering::Relaxed);
server.join().expect("join server");
let _ = std::fs::remove_file(&sock_path);
assert_eq!(
scale,
Some(1.5),
"client must derive 1.5 from mode {PHYSICAL_WIDTH} / logical {LOGICAL_WIDTH}"
);
}
}