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 exposes a relatively simple API for a compositor to create EI seats and devices, mirroring the `Seat` API. It also provides an `InputBackend` that converts emulated input to Smithay input events. 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. We may want to change things in the future to accommodate receiver contexts, etc. But this API is fairly minimal to use, so future breaking changes shouldn't be too challenging for a compositor to adapt to.
This commit is contained in:
parent
66fc28b98e
commit
7878e6ed11
7 changed files with 852 additions and 1 deletions
|
|
@ -74,6 +74,7 @@ aliasable = { version = "0.1.3", optional = true }
|
||||||
atomic_float = "1.1.0"
|
atomic_float = "1.1.0"
|
||||||
sha2 = "0.10.9"
|
sha2 = "0.10.9"
|
||||||
tracy-client = { version = "0.18.4", default-features = false, optional = true }
|
tracy-client = { version = "0.18.4", default-features = false, optional = true }
|
||||||
|
reis = { version = "0.6.0", features = ["calloop"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
clap = { version = "4", features = ["derive"] }
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|
@ -87,7 +88,7 @@ pkg-config = { version = "0.3.17", optional = true }
|
||||||
cc = { version = "1.0.79", optional = true }
|
cc = { version = "1.0.79", optional = true }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["backend_drm", "backend_gbm", "backend_libinput", "backend_udev", "backend_session_libseat", "backend_x11", "backend_winit", "desktop", "renderer_gl", "renderer_pixman", "renderer_multi", "xwayland", "wayland_frontend", "backend_vulkan"]
|
default = ["backend_drm", "backend_gbm", "backend_libinput", "backend_udev", "backend_session_libseat", "backend_x11", "backend_winit", "desktop", "renderer_gl", "renderer_pixman", "renderer_multi", "xwayland", "wayland_frontend", "backend_vulkan", "backend_libei"]
|
||||||
backend_winit = ["winit", "backend_egl", "wayland-client", "wayland-cursor", "wayland-egl", "renderer_gl"]
|
backend_winit = ["winit", "backend_egl", "wayland-client", "wayland-cursor", "wayland-egl", "renderer_gl"]
|
||||||
backend_x11 = ["x11rb", "x11rb/dri3", "x11rb/xfixes", "x11rb/xinput", "x11rb/present", "x11rb_event_source", "backend_gbm", "backend_drm", "backend_egl"]
|
backend_x11 = ["x11rb", "x11rb/dri3", "x11rb/xfixes", "x11rb/xinput", "x11rb/present", "x11rb_event_source", "backend_gbm", "backend_drm", "backend_egl"]
|
||||||
backend_drm = ["drm", "drm-ffi"]
|
backend_drm = ["drm", "drm-ffi"]
|
||||||
|
|
@ -100,6 +101,7 @@ backend_session = []
|
||||||
backend_udev = ["udev", "input/udev"]
|
backend_udev = ["udev", "input/udev"]
|
||||||
backend_vulkan = ["ash", "scopeguard"]
|
backend_vulkan = ["ash", "scopeguard"]
|
||||||
backend_session_libseat = ["backend_session", "libseat"]
|
backend_session_libseat = ["backend_session", "libseat"]
|
||||||
|
backend_libei = ["wayland_frontend"]
|
||||||
desktop = []
|
desktop = []
|
||||||
renderer_gl = ["gl_generator", "backend_egl"]
|
renderer_gl = ["gl_generator", "backend_egl"]
|
||||||
renderer_glow = ["renderer_gl", "glow"]
|
renderer_glow = ["renderer_gl", "glow"]
|
||||||
|
|
|
||||||
301
src/backend/libei/input.rs
Normal file
301
src/backend/libei/input.rs
Normal file
|
|
@ -0,0 +1,301 @@
|
||||||
|
// Implementation of `InputBackend` for reis requests
|
||||||
|
|
||||||
|
use reis::{
|
||||||
|
eis,
|
||||||
|
request::{self, DeviceCapability},
|
||||||
|
};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use crate::backend::input::{self, InputBackend};
|
||||||
|
|
||||||
|
use super::EiInput;
|
||||||
|
|
||||||
|
/// Generic ei scroll event
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum ScrollEvent {
|
||||||
|
/// Continuous scroll event
|
||||||
|
Delta(request::ScrollDelta),
|
||||||
|
/// Scroll cancel event
|
||||||
|
Cancel(request::ScrollCancel),
|
||||||
|
/// Discrete scroll event
|
||||||
|
Discrete(request::ScrollDiscrete),
|
||||||
|
/// Scroll stop event
|
||||||
|
Stop(request::ScrollStop),
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = request::TouchCancel;
|
||||||
|
type TouchFrameEvent = input::UnusedEvent;
|
||||||
|
|
||||||
|
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 {
|
||||||
|
use reis::Interface;
|
||||||
|
use rustix::fd::{AsFd, AsRawFd};
|
||||||
|
let object = self.device().as_object();
|
||||||
|
let id = object.id();
|
||||||
|
// XXX don't panic?
|
||||||
|
// don't need to be valid after calloop source is destroyed?
|
||||||
|
// - (source keeps backend open, so weak handle upgrades)
|
||||||
|
let fd = object.backend().unwrap().as_fd().as_raw_fd();
|
||||||
|
format!("ei-{fd}-{id}")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn name(&self) -> String {
|
||||||
|
self.name().unwrap_or("").to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_capability(&self, capability: input::DeviceCapability) -> bool {
|
||||||
|
match capability {
|
||||||
|
input::DeviceCapability::Gesture => false,
|
||||||
|
input::DeviceCapability::Keyboard => self.has_capability(DeviceCapability::Keyboard),
|
||||||
|
input::DeviceCapability::Pointer => {
|
||||||
|
self.has_capability(DeviceCapability::Pointer)
|
||||||
|
|| self.has_capability(DeviceCapability::PointerAbsolute)
|
||||||
|
}
|
||||||
|
input::DeviceCapability::Switch => false,
|
||||||
|
input::DeviceCapability::TabletPad => false,
|
||||||
|
input::DeviceCapability::TabletTool => false,
|
||||||
|
input::DeviceCapability::Touch => self.has_capability(DeviceCapability::Touch),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usb_id(&self) -> Option<(u32, u32)> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn syspath(&self) -> Option<PathBuf> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: request::DeviceEvent + request::EventTime> input::Event<EiInput> for T {
|
||||||
|
fn time(&self) -> u64 {
|
||||||
|
request::EventTime::time(self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn device(&self) -> request::Device {
|
||||||
|
request::DeviceEvent::device(self).clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl input::KeyboardKeyEvent<EiInput> for request::KeyboardKey {
|
||||||
|
fn key_code(&self) -> input::Keycode {
|
||||||
|
input::Keycode::from(self.key + 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl input::Event<EiInput> 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<EiInput> for ScrollEvent {
|
||||||
|
fn amount(&self, axis: input::Axis) -> Option<f64> {
|
||||||
|
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<f64> {
|
||||||
|
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<EiInput> 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<EiInput> 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<EiInput> for request::PointerMotionAbsolute {}
|
||||||
|
impl input::AbsolutePositionEvent<EiInput> 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 {
|
||||||
|
self.dx_absolute.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn y_transformed(&self, _height: i32) -> f64 {
|
||||||
|
self.dy_absolute.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl input::TouchDownEvent<EiInput> for request::TouchDown {}
|
||||||
|
impl input::TouchEvent<EiInput> for request::TouchDown {
|
||||||
|
fn slot(&self) -> input::TouchSlot {
|
||||||
|
Some(self.touch_id).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl input::AbsolutePositionEvent<EiInput> for request::TouchDown {
|
||||||
|
fn x(&self) -> f64 {
|
||||||
|
self.x.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn y(&self) -> f64 {
|
||||||
|
self.y.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn x_transformed(&self, _width: i32) -> f64 {
|
||||||
|
self.x.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn y_transformed(&self, _height: i32) -> f64 {
|
||||||
|
self.y.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl input::TouchUpEvent<EiInput> for request::TouchUp {}
|
||||||
|
impl input::TouchEvent<EiInput> for request::TouchUp {
|
||||||
|
fn slot(&self) -> input::TouchSlot {
|
||||||
|
Some(self.touch_id).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl input::TouchMotionEvent<EiInput> for request::TouchMotion {}
|
||||||
|
impl input::TouchEvent<EiInput> for request::TouchMotion {
|
||||||
|
fn slot(&self) -> input::TouchSlot {
|
||||||
|
Some(self.touch_id).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl input::AbsolutePositionEvent<EiInput> for request::TouchMotion {
|
||||||
|
fn x(&self) -> f64 {
|
||||||
|
self.x.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn y(&self) -> f64 {
|
||||||
|
self.y.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn x_transformed(&self, _width: i32) -> f64 {
|
||||||
|
self.x.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn y_transformed(&self, _height: i32) -> f64 {
|
||||||
|
self.y.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl input::TouchCancelEvent<EiInput> for request::TouchCancel {}
|
||||||
|
impl input::TouchEvent<EiInput> for request::TouchCancel {
|
||||||
|
fn slot(&self) -> input::TouchSlot {
|
||||||
|
Some(self.touch_id).into()
|
||||||
|
}
|
||||||
|
}
|
||||||
269
src/backend/libei/mod.rs
Normal file
269
src/backend/libei/mod.rs
Normal file
|
|
@ -0,0 +1,269 @@
|
||||||
|
//! Input backend for `libei` sender contexts
|
||||||
|
//!
|
||||||
|
//! ``` no_run
|
||||||
|
//! # let event_loop: calloop::EventLoop<()> = todo!();
|
||||||
|
//!
|
||||||
|
//! use reis::{calloop::EisListenerSource, eis};
|
||||||
|
//! use smithay::{
|
||||||
|
//! backend::libei::{EiInput, EiInputEvent},
|
||||||
|
//! input::keyboard::XkbConfig,
|
||||||
|
//! };
|
||||||
|
//!
|
||||||
|
//! let listener = eis::Listener::bind_auto().unwrap();
|
||||||
|
//! let listener_source = EisListenerSource::new(listener);
|
||||||
|
//! let handle = event_loop.handle();
|
||||||
|
//! event_loop.handle().insert_source(listener_source, |context, _, _| {
|
||||||
|
//! let source = EiInput::new(context);
|
||||||
|
//! handle.insert_source(source, |event, connection, _data| {
|
||||||
|
//! match event {
|
||||||
|
//! EiInputEvent::Connected => {
|
||||||
|
//! let seat = connection.add_seat("default");
|
||||||
|
//! let _ = seat.add_keyboard("virtual keyboard", XkbConfig::default());
|
||||||
|
//! seat.add_pointer("virtual pointer");
|
||||||
|
//! seat.add_pointer_absolute("virtual absolute pointer");
|
||||||
|
//! seat.add_touch("virtual touch");
|
||||||
|
//! }
|
||||||
|
//! EiInputEvent::Disconnected => {}
|
||||||
|
//! EiInputEvent::Event(event) => {
|
||||||
|
//! // Pass input event to compositor's input event handling logic
|
||||||
|
//! // ...
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! }).unwrap();
|
||||||
|
//! Ok(calloop::PostAction::Continue)
|
||||||
|
//! }).unwrap();
|
||||||
|
//! ```
|
||||||
|
|
||||||
|
// TODO: Add helper for receiver contexts
|
||||||
|
|
||||||
|
use calloop::{EventSource, PostAction, Readiness, Token, TokenFactory};
|
||||||
|
use reis::{
|
||||||
|
calloop::EisRequestSourceEvent,
|
||||||
|
eis,
|
||||||
|
request::{DeviceCapability, EisRequest},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
io,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::backend::input::InputEvent;
|
||||||
|
|
||||||
|
mod input;
|
||||||
|
pub use input::ScrollEvent;
|
||||||
|
mod seat;
|
||||||
|
pub use seat::EiInputSeat;
|
||||||
|
|
||||||
|
/// An [`EventSource`] for receiving input from an EI sender context and
|
||||||
|
/// converting to [`InputEvent`]s.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EiInput {
|
||||||
|
source: reis::calloop::EisRequestSource,
|
||||||
|
connection: Option<EiInputConnection>,
|
||||||
|
event_sender: calloop::channel::Sender<InputEvent<EiInput>>,
|
||||||
|
channel_source: calloop::channel::Channel<InputEvent<EiInput>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EiInput {
|
||||||
|
/// Create an EI sender event source.
|
||||||
|
///
|
||||||
|
/// `context` should be a new EI socket that has not been used yet.
|
||||||
|
pub fn new(context: eis::Context) -> Self {
|
||||||
|
let (event_sender, channel_source) = calloop::channel::channel();
|
||||||
|
Self {
|
||||||
|
source: reis::calloop::EisRequestSource::new(context, 0),
|
||||||
|
event_sender,
|
||||||
|
channel_source,
|
||||||
|
connection: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A connection for an EI sender context that can be used to add seats and
|
||||||
|
/// devices.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct EiInputConnection(Arc<EiInputConnectionInner>);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct EiInputConnectionInner {
|
||||||
|
connection: reis::request::Connection,
|
||||||
|
event_sender: calloop::channel::Sender<InputEvent<EiInput>>,
|
||||||
|
seats: Mutex<Vec<EiInputSeat>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EiInputConnection {
|
||||||
|
fn new(
|
||||||
|
connection: reis::request::Connection,
|
||||||
|
event_sender: calloop::channel::Sender<InputEvent<EiInput>>,
|
||||||
|
) -> Self {
|
||||||
|
Self(Arc::new(EiInputConnectionInner {
|
||||||
|
connection,
|
||||||
|
event_sender,
|
||||||
|
seats: Mutex::new(Vec::new()),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a seat to the EI connection
|
||||||
|
pub fn add_seat(&self, name: &str) -> EiInputSeat {
|
||||||
|
let seat = self.0.connection.add_seat(
|
||||||
|
Some(name),
|
||||||
|
// Capabilities can't be added to a seat; so advertise them
|
||||||
|
// all, but only create relevant devices on bind.
|
||||||
|
DeviceCapability::Pointer
|
||||||
|
| DeviceCapability::PointerAbsolute
|
||||||
|
| DeviceCapability::Keyboard
|
||||||
|
| DeviceCapability::Touch
|
||||||
|
| DeviceCapability::Scroll
|
||||||
|
| DeviceCapability::Button,
|
||||||
|
);
|
||||||
|
let seat = EiInputSeat::new(self, seat, self.0.event_sender.clone());
|
||||||
|
self.0.seats.lock().unwrap().push(seat.clone());
|
||||||
|
seat
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send buffered events on EI socket
|
||||||
|
pub fn flush(&self) -> rustix::io::Result<()> {
|
||||||
|
self.0.connection.flush()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An event produced by an [`EiInput`] event source.
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum EiInputEvent {
|
||||||
|
/// The client has finished the EI handshake. Seats and devices can
|
||||||
|
/// then be added.
|
||||||
|
Connected,
|
||||||
|
/// The client has disconnected from the server.
|
||||||
|
Disconnected,
|
||||||
|
/// An input event has been received from the client.
|
||||||
|
Event(InputEvent<EiInput>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventSource for EiInput {
|
||||||
|
type Event = EiInputEvent;
|
||||||
|
type Metadata = EiInputConnection;
|
||||||
|
type Ret = ();
|
||||||
|
type Error = io::Error;
|
||||||
|
|
||||||
|
fn process_events<F>(
|
||||||
|
&mut self,
|
||||||
|
readiness: Readiness,
|
||||||
|
token: Token,
|
||||||
|
mut cb: F,
|
||||||
|
) -> Result<PostAction, <Self as EventSource>::Error>
|
||||||
|
where
|
||||||
|
F: FnMut(EiInputEvent, &mut EiInputConnection),
|
||||||
|
{
|
||||||
|
let _ = self.channel_source.process_events(readiness, token, |event, ()| {
|
||||||
|
if let calloop::channel::Event::Msg(event) = event {
|
||||||
|
// Can't create device until there's a connection, so no channel messages
|
||||||
|
let connection = self.connection.as_mut().unwrap();
|
||||||
|
cb(EiInputEvent::Event(event), connection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
self.source.process_events(readiness, token, |event, connection| {
|
||||||
|
// Wrap connection in `EiInputConnection` if not created yet
|
||||||
|
if self.connection.is_none() {
|
||||||
|
self.connection = Some(EiInputConnection::new(
|
||||||
|
connection.clone(),
|
||||||
|
self.event_sender.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let connection = self.connection.as_mut().unwrap();
|
||||||
|
|
||||||
|
match event {
|
||||||
|
Ok(EisRequestSourceEvent::Connected) => {
|
||||||
|
if connection.0.connection.context_type() == eis::handshake::ContextType::Receiver {
|
||||||
|
connection
|
||||||
|
.0
|
||||||
|
.connection
|
||||||
|
.disconnected(eis::connection::DisconnectReason::Disconnected, None);
|
||||||
|
let _ = connection.flush();
|
||||||
|
return Ok(PostAction::Remove);
|
||||||
|
}
|
||||||
|
cb(EiInputEvent::Connected, connection);
|
||||||
|
}
|
||||||
|
Ok(EisRequestSourceEvent::Request(EisRequest::Disconnect)) => {
|
||||||
|
cb(EiInputEvent::Disconnected, connection);
|
||||||
|
return Ok(PostAction::Remove);
|
||||||
|
}
|
||||||
|
Ok(EisRequestSourceEvent::Request(EisRequest::Bind(request))) => {
|
||||||
|
if let Some(seat) = connection
|
||||||
|
.0
|
||||||
|
.seats
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.find(|seat| **seat == request.seat)
|
||||||
|
{
|
||||||
|
seat.bind(request.capabilities);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(EisRequestSourceEvent::Request(request)) => {
|
||||||
|
if let Some(input_event) = convert_request(request) {
|
||||||
|
cb(EiInputEvent::Event(input_event), connection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
tracing::error!("Libei client error: {}", err);
|
||||||
|
return Ok(PostAction::Remove);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = connection.flush();
|
||||||
|
Ok(PostAction::Continue)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register(
|
||||||
|
&mut self,
|
||||||
|
poll: &mut calloop::Poll,
|
||||||
|
token_factory: &mut TokenFactory,
|
||||||
|
) -> Result<(), calloop::Error> {
|
||||||
|
self.channel_source.register(poll, token_factory)?;
|
||||||
|
self.source.register(poll, token_factory)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reregister(
|
||||||
|
&mut self,
|
||||||
|
poll: &mut calloop::Poll,
|
||||||
|
token_factory: &mut TokenFactory,
|
||||||
|
) -> Result<(), calloop::Error> {
|
||||||
|
self.channel_source.reregister(poll, token_factory)?;
|
||||||
|
self.source.reregister(poll, token_factory)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unregister(&mut self, poll: &mut calloop::Poll) -> Result<(), calloop::Error> {
|
||||||
|
self.channel_source.unregister(poll)?;
|
||||||
|
self.source.unregister(poll)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn convert_request(request: EisRequest) -> Option<InputEvent<EiInput>> {
|
||||||
|
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::TouchCancel(event) => Some(InputEvent::TouchCancel { event }),
|
||||||
|
EisRequest::Frame(_) => None,
|
||||||
|
EisRequest::Disconnect
|
||||||
|
| EisRequest::Bind(_)
|
||||||
|
| EisRequest::DeviceStartEmulating(_)
|
||||||
|
| EisRequest::DeviceStopEmulating(_) => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
274
src/backend/libei/seat.rs
Normal file
274
src/backend/libei/seat.rs
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
use reis::{eis::device::DeviceType, enumflags2::BitFlags, request::DeviceCapability};
|
||||||
|
use std::sync::{Arc, Mutex, Weak};
|
||||||
|
use xkbcommon::xkb;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
backend::input::InputEvent,
|
||||||
|
input::keyboard::{KeymapFile, XkbConfig},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::{EiInput, EiInputConnection, EiInputConnectionInner};
|
||||||
|
|
||||||
|
/// A seat advertised on an EI sender context
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct EiInputSeat(Arc<Mutex<EiInputSeatInner>>);
|
||||||
|
|
||||||
|
impl PartialEq<reis::request::Seat> for EiInputSeat {
|
||||||
|
fn eq(&self, other: &reis::request::Seat) -> bool {
|
||||||
|
self.0.lock().unwrap().seat == *other
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EiInputSeat {
|
||||||
|
pub(super) fn new(
|
||||||
|
connection: &EiInputConnection,
|
||||||
|
seat: reis::request::Seat,
|
||||||
|
event_sender: calloop::channel::Sender<InputEvent<EiInput>>,
|
||||||
|
) -> Self {
|
||||||
|
Self(Arc::new(Mutex::new(EiInputSeatInner {
|
||||||
|
connection: Arc::downgrade(&connection.0),
|
||||||
|
seat,
|
||||||
|
event_sender,
|
||||||
|
keyboard: None,
|
||||||
|
pointer: None,
|
||||||
|
pointer_absolute: None,
|
||||||
|
touch: None,
|
||||||
|
device_keyboard: None,
|
||||||
|
device_pointer: None,
|
||||||
|
device_pointer_absolute: None,
|
||||||
|
device_touch: None,
|
||||||
|
bound_capabilities: BitFlags::empty(),
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn bind(&self, capabilities: BitFlags<DeviceCapability>) {
|
||||||
|
let mut inner = self.0.lock().unwrap();
|
||||||
|
inner.bound_capabilities = capabilities;
|
||||||
|
inner.refresh_devices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a keyboard device to the EI seat
|
||||||
|
///
|
||||||
|
/// Calling on a seat that already has a keyboard device will remove
|
||||||
|
/// that device and add a new one.
|
||||||
|
pub fn add_keyboard(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
xkb_config: XkbConfig<'_>,
|
||||||
|
) -> Result<(), crate::input::keyboard::Error> {
|
||||||
|
let mut inner = self.0.lock().unwrap();
|
||||||
|
inner.device_keyboard = None;
|
||||||
|
let context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
|
||||||
|
let keymap = xkb_config.compile_keymap(&context).map_err(|_| {
|
||||||
|
tracing::debug!("Loading keymap from XkbConfig failed");
|
||||||
|
crate::input::keyboard::Error::BadKeymap
|
||||||
|
})?;
|
||||||
|
let keymap_file = KeymapFile::new(&keymap);
|
||||||
|
inner.keyboard = Some((name.to_string(), keymap_file));
|
||||||
|
inner.refresh_devices();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove keyboard device from the EI seat
|
||||||
|
pub fn remove_keyboard(&self) {
|
||||||
|
let mut inner = self.0.lock().unwrap();
|
||||||
|
inner.device_keyboard = None;
|
||||||
|
inner.keyboard = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a pointer device to the EI seat
|
||||||
|
///
|
||||||
|
/// The EI device will have button and scroll capabilities in addition
|
||||||
|
/// to the pointer capability.
|
||||||
|
///
|
||||||
|
/// Calling on a seat that already has a pointer device will remove
|
||||||
|
/// that device and add a new one.
|
||||||
|
pub fn add_pointer(&self, name: &str) {
|
||||||
|
let mut inner = self.0.lock().unwrap();
|
||||||
|
inner.device_pointer = None;
|
||||||
|
inner.pointer = Some(name.to_string());
|
||||||
|
inner.refresh_devices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove pointer device from the EI seat
|
||||||
|
pub fn remove_pointer(&self) {
|
||||||
|
let mut inner = self.0.lock().unwrap();
|
||||||
|
inner.device_pointer = None;
|
||||||
|
inner.pointer = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an absolute pointer device to the EI seat
|
||||||
|
///
|
||||||
|
/// The EI device will have button and scroll capabilities in addition
|
||||||
|
/// to the pointer absolute capability.
|
||||||
|
///
|
||||||
|
/// Calling on a seat that already has an absolute pointer device will remove
|
||||||
|
/// that device and add a new one.
|
||||||
|
pub fn add_pointer_absolute(&self, name: &str) {
|
||||||
|
let mut inner = self.0.lock().unwrap();
|
||||||
|
inner.device_pointer_absolute = None;
|
||||||
|
inner.pointer_absolute = Some(name.to_string());
|
||||||
|
inner.refresh_devices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove absolute pointer device from the EI seat
|
||||||
|
pub fn remove_pointer_absolute(&self) {
|
||||||
|
let mut inner = self.0.lock().unwrap();
|
||||||
|
inner.device_pointer_absolute = None;
|
||||||
|
inner.pointer_absolute = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a touch device to the EI seat
|
||||||
|
///
|
||||||
|
/// Calling on a seat that already has a touch device will remove
|
||||||
|
/// that device and add a new one.
|
||||||
|
pub fn add_touch(&self, name: &str) {
|
||||||
|
let mut inner = self.0.lock().unwrap();
|
||||||
|
inner.device_touch = None;
|
||||||
|
inner.touch = Some(name.to_string());
|
||||||
|
inner.refresh_devices();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove touch device from the EI seat
|
||||||
|
pub fn remove_touch(&self) {
|
||||||
|
let mut inner = self.0.lock().unwrap();
|
||||||
|
inner.device_touch = None;
|
||||||
|
inner.touch = None;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove seat from EI connection
|
||||||
|
pub fn remove(&self) {
|
||||||
|
let inner = self.0.lock().unwrap();
|
||||||
|
inner.seat.remove();
|
||||||
|
if let Some(connection) = inner.connection.upgrade() {
|
||||||
|
let mut seats = connection.seats.lock().unwrap();
|
||||||
|
if let Some(idx) = seats.iter().position(|s| Arc::ptr_eq(&s.0, &self.0)) {
|
||||||
|
seats.remove(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct EiInputSeatInner {
|
||||||
|
connection: Weak<EiInputConnectionInner>,
|
||||||
|
seat: reis::request::Seat,
|
||||||
|
bound_capabilities: BitFlags<DeviceCapability>,
|
||||||
|
event_sender: calloop::channel::Sender<InputEvent<EiInput>>,
|
||||||
|
// Interfaces advertised by the server
|
||||||
|
keyboard: Option<(String, KeymapFile)>,
|
||||||
|
pointer: Option<String>,
|
||||||
|
pointer_absolute: Option<String>,
|
||||||
|
touch: Option<String>,
|
||||||
|
// Devices created in response to client bind
|
||||||
|
device_keyboard: Option<DeviceDropWrapper>,
|
||||||
|
device_pointer: Option<DeviceDropWrapper>,
|
||||||
|
device_pointer_absolute: Option<DeviceDropWrapper>,
|
||||||
|
device_touch: Option<DeviceDropWrapper>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EiInputSeatInner {
|
||||||
|
// Add any devices the server provides, for capabilities the client has bound, if no device yet
|
||||||
|
fn refresh_devices(&mut self) {
|
||||||
|
if self.device_keyboard.is_none() && self.bound_capabilities.contains(DeviceCapability::Keyboard) {
|
||||||
|
if let Some((name, keymap_file)) = self.keyboard.as_ref() {
|
||||||
|
let device = self.seat.add_device(
|
||||||
|
Some(name),
|
||||||
|
DeviceType::Virtual,
|
||||||
|
DeviceCapability::Keyboard.into(),
|
||||||
|
|device| {
|
||||||
|
let keyboard = device.interface::<reis::eis::Keyboard>().unwrap();
|
||||||
|
let _ = keymap_file.with_fd(true, |fd, len| {
|
||||||
|
keyboard.keymap(reis::eis::keyboard::KeymapType::Xkb, len as u32, fd);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
device.resumed();
|
||||||
|
let _ = self.event_sender.send(InputEvent::DeviceAdded {
|
||||||
|
device: device.clone(),
|
||||||
|
});
|
||||||
|
self.device_keyboard = Some(DeviceDropWrapper::new(device, &self.event_sender));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.device_pointer.is_none() && self.bound_capabilities.contains(DeviceCapability::Pointer) {
|
||||||
|
if let Some(name) = self.pointer.as_ref() {
|
||||||
|
let device = self.seat.add_device(
|
||||||
|
Some(name),
|
||||||
|
DeviceType::Virtual,
|
||||||
|
DeviceCapability::Pointer | DeviceCapability::Button | DeviceCapability::Scroll,
|
||||||
|
|_| {},
|
||||||
|
);
|
||||||
|
device.resumed();
|
||||||
|
let _ = self.event_sender.send(InputEvent::DeviceAdded {
|
||||||
|
device: device.clone(),
|
||||||
|
});
|
||||||
|
self.device_pointer = Some(DeviceDropWrapper::new(device, &self.event_sender));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.device_pointer_absolute.is_none()
|
||||||
|
&& self
|
||||||
|
.bound_capabilities
|
||||||
|
.contains(DeviceCapability::PointerAbsolute)
|
||||||
|
{
|
||||||
|
if let Some(name) = self.pointer_absolute.as_ref() {
|
||||||
|
let device = self.seat.add_device(
|
||||||
|
Some(name),
|
||||||
|
DeviceType::Virtual,
|
||||||
|
DeviceCapability::PointerAbsolute | DeviceCapability::Button | DeviceCapability::Scroll,
|
||||||
|
|_| {},
|
||||||
|
);
|
||||||
|
device.resumed();
|
||||||
|
let _ = self.event_sender.send(InputEvent::DeviceAdded {
|
||||||
|
device: device.clone(),
|
||||||
|
});
|
||||||
|
self.device_pointer_absolute = Some(DeviceDropWrapper::new(device, &self.event_sender));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.device_touch.is_none() && self.bound_capabilities.contains(DeviceCapability::Touch) {
|
||||||
|
if let Some(name) = self.touch.as_ref() {
|
||||||
|
let device = self.seat.add_device(
|
||||||
|
Some(name),
|
||||||
|
DeviceType::Virtual,
|
||||||
|
DeviceCapability::Touch.into(),
|
||||||
|
|_| {},
|
||||||
|
);
|
||||||
|
device.resumed();
|
||||||
|
let _ = self.event_sender.send(InputEvent::DeviceAdded {
|
||||||
|
device: device.clone(),
|
||||||
|
});
|
||||||
|
self.device_touch = Some(DeviceDropWrapper::new(device, &self.event_sender));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper that remove the device on drop, and send `DeviceRemoved`
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct DeviceDropWrapper {
|
||||||
|
device: reis::request::Device,
|
||||||
|
event_sender: calloop::channel::Sender<InputEvent<EiInput>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DeviceDropWrapper {
|
||||||
|
fn new(
|
||||||
|
device: reis::request::Device,
|
||||||
|
event_sender: &calloop::channel::Sender<InputEvent<EiInput>>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
device,
|
||||||
|
event_sender: event_sender.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for DeviceDropWrapper {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let _ = self.event_sender.send(InputEvent::DeviceRemoved {
|
||||||
|
device: self.device.clone(),
|
||||||
|
});
|
||||||
|
self.device.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -105,6 +105,9 @@ pub mod winit;
|
||||||
#[cfg(feature = "backend_x11")]
|
#[cfg(feature = "backend_x11")]
|
||||||
pub mod x11;
|
pub mod x11;
|
||||||
|
|
||||||
|
#[cfg(feature = "backend_libei")]
|
||||||
|
pub mod libei;
|
||||||
|
|
||||||
/// Error that can happen when swapping buffers.
|
/// Error that can happen when swapping buffers.
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
pub enum SwapBuffersError {
|
pub enum SwapBuffersError {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ pub use glow;
|
||||||
pub use input;
|
pub use input;
|
||||||
#[cfg(feature = "renderer_pixman")]
|
#[cfg(feature = "renderer_pixman")]
|
||||||
pub use pixman;
|
pub use pixman;
|
||||||
|
pub use reis;
|
||||||
pub use rustix;
|
pub use rustix;
|
||||||
#[cfg(feature = "tracy_gpu_profiling")]
|
#[cfg(feature = "tracy_gpu_profiling")]
|
||||||
pub use tracy_client;
|
pub use tracy_client;
|
||||||
|
|
|
||||||
|
|
@ -2,3 +2,4 @@
|
||||||
|
|
||||||
[default.extend-words]
|
[default.extend-words]
|
||||||
WRONLY = "WRONLY"
|
WRONLY = "WRONLY"
|
||||||
|
eis = "eis"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue