Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
fix(xwayland): derive X11 surface pointer coordinates from geometry, not dest state
The residual hover flicker after 1ffd0e9: now and then the hovered item
and a control up-left of it flickered together — one pointer event
reaching Houdini at half its coordinates. wlr_scene_node_at maps the
point through the buffer's CURRENT dest size, and the commit hook only
closes the gap the surface commit opens; any other writer of dest sizes
(a transaction's frozen copy, a fullscreen tick, the per-frame pass
racing an event) reopens it for one event.
The scene hit-test now derives an X11 surface's point from what is
invariant instead: the node's layout origin and the scale the buffer is
meant to be shown at (output scale over overview zoom for a toplevel,
output scale for an override-redirect). Input no longer depends on the
dest state at all; rendering keeps the commit hook and per-frame pass.
Verified headless at output scale 2 with the GTK4 X11 motion logger: 86
motion samples with no step above 2 units and a 50-pixel drag arriving
as 50, and a click on an item of a half-scaled popover still delivered.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/server/scene.rs | 37 +++++++++++++++++++++++++++++++++++++
1 file changed, 37 insertions(+)
diff --git a/src/server/scene.rs b/src/server/scene.rs
index 078145f..ab0e31d 100644
--- a/src/server/scene.rs
+++ b/src/server/scene.rs
@@ -199,6 +199,43 @@ impl Scene {
}
let surface = ffi::river_scene_node_get_surface(node);
+ // An X11 surface under xwayland_hidpi is a physical-pixel
+ // buffer drawn at 1/scale, and wlr_scene_node_at maps the
+ // point through the buffer's CURRENT dest size. That size
+ // is reset to natural on every commit and restored by the
+ // commit hook — but any other writer of dest sizes (a
+ // transaction's frozen copy, a fullscreen tick) opens the
+ // same gap, in which a pointer event reaches the client at
+ // half its coordinates: Houdini's hover jumping up-left for
+ // a frame. Derive the surface point from what is INVARIANT
+ // instead — the node's layout origin and the scale the
+ // buffer is meant to be shown at — so input never depends
+ // on the dest state.
+ let (mut sx, mut sy) = (sx, sy);
+ if !surface.is_null() {
+ let ratio: Option<f64> = match scene_node_data.data {
+ SceneNodeDataVal::Window(window)
+ if !window.is_null()
+ && matches!((*window).impl_type, crate::window::WindowImpl::Xwayland(_)) =>
+ {
+ let s = crate::xwayland_window::x11_scale((*window).server) as f64;
+ let zoom = if (*window).scale > 0.0 { (*window).scale } else { 1.0 };
+ (s != 1.0).then_some(s / zoom)
+ }
+ SceneNodeDataVal::OverrideRedirect(or) if !or.is_null() => {
+ let s = crate::xwayland_window::x11_scale((*or).server) as f64;
+ (s != 1.0).then_some(s)
+ }
+ _ => None,
+ };
+ if let Some(ratio) = ratio {
+ let (mut nx, mut ny) = (0, 0);
+ if ffi::wlr_scene_node_coords(node, &mut nx, &mut ny) {
+ sx = (lx - nx as f64) * ratio;
+ sy = (ly - ny as f64) * ratio;
+ }
+ }
+ }
result = Some(AtResult {
node,
surface,