Skip to main content

ashpd/desktop/
input_capture.rs

1//! # Examples
2//!
3//! ## A Note of Warning Regarding the GNOME Portal Implementation
4//!
5//! `xdg-desktop-portal-gnome` in version 46.0 has a
6//! [bug](https://gitlab.gnome.org/GNOME/xdg-desktop-portal-gnome/-/issues/126)
7//! that prevents reenabling a disabled session.
8//!
9//! Since changing barrier locations requires a session to be disabled,
10//! it is currently (as of GNOME 46) not possible to change barriers
11//! after a session has been enabled!
12//!
13//! (the [official documentation](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-setpointerbarriers)
14//! states that a
15//! [`InputCapture::set_pointer_barriers()`][set_pointer_barriers]
16//! request suspends the capture session but in reality the GNOME
17//! desktop portal enforces a
18//! [`InputCapture::disable()`][disable]
19//! request
20//! in order to use
21//! [`InputCapture::set_pointer_barriers()`][set_pointer_barriers]
22//! )
23//!
24//! [set_pointer_barriers]: crate::desktop::input_capture::InputCapture::set_pointer_barriers
25//! [disable]: crate::desktop::input_capture::InputCapture::disable
26//!
27//! ## Retrieving an Ei File Descriptor
28//!
29//! The input capture portal is used to negotiate the input capture
30//! triggers and enable input capturing.
31//!
32//! Actual input capture events are then communicated over a unix
33//! stream using the [libei protocol](https://gitlab.freedesktop.org/libinput/libei).
34//!
35//! The lifetime of an ei file descriptor is bound by a capture session.
36//!
37//! ```rust,no_run
38//! use std::os::fd::AsRawFd;
39//!
40//! use ashpd::desktop::input_capture::{Capabilities, CreateSessionOptions, InputCapture};
41//!
42//! async fn run() -> ashpd::Result<()> {
43//!     let input_capture = InputCapture::new().await?;
44//!     let (session, capabilities) = input_capture
45//!         .create_session(
46//!             None,
47//!             CreateSessionOptions::default().set_capabilities(
48//!                 Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen,
49//!             ),
50//!         )
51//!         .await?;
52//!     eprintln!("capabilities: {capabilities}");
53//!
54//!     let eifd = input_capture
55//!         .connect_to_eis(&session, Default::default())
56//!         .await?;
57//!     eprintln!("eifd: {}", eifd.as_raw_fd());
58//!     Ok(())
59//! }
60//! ```
61//!
62//!
63//! ## Selecting Pointer Barriers.
64//!
65//! Input capture is triggered through pointer barriers that are provided
66//! by the client.
67//!
68//! The provided barriers need to be positioned at the edges of outputs
69//! (monitors) and can be denied by the compositor for various reasons, such as
70//! wrong placement.
71//!
72//! For debugging why a barrier placement failed, the logs of the
73//! active portal implementation can be useful, e.g.:
74//!
75//! ```sh
76//! journalctl --user -xeu xdg-desktop-portal-gnome.service
77//! ```
78//!
79//! The following example sets up barriers according to `pos`
80//! (either `Left`, `Right`, `Top` or `Bottom`).
81//!
82//! Note that barriers positioned between two monitors will be denied
83//! and returned in the `failed_barrier_ids` vector.
84//!
85//! ```rust,no_run
86//! use ashpd::desktop::input_capture::{
87//!     Barrier, BarrierID, BarrierPosition, Capabilities, CreateSessionOptions, InputCapture,
88//! };
89//!
90//! #[allow(unused)]
91//! enum Position {
92//!     Left,
93//!     Right,
94//!     Top,
95//!     Bottom,
96//! }
97//!
98//! async fn run() -> ashpd::Result<()> {
99//!     let input_capture = InputCapture::new().await?;
100//!     let (session, _capabilities) = input_capture
101//!         .create_session(
102//!             None,
103//!             CreateSessionOptions::default().set_capabilities(
104//!                 Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen,
105//!             ),
106//!         )
107//!         .await?;
108//!
109//!     let pos = Position::Left;
110//!     let zones = input_capture
111//!         .zones(&session, Default::default())
112//!         .await?
113//!         .response()?;
114//!     eprintln!("zones: {zones:?}");
115//!     let barriers = zones
116//!         .regions()
117//!         .iter()
118//!         .enumerate()
119//!         .map(|(n, r)| {
120//!             let id = BarrierID::new((n + 1) as u32).expect("barrier-id must be non-zero");
121//!             let (x, y) = (r.x_offset(), r.y_offset());
122//!             let (width, height) = (r.width() as i32, r.height() as i32);
123//!             let barrier_pos = match pos {
124//!                 Position::Left => BarrierPosition::new(x, y, x, y + height - 1), // start pos, end pos, inclusive
125//!                 Position::Right => BarrierPosition::new(x + width, y, x + width, y + height - 1),
126//!                 Position::Top => BarrierPosition::new(x, y, x + width - 1, y),
127//!                 Position::Bottom => BarrierPosition::new(x, y + height, x + width - 1, y + height),
128//!             };
129//!             Barrier::new(id, barrier_pos)
130//!         })
131//!         .collect::<Vec<_>>();
132//!
133//!     eprintln!("requested barriers: {barriers:?}");
134//!
135//!     let request = input_capture
136//!         .set_pointer_barriers(&session, &barriers, zones.zone_set(), Default::default())
137//!         .await?;
138//!     let response = request.response()?;
139//!     let failed_barrier_ids = response.failed_barriers();
140//!
141//!     eprintln!("failed barrier ids: {:?}", failed_barrier_ids);
142//!
143//!     Ok(())
144//! }
145//! ```
146//!
147//! ## Enabling Input Capture and Retrieving Captured Input Events.
148//!
149//! The following full example uses the [reis crate](https://docs.rs/reis/0.2.0/reis/)
150//! for libei communication.
151//!
152//! Input Capture can be released using ESC.
153//!
154//! ```rust,no_run
155//! use std::{collections::HashMap, os::unix::net::UnixStream, sync::OnceLock, time::Duration};
156//!
157//! use ashpd::desktop::input_capture::{
158//!     Barrier, BarrierID, BarrierPosition, Capabilities, CreateSessionOptions, InputCapture, ReleaseOptions,
159//! };
160//! use futures_util::StreamExt;
161//! use reis::{
162//!     ei::{self, keyboard::KeyState},
163//!     event::{DeviceCapability, EiEvent, KeyboardKey},
164//! };
165//!
166//! #[allow(unused)]
167//! enum Position {
168//!     Left,
169//!     Right,
170//!     Top,
171//!     Bottom,
172//! }
173//!
174//! static INTERFACES: OnceLock<HashMap<&'static str, u32>> = OnceLock::new();
175//!
176//! async fn run() -> ashpd::Result<()> {
177//!     let input_capture = InputCapture::new().await?;
178//!
179//!     let (session, _cap) = input_capture
180//!         .create_session(
181//!             None,
182//!             CreateSessionOptions::default().set_capabilities(
183//!                 Capabilities::Keyboard | Capabilities::Pointer | Capabilities::Touchscreen,
184//!             ),
185//!         )
186//!         .await?;
187//!
188//!     // connect to eis server
189//!     let fd = input_capture
190//!         .connect_to_eis(&session, Default::default())
191//!         .await?;
192//!
193//!     // create unix stream from fd
194//!     let stream = UnixStream::from(fd);
195//!     stream.set_nonblocking(true)?;
196//!
197//!     // create ei context
198//!     let context = ei::Context::new(stream)?;
199//!     context.flush().unwrap();
200//!
201//!     let (_connection, mut event_stream) = context
202//!         .handshake_tokio("ashpd-mre", ei::handshake::ContextType::Receiver)
203//!         .await
204//!         .expect("ei handshake failed");
205//!
206//!     let pos = Position::Left;
207//!     let zones = input_capture
208//!         .zones(&session, Default::default())
209//!         .await?
210//!         .response()?;
211//!     eprintln!("zones: {zones:?}");
212//!     let barriers = zones
213//!         .regions()
214//!         .iter()
215//!         .enumerate()
216//!         .map(|(n, r)| {
217//!             let id = BarrierID::new((n + 1) as u32).expect("barrier-id must be non-zero");
218//!             let (x, y) = (r.x_offset(), r.y_offset());
219//!             let (width, height) = (r.width() as i32, r.height() as i32);
220//!             let barrier_pos = match pos {
221//!                 Position::Left => BarrierPosition::new(x, y, x, y + height - 1), // start pos, end pos, inclusive
222//!                 Position::Right => BarrierPosition::new(x + width, y, x + width, y + height - 1),
223//!                 Position::Top => BarrierPosition::new(x, y, x + width - 1, y),
224//!                 Position::Bottom => BarrierPosition::new(x, y + height, x + width - 1, y + height),
225//!             };
226//!             Barrier::new(id, barrier_pos)
227//!         })
228//!         .collect::<Vec<_>>();
229//!
230//!     eprintln!("requested barriers: {barriers:?}");
231//!
232//!     let request = input_capture
233//!         .set_pointer_barriers(&session, &barriers, zones.zone_set(), Default::default())
234//!         .await?;
235//!     let response = request.response()?;
236//!     let failed_barrier_ids = response.failed_barriers();
237//!
238//!     eprintln!("failed barrier ids: {:?}", failed_barrier_ids);
239//!
240//!     input_capture.enable(&session, Default::default()).await?;
241//!
242//!     let mut activate_stream = input_capture.receive_activated().await?;
243//!
244//!     loop {
245//!         let activated = activate_stream.next().await.unwrap();
246//!
247//!         eprintln!("activated: {activated:?}");
248//!         loop {
249//!             let ei_event = event_stream.next().await.unwrap().unwrap();
250//!             eprintln!("ei event: {ei_event:?}");
251//!             if let EiEvent::SeatAdded(seat_event) = &ei_event {
252//!                 seat_event.seat.bind_capabilities(
253//!                     DeviceCapability::Pointer
254//!                         | DeviceCapability::PointerAbsolute
255//!                         | DeviceCapability::Keyboard
256//!                         | DeviceCapability::Touch
257//!                         | DeviceCapability::Scroll
258//!                         | DeviceCapability::Button,
259//!                 );
260//!                 context.flush().unwrap();
261//!             }
262//!             if let EiEvent::DeviceAdded(_) = ei_event {
263//!                 // new device added -> restart capture
264//!                 break;
265//!             };
266//!             if let EiEvent::KeyboardKey(KeyboardKey { key, state, .. }) = ei_event {
267//!                 if key == 1 && state == KeyState::Press {
268//!                     // esc pressed
269//!                     break;
270//!                 }
271//!             }
272//!         }
273//!
274//!         eprintln!("releasing input capture");
275//!         let (x, y) = activated.cursor_position().unwrap();
276//!         let (x, y) = (x as f64, y as f64);
277//!         let cursor_pos = match pos {
278//!             Position::Left => (x + 1., y),
279//!             Position::Right => (x - 1., y),
280//!             Position::Top => (x, y - 1.),
281//!             Position::Bottom => (x, y + 1.),
282//!         };
283//!         input_capture
284//!             .release(
285//!                 &session,
286//!                 ReleaseOptions::default()
287//!                     .set_activation_id(activated.activation_id())
288//!                     .set_cursor_position(cursor_pos),
289//!             )
290//!             .await?;
291//!     }
292//! }
293//! ```
294
295use std::{collections::HashMap, num::NonZeroU32, os::fd::OwnedFd};
296
297use enumflags2::{BitFlags, bitflags};
298use futures_util::Stream;
299use serde::{Deserialize, Serialize, de::Visitor};
300use serde_repr::{Deserialize_repr, Serialize_repr};
301use zbus::zvariant::{
302    self, ObjectPath, Optional, OwnedObjectPath, OwnedValue, Type,
303    as_value::{self, optional},
304};
305
306use super::{HandleToken, PersistMode, Request, Session, session::SessionPortal};
307use crate::{Error, WindowIdentifier, proxy::Proxy};
308
309#[derive(Serialize_repr, Deserialize_repr, PartialEq, Eq, Debug, Copy, Clone, Type)]
310#[bitflags]
311#[repr(u32)]
312/// Supported capabilities
313pub enum Capabilities {
314    /// Keyboard
315    Keyboard,
316    /// Pointer
317    Pointer,
318    /// Touchscreen
319    Touchscreen,
320}
321
322#[derive(Debug, Serialize, Type, Default)]
323#[zvariant(signature = "dict")]
324/// Specified options for a [`InputCapture::create_session`] request.
325pub struct CreateSessionOptions {
326    #[serde(with = "as_value")]
327    handle_token: HandleToken,
328    #[serde(with = "as_value")]
329    session_handle_token: HandleToken,
330    #[serde(with = "as_value")]
331    capabilities: BitFlags<Capabilities>,
332}
333
334impl CreateSessionOptions {
335    /// Request the specified capabilities.
336    pub fn set_capabilities(mut self, capabilities: BitFlags<Capabilities>) -> Self {
337        self.capabilities = capabilities;
338        self
339    }
340}
341
342#[derive(Debug, Deserialize, Type)]
343#[zvariant(signature = "dict")]
344struct CreateSessionResponse {
345    #[serde(with = "as_value")]
346    session_handle: OwnedObjectPath,
347    #[serde(with = "as_value")]
348    capabilities: BitFlags<Capabilities>,
349}
350
351#[derive(Debug, Serialize, Type, Default)]
352#[zvariant(signature = "dict")]
353/// Specified options for a [`InputCapture::create_session2`] request.
354pub struct CreateSession2Options {
355    #[serde(with = "as_value")]
356    session_handle_token: HandleToken,
357}
358
359#[derive(Debug, Deserialize, Type)]
360#[zvariant(signature = "dict")]
361struct CreateSession2Results {
362    #[serde(with = "as_value")]
363    session_handle: OwnedObjectPath,
364}
365
366#[derive(Debug, Serialize, Type, Default)]
367#[zvariant(signature = "dict")]
368/// Specified options for a [`InputCapture::start`] request.
369pub struct StartOptions {
370    #[serde(with = "as_value")]
371    handle_token: HandleToken,
372    #[serde(with = "as_value")]
373    capabilities: BitFlags<Capabilities>,
374    #[serde(with = "optional", skip_serializing_if = "Option::is_none")]
375    restore_token: Option<String>,
376    #[serde(with = "optional", skip_serializing_if = "Option::is_none")]
377    persist_mode: Option<PersistMode>,
378}
379
380impl StartOptions {
381    /// Request the specified capabilities.
382    pub fn set_capabilities(mut self, capabilities: BitFlags<Capabilities>) -> Self {
383        self.capabilities = capabilities;
384        self
385    }
386
387    /// Set the token to restore a previous persistent session.
388    pub fn set_restore_token(mut self, restore_token: impl Into<Option<String>>) -> Self {
389        self.restore_token = restore_token.into();
390        self
391    }
392
393    /// Set the persist mode for this session.
394    pub fn set_persist_mode(mut self, persist_mode: impl Into<Option<PersistMode>>) -> Self {
395        self.persist_mode = persist_mode.into();
396        self
397    }
398}
399
400#[derive(Debug, Deserialize, Type)]
401#[zvariant(signature = "dict")]
402/// Response of [`InputCapture::create_session`] request.
403pub struct StartResponse {
404    #[serde(with = "as_value")]
405    capabilities: BitFlags<Capabilities>,
406    #[serde(default, with = "optional")]
407    clipboard_enabled: Option<bool>,
408    #[serde(default, with = "optional")]
409    restore_token: Option<String>,
410}
411
412impl StartResponse {
413    /// The capabilities available to this session.
414    pub fn capabilities(&self) -> BitFlags<Capabilities> {
415        self.capabilities
416    }
417
418    /// Whether the clipboard was enabled.
419    pub fn is_clipboard_enabled(&self) -> bool {
420        self.clipboard_enabled.unwrap_or(false)
421    }
422
423    /// The session restore token.
424    pub fn restore_token(&self) -> Option<&str> {
425        self.restore_token.as_deref()
426    }
427}
428
429#[derive(Default, Debug, Serialize, Type)]
430#[zvariant(signature = "dict")]
431/// Specified options for a [`InputCapture::zones`] request.
432pub struct GetZonesOptions {
433    #[serde(with = "as_value")]
434    handle_token: HandleToken,
435}
436
437#[derive(Default, Debug, Serialize, Type)]
438#[zvariant(signature = "dict")]
439/// Specified options for a [`InputCapture::set_pointer_barriers`] request.
440pub struct SetPointerBarriersOptions {
441    #[serde(with = "as_value")]
442    handle_token: HandleToken,
443}
444
445#[derive(Default, Debug, Serialize, Type)]
446#[zvariant(signature = "dict")]
447/// Specified options for a [`InputCapture::enable`] request.
448pub struct EnableOptions {}
449
450#[derive(Default, Debug, Serialize, Type)]
451#[zvariant(signature = "dict")]
452/// Specified options for a [`InputCapture::disable`] request.
453pub struct DisableOptions {}
454
455#[derive(Default, Debug, Serialize, Type)]
456#[zvariant(signature = "dict")]
457/// Specified options for a [`InputCapture::release`] request.
458pub struct ReleaseOptions {
459    #[serde(with = "optional", skip_serializing_if = "Option::is_none")]
460    activation_id: Option<u32>,
461    #[serde(with = "optional", skip_serializing_if = "Option::is_none")]
462    cursor_position: Option<(f64, f64)>,
463}
464
465impl ReleaseOptions {
466    /// The same activation_id number as in the corresponding "Activated"
467    /// signal.
468    pub fn set_activation_id(mut self, activation_id: impl Into<Option<u32>>) -> Self {
469        self.activation_id = activation_id.into();
470        self
471    }
472
473    /// The suggested cursor position within the Zones available in this
474    /// session.
475    pub fn set_cursor_position(mut self, cursor_position: impl Into<Option<(f64, f64)>>) -> Self {
476        self.cursor_position = cursor_position.into();
477        self
478    }
479}
480
481#[derive(Default, Debug, Serialize, Type)]
482#[zvariant(signature = "dict")]
483/// Specified options for a [`InputCapture::connect_to_eis`] request.
484pub struct ConnectToEISOptions {}
485
486/// Indicates that an input capturing session was disabled.
487#[derive(Debug, Deserialize, Type)]
488#[zvariant(signature = "(oa{sv})")]
489pub struct Disabled(OwnedObjectPath, HashMap<String, OwnedValue>);
490
491impl Disabled {
492    /// Session that was disabled.
493    pub fn session_handle(&self) -> ObjectPath<'_> {
494        self.0.as_ref()
495    }
496
497    /// Optional information
498    pub fn options(&self) -> &HashMap<String, OwnedValue> {
499        &self.1
500    }
501}
502
503#[derive(Debug, Deserialize, Type)]
504#[zvariant(signature = "dict")]
505struct DeactivatedOptions {
506    #[serde(default, with = "optional")]
507    activation_id: Option<u32>,
508}
509
510/// Indicates that an input capturing session was deactivated.
511#[derive(Debug, Deserialize, Type)]
512#[zvariant(signature = "(oa{sv})")]
513pub struct Deactivated(OwnedObjectPath, DeactivatedOptions);
514
515impl Deactivated {
516    /// Session that was deactivated.
517    pub fn session_handle(&self) -> ObjectPath<'_> {
518        self.0.as_ref()
519    }
520
521    /// The same activation_id number as in the corresponding "Activated"
522    /// signal.
523    pub fn activation_id(&self) -> Option<u32> {
524        self.1.activation_id
525    }
526}
527
528#[derive(Debug, Deserialize, Type)]
529#[zvariant(signature = "dict")]
530struct ActivatedOptions {
531    #[serde(default, with = "optional")]
532    activation_id: Option<u32>,
533    #[serde(default, with = "optional")]
534    cursor_position: Option<(f32, f32)>,
535    #[serde(default, with = "optional")]
536    barrier_id: Option<ActivatedBarrier>,
537}
538
539/// Indicates that an input capturing session was activated.
540#[derive(Debug, Deserialize, Type)]
541#[zvariant(signature = "(oa{sv})")]
542pub struct Activated(OwnedObjectPath, ActivatedOptions);
543
544impl Activated {
545    /// Session that was activated.
546    pub fn session_handle(&self) -> ObjectPath<'_> {
547        self.0.as_ref()
548    }
549
550    /// A number that can be used to synchronize with the transport-layer.
551    pub fn activation_id(&self) -> Option<u32> {
552        self.1.activation_id
553    }
554
555    /// The current cursor position in the same coordinate space as the zones.
556    pub fn cursor_position(&self) -> Option<(f32, f32)> {
557        self.1.cursor_position
558    }
559
560    /// The barrier that was triggered or None,
561    /// if the input-capture was not triggered by a barrier
562    pub fn barrier_id(&self) -> Option<ActivatedBarrier> {
563        self.1.barrier_id
564    }
565}
566
567#[derive(Clone, Copy, Debug, Type)]
568#[zvariant(signature = "u")]
569/// information about an activation barrier
570pub enum ActivatedBarrier {
571    /// [`BarrierID`] of the triggered barrier
572    Barrier(BarrierID),
573    /// The id of the triggered barrier could not be determined,
574    /// e.g. because of multiple barriers at the same location.
575    UnknownBarrier,
576}
577
578impl<'de> Deserialize<'de> for ActivatedBarrier {
579    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
580    where
581        D: serde::Deserializer<'de>,
582    {
583        let visitor = ActivatedBarrierVisitor {};
584        deserializer.deserialize_u32(visitor)
585    }
586}
587
588struct ActivatedBarrierVisitor {}
589
590impl Visitor<'_> for ActivatedBarrierVisitor {
591    type Value = ActivatedBarrier;
592
593    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
594        write!(formatter, "an unsigned 32bit integer (u32)")
595    }
596
597    fn visit_u32<E>(self, v: u32) -> Result<Self::Value, E>
598    where
599        E: serde::de::Error,
600    {
601        match BarrierID::new(v) {
602            Some(v) => Ok(ActivatedBarrier::Barrier(v)),
603            None => Ok(ActivatedBarrier::UnknownBarrier),
604        }
605    }
606}
607
608#[derive(Debug, Deserialize, Type)]
609#[zvariant(signature = "dict")]
610struct ZonesChangedOptions {
611    #[serde(default, with = "optional")]
612    zone_set: Option<u32>,
613}
614
615/// Indicates that zones available to this session changed.
616#[derive(Debug, Deserialize, Type)]
617#[zvariant(signature = "(oa{sv})")]
618pub struct ZonesChanged(OwnedObjectPath, ZonesChangedOptions);
619
620impl ZonesChanged {
621    /// Session that was deactivated.
622    pub fn session_handle(&self) -> ObjectPath<'_> {
623        self.0.as_ref()
624    }
625
626    ///  The zone_set ID of the invalidated zone.
627    pub fn zone_set(&self) -> Option<u32> {
628        self.1.zone_set
629    }
630}
631
632/// A region of a [`Zones`].
633#[derive(Debug, Clone, Copy, Deserialize, Type)]
634#[zvariant(signature = "(uuii)")]
635pub struct Region(u32, u32, i32, i32);
636
637impl Region {
638    /// The width.
639    pub fn width(self) -> u32 {
640        self.0
641    }
642
643    /// The height
644    pub fn height(self) -> u32 {
645        self.1
646    }
647
648    /// The x offset.
649    pub fn x_offset(self) -> i32 {
650        self.2
651    }
652
653    /// The y offset.
654    pub fn y_offset(self) -> i32 {
655        self.3
656    }
657}
658
659/// A response of [`InputCapture::zones`].
660#[derive(Debug, Type, Deserialize)]
661#[zvariant(signature = "dict")]
662pub struct Zones {
663    #[serde(default, with = "as_value")]
664    zones: Vec<Region>,
665    #[serde(default, with = "as_value")]
666    zone_set: u32,
667}
668
669impl Zones {
670    /// A list of regions.
671    pub fn regions(&self) -> &[Region] {
672        &self.zones
673    }
674
675    /// A unique ID to be used in [`InputCapture::set_pointer_barriers`].
676    pub fn zone_set(&self) -> u32 {
677        self.zone_set
678    }
679}
680
681/// A barrier ID.
682pub type BarrierID = NonZeroU32;
683
684/// Position of a barrier defined by two points (x1, y1) and (x2, y2).
685///
686/// Barriers are typically placed along screen edges.
687#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Type)]
688#[zvariant(signature = "(iiii)")]
689pub struct BarrierPosition {
690    /// x coordinate of the first point
691    x1: i32,
692    /// y coordinate of the first point
693    y1: i32,
694    /// x coordinate of the second point
695    x2: i32,
696    /// y coordinate of the second point
697    y2: i32,
698}
699
700impl BarrierPosition {
701    /// Create a new barrier position represented by the
702    /// points x1/y1 to x2/y2.
703    pub fn new(x1: i32, y1: i32, x2: i32, y2: i32) -> Self {
704        Self { x1, y1, x2, y2 }
705    }
706
707    /// Convert to a tuple (x1, y1, x2, y2).
708    pub fn as_tuple(&self) -> (i32, i32, i32, i32) {
709        (self.x1, self.y1, self.x2, self.y2)
710    }
711
712    /// The x coordinate of the first point of the barrier.
713    pub fn x1(&self) -> i32 {
714        self.x1
715    }
716
717    /// The y coordinate of the second point of the barrier.
718    pub fn y1(&self) -> i32 {
719        self.y1
720    }
721
722    /// The x coordinate of the second point of the barrier.
723    pub fn x2(&self) -> i32 {
724        self.x2
725    }
726
727    /// The y coordinate of the second point of the barrier.
728    pub fn y2(&self) -> i32 {
729        self.y2
730    }
731}
732
733impl From<(i32, i32, i32, i32)> for BarrierPosition {
734    fn from(pos: (i32, i32, i32, i32)) -> Self {
735        Self {
736            x1: pos.0,
737            y1: pos.1,
738            x2: pos.2,
739            y2: pos.3,
740        }
741    }
742}
743
744impl From<BarrierPosition> for (i32, i32, i32, i32) {
745    fn from(pos: BarrierPosition) -> Self {
746        pos.as_tuple()
747    }
748}
749
750impl Serialize for BarrierPosition {
751    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
752    where
753        S: serde::Serializer,
754    {
755        self.as_tuple().serialize(serializer)
756    }
757}
758
759#[derive(Debug, Serialize, Type)]
760#[zvariant(signature = "dict")]
761/// Input Barrier.
762pub struct Barrier {
763    #[serde(with = "as_value")]
764    barrier_id: BarrierID,
765    #[serde(with = "as_value")]
766    position: BarrierPosition,
767}
768
769impl Barrier {
770    /// Create a new barrier.
771    pub fn new(barrier_id: BarrierID, position: impl Into<BarrierPosition>) -> Self {
772        Self {
773            barrier_id,
774            position: position.into(),
775        }
776    }
777
778    /// Get the barrier ID.
779    pub fn barrier_id(&self) -> BarrierID {
780        self.barrier_id
781    }
782
783    /// Get the barrier position.
784    pub fn position(&self) -> BarrierPosition {
785        self.position
786    }
787}
788
789/// A response to [`InputCapture::set_pointer_barriers`]
790#[derive(Debug, Deserialize, Type)]
791#[zvariant(signature = "dict")]
792pub struct SetPointerBarriersResponse {
793    #[serde(default, with = "as_value")]
794    failed_barriers: Vec<BarrierID>,
795}
796
797impl SetPointerBarriersResponse {
798    /// List of pointer barriers that have been denied
799    pub fn failed_barriers(&self) -> &[BarrierID] {
800        &self.failed_barriers
801    }
802}
803
804/// Wrapper of the DBus interface: [`org.freedesktop.portal.InputCapture`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html).
805#[doc(alias = "org.freedesktop.portal.InputCapture")]
806pub struct InputCapture(Proxy<'static>);
807
808impl InputCapture {
809    /// Create a new instance of [`InputCapture`].
810    pub async fn new() -> Result<InputCapture, Error> {
811        let proxy = Proxy::new_desktop("org.freedesktop.portal.InputCapture").await?;
812        Ok(Self(proxy))
813    }
814
815    /// Create a new instance of [`InputCapture`].
816    pub async fn with_connection(connection: zbus::Connection) -> Result<InputCapture, Error> {
817        let proxy =
818            Proxy::new_desktop_with_connection(connection, "org.freedesktop.portal.InputCapture")
819                .await?;
820        Ok(Self(proxy))
821    }
822
823    /// Returns the version of the portal interface.
824    pub fn version(&self) -> u32 {
825        self.0.version()
826    }
827
828    /// Create an input capture session.
829    ///
830    /// # Specifications
831    ///
832    /// See also [`CreateSession`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-createsession).
833    #[doc(alias = "CreateSession")]
834    pub async fn create_session(
835        &self,
836        identifier: Option<&WindowIdentifier>,
837        options: CreateSessionOptions,
838    ) -> Result<(Session<Self>, BitFlags<Capabilities>), Error> {
839        let identifier = Optional::from(identifier);
840        let (request, proxy) = futures_util::try_join!(
841            self.0.request::<CreateSessionResponse>(
842                &options.handle_token,
843                "CreateSession",
844                (identifier, &options)
845            ),
846            Session::from_unique_name(self.0.connection().clone(), &options.session_handle_token),
847        )?;
848        let response = request.response()?;
849        assert_eq!(proxy.path(), &response.session_handle.as_ref());
850        Ok((proxy, response.capabilities))
851    }
852
853    /// Create an input capture session.
854    ///
855    /// The session must be started with [`start`][`InputCapture::start`]
856    /// before using methods which take a session.
857    ///
858    /// This method was added in version 2 of the interface.
859    ///
860    /// # Example
861    ///
862    /// Use the following approach to start a session and fall back to
863    /// the legacy [`create_session`][`InputCapture::create_session`]
864    /// for portals that only implement version 1.
865    ///
866    /// ```rust,no_run
867    /// use ashpd::{
868    ///     Error,
869    ///     desktop::{
870    ///         PersistMode,
871    ///         input_capture::{Capabilities, InputCapture, StartOptions},
872    ///     },
873    /// };
874    ///
875    /// # async fn run() -> ashpd::Result<()> {
876    /// let input_capture = InputCapture::new().await?;
877    /// let opts = ashpd::desktop::input_capture::CreateSession2Options::default();
878    ///
879    /// let session = match input_capture.create_session2(opts).await {
880    ///     Ok(sess) => {
881    ///         // Version 2: explicitly start the session
882    ///         let opts = StartOptions::default()
883    ///             .set_capabilities(Capabilities::Keyboard | Capabilities::Pointer);
884    ///         input_capture.start(&sess, None, opts).await?;
885    ///         sess
886    ///     }
887    ///     Err(Error::RequiresVersion(_, _)) => {
888    ///         // Version 1: fallback to legacy API, starts implicitly
889    ///         let opts = ashpd::desktop::input_capture::CreateSessionOptions::default()
890    ///             .set_capabilities(Capabilities::Keyboard | Capabilities::Pointer);
891    ///         let (session, _capabilities) = input_capture.create_session(None, opts).await?;
892    ///         session
893    ///     }
894    ///     Err(e) => return Err(e),
895    /// };
896    /// # Ok(())
897    /// # }
898    /// ```
899    ///
900    /// # Specifications
901    ///
902    /// See also [`CreateSession2`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-createsession2).
903    #[doc(alias = "CreateSession2")]
904    pub async fn create_session2(
905        &self,
906        options: CreateSession2Options,
907    ) -> Result<Session<Self>, Error> {
908        let proxy =
909            Session::from_unique_name(self.0.connection().clone(), &options.session_handle_token)
910                .await?;
911        let response = self
912            .0
913            .call_versioned::<CreateSession2Results>("CreateSession2", &options, 2)
914            .await?;
915        assert_eq!(proxy.path(), &response.session_handle.as_ref());
916        Ok(proxy)
917    }
918
919    /// Start the input capture session.
920    ///
921    /// This will typically result in the portal presenting a dialog letting
922    /// the user decide whether they want to allow the input of the session
923    /// to be captured, and what capabilities to support.
924    ///
925    /// This method may only be called once on a session previously created
926    /// with [`create_session2`][`InputCapture::create_session2`].
927    ///
928    /// This method was added in version 2 of the interface.
929    ///
930    /// # Arguments
931    ///
932    /// * `session` - A [`Session`], created with
933    ///   [`create_session2()`][`InputCapture::create_session2`].
934    /// * `identifier` - Identifier for the application window.
935    /// * `capabilities` - Bitmask of requested capabilities.
936    /// * `restore_token` - The token to restore a previous session.
937    /// * `persist_mode` - How this session should persist.
938    ///
939    /// # Specifications
940    ///
941    /// See also [`Start`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-start).
942    #[doc(alias = "Start")]
943    pub async fn start(
944        &self,
945        session: &Session<Self>,
946        identifier: Option<&WindowIdentifier>,
947        options: StartOptions,
948    ) -> Result<Request<StartResponse>, Error> {
949        let identifier = Optional::from(identifier);
950        self.0
951            .request_versioned(
952                &options.handle_token,
953                "Start",
954                (session, identifier, &options),
955                2,
956            )
957            .await
958    }
959    /// A set of currently available input zones for this session.
960    ///
961    /// # Specifications
962    ///
963    /// See also [`GetZones`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-getzones).
964    #[doc(alias = "GetZones")]
965    pub async fn zones(
966        &self,
967        session: &Session<Self>,
968        options: GetZonesOptions,
969    ) -> Result<Request<Zones>, Error> {
970        self.0
971            .request(&options.handle_token, "GetZones", (session, &options))
972            .await
973    }
974
975    /// Set up zero or more pointer barriers.
976    ///
977    /// # Specifications
978    ///
979    /// See also [`SetPointerBarriers`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-setpointerbarriers).
980    #[doc(alias = "SetPointerBarriers")]
981    pub async fn set_pointer_barriers(
982        &self,
983        session: &Session<Self>,
984        barriers: &[Barrier],
985        zone_set: u32,
986        options: SetPointerBarriersOptions,
987    ) -> Result<Request<SetPointerBarriersResponse>, Error> {
988        self.0
989            .request(
990                &options.handle_token,
991                "SetPointerBarriers",
992                &(session, &options, barriers, zone_set),
993            )
994            .await
995    }
996
997    /// Enable input capturing.
998    ///
999    /// # Specifications
1000    ///
1001    /// See also [`Enable`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-enable).
1002    #[doc(alias = "Enable")]
1003    pub async fn enable(
1004        &self,
1005        session: &Session<Self>,
1006        options: EnableOptions,
1007    ) -> Result<(), Error> {
1008        self.0.call("Enable", &(session, &options)).await
1009    }
1010
1011    /// Disable input capturing.
1012    ///
1013    /// # Specifications
1014    ///
1015    /// See also [`Disable`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-disable).
1016    #[doc(alias = "Disable")]
1017    pub async fn disable(
1018        &self,
1019        session: &Session<Self>,
1020        options: DisableOptions,
1021    ) -> Result<(), Error> {
1022        self.0.call("Disable", &(session, &options)).await
1023    }
1024
1025    /// Release any ongoing input capture.
1026    ///
1027    /// # Specifications
1028    ///
1029    /// See also [`Release`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-release).
1030    #[doc(alias = "Release")]
1031    pub async fn release(
1032        &self,
1033        session: &Session<Self>,
1034        options: ReleaseOptions,
1035    ) -> Result<(), Error> {
1036        self.0.call("Release", &(session, &options)).await
1037    }
1038
1039    /// Connect to EIS.
1040    ///
1041    /// # Specifications
1042    ///
1043    /// See also [`ConnectToEIS`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-connecttoeis).
1044    #[doc(alias = "ConnectToEIS")]
1045    pub async fn connect_to_eis(
1046        &self,
1047        session: &Session<Self>,
1048        options: ConnectToEISOptions,
1049    ) -> Result<OwnedFd, Error> {
1050        let fd = self
1051            .0
1052            .call::<zvariant::OwnedFd>("ConnectToEIS", &(session, options))
1053            .await?;
1054        Ok(fd.into())
1055    }
1056
1057    /// Signal emitted when the application will no longer receive captured
1058    /// events.
1059    ///
1060    /// # Specifications
1061    ///
1062    /// See also [`Disabled`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-disabled).
1063    #[doc(alias = "Disabled")]
1064    pub async fn receive_disabled(&self) -> Result<impl Stream<Item = Disabled>, Error> {
1065        self.0.signal("Disabled").await
1066    }
1067
1068    /// Signal emitted when input capture starts and
1069    /// input events are about to be sent to the application.
1070    ///
1071    /// # Specifications
1072    ///
1073    /// See also [`Activated`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-activated).
1074    #[doc(alias = "Activated")]
1075    pub async fn receive_activated(&self) -> Result<impl Stream<Item = Activated>, Error> {
1076        self.0.signal("Activated").await
1077    }
1078
1079    /// Signal emitted when input capture stopped and input events
1080    /// are no longer sent to the application.
1081    ///
1082    /// # Specifications
1083    ///
1084    /// See also [`Deactivated`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-deactivated).
1085    #[doc(alias = "Deactivated")]
1086    pub async fn receive_deactivated(&self) -> Result<impl Stream<Item = Deactivated>, Error> {
1087        self.0.signal("Deactivated").await
1088    }
1089
1090    /// Signal emitted when the set of zones available to this session change.
1091    ///
1092    /// # Specifications
1093    ///
1094    /// See also [`ZonesChanged`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-zoneschanged).
1095    #[doc(alias = "ZonesChanged")]
1096    pub async fn receive_zones_changed(&self) -> Result<impl Stream<Item = ZonesChanged>, Error> {
1097        self.0.signal("ZonesChanged").await
1098    }
1099
1100    /// Supported capabilities.
1101    ///
1102    /// # Specifications
1103    ///
1104    /// See also [`SupportedCapabilities`](https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.InputCapture.html#org-freedesktop-portal-inputcapture-supportedcapabilities).
1105    #[doc(alias = "SupportedCapabilities")]
1106    pub async fn supported_capabilities(&self) -> Result<BitFlags<Capabilities>, Error> {
1107        self.0.property("SupportedCapabilities").await
1108    }
1109}
1110
1111impl std::ops::Deref for InputCapture {
1112    type Target = zbus::Proxy<'static>;
1113
1114    fn deref(&self) -> &Self::Target {
1115        &self.0
1116    }
1117}
1118
1119impl crate::Sealed for InputCapture {}
1120impl SessionPortal for InputCapture {}
1121#[cfg(feature = "clipboard")]
1122impl crate::desktop::clipboard::IsClipboardSession for InputCapture {}
1123
1124#[cfg(test)]
1125mod tests {
1126    use super::BarrierPosition;
1127
1128    #[test]
1129    fn test_barrier_position() {
1130        let pos = BarrierPosition::new(1, 2, 3, 4);
1131        assert_eq!(pos.as_tuple(), (1, 2, 3, 4));
1132        assert_eq!(pos.x1(), 1);
1133        assert_eq!(pos.y1(), 2);
1134        assert_eq!(pos.x2(), 3);
1135        assert_eq!(pos.y2(), 4);
1136
1137        let string = serde_json::to_string(&pos).unwrap();
1138        assert_eq!(string, "[1,2,3,4]");
1139
1140        let pos2 = BarrierPosition::from((1, 2, 3, 4));
1141        assert_eq!(pos, pos2);
1142    }
1143}