Reviewed-on: iris/iris#17 Reviewed-by: iris <2+iris@noreply.localhost> Co-authored-by: iris-ai <4+iris-ai@noreply.localhost>
142 lines
5.1 KiB
Rust
142 lines
5.1 KiB
Rust
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<wl_seat::WlSeat>,
|
|
manager: Option<ZwlrVirtualPointerManagerV1>,
|
|
}
|
|
|
|
impl Dispatch<wl_registry::WlRegistry, ()> for Globals {
|
|
fn event(
|
|
state: &mut Self,
|
|
registry: &wl_registry::WlRegistry,
|
|
event: wl_registry::Event,
|
|
_: &(),
|
|
_: &Connection,
|
|
qh: &QueueHandle<Self>,
|
|
) {
|
|
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<String> = 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);
|
|
}
|