From e4bb42f2c065b8df2ddbe67e23c9f420012034df Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 23 Jul 2026 18:49:35 +0200 Subject: [PATCH 01/24] i18n: translation updates from weblate Co-authored-by: Prasanta-Hembram --- resources/i18n/sat/cosmic_comp.ftl | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/i18n/sat/cosmic_comp.ftl diff --git a/resources/i18n/sat/cosmic_comp.ftl b/resources/i18n/sat/cosmic_comp.ftl new file mode 100644 index 00000000..e69de29b From 53a8d0bea2c1a361c1a0c57b20b4adc4b748c2a0 Mon Sep 17 00:00:00 2001 From: Timo Strunk Date: Wed, 22 Jul 2026 22:31:41 +0200 Subject: [PATCH 02/24] fix(kms): join surface threads before exit to avoid SIGABRT on shutdown When the kiosk child exits, calling process::exit() directly from the calloop callback runs libc's atexit handlers (Mesa's util_queue_kill_threads) while a KMS surface thread may still be mid-frame in eglCreateSync(). Concurrent Mesa teardown and live EGL use corrupt the heap and glibc aborts the process ("free(): corrupted unsorted chunks", SIGABRT). Stop the event loop cleanly via should_stop instead, stashing the child's exit code in Common::kiosk_exit_code. After the loop returns, pause() each DRM device (releasing master so the surface drop path skips its blocking clear_state(), and the incoming compositor gets master immediately), then drop_and_join() every surface so no thread is left in Mesa. Only then is process::exit() called, at which point atexit is safe to run. The join is done here rather than in Surface::Drop because an unconditional join in Drop deadlocks against apply_config_for_outputs during normal operation; once the event loop has stopped, the surface thread can process ThreadCommand::End and exit without the main thread servicing any pending message. https://github.com/pop-os/cosmic-comp/issues/2375 Authored by Claude Opus 4.8 Reviewed by Timo Strunk Co-Authored-By: Claude Opus 4.8 --- src/lib.rs | 36 ++++++++++++++++++++++++++++-------- src/state.rs | 2 ++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 542162eb..f7282942 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -16,7 +16,7 @@ use smithay::{ }; use anyhow::{Context, Result}; -use state::{LastRefresh, State}; +use state::{BackendData, LastRefresh, State}; use std::{ env, ffi::OsString, @@ -222,19 +222,17 @@ pub fn run(hooks: crate::hooks::Hooks) -> Result<(), Box> { // Kiosk child exited with status Ok(Some(exit_status)) => { info!("Command exited with status {:?}", exit_status); - match exit_status.code() { - // Exiting with the same status as the kiosk child - Some(code) => process::exit(code), - // The kiosk child exited with signal, exiting with error - None => process::exit(1), - } + // Stop cleanly so surface threads are joined before exit() (signal -> 1). + state.common.kiosk_exit_code = Some(exit_status.code().unwrap_or(1)); + state.common.should_stop = true; } // Command still running Ok(None) => {} // Kiosk child disappeared, exiting with error Err(err) => { warn!(?err, "Failed to wait for command"); - process::exit(1); + state.common.kiosk_exit_code = Some(1); + state.common.should_stop = true; } } } @@ -245,10 +243,32 @@ pub fn run(hooks: crate::hooks::Hooks) -> Result<(), Box> { let _ = child.kill(); } + let kiosk_exit_code = state.common.kiosk_exit_code; + + // Join surface threads before exit() so no thread is mid-eglCreateSync when + // Mesa's atexit handlers run and corrupt the heap (issue #2375). Safe here + // because the event loop has stopped; an unconditional join in Surface::Drop + // would instead deadlock against apply_config_for_outputs. + if let BackendData::Kms(kms) = &mut state.backend { + // Release master first so the surface drop path skips its blocking commit. + for device in kms.drm_devices.values_mut() { + device.drm.pause(); + } + for device in kms.drm_devices.values_mut() { + for (_, surface) in device.inner.surfaces.drain() { + surface.drop_and_join(); + } + } + } + // drop eventloop & state before logger std::mem::drop(event_loop); std::mem::drop(state); + if let Some(code) = kiosk_exit_code { + process::exit(code); + } + Ok(()) } diff --git a/src/state.rs b/src/state.rs index 21cbb400..ce546505 100644 --- a/src/state.rs +++ b/src/state.rs @@ -244,6 +244,7 @@ pub struct Common { pub clock: Clock, pub startup_done: Arc, pub should_stop: bool, + pub kiosk_exit_code: Option, pub gesture_state: Option, @@ -754,6 +755,7 @@ impl State { clock, startup_done: Arc::new(AtomicBool::new(false)), should_stop: false, + kiosk_exit_code: None, gesture_state: None, kiosk_child: None, From 24d29008f0ce00074612b49987e47468db3ef231 Mon Sep 17 00:00:00 2001 From: Timo Strunk Date: Wed, 22 Jul 2026 22:31:41 +0200 Subject: [PATCH 03/24] fix(kms): skip empty DRM cleanup commit in display_configuration display_configuration() issued an ALLOW_MODESET atomic commit on every udev event, even when no planes needed detaching. The empty commit is a no-op modeset that interferes with in-flight commits on the render thread, and it fails with EPERM whenever we don't hold DRM master (e.g. a render-only secondary GPU), aborting device enumeration. Track whether any property was added and skip the commit when there is nothing to do. A commit that does have changes still propagates its error as before, so a genuine failure to apply cleanup is not silently swallowed. https://github.com/pop-os/cosmic-comp/issues/2375 Authored by Claude Opus 4.8 Reviewed by Timo Strunk Co-Authored-By: Claude Opus 4.8 --- src/backend/kms/drm_helpers.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/backend/kms/drm_helpers.rs b/src/backend/kms/drm_helpers.rs index 8629c9d7..46659613 100644 --- a/src/backend/kms/drm_helpers.rs +++ b/src/backend/kms/drm_helpers.rs @@ -75,6 +75,7 @@ pub fn display_configuration( // And then cleanup if device.is_atomic() { let mut req = AtomicModeReq::new(); + let mut has_changes = false; let plane_handles = device.plane_handles()?; // We cannot just shortcut and use the legacy api for all cleanups because of this. @@ -95,10 +96,15 @@ pub fn display_configuration( let fb_id = get_prop(device, plane, "FB_ID")?; req.add_property(plane, crtc_id, property::Value::CRTC(None)); req.add_property(plane, fb_id, property::Value::Framebuffer(None)); + has_changes = true; } } } - device.atomic_commit(AtomicCommitFlags::ALLOW_MODESET, req)?; + // Skip an empty commit: a no-op modeset that also fails with EPERM + // without DRM master (e.g. a render-only secondary GPU). + if has_changes { + device.atomic_commit(AtomicCommitFlags::ALLOW_MODESET, req)?; + } } else { for crtc in res_handles.crtcs() { #[allow(deprecated)] From 6108cef8b368653c51c0bbe6f81132a5a80b5c46 Mon Sep 17 00:00:00 2001 From: RipleyTom Date: Fri, 24 Jul 2026 09:56:52 +0200 Subject: [PATCH 04/24] fix: workaround for Chromium apps breaking below 1.0 scaling --- src/state.rs | 7 +++++-- src/wayland/handlers/fractional_scale.rs | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/state.rs b/src/state.rs index ce546505..24ccd9e1 100644 --- a/src/state.rs +++ b/src/state.rs @@ -956,8 +956,11 @@ impl Common { ); if let Some(output) = primary_scanout_output { with_fractional_scale(states, |fraction_scale| { - fraction_scale - .set_preferred_scale(output.current_scale().fractional_scale()); + // The 1.0 clamp is a workaround for Chromium + // TODO: remove if Chromium ever gets fixed + fraction_scale.set_preferred_scale( + output.current_scale().fractional_scale().max(1.0), + ); }); } } diff --git a/src/wayland/handlers/fractional_scale.rs b/src/wayland/handlers/fractional_scale.rs index aea0cc7e..dcc8d7f9 100644 --- a/src/wayland/handlers/fractional_scale.rs +++ b/src/wayland/handlers/fractional_scale.rs @@ -49,7 +49,10 @@ impl FractionalScaleHandler for State { with_states(&surface, |states| { with_fractional_scale(states, |fractional_scale| { - fractional_scale.set_preferred_scale(output.current_scale().fractional_scale()); + // The 1.0 clamp is a workaround for Chromium + // TODO: remove if Chromium ever gets fixed + fractional_scale + .set_preferred_scale(output.current_scale().fractional_scale().max(1.0)); }); }); } From 44de3063ee30ba704592fd6c06db62639292f9f2 Mon Sep 17 00:00:00 2001 From: Tom Grushka Date: Thu, 23 Jul 2026 22:43:56 -0600 Subject: [PATCH 05/24] dbus: Fix deadlock in a11y keyboard monitor A11yKeyboardMonitorState::key_event calls self.has_key_grab() inside self.clients guard loop. A "well-behaved" a11y client calls `WatchKeyboard` before `SetKeyGrabs`. If WatchKeyboard is not called first, the `!client.watched` guard does not short-circuit, so `has_key_grab()` runs and locks `self.clients` again. This causes a deadlock and the compositor freezes, requiring a hard reboot. This commit moves the `self.has_key_grab()` before the guard to prevent a double lock. --- src/dbus/a11y_keyboard_monitor.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/dbus/a11y_keyboard_monitor.rs b/src/dbus/a11y_keyboard_monitor.rs index cf66a54b..c97aa590 100644 --- a/src/dbus/a11y_keyboard_monitor.rs +++ b/src/dbus/a11y_keyboard_monitor.rs @@ -150,9 +150,10 @@ impl A11yKeyboardMonitorState { } pub fn key_event(&self, modifiers: &ModifiersState, keysym: &KeysymHandle, state: KeyState) { + let has_key_grab = self.has_key_grab(modifiers, keysym.modified_sym()); let clients = self.clients.lock().unwrap(); for (unique_name, client) in clients.0.iter() { - if !client.watched && !self.has_key_grab(modifiers, keysym.modified_sym()) { + if !client.watched && !has_key_grab { continue; } From 5ce21fb4f3de9671d6cb6c4738d9bd02d541a57a Mon Sep 17 00:00:00 2001 From: RipleyTom Date: Thu, 23 Jul 2026 19:28:32 +0200 Subject: [PATCH 06/24] fix: ensure X window focus on unmap/map --- src/shell/focus/mod.rs | 23 +++++++++++++++++++++++ src/shell/focus/target.rs | 2 +- src/xwayland.rs | 2 +- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/shell/focus/mod.rs b/src/shell/focus/mod.rs index 4e88c499..cab37a85 100644 --- a/src/shell/focus/mod.rs +++ b/src/shell/focus/mod.rs @@ -212,6 +212,29 @@ impl Shell { state.common.shell.write().update_active(); } + // We suppress Element(X) to Fullscreen(X) transition to avoid + // loss of focus by the X window when having a transition to fullscreen + // but in the case of X11 unmap/map the leave/enter needs to happen for the X11 + // internal state to be focused on the window + pub fn set_focus_on_x11_map( + state: &mut State, + target: &KeyboardFocusTarget, + seat: &Seat, + update_cursor: bool, + ) { + let need_reset = seat + .get_keyboard() + .and_then(|keyboard| keyboard.current_focus()) + .and_then(|current| current.x11_surface()) + .is_some_and(|current| Some(current) == target.x11_surface()); + + if need_reset { + update_focus_state(seat, None, state, None, false); + } + + Shell::set_focus(state, Some(target), seat, None, update_cursor); + } + pub fn append_focus_stack(&mut self, target: impl Into, seat: &Seat) { let target = target.into(); if target.is_minimized() { diff --git a/src/shell/focus/target.rs b/src/shell/focus/target.rs index 929cd92e..80b1a34f 100644 --- a/src/shell/focus/target.rs +++ b/src/shell/focus/target.rs @@ -326,7 +326,7 @@ impl KeyboardFocusTarget { } } - fn x11_surface(&self) -> Option { + pub fn x11_surface(&self) -> Option { match self { KeyboardFocusTarget::Element(mapped) => mapped.active_window().x11_surface().cloned(), KeyboardFocusTarget::Fullscreen(surface) => surface.x11_surface().cloned(), diff --git a/src/xwayland.rs b/src/xwayland.rs index 25b8b0e0..83978b6e 100644 --- a/src/xwayland.rs +++ b/src/xwayland.rs @@ -858,7 +858,7 @@ impl XwmHandler for State { if let Some(target) = res { let seat = shell.seats.last_active().clone(); std::mem::drop(shell); - Shell::set_focus(self, Some(&target), &seat, None, false); + Shell::set_focus_on_x11_map(self, &target, &seat, false); } } } From 9514b4945edc2656b38a34419c3a8618f490cb17 Mon Sep 17 00:00:00 2001 From: v0up3r Date: Mon, 27 Jul 2026 19:21:35 -0300 Subject: [PATCH 07/24] fix(kms): don't panic when renderer creation fails after a GPU reset When the DRM device is lost transiently, for example when the amdgpu driver performs a GPU reset and recovers ("device wedged, but recovered through reset"), acquiring a renderer in `SurfaceThreadState::redraw` fails. The three `renderer`/`single_renderer` calls used `.unwrap()`, so the failure panicked the surface's render thread. Because that panic unwinds across the EGL/GBM FFI boundary it aborts the whole process (SIGABRT), dropping the session back to the greeter and closing every running application. `redraw` is already fallible and its caller logs the error and reschedules a redraw, and the frame-submission path (`render_frame`/`queue_frame`) already propagates errors the same way. Propagate the renderer-acquisition errors too, so a transient device loss is retried on the next frame, once the device is back, instead of taking down the compositor. Relates to #649. Assisted by an AI coding tool; I have reviewed and fully understand the change and am able to maintain it and respond to review. --- src/backend/kms/surface/mod.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/backend/kms/surface/mod.rs b/src/backend/kms/surface/mod.rs index bb28bc3c..eb232377 100644 --- a/src/backend/kms/surface/mod.rs +++ b/src/backend/kms/surface/mod.rs @@ -1006,12 +1006,16 @@ impl SurfaceThreadState { &self.shell.read(), ); + // Acquiring a renderer can fail transiently when the underlying DRM + // device is lost (e.g. after a GPU reset). let mut renderer = if render_node != self.target_node { self.api .renderer(&render_node, &self.target_node, compositor.format()) - .unwrap() + .map_err(|err| anyhow::format_err!("Failed to create renderer: {:?}", err))? } else { - self.api.single_renderer(&self.target_node).unwrap() + self.api + .single_renderer(&self.target_node) + .map_err(|err| anyhow::format_err!("Failed to create renderer: {:?}", err))? }; self.timings.start_render(&self.clock); @@ -1267,7 +1271,10 @@ impl SurfaceThreadState { }) .context("Failed to draw to offscreen render target")?; - renderer = self.api.single_renderer(&self.target_node).unwrap(); + renderer = self + .api + .single_renderer(&self.target_node) + .map_err(|err| anyhow::format_err!("Failed to create renderer: {:?}", err))?; elements = postprocess_elements( &mut renderer, From 8d450d497763912c4575c0f28dda661dafd4366c Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 27 Jul 2026 15:41:58 -0600 Subject: [PATCH 08/24] fix: clamp pointer edge after applying the output offset --- src/input/mod.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/input/mod.rs b/src/input/mod.rs index 0cb26a9d..39bff9dc 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -393,10 +393,12 @@ impl State { // - output_geometry.size so that we don't send leave events to a fullscreen app // - logical size so that the position doesn't end up outside the actual size of the output // See https://github.com/pop-os/cosmic-comp/pull/2568 - let max_x = output_geometry_loc.x - + logical.w.min(output_geometry.size.w as f64).next_down(); - let max_y = output_geometry_loc.y - + logical.h.min(output_geometry.size.h as f64).next_down(); + let max_x = (output_geometry_loc.x + + logical.w.min(output_geometry.size.w as f64)) + .next_down(); + let max_y = (output_geometry_loc.y + + logical.h.min(output_geometry.size.h as f64)) + .next_down(); position.x = position.x.clamp(output_geometry_loc.x, max_x); position.y = position.y.clamp(output_geometry_loc.y, max_y); From ffeda3375a7e60ace6ae64b19432f1f0c1fc1034 Mon Sep 17 00:00:00 2001 From: zyads Date: Mon, 27 Jul 2026 12:13:52 -0400 Subject: [PATCH 09/24] fix(corner_radius): check LayerHookId for layer surfaces GetCornerRadiusLayer looked up the existing hook via ToplevelHookId while inserting into LayerHookId a few lines later. Both are Mutex)>> so it compiles, but they are distinct data_map slots: the "does this surface already have a corner-radius object?" check for a layer surface read the toplevel slot, so it could not see a layer object that was already there, and it created a toplevel entry on a layer surface as a side effect. Every other read of the layer hook already uses LayerHookId. Disclosure: found and written with AI assistance (Claude). The change is the one line quoted above; I understand it and verified the tree builds with it. --- src/wayland/protocols/corner_radius.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wayland/protocols/corner_radius.rs b/src/wayland/protocols/corner_radius.rs index be6f9ca0..51dc1153 100644 --- a/src/wayland/protocols/corner_radius.rs +++ b/src/wayland/protocols/corner_radius.rs @@ -190,7 +190,7 @@ where let radius_exists = with_states(surface.wl_surface(), |surface_data| { let hook_id = surface_data .data_map - .get_or_insert_threadsafe(|| ToplevelHookId::new(None)); + .get_or_insert_threadsafe(|| LayerHookId::new(None)); let guard = hook_id.lock().unwrap(); guard.as_ref().map(|(_, t)| t.upgrade().is_ok()) }); From d687285338dc35947a3262c119e92313ee02ef01 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 29 Jul 2026 11:01:49 +0200 Subject: [PATCH 10/24] i18n: translation updates from weblate Co-authored-by: Andi Chandler Co-authored-by: Davide Co-authored-by: Sproston Co-authored-by: Wilbur Lindqvist Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-comp/en_GB/ Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-comp/it/ Translation: Pop OS/COSMIC Comp --- resources/i18n/en-GB/cosmic_comp.ftl | 20 ++++++++++++++++++++ resources/i18n/it/cosmic_comp.ftl | 6 +++--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/resources/i18n/en-GB/cosmic_comp.ftl b/resources/i18n/en-GB/cosmic_comp.ftl index e69de29b..0bd9c20b 100644 --- a/resources/i18n/en-GB/cosmic_comp.ftl +++ b/resources/i18n/en-GB/cosmic_comp.ftl @@ -0,0 +1,20 @@ +a11y-zoom-move-continuously = View moves continuously with pointer +a11y-zoom-move-onedge = View moves when pointer reaches edge +a11y-zoom-move-centered = View moves to keep pointer centred +a11y-zoom-settings = Magnifier settings... +grow-window = Grow +shrink-window = Shrink +swap-windows = Swap Windows +stack-windows = Stack Windows +unknown-keybinding = +window-menu-minimize = Minimise +window-menu-maximize = Maximise +window-menu-fullscreen = Fullscreen +window-menu-tiled = Float window +window-menu-screenshot = Take screenshot +window-menu-move = Move +window-menu-resize = Resize +window-menu-move-prev-workspace = Move to previous workspace +window-menu-move-next-workspace = Move to next workspace +window-menu-stack = Create window stack +window-menu-unstack-all = Unstack windows diff --git a/resources/i18n/it/cosmic_comp.ftl b/resources/i18n/it/cosmic_comp.ftl index ba92609f..b1eb56c3 100644 --- a/resources/i18n/it/cosmic_comp.ftl +++ b/resources/i18n/it/cosmic_comp.ftl @@ -1,4 +1,4 @@ -grow-window = Ingrandisci +grow-window = Crescere shrink-window = Riduci swap-windows = Scambia le finestre stack-windows = Sovrapponi le finestre @@ -6,7 +6,7 @@ unknown-keybinding = window-menu-minimize = Minimizza window-menu-maximize = Massimizza window-menu-tiled = Finestra flottante -window-menu-screenshot = Cattura una schermata +window-menu-screenshot = Scatta uno screenshot window-menu-move = Sposta window-menu-resize = Ridimensiona window-menu-move-prev-workspace = Sposta all'area di lavoro precedente @@ -22,7 +22,7 @@ window-menu-resize-edge-left = Sinistra window-menu-resize-edge-right = Destra window-menu-resize-edge-bottom = In basso window-menu-fullscreen = Schermo intero -a11y-zoom-settings = Opzioni lente d'ingrandimento... +a11y-zoom-settings = Impostazioni lente d'ingrandimento... a11y-zoom-move-continuously = Segue sempre il cursorse a11y-zoom-move-centered = Mantiene il cursore centrato a11y-zoom-move-onedge = Segue il cursore ai bordi From 091583ac84abac02967ae358cf9570ddfef63b31 Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Fri, 9 Jan 2026 13:47:31 +0100 Subject: [PATCH 11/24] backend: Support wl-dmabuf v6 --- Cargo.lock | 30 ++- Cargo.toml | 2 +- src/backend/kms/device.rs | 32 +-- src/backend/kms/mod.rs | 186 ++++++++++++++---- src/backend/kms/surface/mod.rs | 64 +++--- src/backend/winit.rs | 5 +- src/backend/x11.rs | 5 +- src/shell/element/surface.rs | 5 +- src/state.rs | 11 +- src/utils/global.rs | 28 +++ src/utils/mod.rs | 1 + src/wayland/handlers/dmabuf.rs | 19 +- src/wayland/protocols/drm.rs | 91 ++++++--- .../protocols/output_configuration/mod.rs | 29 +-- 14 files changed, 298 insertions(+), 210 deletions(-) create mode 100644 src/utils/global.rs diff --git a/Cargo.lock b/Cargo.lock index fd733056..aec3b990 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -312,12 +312,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atomic_float" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a" - [[package]] name = "atomicwrites" version = "0.4.2" @@ -1194,7 +1188,7 @@ dependencies = [ "libc", "option-ext", "redox_users", - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -1508,7 +1502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2695,7 +2689,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2818,7 +2812,7 @@ version = "1.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a1886916523694cd6ea3d175f03a1e5010699a2a4cc13696d83d7bea1d80638" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -3428,7 +3422,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -4524,7 +4518,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4819,12 +4813,11 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smithay" version = "0.7.0" -source = "git+https://github.com/smithay/smithay.git?rev=1ed69cb#1ed69cb55e9d0e9f38887c3b7d53d24365c4b14f" +source = "git+https://github.com/smithay/smithay.git?rev=cdc03f7#cdc03f73ce57d3352c0c57a69971e3e16544051c" dependencies = [ "aliasable", "appendlist", "ash", - "atomic_float", "bitflags 2.11.0", "calloop", "cc", @@ -4846,6 +4839,7 @@ dependencies = [ "libseat", "pixman", "pkg-config", + "portable-atomic", "profiling", "rand 0.9.2", "reis", @@ -5113,7 +5107,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -5887,9 +5881,9 @@ dependencies = [ [[package]] name = "wayland-protocols" -version = "0.32.12" +version = "0.32.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +checksum = "23d0c813de3daa2ed6520af85a3bd49b0e722a3078506899aa9686fea58dc4b6" dependencies = [ "bitflags 2.11.0", "wayland-backend", @@ -6132,7 +6126,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 3f8a9e73..fb9fa0bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,4 +144,4 @@ lto = "fat" cosmic-protocols = { git = "https://github.com/pop-os//cosmic-protocols", branch = "main" } [patch.crates-io] -smithay = { git = "https://github.com/smithay/smithay.git", rev = "1ed69cb" } +smithay = { git = "https://github.com/smithay/smithay.git", rev = "cdc03f7" } diff --git a/src/backend/kms/device.rs b/src/backend/kms/device.rs index 44682dc5..418a16f3 100644 --- a/src/backend/kms/device.rs +++ b/src/backend/kms/device.rs @@ -59,7 +59,7 @@ use std::{ time::Duration, }; -use super::{drm_helpers, socket::Socket, surface::Surface}; +use super::{drm_helpers, surface::Surface}; #[derive(Debug)] pub struct EGLInternals { @@ -98,14 +98,12 @@ pub struct Device { pub texture_formats: FormatSet, event_token: Option, - pub socket: Option, } #[derive(Debug)] struct ReusableDevice { leasing_global: Option, active_clients: HashSet, - socket: Option, } #[derive(Debug)] @@ -593,7 +591,7 @@ impl State { } } - pub fn device_removed(&mut self, dev: dev_t, dh: &DisplayHandle) -> Result<()> { + pub fn device_removed(&mut self, dev: dev_t, _dh: &DisplayHandle) -> Result<()> { let backend = self.backend.kms(); // we can't use DrmNode::from_node_id, because that assumes the node is still on sysfs let drm_node = backend @@ -616,13 +614,6 @@ impl State { if let Some(token) = device.event_token.take() { self.common.event_loop_handle.remove(token); } - if let Some(socket) = device.socket.take() { - self.common.event_loop_handle.remove(socket.token); - self.common - .dmabuf_state - .destroy_global::(dh, socket.dmabuf_global); - dh.remove_global::(socket.drm_global); - } backend.api.as_mut().remove_node(&device.inner.render_node); backend .primary_node @@ -755,23 +746,7 @@ impl Device { let ReusableDevice { leasing_global, active_clients, - socket, } = reuse.unwrap_or_else(|| { - let socket = match (!is_software) - .then(|| common.create_socket(dh, render_node, texture_formats.clone())) - .transpose() - { - Ok(socket) => socket, - Err(err) => { - warn!( - ?err, - "Failed to initialize hardware-acceleration for clients on {}.", - render_node, - ); - None - } - }; - let leasing_global = match (!is_software) .then(|| DrmLeaseState::new::(dh, &dev_node)) .transpose() @@ -790,7 +765,6 @@ impl Device { ReusableDevice { leasing_global, active_clients: HashSet::new(), - socket, } }); @@ -831,7 +805,6 @@ impl Device { texture_formats, event_token: Some(token), - socket, }) } @@ -892,7 +865,6 @@ impl Device { let device = ReusableDevice { leasing_global: self.inner.leasing_global, active_clients: self.inner.active_clients, - socket: self.socket, }; let state = OldDeviceState { diff --git a/src/backend/kms/mod.rs b/src/backend/kms/mod.rs index 16559e4e..df711387 100644 --- a/src/backend/kms/mod.rs +++ b/src/backend/kms/mod.rs @@ -4,12 +4,15 @@ use crate::{ config::{CompOutputConfig, ScreenFilter}, shell::Shell, state::BackendData, - utils::{env::dev_var, prelude::*}, - wayland::protocols::output_power::OutputPowerState, + utils::{env::dev_var, global::remove_global_with_timer, prelude::*}, + wayland::protocols::{drm::WlDrmState, output_power::OutputPowerState}, }; -use anyhow::{Context, Result}; -use calloop::LoopSignal; +use anyhow::{self, Context, Result}; +use calloop::{ + LoopSignal, + timer::{TimeoutAction, Timer}, +}; use cosmic_comp_config::output::comp::{AdaptiveSync, OutputState}; use indexmap::IndexMap; use render::gles::GbmGlowBackend; @@ -32,11 +35,12 @@ use smithay::{ control::{Device as _, connector::Interface, crtc}, }, input::{self, Libinput}, + wayland_protocols::wp::linux_dmabuf::zv1::server::zwp_linux_dmabuf_feedback_v1::TrancheFlags, wayland_server::{Client, DisplayHandle}, }, utils::{Clock, DevPath, Monotonic, Size}, wayland::{ - dmabuf::DmabufGlobal, + dmabuf::{DmabufFeedbackBuilder, DmabufGlobal}, drm_syncobj::{DrmSyncobjState, supports_syncobj_eventfd}, relative_pointer::RelativePointerManagerState, }, @@ -48,12 +52,12 @@ use std::{ collections::{HashMap, HashSet}, path::Path, sync::{Arc, RwLock, atomic::AtomicBool}, + time::Duration, }; mod device; mod drm_helpers; pub mod render; -mod socket; mod surface; use device::*; pub(crate) use surface::Surface; @@ -74,6 +78,7 @@ pub struct KmsState { libinput: Libinput, pub syncobj_state: Option, + pub dmabuf_global: Option, } pub struct KmsGuard<'a> { @@ -131,6 +136,7 @@ pub fn init_backend( libinput: libinput_context, syncobj_state: None, + dmabuf_global: None, }); // manually add already present gpus @@ -142,9 +148,10 @@ pub fn init_backend( } } - if let Err(err) = state.backend.kms().select_primary_gpu(dh) { + if let Err(err) = state.select_primary_gpu(dh) { warn!("Failed to determine primary gpu: {}", err); } + state.update_default_feedback(); if let Err(err) = state.refresh_output_config() { info!( @@ -317,11 +324,13 @@ fn init_udev( { let backend = state.backend.kms(); - if matches!(event, UdevEvent::Added { .. } | UdevEvent::Removed { .. }) - && backend.primary_node.read().unwrap().is_none() - && let Err(err) = state.backend.kms().select_primary_gpu(&dh) - { - warn!("Failed to determine a new primary gpu: {}", err); + if matches!(event, UdevEvent::Added { .. } | UdevEvent::Removed { .. }) { + if backend.primary_node.read().unwrap().is_none() + && let Err(err) = state.select_primary_gpu(&dh) + { + warn!("Failed to determine a new primary gpu: {}", err); + } + state.update_default_feedback(); } } @@ -465,50 +474,118 @@ impl State { device.drm.pause(); } } -} -impl KmsState { fn select_primary_gpu(&mut self, dh: &DisplayHandle) -> Result<()> { // We don't have to check the allow/blocklist here, // as any disallowed devices won't be in `self.drm_devices`. - let mut primary_node = self.primary_node.write().unwrap(); + let kms = self.backend.kms(); + let mut primary_node = kms.primary_node.write().unwrap(); let _ = primary_node.take(); // if we error don't leave an old node in place - *primary_node = determine_primary_gpu(&self.drm_devices, self.session.seat())?; + *primary_node = determine_primary_gpu(&kms.drm_devices, kms.session.seat())?; if let Some(node) = *primary_node { info!("Using {} as primary gpu for rendering.", node); - self.software_renderer.take(); - } else if self.software_renderer.is_none() { + kms.software_renderer.take(); + + // setup minimal feedback. We will update it in `update_default_feedback` + let primary_formats = kms + .drm_devices + .values() + .find(|dev| dev.inner.render_node == node) + .unwrap() + .texture_formats + .clone(); + let feedback = DmabufFeedbackBuilder::new(node.dev_id(), primary_formats.clone()) + .build() + .unwrap(); + + if let Some(global) = kms.dmabuf_global.as_ref() { + self.common + .dmabuf_state + .set_default_feedback(global, &feedback); + } else { + let dmabuf_global = self + .common + .dmabuf_state + .create_global_with_default_feedback::(dh, &feedback); + kms.dmabuf_global = Some(dmabuf_global); + }; + + let device_path = node + .dev_path_with_type(NodeType::Render) + .or_else(|| node.dev_path()) + .ok_or(anyhow::anyhow!( + "Could not determine path for gpu node: {}", + node + ))?; + + if let Some(drm) = self.common.wl_drm_state.as_mut() { + drm.update_device(device_path, primary_formats); + } else { + self.common.wl_drm_state = Some(WlDrmState::new::( + dh, + device_path, + primary_formats, + kms.dmabuf_global.as_ref().unwrap(), + )); + } + } else if kms.software_renderer.is_none() { info!("Failed to find a suitable gpu, using software renderingr"); - self.software_renderer = match software_renderer() { + kms.software_renderer = match software_renderer() { Ok(renderer) => Some(renderer), Err(err) => { error!(?err, "Failed to initialize software EGL renderer."); None } }; + + if let Some(drm) = self.common.wl_drm_state.take() { + remove_global_with_timer(dh, &self.common.event_loop_handle, drm.global().clone()); + } + if let Some(global) = kms.dmabuf_global.take() { + self.common + .dmabuf_state + .disable_global::(dh, &global); + let source = Timer::from_duration(Duration::from_secs(5)); + let res = + self.common + .event_loop_handle + .insert_source(source, move |_, _, state| { + state + .common + .dmabuf_state + .destroy_global::(&state.common.display_handle, global); + TimeoutAction::Drop + }); + if let Err(err) = res { + tracing::error!( + "failed to insert timer source to destroy output global: {}", + err + ); + } + } } if !crate::utils::env::bool_var("COSMIC_DISABLE_SYNCOBJ").unwrap_or(false) { if let Some(primary_node) = primary_node .as_ref() .and_then(|node| node.node_with_type(NodeType::Primary).and_then(|x| x.ok())) - && let Some(device) = self.drm_devices.get(&primary_node) + && let Some(device) = kms.drm_devices.get(&primary_node) { let import_device = device.drm.device().device_fd().clone(); if supports_syncobj_eventfd(&import_device) { - if let Some(state) = self.syncobj_state.as_mut() { + if let Some(state) = kms.syncobj_state.as_mut() { state.update_device(import_device); } else { let syncobj_state = DrmSyncobjState::new::(dh, import_device); - self.syncobj_state = Some(syncobj_state); + kms.syncobj_state = Some(syncobj_state); } return Ok(()); } } - if let Some(old_state) = self.syncobj_state.take() { + if let Some(old_state) = kms.syncobj_state.take() { dh.remove_global::(old_state.into_global()); } } @@ -516,6 +593,45 @@ impl KmsState { Ok(()) } + fn update_default_feedback(&mut self) { + let kms = self.backend.kms(); + let primary_node = kms.primary_node.read().unwrap(); + if let Some(primary_node) = *primary_node { + let primary_formats = kms + .drm_devices + .values() + .find(|dev| dev.inner.render_node == primary_node) + .unwrap() + .texture_formats + .clone(); + + let mut feedback = + DmabufFeedbackBuilder::new(primary_node.dev_id(), primary_formats.clone()); + for dev in kms + .drm_devices + .values() + .filter(|dev| dev.inner.render_node != primary_node) + { + feedback = feedback.add_preference_tranche( + dev.inner.render_node.dev_id(), + TrancheFlags::Sampling, + dev.texture_formats.iter().cloned(), + 6..=6, + ); + } + + let default_feedback = feedback.build().unwrap(); + self.common.dmabuf_state.set_default_feedback( + kms.dmabuf_global + .as_ref() + .expect("Primary node but no dmabuf global?"), + &default_feedback, + ); + } + } +} + +impl KmsState { pub fn switch_vt(&mut self, num: i32) -> Result<(), anyhow::Error> { self.session.change_vt(num).map_err(Into::into) } @@ -523,29 +639,25 @@ impl KmsState { pub fn dmabuf_imported( &mut self, client: Option, - global: &DmabufGlobal, + _global: &DmabufGlobal, dmabuf: Dmabuf, ) -> Result { + let device_node = dmabuf + .node() + .unwrap_or_else(|| self.primary_node.read().unwrap().unwrap()); let mut device = self .drm_devices .values_mut() - .find(|device| { - device - .socket - .as_ref() - .map(|s| &s.dmabuf_global == global) - .unwrap_or(false) - }) - .context("Couldn't find gpu for dmabuf global")?; + .find(|dev| dev.inner.render_node == device_node) + .ok_or(anyhow::anyhow!( + "Unable to find device for node: {}", + device_node + ))?; // If device advertised to client doesn't support format/modifier, select // first device that does. This is needed for image-copy from // output/toplevel on a different node. - // - // TODO: After - // https://gitlab.freedesktop.org/wayland/wayland-protocols/-/merge_requests/268, - // only try the device specified explicitly by the client, if set. - if !device.texture_formats.contains(&dmabuf.format()) { + if dmabuf.node().is_none() && !device.texture_formats.contains(&dmabuf.format()) { device = self .drm_devices .values_mut() diff --git a/src/backend/kms/surface/mod.rs b/src/backend/kms/surface/mod.rs index eb232377..cbc89381 100644 --- a/src/backend/kms/surface/mod.rs +++ b/src/backend/kms/surface/mod.rs @@ -1529,7 +1529,7 @@ fn get_surface_dmabuf_feedback( render_node: DrmNode, target_node: DrmNode, render_formats: FormatSet, - _target_formats: FormatSet, + target_formats: FormatSet, primary_plane_formats: FormatSet, overlay_plane_formats: Option, ) -> SurfaceDmabufFeedback { @@ -1547,60 +1547,40 @@ fn get_surface_dmabuf_feedback( .cloned() .collect::() }); - let builder = DmabufFeedbackBuilder::new(render_node.dev_id(), render_formats); - /* - // Sadly no implementation would pick this up as a preferred render tranche, - // where the combined formats would increase our chances of doing a dmabuf copy. - // .. So we should probably not advertise this on the off-chance it actually triggers bugs. - // + let mut builder = DmabufFeedbackBuilder::new(render_node.dev_id(), render_formats.clone()); - let combined_formats = render_formats.intersection(&target_formats).cloned().collect::(); - if target_node != render_node.dev_id() && !combined_formats.is_empty() { - builder = builder.add_preference_tranche( - render_node.dev_id(), - None, - combined_formats, - ); - }; - - // We also can't advertise scan out tranches for the actual display device, - // as e.g. the nvidia driver might then send us dmabufs, that makes e.g. the iris hangs on import... - if target_node != render_node.dev_id() && !combined_formats.is_empty() { + if target_node != render_node { builder = builder.add_preference_tranche( target_node.dev_id(), - Some(zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout), - combined_formats, + zwp_linux_dmabuf_feedback_v1::TrancheFlags::Sampling, + target_formats, + 6..=6, ); }; - - // So no fun combinations, we gotta wait for dmabuf-v6 - */ - let render_feedback = builder.clone().build().unwrap(); - let primary_scanout_feedback = (target_node == render_node).then(|| { + + let primary_scanout_feedback = builder + .clone() + .add_preference_tranche( + target_node.dev_id(), + zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout, + primary_plane_formats, + 4..=6, + ) + .build() + .unwrap(); + let overlay_scanout_feedback = overlay_plane_formats.map(|formats| { builder - .clone() .add_preference_tranche( - render_node.dev_id(), - Some(zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout), - primary_plane_formats, + target_node.dev_id(), + zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout, + formats, + 4..=6, ) .build() .unwrap() }); - let overlay_scanout_feedback = overlay_plane_formats - .filter(|_| target_node == render_node) - .map(|formats| { - builder - .add_preference_tranche( - render_node.dev_id(), - Some(zwp_linux_dmabuf_feedback_v1::TrancheFlags::Scanout), - formats, - ) - .build() - .unwrap() - }); SurfaceDmabufFeedback { render_feedback, diff --git a/src/backend/winit.rs b/src/backend/winit.rs index 0ff249a7..24ef64b4 100644 --- a/src/backend/winit.rs +++ b/src/backend/winit.rs @@ -6,6 +6,7 @@ use crate::{ shell::{Devices, SeatExt}, state::{BackendData, Common}, utils::prelude::*, + wayland::protocols::drm::WlDrmState, }; use anyhow::{Context, Result, anyhow}; use cosmic_comp_config::output::comp::{OutputConfig, TransformDef}; @@ -273,7 +274,7 @@ fn init_egl_client_side( .create_global_with_default_feedback::(dh, &feedback); let render_node = render_node.unwrap().unwrap(); - let _drm_global_id = state.common.wl_drm_state.create_global::( + state.common.wl_drm_state = Some(WlDrmState::new::( dh, render_node .dev_path_with_type(NodeType::Render) @@ -284,7 +285,7 @@ fn init_egl_client_side( ))?, dmabuf_formats, &dmabuf_global, - ); + )); info!("EGL hardware-acceleration enabled."); } diff --git a/src/backend/x11.rs b/src/backend/x11.rs index bd0e1b34..98b27453 100644 --- a/src/backend/x11.rs +++ b/src/backend/x11.rs @@ -6,6 +6,7 @@ use crate::{ shell::{Devices, SeatExt}, state::{BackendData, Common}, utils::prelude::*, + wayland::protocols::drm::WlDrmState, }; use anyhow::{Context, Result, anyhow}; use cosmic_comp_config::output::comp::OutputConfig; @@ -497,7 +498,7 @@ where .common .dmabuf_state .create_global_with_default_feedback::(dh, &default_feedback); - let _drm_global_id = state.common.wl_drm_state.create_global::( + state.common.wl_drm_state = Some(WlDrmState::new::( dh, render_node .dev_path_with_type(NodeType::Render) @@ -508,7 +509,7 @@ where ))?, renderer.dmabuf_formats(), &dmabuf_global, - ); + )); info!("EGL hardware-acceleration enabled."); diff --git a/src/shell/element/surface.rs b/src/shell/element/surface.rs index e146efeb..99bb2f78 100644 --- a/src/shell/element/surface.rs +++ b/src/shell/element/surface.rs @@ -838,10 +838,7 @@ impl CosmicSurface { self.0 .send_dmabuf_feedback(output, primary_scan_out_output, |_, data| { if is_fullscreen { - feedback - .primary_scanout_feedback - .as_ref() - .unwrap_or(&feedback.render_feedback) + &feedback.primary_scanout_feedback } else if frame_time_filter_fn(data) == Kind::ScanoutCandidate { feedback .overlay_scanout_feedback diff --git a/src/state.rs b/src/state.rs index 24ccd9e1..8cfee1a9 100644 --- a/src/state.rs +++ b/src/state.rs @@ -276,7 +276,7 @@ pub struct Common { pub idle_inhibiting_surfaces: HashSet, pub shm_state: ShmState, pub cursor_shape_manager_state: CursorShapeManagerState, - pub wl_drm_state: WlDrmState>, + pub wl_drm_state: Option>>, pub viewporter_state: ViewporterState, pub kde_decoration_state: KdeDecorationState, pub xdg_decoration_state: XdgDecorationState, @@ -326,7 +326,7 @@ pub enum LockedBackend<'a> { pub struct SurfaceDmabufFeedback { pub render_feedback: DmabufFeedback, pub overlay_scanout_feedback: Option, - pub primary_scanout_feedback: Option, + pub primary_scanout_feedback: DmabufFeedback, } #[derive(Debug)] @@ -671,7 +671,7 @@ impl State { let cursor_shape_manager_state = CursorShapeManagerState::new::(dh); let seat_state = SeatState::::new(); let viewporter_state = ViewporterState::new::(dh); - let wl_drm_state = WlDrmState::>::default(); + let wl_drm_state = None; let kde_decoration_state = KdeDecorationState::new::(dh, Mode::Client); let xdg_decoration_state = XdgDecorationState::new::(dh); let session_lock_manager_state = @@ -1066,10 +1066,7 @@ impl Common { surface, render_element_states, &feedback.render_feedback, - feedback - .primary_scanout_feedback - .as_ref() - .unwrap_or(&feedback.render_feedback), + &feedback.primary_scanout_feedback, ) }, ) diff --git a/src/utils/global.rs b/src/utils/global.rs new file mode 100644 index 00000000..e309f68a --- /dev/null +++ b/src/utils/global.rs @@ -0,0 +1,28 @@ +use std::time::Duration; + +use calloop::{ + LoopHandle, + timer::{TimeoutAction, Timer}, +}; +use smithay::reexports::wayland_server::DisplayHandle; +use wayland_backend::server::GlobalId; + +pub fn remove_global_with_timer( + dh: &DisplayHandle, + event_loop_handle: &LoopHandle, + id: GlobalId, +) { + dh.disable_global::(id.clone()); + let source = Timer::from_duration(Duration::from_secs(5)); + let dh = dh.clone(); + let res = event_loop_handle.insert_source(source, move |_, _, _state| { + dh.remove_global::(id.clone()); + TimeoutAction::Drop + }); + if let Err(err) = res { + tracing::error!( + "failed to insert timer source to destroy output global: {}", + err + ); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index ac1df019..401597cc 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -4,6 +4,7 @@ pub mod env; mod ids; pub(crate) use self::ids::id_gen; pub mod geometry; +pub mod global; pub mod iced; pub mod prelude; pub mod quirks; diff --git a/src/wayland/handlers/dmabuf.rs b/src/wayland/handlers/dmabuf.rs index 4d9732bb..572fe01b 100644 --- a/src/wayland/handlers/dmabuf.rs +++ b/src/wayland/handlers/dmabuf.rs @@ -40,7 +40,7 @@ impl DmabufHandler for State { fn new_surface_feedback( &mut self, surface: &WlSurface, - global: &DmabufGlobal, + _global: &DmabufGlobal, ) -> Option { let BackendData::Kms(kms) = &self.backend else { return None; @@ -57,18 +57,7 @@ impl DmabufHandler for State { f.ended_at.is_none() && f.surface.has_surface(surface, WindowSurfaceType::all()) }); - let node = kms - .drm_devices - .values() - .find(|device| { - device - .socket - .as_ref() - .map(|s| &s.dmabuf_global == global) - .unwrap_or(false) - })? - .inner - .render_node; + let node = kms.primary_node.read().unwrap().unwrap(); let kms_surface = kms .drm_devices .values() @@ -77,9 +66,7 @@ impl DmabufHandler for State { Some(with_states(surface, |data| { if is_fullscreen { - feedback - .primary_scanout_feedback - .unwrap_or(feedback.render_feedback) + feedback.primary_scanout_feedback } else if frame_time_filter_fn(data) == Kind::ScanoutCandidate { feedback .overlay_scanout_feedback diff --git a/src/wayland/protocols/drm.rs b/src/wayland/protocols/drm.rs index c86e5ff0..d2902c98 100644 --- a/src/wayland/protocols/drm.rs +++ b/src/wayland/protocols/drm.rs @@ -38,7 +38,11 @@ use smithay::{ }; use tracing::trace; -use std::{convert::TryFrom, path::PathBuf, sync::Arc}; +use std::{ + convert::TryFrom, + path::PathBuf, + sync::{Arc, Mutex}, +}; pub enum ImportError { Failed, @@ -52,19 +56,28 @@ pub trait DrmHandler { } } -#[derive(Debug, Default)] -pub struct WlDrmState(std::marker::PhantomData); +#[derive(Debug)] +pub struct WlDrmState { + device: Arc>, + global: GlobalId, + _marker: std::marker::PhantomData, +} + +#[derive(Debug)] +pub struct DrmGlobalDeviceData { + formats: Vec, + path: PathBuf, +} /// Data associated with a drm global. pub struct DrmGlobalData { filter: Box Fn(&'a Client) -> bool + Send + Sync>, - formats: Arc>, - device_path: PathBuf, dmabuf_global: DmabufGlobal, + device: Arc>, } pub struct DrmInstanceData { - formats: Arc>, + device: Arc>, dmabuf_global: DmabufGlobal, } @@ -86,16 +99,24 @@ where data_init: &mut DataInit<'_, D>, ) { let data = DrmInstanceData { - formats: global_data.formats.clone(), + device: global_data.device.clone(), dmabuf_global: global_data.dmabuf_global, }; let drm_instance = data_init.init(resource, data); - drm_instance.device(global_data.device_path.to_string_lossy().into_owned()); + drm_instance.device( + global_data + .device + .lock() + .unwrap() + .path + .to_string_lossy() + .into_owned(), + ); if drm_instance.version() >= 2 { drm_instance.capabilities(wl_drm::Capability::Prime as u32); } - for format in global_data.formats.iter() { + for format in global_data.device.lock().unwrap().formats.iter() { if let Ok(converted) = wl_drm::Format::try_from(*format as u32) { drm_instance.format(converted as u32); } @@ -148,7 +169,7 @@ where } => { let format = match Fourcc::try_from(format) { Ok(format) => { - if !data.formats.contains(&format) { + if !data.device.lock().unwrap().formats.contains(&format) { drm.post_error( wl_drm::Error::InvalidFormat, String::from("Format not advertised by wl_drm"), @@ -220,13 +241,12 @@ where } impl WlDrmState { - pub fn create_global( - &mut self, + pub fn new( display: &DisplayHandle, device_path: PathBuf, formats: FormatSet, dmabuf_global: &DmabufGlobal, - ) -> GlobalId + ) -> Self where D: GlobalDispatch + Dispatch @@ -234,19 +254,16 @@ impl WlDrmState { + DmabufHandler + 'static, { - self.create_global_with_filter::(display, device_path, formats, dmabuf_global, |_| { - true - }) + Self::new_with_filter::(display, device_path, formats, dmabuf_global, |_| true) } - pub fn create_global_with_filter( - &mut self, + pub fn new_with_filter( display: &DisplayHandle, device_path: PathBuf, formats: FormatSet, dmabuf_global: &DmabufGlobal, client_filter: F, - ) -> GlobalId + ) -> Self where D: GlobalDispatch + Dispatch @@ -255,21 +272,45 @@ impl WlDrmState { + 'static, F: for<'a> Fn(&'a Client) -> bool + Send + Sync + 'static, { - let formats = Arc::new( - formats + let device = Arc::new(Mutex::new(DrmGlobalDeviceData { + formats: formats .into_iter() .filter(|f| f.modifier == Modifier::Invalid) .map(|f| f.code) .collect(), - ); + path: device_path, + })); + let data = DrmGlobalData { filter: Box::new(client_filter), - formats, - device_path, + device: device.clone(), dmabuf_global: *dmabuf_global, }; - display.create_global::(2, data) + let global = display.create_global::(2, data); + + WlDrmState { + device, + global, + _marker: Default::default(), + } + } + + pub fn update_device(&mut self, device_path: PathBuf, formats: FormatSet) { + let new_device = DrmGlobalDeviceData { + formats: formats + .into_iter() + .filter(|f| f.modifier == Modifier::Invalid) + .map(|f| f.code) + .collect(), + path: device_path, + }; + + *self.device.lock().unwrap() = new_device; + } + + pub fn global(&self) -> &GlobalId { + &self.global } } diff --git a/src/wayland/protocols/output_configuration/mod.rs b/src/wayland/protocols/output_configuration/mod.rs index 4bd1c0bd..d413e634 100644 --- a/src/wayland/protocols/output_configuration/mod.rs +++ b/src/wayland/protocols/output_configuration/mod.rs @@ -1,9 +1,6 @@ // SPDX-License-Identifier: GPL-3.0-only -use calloop::{ - LoopHandle, - timer::{TimeoutAction, Timer}, -}; +use calloop::LoopHandle; use cosmic_comp_config::output::comp::AdaptiveSync; use cosmic_protocols::output_management::v1::server::{ zcosmic_output_configuration_head_v1::ZcosmicOutputConfigurationHeadV1, @@ -30,7 +27,7 @@ use smithay::{ utils::{Logical, Physical, Point, Size, Transform}, wayland::output::WlOutputData, }; -use std::{convert::TryFrom, sync::Mutex, time::Duration}; +use std::{convert::TryFrom, sync::Mutex}; mod handlers; @@ -519,26 +516,6 @@ where } } -fn remove_global_with_timer( - dh: &DisplayHandle, - event_loop_handle: &LoopHandle, - id: GlobalId, -) { - dh.disable_global::(id.clone()); - let source = Timer::from_duration(Duration::from_secs(5)); - let dh = dh.clone(); - let res = event_loop_handle.insert_source(source, move |_, _, _state| { - dh.remove_global::(id.clone()); - TimeoutAction::Drop - }); - if let Err(err) = res { - tracing::error!( - "failed to insert timer source to destroy output global: {}", - err - ); - } -} - macro_rules! delegate_output_configuration { ($(@<$( $lt:tt $( : $clt:tt $(+ $dlt:tt )* )? ),+>)? $ty: ty) => { smithay::reexports::wayland_server::delegate_global_dispatch!($(@< $( $lt $( : $clt $(+ $dlt )* )? ),+ >)? $ty: [ @@ -578,4 +555,4 @@ macro_rules! delegate_output_configuration { } pub(crate) use delegate_output_configuration; -use crate::utils::prelude::OutputExt; +use crate::utils::{global::remove_global_with_timer, prelude::OutputExt}; From 789e0b363457094392697f65c40ac0047d29cc95 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 30 Jul 2026 13:03:22 +0200 Subject: [PATCH 12/24] i18n: translation updates from weblate Co-authored-by: Andi Chandler Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-comp/en_GB/ Translation: Pop OS/COSMIC Comp --- resources/i18n/en-GB/cosmic_comp.ftl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/resources/i18n/en-GB/cosmic_comp.ftl b/resources/i18n/en-GB/cosmic_comp.ftl index 0bd9c20b..853cc97d 100644 --- a/resources/i18n/en-GB/cosmic_comp.ftl +++ b/resources/i18n/en-GB/cosmic_comp.ftl @@ -18,3 +18,11 @@ window-menu-move-prev-workspace = Move to previous workspace window-menu-move-next-workspace = Move to next workspace window-menu-stack = Create window stack window-menu-unstack-all = Unstack windows +window-menu-unstack = Unstack window +window-menu-sticky = Sticky window +window-menu-close = Close +window-menu-close-all = Close all windows +window-menu-resize-edge-top = Top +window-menu-resize-edge-left = Left +window-menu-resize-edge-right = Right +window-menu-resize-edge-bottom = Bottom From 58c018dd40eb35047751dc3b68d9d31d14c03923 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 31 Jul 2026 13:42:00 -0600 Subject: [PATCH 13/24] fix(shell): take presentation feedback from fullscreen surfaces --- src/shell/mod.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 39d692e2..d9f95976 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -5024,6 +5024,20 @@ impl Shell { let mut output_presentation_feedback = OutputPresentationFeedback::new(output); if let Some(active) = self.active_space(output) { + for fs in active.get_fullscreen_surfaces() { + fs.surface.take_presentation_feedback( + &mut output_presentation_feedback, + surface_primary_scanout_output, + |surface, _| { + surface_presentation_feedback_flags_from_states( + surface, + None, + render_element_states, + ) + }, + ); + } + active.mapped().for_each(|mapped| { mapped.active_window().take_presentation_feedback( &mut output_presentation_feedback, From f89c78b9bd58e4d8d6c9097c5778659e49417d58 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 5 Aug 2026 16:02:29 +0200 Subject: [PATCH 14/24] i18n: translation updates from weblate Co-authored-by: Hosted Weblate Co-authored-by: Martin Meizoso Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-comp/gl/ Translation: Pop OS/COSMIC Comp --- resources/i18n/gl/cosmic_comp.ftl | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 resources/i18n/gl/cosmic_comp.ftl diff --git a/resources/i18n/gl/cosmic_comp.ftl b/resources/i18n/gl/cosmic_comp.ftl new file mode 100644 index 00000000..1176cad6 --- /dev/null +++ b/resources/i18n/gl/cosmic_comp.ftl @@ -0,0 +1,28 @@ +a11y-zoom-move-continuously = A vista móvese continuamente co punteiro +a11y-zoom-move-onedge = A vista móvese cando o punteiro chega ao bordo +a11y-zoom-move-centered = Mover a vista para manter o cursor centrado +a11y-zoom-settings = Configuración da lupa... +grow-window = Aumentar +shrink-window = Encoller +swap-windows = Cambiar xanelas +stack-windows = Apilar xanelas +unknown-keybinding = +window-menu-minimize = Minimizar +window-menu-maximize = Maximizar +window-menu-fullscreen = Pantalla Completa +window-menu-tiled = Xanela flotante +window-menu-screenshot = Capturar pantalla +window-menu-move = Mover +window-menu-resize = Redimensionar +window-menu-move-prev-workspace = Mover ao espazo de traballo anterior +window-menu-move-next-workspace = Mover ao seguinte espazo de traballo +window-menu-stack = Crear pila de xanelas +window-menu-unstack-all = Desapilar xanelas +window-menu-unstack = Desapilar xanela +window-menu-sticky = Xanela persistente +window-menu-close = Pechar +window-menu-close-all = Pechar todas as xanelas +window-menu-resize-edge-top = Arriba +window-menu-resize-edge-left = Esquerda +window-menu-resize-edge-right = Dereita +window-menu-resize-edge-bottom = Fondo From abfb25a672353ece2ef7d1960be93f1ff74ea195 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 14 Aug 2026 08:51:31 +0200 Subject: [PATCH 15/24] i18n: translation updates from weblate Co-authored-by: Michael Millet Co-authored-by: Steen Rabol Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-comp/da/ Translation: Pop OS/COSMIC Comp --- resources/i18n/da/cosmic_comp.ftl | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/resources/i18n/da/cosmic_comp.ftl b/resources/i18n/da/cosmic_comp.ftl index e69de29b..1cee8eac 100644 --- a/resources/i18n/da/cosmic_comp.ftl +++ b/resources/i18n/da/cosmic_comp.ftl @@ -0,0 +1,24 @@ +a11y-zoom-move-continuously = Visningen bevæger sig kontinuerligt med markøren +a11y-zoom-move-onedge = Vis bevægelser når markør rammer kant +a11y-zoom-move-centered = Vis bevægelser, for at holde markør centreret +a11y-zoom-settings = Forstørrelsesglas-indstillinger... +grow-window = Gro +shrink-window = Skrump +swap-windows = Byt Vinduer +stack-windows = Stabel Vinduer +window-menu-tiled = Gør vindue flydende +window-menu-screenshot = Tag skærmbillede +window-menu-move = Flyt +window-menu-resize = Skift størrelse +window-menu-move-prev-workspace = Flyt til forrige arbejdsområde +window-menu-move-next-workspace = Flyt til næste arbejdsområde +window-menu-stack = Opret vinduestak +window-menu-unstack-all = Udtag vinduer af stak +window-menu-unstack = Udtag vindue af stak +window-menu-sticky = Fastgjort vindue +window-menu-close = Luk +window-menu-close-all = Luk alle vinduer +window-menu-resize-edge-top = Top +window-menu-resize-edge-left = Venstre +window-menu-resize-edge-right = Højre +window-menu-resize-edge-bottom = Bund From 6af1dba69bf4a2de31d4334fd3f8c934fdc62248 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 17 Aug 2026 08:51:26 +0200 Subject: [PATCH 16/24] i18n: translation updates from weblate Co-authored-by: Michael Millet Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-comp/da/ Translation: Pop OS/COSMIC Comp --- resources/i18n/da/cosmic_comp.ftl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/resources/i18n/da/cosmic_comp.ftl b/resources/i18n/da/cosmic_comp.ftl index 1cee8eac..bd0e1edb 100644 --- a/resources/i18n/da/cosmic_comp.ftl +++ b/resources/i18n/da/cosmic_comp.ftl @@ -22,3 +22,6 @@ window-menu-resize-edge-top = Top window-menu-resize-edge-left = Venstre window-menu-resize-edge-right = Højre window-menu-resize-edge-bottom = Bund +window-menu-minimize = Minimér +window-menu-maximize = Maksimér +window-menu-fullscreen = Fuld skærm From 18ae0826150757436e0c4d37140797c3879fe873 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Tue, 4 Aug 2026 01:09:16 -0700 Subject: [PATCH 17/24] Update `smithay` with tablet API changes For now, only `WlSurface` receives tablet focus, as was previously the case. Also updates `reis` to latest bugfix release. --- Cargo.lock | 41 ++++---- Cargo.toml | 2 +- src/input/mod.rs | 135 +++++++++++++++++-------- src/wayland/handlers/tablet_manager.rs | 4 +- 4 files changed, 119 insertions(+), 63 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index aec3b990..f2364e9e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1275,7 +1275,7 @@ checksum = "80bc8c5c6c2941f70a55c15f8d9f00f9710ebda3ffda98075f996a0e6c92756f" dependencies = [ "bitflags 2.11.0", "bytemuck", - "drm-ffi 0.9.0", + "drm-ffi 0.9.1", "drm-fourcc", "libc", "rustix 0.38.44", @@ -1293,12 +1293,12 @@ dependencies = [ [[package]] name = "drm-ffi" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e41459d99a9b529845f6d2c909eb9adf3b6d2f82635ae40be8de0601726e8b" +checksum = "51a91c9b32ac4e8105dec255e849e0d66e27d7c34d184364fb93e469db08f690" dependencies = [ - "drm-sys 0.8.0", - "rustix 0.38.44", + "drm-sys 0.8.1", + "rustix 1.1.4", ] [[package]] @@ -1319,12 +1319,12 @@ dependencies = [ [[package]] name = "drm-sys" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bafb66c8dbc944d69e15cfcc661df7e703beffbaec8bd63151368b06c5f9858c" +checksum = "ecc8e1361066d91f5ffccff060a3c3be9c3ecde15be2959c1937595f7a82a9f8" dependencies = [ "libc", - "linux-raw-sys 0.6.5", + "linux-raw-sys 0.9.4", ] [[package]] @@ -1502,7 +1502,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -2689,7 +2689,7 @@ dependencies = [ "portable-atomic", "portable-atomic-util", "serde_core", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -3060,6 +3060,12 @@ version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a385b1be4e5c3e362ad2ffa73c392e53f031eaa5b7d648e64cd87f27f6063d7" +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + [[package]] name = "linux-raw-sys" version = "0.12.1" @@ -4348,9 +4354,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reis" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81f3fedd2777cde52c1be5e572efbec485eac7b801c47820eda388d4f13b9c4b" +checksum = "4b967ec6489a42067a20724f11987bb96178ea796873cbe1afd25fb93ee6f85f" dependencies = [ "calloop", "enumflags2", @@ -4518,7 +4524,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -4813,7 +4819,7 @@ checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "smithay" version = "0.7.0" -source = "git+https://github.com/smithay/smithay.git?rev=cdc03f7#cdc03f73ce57d3352c0c57a69971e3e16544051c" +source = "git+https://github.com/smithay/smithay.git?rev=5fb12b8#5fb12b87407b3680135c45d94214c5f1b1d0fbea" dependencies = [ "aliasable", "appendlist", @@ -4824,8 +4830,9 @@ dependencies = [ "cursor-icon", "downcast-rs", "drm 0.14.1", - "drm-ffi 0.9.0", + "drm-ffi 0.9.1", "drm-fourcc", + "drm-sys 0.8.1", "encoding_rs", "errno", "gbm", @@ -5107,7 +5114,7 @@ dependencies = [ "getrandom 0.4.1", "once_cell", "rustix 1.1.4", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -5406,7 +5413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c5f7c95348f20c1c913d72157b3c6dee6ea3e30b3d19502c5a7f6d3f160dacbf" dependencies = [ "cc", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fb9fa0bc..cfe97687 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -144,4 +144,4 @@ lto = "fat" cosmic-protocols = { git = "https://github.com/pop-os//cosmic-protocols", branch = "main" } [patch.crates-io] -smithay = { git = "https://github.com/smithay/smithay.git", rev = "cdc03f7" } +smithay = { git = "https://github.com/smithay/smithay.git", rev = "5fb12b8" } diff --git a/src/input/mod.rs b/src/input/mod.rs index 39bff9dc..f323cf4a 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -56,6 +56,7 @@ use smithay::{ GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, MotionEvent, PointerGrab, PointerHandle, RelativeMotionEvent, }, + tablet::{TabletDescriptor, TabletSeatTrait, tool}, touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent}, }, output::Output, @@ -70,7 +71,6 @@ use smithay::{ keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat, pointer_constraints::{PointerConstraint, with_pointer_constraint}, seat::WaylandFocus, - tablet_manager::{TabletDescriptor, TabletSeatTrait}, }, }; use tracing::{error, trace}; @@ -179,7 +179,7 @@ impl State { let led_state = seat.get_keyboard().unwrap().led_state(); seat.devices().add_device(&device, led_state); if device.has_capability(DeviceCapability::TabletTool) { - seat.tablet_seat().add_tablet::( + seat.tablet_seat().add_wp_tablet( &self.common.display_handle, &TabletDescriptor::from(&device), ); @@ -1441,37 +1441,36 @@ impl State { let tablet_seat = seat.tablet_seat(); - let tablet = tablet_seat.get_tablet(&TabletDescriptor::from(&event.device())); let tool = tablet_seat.get_tool(&event.tool()); - if let (Some(tablet), Some(tool)) = (tablet, tool) { - if event.pressure_has_changed() { - tool.pressure(event.pressure()); - } - if event.distance_has_changed() { - tool.distance(event.distance()); - } - if event.tilt_has_changed() { - tool.tilt(event.tilt()); - } - if event.slider_has_changed() { - tool.slider_position(event.slider_position()); - } - if event.rotation_has_changed() { - tool.rotation(event.rotation()); - } - if event.wheel_has_changed() { - tool.wheel(event.wheel_delta(), event.wheel_delta_discrete()); - } + if let Some(tool) = tool { + let frame = tool::AxisFrame { + pressure: event.pressure_has_changed().then(|| event.pressure()), + distance: event.distance_has_changed().then(|| event.distance()), + + tilt: event.tilt_has_changed().then(|| event.tilt()), + rotation: event.rotation_has_changed().then(|| event.rotation()), + + slider: event.slider_has_changed().then(|| event.slider_position()), + wheel: event + .wheel_has_changed() + .then(|| (event.wheel_delta(), event.wheel_delta_discrete())), + }; + + tool.axis(self, frame); tool.motion( - position.as_logical(), + self, under .and_then(|(f, loc)| f.wl_surface().map(|s| (s.into_owned(), loc))), - &tablet, - SERIAL_COUNTER.next_serial(), - event.time_msec(), + &tool::MotionEvent { + location: position.as_logical(), + serial: SERIAL_COUNTER.next_serial(), + time: event.time_msec(), + }, ); + + tool.frame(self, event.time_msec()); } } } @@ -1509,25 +1508,53 @@ impl State { let tablet = tablet_seat.get_tablet(&TabletDescriptor::from(&event.device())); let dh = self.common.display_handle.clone(); - let tool = tablet_seat.add_tool::(self, &dh, &event.tool()); + let tool = tablet_seat + .get_tool(&event.tool()) + .unwrap_or_else(|| tablet_seat.add_wp_tool(self, &dh, &event.tool())); if let Some(tablet) = tablet { + let serial = SERIAL_COUNTER.next_serial(); + + let frame = tool::AxisFrame { + pressure: event.pressure_has_changed().then(|| event.pressure()), + distance: event.distance_has_changed().then(|| event.distance()), + + tilt: event.tilt_has_changed().then(|| event.tilt()), + rotation: event.rotation_has_changed().then(|| event.rotation()), + + slider: event.slider_has_changed().then(|| event.slider_position()), + wheel: event + .wheel_has_changed() + .then(|| (event.wheel_delta(), event.wheel_delta_discrete())), + }; + match event.state() { ProximityState::In => { - if let Some(under) = under.and_then(|(f, loc)| { + let under = under.and_then(|(f, loc)| { f.wl_surface().map(|s| (s.into_owned(), loc)) - }) { - tool.proximity_in( - position.as_logical(), - under, - &tablet, - SERIAL_COUNTER.next_serial(), - event.time_msec(), - ) - } + }); + tool.proximity_in( + self, + under, + tablet, + &tool::ProximityInEvent { + location: position.as_logical(), + axis: Some(frame), + serial: SERIAL_COUNTER.next_serial(), + time: event.time_msec(), + }, + ) } - ProximityState::Out => tool.proximity_out(event.time_msec()), + ProximityState::Out => tool.proximity_out( + self, + &tool::ProximityOutEvent { + serial, + time: event.time_msec(), + }, + ), } + + tool.frame(self, event.time_msec()); } } } @@ -1543,14 +1570,29 @@ impl State { self.common.idle_notifier_state.notify_activity(&seat); notify_cursor_activity(self, &seat); if let Some(tool) = seat.tablet_seat().get_tool(&event.tool()) { + let serial = SERIAL_COUNTER.next_serial(); match event.tip_state() { TabletToolTipState::Down => { - tool.tip_down(SERIAL_COUNTER.next_serial(), event.time_msec()); + tool.down( + self, + &tool::DownEvent { + serial, + time: event.time_msec(), + }, + ); } TabletToolTipState::Up => { - tool.tip_up(event.time_msec()); + tool.up( + self, + &tool::UpEvent { + serial, + time: event.time_msec(), + }, + ); } } + + tool.frame(self, event.time_msec()); } } } @@ -1567,11 +1609,16 @@ impl State { notify_cursor_activity(self, &seat); if let Some(tool) = seat.tablet_seat().get_tool(&event.tool()) { tool.button( - event.button(), - event.button_state(), - SERIAL_COUNTER.next_serial(), - event.time_msec(), + self, + &tool::ButtonEvent { + button: event.button(), + state: event.button_state(), + serial: SERIAL_COUNTER.next_serial(), + time: event.time_msec(), + }, ); + + tool.frame(self, event.time_msec()); } } } diff --git a/src/wayland/handlers/tablet_manager.rs b/src/wayland/handlers/tablet_manager.rs index 60469ba9..e4b84434 100644 --- a/src/wayland/handlers/tablet_manager.rs +++ b/src/wayland/handlers/tablet_manager.rs @@ -3,10 +3,12 @@ use crate::state::State; use smithay::{ backend::input::TabletToolDescriptor, input::pointer::CursorImageStatus, - wayland::tablet_manager::TabletSeatHandler, + input::tablet::TabletSeatHandler, reexports::wayland_server::protocol::wl_surface, }; impl TabletSeatHandler for State { + type ToolFocus = wl_surface::WlSurface; + fn tablet_tool_image(&mut self, _tool: &TabletToolDescriptor, _image: CursorImageStatus) { // TODO display cursor for each tablet tool } From 861b7326715ff529dc26de44704051c61496f87a Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 19 Jun 2026 18:38:58 -0700 Subject: [PATCH 18/24] input: Handle scroll events that only have `amount_v120` X11, winit, and libei backends all can produce scroll events with `amount_v120()` but no `amount()`. --- src/input/mod.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/input/mod.rs b/src/input/mod.rs index f323cf4a..d7dbeb20 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -968,7 +968,10 @@ impl State { } } else { let mut frame = AxisFrame::new(event.time_msec()).source(event.source()); - if let Some(horizontal_amount) = event.amount(Axis::Horizontal) { + let horizontal_amount = event + .amount(Axis::Horizontal) + .or_else(|| Some(event.amount_v120(Axis::Horizontal)? * 15.0 / 120.)); + if let Some(horizontal_amount) = horizontal_amount { if horizontal_amount != 0.0 { frame = frame .relative_direction( @@ -986,7 +989,10 @@ impl State { frame = frame.stop(Axis::Horizontal); } } - if let Some(vertical_amount) = event.amount(Axis::Vertical) { + let vertical_amount = event + .amount(Axis::Vertical) + .or_else(|| Some(event.amount_v120(Axis::Vertical)? * 15.0 / 120.)); + if let Some(vertical_amount) = vertical_amount { if vertical_amount != 0.0 { frame = frame .relative_direction( From e939a72b30078990ea7b4a91d6243ef98130c555 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Wed, 8 May 2024 21:16:10 -0700 Subject: [PATCH 19/24] Initial Ei protocol support --- Cargo.toml | 1 + src/lib.rs | 3 +++ src/libei.rs | 46 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 src/libei.rs diff --git a/Cargo.toml b/Cargo.toml index cfe97687..7d4ff3af 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -97,6 +97,7 @@ features = [ "backend_drm", "backend_gbm", "backend_egl", + "backend_libei", "backend_libinput", "backend_session_libseat", "backend_udev", diff --git a/src/lib.rs b/src/lib.rs index f7282942..d0bf28b7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -43,6 +43,7 @@ pub mod dbus; pub mod debug; pub mod hooks; pub mod input; +pub mod libei; mod logger; pub mod session; pub mod shell; @@ -179,6 +180,8 @@ pub fn run(hooks: crate::hooks::Hooks) -> Result<(), Box> { // 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 new file mode 100644 index 00000000..c0c1ed3e --- /dev/null +++ b/src/libei.rs @@ -0,0 +1,46 @@ +use reis::calloop::EisListenerSource; +use reis::eis; +use smithay::reexports::reis; + +use smithay::backend::libei::{EiInput, EiInputEvent}; +use smithay::input::keyboard::XkbConfig; +use smithay::reexports::calloop; + +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; + } + }; + + unsafe { std::env::set_var("LIBEI_SOCKET", listener.path()) }; + + let listener_source = EisListenerSource::new(listener); + let handle_clone = handle.clone(); + handle + .insert_source(listener_source, move |context, _, _| { + let source = EiInput::new(context); + handle_clone + .insert_source(source, |event, connection, data| match event { + EiInputEvent::Connected => { + 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"); + } + EiInputEvent::Disconnected => {} + EiInputEvent::Event(event) => { + data.process_input_event(event); + } + }) + .unwrap(); + Ok(calloop::PostAction::Continue) + }) + .unwrap(); +} From f4ee52c46a7eae97042dc96d18419bf7796356bd Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 5 Jun 2026 10:42:19 -0600 Subject: [PATCH 20/24] 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 --- src/backend/kms/mod.rs | 2 +- src/backend/winit.rs | 8 +- src/backend/x11.rs | 4 +- src/config/mod.rs | 22 + src/dbus/ei.rs | 79 ++ src/dbus/mod.rs | 9 + src/input/actions.rs | 23 +- src/input/mod.rs | 796 +++++++++++++++---- src/lib.rs | 6 +- src/libei.rs | 228 +++++- src/shell/layout/tiling/grabs/swap.rs | 2 + src/shell/seats.rs | 92 ++- src/state.rs | 25 + src/wayland/handlers/output_configuration.rs | 10 + src/xwayland.rs | 4 +- 15 files changed, 1092 insertions(+), 218 deletions(-) create mode 100644 src/dbus/ei.rs 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(); From c423e8282029bd2769d5a93a738b3e2092d5095a Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 19 Jun 2026 18:38:26 -0700 Subject: [PATCH 21/24] libei: on KMS, schedule render after handling input events Matches behavior of libinput backend. --- src/libei.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libei.rs b/src/libei.rs index 1d748741..ad3a674d 100644 --- a/src/libei.rs +++ b/src/libei.rs @@ -11,7 +11,8 @@ use smithay::wayland::text_input::TextInputSeat; use crate::config::xkb_config_to_wl; use crate::input::InputBackendId; -use crate::state::State; +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; @@ -139,6 +140,11 @@ pub fn setup_ei( 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); + } + } } } } From 861a460e6c66b26bc910e6a96afd96b8e45d9861 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Wed, 5 Aug 2026 01:47:07 -0700 Subject: [PATCH 22/24] chore: Fix `clippy` lints --- src/input/mod.rs | 2 +- src/wayland/handlers/xdg_shell/mod.rs | 20 +++++++++----------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/input/mod.rs b/src/input/mod.rs index c93b3405..67780d53 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -3054,7 +3054,7 @@ pub fn update_output_image_copy_cursor_position( position: Point, ) { let output_geometry = output.geometry(); - for session in cursor_sessions_for_output(&shell, &output) { + for session in cursor_sessions_for_output(shell, output) { if let Some(cursor_geometry) = seat.cursor_geometry( (position - output_geometry.loc.to_f64()) .as_logical() diff --git a/src/wayland/handlers/xdg_shell/mod.rs b/src/wayland/handlers/xdg_shell/mod.rs index b8288019..b4f16c4c 100644 --- a/src/wayland/handlers/xdg_shell/mod.rs +++ b/src/wayland/handlers/xdg_shell/mod.rs @@ -333,17 +333,15 @@ impl XdgShellHandler for State { if let Some(ref grab) = grab { if grab.has_ended() { should_ungrab = true; - } else if let Some(target) = grab.current_grab() { - if let Some(wl_surface) = target.wl_surface() { - if wl_surface.as_ref() == surface.wl_surface() - || smithay::desktop::PopupManager::popups_for_surface( - surface.wl_surface(), - ) - .any(|(p, _)| p.wl_surface() == wl_surface.as_ref()) - { - should_ungrab = true; - } - } + } else if let Some(target) = grab.current_grab() + && let Some(wl_surface) = target.wl_surface() + && (wl_surface.as_ref() == surface.wl_surface() + || smithay::desktop::PopupManager::popups_for_surface( + surface.wl_surface(), + ) + .any(|(p, _)| p.wl_surface() == wl_surface.as_ref())) + { + should_ungrab = true; } } if should_ungrab { From 8806436f81a82c38e7d18e1ec2ff1edc201faacc Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 17 Aug 2026 09:35:31 -0700 Subject: [PATCH 23/24] libei: Add `com.system76.CosmicOSK` to DBus whitelist --- src/dbus/ei.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/dbus/ei.rs b/src/dbus/ei.rs index 6391dc76..263bced4 100644 --- a/src/dbus/ei.rs +++ b/src/dbus/ei.rs @@ -8,9 +8,10 @@ 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", -)]; +static ALLOWED_NAMES: &[WellKnownName] = &[ + WellKnownName::from_static_str_unchecked("org.freedesktop.impl.portal.desktop.cosmic"), + WellKnownName::from_static_str_unchecked("com.system76.CosmicOSK"), +]; /// Channel for handing the EI socketpair (and requested device types) /// It's `None` until the EI sender side has been set up From f1d1c0025769b48507b3991a2f7dc0ccbba7ec94 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 18 Aug 2026 11:09:23 +0200 Subject: [PATCH 24/24] i18n: translation updates from weblate Co-authored-by: Michael Millet Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-comp/da/ Translation: Pop OS/COSMIC Comp --- resources/i18n/da/cosmic_comp.ftl | 1 + 1 file changed, 1 insertion(+) diff --git a/resources/i18n/da/cosmic_comp.ftl b/resources/i18n/da/cosmic_comp.ftl index bd0e1edb..ad4befea 100644 --- a/resources/i18n/da/cosmic_comp.ftl +++ b/resources/i18n/da/cosmic_comp.ftl @@ -25,3 +25,4 @@ window-menu-resize-edge-bottom = Bund window-menu-minimize = Minimér window-menu-maximize = Maksimér window-menu-fullscreen = Fuld skærm +unknown-keybinding =