diff options
| author | srdusr <[email protected]> | 2026-04-20 00:48:00 +0200 |
|---|---|---|
| committer | srdusr <[email protected]> | 2026-04-20 00:48:00 +0200 |
| commit | 436d42da6ef61a5ea20d5102c4baed7bf0993606 (patch) | |
| tree | 5b013ae1877d81bfb67e422fdf56896d761076ed /tools | |
| parent | 1f708f8aa09bf8bbce82314a76b7f34def90b798 (diff) | |
| download | srdwm-436d42da6ef61a5ea20d5102c4baed7bf0993606.tar.gz srdwm-436d42da6ef61a5ea20d5102c4baed7bf0993606.zip | |
Confirm Nemo's popup works; fix the two bugs that hid it, and shadow bleed across a monitor seam
Nemo's right-click context menu was the last open punch-list item, parked
twice as untestable. It works: verified end to end in a throwaway nested
compositor, menu and submenu both, at the correct position and stacking.
The popup path itself needed no fix, so the POPUP-GEOM-DIAG/POPUP-GRAB-DIAG
diagnostics are removed.
Two real bugs turned up in the way of testing it.
zwlr_virtual_pointer was a silent no-op on the winit backend. Every
Motion/MotionAbsolute handler read UdevState::bounds() behind an early
return when state.udev was None, and that field is Some only for the DRM
backend. The protocol advertised its global, accepted create_virtual_pointer
and accepted every request, then discarded all motion with no error and no
log. That is the backend a nested instance runs on, so the only safe way to
drive a throwaway compositor - a Wayland client of that compositor, which
cannot reach any other session, unlike ydotool's /dev/uinput writes - did
not work at all. Bounds now come from WindowManager::monitors() when udev is
absent; both backends fill that list from Platform::monitors().
The winit backend's screencopy pass rendered no popups and no shadows. It
re-renders the scene offscreen, and that second scene was missing tiers, so
grim on a nested instance reported the opposite of the truth: a menu drawing
perfectly on screen photographed as absent. The DRM backend never had this,
since it serves screencopy from the on-screen frame it just drew. Border
strips are still missing from that pass, called out in the code rather than
left silent.
Also fixed, from the "windows show a bit in the other monitor" report:
shadow_rect expanded by SHADOW_SIZE on every side with no monitor-boundary
awareness, so a window flush against a seam put its 24px shadow strip on the
neighbouring screen. shadow_rect_clipped clips to the bounding box of the
monitors the window's geometry actually touches - not just its assigned
one, since a window straddling a seam really does occupy both and clipping
there would cut its shadow off mid-body. The bitmap's own extent stays
unclipped, because the src rectangle indexes into it; only the fragment list
is clipped. Six tests on the incident's own numbers. Not confirmed on
screen: the nested backend cannot produce a second monitor.
New tool: tools/virtual-pointer-click, a scriptable virtual-pointer driver
that acknowledges each command after its round-trip, so a test script can
put a screenshot between a move and the click that follows it.
489 tests pass, clippy clean.
Diffstat (limited to 'tools')
| -rw-r--r-- | tools/virtual-pointer-click/Cargo.lock | 216 | ||||
| -rw-r--r-- | tools/virtual-pointer-click/Cargo.toml | 18 | ||||
| -rw-r--r-- | tools/virtual-pointer-click/src/main.rs | 161 |
3 files changed, 395 insertions, 0 deletions
diff --git a/tools/virtual-pointer-click/Cargo.lock b/tools/virtual-pointer-click/Cargo.lock new file mode 100644 index 0000000..f4dbd31 --- /dev/null +++ b/tools/virtual-pointer-click/Cargo.lock @@ -0,0 +1,216 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "cc" +version = "1.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad534f4357a5264cce5019c989cf66a4f0dc4e0d1b1d15f8aacec0ff7360273" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quick-xml" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "virtual-pointer-click" +version = "0.1.0" +dependencies = [ + "wayland-client", + "wayland-protocols-wlr", +] + +[[package]] +name = "wayland-backend" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38a91b4eaddff87b1cd1074985e3713da4af2c49742d1b356b2c01670a67a078" +dependencies = [ + "cc", + "downcast-rs", + "rustix", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags", + "rustix", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "pkg-config", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/tools/virtual-pointer-click/Cargo.toml b/tools/virtual-pointer-click/Cargo.toml new file mode 100644 index 0000000..9f94ef2 --- /dev/null +++ b/tools/virtual-pointer-click/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "virtual-pointer-click" +version = "0.1.0" +edition = "2021" + +# Standalone - deliberately not a member of the main workspace at +# ../../Cargo.toml, same reasoning as ../virtual-pointer-pin-test's and +# ../toplevel-activate's own Cargo.toml: building/testing srdwm itself never +# has to build this too. A debug tool, not part of the shipped product. +[workspace] + +[[bin]] +name = "vpclick" +path = "src/main.rs" + +[dependencies] +wayland-client = "=0.31.14" +wayland-protocols-wlr = { version = "=0.3.12", features = ["client"] } diff --git a/tools/virtual-pointer-click/src/main.rs b/tools/virtual-pointer-click/src/main.rs new file mode 100644 index 0000000..993de45 --- /dev/null +++ b/tools/virtual-pointer-click/src/main.rs @@ -0,0 +1,161 @@ +// A scriptable zwlr_virtual_pointer_unstable_v1 driver: reads one command +// per line from stdin and turns it into a virtual-pointer request against +// whatever compositor WAYLAND_DISPLAY names. +// +// WHY THIS EXISTS RATHER THAN ydotool +// +// ydotool writes to /dev/uinput. That is a kernel-level device shared by +// every session on the machine, so a synthetic click from it lands wherever +// the real seat's focus happens to be - which, when the target is a nested +// throwaway compositor, is very often the user's real desktop instead. This +// tool is an ordinary Wayland client of one specific compositor, so its +// input physically cannot reach any other one. That makes it the safe way +// to drive a nested test instance, which is the only reason it exists. +// +// ../virtual-pointer-pin-test is not this: it is a fixed left-button drag +// used to verify pinned delivery, and it blocks on stdin exactly once. This +// one stays alive and takes a stream of commands, so a shell script can +// interleave a `grim` screenshot between a move and the click that follows +// it - which is what "never click at a position you have not verified +// first" actually requires. +// +// Usage: +// vpclick (commands on stdin, one per line) +// +// move <x> <y> absolute, as a fraction of EXTENT (5000 5000 = centre) +// press <button> left | right | middle +// release <button> +// click <button> press then release +// sync round-trip and acknowledge, nothing else +// quit exit +// +// Every command prints "ok <command>" once it has round-tripped, so a +// driving script can wait for the compositor to have actually seen it +// rather than sleeping and hoping. + +use std::io::{BufRead, Write}; + +use wayland_client::protocol::wl_pointer::ButtonState; +use wayland_client::protocol::{wl_registry, wl_seat::WlSeat}; +use wayland_client::{Connection, Dispatch, EventQueue, QueueHandle}; +use wayland_protocols_wlr::virtual_pointer::v1::client::zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1; +use wayland_protocols_wlr::virtual_pointer::v1::client::zwlr_virtual_pointer_v1::{self, ZwlrVirtualPointerV1}; + +const EXTENT: u32 = 10000; + +/// Linux `input-event-codes.h` button codes - what the protocol asks for +/// verbatim, not a wl_pointer enum. +const BTN_LEFT: u32 = 0x110; +const BTN_RIGHT: u32 = 0x111; +const BTN_MIDDLE: u32 = 0x112; + +struct State { + seat: Option<WlSeat>, + manager: Option<ZwlrVirtualPointerManagerV1>, +} + +impl Dispatch<wl_registry::WlRegistry, ()> for State { + fn event(state: &mut Self, registry: &wl_registry::WlRegistry, event: wl_registry::Event, _: &(), _: &Connection, qh: &QueueHandle<Self>) { + if let wl_registry::Event::Global { name, interface, version } = event { + if interface == "wl_seat" { + state.seat = Some(registry.bind::<WlSeat, _, _>(name, version.min(9), qh, ())); + } else if interface == "zwlr_virtual_pointer_manager_v1" { + state.manager = Some(registry.bind::<ZwlrVirtualPointerManagerV1, _, _>(name, version.min(2), qh, ())); + } + } + } +} + +impl Dispatch<WlSeat, ()> for State { + fn event(_: &mut Self, _: &WlSeat, _: wayland_client::protocol::wl_seat::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {} +} + +impl Dispatch<ZwlrVirtualPointerManagerV1, ()> for State { + fn event(_: &mut Self, _: &ZwlrVirtualPointerManagerV1, _: wayland_protocols_wlr::virtual_pointer::v1::client::zwlr_virtual_pointer_manager_v1::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) { + } +} + +impl Dispatch<ZwlrVirtualPointerV1, ()> for State { + fn event(_: &mut Self, _: &ZwlrVirtualPointerV1, _: zwlr_virtual_pointer_v1::Event, _: &(), _: &Connection, _: &QueueHandle<Self>) {} +} + +fn button_code(name: &str) -> Option<u32> { + match name { + "left" => Some(BTN_LEFT), + "right" => Some(BTN_RIGHT), + "middle" => Some(BTN_MIDDLE), + _ => None, + } +} + +fn main() { + let conn = Connection::connect_to_env().expect("failed to connect to the Wayland compositor - is WAYLAND_DISPLAY set?"); + let display = conn.display(); + let mut queue: EventQueue<State> = conn.new_event_queue(); + let qh = queue.handle(); + let _registry = display.get_registry(&qh, ()); + + let mut state = State { seat: None, manager: None }; + queue.roundtrip(&mut state).expect("initial roundtrip failed"); + + let Some(manager) = state.manager.clone() else { + eprintln!("compositor does not advertise zwlr_virtual_pointer_manager_v1"); + std::process::exit(1); + }; + let Some(seat) = state.seat.clone() else { + eprintln!("compositor does not advertise wl_seat"); + std::process::exit(1); + }; + + let pointer = manager.create_virtual_pointer(Some(&seat), &qh, ()); + queue.roundtrip(&mut state).expect("create_virtual_pointer roundtrip failed"); + println!("ready pid {}", std::process::id()); + let _ = std::io::stdout().flush(); + + // A monotonically rising millisecond stamp. Some compositors ignore + // this entirely, but a click whose press and release carry the same + // timestamp is indistinguishable from a double-click to anything that + // does look, so it is worth stepping. + let mut time: u32 = 1; + let stdin = std::io::stdin(); + for line in stdin.lock().lines() { + let line = line.expect("failed to read stdin"); + let parts: Vec<&str> = line.split_whitespace().collect(); + let Some(&cmd) = parts.first() else { continue }; + match cmd { + "move" if parts.len() == 3 => { + let x: u32 = parts[1].parse().expect("x must be a number"); + let y: u32 = parts[2].parse().expect("y must be a number"); + pointer.motion_absolute(time, x, y, EXTENT, EXTENT); + pointer.frame(); + } + "press" | "release" | "click" if parts.len() == 2 => { + let Some(code) = button_code(parts[1]) else { + println!("err unknown button {}", parts[1]); + let _ = std::io::stdout().flush(); + continue; + }; + if cmd != "release" { + pointer.button(time, code, ButtonState::Pressed); + pointer.frame(); + time += 10; + } + if cmd != "press" { + pointer.button(time, code, ButtonState::Released); + pointer.frame(); + } + } + "sync" => {} + "quit" => break, + _ => { + println!("err bad command: {line}"); + let _ = std::io::stdout().flush(); + continue; + } + } + time += 10; + queue.roundtrip(&mut state).expect("roundtrip failed"); + println!("ok {line}"); + let _ = std::io::stdout().flush(); + } +} |