use iris::harness::{TouchAction, TouchScript}; use std::time::Duration; use wayland_client::protocol::wl_pointer::ButtonState; use wayland_client::protocol::{wl_registry, wl_seat}; use wayland_client::{Connection, Dispatch, QueueHandle, delegate_noop}; use wayland_protocols_wlr::virtual_pointer::v1::client::{ zwlr_virtual_pointer_manager_v1::ZwlrVirtualPointerManagerV1, zwlr_virtual_pointer_v1::ZwlrVirtualPointerV1, }; const BTN_LEFT: u32 = 0x110; const SETTLE: Duration = Duration::from_millis(200); #[derive(Default)] struct Globals { seat: Option, manager: Option, } impl Dispatch for Globals { fn event( state: &mut Self, registry: &wl_registry::WlRegistry, event: wl_registry::Event, _: &(), _: &Connection, qh: &QueueHandle, ) { let wl_registry::Event::Global { name, interface, version, } = event else { return; }; match interface.as_str() { "wl_seat" => { state.seat = Some(registry.bind(name, version.min(7), qh, ())); } "zwlr_virtual_pointer_manager_v1" => { state.manager = Some(registry.bind(name, version.min(2), qh, ())); } _ => {} } } } delegate_noop!(Globals: ignore wl_seat::WlSeat); delegate_noop!(Globals: ZwlrVirtualPointerManagerV1); delegate_noop!(Globals: ZwlrVirtualPointerV1); fn main() { let args: Vec = std::env::args().skip(1).collect(); let [width, height, path] = args.as_slice() else { eprintln!("usage: replay-touch WIDTH HEIGHT FILE"); std::process::exit(2); }; let (width, height) = (parse(width, "WIDTH"), parse(height, "HEIGHT")); let text = std::fs::read_to_string(path) .unwrap_or_else(|e| fail(&format!("could not read {path}: {e}"))); let script = TouchScript::parse(&text).unwrap_or_else(|e| fail(&e)); let conn = Connection::connect_to_env().unwrap_or_else(|e| { fail(&format!( "no wayland display ({e}); is WAYLAND_DISPLAY set?" )) }); let mut queue = conn.new_event_queue(); let qh = queue.handle(); let display = conn.display(); display.get_registry(&qh, ()); let mut globals = Globals::default(); queue .roundtrip(&mut globals) .unwrap_or_else(|e| fail(&format!("wayland roundtrip failed: {e}"))); let manager = globals.manager.as_ref().unwrap_or_else(|| { fail( "this compositor does not offer zwlr_virtual_pointer_manager_v1, so a pointer cannot \ be synthesised; sway and every wlroots compositor do", ) }); let pointer = manager.create_virtual_pointer(globals.seat.as_ref(), &qh, ()); // Put the pointer where the gesture starts and let the compositor // settle before anything is pressed. Without this the press is // dropped: sway has just learned about this pointer, and a button // sent in the same breath as the motion that first puts it over a // window arrives before there is a focused surface to send it to -- // winit sees `CursorEntered`, the moves and the *release*, never the // press, so the gesture reads as a hover and nothing scrolls. Found // by printing winit's own events; the settle is what fixed it. if let Some(first) = script.samples.first() { pointer.motion_absolute(0, first.pos.x as u32, first.pos.y as u32, width, height); pointer.frame(); conn.flush() .unwrap_or_else(|e| fail(&format!("flush: {e}"))); std::thread::sleep(SETTLE); } let mut previous = 0; for sample in &script.samples { std::thread::sleep(Duration::from_millis(sample.t_ms - previous)); previous = sample.t_ms; let t = sample.t_ms as u32; pointer.motion_absolute(t, sample.pos.x as u32, sample.pos.y as u32, width, height); pointer.frame(); // The button goes in a frame of its own, *after* the motion has // been committed. Sent in the same frame as the motion that // first puts the pointer over the window, sway drops it: the // client sees `CursorEntered` and the moves but never a // `MouseInput { state: Pressed }`, so the whole gesture reads as // a hover and nothing scrolls. Found exactly that way, by // printing winit's events. let state = match sample.action { TouchAction::Down => Some(ButtonState::Pressed), TouchAction::Up => Some(ButtonState::Released), TouchAction::Move => None, }; if let Some(state) = state { pointer.button(t, BTN_LEFT, state); pointer.frame(); } conn.flush() .unwrap_or_else(|e| fail(&format!("flush: {e}"))); } pointer.destroy(); conn.flush().ok(); } fn parse(text: &str, what: &str) -> u32 { text.parse() .unwrap_or_else(|_| fail(&format!("{what} is not a whole number: {text:?}"))) } fn fail(message: &str) -> ! { eprintln!("replay-touch: {message}"); std::process::exit(1); }