srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/tools/virtual-pointer-click/src
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-04-20 00:48:00 +0200
committersrdusr <[email protected]>2026-04-20 00:48:00 +0200
commit436d42da6ef61a5ea20d5102c4baed7bf0993606 (patch)
tree5b013ae1877d81bfb67e422fdf56896d761076ed /tools/virtual-pointer-click/src
parent1f708f8aa09bf8bbce82314a76b7f34def90b798 (diff)
downloadsrdwm-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/virtual-pointer-click/src')
-rw-r--r--tools/virtual-pointer-click/src/main.rs161
1 files changed, 161 insertions, 0 deletions
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();
+ }
+}