use std::os::unix::net::UnixStream; use reis::eis; use smithay::reexports::reis; 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::{BackendData, State}; use crate::utils::{geometry::RectGlobalExt, prelude::OutputExt}; // 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; // 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"; 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 { 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 { let (sender, channel) = calloop::channel::channel::(); let handle_clone = handle.clone(); handle .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); 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"); 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::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); if matches!(data.backend, BackendData::Kms(_)) { for output in data.common.shell.read().outputs() { data.backend.kms().schedule_render(output); } } } } } EiInputEvent::TextKeysym { keysym, state } => { data.inject_ei_text_keysym(connection.eis_connection(), keysym, state); } EiInputEvent::TextUtf8 { text } => { data.inject_ei_text(&text); } }) { tracing::error!("Failed to insert EI input source: {}", err); } }) .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::>(); 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 = 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); } } }