From a5bdcd0972c664065b4609eb74b2873894f073d9 Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Wed, 11 Feb 2026 17:41:15 +0100 Subject: [PATCH] implement xdg-foreign to put capture dialog on top --- Cargo.lock | 46 +++++++++++++++++++++++++++++-------- input-capture/src/lib.rs | 35 ++++++++++++++++++++++++---- input-capture/src/libei.rs | 29 +++++++++++++++++------ lan-mouse-gtk/Cargo.toml | 2 ++ lan-mouse-gtk/src/lib.rs | 25 +++++++++++++++++++- lan-mouse-gtk/src/window.rs | 2 +- lan-mouse-ipc/src/lib.rs | 8 +++++++ src/capture.rs | 32 +++++++++++++------------- src/capture_test.rs | 4 +++- src/service.rs | 26 +++++++++++++++++++-- 10 files changed, 166 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91cb6b7..68fd5d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1121,6 +1121,30 @@ dependencies = [ "system-deps", ] +[[package]] +name = "gdk4-wayland" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd34518488cd624a85e75e82540bc24c72cfeb0aea6bad7faed683ca3977dba0" +dependencies = [ + "gdk4", + "gdk4-wayland-sys", + "gio", + "glib", + "libc", +] + +[[package]] +name = "gdk4-wayland-sys" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7a0f2332c531d62ee3f14f5e839ac1abac59e9b052adf1495124c00d89a34b" +dependencies = [ + "glib-sys", + "libc", + "system-deps", +] + [[package]] name = "generator" version = "0.8.5" @@ -1884,6 +1908,7 @@ name = "lan-mouse-gtk" version = "0.2.0" dependencies = [ "async-channel", + "gdk4-wayland", "glib-build-tools", "gtk4", "hostname", @@ -1891,6 +1916,7 @@ dependencies = [ "libadwaita", "log", "thiserror 2.0.12", + "wayland-client", ] [[package]] @@ -2514,9 +2540,9 @@ dependencies = [ [[package]] name = "quick-xml" -version = "0.37.5" +version = "0.38.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb" +checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c" dependencies = [ "memchr", ] @@ -3573,9 +3599,9 @@ dependencies = [ [[package]] name = "wayland-backend" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "673a33c33048a5ade91a6b139580fa174e19fb0d23f396dca9fa15f2e1e49b35" +checksum = "fee64194ccd96bf648f42a65a7e589547096dfa702f7cadef84347b66ad164f9" dependencies = [ "cc", "downcast-rs", @@ -3586,9 +3612,9 @@ dependencies = [ [[package]] name = "wayland-client" -version = "0.31.11" +version = "0.31.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" +checksum = "b8e6faa537fbb6c186cb9f1d41f2f811a4120d1b57ec61f50da451a0c5122bec" dependencies = [ "bitflags 2.9.1", "rustix 1.1.3", @@ -3636,9 +3662,9 @@ dependencies = [ [[package]] name = "wayland-scanner" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54cb1e9dc49da91950bdfd8b848c49330536d9d1fb03d4bfec8cae50caa50ae3" +checksum = "5423e94b6a63e68e439803a3e153a9252d5ead12fd853334e2ad33997e3889e3" dependencies = [ "proc-macro2", "quick-xml", @@ -3647,9 +3673,9 @@ dependencies = [ [[package]] name = "wayland-sys" -version = "0.31.7" +version = "0.31.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34949b42822155826b41db8e5d0c1be3a2bd296c747577a43a3e6daefc296142" +checksum = "1e6dbfc3ac5ef974c92a2235805cc0114033018ae1290a72e474aa8b28cbbdfd" dependencies = [ "pkg-config", ] diff --git a/input-capture/src/lib.rs b/input-capture/src/lib.rs index 4767503..ec9300e 100644 --- a/input-capture/src/lib.rs +++ b/input-capture/src/lib.rs @@ -2,6 +2,7 @@ use std::{ collections::{HashMap, HashSet, VecDeque}, fmt::Display, mem::swap, + sync::{Arc, Mutex}, task::{Poll, ready}, }; @@ -129,6 +130,23 @@ pub struct InputCapture { pending: VecDeque<(CaptureHandle, CaptureEvent)>, } +#[derive(Clone, Debug)] +pub enum WindowIdentifier { + Wayland(String), + X11(u32), +} + +impl Into for WindowIdentifier { + fn into(self) -> ashpd::WindowIdentifier { + match self { + WindowIdentifier::Wayland(handle) => { + ashpd::WindowIdentifier::from_xdg_foreign_exported_v2(handle) + } + WindowIdentifier::X11(_) => todo!(), + } + } +} + impl InputCapture { /// create a new client with the given id pub async fn create(&mut self, id: CaptureHandle, pos: Position) -> Result<(), CaptureError> { @@ -177,8 +195,11 @@ impl InputCapture { } /// creates a new [`InputCapture`] - pub async fn new(backend: Option) -> Result { - let capture = create(backend).await?; + pub async fn new( + backend: Option, + window_identifier: Arc>>, + ) -> Result { + let capture = create(backend, window_identifier).await?; Ok(Self { capture, id_map: Default::default(), @@ -280,13 +301,16 @@ trait Capture: Stream> + U async fn create_backend( backend: Backend, + window_identifier: Arc>>, ) -> Result< Box>>, CaptureCreationError, > { match backend { #[cfg(all(unix, feature = "libei", not(target_os = "macos")))] - Backend::InputCapturePortal => Ok(Box::new(libei::LibeiInputCapture::new().await?)), + Backend::InputCapturePortal => Ok(Box::new( + libei::LibeiInputCapture::new(window_identifier).await?, + )), #[cfg(all(unix, feature = "layer_shell", not(target_os = "macos")))] Backend::LayerShell => Ok(Box::new(layer_shell::LayerShellInputCapture::new()?)), #[cfg(all(unix, feature = "x11", not(target_os = "macos")))] @@ -301,12 +325,13 @@ async fn create_backend( async fn create( backend: Option, + window_identifier: Arc>>, ) -> Result< Box>>, CaptureCreationError, > { if let Some(backend) = backend { - let b = create_backend(backend).await; + let b = create_backend(backend, window_identifier).await; if b.is_ok() { log::info!("using capture backend: {backend}"); } @@ -325,7 +350,7 @@ async fn create( #[cfg(target_os = "macos")] Backend::MacOs, ] { - match create_backend(backend).await { + match create_backend(backend, window_identifier.clone()).await { Ok(b) => { log::info!("using capture backend: {backend}"); return Ok(b); diff --git a/input-capture/src/libei.rs b/input-capture/src/libei.rs index 3609f88..8894392 100644 --- a/input-capture/src/libei.rs +++ b/input-capture/src/libei.rs @@ -23,7 +23,7 @@ use std::{ os::unix::net::UnixStream, pin::Pin, rc::Rc, - sync::Arc, + sync::{Arc, Mutex}, task::{Context, Poll}, }; use tokio::{ @@ -39,7 +39,7 @@ use futures_core::Stream; use input_event::Event; -use crate::CaptureEvent; +use crate::{CaptureEvent, WindowIdentifier}; use super::{ Capture as LanMouseInputCapture, Position, @@ -153,11 +153,15 @@ async fn update_barriers( async fn create_session( input_capture: &InputCapture, + window_identifier: Arc>>, ) -> std::result::Result<(Session, BitFlags), ashpd::Error> { - log::debug!("creating input capture session"); + let window_identifier = window_identifier.lock().unwrap().clone(); + log::debug!("creating input capture session: {window_identifier:?}"); + let ashpd_window_identifier: Option = + window_identifier.map(|i| i.into()); input_capture .create_session( - None, + ashpd_window_identifier.as_ref(), Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen, ) .await @@ -202,10 +206,15 @@ async fn libei_event_handler( } impl LibeiInputCapture { - pub async fn new() -> std::result::Result { + /// creates a new libei input capture + /// `window_id` is a window identifier for user prompts + pub async fn new( + window_identifier: Arc>>, + ) -> std::result::Result { let input_capture = Box::pin(InputCapture::new().await?); let input_capture_ptr = input_capture.as_ref().get_ref() as *const InputCapture; - let first_session = Some(create_session(unsafe { &*input_capture_ptr }).await?); + let first_session = + Some(create_session(unsafe { &*input_capture_ptr }, window_identifier.clone()).await?); let (event_tx, event_rx) = mpsc::channel(1); let (notify_capture, notify_rx) = mpsc::channel(1); @@ -220,6 +229,7 @@ impl LibeiInputCapture { first_session, event_tx, cancellation_token.clone(), + window_identifier, ); let capture_task = tokio::task::spawn_local(capture); @@ -244,6 +254,7 @@ async fn do_capture( session: Option<(Session, BitFlags)>, event_tx: Sender<(Position, CaptureEvent)>, cancellation_token: CancellationToken, + window_identifier: Arc>>, ) -> Result<(), CaptureError> { let mut session = session.map(|s| s.0); @@ -289,7 +300,11 @@ async fn do_capture( // create session let mut session = match session.take() { Some(s) => s, - None => create_session(input_capture).await?.0, + None => { + create_session(input_capture, window_identifier.clone()) + .await? + .0 + } }; let capture_session = do_capture_session( diff --git a/lan-mouse-gtk/Cargo.toml b/lan-mouse-gtk/Cargo.toml index 4a1a202..81aa4c1 100644 --- a/lan-mouse-gtk/Cargo.toml +++ b/lan-mouse-gtk/Cargo.toml @@ -8,12 +8,14 @@ repository = "https://github.com/feschber/lan-mouse" [dependencies] gtk = { package = "gtk4", version = "0.9.0", features = ["v4_2"] } +gdk4_wayland = { package = "gdk4-wayland", version="0.9.6" } adw = { package = "libadwaita", version = "0.7.0", features = ["v1_1"] } async-channel = { version = "2.1.1" } hostname = "0.4.0" log = "0.4.20" lan-mouse-ipc = { path = "../lan-mouse-ipc", version = "0.2.0" } thiserror = "2.0.0" +wayland-client = "0.31.12" [build-dependencies] glib-build-tools = { version = "0.20.0" } diff --git a/lan-mouse-gtk/src/lib.rs b/lan-mouse-gtk/src/lib.rs index 9908e05..d11dd9b 100644 --- a/lan-mouse-gtk/src/lib.rs +++ b/lan-mouse-gtk/src/lib.rs @@ -10,7 +10,7 @@ use std::{env, process, str}; use window::Window; -use lan_mouse_ipc::FrontendEvent; +use lan_mouse_ipc::{FrontendEvent, FrontendRequest, WindowIdentifier}; use adw::Application; use gtk::{IconTheme, gdk::Display, glib::clone, prelude::*}; @@ -19,6 +19,8 @@ use gtk::{gio, glib, prelude::ApplicationExt}; use self::client_object::ClientObject; use self::key_object::KeyObject; +use gdk4_wayland::WaylandToplevel; + use thiserror::Error; #[derive(Error, Debug)] @@ -124,6 +126,27 @@ fn build_ui(app: &Application) { let window = Window::new(app, frontend_tx); + // export TopLevel handle and send it to the service so that it can put the InpuCapture / RemoteDesktop + // windows on top of it using xdg-foreign. + window.connect_show(|window| { + // needs the surface so we have to present first! + if let Some(surface) = window.surface() { + if surface.display().backend().is_wayland() { + // let surface = surface.downcast::(); + let toplevel = surface.downcast::().expect("xdg-toplevel"); + let window = window.clone(); + toplevel.export_handle(move |_toplevel, handle| { + if let Ok(handle) = handle { + let handle = handle.to_string(); + window.request(FrontendRequest::WindowIdentifier( + WindowIdentifier::Wayland(handle), + )); + } + }); + } + } + }); + glib::spawn_future_local(clone!( #[weak] window, diff --git a/lan-mouse-gtk/src/window.rs b/lan-mouse-gtk/src/window.rs index 47db926..7a12fe0 100644 --- a/lan-mouse-gtk/src/window.rs +++ b/lan-mouse-gtk/src/window.rs @@ -422,7 +422,7 @@ impl Window { self.request(FrontendRequest::RemoveAuthorizedKey(fp)); } - fn request(&self, request: FrontendRequest) { + pub(crate) fn request(&self, request: FrontendRequest) { let mut requester = self.imp().frontend_request_writer.borrow_mut(); let requester = requester.as_mut().unwrap(); if let Err(e) = requester.request(request) { diff --git a/lan-mouse-ipc/src/lib.rs b/lan-mouse-ipc/src/lib.rs index a2c7112..9e65d54 100644 --- a/lan-mouse-ipc/src/lib.rs +++ b/lan-mouse-ipc/src/lib.rs @@ -255,6 +255,14 @@ pub enum FrontendRequest { UpdateEnterHook(u64, Option), /// save config file SaveConfiguration, + /// window identifier used to present input-capture / remote-desktop prompts + WindowIdentifier(WindowIdentifier), +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub enum WindowIdentifier { + Wayland(String), + X11(u32), } #[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)] diff --git a/src/capture.rs b/src/capture.rs index e4d9c8a..d24b508 100644 --- a/src/capture.rs +++ b/src/capture.rs @@ -1,12 +1,14 @@ use std::{ cell::{Cell, RefCell}, rc::Rc, + sync::{Arc, Mutex}, time::{Duration, Instant}, }; use futures::StreamExt; use input_capture::{ CaptureError, CaptureEvent, CaptureHandle, InputCapture, InputCaptureError, Position, + WindowIdentifier, }; use input_event::scancode; use lan_mouse_proto::ProtoEvent; @@ -49,7 +51,7 @@ pub(crate) enum CaptureType { EnterOnly, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Debug)] enum CaptureRequest { /// capture must release the mouse Release, @@ -66,6 +68,7 @@ impl Capture { backend: Option, conn: LanMouseConnection, release_bind: Vec, + window_identifier: Arc>>, ) -> Self { let (request_tx, request_rx) = channel(); let (event_tx, event_rx) = channel(); @@ -80,6 +83,7 @@ impl Capture { request_rx, release_bind: Rc::new(RefCell::new(release_bind)), state: Default::default(), + window_identifier, }; let task = spawn_local(capture_task.run()); Self { @@ -160,6 +164,7 @@ struct CaptureTask { release_bind: Rc>>, request_rx: Receiver, state: State, + window_identifier: Arc>>, } impl CaptureTask { @@ -194,6 +199,7 @@ impl CaptureTask { } async fn run(mut self) { + tokio::time::sleep(Duration::from_secs(1)).await; loop { if let Err(e) = self.do_capture().await { log::warn!("input capture exited: {e}"); @@ -201,10 +207,10 @@ impl CaptureTask { loop { tokio::select! { r = self.request_rx.recv() => match r.expect("channel closed") { - CaptureRequest::Reenable => break, - CaptureRequest::Create(h, p, t) => self.add_capture(h, p, t), - CaptureRequest::Destroy(h) => self.remove_capture(h), - CaptureRequest::Release => { /* nothing to do */ } + CaptureRequest::Reenable=>break, + CaptureRequest::Create(h,p,t)=>self.add_capture(h,p,t), + CaptureRequest::Destroy(h)=>self.remove_capture(h), + CaptureRequest::Release=>{} }, _ = self.cancellation_token.cancelled() => return, } @@ -215,7 +221,7 @@ impl CaptureTask { async fn do_capture(&mut self) -> Result<(), InputCaptureError> { /* allow cancelling capture request */ let mut capture = tokio::select! { - r = InputCapture::new(self.backend) => r?, + r = InputCapture::new(self.backend, self.window_identifier.clone()) => r?, _ = self.cancellation_token.cancelled() => return Ok(()), }; @@ -285,16 +291,10 @@ impl CaptureTask { } }, e = self.request_rx.recv() => match e.expect("channel closed") { - CaptureRequest::Reenable => { /* already active */ }, - CaptureRequest::Release => self.release_capture(capture).await?, - CaptureRequest::Create(h, p, t) => { - self.add_capture(h, p, t); - capture.create(h, p).await?; - } - CaptureRequest::Destroy(h) => { - self.remove_capture(h); - capture.destroy(h).await?; - } + CaptureRequest::Reenable=>{}, + CaptureRequest::Release=>self.release_capture(capture).await?, + CaptureRequest::Create(h,p,t)=>{self.add_capture(h,p,t);capture.create(h,p).await?;} + CaptureRequest::Destroy(h)=>{self.remove_capture(h);capture.destroy(h).await?;} }, _ = self.cancellation_token.cancelled() => break, } diff --git a/src/capture_test.rs b/src/capture_test.rs index 5af2218..5c82c8f 100644 --- a/src/capture_test.rs +++ b/src/capture_test.rs @@ -1,3 +1,5 @@ +use std::sync::{Arc, Mutex}; + use crate::config::Config; use clap::Args; use futures::StreamExt; @@ -12,7 +14,7 @@ pub async fn run(config: Config, _args: TestCaptureArgs) -> Result<(), InputCapt log::info!("creating input capture"); let backend = config.capture_backend().map(|b| b.into()); loop { - let mut input_capture = InputCapture::new(backend).await?; + let mut input_capture = InputCapture::new(backend, Arc::new(Mutex::new(None))).await?; log::info!("creating clients"); input_capture.create(0, Position::Left).await?; input_capture.create(4, Position::Left).await?; diff --git a/src/service.rs b/src/service.rs index ff7c22f..e9179ae 100644 --- a/src/service.rs +++ b/src/service.rs @@ -19,7 +19,7 @@ use std::{ collections::{HashMap, HashSet, VecDeque}, io, net::{IpAddr, SocketAddr}, - sync::{Arc, RwLock}, + sync::{Arc, Mutex, RwLock}, }; use thiserror::Error; use tokio::{process::Command, signal, sync::Notify}; @@ -70,6 +70,7 @@ pub struct Service { /// map from capture handle to connection info incoming_conn_info: HashMap, next_trigger_handle: u64, + window_identifier: Arc>>, } #[derive(Debug)] @@ -115,7 +116,13 @@ impl Service { // input capture + emulation let capture_backend = config.capture_backend().map(|b| b.into()); - let capture = Capture::new(capture_backend, conn, config.release_bind()); + let window_identifier = Arc::new(Mutex::new(None)); + let capture = Capture::new( + capture_backend, + conn, + config.release_bind(), + window_identifier.clone(), + ); let emulation_backend = config.emulation_backend().map(|b| b.into()); let emulation = Emulation::new(emulation_backend, listener); @@ -140,6 +147,7 @@ impl Service { incoming_conn_info: Default::default(), incoming_conns: Default::default(), next_trigger_handle: 0, + window_identifier, }; Ok(service) } @@ -231,6 +239,20 @@ impl Service { self.update_enter_hook(handle, enter_hook) } FrontendRequest::SaveConfiguration => self.save_config(), + FrontendRequest::WindowIdentifier(handle) => { + log::info!("xdg-foreign handle: {handle:?}"); + self.window_identifier + .lock() + .unwrap() + .replace(match handle { + lan_mouse_ipc::WindowIdentifier::Wayland(handle) => { + input_capture::WindowIdentifier::Wayland(handle) + } + lan_mouse_ipc::WindowIdentifier::X11(xid) => { + input_capture::WindowIdentifier::X11(xid) + } + }); + } } }