ltk/a11y/
mod.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! 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<ActionRequest>;

/// 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<Self>
	{
		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<F>( &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<TreeUpdate>
	{
		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<ActionRequest>,
}

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 ) {}
}