Skip to main content

a7n_platform/
feedback.rs

1// SPDX-License-Identifier: GPL-3.0-or-later
2
3//! Visual and haptic feedback while the pointer pushes against a screen edge
4//! and after it lands on this machine.
5//!
6//! # Backend contract
7//!
8//! - [`start`] runs once at daemon startup on a tokio thread. A backend creates
9//!   its channel with [`channel`], moves the receiver to its own OS UI thread
10//!   and returns the [`Feedback`]. It returns `None` when it cannot start;
11//!   it logs nothing and never panics.
12//! - The UI thread owns every window or OS object. Commands arrive on a
13//!   bounded channel (read it with `blocking_recv`). The sender never blocks
14//!   and drops commands when the thread is behind, so the backend must treat
15//!   every command as absolute state, never as a delta.
16//! - Never steal focus, never take input, and make every window click-through
17//!   and invisible to screen capture where the OS allows it. Nothing may
18//!   appear in the task bar, dock or window switcher.
19//! - Honouring the OS reduce-motion setting is the backend's job: with it on,
20//!   show the same state without animation (for example a static edge strip
21//!   and a landing marker that does not pulse).
22//! - Never log positions or progress.
23//! - Commands:
24//!   - [`Command::Edge`]: draw the push glow along `edge` of the display
25//!     `display` (desktop coordinates; the daemon sends the bounds of the
26//!     whole local desktop it captures or injects in, so a backend that knows
27//!     per-monitor bounds clamps the glow to the monitor containing the
28//!     pointer). `progress` runs 0..=1000 per-mille of the resistance that
29//!     is built up; the glow grows with it. Repeated commands update it.
30//!   - [`Command::Clear`]: drain an abandoned push.
31//!   - [`Command::Crossed`]: play the departure finish after breakthrough.
32//!   - [`Command::Landing`]: briefly mark the desktop point `(x, y)` where the
33//!     pointer landed, then fade out on its own.
34//!   - [`Command::LandingAtCursor`]: mark the current OS cursor position.
35//!   - [`Command::Tick`]: one trackpad haptic tick. The daemon only sends it
36//!     when the user enabled haptics; backends without haptics ignore it.
37
38use a7n_proto::v1::Edge;
39use tokio::sync::mpsc;
40
41use crate::input::Bounds;
42
43mod effects;
44mod look;
45mod palette;
46pub mod parameters;
47#[cfg(feature = "fx-preview")]
48pub mod preview;
49mod raster;
50mod timeline;
51pub use look::{Color, Landing, Look, Size, Style};
52
53#[cfg(target_os = "macos")]
54mod macos;
55#[cfg(any(target_os = "macos", test))]
56mod macos_geometry;
57#[cfg(windows)]
58mod windows;
59#[cfg(all(test, not(windows)))]
60#[path = "feedback/windows/geometry.rs"]
61mod windows_geometry;
62#[cfg(all(unix, not(target_os = "macos")))]
63mod x11;
64
65/// Commands the daemon may queue before the backend catches up.
66const COMMAND_CAPACITY: usize = 16;
67
68#[derive(Debug, Clone, Copy, PartialEq)]
69pub enum Command {
70    Edge {
71        display: Bounds,
72        edge: Edge,
73        progress: u16,
74        look: Look,
75    },
76    Clear,
77    Crossed,
78    Landing {
79        x: i32,
80        y: i32,
81        look: Look,
82    },
83    LandingAtCursor {
84        look: Look,
85    },
86    Tick,
87}
88
89/// Handle to a running feedback backend.
90#[derive(Debug, Clone)]
91pub struct Feedback {
92    commands: mpsc::Sender<Command>,
93}
94
95impl Feedback {
96    /// Queues a command. A full or closed queue drops it: the input path must
97    /// never wait for the UI.
98    pub fn send(&self, command: Command) {
99        let _ = self.commands.try_send(command);
100    }
101}
102
103/// A feedback handle and the receiver its backend thread reads.
104#[must_use]
105pub fn channel() -> (Feedback, mpsc::Receiver<Command>) {
106    let (commands, receiver) = mpsc::channel(COMMAND_CAPACITY);
107    (Feedback { commands }, receiver)
108}
109
110/// Whether this session can show feedback: Windows, macOS and X11.
111#[must_use]
112pub fn supported() -> bool {
113    #[cfg(any(windows, target_os = "macos"))]
114    {
115        true
116    }
117    #[cfg(all(unix, not(target_os = "macos")))]
118    {
119        crate::input::detect_backend() == crate::input::Backend::X11
120    }
121    #[cfg(not(any(windows, unix)))]
122    {
123        false
124    }
125}
126
127/// Starts the backend, or returns `None` when this session has none.
128#[must_use]
129pub fn start() -> Option<Feedback> {
130    supported().then(platform_start).flatten()
131}
132
133/// Whether this OS can play haptic ticks.
134#[must_use]
135pub const fn haptics_supported() -> bool {
136    cfg!(target_os = "macos")
137}
138
139#[cfg(target_os = "macos")]
140use macos::start as platform_start;
141#[cfg(windows)]
142use windows::start as platform_start;
143#[cfg(all(unix, not(target_os = "macos")))]
144use x11::start as platform_start;
145#[cfg(not(any(windows, unix)))]
146fn platform_start() -> Option<Feedback> {
147    None
148}