implement xdg-foreign to put capture dialog on top

This commit is contained in:
Ferdinand Schober 2026-02-11 17:41:15 +01:00 committed by Ferdinand Schober
parent 3e7b04c184
commit 304d8a193f
10 changed files with 147 additions and 18 deletions

25
Cargo.lock generated
View file

@ -1087,6 +1087,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 = "generic-array"
version = "0.14.7"
@ -1889,6 +1913,7 @@ name = "lan-mouse-gtk"
version = "0.2.0"
dependencies = [
"async-channel",
"gdk4-wayland",
"glib-build-tools",
"gtk4",
"hostname",

View file

@ -2,6 +2,7 @@ use std::{
collections::{HashMap, HashSet, VecDeque},
fmt::Display,
mem::swap,
sync::{Arc, Mutex},
task::{Poll, ready},
};
@ -129,6 +130,24 @@ pub struct InputCapture {
pending: VecDeque<(CaptureHandle, CaptureEvent)>,
}
#[derive(Clone, Debug)]
pub enum WindowIdentifier {
Wayland(String),
X11(u32),
}
#[cfg(all(unix, feature = "libei"))]
impl Into<ashpd::WindowIdentifier> for WindowIdentifier {
fn into(self) -> ashpd::WindowIdentifier {
match self {
WindowIdentifier::Wayland(handle) => {
ashpd::WindowIdentifier::from_xdg_foreign_exported(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> {
@ -190,8 +209,11 @@ impl InputCapture {
}
/// creates a new [`InputCapture`]
pub async fn new(backend: Option<Backend>) -> Result<Self, CaptureCreationError> {
let capture = create(backend).await?;
pub async fn new(
backend: Option<Backend>,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> Result<Self, CaptureCreationError> {
let capture = create(backend, window_identifier).await?;
Ok(Self {
capture,
id_map: Default::default(),
@ -293,13 +315,16 @@ trait Capture: Stream<Item = Result<(Position, CaptureEvent), CaptureError>> + U
async fn create_backend(
backend: Backend,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> Result<
Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
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")))]
@ -314,12 +339,13 @@ async fn create_backend(
async fn create(
backend: Option<Backend>,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> Result<
Box<dyn Capture<Item = Result<(Position, CaptureEvent), CaptureError>>>,
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}");
}
@ -338,7 +364,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);

View file

@ -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,
@ -161,13 +161,17 @@ async fn update_barriers(
async fn create_session(
input_capture: &InputCapture,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> std::result::Result<(Session<InputCapture>, BitFlags<Capabilities>), ashpd::Error> {
log::debug!("creating input capture session");
log::debug!("creating input capture session: {window_identifier:?}");
let window_identifier = window_identifier.lock().unwrap().clone();
let create_session_options = CreateSessionOptions::default().set_capabilities(
Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen,
);
let ashpd_window_identifier: Option<ashpd::WindowIdentifier> =
window_identifier.map(|i| i.into());
input_capture
.create_session(None, create_session_options)
.create_session(ashpd_window_identifier.as_ref(), create_session_options)
.await
}
@ -212,10 +216,15 @@ async fn libei_event_handler(
}
impl LibeiInputCapture {
pub async fn new() -> std::result::Result<Self, LibeiCaptureCreationError> {
/// creates a new libei input capture
/// `window_id` is a window identifier for user prompts
pub async fn new(
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> std::result::Result<Self, LibeiCaptureCreationError> {
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);
@ -230,6 +239,7 @@ impl LibeiInputCapture {
first_session,
event_tx,
cancellation_token.clone(),
window_identifier,
);
let capture_task = tokio::task::spawn_local(capture);
@ -254,6 +264,7 @@ async fn do_capture(
session: Option<(Session<InputCapture>, BitFlags<Capabilities>)>,
event_tx: Sender<(Position, CaptureEvent)>,
cancellation_token: CancellationToken,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> Result<(), CaptureError> {
let mut session = session.map(|s| s.0);
@ -299,7 +310,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(

View file

@ -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" }

View file

@ -14,7 +14,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::*};
@ -23,6 +23,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)]
@ -227,6 +229,27 @@ fn build_ui(app: &Application) {
});
}
// 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::<WaylandSurface>();
let toplevel = surface.downcast::<WaylandToplevel>().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,

View file

@ -433,7 +433,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) {

View file

@ -255,6 +255,14 @@ pub enum FrontendRequest {
UpdateEnterHook(u64, Option<String>),
/// 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)]

View file

@ -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::{Event, KeyboardEvent, scancode};
use lan_mouse_proto::ProtoEvent;
@ -68,6 +70,7 @@ impl Capture {
backend: Option<input_capture::Backend>,
conn: LanMouseConnection,
release_bind: Vec<scancode::Linux>,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
) -> Self {
let (request_tx, request_rx) = channel();
let (event_tx, event_rx) = channel();
@ -82,6 +85,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 {
@ -166,6 +170,7 @@ struct CaptureTask {
release_bind: Rc<RefCell<Vec<scancode::Linux>>>,
request_rx: Receiver<CaptureRequest>,
state: State,
window_identifier: Arc<Mutex<Option<WindowIdentifier>>>,
}
impl CaptureTask {
@ -200,6 +205,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}");
@ -224,7 +230,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(()),
};

View file

@ -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?;

View file

@ -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<ClientHandle, Incoming>,
next_trigger_handle: u64,
window_identifier: Arc<Mutex<Option<input_capture::WindowIdentifier>>>,
}
#[derive(Debug)]
@ -101,7 +102,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);
@ -126,6 +133,7 @@ impl Service {
incoming_conn_info: Default::default(),
incoming_conns: Default::default(),
next_trigger_handle: 0,
window_identifier,
};
Ok(service)
}
@ -218,6 +226,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)
}
});
}
}
}