From 6075f776bb3befc4ae4fd4578aea250203272745 Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Mon, 25 Mar 2024 17:22:55 +0100 Subject: [PATCH 1/7] doc: examples for input capture portal --- Cargo.toml | 1 + src/desktop/input_capture.rs | 266 +++++++++++++++++++++++++++++++++++ 2 files changed, 267 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index c1d2318a9..c09f0fb1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,6 +55,7 @@ zbus = { version = "4.0", default-features = false, features = ["url"] } [dev-dependencies] serde_json = "1.0" +reis = { version = "0.2.0", features = [ "tokio" ] } [package.metadata.docs.rs] features = ["gtk4", "raw_handle"] diff --git a/src/desktop/input_capture.rs b/src/desktop/input_capture.rs index fa7fd8593..3385a5414 100644 --- a/src/desktop/input_capture.rs +++ b/src/desktop/input_capture.rs @@ -1,3 +1,269 @@ +//! # Examples +//! +//! ## Retrieving an Ei File Descriptor +//! +//! The input capture portal is used to negotiate the input capture +//! triggers and enable input capturing. +//! +//! Actual input capture events are then communicated over a unix +//! stream using the [libei protocol](https://gitlab.freedesktop.org/libinput/libei). +//! +//! The lifetime of an ei file descriptor is bound by a capture session. +//! +//! ```rust,no_run +//! use ashpd::desktop::input_capture::{Capabilities, InputCapture}; +//! use std::os::fd::AsRawFd; +//! +//! async fn run() -> ashpd::Result<()> { +//! let input_capture = InputCapture::new().await?; +//! let (session, capabilities) = input_capture +//! .create_session( +//! &ashpd::WindowIdentifier::default(), +//! Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen, +//! ) +//! .await?; +//! eprintln!("capabilities: {capabilities}"); +//! +//! let eifd = input_capture.connect_to_eis(&session).await?; +//! eprintln!(" eifd: {}", eifd.as_raw_fd()); +//! Ok(()) +//! } +//! ``` +//! +//! +//! ## Selecting Pointer Barriers. +//! +//! Input capture is triggered through pointer barriers that are provided +//! by the client. +//! +//! The provided barriers need to be positioned at the edges of outputs (monitors) +//! and can be denied by the compositor for various reasons, such as wrong placement. +//! +//! For debugging why a barrier placement failed, the logs of the +//! active portal implementation can be useful, e.g.: +//! +//! ```sh +//! journalctl --user -xeu xdg-desktop-portal-gnome.service +//! ``` +//! +//! The following example sets up barriers according to `pos` +//! (either `Left`, `Right`, `Top` or `Bottom`). +//! +//! Note that barriers positioned between two monitors will be denied +//! and returned in the `failed_barrier_ids` vector. +//! +//! ```rust,no_run +//! use ashpd::desktop::input_capture::{Barrier, Capabilities, InputCapture}; +//! +//! #[allow(unused)] +//! enum Position { +//! Left, +//! Right, +//! Top, +//! Bottom, +//! } +//! +//! async fn run() -> ashpd::Result<()> { +//! let input_capture = InputCapture::new().await?; +//! let (session, _capabilities) = input_capture +//! .create_session( +//! &ashpd::WindowIdentifier::default(), +//! Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen, +//! ) +//! .await?; +//! +//! let pos = Position::Left; +//! let zones = input_capture.zones(&session).await?.response()?; +//! eprintln!("zones: {zones:?}"); +//! let barriers = zones +//! .regions() +//! .iter() +//! .enumerate() +//! .map(|(n, r)| { +//! let id = n as u32; +//! let (x, y) = (r.x_offset(), r.y_offset()); +//! let (width, height) = (r.width() as i32, r.height() as i32); +//! let barrier_pos = match pos { +//! Position::Left => (x, y, x, y + height - 1), // start pos, end pos, inclusive +//! Position::Right => (x + width, y, x + width, y + height - 1), +//! Position::Top => (x, y, x + width - 1, y), +//! Position::Bottom => (x, y + height, x + width - 1, y + height), +//! }; +//! Barrier::new(id, barrier_pos) +//! }) +//! .collect::>(); +//! +//! eprintln!("requested barriers: {barriers:?}"); +//! +//! let request = input_capture +//! .set_pointer_barriers(&session, &barriers, zones.zone_set()) +//! .await?; +//! let response = request.response()?; +//! let failed_barrier_ids = response.failed_barriers(); +//! +//! eprintln!("failed barrier ids: {:?}", failed_barrier_ids); +//! +//! Ok(()) +//! } +//! ``` +//! +//! ## Enabling Input Capture and Retrieving Captured Input Events. +//! +//! The following full example uses the [reis crate](https://docs.rs/reis/0.2.0/reis/) +//! for libei communication. +//! +//! Input Capture can be released using ESC. +//! +//! ```rust,no_run +//! use ashpd::desktop::input_capture::{Barrier, Capabilities, InputCapture}; +//! use futures_util::StreamExt; +//! use reis::{ +//! ei::{self, keyboard::KeyState}, +//! event::{DeviceCapability, EiEvent, KeyboardKey}, +//! tokio::{EiConvertEventStream, EiEventStream}, +//! }; +//! use std::{collections::HashMap, os::unix::net::UnixStream, sync::OnceLock, time::Duration}; +//! +//! #[allow(unused)] +//! enum Position { +//! Left, +//! Right, +//! Top, +//! Bottom, +//! } +//! +//! static INTERFACES: OnceLock> = OnceLock::new(); +//! +//! async fn run() -> ashpd::Result<()> { +//! let input_capture = InputCapture::new().await.unwrap(); +//! +//! let (session, _cap) = input_capture +//! .create_session( +//! &ashpd::WindowIdentifier::default(), +//! Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen, +//! ) +//! .await?; +//! +//! // connect to eis server +//! eprintln!("connect_to_eis"); +//! let fd = input_capture.connect_to_eis(&session).await?; +//! +//! // create unix stream from fd +//! let stream = UnixStream::from(fd); +//! stream.set_nonblocking(true)?; +//! +//! // create ei context +//! let context = ei::Context::new(stream)?; +//! context.flush().unwrap(); +//! +//! let mut event_stream = EiEventStream::new(context.clone())?; +//! let interfaces = INTERFACES.get_or_init(|| { +//! let mut m = HashMap::new(); +//! m.insert("ei_connection", 1); +//! m.insert("ei_callback", 1); +//! m.insert("ei_pingpong", 1); +//! m.insert("ei_seat", 1); +//! m.insert("ei_device", 2); +//! m.insert("ei_pointer", 1); +//! m.insert("ei_pointer_absolute", 1); +//! m.insert("ei_scroll", 1); +//! m.insert("ei_button", 1); +//! m.insert("ei_keyboard", 1); +//! m.insert("ei_touchscreen", 1); +//! m +//! }); +//! let response = reis::tokio::ei_handshake( +//! &mut event_stream, +//! "ashpd-mre", +//! ei::handshake::ContextType::Receiver, +//! interfaces, +//! ) +//! .await +//! .expect("ei handshake failed"); +//! +//! let mut event_stream = EiConvertEventStream::new(event_stream, response.serial); +//! +//! let pos = Position::Left; +//! let zones = input_capture.zones(&session).await?.response()?; +//! eprintln!("zones: {zones:?}"); +//! let barriers = zones +//! .regions() +//! .iter() +//! .enumerate() +//! .map(|(n, r)| { +//! let id = n as u32; +//! let (x, y) = (r.x_offset(), r.y_offset()); +//! let (width, height) = (r.width() as i32, r.height() as i32); +//! let barrier_pos = match pos { +//! Position::Left => (x, y, x, y + height - 1), // start pos, end pos, inclusive +//! Position::Right => (x + width, y, x + width, y + height - 1), +//! Position::Top => (x, y, x + width - 1, y), +//! Position::Bottom => (x, y + height, x + width - 1, y + height), +//! }; +//! Barrier::new(id, barrier_pos) +//! }) +//! .collect::>(); +//! +//! eprintln!("requested barriers: {barriers:?}"); +//! +//! let request = input_capture +//! .set_pointer_barriers(&session, &barriers, zones.zone_set()) +//! .await?; +//! let response = request.response()?; +//! let failed_barrier_ids = response.failed_barriers(); +//! +//! eprintln!("failed barrier ids: {:?}", failed_barrier_ids); +//! +//! input_capture.enable(&session).await?; +//! +//! let mut activate_stream = input_capture.receive_activated().await?; +//! +//! loop { +//! let activated = activate_stream.next().await.unwrap(); +//! +//! eprintln!("activated: {activated:?}"); +//! loop { +//! let ei_event = event_stream.next().await.unwrap().unwrap(); +//! eprintln!("ei event: {ei_event:?}"); +//! if let EiEvent::SeatAdded(seat_event) = &ei_event { +//! seat_event.seat.bind_capabilities(&[ +//! DeviceCapability::Pointer, +//! DeviceCapability::PointerAbsolute, +//! DeviceCapability::Keyboard, +//! DeviceCapability::Touch, +//! DeviceCapability::Scroll, +//! DeviceCapability::Button, +//! ]); +//! context.flush().unwrap(); +//! } +//! if let EiEvent::DeviceAdded(_) = ei_event { +//! // new device added -> restart capture +//! break; +//! }; +//! if let EiEvent::KeyboardKey(KeyboardKey { key, state, .. }) = ei_event { +//! if key == 1 && state == KeyState::Press { +//! // esc pressed +//! break; +//! } +//! } +//! } +//! +//! eprintln!("releasing input capture"); +//! let (x, y) = activated.cursor_position(); +//! let (x, y) = (x as f64, y as f64); +//! let cursor_pos = match pos { +//! Position::Left => (x + 1., y), +//! Position::Right => (x - 1., y), +//! Position::Top => (x, y - 1.), +//! Position::Bottom => (x, y + 1.), +//! }; +//! input_capture +//! .release(&session, activated.activation_id(), cursor_pos) +//! .await?; +//! } +//! } +//! ``` + use std::{collections::HashMap, os::fd::OwnedFd}; use enumflags2::{bitflags, BitFlags}; From 9c318945b7a102c5a62dfc1d30278a9beb5c6905 Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Mon, 25 Mar 2024 17:28:06 +0100 Subject: [PATCH 2/7] update wrong signal documentation --- src/desktop/input_capture.rs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/desktop/input_capture.rs b/src/desktop/input_capture.rs index 3385a5414..64e202712 100644 --- a/src/desktop/input_capture.rs +++ b/src/desktop/input_capture.rs @@ -355,7 +355,7 @@ struct DeactivatedOptions { activation_id: u32, } -/// Indicates that an input capturing session was disabled. +/// Indicates that an input capturing session was deactivated. #[derive(Debug, Deserialize, Type)] #[zvariant(signature = "(oa{sv})")] pub struct Deactivated(OwnedObjectPath, DeactivatedOptions); @@ -381,7 +381,7 @@ struct ActivatedOptions { barrier_id: BarrierID, } -/// Indicates that an input capturing session was disabled. +/// Indicates that an input capturing session was activated. #[derive(Debug, Deserialize, Type)] #[zvariant(signature = "(oa{sv})")] pub struct Activated(OwnedObjectPath, ActivatedOptions); @@ -414,7 +414,7 @@ struct ZonesChangedOptions { zone_set: u32, } -/// Indicates that an input capturing session was disabled. +/// Indicates that zones available to this session changed. #[derive(Debug, Deserialize, Type)] #[zvariant(signature = "(oa{sv})")] pub struct ZonesChanged(OwnedObjectPath, ZonesChangedOptions); @@ -655,8 +655,8 @@ impl<'a> InputCapture<'a> { self.0.signal("Disabled").await } - /// Signal emitted when the application will no longer receive captured - /// events. + /// Signal emitted when input capture starts and + /// input events are about to be sent to the application. /// /// # Specifications /// @@ -666,8 +666,8 @@ impl<'a> InputCapture<'a> { self.0.signal("Activated").await } - /// Signal emitted when the application will no longer receive captured - /// events. + /// Signal emitted when input capture stopped and input events + /// are no longer sent to the application. /// /// # Specifications /// @@ -677,8 +677,7 @@ impl<'a> InputCapture<'a> { self.0.signal("Deactivated").await } - /// Signal emitted when the application will no longer receive captured - /// events. + /// Signal emitted when the set of zones available to this session change. /// /// # Specifications /// From 9e2d635c9c2dd4e5011c8c8233a573cdeb8ee215 Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Mon, 25 Mar 2024 22:31:40 +0100 Subject: [PATCH 3/7] Update src/desktop/input_capture.rs Co-authored-by: Bilal Elmoussaoui --- src/desktop/input_capture.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/desktop/input_capture.rs b/src/desktop/input_capture.rs index 64e202712..b4441d734 100644 --- a/src/desktop/input_capture.rs +++ b/src/desktop/input_capture.rs @@ -135,7 +135,7 @@ //! static INTERFACES: OnceLock> = OnceLock::new(); //! //! async fn run() -> ashpd::Result<()> { -//! let input_capture = InputCapture::new().await.unwrap(); +//! let input_capture = InputCapture::new().await?; //! //! let (session, _cap) = input_capture //! .create_session( From ff42a4f85e9d87cf909838e07034c65a08d20614 Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Mon, 25 Mar 2024 22:31:52 +0100 Subject: [PATCH 4/7] Update src/desktop/input_capture.rs Co-authored-by: Bilal Elmoussaoui --- src/desktop/input_capture.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/desktop/input_capture.rs b/src/desktop/input_capture.rs index b4441d734..02052e1d7 100644 --- a/src/desktop/input_capture.rs +++ b/src/desktop/input_capture.rs @@ -25,7 +25,7 @@ //! eprintln!("capabilities: {capabilities}"); //! //! let eifd = input_capture.connect_to_eis(&session).await?; -//! eprintln!(" eifd: {}", eifd.as_raw_fd()); +//! eprintln!("eifd: {}", eifd.as_raw_fd()); //! Ok(()) //! } //! ``` From 319614ea899951ea6669ca3852c350f80ca71da6 Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Mon, 25 Mar 2024 22:32:02 +0100 Subject: [PATCH 5/7] Update src/desktop/input_capture.rs Co-authored-by: Bilal Elmoussaoui --- src/desktop/input_capture.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/desktop/input_capture.rs b/src/desktop/input_capture.rs index 02052e1d7..0fba94f4d 100644 --- a/src/desktop/input_capture.rs +++ b/src/desktop/input_capture.rs @@ -145,7 +145,6 @@ //! .await?; //! //! // connect to eis server -//! eprintln!("connect_to_eis"); //! let fd = input_capture.connect_to_eis(&session).await?; //! //! // create unix stream from fd From 361ddd88c1f994f6f0f3ba6051acf65e42d968eb Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Mon, 25 Mar 2024 22:37:01 +0100 Subject: [PATCH 6/7] use hashmap from --- src/desktop/input_capture.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/desktop/input_capture.rs b/src/desktop/input_capture.rs index 0fba94f4d..7eae49b61 100644 --- a/src/desktop/input_capture.rs +++ b/src/desktop/input_capture.rs @@ -157,19 +157,19 @@ //! //! let mut event_stream = EiEventStream::new(context.clone())?; //! let interfaces = INTERFACES.get_or_init(|| { -//! let mut m = HashMap::new(); -//! m.insert("ei_connection", 1); -//! m.insert("ei_callback", 1); -//! m.insert("ei_pingpong", 1); -//! m.insert("ei_seat", 1); -//! m.insert("ei_device", 2); -//! m.insert("ei_pointer", 1); -//! m.insert("ei_pointer_absolute", 1); -//! m.insert("ei_scroll", 1); -//! m.insert("ei_button", 1); -//! m.insert("ei_keyboard", 1); -//! m.insert("ei_touchscreen", 1); -//! m +//! HashMap::from([ +//! ("ei_connection", 1), +//! ("ei_callback", 1), +//! ("ei_pingpong", 1), +//! ("ei_seat", 1), +//! ("ei_device", 2), +//! ("ei_pointer", 1), +//! ("ei_pointer_absolute", 1), +//! ("ei_scroll", 1), +//! ("ei_button", 1), +//! ("ei_keyboard", 1), +//! ("ei_touchscreen", 1), +//! ]) //! }); //! let response = reis::tokio::ei_handshake( //! &mut event_stream, From 704cf7fbac5c969856a3975baabae31bf941ef65 Mon Sep 17 00:00:00 2001 From: Ferdinand Schober Date: Mon, 25 Mar 2024 23:27:56 +0100 Subject: [PATCH 7/7] add warning about gnome portal --- src/desktop/input_capture.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/desktop/input_capture.rs b/src/desktop/input_capture.rs index 7eae49b61..93bc8d7a5 100644 --- a/src/desktop/input_capture.rs +++ b/src/desktop/input_capture.rs @@ -1,5 +1,29 @@ //! # Examples //! +//! ## A Note of Warning Regarding the GNOME Portal Implementation +//! +//! `xdg-desktop-portal-gnome` in version 46.0 has a +//! [bug](https://gitlab.gnome.org/GNOME/xdg-desktop-portal-gnome/-/issues/126) +//! that prevents reenabling a disabled session. +//! +//! Since changing barrier locations requires a session to be disabled, +//! it is currently (as of GNOME 46) not possible to change barriers +//! after a session has been enabled! +//! +//! (the [official documentation](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-setpointerbarriers) +//! states that a +//! [`InputCapture::set_pointer_barriers()`][set_pointer_barriers] +//! request suspends the capture session but in reality the GNOME +//! desktop portal enforces a +//! [`InputCapture::disable()`][disable] +//! request +//! in order to use +//! [`InputCapture::set_pointer_barriers()`][set_pointer_barriers] +//! ) +//! +//! [set_pointer_barriers]: crate::desktop::input_capture::InputCapture::set_pointer_barriers +//! [disable]: crate::desktop::input_capture::InputCapture::disable +//! //! ## Retrieving an Ei File Descriptor //! //! The input capture portal is used to negotiate the input capture