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}