use std::sync::OnceLock;
use x11rb::connection::Connection as _;
use x11rb::protocol::xproto::Window;
use x11rb::protocol::xtest::ConnectionExt as _;
use x11rb::rust_connection::RustConnection;
use x11rb::wrapper::ConnectionExt as _;
use x11rb::CURRENT_TIME;
use crate::engine::{EngineError, EngineResult};
#[derive(Debug, Clone, Copy)]
pub(crate) struct ScreenPoint {
pub x: i32,
pub y: i32,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Button {
Left,
#[allow(dead_code)]
Middle,
#[allow(dead_code)]
Right,
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum InputEvent {
Move(ScreenPoint),
Press(Button),
Release(Button),
}
pub(crate) trait InputDispatcher: Send + Sync {
fn backend_name(&self) -> &'static str;
fn dispatch(&self, ev: InputEvent) -> EngineResult<()>;
fn flush(&self) -> EngineResult<()>;
}
pub(crate) fn detect() -> Option<Box<dyn InputDispatcher>> {
if is_wayland_session() {
if let Some(b) = LibeiDispatcher::try_new() {
return Some(Box::new(b));
}
}
if let Some(b) = XtestDispatcher::try_new() {
return Some(Box::new(b));
}
None
}
fn is_wayland_session() -> bool {
std::env::var_os("WAYLAND_DISPLAY").is_some()
&& std::env::var("XDG_SESSION_TYPE").as_deref() == Ok("wayland")
}
static DISPATCHER: OnceLock<Option<Box<dyn InputDispatcher + 'static>>> = OnceLock::new();
pub(crate) fn dispatcher() -> EngineResult<&'static (dyn InputDispatcher + 'static)> {
let cell = DISPATCHER.get_or_init(detect);
match cell.as_deref() {
Some(d) => Ok(d),
None => Err(EngineError::Unsupported {
engine: "wpe",
primitive: "cursor_op",
}),
}
}
#[allow(dead_code)]
pub(crate) fn active_backend_name() -> Option<&'static str> {
DISPATCHER
.get_or_init(detect)
.as_deref()
.map(InputDispatcher::backend_name)
}
const XT_MOTION_NOTIFY: u8 = 6;
const XT_BUTTON_PRESS: u8 = 4;
const XT_BUTTON_RELEASE: u8 = 5;
struct XtestDispatcher {
conn: RustConnection,
root: Window,
}
impl XtestDispatcher {
fn try_new() -> Option<Self> {
let (conn, screen_num) = RustConnection::connect(None).ok()?;
let root = conn.setup().roots.get(screen_num)?.root;
Some(Self { conn, root })
}
}
impl InputDispatcher for XtestDispatcher {
fn backend_name(&self) -> &'static str {
"xtest"
}
fn dispatch(&self, ev: InputEvent) -> EngineResult<()> {
let (type_, detail, root_x, root_y) = match ev {
InputEvent::Move(p) => (
XT_MOTION_NOTIFY,
0_u8,
clamp_i16(p.x),
clamp_i16(p.y),
),
InputEvent::Press(b) => (XT_BUTTON_PRESS, button_code(b), 0, 0),
InputEvent::Release(b) => (XT_BUTTON_RELEASE, button_code(b), 0, 0),
};
self.conn
.xtest_fake_input(type_, detail, CURRENT_TIME, self.root, root_x, root_y, 0)
.map_err(|e| EngineError::Other(format!("xtest_fake_input: {e}")))?
.ignore_error();
Ok(())
}
fn flush(&self) -> EngineResult<()> {
self.conn
.flush()
.map_err(|e| EngineError::Other(format!("XFlush: {e}")))?;
self.conn
.sync()
.map_err(|e| EngineError::Other(format!("XSync: {e}")))?;
Ok(())
}
}
#[allow(clippy::cast_possible_truncation)]
fn clamp_i16(v: i32) -> i16 {
v.clamp(i32::from(i16::MIN), i32::from(i16::MAX)) as i16
}
fn button_code(b: Button) -> u8 {
match b {
Button::Left => 1,
Button::Middle => 2,
Button::Right => 3,
}
}
struct LibeiDispatcher;
impl LibeiDispatcher {
fn try_new() -> Option<Self> {
None
}
}
impl InputDispatcher for LibeiDispatcher {
fn backend_name(&self) -> &'static str {
"libei"
}
fn dispatch(&self, _ev: InputEvent) -> EngineResult<()> {
Err(EngineError::Unsupported {
engine: "wpe",
primitive: "cursor_op:libei",
})
}
fn flush(&self) -> EngineResult<()> {
Ok(())
}
}