diff --git a/src/backend/kms/mod.rs b/src/backend/kms/mod.rs index df711387..6da04aac 100644 --- a/src/backend/kms/mod.rs +++ b/src/backend/kms/mod.rs @@ -211,7 +211,7 @@ fn init_libinput( state.backend.kms().input_devices.remove(&*device.name()); } - state.process_input_event(event); + state.process_input_event(event, crate::input::InputBackendId::Normal); for output in state.common.shell.read().outputs() { state.backend.kms().schedule_render(output); diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 24ef64b4..8f3d14c5 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -310,7 +310,9 @@ impl State { WinitEvent::Focus(true) => { for seat in self.common.shell.read().seats.iter() { let devices = seat.user_data().get::().unwrap(); - if devices.has_device(&WinitVirtualDevice) { + if devices + .has_device(&WinitVirtualDevice, &crate::input::InputBackendId::Normal) + { seat.set_active_output(&self.backend.winit().output); break; } @@ -340,7 +342,9 @@ impl State { render_ping.ping(); } WinitEvent::Redraw => render_ping.ping(), - WinitEvent::Input(event) => self.process_input_event(event), + WinitEvent::Input(event) => { + self.process_input_event(event, crate::input::InputBackendId::Normal) + } WinitEvent::CloseRequested => { self.common.should_stop = true; } diff --git a/src/backend/x11.rs b/src/backend/x11.rs index 98b27453..ed92e844 100644 --- a/src/backend/x11.rs +++ b/src/backend/x11.rs @@ -534,14 +534,14 @@ impl State { let device = event.device(); for seat in self.common.shell.read().seats.iter() { let devices = seat.user_data().get::().unwrap(); - if devices.has_device(&device) { + if devices.has_device(&device, &crate::input::InputBackendId::Normal) { seat.set_active_output(&output); break; } } }; - self.process_input_event(event); + self.process_input_event(event, crate::input::InputBackendId::Normal); // TODO actually figure out the output for output in self.common.shell.read().outputs() { self.backend.x11().schedule_render(output); diff --git a/src/config/mod.rs b/src/config/mod.rs index 1d9b9c92..4a2500fd 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -1,6 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only use crate::{ + input::InputBackendId, shell::Shell, state::{BackendData, State}, utils::prelude::OutputExt, @@ -820,6 +821,27 @@ fn config_changed(config: cosmic_config::Config, keys: Vec, state: &mut } } } + let ei_connections = state + .common + .ei_keyboard_source + .keys() + .cloned() + .collect::>(); + for conn in &ei_connections { + state.release_ei_keyboard(conn); + state.clear_input_source_state(&InputBackendId::Ei(conn.clone())); + } + for ei_seat in state.common.ei_seats.values() { + if let Err(err) = + ei_seat.add_keyboard("virtual keyboard", xkb_config_to_wl(&value)) + { + warn!(?err, "Failed to update libei keyboard keymap"); + } + } + if !state.common.ei_seats.is_empty() { + let seat = state.common.shell.read().seats.last_active().clone(); + state.broadcast_ei_keyboard_modifiers(&seat); + } state.common.config.cosmic_conf.xkb_config = value; } "keyboard_config" => { diff --git a/src/dbus/ei.rs b/src/dbus/ei.rs new file mode 100644 index 00000000..6391dc76 --- /dev/null +++ b/src/dbus/ei.rs @@ -0,0 +1,79 @@ +use std::{ + os::unix::net::UnixStream, + sync::{Arc, Mutex}, +}; + +use smithay::reexports::calloop; +use zbus::names::{UniqueName, WellKnownName}; + +use super::name_owners::NameOwners; + +static ALLOWED_NAMES: &[WellKnownName] = &[WellKnownName::from_static_str_unchecked( + "org.freedesktop.impl.portal.desktop.cosmic", +)]; + +/// Channel for handing the EI socketpair (and requested device types) +/// It's `None` until the EI sender side has been set up +type EiSender = Arc>>>; + +struct Ei { + ei_sender: EiSender, + name_owners: NameOwners, +} + +impl Ei { + async fn check_sender_allowed(&self, sender: &UniqueName<'_>) -> zbus::fdo::Result<()> { + if self.name_owners.check_owner(sender, ALLOWED_NAMES).await { + Ok(()) + } else { + Err(zbus::fdo::Error::AccessDenied("Access denied".to_string())) + } + } +} + +#[zbus::interface(name = "com.system76.CosmicComp.Ei")] +impl Ei { + /// Create a new EI sender context + async fn get_sender_socket( + &self, + device_types: u32, + #[zbus(header)] header: zbus::message::Header<'_>, + ) -> zbus::fdo::Result { + if let Some(sender) = header.sender() { + self.check_sender_allowed(sender).await?; + } + + let (comp_stream, client_stream) = UnixStream::pair().map_err(|err| { + zbus::fdo::Error::Failed(format!("Failed to create socket pair: {err}")) + })?; + + { + let guard = self.ei_sender.lock().unwrap(); + let sender = guard + .as_ref() + .ok_or_else(|| zbus::fdo::Error::Failed("EI sender not available".to_string()))?; + sender.send((comp_stream, device_types)).map_err(|err| { + zbus::fdo::Error::Failed(format!("Failed to hand off EI socket: {err}")) + })?; + } + + Ok(std::os::fd::OwnedFd::from(client_stream).into()) + } +} + +/// Register the `com.system76.CosmicComp.Ei` interface on the shared session connection. +pub async fn init( + conn: &zbus::Connection, + name_owners: &NameOwners, + ei_sender: EiSender, +) -> zbus::Result<()> { + let ei = Ei { + ei_sender, + name_owners: name_owners.clone(), + }; + conn.object_server() + .at("/com/system76/CosmicComp/Ei", ei) + .await?; + conn.request_name("com.system76.CosmicComp").await?; + Ok(()) +} diff --git a/src/dbus/mod.rs b/src/dbus/mod.rs index 5b9754c3..9138caee 100644 --- a/src/dbus/mod.rs +++ b/src/dbus/mod.rs @@ -9,11 +9,13 @@ use std::{ cell::{RefCell, RefMut}, collections::HashMap, rc::Rc, + sync::{Arc, Mutex}, }; use tracing::{error, warn}; pub mod a11y_keyboard_monitor; use a11y_keyboard_monitor::A11yKeyboardMonitorState; +pub mod ei; #[cfg(feature = "logind")] pub mod logind; mod name_owners; @@ -29,6 +31,7 @@ struct DBusStateInner { session_conn: zbus::Result, system_conn: zbus::Result, a11y_keyboard_monitor: RefCell>, + ei_sender: Arc>>>, } impl DBusState { @@ -42,6 +45,7 @@ impl DBusState { session_conn, system_conn, a11y_keyboard_monitor: RefCell::new(None), + ei_sender: Arc::new(Mutex::new(None)), })); evlh.insert_source(source, |_, _, _| {}).unwrap(); let state_clone = state.clone(); @@ -65,6 +69,10 @@ impl DBusState { RefMut::filter_map(self.0.a11y_keyboard_monitor.borrow_mut(), |x| x.as_mut()).ok() } + pub fn set_ei_sender(&self, sender: calloop::channel::Sender) { + *self.0.ei_sender.lock().unwrap() = Some(sender); + } + // TODO Lazy async init when we don't have anything blocking main thread async fn session_conn(&self) -> zbus::Result<&zbus::Connection> { self.0.session_conn.as_ref().map_err(|err| err.clone()) @@ -85,6 +93,7 @@ async fn init_session(state: &DBusState) -> zbus::Result<()> { let a11y_keyboard_monitor_state = A11yKeyboardMonitorState::new(conn, &name_owners, &state.0.executor).await?; *state.0.a11y_keyboard_monitor.borrow_mut() = Some(a11y_keyboard_monitor_state); + ei::init(conn, &name_owners, state.0.ei_sender.clone()).await?; Ok(()) } diff --git a/src/input/actions.rs b/src/input/actions.rs index 3d017697..a4932597 100644 --- a/src/input/actions.rs +++ b/src/input/actions.rs @@ -2,6 +2,7 @@ use crate::{ config::{Action, PrivateAction}, + input::InputBackendId, shell::{ FocusResult, InvalidWorkspaceIndex, MoveResult, SeatExt, Trigger, WorkspaceDelta, focus::{FocusTarget, target::KeyboardFocusTarget}, @@ -39,6 +40,7 @@ impl State { pub fn handle_action( &mut self, action: Action, + backend_id: &InputBackendId, seat: &Seat, serial: Serial, time: u32, @@ -69,7 +71,7 @@ impl State { Action::Shortcut(action) => { let propagate = propagate_by_default(&action); self.handle_shortcut_action( - action, seat, serial, time, pattern, direction, propagate, + action, backend_id, seat, serial, time, pattern, direction, propagate, ) } Action::Private(PrivateAction::Escape) => { @@ -143,6 +145,7 @@ impl State { pub fn handle_shortcut_action( &mut self, action: shortcuts::Action, + backend_id: &InputBackendId, seat: &Seat, serial: Serial, time: u32, @@ -232,6 +235,7 @@ impl State { { self.handle_shortcut_action( Action::SwitchOutput(inferred), + backend_id, seat, serial, time, @@ -271,6 +275,7 @@ impl State { { self.handle_shortcut_action( Action::SwitchOutput(inferred), + backend_id, seat, serial, time, @@ -392,6 +397,7 @@ impl State { } else { Action::SendToOutput(inferred) }, + backend_id, seat, serial, time, @@ -417,6 +423,7 @@ impl State { } else { Action::SendToWorkspace(1) }, + backend_id, seat, serial, time, @@ -483,6 +490,7 @@ impl State { } else { Action::SendToOutput(inferred) }, + backend_id, seat, serial, time, @@ -508,6 +516,7 @@ impl State { } else { Action::SendToLastWorkspace }, + backend_id, seat, serial, time, @@ -532,7 +541,9 @@ impl State { if propagate && let Some((serial, prev_output, prev_idx)) = shell.previous_workspace_idx.take() - && seat.last_modifier_change().is_some_and(|s| s == serial) + && seat + .last_modifier_change_for(backend_id) + .is_some_and(|s| s == serial) && prev_output == current_output { let _ = shell.activate( @@ -705,6 +716,7 @@ impl State { if res.is_ok() { self.handle_shortcut_action( Action::SwitchOutput(direction), + backend_id, seat, serial, time, @@ -741,7 +753,8 @@ impl State { }; if let Some(direction) = dir { - if let Some(last_mod_serial) = seat.last_modifier_change() { + if let Some(last_mod_serial) = seat.last_modifier_change_for(backend_id) + { let mut shell = self.common.shell.write(); if !shell .previous_workspace_idx @@ -776,6 +789,7 @@ impl State { self.handle_shortcut_action( action, + backend_id, seat, serial, time, @@ -800,7 +814,7 @@ impl State { .move_current_element(direction, seat); match res { MoveResult::MoveFurther(_move_further) => { - if let Some(last_mod_serial) = seat.last_modifier_change() { + if let Some(last_mod_serial) = seat.last_modifier_change_for(backend_id) { let mut shell = self.common.shell.write(); if !shell .previous_workspace_idx @@ -834,6 +848,7 @@ impl State { self.handle_shortcut_action( action, + backend_id, seat, serial, time, diff --git a/src/input/mod.rs b/src/input/mod.rs index d7dbeb20..c93b3405 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -11,7 +11,7 @@ use crate::{ }, input::gestures::{GestureState, SwipeAction}, shell::{ - LastModifierChange, SeatExt, Trigger, + SeatExt, Trigger, focus::{ Stage, render_input_order, target::{KeyboardFocusTarget, PointerFocusTarget}, @@ -42,14 +42,14 @@ use smithay::{ backend::input::{ AbsolutePositionEvent, Axis, AxisRelativeDirection, AxisSource, Device, DeviceCapability, GestureBeginEvent, GestureEndEvent, GesturePinchUpdateEvent as _, - GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, - PointerAxisEvent, ProximityState, TabletToolButtonEvent, TabletToolEvent, - TabletToolProximityEvent, TabletToolTipEvent, TabletToolTipState, TouchEvent, + GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState, PointerAxisEvent, + ProximityState, TabletToolButtonEvent, TabletToolEvent, TabletToolProximityEvent, + TabletToolTipEvent, TabletToolTipState, TouchEvent, }, desktop::{PopupKeyboardGrab, WindowSurfaceType, utils::under_from_surface_tree}, input::{ Seat, - keyboard::{FilterResult, KeysymHandle, ModifiersState}, + keyboard::{FilterResult, KeyboardSource, KeysymHandle, ModifiersState}, pointer::{ AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, GesturePinchUpdateEvent, @@ -64,7 +64,7 @@ use smithay::{ input::Device as InputDevice, wayland_server::{Resource as _, protocol::wl_surface::WlSurface}, }, - utils::{Clock, Logical, Monotonic, Point, Rectangle, SERIAL_COUNTER, Serial}, + utils::{Clock, Logical, Monotonic, Point, Rectangle, SERIAL_COUNTER, Serial, Size}, wayland::{ compositor::CompositorHandler, image_copy_capture::CursorSessionRef, @@ -80,7 +80,7 @@ use std::{ any::Any, borrow::Cow, cell::RefCell, - collections::HashSet, + collections::{HashMap, HashSet}, ops::ControlFlow, time::{Duration, Instant}, }; @@ -88,6 +88,19 @@ use std::{ pub mod actions; pub mod gestures; +/// Identifies the input backend instance an event came from, used to disambiguate device ids +/// (which are only unique within a single backend instance, see +/// [`smithay::backend::input::Device::id`]). +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum InputBackendId { + /// The session input backend (libinput / winit / x11) these are mutually exclusive + Normal, + /// A specific Ei client connection + Ei(smithay::reexports::reis::eis::Connection), + /// The `zwp_virtual_keyboard_v1` protocol (all virtual keyboards share this source) + VirtualKeyboard, +} + /// Used for debouncing focus updates due to pointer motion, if after the focus change is /// triggered the event will cancel if the pointer moves to the original target #[derive(Debug)] @@ -100,19 +113,35 @@ pub struct PointerFocusState { } #[derive(Default)] -pub struct SupressedKeys(RefCell)>>); +pub struct SupressedKeys( + RefCell)>>>, +); #[derive(Default)] -pub struct SupressedButtons(RefCell>); +pub struct SupressedButtons(RefCell>>); #[derive(Default, Debug)] -pub struct ModifiersShortcutQueue(RefCell>); +pub struct ModifiersShortcutQueue(RefCell>); impl SupressedKeys { - fn add(&self, keysym: &KeysymHandle, token: impl Into>) { - self.0.borrow_mut().push((keysym.raw_code(), token.into())); + fn add( + &self, + backend_id: &InputBackendId, + keysym: &KeysymHandle, + token: impl Into>, + ) { + self.0 + .borrow_mut() + .entry(backend_id.clone()) + .or_default() + .push((keysym.raw_code(), token.into())); } - fn filter(&self, keysym: &KeysymHandle) -> Option> { - let mut keys = self.0.borrow_mut(); + fn filter( + &self, + backend_id: &InputBackendId, + keysym: &KeysymHandle, + ) -> Option> { + let mut by_source = self.0.borrow_mut(); + let keys = by_source.get_mut(backend_id)?; let (removed, remaining) = keys .drain(..) .partition(|(key, _)| *key == keysym.raw_code()); @@ -129,44 +158,60 @@ impl SupressedKeys { .collect::>(), ) } + + fn clear_source(&self, backend_id: &InputBackendId) { + self.0.borrow_mut().remove(backend_id); + } } impl SupressedButtons { - fn add(&self, button: u32) { - self.0.borrow_mut().insert(button); + fn add(&self, backend_id: &InputBackendId, button: u32) { + self.0 + .borrow_mut() + .entry(backend_id.clone()) + .or_default() + .insert(button); } - fn remove(&self, button: u32) -> bool { - self.0.borrow_mut().remove(&button) + fn remove(&self, backend_id: &InputBackendId, button: u32) -> bool { + self.0 + .borrow_mut() + .get_mut(backend_id) + .is_some_and(|buttons| buttons.remove(&button)) + } + + fn clear_source(&self, backend_id: &InputBackendId) { + self.0.borrow_mut().remove(backend_id); } } impl ModifiersShortcutQueue { - pub fn set(&self, binding: shortcuts::Binding) { - let mut set = self.0.borrow_mut(); - *set = Some(binding); + pub fn set(&self, backend_id: &InputBackendId, binding: shortcuts::Binding) { + self.0.borrow_mut().insert(backend_id.clone(), binding); } - pub fn take(&self, binding: &shortcuts::Binding) -> bool { + pub fn take(&self, backend_id: &InputBackendId, binding: &shortcuts::Binding) -> bool { let mut set = self.0.borrow_mut(); - if set.is_some() && set.as_ref().unwrap() == binding { - *set = None; + if set.get(backend_id).is_some_and(|queued| queued == binding) { + set.remove(backend_id); true } else { false } } - pub fn clear(&self) { - let mut set = self.0.borrow_mut(); - *set = None; + pub fn clear(&self, backend_id: &InputBackendId) { + self.0.borrow_mut().remove(backend_id); } } impl State { #[profiling::function] - pub fn process_input_event(&mut self, event: InputEvent) - where + pub fn process_input_event( + &mut self, + event: InputEvent, + backend_id: InputBackendId, + ) where ::Device: 'static, { crate::wayland::handlers::output_power::set_all_surfaces_dpms_on(self); @@ -177,19 +222,25 @@ impl State { let shell = self.common.shell.read(); let seat = shell.seats.last_active(); let led_state = seat.get_keyboard().unwrap().led_state(); - seat.devices().add_device(&device, led_state); + seat.devices().add_device(&device, led_state, &backend_id); if device.has_capability(DeviceCapability::TabletTool) { seat.tablet_seat().add_wp_tablet( &self.common.display_handle, &TabletDescriptor::from(&device), ); } + let has_keyboard = device.has_capability(DeviceCapability::Keyboard); + std::mem::drop(shell); + // send the seat's current modifier state to the new ei keyboard + if has_keyboard && let InputBackendId::Ei(conn) = &backend_id { + self.send_ei_keyboard_modifiers(conn); + } } InputEvent::DeviceRemoved { device } => { for seat in &mut self.common.shell.read().seats.iter() { let devices = seat.devices(); - if devices.has_device(&device) { - devices.remove_device(&device); + if devices.has_device(&device, &backend_id) { + devices.remove_device(&device, &backend_id); if device.has_capability(DeviceCapability::TabletTool) { seat.tablet_seat() .remove_tablet(&TabletDescriptor::from(&device)); @@ -210,7 +261,7 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); @@ -231,51 +282,17 @@ impl State { serial, time, |data, modifiers, handle| { - if previous_modifiers != *modifiers { - *seat - .user_data() - .get::() - .unwrap() - .0 - .lock() - .unwrap() = Some(serial); - } - - let current_focus = seat.get_keyboard().unwrap().current_focus(); - let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| { - f.wl_surface() - .map(|surface| { - seat.keyboard_shortcuts_inhibitor_for_surface(&surface) - .map(|inhibitor| inhibitor.is_active()) - .unwrap_or(false) - || seat.has_active_xwayland_grab(&surface) - }) - .unwrap_or(false) - }); - let sym = handle.modified_sym(); - - let result = Self::filter_keyboard_input( - data, &event, &seat, modifiers, handle, serial, - ); - - if (matches!(result, FilterResult::Forward) - && !seat.get_keyboard().unwrap().is_grabbed() - && !shortcuts_inhibited - && !matches!( - current_focus, - Some(KeyboardFocusTarget::LockSurface(_)) - )) - // we don't want to accidentally leave any keys pressed - // and do more filtering in `xwayland_notify_key_event` - // for released keys - || state == KeyState::Released - { - data.common.xwayland_notify_key_event( - sym, keycode, state, serial, time, - ); - } - - result + data.process_keyboard_filter( + &backend_id, + &seat, + modifiers, + handle, + serial, + time, + keycode, + state, + previous_modifiers, + ) }, ) .flatten() @@ -286,7 +303,7 @@ impl State { FilterResult::<()>::Forward }); } - self.handle_action(action, &seat, serial, time, pattern, None) + self.handle_action(action, &backend_id, &seat, serial, time, pattern, None) } // If we want to track numlock state so it can be reused on the next boot... @@ -311,7 +328,11 @@ impl State { use smithay::backend::input::PointerMotionEvent; let shell = self.common.shell.write(); - if let Some(seat) = shell.seats.for_device(&event.device()).cloned() { + if let Some(seat) = shell + .seats + .for_device(&event.device(), &backend_id) + .cloned() + { self.common.idle_notifier_state.notify_activity(&seat); notify_cursor_activity(self, &seat); let current_output = seat.active_output(); @@ -665,19 +686,47 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); notify_cursor_activity(self, &seat); - let output = seat.active_output(); - let output_geometry = output.geometry(); - let position = output_geometry.loc.to_f64() - + smithay::backend::input::AbsolutePositionEvent::position_transformed( - &event, - output_geometry.size.as_logical(), - ) - .as_global(); + let (output, position) = if matches!(&backend_id, InputBackendId::Ei(_)) { + // EI absolute coordinates are in the compositor's *global* + // logical space: each advertised region carries its output's + // global offset, so the client sends a global position. Use + // the coordinate directly and find the output it lands in, + // rather than mapping relative to the focused output (which + // cannot address other monitors). This is the KWin/mutter + // model. + let position = + smithay::backend::input::AbsolutePositionEvent::position_transformed( + &event, + // smithay's EI impl ignores the size and returns the + // raw coordinate. + Size::from((0, 0)), + ) + .as_global(); + let output = self + .common + .shell + .read() + .outputs() + .find(|o| o.geometry().to_f64().contains(position)) + .cloned() + .unwrap_or_else(|| seat.active_output()); + (output, position) + } else { + let output = seat.active_output(); + let output_geometry = output.geometry(); + let position = output_geometry.loc.to_f64() + + smithay::backend::input::AbsolutePositionEvent::position_transformed( + &event, + output_geometry.size.as_logical(), + ) + .as_global(); + (output, position) + }; let serial = SERIAL_COUNTER.next_serial(); let under = State::surface_under(position, &output, &self.common.shell.write()) .map(|(target, pos)| (target, pos.as_logical())); @@ -694,6 +743,14 @@ impl State { ); ptr.frame(self); + // Keep the seat's active output following the pointer. Click-to- + // focus (PointerButton) resolves its target via + // `seat.active_output()` + let previous_output = seat.active_output(); + if previous_output != output { + seat.set_active_output(&output); + } + let shell = self.common.shell.read(); update_output_image_copy_cursor_position( &shell, @@ -713,7 +770,7 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned() else { return; @@ -736,7 +793,26 @@ impl State { let serial = SERIAL_COUNTER.next_serial(); let button = event.button_code(); - let mut pass_event = !seat.supressed_buttons().remove(button); + // Track buttons held by a libei source so they can be released if the connection + // drops mid-press + if let InputBackendId::Ei(conn) = &backend_id { + match event.state() { + ButtonState::Pressed => { + self.common + .ei_pointer_buttons + .entry(conn.clone()) + .or_default() + .insert(button); + } + ButtonState::Released => { + if let Some(held) = self.common.ei_pointer_buttons.get_mut(conn) { + held.remove(&button); + } + } + } + } + + let mut pass_event = !seat.supressed_buttons().remove(&backend_id, button); if event.state() == ButtonState::Pressed { // change the keyboard focus unless the pointer is grabbed // We test for any matching surface type here but always use the root @@ -758,7 +834,7 @@ impl State { ); if let Some(target) = under.filter(|_| !on_resize_fork) { if let Some(surface) = target.toplevel().map(Cow::into_owned) - && seat.get_keyboard().unwrap().modifier_state().logo + && self.source_modifiers(&backend_id, &seat).logo && !shortcuts_inhibited { let seat_clone = seat.clone(); @@ -769,17 +845,18 @@ impl State { // aimed at the compositor and shouldn't be passed // to the application. pass_event = false; - seat.supressed_buttons().add(button); + seat.supressed_buttons().add(&backend_id, button); }; fn dispatch_grab + 'static>( grab: Option<(G, smithay::input::pointer::Focus)>, seat: Seat, + backend_id: &InputBackendId, serial: Serial, state: &mut State, ) { if let Some((target, focus)) = grab { - seat.modifiers_shortcut_queue().clear(); + seat.modifiers_shortcut_queue().clear(backend_id); seat.get_pointer() .unwrap() @@ -791,6 +868,7 @@ impl State { match mouse_button { smithay::backend::input::MouseButton::Left => { supress_button(); + let backend_id = backend_id.clone(); self.common.event_loop_handle.insert_idle( move |state| { let mut shell = state.common.shell.write(); @@ -805,12 +883,19 @@ impl State { false, ); drop(shell); - dispatch_grab(res, seat_clone, serial, state); + dispatch_grab( + res, + seat_clone, + &backend_id, + serial, + state, + ); }, ); } smithay::backend::input::MouseButton::Right => { supress_button(); + let backend_id = backend_id.clone(); self.common.event_loop_handle.insert_idle( move |state| { let mut shell = state.common.shell.write(); @@ -868,7 +953,13 @@ impl State { false, ); drop(shell); - dispatch_grab(res, seat_clone, serial, state); + dispatch_grab( + res, + seat_clone, + &backend_id, + serial, + state, + ); }, ); } @@ -932,13 +1023,13 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); notify_cursor_activity(self, &seat); - if seat.get_keyboard().unwrap().modifier_state().logo + if self.source_modifiers(&backend_id, &seat).logo && self .common .config @@ -946,7 +1037,7 @@ impl State { .accessibility_zoom .enable_mouse_zoom_shortcuts { - seat.modifiers_shortcut_queue().clear(); + seat.modifiers_shortcut_queue().clear(&backend_id); if let Some(mut percentage) = event .amount_v120(Axis::Vertical) .map(|val| val / 120.) @@ -1023,7 +1114,7 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); @@ -1049,7 +1140,7 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); @@ -1150,7 +1241,7 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); @@ -1195,7 +1286,7 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); @@ -1217,7 +1308,7 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); @@ -1239,7 +1330,7 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); @@ -1261,7 +1352,7 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); @@ -1283,7 +1374,7 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); @@ -1302,17 +1393,40 @@ impl State { InputEvent::TouchDown { event, .. } => { let shell = self.common.shell.write(); - if let Some(seat) = shell.seats.for_device(&event.device()).cloned() { + if let Some(seat) = shell + .seats + .for_device(&event.device(), &backend_id) + .cloned() + { self.common.idle_notifier_state.notify_activity(&seat); - let Some(output) = - mapped_output_for_device(&self.common.config, &shell, &event.device()) + // Check if the touch is from an ei device or a mapped device + // EI absolute coordinates are already in the compositor's global logical + // space (each advertised region carries its output's global offset), so use + // them directly and find the output they land in. + let (output, position) = if matches!(&backend_id, InputBackendId::Ei(_)) { + let position = + smithay::backend::input::AbsolutePositionEvent::position_transformed( + &event, + Size::from((0, 0)), + ) + .as_global(); + let output = shell + .outputs() + .find(|o| o.geometry().to_f64().contains(position)) .cloned() - else { - return; + .unwrap_or_else(|| seat.active_output()); + (output, position) + } else { + let Some(output) = + mapped_output_for_device(&self.common.config, &shell, &event.device()) + .cloned() + else { + return; + }; + let position = + transform_output_mapped_position(&output, &event, shell.zoom_state()); + (output, position) }; - - let position = - transform_output_mapped_position(&output, &event, shell.zoom_state()); let under = State::surface_under(position, &output, &shell) .map(|(target, pos)| (target, pos.as_logical())); @@ -1334,17 +1448,36 @@ impl State { } InputEvent::TouchMotion { event, .. } => { let shell = self.common.shell.write(); - if let Some(seat) = shell.seats.for_device(&event.device()).cloned() { + if let Some(seat) = shell + .seats + .for_device(&event.device(), &backend_id) + .cloned() + { self.common.idle_notifier_state.notify_activity(&seat); - let Some(output) = - mapped_output_for_device(&self.common.config, &shell, &event.device()) + let (output, position) = if matches!(&backend_id, InputBackendId::Ei(_)) { + let position = + smithay::backend::input::AbsolutePositionEvent::position_transformed( + &event, + Size::from((0, 0)), + ) + .as_global(); + let output = shell + .outputs() + .find(|o| o.geometry().to_f64().contains(position)) .cloned() - else { - return; + .unwrap_or_else(|| seat.active_output()); + (output, position) + } else { + let Some(output) = + mapped_output_for_device(&self.common.config, &shell, &event.device()) + .cloned() + else { + return; + }; + let position = + transform_output_mapped_position(&output, &event, shell.zoom_state()); + (output, position) }; - - let position = - transform_output_mapped_position(&output, &event, shell.zoom_state()); let under = State::surface_under(position, &output, &shell) .map(|(target, pos)| (target, pos.as_logical())); @@ -1370,7 +1503,10 @@ impl State { shell.set_overview_mode(None, self.common.event_loop_handle.clone()); } - let maybe_seat = shell.seats.for_device(&event.device()).cloned(); + let maybe_seat = shell + .seats + .for_device(&event.device(), &backend_id) + .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); std::mem::drop(shell); @@ -1392,7 +1528,7 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); @@ -1406,7 +1542,7 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); @@ -1417,7 +1553,11 @@ impl State { InputEvent::TabletToolAxis { event, .. } => { let shell = self.common.shell.write(); - if let Some(seat) = shell.seats.for_device(&event.device()).cloned() { + if let Some(seat) = shell + .seats + .for_device(&event.device(), &backend_id) + .cloned() + { self.common.idle_notifier_state.notify_activity(&seat); notify_cursor_activity(self, &seat); let Some(output) = @@ -1482,7 +1622,11 @@ impl State { } InputEvent::TabletToolProximity { event, .. } => { let shell = self.common.shell.write(); - if let Some(seat) = shell.seats.for_device(&event.device()).cloned() { + if let Some(seat) = shell + .seats + .for_device(&event.device(), &backend_id) + .cloned() + { self.common.idle_notifier_state.notify_activity(&seat); notify_cursor_activity(self, &seat); let Some(output) = @@ -1570,7 +1714,7 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); @@ -1608,7 +1752,7 @@ impl State { .shell .read() .seats - .for_device(&event.device()) + .for_device(&event.device(), &backend_id) .cloned(); if let Some(seat) = maybe_seat { self.common.idle_notifier_state.notify_activity(&seat); @@ -1670,15 +1814,335 @@ impl State { } } + /// The modifier state held by the source that produced an event. + pub(crate) fn source_modifiers( + &self, + _backend_id: &InputBackendId, + seat: &Seat, + ) -> ModifiersState { + seat.get_keyboard() + .map(|k| k.modifier_state()) + .unwrap_or_default() + } + + pub(crate) fn clear_input_source_state(&mut self, backend_id: &InputBackendId) { + let seats = self + .common + .shell + .read() + .seats + .iter() + .cloned() + .collect::>(); + for seat in seats { + seat.supressed_keys().clear_source(backend_id); + seat.supressed_buttons().clear_source(backend_id); + seat.modifiers_shortcut_queue().clear(backend_id); + seat.clear_last_modifier_change(backend_id); + } + } + + /// Release the keys this libei connection still holds on the shared seat, so they don't stay + /// stuck in the focused client. Keeps the connection's source valid (only clears held keys), + /// so it's safe to call both on disconnect and on a keymap change. Does not remove the source + /// from [`Common::ei_keyboard_source`], the disconnect path does that. + pub(crate) fn release_ei_keyboard(&mut self, conn: &smithay::reexports::reis::eis::Connection) { + let seat = self.common.shell.read().seats.last_active().clone(); + let Some(keyboard) = seat.get_keyboard() else { + return; + }; + if let Some(source) = self.common.ei_keyboard_source.get(conn).copied() { + keyboard.release_source(self, source); + } + } + + /// Release any pointer buttons this libei connection still holds + pub(crate) fn release_ei_pointer(&mut self, conn: &smithay::reexports::reis::eis::Connection) { + let buttons: Vec = self + .common + .ei_pointer_buttons + .get(conn) + .map(|held| held.iter().copied().collect()) + .unwrap_or_default(); + if buttons.is_empty() { + return; + } + let seat = self.common.shell.read().seats.last_active().clone(); + let Some(pointer) = seat.get_pointer() else { + return; + }; + let time = self.common.clock.now().as_millis(); + for button in buttons { + let serial = SERIAL_COUNTER.next_serial(); + pointer.button( + self, + &smithay::input::pointer::ButtonEvent { + button, + state: smithay::backend::input::ButtonState::Released, + serial, + time, + }, + ); + } + pointer.frame(self); + } + + /// Mirror the seat's current modifier state to every libei sender with a keyboard via + /// `ei_keyboard.modifiers` + pub(crate) fn broadcast_ei_keyboard_modifiers(&self, seat: &Seat) { + if self.common.ei_seats.is_empty() { + return; + } + let Some(keyboard) = seat.get_keyboard() else { + return; + }; + let s = keyboard.modifier_state().serialized; + for ei_seat in self.common.ei_seats.values() { + ei_seat.keyboard_modifiers(s.depressed, s.locked, s.latched, s.layout_effective); + } + } + + /// Send the seat's current modifier state to a single libei connection, used when that + /// connection's `ei_keyboard` device is created. The EI spec expects the current (nonzero) + /// modifier state to be announced once the device is live, so the client doesn't have to + /// wait for the next change to learn e.g. that Caps Lock is on. + pub(crate) fn send_ei_keyboard_modifiers( + &self, + conn: &smithay::reexports::reis::eis::Connection, + ) { + let Some(ei_seat) = self.common.ei_seats.get(conn) else { + return; + }; + let mods = { + let shell = self.common.shell.read(); + shell + .seats + .last_active() + .get_keyboard() + .map(|keyboard| keyboard.modifier_state().serialized) + }; + if let Some(s) = mods { + ei_seat.keyboard_modifiers(s.depressed, s.locked, s.latched, s.layout_effective); + } + } + + pub(crate) fn process_keyboard_filter( + &mut self, + backend_id: &InputBackendId, + seat: &Seat, + modifiers: &ModifiersState, + handle: KeysymHandle<'_>, + serial: Serial, + time: u32, + keycode: Keycode, + key_state: KeyState, + previous_modifiers: ModifiersState, + ) -> FilterResult> { + if previous_modifiers != *modifiers { + seat.set_last_modifier_change(backend_id, serial); + self.broadcast_ei_keyboard_modifiers(seat); + } + + let current_focus = seat.get_keyboard().unwrap().current_focus(); + let shortcuts_inhibited = current_focus.as_ref().is_some_and(|f| { + f.wl_surface() + .map(|surface| { + seat.keyboard_shortcuts_inhibitor_for_surface(&surface) + .map(|inhibitor| inhibitor.is_active()) + .unwrap_or(false) + || seat.has_active_xwayland_grab(&surface) + }) + .unwrap_or(false) + }); + let sym = handle.modified_sym(); + + let result = self.filter_keyboard_input( + backend_id, seat, modifiers, handle, serial, keycode, key_state, time, + ); + + if (matches!(result, FilterResult::Forward) + && !seat.get_keyboard().unwrap().is_grabbed() + && !shortcuts_inhibited + && !matches!(current_focus, Some(KeyboardFocusTarget::LockSurface(_)))) + // we don't want to accidentally leave any keys pressed + || key_state == KeyState::Released + { + self.common + .xwayland_notify_key_event(sym, keycode, key_state, *modifiers, serial, time); + } + + result + } + + /// Inject a key from an auxiliary source (a virtual keyboard, or libei) into the shared + /// seat keyboard, tagged with `source` so its held keys are tracked independently of the + /// physical keyboard + /// + /// `handle_shortcuts` is `false` when synthesizing releases so still-held keys are just + /// forwarded without re-triggering bindings. + pub(crate) fn inject_source_key( + &mut self, + source: KeyboardSource, + backend_id: &InputBackendId, + seat: &Seat, + keycode: Keycode, + key_state: KeyState, + handle_shortcuts: bool, + ) { + let Some(keyboard) = seat.get_keyboard() else { + return; + }; + let serial = SERIAL_COUNTER.next_serial(); + let time = self.common.clock.now().as_millis(); + let previous_modifiers = keyboard.modifier_state(); + let result = keyboard + .input_from_source( + source, + self, + keycode, + key_state, + serial, + time, + |data, modifiers, handle| { + if handle_shortcuts { + data.process_keyboard_filter( + backend_id, + seat, + modifiers, + handle, + serial, + time, + keycode, + key_state, + previous_modifiers, + ) + } else { + FilterResult::Forward + } + }, + ) + .flatten(); + + if let Some((action, pattern)) = result { + self.handle_action(action, backend_id, seat, serial, time, pattern, None); + } + } + + /// Inject a real key from a libei connection's `ei_keyboard` into the shared seat keyboard, + /// tagged with the connection's source so it behaves like any other keyboard (its keycodes + /// are interpreted with the seat keymap, which the compositor already forced onto the + /// `ei_keyboard` device). + pub(crate) fn inject_ei_key( + &mut self, + conn: &smithay::reexports::reis::eis::Connection, + keycode: Keycode, + key_state: KeyState, + ) { + let seat = self.common.shell.read().seats.last_active().clone(); + let backend_id = InputBackendId::Ei(conn.clone()); + let Some(source) = self.common.ei_keyboard_source.get(conn).copied() else { + return; + }; + self.inject_source_key(source, &backend_id, &seat, keycode, key_state, true); + } + + /// Inject a keysym from a libei `ei_text` device. + pub(crate) fn inject_ei_text_keysym( + &mut self, + conn: &smithay::reexports::reis::eis::Connection, + keysym: u32, + key_state: KeyState, + ) { + use smithay::wayland::input_method::InputMethodSeat; + use smithay::wayland::text_input::TextInputSeat; + + let keysym = Keysym::new(keysym); + + // `ei_text` is a tap: act on the press, ignore the release. + if key_state != KeyState::Pressed { + return; + } + let seat = self.common.shell.read().seats.last_active().clone(); + let Some(keyboard) = seat.get_keyboard() else { + return; + }; + + let mods = keyboard.modifier_state(); + let no_mods = !(mods.ctrl || mods.alt || mods.shift || mods.logo); + + // Is this keysym plain text, or not (Escape, F-keys, arrows, ...)? + let is_text = keysym.key_char().is_some_and(|c| !c.is_control()); + + // Resolve to a real keycode and feed it through the shared seat when this is *not* plain + // text, or when a modifier is held. + if !is_text || !no_mods { + if let Some(keycode) = keyboard.keycode_for_keysym(keysym) + && let Some(source) = self.common.ei_keyboard_source.get(conn).copied() + { + let backend_id = InputBackendId::Ei(conn.clone()); + self.inject_source_key( + source, + &backend_id, + &seat, + keycode, + KeyState::Pressed, + true, + ); + self.inject_source_key( + source, + &backend_id, + &seat, + keycode, + KeyState::Released, + true, + ); + return; + } + // No keycode for this keysym in the seat keymap (out-of-layout / Unicode), so fall + // through to the text paths below. Any held modifier is lost — best effort. + tracing::warn!( + "[ei-text] -> keysym not in seat keymap; falling back (modifier not applied)" + ); + } + + // No modifier held: a printable, non-control character committed directly through the + // text-input protocol when a text-input client is focused (and no real IME). This also + // covers out-of-layout / Unicode that has no keycode. + if no_mods + && is_text + && let Some(c) = keysym.key_char() + && !seat.input_method().has_instance() + { + let text_input = seat.text_input(); + let mut handled = false; + text_input.with_active_text_input(|ti, _surface| { + ti.commit_string(Some(c.to_string())); + handled = true; + }); + if handled { + text_input.done(false); + return; + } + } + + // Otherwise inject the keysym as a keycode tap via a temporary keymap delivered to just + // the focused client (reaches non-text-input apps: terminals, games, ...). This never + // touches the seat's own keyboard state. + keyboard.inject_text_keysyms(self, &[keysym]); + } + /// Determine is key event should be intercepted as a key binding, or forwarded to surface #[profiling::function] - pub fn filter_keyboard_input>( + pub fn filter_keyboard_input( &mut self, - event: &E, + backend_id: &InputBackendId, seat: &Seat, modifiers: &ModifiersState, handle: KeysymHandle<'_>, serial: Serial, + keycode: Keycode, + key_state: KeyState, + time: u32, ) -> FilterResult> { // Pre-compute for layout-agnostic shortcut matching let raw_syms = handle.raw_syms(); @@ -1696,7 +2160,12 @@ impl State { let keyboard_grabbed = keyboard.with_grab(|_serial, grab| { grab.is::() || grab.is::>() }) == Some(true); - let is_grabbed = keyboard_grabbed || pointer.is_grabbed(); + // A virtual-keyboard key can arrive while the seat's pointer is grabbed by that + // same on-screen keyboard's own button press (the implicit grab from clicking an OSK + // key). That pointer grab must not capture the injected key, otherwise e.g. + // pressing esc on a virtual keyboard gets swallowed here + let from_vk = matches!(backend_id, InputBackendId::VirtualKeyboard); + let is_grabbed = keyboard_grabbed || (pointer.is_grabbed() && !from_vk); let current_focus = keyboard.current_focus(); //this should fall back to active output since there may not be a focused output @@ -1714,7 +2183,7 @@ impl State { }); if let Some(a11y_keyboard_monitor) = self.common.dbus_state.a11y_keyboard_monitor() { - a11y_keyboard_monitor.key_event(modifiers, &handle, event.state()); + a11y_keyboard_monitor.key_event(modifiers, &handle, key_state); } // Leave move overview mode, if any modifier was released @@ -1736,7 +2205,7 @@ impl State { || (action_pattern.modifiers.shift && !modifiers.shift) || (action_pattern.key.is_some() && key_matches(action_pattern.key.unwrap()) - && event.state() == KeyState::Released)) + && key_state == KeyState::Released)) { shell.set_overview_mode(None, self.common.event_loop_handle.clone()); @@ -1752,7 +2221,7 @@ impl State { // Leave or update resize mode, if modifiers changed or initial key was released if let Some(action_pattern) = shell.resize_mode().0.active_binding() { if action_pattern.key.is_some() - && event.state() == KeyState::Released + && key_state == KeyState::Released && key_matches(action_pattern.key.unwrap()) { shell.set_resize_mode( @@ -1805,7 +2274,7 @@ impl State { let action = Action::Private(PrivateAction::Resizing( direction, edge.into(), - cosmic_keystate_from_smithay(event.state()), + cosmic_keystate_from_smithay(key_state), )); let key_pattern = shortcuts::Binding { modifiers: cosmic_modifiers_from_smithay(*modifiers), @@ -1814,8 +2283,8 @@ impl State { description: None, }; - if event.state() == KeyState::Released { - if let Some(tokens) = seat.supressed_keys().filter(&handle) { + if key_state == KeyState::Released { + if let Some(tokens) = seat.supressed_keys().filter(backend_id, &handle) { for token in tokens { self.common.event_loop_handle.remove(token); } @@ -1824,8 +2293,8 @@ impl State { let seat_clone = seat.clone(); let action_clone = action.clone(); let key_pattern_clone = key_pattern.clone(); + let backend_id_clone = backend_id.clone(); let start = Instant::now(); - let time = event.time_msec(); let token = self .common .event_loop_handle @@ -1835,6 +2304,7 @@ impl State { let duration = current.duration_since(start).as_millis(); state.handle_action( action_clone.clone(), + &backend_id_clone, &seat_clone, serial, time.overflowing_add(duration as u32).0, @@ -1846,7 +2316,7 @@ impl State { ) .ok(); - seat.supressed_keys().add(&handle, token); + seat.supressed_keys().add(backend_id, &handle, token); } return FilterResult::Intercept(Some((action, key_pattern))); } @@ -1857,13 +2327,13 @@ impl State { // cancel grabs if is_grabbed && handle.modified_sym() == Keysym::Escape - && event.state() == KeyState::Pressed + && key_state == KeyState::Pressed && !modifiers.alt && !modifiers.ctrl && !modifiers.logo && !modifiers.shift { - seat.supressed_keys().add(&handle, None); + seat.supressed_keys().add(backend_id, &handle, None); return FilterResult::Intercept(Some(( Action::Private(PrivateAction::Escape), shortcuts::Binding { @@ -1876,7 +2346,7 @@ impl State { } if let Some(mut a11y_keyboard_monitor) = self.common.dbus_state.a11y_keyboard_monitor() { - if event.state() == KeyState::Released { + if key_state == KeyState::Released { let removed = a11y_keyboard_monitor.remove_active_virtual_mod(handle.modified_sym()); // If `Caps_Lock` is a virtual modifier, and is in locked state, clear it @@ -1885,7 +2355,7 @@ impl State { && (modifiers.serialized.locked & 2) != 0 { let seat = seat.clone(); - let key_code = event.key_code(); + let key_code = keycode; self.common.event_loop_handle.insert_idle(move |state| { if let Some(keyboard) = seat.get_keyboard() { let serial = SERIAL_COUNTER.next_serial(); @@ -1910,7 +2380,7 @@ impl State { } }); } - } else if event.state() == KeyState::Pressed + } else if key_state == KeyState::Pressed && a11y_keyboard_monitor.has_virtual_mod(handle.modified_sym()) { a11y_keyboard_monitor.add_active_virtual_mod(handle.modified_sym()); @@ -1919,15 +2389,15 @@ impl State { "active virtual mods: {:?}", a11y_keyboard_monitor.active_virtual_mods() ); - seat.supressed_keys().add(&handle, None); + seat.supressed_keys().add(backend_id, &handle, None); return FilterResult::Intercept(None); } } // Skip released events for initially surpressed keys - if event.state() == KeyState::Released - && let Some(tokens) = seat.supressed_keys().filter(&handle) + if key_state == KeyState::Released + && let Some(tokens) = seat.supressed_keys().filter(backend_id, &handle) { for token in tokens { self.common.event_loop_handle.remove(token); @@ -1936,7 +2406,7 @@ impl State { } // Handle VT switches - if event.state() == KeyState::Pressed + if key_state == KeyState::Pressed && (Keysym::XF86_Switch_VT_1.raw()..=Keysym::XF86_Switch_VT_12.raw()) .contains(&handle.modified_sym().raw()) { @@ -1945,18 +2415,18 @@ impl State { ) { error!(?err, "Failed switching virtual terminal."); } - seat.supressed_keys().add(&handle, None); + seat.supressed_keys().add(backend_id, &handle, None); return FilterResult::Intercept(None); } if let Some(a11y_keyboard_monitor) = self.common.dbus_state.a11y_keyboard_monitor() - && event.state() == KeyState::Pressed + && key_state == KeyState::Pressed && (a11y_keyboard_monitor.has_keyboard_grab() || a11y_keyboard_monitor.has_key_grab(modifiers, handle.modified_sym())) { let modifiers_queue = seat.modifiers_shortcut_queue(); - modifiers_queue.clear(); - seat.supressed_keys().add(&handle, None); + modifiers_queue.clear(backend_id); + seat.supressed_keys().add(backend_id, &handle, None); return FilterResult::Intercept(None); } @@ -1972,11 +2442,11 @@ impl State { // is this a released (triggered) modifier-only binding? if binding.key.is_none() - && event.state() == KeyState::Released + && key_state == KeyState::Released && !cosmic_modifiers_eq_smithay(&binding.modifiers, modifiers) - && modifiers_queue.take(binding) + && modifiers_queue.take(backend_id, binding) { - modifiers_queue.clear(); + modifiers_queue.clear(backend_id); return FilterResult::Intercept(Some(( Action::Shortcut(action.clone()), binding.clone(), @@ -1985,21 +2455,21 @@ impl State { // could this potentially become a modifier-only binding? if binding.key.is_none() - && event.state() == KeyState::Pressed + && key_state == KeyState::Pressed && cosmic_modifiers_eq_smithay(&binding.modifiers, modifiers) { - modifiers_queue.set(binding.clone()); + modifiers_queue.set(backend_id, binding.clone()); clear_queue = false; } // is this a normal binding? if binding.key.is_some() - && event.state() == KeyState::Pressed + && key_state == KeyState::Pressed && key_matches(binding.key.unwrap()) && cosmic_modifiers_eq_smithay(&binding.modifiers, modifiers) { - modifiers_queue.clear(); - seat.supressed_keys().add(&handle, None); + modifiers_queue.clear(backend_id); + seat.supressed_keys().add(backend_id, &handle, None); return FilterResult::Intercept(Some(( Action::Shortcut(action.clone()), binding.clone(), @@ -2010,7 +2480,7 @@ impl State { // no binding if clear_queue { - seat.modifiers_shortcut_queue().clear(); + seat.modifiers_shortcut_queue().clear(backend_id); } // keys are passed through to apps FilterResult::Forward diff --git a/src/lib.rs b/src/lib.rs index d0bf28b7..aa31a464 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -177,11 +177,13 @@ pub fn run(hooks: crate::hooks::Hooks) -> Result<(), Box> { with_xwayland, kiosk_command, ); + // Set up the libei sender side before the backend spawns Xwayland. + let ei_sender = libei::setup_ei(&event_loop.handle()); + state.common.dbus_state.set_ei_sender(ei_sender); + // init backend backend::init_backend_auto(&display, &mut event_loop, &mut state)?; - libei::listen_eis(&event_loop.handle()); - if let Err(err) = theme::watch_theme(event_loop.handle()) { warn!(?err, "Failed to watch theme"); } diff --git a/src/libei.rs b/src/libei.rs index c0c1ed3e..1d748741 100644 --- a/src/libei.rs +++ b/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 { + 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(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::>(); + 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); + } + } } diff --git a/src/shell/layout/tiling/grabs/swap.rs b/src/shell/layout/tiling/grabs/swap.rs index 4c22209f..afe93cf0 100644 --- a/src/shell/layout/tiling/grabs/swap.rs +++ b/src/shell/layout/tiling/grabs/swap.rs @@ -13,6 +13,7 @@ use smithay::{ use crate::{ config::key_bindings::cosmic_modifiers_from_smithay, + input::InputBackendId, shell::{Trigger, layout::tiling::NodeDesc}, state::State, }; @@ -73,6 +74,7 @@ impl KeyboardGrab for SwapWindowGrab { data.handle_shortcut_action( shortcuts::Action::Focus(direction), + &InputBackendId::Normal, &self.seat, serial, time, diff --git a/src/shell/seats.rs b/src/shell/seats.rs index 11211e90..27b608cf 100644 --- a/src/shell/seats.rs +++ b/src/shell/seats.rs @@ -5,7 +5,7 @@ use std::{any::Any, cell::RefCell, collections::HashMap, sync::Mutex}; use crate::{ backend::render::cursor::CursorState, config::{Config, xkb_config_to_wl}, - input::{ModifiersShortcutQueue, SupressedButtons, SupressedKeys}, + input::{InputBackendId, ModifiersShortcutQueue, SupressedButtons, SupressedKeys}, state::State, }; use smithay::{ @@ -82,12 +82,25 @@ impl Seats { self.last_active = Some(seat.clone()); } - pub fn for_device(&self, device: &D) -> Option<&Seat> { - self.iter().find(|seat| { - let userdata = seat.user_data(); - let devices = userdata.get::().unwrap(); - devices.has_device(device) - }) + pub fn for_device( + &self, + device: &D, + backend_id: &InputBackendId, + ) -> Option<&Seat> { + self.iter() + .find(|seat| { + let userdata = seat.user_data(); + let devices = userdata.get::().unwrap(); + devices.has_device(device, backend_id) + }) + .or_else(|| { + // EI devices can be transiently unregistered while the compositor recreates + // the absolute-pointer device (e.g. on a scale/geometry change), which would + // otherwise drop all pointer/touch input until the client re-binds it. EI is + // single-seat, so fall back to the active seat here, matching the EI keyboard + // path, which always targets the active seat. + matches!(backend_id, InputBackendId::Ei(_)).then(|| self.last_active()) + }) } } @@ -96,6 +109,7 @@ impl Devices { &self, device: &D, led_state: LedState, + backend_id: &InputBackendId, ) -> Vec { let id = device.id(); let mut map = self.capabilities.borrow_mut(); @@ -113,7 +127,7 @@ impl Devices { .cloned() .filter(|c| map.values().flatten().all(|has| *c != *has)) .collect::>(); - map.insert(id, caps); + map.insert((backend_id.clone(), id), caps); if device.has_capability(DeviceCapability::Keyboard) && let Some(device) = ::downcast_ref::(device) @@ -126,11 +140,18 @@ impl Devices { new_caps } - pub fn has_device(&self, device: &D) -> bool { - self.capabilities.borrow().contains_key(&device.id()) + /// Whether the given backend's device with this id is registered on the seat. + pub fn has_device(&self, device: &D, backend_id: &InputBackendId) -> bool { + self.capabilities + .borrow() + .contains_key(&(backend_id.clone(), device.id())) } - pub fn remove_device(&self, device: &D) -> Vec { + pub fn remove_device( + &self, + device: &D, + backend_id: &InputBackendId, + ) -> Vec { let id = device.id(); let mut keyboards = self.keyboards.borrow_mut(); @@ -139,7 +160,7 @@ impl Devices { } let mut map = self.capabilities.borrow_mut(); - map.remove(&id) + map.remove(&(backend_id.clone(), id)) .unwrap_or_default() .into_iter() .filter(|c| map.values().flatten().all(|has| *c != *has)) @@ -155,7 +176,8 @@ impl Devices { #[derive(Default)] pub struct Devices { - capabilities: RefCell>>, + // Keyed by `(backend, device_id)` + capabilities: RefCell>>, // Used for updating keyboard leds on kms backend keyboards: RefCell>, } @@ -185,7 +207,7 @@ struct FocusedOutput(pub Mutex>); pub struct PointerConstraintHint(pub Mutex)>>); #[derive(Default)] -pub struct LastModifierChange(pub Mutex>); +pub struct LastModifierChange(pub Mutex<(HashMap, Option)>); pub fn create_seat( dh: &DisplayHandle, @@ -265,6 +287,9 @@ pub trait SeatExt { fn supressed_buttons(&self) -> &SupressedButtons; fn modifiers_shortcut_queue(&self) -> &ModifiersShortcutQueue; fn last_modifier_change(&self) -> Option; + fn last_modifier_change_for(&self, backend_id: &InputBackendId) -> Option; + fn set_last_modifier_change(&self, backend_id: &InputBackendId, serial: Serial); + fn clear_last_modifier_change(&self, backend_id: &InputBackendId); fn pointer_constraint_hint(&self) -> Option<(WlSurface, Point)>; fn set_pointer_constraint_hint(&self, hint: Option<(WlSurface, Point)>); @@ -343,13 +368,48 @@ impl SeatExt for Seat { } fn last_modifier_change(&self) -> Option { - *self - .user_data() + self.user_data() .get::() .unwrap() .0 .lock() .unwrap() + .1 + } + + fn last_modifier_change_for(&self, backend_id: &InputBackendId) -> Option { + self.user_data() + .get::() + .unwrap() + .0 + .lock() + .unwrap() + .0 + .get(backend_id) + .copied() + } + + fn set_last_modifier_change(&self, backend_id: &InputBackendId, serial: Serial) { + let mut guard = self + .user_data() + .get::() + .unwrap() + .0 + .lock() + .unwrap(); + guard.0.insert(backend_id.clone(), serial); + guard.1 = Some(serial); + } + + fn clear_last_modifier_change(&self, backend_id: &InputBackendId) { + self.user_data() + .get::() + .unwrap() + .0 + .lock() + .unwrap() + .0 + .remove(backend_id); } fn pointer_constraint_hint(&self) -> Option<(WlSurface, Point)> { diff --git a/src/state.rs b/src/state.rs index 8cfee1a9..5ccc7a7b 100644 --- a/src/state.rs +++ b/src/state.rs @@ -248,6 +248,28 @@ pub struct Common { pub gesture_state: Option, + /// Active libei sender seats, keyed by their `eis` connection. Tracked so their virtual + /// keyboards can be re-created when the keyboard configuration changes at runtime. + pub ei_seats: std::collections::HashMap< + smithay::reexports::reis::eis::Connection, + smithay::backend::libei::EiInputSeat, + >, + + /// The shared-seat [`KeyboardSource`] assigned to each libei connection, so its + /// `ei_keyboard` key events feed the seat keyboard with independent per-source hold + /// tracking (and can be released together on disconnect). Keyed by connection. + pub ei_keyboard_source: std::collections::HashMap< + smithay::reexports::reis::eis::Connection, + smithay::input::keyboard::KeyboardSource, + >, + + /// Pointer buttons currently held by each libei connection, so they can be released when the + /// connection drops + pub ei_pointer_buttons: std::collections::HashMap< + smithay::reexports::reis::eis::Connection, + std::collections::HashSet, + >, + pub kiosk_child: Option, pub theme: cosmic::Theme, @@ -757,6 +779,9 @@ impl State { should_stop: false, kiosk_exit_code: None, gesture_state: None, + ei_seats: std::collections::HashMap::new(), + ei_keyboard_source: std::collections::HashMap::new(), + ei_pointer_buttons: std::collections::HashMap::new(), kiosk_child: None, theme: cosmic::theme::system_preference(), diff --git a/src/wayland/handlers/output_configuration.rs b/src/wayland/handlers/output_configuration.rs index 6ab678f5..4f7000fd 100644 --- a/src/wayland/handlers/output_configuration.rs +++ b/src/wayland/handlers/output_configuration.rs @@ -230,6 +230,16 @@ impl State { state.common.output_configuration_state.update(); }); + // Output scale or geometry may have changed. EI absolute-pointer regions + // are immutable per device, so any connected EI client (e.g. an RDP server) + // keeps mapping with the old scale until it reconnects. Recreate the + // device with the updated region so the mapping tracks the change live. + // (drop the backend lock first: refresh borrows `self` immutably.) + drop(backend); + if !test_only { + crate::libei::refresh_absolute_pointer_regions(self); + } + true } } diff --git a/src/xwayland.rs b/src/xwayland.rs index 83978b6e..da3fb36b 100644 --- a/src/xwayland.rs +++ b/src/xwayland.rs @@ -115,7 +115,7 @@ impl State { &self.common.display_handle, None, std::iter::empty::<(OsString, OsString)>(), - std::iter::empty::(), + ["-enable-ei-portal"], true, Stdio::null(), Stdio::null(), @@ -394,6 +394,7 @@ impl Common { sym: Keysym, code: Keycode, state: KeyState, + modifiers: ModifiersState, serial: Serial, time: u32, ) { @@ -418,7 +419,6 @@ impl Common { .last_active() .get_keyboard() .unwrap(); - let modifiers = keyboard.modifier_state(); let is_modifier = sym.is_modifier_key(); let xstate = self.xwayland_state.as_mut().unwrap();