Fix Wayland cursor density in physical desktop layouts

This commit is contained in:
fufesou
2026-09-14 12:32:16 +08:00
parent 6a62b7bbd9
commit 70e345a76d
5 changed files with 103 additions and 1 deletions

View File

@@ -320,6 +320,9 @@ pub struct WaylandDisplayInfo {
pub width: i32,
pub height: i32,
pub logical_size: Option<(i32, i32)>,
// wl_output.scale is independent of desktop coordinates. Older probes omit it.
#[serde(default)]
pub scale_factor: i32,
pub refresh_rate: i32,
/// Output rotation in degrees (0/90/180/270), from `wl_output.geometry`. The mode keeps its
/// unrotated dimensions and `logical_size` arrives already swapped, so without this field a
@@ -450,6 +453,7 @@ fn collect_wayland_displays(conn: &Connection) -> ResultType<Vec<WaylandDisplayI
width,
height,
logical_size,
scale_factor: info.scale_factor,
refresh_rate,
transform,
});
@@ -542,9 +546,11 @@ mod tests {
let old = r#"{"name":"HDMI-1","x":0,"y":0,"width":1920,"height":1080,"logical_size":null,"refresh_rate":60}"#;
let info: WaylandDisplayInfo = serde_json::from_str(old).unwrap();
assert_eq!(info.transform, 0);
assert_eq!(info.scale_factor, 0);
let roundtrip: WaylandDisplayInfo =
serde_json::from_str(&serde_json::to_string(&info).unwrap()).unwrap();
assert_eq!(roundtrip.transform, 0);
assert_eq!(roundtrip.scale_factor, 0);
}
#[test]

View File

@@ -356,6 +356,17 @@ pub fn get_layout_for_uinput_live() -> Option<((i32, i32, i32, i32), Vec<Display
}
match enumerate_displays() {
Ok(displays) => {
#[cfg(feature = "drm")]
{
// Output density can change without moving any desktop-coordinate rectangle.
let mut cache = DISPLAYS.lock().unwrap();
if let Some(updated) = cache
.as_deref()
.and_then(|cached| updated_cursor_scales(cached, &displays))
{
*cache = Some(Arc::new(updated));
}
}
desktop_rect_of(&displays).map(|rect| (rect, logical_rects_of(&displays)))
}
Err(_err) => {
@@ -366,6 +377,30 @@ pub fn get_layout_for_uinput_live() -> Option<((i32, i32, i32, i32), Vec<Display
}
}
#[cfg(feature = "drm")]
fn updated_cursor_scales(cached: &Displays, live: &[WaylandDisplayInfo]) -> Option<Displays> {
let mut displays = cached.displays.clone();
let mut changed = false;
for output in &mut displays {
// Geometry changes retain the existing layout invalidation/rebuild path.
let Some(current) = live.iter().find(|d| {
d.name == output.name
&& (d.x, d.y) == (output.x, output.y)
&& (d.width, d.height) == (output.width, output.height)
&& d.transform == output.transform
&& d.logical_size == output.logical_size
}) else {
continue;
};
changed |= output.scale_factor != current.scale_factor;
output.scale_factor = current.scale_factor;
}
changed.then_some(Displays {
primary: cached.primary,
displays,
})
}
fn desktop_rect_of(displays: &[WaylandDisplayInfo]) -> Option<(i32, i32, i32, i32)> {
if displays.is_empty() {
return None;
@@ -604,6 +639,7 @@ mod tests {
width,
height,
logical_size,
scale_factor: 1,
refresh_rate: 60,
transform: 0,
}
@@ -614,6 +650,33 @@ mod tests {
assert_eq!(desktop_rect_of(&[]), None);
}
#[cfg(feature = "drm")]
#[test]
fn output_scale_refresh_preserves_layout_and_existing_snapshots() {
let mut output = display(0, 0, 2560, 1600, Some((2560, 1600)));
output.name = "eDP-1".into();
output.scale_factor = 3;
let cached = Displays {
primary: 0,
displays: vec![output.clone()],
};
output.scale_factor = 2;
let updated = updated_cursor_scales(&cached, &[output.clone()]).unwrap();
assert_eq!(updated.displays[0].scale_factor, 2);
assert_eq!(cached.displays[0].scale_factor, 3);
assert_eq!(updated.primary, cached.primary);
assert_eq!(
logical_rects_of(&updated.displays),
logical_rects_of(&cached.displays)
);
assert!(updated_cursor_scales(&updated, &[output.clone()]).is_none());
output.name = "HDMI-1".into();
assert!(updated_cursor_scales(&cached, &[output.clone()]).is_none());
output.name = "eDP-1".into();
output.logical_size = Some((1280, 800));
assert!(updated_cursor_scales(&cached, &[output]).is_none());
}
#[test]
fn test_desktop_rect_single_display_uses_physical_size() {
let displays = [display(0, 0, 2880, 1800, Some((1859, 1162)))];

View File

@@ -128,7 +128,14 @@ fn wayland_scale(display: &base::platform::linux::WaylandDisplayInfo) -> ResultT
} else {
display.width
};
Ok(f64::from(width) / f64::from(logical_width))
let scale = f64::from(width) / f64::from(logical_width);
// Mutter can report physical desktop coordinates even at 2x/3x output scale.
// Keep geometry-derived fractional densities for logically scaled desktops.
Ok(if scale == 1.0 && display.scale_factor > 1 {
f64::from(display.scale_factor)
} else {
scale
})
}
#[cfg(test)]
@@ -155,6 +162,7 @@ mod tests {
width: 1280,
height: 800,
logical_size: Some((600, 960)),
scale_factor: 2,
refresh_rate: 60000,
transform: 90,
};
@@ -162,4 +170,27 @@ mod tests {
assert_ne!(cache_id(1, 1.0), cache_id(1, 2.0));
assert_eq!(cache_id(1, 0.0), 1);
}
#[cfg(feature = "drm")]
#[test]
fn cursor_density_distinguishes_output_scale_from_desktop_coordinates() {
for (scale_factor, logical_size, expected) in [
(3, (2560, 1600), 3.0), // GNOME can use physical desktop coordinates.
(2, (2048, 1280), 1.25), // Keep fractional scaling from logical geometry.
(0, (2560, 1600), 1.0), // Older probe snapshots omit wl_output.scale.
] {
let display = base::platform::linux::WaylandDisplayInfo {
name: "eDP-1".into(),
x: 0,
y: 0,
width: 2560,
height: 1600,
logical_size: Some(logical_size),
scale_factor,
refresh_rate: 120000,
transform: 0,
};
assert_eq!(wayland_scale(&display).unwrap(), expected);
}
}
}

View File

@@ -2158,6 +2158,7 @@ mod drm_capturer_tests {
width: w,
height: h,
logical_size: Some((w, h)),
scale_factor: 1,
refresh_rate: 60,
transform: 0,
}

View File

@@ -60,6 +60,7 @@ mod tests {
width: 1280,
height: 800,
logical_size: Some((logical_width, logical_width * 800 / 1280)),
scale_factor: 1,
refresh_rate: 60000,
transform: 0,
}],