From 4a20e037ca80fba5ea293843577aadb92b701eee Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Thu, 12 Oct 2023 17:31:55 -0700 Subject: [PATCH] Ei protocol support using `reis` The Ei protocol is needed for the xdg-desktop-portal `RemoteDesktop` portal to emulate input devices, as well as by the `InputCapture` portal for Synergy-like uses (input-leap supports Wayland with this portal). This is quite incomplete, but the `type-text` example in `reis` now works in Anvil. Reis could still use somewhat better higher-level servere-side APIs. It might make sense if ei exposed seats and devices matching those in Smithay (or would compositors not always want to do that?). Not sure how best to handle that. Receiver contexts for the `InputCapture` portal are also a bit more complicated to implement. Those involve capturing input once the cursor crosses outside the display. That isn't implemented at all here yet. --- Cargo.toml | 2 +- anvil/src/lib.rs | 1 + anvil/src/libei.rs | 32 +++ anvil/src/udev.rs | 2 + src/backend/libei/mod.rs | 566 +++++++++++++++++++++++++++++++++++++++ src/backend/mod.rs | 2 + src/reexports.rs | 1 + 7 files changed, 605 insertions(+), 1 deletion(-) create mode 100644 anvil/src/libei.rs create mode 100644 src/backend/libei/mod.rs diff --git a/Cargo.toml b/Cargo.toml index 2d7ce369899f..11b4d354e891 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,7 +71,7 @@ encoding_rs = { version = "0.8.33", optional = true } profiling = "1.0" smallvec = "1.11" pixman = { version = "0.1.0", features = ["drm-fourcc"], optional = true } - +reis = { git = "https://github.com/ids1024/reis", features = ["calloop"] } [dev-dependencies] clap = { version = "4", features = ["derive"] } diff --git a/anvil/src/lib.rs b/anvil/src/lib.rs index 2e3833fb9c69..3bae4b638bef 100644 --- a/anvil/src/lib.rs +++ b/anvil/src/lib.rs @@ -11,6 +11,7 @@ pub mod cursor; pub mod drawing; pub mod focus; pub mod input_handler; +pub mod libei; pub mod render; pub mod shell; pub mod state; diff --git a/anvil/src/libei.rs b/anvil/src/libei.rs new file mode 100644 index 000000000000..74487745f834 --- /dev/null +++ b/anvil/src/libei.rs @@ -0,0 +1,32 @@ +use reis::calloop::EisListenerSource; +use reis::eis; +use smithay::reexports::reis; + +use smithay::backend::libei::EiInput; +use smithay::reexports::calloop; + +use crate::state::AnvilState; +use crate::udev::UdevData; + +pub fn listen_eis(handle: &calloop::LoopHandle<'static, AnvilState>) { + let path = reis::default_socket_path().unwrap(); + std::fs::remove_file(&path); // XXX in use? + let listener = eis::Listener::bind(&path).unwrap(); + let listener_source = EisListenerSource::new(listener); + + std::env::set_var("LIBEI_SOCKET", path); + + let handle_clone = handle.clone(); + handle + .insert_source(listener_source, move |context, _, _| { + let source = EiInput::new(context); + handle_clone + .insert_source(source, |event, _, data| { + let dh = data.display_handle.clone(); + data.process_input_event(&dh, event); + }) + .unwrap(); + Ok(calloop::PostAction::Continue) + }) + .unwrap(); +} diff --git a/anvil/src/udev.rs b/anvil/src/udev.rs index 53a3546ea1b9..9dd1329c9e8f 100644 --- a/anvil/src/udev.rs +++ b/anvil/src/udev.rs @@ -464,6 +464,8 @@ pub fn run_udev() { #[cfg(feature = "xwayland")] state.start_xwayland(); + crate::libei::listen_eis(&event_loop.handle()); + /* * And run our loop */ diff --git a/src/backend/libei/mod.rs b/src/backend/libei/mod.rs new file mode 100644 index 000000000000..87bbee6012fe --- /dev/null +++ b/src/backend/libei/mod.rs @@ -0,0 +1,566 @@ +// Have some way to set if socket can be used for reciver, sender, or both? +// - restrict what devices it can use? +// For emulation: +// - create a seat for each seat +// - create a device for pointer, touch, keyboard, if the seat has that +// * send keymap for keyboard +// - direct emulated input on these to the relevant handles +// For reciever context: +// - do we need to pass the application any requests from the client? +// re-export listener source? + +use calloop::{EventSource, PostAction, Readiness, Token, TokenFactory}; +use once_cell::sync::Lazy; +use reis::{ + calloop::{ConnectedContextState, EisRequestSourceEvent}, + eis::{self, device::DeviceType}, + request::{self, DeviceCapability, EisRequest}, +}; +use rustix::fd::AsFd; +use std::{collections::HashMap, ffi::CString, io, path::PathBuf}; +use xkbcommon::xkb; + +use crate::{ + backend::input::{self, InputBackend, InputEvent}, + input::keyboard::XkbConfig, + utils::sealed_file::SealedFile, +}; + +static SERVER_INTERFACES: Lazy> = Lazy::new(|| { + let mut m = HashMap::new(); + m.insert("ei_callback", 1); + m.insert("ei_connection", 1); + m.insert("ei_seat", 1); + m.insert("ei_device", 1); + m.insert("ei_pingpong", 1); + m.insert("ei_keyboard", 1); + m.insert("ei_pointer", 1); + m.insert("ei_pointer_absolute", 1); + m.insert("ei_button", 1); + m.insert("ei_scroll", 1); + m.insert("ei_touchscreen", 1); + m +}); + +struct SenderState { + name: Option, + connection: eis::Connection, + seat: eis::Seat, + last_serial: u32, +} + +impl SenderState { + fn new(name: Option, connection: eis::Connection) -> Self { + // TODO create seat, etc. + // check protocol versions + let seat = connection.seat(1); + seat.name("default"); + seat.capability(0x2, "ei_pointer"); + seat.capability(0x4, "ei_pointer_absolute"); + seat.capability(0x8, "ei_button"); + seat.capability(0x10, "ei_scroll"); + seat.capability(0x20, "ei_keyboard"); + seat.capability(0x40, "ei_touchscreen"); + seat.done(); + Self { + name, + connection, + seat, + last_serial: 0, + } + } +} + +#[derive(Debug)] +pub struct EiInput { + source: reis::calloop::EisRequestSource, + seat: Option, +} + +impl EiInput { + pub fn new(context: eis::Context) -> Self { + Self { + source: reis::calloop::EisRequestSource::new(context, &SERVER_INTERFACES, 0), + seat: None, + } + } +} + +fn disconnected( + connected_state: &ConnectedContextState, + reason: eis::connection::DisconnectReason, + explaination: &str, +) -> io::Result { + connected_state.connection.disconnected( + connected_state.request_converter.last_serial(), + reason, + explaination, + ); + connected_state.context.flush(); + Ok(calloop::PostAction::Remove) +} + +impl InputBackend for EiInput { + type Device = request::Device; + type KeyboardKeyEvent = request::KeyboardKey; + type PointerAxisEvent = ScrollEvent; + type PointerButtonEvent = request::Button; + type PointerMotionEvent = request::PointerMotion; + type PointerMotionAbsoluteEvent = request::PointerMotionAbsolute; + + type GestureSwipeBeginEvent = input::UnusedEvent; + type GestureSwipeUpdateEvent = input::UnusedEvent; + type GestureSwipeEndEvent = input::UnusedEvent; + type GesturePinchBeginEvent = input::UnusedEvent; + type GesturePinchUpdateEvent = input::UnusedEvent; + type GesturePinchEndEvent = input::UnusedEvent; + type GestureHoldBeginEvent = input::UnusedEvent; + type GestureHoldEndEvent = input::UnusedEvent; + + type TouchDownEvent = request::TouchDown; + type TouchUpEvent = request::TouchUp; + type TouchMotionEvent = request::TouchMotion; + type TouchCancelEvent = input::UnusedEvent; // XXX? + type TouchFrameEvent = input::UnusedEvent; // XXX + + type TabletToolAxisEvent = input::UnusedEvent; + type TabletToolProximityEvent = input::UnusedEvent; + type TabletToolTipEvent = input::UnusedEvent; + type TabletToolButtonEvent = input::UnusedEvent; + + type SwitchToggleEvent = input::UnusedEvent; + + type SpecialEvent = input::UnusedEvent; +} + +impl input::Device for request::Device { + fn id(&self) -> String { + self.name().unwrap_or("").to_string() + } + + fn name(&self) -> String { + self.name().unwrap_or("").to_string() + } + + fn has_capability(&self, capability: input::DeviceCapability) -> bool { + if let Ok(capability) = DeviceCapability::try_from(capability) { + self.has_capability(capability) + } else { + false + } + } + + fn usb_id(&self) -> Option<(u32, u32)> { + None + } + + fn syspath(&self) -> Option { + None + } +} + +impl input::Event for T { + fn time(&self) -> u64 { + request::EventTime::time(self) + } + + fn device(&self) -> request::Device { + request::DeviceEvent::device(self).clone() + } +} + +impl input::KeyboardKeyEvent for request::KeyboardKey { + fn key_code(&self) -> u32 { + self.key + } + + fn state(&self) -> input::KeyState { + match self.state { + eis::keyboard::KeyState::Released => input::KeyState::Released, + eis::keyboard::KeyState::Press => input::KeyState::Pressed, + } + } + + fn count(&self) -> u32 { + 1 + } +} + +pub enum ScrollEvent { + Delta(request::ScrollDelta), + Cancel(request::ScrollCancel), + Discrete(request::ScrollDiscrete), + Stop(request::ScrollStop), +} + +impl input::Event for ScrollEvent { + fn time(&self) -> u64 { + match self { + Self::Delta(evt) => evt.time(), + Self::Cancel(evt) => evt.time(), + Self::Discrete(evt) => evt.time(), + Self::Stop(evt) => evt.time(), + } + } + + fn device(&self) -> request::Device { + match self { + Self::Delta(evt) => evt.device(), + Self::Cancel(evt) => evt.device(), + Self::Discrete(evt) => evt.device(), + Self::Stop(evt) => evt.device(), + } + } +} + +impl input::PointerAxisEvent for ScrollEvent { + fn amount(&self, axis: input::Axis) -> Option { + match self { + Self::Delta(evt) => match axis { + input::Axis::Horizontal if evt.dx != 0.0 => Some(evt.dx.into()), + input::Axis::Vertical if evt.dy != 0.0 => Some(evt.dy.into()), + _ => None, + }, + // Same as Mutter + Self::Cancel(evt) => match axis { + input::Axis::Horizontal if evt.x => Some(0.01), + input::Axis::Vertical if evt.y => Some(0.01), + _ => None, + }, + Self::Discrete(_evt) => None, + Self::Stop(evt) => match axis { + input::Axis::Horizontal if evt.x => Some(0.0), + input::Axis::Vertical if evt.y => Some(0.0), + _ => None, + }, + } + } + + fn amount_v120(&self, axis: input::Axis) -> Option { + match self { + Self::Discrete(evt) => match axis { + input::Axis::Horizontal if evt.discrete_dx != 0 => Some(evt.discrete_dx.into()), + input::Axis::Vertical if evt.discrete_dy != 0 => Some(evt.discrete_dy.into()), + _ => None, + }, + _ => None, + } + } + + fn source(&self) -> input::AxisSource { + // Mutter seems to also use wheel for all the scroll events + input::AxisSource::Wheel + } + + fn relative_direction(&self, _axis: input::Axis) -> input::AxisRelativeDirection { + input::AxisRelativeDirection::Identical + } +} + +impl input::PointerButtonEvent for request::Button { + fn button_code(&self) -> u32 { + self.button + } + + fn state(&self) -> input::ButtonState { + match self.state { + eis::button::ButtonState::Press => input::ButtonState::Pressed, + eis::button::ButtonState::Released => input::ButtonState::Released, + } + } +} + +impl input::PointerMotionEvent for request::PointerMotion { + fn delta_x(&self) -> f64 { + self.dx.into() + } + + fn delta_y(&self) -> f64 { + self.dy.into() + } + + fn delta_x_unaccel(&self) -> f64 { + self.dx.into() + } + + fn delta_y_unaccel(&self) -> f64 { + self.dy.into() + } +} + +impl input::PointerMotionAbsoluteEvent for request::PointerMotionAbsolute {} +impl input::AbsolutePositionEvent for request::PointerMotionAbsolute { + fn x(&self) -> f64 { + self.dx_absolute.into() + } + + fn y(&self) -> f64 { + self.dy_absolute.into() + } + + fn x_transformed(&self, _width: i32) -> f64 { + // XXX ? + self.dx_absolute.into() + } + + fn y_transformed(&self, _height: i32) -> f64 { + self.dy_absolute.into() + } +} + +impl input::TouchDownEvent for request::TouchDown {} +impl input::TouchEvent for request::TouchDown { + fn slot(&self) -> input::TouchSlot { + Some(self.touch_id).into() + } +} +impl input::AbsolutePositionEvent for request::TouchDown { + fn x(&self) -> f64 { + self.x.into() + } + + fn y(&self) -> f64 { + self.y.into() + } + + fn x_transformed(&self, _width: i32) -> f64 { + // XXX ? + self.x.into() + } + + fn y_transformed(&self, _height: i32) -> f64 { + self.y.into() + } +} + +impl input::TouchUpEvent for request::TouchUp {} +impl input::TouchEvent for request::TouchUp { + fn slot(&self) -> input::TouchSlot { + Some(self.touch_id).into() + } +} + +impl input::TouchMotionEvent for request::TouchMotion {} +impl input::TouchEvent for request::TouchMotion { + fn slot(&self) -> input::TouchSlot { + Some(self.touch_id).into() + } +} +impl input::AbsolutePositionEvent for request::TouchMotion { + fn x(&self) -> f64 { + self.x.into() + } + + fn y(&self) -> f64 { + self.y.into() + } + + fn x_transformed(&self, _width: i32) -> f64 { + // XXX ? + self.x.into() + } + + fn y_transformed(&self, _height: i32) -> f64 { + self.y.into() + } +} + +impl EventSource for EiInput { + type Event = InputEvent; + type Metadata = (); + type Ret = (); + type Error = io::Error; + + fn process_events( + &mut self, + readiness: Readiness, + token: Token, + mut cb: F, + ) -> Result::Error> + where + F: FnMut(InputEvent, &mut ()) -> (), + { + self.source + .process_events(readiness, token, |event, connected_state| { + match event { + Ok(EisRequestSourceEvent::Connected) => { + let seat = connected_state.request_converter.add_seat( + Some("default"), + &[ + DeviceCapability::Pointer, + DeviceCapability::PointerAbsolute, + DeviceCapability::Keyboard, + DeviceCapability::Touch, + DeviceCapability::Scroll, + DeviceCapability::Button, + ], + ); + + self.seat = Some(seat); + } + Ok(EisRequestSourceEvent::Request(EisRequest::Disconnect)) => { + return Ok(PostAction::Remove); + } + Ok(EisRequestSourceEvent::Request(EisRequest::Bind(request))) => { + let capabilities = request.capabilities; + + // TODO Handle in converter + if capabilities & 0x7e != capabilities { + let serial = connected_state.request_converter.next_serial(); + request.seat.eis_seat().destroyed(serial); + return disconnected( + connected_state, + eis::connection::DisconnectReason::Value, + "Invalid capabilities", + ); + } + + if connected_state.has_interface("ei_keyboard") + && capabilities & 2 << DeviceCapability::Keyboard as u64 != 0 + { + // XXX use seat keymap + let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS); + let keymap = XkbConfig::default().compile_keymap(&context).unwrap(); + let keymap_text = keymap.get_as_string(xkb::KEYMAP_FORMAT_TEXT_V1); + let file = SealedFile::with_data( + CString::new("eis-keymap").unwrap(), + keymap_text.as_bytes(), + ) + .unwrap(); + + let device = connected_state.request_converter.add_device( + self.seat.as_ref().unwrap(), + Some("keyboard"), + DeviceType::Virtual, + &[DeviceCapability::Keyboard], + |device| { + let keyboard = device.interface::().unwrap(); + keyboard.keymap( + eis::keyboard::KeymapType::Xkb, + keymap_text.len() as _, + file.as_fd(), + ); + }, + ); + } + + // XXX button/etc should be on same object + if connected_state.has_interface("ei_pointer") + && capabilities & 2 << DeviceCapability::Pointer as u64 != 0 + { + connected_state.request_converter.add_device( + self.seat.as_ref().unwrap(), + Some("pointer"), + DeviceType::Virtual, + &[DeviceCapability::Pointer], + |_| {}, + ); + } + + if connected_state.has_interface("ei_touchscreen") + && capabilities & 2 << DeviceCapability::Touch as u64 != 0 + { + connected_state.request_converter.add_device( + self.seat.as_ref().unwrap(), + Some("touch"), + DeviceType::Virtual, + &[DeviceCapability::Touch], + |_| {}, + ); + } + + if connected_state.has_interface("ei_pointer_absolute") + && capabilities & 2 << DeviceCapability::PointerAbsolute as u64 != 0 + { + connected_state.request_converter.add_device( + self.seat.as_ref().unwrap(), + Some("pointer-abs"), + DeviceType::Virtual, + &[DeviceCapability::PointerAbsolute], + |_| {}, + ); + } + + // TODO create devices; compare against current bitflag + } + Ok(EisRequestSourceEvent::Request(request)) => { + if let Some(input_event) = convert_request(request) { + cb(input_event, &mut ()); + } + } + Ok(EisRequestSourceEvent::InvalidObject(_object_id)) => {} + Err(err) => { + tracing::error!("Libei client error: {}", err); + return Ok(PostAction::Remove); + } + } + connected_state.context.flush(); + Ok(PostAction::Continue) + }) + } + + fn register( + &mut self, + poll: &mut calloop::Poll, + token_factory: &mut TokenFactory, + ) -> Result<(), calloop::Error> { + self.source.register(poll, token_factory) + } + + fn reregister( + &mut self, + poll: &mut calloop::Poll, + token_factory: &mut TokenFactory, + ) -> Result<(), calloop::Error> { + self.source.reregister(poll, token_factory) + } + + fn unregister(&mut self, poll: &mut calloop::Poll) -> Result<(), calloop::Error> { + self.source.unregister(poll) + } +} + +fn convert_request(request: EisRequest) -> Option> { + match request { + EisRequest::KeyboardKey(event) => Some(InputEvent::Keyboard { event }), + EisRequest::PointerMotion(event) => Some(InputEvent::PointerMotion { event }), + EisRequest::PointerMotionAbsolute(event) => Some(InputEvent::PointerMotionAbsolute { event }), + EisRequest::Button(event) => Some(InputEvent::PointerButton { event }), + EisRequest::ScrollDelta(event) => Some(InputEvent::PointerAxis { + event: ScrollEvent::Delta(event), + }), + EisRequest::ScrollStop(event) => Some(InputEvent::PointerAxis { + event: ScrollEvent::Stop(event), + }), + EisRequest::ScrollCancel(event) => Some(InputEvent::PointerAxis { + event: ScrollEvent::Cancel(event), + }), + EisRequest::ScrollDiscrete(event) => Some(InputEvent::PointerAxis { + event: ScrollEvent::Discrete(event), + }), + EisRequest::TouchDown(event) => Some(InputEvent::TouchDown { event }), + EisRequest::TouchUp(event) => Some(InputEvent::TouchUp { event }), + EisRequest::TouchMotion(event) => Some(InputEvent::TouchMotion { event }), + EisRequest::Frame(_) => None, // TODO + EisRequest::Disconnect + | EisRequest::Bind(_) + | EisRequest::DeviceStartEmulating(_) + | EisRequest::DeviceStopEmulating(_) => None, + } +} + +// XXX not a direct match? +impl TryFrom for DeviceCapability { + type Error = (); + fn try_from(other: input::DeviceCapability) -> Result { + match other { + input::DeviceCapability::Gesture => Err(()), + input::DeviceCapability::Keyboard => Ok(DeviceCapability::Keyboard), + input::DeviceCapability::Pointer => Ok(DeviceCapability::Pointer), + input::DeviceCapability::Switch => Err(()), + input::DeviceCapability::TabletPad => Err(()), + input::DeviceCapability::TabletTool => Err(()), + input::DeviceCapability::Touch => Ok(DeviceCapability::Touch), + } + } +} diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 6e4aecde5daf..4d0e7707b0f5 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -103,6 +103,8 @@ pub mod winit; #[cfg(feature = "backend_x11")] pub mod x11; +pub mod libei; + /// Error that can happen when swapping buffers. #[derive(Debug, thiserror::Error)] pub enum SwapBuffersError { diff --git a/src/reexports.rs b/src/reexports.rs index ebe577d32ca5..91c0db911c60 100644 --- a/src/reexports.rs +++ b/src/reexports.rs @@ -13,6 +13,7 @@ pub use glow; pub use input; #[cfg(feature = "renderer_pixman")] pub use pixman; +pub use reis; pub use rustix; #[cfg(feature = "backend_udev")] pub use udev;