Ei protocol support
- Use the configured keyboard layout, track ei seats to reconfigure their keyboards - DBus interface - Disambiguate input devices per backend instance - Add support for `ei_keysym` and `ei_text` - Act as input method for ei_text UTF-8 injection - Create a region per output for abosolute pointer position - Map ei region for touchscreen emulation - Release pointer buttons and keys on removal - Send modifiers to EI connections
This commit is contained in:
parent
e939a72b30
commit
f4ee52c46a
15 changed files with 1092 additions and 218 deletions
228
src/libei.rs
228
src/libei.rs
|
|
@ -1,46 +1,222 @@
|
|||
use reis::calloop::EisListenerSource;
|
||||
use std::os::unix::net::UnixStream;
|
||||
|
||||
use reis::eis;
|
||||
use smithay::reexports::reis;
|
||||
|
||||
use smithay::backend::libei::{EiInput, EiInputEvent};
|
||||
use smithay::input::keyboard::XkbConfig;
|
||||
use smithay::backend::libei::{EiInput, EiInputEvent, EiRegion};
|
||||
use smithay::input::keyboard::Keysym;
|
||||
use smithay::reexports::calloop;
|
||||
use smithay::wayland::input_method::InputMethodSeat;
|
||||
use smithay::wayland::text_input::TextInputSeat;
|
||||
|
||||
use crate::config::xkb_config_to_wl;
|
||||
use crate::input::InputBackendId;
|
||||
use crate::state::State;
|
||||
|
||||
pub fn listen_eis(handle: &calloop::LoopHandle<'static, State>) {
|
||||
let listener = match eis::Listener::bind_auto() {
|
||||
Ok(listener) => listener,
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to bind EI listener socket: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
// Requested device types for an EI connection, mirroring the XDG RemoteDesktop portal `DeviceType` bitmask
|
||||
const DEVICE_TYPE_KEYBOARD: u32 = 1;
|
||||
const DEVICE_TYPE_POINTER: u32 = 2;
|
||||
const DEVICE_TYPE_TOUCHSCREEN: u32 = 4;
|
||||
|
||||
unsafe { std::env::set_var("LIBEI_SOCKET", listener.path()) };
|
||||
// Name of the EI absolute-pointer device. Shared so the connect path and the
|
||||
// re-advertise-on-output-change path recreate the same device.
|
||||
const ABSOLUTE_POINTER_NAME: &str = "virtual absolute pointer";
|
||||
|
||||
let listener_source = EisListenerSource::new(listener);
|
||||
pub type EiRequest = (UnixStream, u32);
|
||||
|
||||
/// Build the regions advertised on the EI absolute devices (absolute pointer and touch) for the
|
||||
/// output layout: one region per output, each at its **global logical** offset with
|
||||
/// its logical size and scale.
|
||||
pub fn absolute_regions(state: &State) -> Vec<EiRegion> {
|
||||
let shell = state.common.shell.read();
|
||||
shell
|
||||
.outputs()
|
||||
.map(|output| {
|
||||
let scale = output.current_scale().fractional_scale();
|
||||
EiRegion {
|
||||
// Keep the signed logical rect
|
||||
// the u32 clamp happens at the `ei_device.region` in smithay.
|
||||
rect: output.geometry().as_logical(),
|
||||
scale: scale as f32,
|
||||
// Tie the region to its output so a client can correlate it with the
|
||||
// matching screencast stream (the portal advertises the same name).
|
||||
mapping_id: Some(output.name()),
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Re-advertise the coordinate regions on every active EI seat's absolute devices.
|
||||
pub fn refresh_absolute_pointer_regions(state: &State) {
|
||||
if state.common.ei_seats.is_empty() {
|
||||
return;
|
||||
}
|
||||
let regions = absolute_regions(state);
|
||||
for seat in state.common.ei_seats.values() {
|
||||
seat.update_regions(®ions);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_ei(
|
||||
handle: &calloop::LoopHandle<'static, State>,
|
||||
) -> calloop::channel::Sender<EiRequest> {
|
||||
let (sender, channel) = calloop::channel::channel::<EiRequest>();
|
||||
let handle_clone = handle.clone();
|
||||
handle
|
||||
.insert_source(listener_source, move |context, _, _| {
|
||||
.insert_source(channel, move |event, _, _| {
|
||||
let calloop::channel::Event::Msg((stream, device_types)) = event else {
|
||||
return;
|
||||
};
|
||||
let context = match eis::Context::new(stream) {
|
||||
Ok(context) => context,
|
||||
Err(err) => {
|
||||
tracing::error!("Failed to create EI context: {}", err);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let source = EiInput::new(context);
|
||||
handle_clone
|
||||
.insert_source(source, |event, connection, data| match event {
|
||||
if let Err(err) =
|
||||
handle_clone.insert_source(source, move |event, connection, data| match event {
|
||||
EiInputEvent::Connected => {
|
||||
let conn = connection.eis_connection().clone();
|
||||
let seat = connection.add_seat("default");
|
||||
// TODO config
|
||||
let _ = seat.add_keyboard("virtual keyboard", XkbConfig::default());
|
||||
seat.add_pointer("virtual pointer");
|
||||
seat.add_pointer_absolute("virtual absoulte pointer");
|
||||
seat.add_touch("virtual touch");
|
||||
let wants_keyboard = device_types & DEVICE_TYPE_KEYBOARD != 0;
|
||||
if wants_keyboard {
|
||||
let conf = data.common.config.xkb_config();
|
||||
// The ei_keyboard device is given the compositor keymap; its key
|
||||
// events feed the shared seat like any other keyboard.
|
||||
let _ = seat.add_keyboard("virtual keyboard", xkb_config_to_wl(&conf));
|
||||
// The text device lets clients inject keysyms/utf8 directly, delivered
|
||||
seat.add_text("virtual text");
|
||||
}
|
||||
if device_types & DEVICE_TYPE_POINTER != 0 {
|
||||
seat.add_pointer("virtual pointer");
|
||||
let regions = absolute_regions(data);
|
||||
seat.add_pointer_absolute(ABSOLUTE_POINTER_NAME, ®ions);
|
||||
}
|
||||
if device_types & DEVICE_TYPE_TOUCHSCREEN != 0 {
|
||||
let regions = absolute_regions(data);
|
||||
seat.add_touch("virtual touch", ®ions);
|
||||
}
|
||||
// Kb-capable connections get a shared-seat source so their `ei_keyboard`
|
||||
// key events feed the seat keyboard tracking.
|
||||
if wants_keyboard {
|
||||
data.common.ei_keyboard_source.insert(
|
||||
conn.clone(),
|
||||
smithay::input::keyboard::KeyboardSource::new_auxiliary(),
|
||||
);
|
||||
}
|
||||
// Track the seat for every connection
|
||||
data.common.ei_seats.insert(conn, seat);
|
||||
data.update_ei_input_method();
|
||||
}
|
||||
EiInputEvent::Disconnected => {
|
||||
let conn = connection.eis_connection().clone();
|
||||
let backend_id = InputBackendId::Ei(conn.clone());
|
||||
// Release any keys/modifiers and pointer buttons this remote still holds
|
||||
data.release_ei_keyboard(&conn);
|
||||
data.release_ei_pointer(&conn);
|
||||
data.clear_input_source_state(&backend_id);
|
||||
data.common.ei_seats.remove(&conn);
|
||||
data.common.ei_keyboard_source.remove(&conn);
|
||||
data.common.ei_pointer_buttons.remove(&conn);
|
||||
data.update_ei_input_method();
|
||||
// Notify the remaining libei clients of the now-cleared modifier state
|
||||
let seat = data.common.shell.read().seats.last_active().clone();
|
||||
data.broadcast_ei_keyboard_modifiers(&seat);
|
||||
}
|
||||
EiInputEvent::Disconnected => {}
|
||||
EiInputEvent::Event(event) => {
|
||||
data.process_input_event(event);
|
||||
use smithay::backend::input::{InputEvent, KeyboardKeyEvent};
|
||||
match event {
|
||||
InputEvent::Keyboard { event } => {
|
||||
data.inject_ei_key(
|
||||
connection.eis_connection(),
|
||||
event.key_code(),
|
||||
event.state(),
|
||||
);
|
||||
}
|
||||
other => {
|
||||
let backend_id =
|
||||
InputBackendId::Ei(connection.eis_connection().clone());
|
||||
data.process_input_event(other, backend_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
EiInputEvent::TextKeysym { keysym, state } => {
|
||||
data.inject_ei_text_keysym(connection.eis_connection(), keysym, state);
|
||||
}
|
||||
EiInputEvent::TextUtf8 { text } => {
|
||||
data.inject_ei_text(&text);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
Ok(calloop::PostAction::Continue)
|
||||
{
|
||||
tracing::error!("Failed to insert EI input source: {}", err);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
.expect("Failed to insert EI channel source into the event loop");
|
||||
|
||||
sender
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Act as the input method for text injection while any text-capable EI connection is
|
||||
/// active, so `ei_text` UTF-8 can be committed into the focused app even without a real
|
||||
/// IME, but only when none is bound (a real IME always wins)
|
||||
pub(crate) fn update_ei_input_method(&mut self) {
|
||||
let active = !self.common.ei_keyboard_source.is_empty();
|
||||
let seats = self
|
||||
.common
|
||||
.shell
|
||||
.read()
|
||||
.seats
|
||||
.iter()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
for seat in seats {
|
||||
let has_ime = seat.input_method().has_instance();
|
||||
let text_input = seat.text_input();
|
||||
if active {
|
||||
if !has_ime {
|
||||
text_input.set_compositor_input_method(true);
|
||||
}
|
||||
} else {
|
||||
text_input.set_compositor_input_method(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inject UTF-8 text (from an EI `ei_text` device) into the focused client.
|
||||
pub fn inject_ei_text(&mut self, text: &str) {
|
||||
let seat = self.common.shell.read().seats.last_active().clone();
|
||||
// Only commit through text-input when we're the active input method (no real IME)
|
||||
if !seat.input_method().has_instance() {
|
||||
let text_input = seat.text_input();
|
||||
let mut injected = false;
|
||||
text_input.with_active_text_input(|ti, _surface| {
|
||||
ti.commit_string(Some(text.to_owned()));
|
||||
injected = true;
|
||||
});
|
||||
if injected {
|
||||
text_input.done(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Bind the whole chunk to spare keycodes in one temporary keymap per batch (delivered
|
||||
// to just the focused client), so we change the keymap ~once per chunk instead of once
|
||||
// per character. Leaves the seat's own keyboard state untouched.
|
||||
let keysyms: Vec<Keysym> = text
|
||||
.chars()
|
||||
.map(Keysym::from_char)
|
||||
.filter(|keysym| keysym.raw() != 0)
|
||||
.collect();
|
||||
let Some(keyboard) = seat.get_keyboard() else {
|
||||
return;
|
||||
};
|
||||
// At most ~247 keysyms fit one spare keymap (keycodes 9..=255); leave margin.
|
||||
const BATCH: usize = 240;
|
||||
for batch in keysyms.chunks(BATCH) {
|
||||
keyboard.inject_text_keysyms(self, batch);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue