teksilo_platform/key_report.rs
1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! Telling a screen reader about each key, where it cannot see them itself.
5//!
6//! A screen reader has to know the key behind every change it hears about:
7//! Orca decides whether a caret move is read as a character, a word or a line
8//! from the last key it saw (`_presentTextAtNewCaretPosition` in
9//! `orca/scripts/default.py`), echoes typed keys, and runs its own commands
10//! (Orca+T, which says the time, and the rest) from keys the application must
11//! then never see.
12//!
13//! On Linux under Wayland it cannot read the keyboard. libatspi 2.52 gives it
14//! the *legacy* keyboard device there, which learns of a key only when the
15//! application that received it reports it to the AT-SPI registry: a call to
16//! `org.a11y.atspi.DeviceEventController.NotifyListenersSync` on the
17//! accessibility bus, whose `bool` answer says whether a listener (Orca,
18//! running one of its commands) took the key. GTK 3's bridge
19//! (`spi_atk_bridge_key_listener` in at-spi2-core's `atk-adaptor/event.c`)
20//! and Qt's (`QSpiApplicationAdaptor::eventFilter`) make that call for every
21//! key they handle, and neither delivers a key a listener took.
22//! `accesskit_unix` makes no such call, so before this module a Teksilo
23//! window in a Wayland session left Orca deaf to every key: no key echo, no
24//! caret speech, no Orca command.
25//!
26//! [`KeyReportGate`] is that call, placed where `teksilo-app` receives winit's
27//! `KeyboardInput`, before anything acts on the key:
28//!
29//! - **When.** Only while an assistive technology is attached, the same
30//! condition under which `accesskit_unix` exposes the tree at all
31//! ([`PlatformWindow::accessibility_active`](crate::PlatformWindow::accessibility_active)).
32//! On an X11 session Orca reads the keyboard itself and registers no
33//! listener, so the registry answers at once that nobody took the key: the
34//! cost of what GTK and Qt already do there.
35//! - **What.** Every physical press and release, a held key's repeats as
36//! presses (as GTK reports each autorepeat event). Not winit's *synthetic*
37//! events, which X11 makes up for keys already held when a window gains or
38//! loses focus: nobody pressed them, and they are delivered untouched.
39//! - **The struct.** `(uiiiisb)`: press 0 / release 1, the X keysym, the X
40//! keycode (the evdev code + 8), the X modifier mask, a time in
41//! milliseconds, the text the key types, and whether it types it (the
42//! crate's `DeviceEvent`). winit reports no lock state, so Num Lock is learnt from
43//! the keys that show it (a keypad digit: on; a keypad motion: off; the Num
44//! Lock key toggles what is known) and carried as Mod2 on every key while
45//! known on; until a key has shown it, it is taken as off.
46//! - **Consumed keys.** A press a listener took is not delivered, and neither
47//! is its release: the release always follows its press, whatever the
48//! registry says about the release itself, so the application never sees a
49//! key go down without coming up or the other way round.
50//! - **Time.** A press waits for the registry's answer, as GTK waits. A
51//! release does not: whether it is delivered is already decided by its
52//! press, and the next press waits behind it anyway, since reports are sent
53//! one at a time, in order.
54//! - **Releases are never lost.** The release of a key whose press was
55//! reported is reported too, whatever window it comes up in (a window whose
56//! adapter a reader has not reached yet included), and never turned away
57//! for lack of room: libatspi's legacy device sets the Orca modifier
58//! (Insert, Caps Lock) from the press and clears it from the release, and a
59//! lost release leaves every later key looking like an Orca command.
60//! Presses behind a registry that hangs are bounded (64) and dropped
61//! unsent once a second old, rather than replayed when it wakes.
62//! - **No registry, or a slow one.** The wait is bounded (200 ms, between Qt's
63//! 100 ms and GTK 3's 500 ms), and a press that runs out of it is
64//! delivered. Until that report is finally answered, the presses that follow
65//! are reported without waiting. So a registry that stops answering costs
66//! one 200 ms pause, however many keys follow; one that answers every
67//! report, but each more than 200 ms late, costs up to 200 ms on every
68//! press made after it has answered the one before. A missing bus or
69//! registry is answered at once, and asked again at most every few
70//! seconds. The D-Bus traffic runs on a thread of its own; the event loop
71//! only ever waits on a channel.
72//! - **Secure fields.** While a secure field has focus (one that declares
73//! `ImePurpose::Password`, as every `PasswordField` does, revealed or not),
74//! a key that types text (a character, a dead key, Space) is reported with
75//! no text and the keysym `VoidSymbol`, press and release; every other key
76//! is reported in full. Its physical keycode and modifiers are still
77//! reported, because Orca matches its own commands on them, so a process
78//! listening to the registry can still tell which physical keys were
79//! pressed there. GTK 3.24 (`atk_key_event_from_gdk_event_key`) and Qt 6.8
80//! (`QSpiApplicationAdaptor::eventFilter`) report such keys in full,
81//! character included. Orca needs none of it there: it echoes nothing in a
82//! password field (`presentKeyboardEvent`) and obscures those keys in its
83//! own log.
84//! - **Input methods.** A key an input method takes never reaches Teksilo as a
85//! `KeyboardInput`, on either Linux backend: on Wayland the compositor gives
86//! it to the input method before the application, and on X11 winit drops
87//! every event `XFilterEvent` claims. So those keys are not reported, as
88//! they are not by a GTK application on Wayland; the composition itself
89//! reaches the reader as text changes.
90//!
91//! On Windows and macOS the gate reports nothing and delivers every key: their
92//! screen readers read the keyboard themselves.
93
94use std::time::Instant;
95
96use winit::event::ElementState;
97use winit::keyboard::{Key, KeyLocation, ModifiersState, NamedKey, PhysicalKey};
98
99#[cfg(all(unix, not(target_os = "macos")))]
100mod atspi;
101mod keysym;
102#[cfg(test)]
103mod tests;
104#[cfg(all(unix, not(target_os = "macos")))]
105mod worker;
106
107/// Whether a key event is a press or a release, as the registry numbers them
108/// (`ATSPI_KEY_PRESSED_EVENT`, `ATSPI_KEY_RELEASED_EVENT`).
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub(crate) enum DeviceEventKind {
111 /// A key went down, or repeated while held.
112 Pressed = 0,
113 /// A key came up.
114 Released = 1,
115}
116
117/// One key event as the AT-SPI registry reads it: the `(uiiiisb)` struct of
118/// `NotifyListenersSync` (Qt's `QSpiDeviceEvent`; the registry's own
119/// introspection XML says `(uiuuisb)`, but its demarshaller reads this).
120#[derive(Debug, Clone, PartialEq, Eq)]
121pub(crate) struct DeviceEvent {
122 /// Press or release.
123 pub kind: DeviceEventKind,
124 /// The X keysym of the key, at the shift level it was pressed at: `0x61`
125 /// for `a`, `0x41` for Shift+`a`, `0xff54` for Down.
126 pub keysym: u32,
127 /// The X keycode: the Linux evdev code plus 8. Orca matches its own
128 /// commands on this.
129 pub hw_code: u32,
130 /// The X modifier mask held with the key: Shift `1 << 0`, Caps Lock
131 /// `1 << 1`, Control `1 << 2`, Alt (Mod1) `1 << 3`, Num Lock (Mod2)
132 /// `1 << 4`, Super (Mod4) `1 << 6`.
133 pub modifiers: u32,
134 /// Milliseconds on a clock of the reporter's own.
135 pub timestamp: u32,
136 /// The text of the key when it has printable text, else empty; a reader
137 /// names a key from its keysym when this is empty.
138 pub event_string: String,
139 /// Whether the key types its text: it has some, and no Control, Alt or
140 /// Super is held.
141 pub is_text: bool,
142}
143
144impl DeviceEvent {
145 /// The event for `key`, with the keyboard in `keyboard`, Num Lock known
146 /// to be on or not, at `timestamp`. When `withhold`, a key that types
147 /// text (a secure field has focus) is reported with no text and
148 /// `VoidSymbol`: its keycode and modifiers only.
149 pub(crate) fn for_key(
150 key: &KeyInput<'_>,
151 keyboard: KeyboardState,
152 num_lock: bool,
153 withhold: bool,
154 timestamp: u32,
155 ) -> Self {
156 let modifiers = keyboard.modifiers;
157 let withheld = withhold && keysym::types_text(key.logical_key);
158 let event_string = if withheld {
159 String::new()
160 } else {
161 keysym::event_string(key.logical_key)
162 };
163 let chorded = modifiers.control_key() || modifiers.alt_key() || modifiers.super_key();
164 Self {
165 kind: match key.state {
166 ElementState::Pressed => DeviceEventKind::Pressed,
167 ElementState::Released => DeviceEventKind::Released,
168 },
169 keysym: if withheld {
170 keysym::VOID_SYMBOL
171 } else {
172 keysym::keysym(key.logical_key, key.location, modifiers.shift_key())
173 },
174 hw_code: keysym::hardware_keycode(key.physical_key),
175 modifiers: keysym::modifier_mask(keyboard, num_lock),
176 timestamp,
177 is_text: !event_string.is_empty() && !chorded,
178 event_string,
179 }
180 }
181}
182
183/// A key event as the reporter needs it: the fields of winit's `KeyEvent`
184/// (which only winit can build) that say which key it was, plus the
185/// `is_synthetic` flag of the `WindowEvent` that carried it.
186#[derive(Debug, Clone, Copy)]
187pub struct KeyInput<'a> {
188 /// The key's place on the keyboard.
189 pub physical_key: PhysicalKey,
190 /// What the key means with the current layout and shift level.
191 pub logical_key: &'a Key,
192 /// Which of two keys of the same meaning it was: left or right, main
193 /// block or keypad.
194 pub location: KeyLocation,
195 /// Pressed or released. A repeat is a press.
196 pub state: ElementState,
197 /// A press the keyboard repeated because the key is held.
198 pub repeat: bool,
199 /// winit made this event up, for a key already held when focus changed.
200 pub synthetic: bool,
201}
202
203impl<'a> KeyInput<'a> {
204 /// The key of a winit `KeyboardInput`.
205 pub fn from_winit(event: &'a winit::event::KeyEvent, is_synthetic: bool) -> Self {
206 Self {
207 physical_key: event.physical_key,
208 logical_key: &event.logical_key,
209 location: event.location,
210 state: event.state,
211 repeat: event.repeat,
212 synthetic: is_synthetic,
213 }
214 }
215}
216
217/// What else is true of the keyboard when a key event arrives.
218#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
219pub struct KeyboardState {
220 /// The modifiers held, as winit last reported them.
221 pub modifiers: ModifiersState,
222 /// Whether Caps Lock is on, as far as the application knows.
223 pub caps_lock: bool,
224}
225
226/// Where a key lands, as far as reporting it goes.
227#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
228pub struct KeyTarget {
229 /// An assistive technology is attached to the window the key came to
230 /// ([`PlatformWindow::accessibility_active`](crate::PlatformWindow::accessibility_active)).
231 pub reader_attached: bool,
232 /// The focused widget is a secure text field (it declares
233 /// `ImePurpose::Password`, as a `PasswordField` does).
234 pub secure_field: bool,
235}
236
237/// What the registry said about a reported key press.
238#[derive(Debug, Clone, Copy, PartialEq, Eq)]
239// Only the Linux reporter and the tests answer anything but `Unanswered`.
240#[cfg_attr(not(all(unix, not(target_os = "macos"))), allow(dead_code))]
241pub(crate) enum ReportOutcome {
242 /// A listener took the key for itself; the application must not see it.
243 Consumed,
244 /// The registry answered, and nobody took the key.
245 NotConsumed,
246 /// No answer in time, or nobody to ask. The key is the application's.
247 Unanswered,
248}
249
250/// Where key events are reported. The platform's is the AT-SPI registry on
251/// Linux; a test hands the gate one of its own.
252pub(crate) trait KeyEventReporter {
253 /// Report a key press (or repeat) and wait, briefly, for the answer.
254 fn report_press(&mut self, event: &DeviceEvent) -> ReportOutcome;
255
256 /// Report a key release. Nothing waits for its answer.
257 fn report_release(&mut self, event: &DeviceEvent);
258}
259
260/// Whether a key event goes on to the application.
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
262pub enum KeyDisposition {
263 /// Dispatch it as usual.
264 Deliver,
265 /// An assistive technology took it: drop it.
266 Drop,
267}
268
269/// Reports each key to the platform's assistive technology before the
270/// application acts on it, and says which keys the application must not see.
271/// See the [module documentation](self).
272pub struct KeyReportGate {
273 reporter: Option<Box<dyn KeyEventReporter>>,
274 /// Builds the platform's reporter the first time one is needed.
275 start: Option<fn() -> Box<dyn KeyEventReporter>>,
276 /// The keys held down, one entry per key from its first press to its
277 /// release. A handful at most.
278 held: Vec<Hold>,
279 /// Whether Num Lock is on, once a key has shown it; `None` until then.
280 /// See [`keysym::num_lock_shown`].
281 num_lock: Option<bool>,
282 /// The origin of [`DeviceEvent::timestamp`].
283 epoch: Instant,
284}
285
286/// What the gate remembers of one key from its first press to its release.
287#[derive(Debug, Clone)]
288struct Hold {
289 key: PhysicalKey,
290 /// Whether any press of this hold (the first, or a repeat) reached the
291 /// application. Its release is delivered exactly when one did.
292 delivered: bool,
293 /// Whether any press of this hold was reported. Its release then is,
294 /// whatever window it comes up in.
295 reported: bool,
296 /// Whether a press of this hold landed in a secure field. Its release
297 /// then keeps the character to itself too, wherever focus has gone.
298 withheld: bool,
299 /// The keysym, text and `is_text` the last reported press of this hold
300 /// carried, which its release carries too: winit composes on presses
301 /// only (the key that completes a dead-key sequence is `é` down and `e`
302 /// up), and Shift can come or go during a hold, while the release is of
303 /// the key that went down. Orca pairs a release with its press by them.
304 named: Option<(u32, String, bool)>,
305}
306
307impl std::fmt::Debug for KeyReportGate {
308 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
309 f.debug_struct("KeyReportGate")
310 .field("started", &self.reporter.is_some())
311 .finish_non_exhaustive()
312 }
313}
314
315impl Default for KeyReportGate {
316 fn default() -> Self {
317 Self::for_platform()
318 }
319}
320
321impl KeyReportGate {
322 /// The gate for this platform. It starts nothing until an assistive
323 /// technology is attached; on Windows and macOS it never reports.
324 pub fn for_platform() -> Self {
325 #[cfg(all(unix, not(target_os = "macos")))]
326 let start: Option<fn() -> Box<dyn KeyEventReporter>> = Some(atspi::start_reporter);
327 #[cfg(not(all(unix, not(target_os = "macos"))))]
328 let start: Option<fn() -> Box<dyn KeyEventReporter>> = None;
329 Self {
330 reporter: None,
331 start,
332 held: Vec::new(),
333 num_lock: None,
334 epoch: Instant::now(),
335 }
336 }
337
338 /// A gate that reports to `reporter`.
339 #[cfg(test)]
340 pub(crate) fn with_reporter(reporter: impl KeyEventReporter + 'static) -> Self {
341 Self {
342 reporter: Some(Box::new(reporter)),
343 start: None,
344 held: Vec::new(),
345 num_lock: None,
346 epoch: Instant::now(),
347 }
348 }
349
350 /// Start the platform's reporter now, so that it is connected before the
351 /// first key. Call it once an assistive technology is attached; it does
352 /// nothing after the first call.
353 pub fn warm_up(&mut self) {
354 self.reporter();
355 }
356
357 fn reporter(&mut self) -> Option<&mut (dyn KeyEventReporter + 'static)> {
358 if self.reporter.is_none()
359 && let Some(start) = self.start.take()
360 {
361 self.reporter = Some(start());
362 }
363 self.reporter.as_deref_mut()
364 }
365
366 /// Report `key` if an assistive technology is attached where it lands
367 /// (`target`), and say whether the application gets it.
368 ///
369 /// A press is dropped when a listener took it. A release goes where the
370 /// presses of its hold went, whatever anyone says about the release, and
371 /// whether or not a reader is still attached: it is delivered when any
372 /// press of the hold was (the first press, or a repeat), so the
373 /// application sees a key come up exactly when it saw it go down. A new
374 /// press, as opposed to a repeat, starts a new hold. A synthetic event is
375 /// neither reported nor dropped.
376 pub fn filter(
377 &mut self,
378 key: &KeyInput<'_>,
379 keyboard: KeyboardState,
380 target: KeyTarget,
381 ) -> KeyDisposition {
382 let at_active = target.reader_attached;
383 if key.synthetic {
384 return KeyDisposition::Deliver;
385 }
386 let pressed = key.state == ElementState::Pressed;
387 if pressed && let Some(on) = keysym::num_lock_shown(key.logical_key, key.location) {
388 self.num_lock = Some(on);
389 }
390 let num_lock = self.num_lock == Some(true);
391 let epoch = self.epoch;
392 // Read only for a report. Wraps after 49 days, as X server time does.
393 let event = |key: &KeyInput<'_>, withhold: bool| {
394 let timestamp = epoch.elapsed().as_millis() as u32;
395 DeviceEvent::for_key(key, keyboard, num_lock, withhold, timestamp)
396 };
397 let secure = target.secure_field;
398 match key.state {
399 ElementState::Pressed => {
400 let reporter = if at_active { self.reporter() } else { None };
401 let reported = reporter.is_some();
402 let mut named = None;
403 let outcome = reporter.map_or(ReportOutcome::Unanswered, |reporter| {
404 let press = event(key, secure);
405 let outcome = reporter.report_press(&press);
406 named = Some((press.keysym, press.event_string, press.is_text));
407 outcome
408 });
409 // The Num Lock key's own event carries the state before it.
410 if *key.logical_key == Key::Named(NamedKey::NumLock) && !key.repeat {
411 self.num_lock = self.num_lock.map(|on| !on);
412 }
413 let delivered = outcome != ReportOutcome::Consumed;
414 match self
415 .held
416 .iter_mut()
417 .find(|hold| hold.key == key.physical_key)
418 {
419 Some(hold) if key.repeat => {
420 hold.delivered |= delivered;
421 hold.reported |= reported;
422 hold.withheld |= secure;
423 hold.named = named.or(hold.named.take());
424 }
425 Some(hold) => {
426 hold.delivered = delivered;
427 hold.reported = reported;
428 hold.withheld = secure;
429 hold.named = named;
430 }
431 None => self.held.push(Hold {
432 key: key.physical_key,
433 delivered,
434 reported,
435 withheld: secure,
436 named,
437 }),
438 }
439 if delivered {
440 KeyDisposition::Deliver
441 } else {
442 KeyDisposition::Drop
443 }
444 }
445 ElementState::Released => {
446 let hold = self
447 .held
448 .iter()
449 .position(|hold| hold.key == key.physical_key)
450 .map(|index| self.held.swap_remove(index));
451 // The registry heard the press, so it hears the release, even
452 // in a window whose adapter no reader has reached yet:
453 // libatspi's legacy device sets the Orca modifier from the one
454 // and clears it from the other.
455 if (at_active || hold.as_ref().is_some_and(|hold| hold.reported))
456 && let Some(reporter) = self.reporter()
457 {
458 let withhold = secure || hold.as_ref().is_some_and(|hold| hold.withheld);
459 let mut release = event(key, withhold);
460 if !withhold
461 && let Some((keysym, text, is_text)) =
462 hold.as_ref().and_then(|hold| hold.named.clone())
463 {
464 release.keysym = keysym;
465 release.event_string = text;
466 release.is_text = is_text;
467 }
468 reporter.report_release(&release);
469 }
470 // A release with no press seen here (the key went down in
471 // another application) is the application's.
472 if hold.is_none_or(|hold| hold.delivered) {
473 KeyDisposition::Deliver
474 } else {
475 KeyDisposition::Drop
476 }
477 }
478 }
479 }
480}