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`]: remove the glow at once (push ended, crossing,
31//! pause, focus moved or the setting turned off).
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 look;
44pub use look::{Color, Landing, Look, Size, Style};
45
46#[cfg(target_os = "macos")]
47mod macos;
48#[cfg(any(target_os = "macos", test))]
49mod macos_geometry;
50#[cfg(windows)]
51mod windows;
52#[cfg(all(test, not(windows)))]
53#[path = "feedback/windows/geometry.rs"]
54mod windows_geometry;
55#[cfg(all(unix, not(target_os = "macos")))]
56mod x11;
57
58/// Commands the daemon may queue before the backend catches up.
59const COMMAND_CAPACITY: usize = 16;
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub enum Command {
63 Edge {
64 display: Bounds,
65 edge: Edge,
66 progress: u16,
67 look: Look,
68 },
69 Clear,
70 Landing {
71 x: i32,
72 y: i32,
73 look: Look,
74 },
75 LandingAtCursor {
76 look: Look,
77 },
78 Tick,
79}
80
81/// Handle to a running feedback backend.
82#[derive(Debug, Clone)]
83pub struct Feedback {
84 commands: mpsc::Sender<Command>,
85}
86
87impl Feedback {
88 /// Queues a command. A full or closed queue drops it: the input path must
89 /// never wait for the UI.
90 pub fn send(&self, command: Command) {
91 let _ = self.commands.try_send(command);
92 }
93}
94
95/// A feedback handle and the receiver its backend thread reads.
96#[must_use]
97pub fn channel() -> (Feedback, mpsc::Receiver<Command>) {
98 let (commands, receiver) = mpsc::channel(COMMAND_CAPACITY);
99 (Feedback { commands }, receiver)
100}
101
102/// Whether this session can show feedback: Windows, macOS and X11.
103#[must_use]
104pub fn supported() -> bool {
105 #[cfg(any(windows, target_os = "macos"))]
106 {
107 true
108 }
109 #[cfg(all(unix, not(target_os = "macos")))]
110 {
111 crate::input::detect_backend() == crate::input::Backend::X11
112 }
113 #[cfg(not(any(windows, unix)))]
114 {
115 false
116 }
117}
118
119/// Starts the backend, or returns `None` when this session has none.
120#[must_use]
121pub fn start() -> Option<Feedback> {
122 supported().then(platform_start).flatten()
123}
124
125/// Whether this OS can play haptic ticks.
126#[must_use]
127pub const fn haptics_supported() -> bool {
128 cfg!(target_os = "macos")
129}
130
131#[cfg(target_os = "macos")]
132use macos::start as platform_start;
133#[cfg(windows)]
134use windows::start as platform_start;
135#[cfg(all(unix, not(target_os = "macos")))]
136use x11::start as platform_start;
137#[cfg(not(any(windows, unix)))]
138fn platform_start() -> Option<Feedback> {
139 None
140}