// SPDX-License-Identifier: LGPL-2.1-only // Copyright (C) 2026 Liberux Labs, S. L. //! AT-SPI2 accessibility via AccessKit. //! //! ltk does not implement the D-Bus AT-SPI2 protocol itself — that is //! delegated to [`accesskit_unix`], which translates a backend-neutral //! [`accesskit::TreeUpdate`] into the corresponding D-Bus calls, //! registers the application against `org.a11y.Bus`, and emits the //! property / state / focus change signals Orca and friends listen //! for. Our job is two-way: //! //! 1. **Outbound**: every frame, after the layout pass has populated //! `widget_rects`, build a fresh [`accesskit::TreeUpdate`] from //! those rects and push it into the adapter through //! [`A11yState::update`]. The runtime calls this from the same //! place that runs `draw_frame`, so the accessible tree is always //! in sync with what the user can see. //! //! 2. **Inbound**: an [`accesskit::ActionRequest`] from an assistive //! technology (Orca pressing the "Default Action" on a button, a //! switch-control device focusing the next node, …) lands on the //! [`ActionForwarder`] running inside the adapter's worker //! thread; the forwarder pushes it through an `mpsc::Sender` into //! the run loop, which drains the channel each iteration and //! translates the request into a synthetic press / focus on the //! target widget. //! //! The adapter is constructed unconditionally — `accesskit_unix` //! never refuses creation, it just stays inactive when no AT client //! attaches to the session bus. Nothing else in the pipeline reads //! the field except the per-frame tree update and the per-iteration //! action inbox drain, so the rest of the runtime is unaware of //! whether an AT is listening or not. pub( crate ) mod tree; use std::sync::mpsc; use accesskit::{ ActionRequest, Rect as A11yRect, TreeUpdate }; /// Channel carrying inbound action requests from the AccessKit /// adapter thread to the run loop. pub( crate ) type ActionInbox = mpsc::Receiver; /// Top-level accessibility state owned by `AppData`. Holds the /// platform adapter alongside the receiver half of the action /// channel. pub( crate ) struct A11yState { adapter: accesskit_unix::Adapter, pub( crate ) action_rx: ActionInbox, } impl A11yState { /// Bring up the AccessKit adapter. Returns `None` only when we /// reserve the right to refuse creation in the future (currently /// always `Some` — `accesskit_unix::Adapter::new` is infallible /// and stays inactive when no AT client is attached). Keeping /// the `Option` lets us toggle accessibility off at runtime via /// an env var or feature flag without re-plumbing every call /// site. pub( crate ) fn try_new( _app_name: &str, _app_id: &str ) -> Option { let ( action_tx, action_rx ) = mpsc::channel(); let adapter = accesskit_unix::Adapter::new( ActivationStub, ActionForwarder { tx: action_tx }, DeactivationStub, ); Some( Self { adapter, action_rx } ) } /// Push a freshly-built tree into the adapter. The closure is /// invoked synchronously **only** when AT-SPI2 is currently /// observing the application (an AT client connected to the /// daemon and queried us at least once), so the cost of /// constructing the tree is paid only when something is actually /// listening. pub( crate ) fn update( &mut self, factory: F ) where F: FnOnce() -> TreeUpdate, { self.adapter.update_if_active( factory ); } /// AT-SPI clients filter by window focus; without it Orca skips /// the surface entirely. pub( crate ) fn set_window_focus( &mut self, focused: bool ) { self.adapter.update_window_focus_state( focused ); } pub( crate ) fn set_window_bounds( &mut self, width: f64, height: f64 ) { let r = A11yRect { x0: 0.0, y0: 0.0, x1: width, y1: height }; self.adapter.set_root_window_bounds( r, r ); } #[ allow( dead_code ) ] pub( crate ) fn notify_focus( &mut self, new_focus: accesskit::NodeId ) { self.adapter.update_if_active( || { TreeUpdate { nodes: vec![], tree: None, focus: new_focus, } } ); } } /// Activation handler the adapter calls when an assistive technology /// first attaches and asks for the initial tree. We hand back an /// empty root — the first proper update comes from the run loop on /// the very next frame, which is the natural moment to materialise a /// real tree (the widget_rects are populated by then). struct ActivationStub; impl accesskit::ActivationHandler for ActivationStub { fn request_initial_tree( &mut self ) -> Option { Some( tree::empty_root() ) } } /// Action handler that simply forwards every incoming request to the /// main event loop. The translation from `ActionRequest` to a /// synthetic press / focus happens on the main thread so it can /// touch the rest of `AppData` without locking. struct ActionForwarder { tx: mpsc::Sender, } impl accesskit::ActionHandler for ActionForwarder { fn do_action( &mut self, request: ActionRequest ) { let _ = self.tx.send( request ); } } /// Deactivation handler. AccessKit invokes this when every AT client /// detaches; we have no per-client cleanup so it's a no-op. struct DeactivationStub; impl accesskit::DeactivationHandler for DeactivationStub { fn deactivate_accessibility( &mut self ) {} }