GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(a11y): publish the accessibility tree over AT-SPI (phase 2, Linux)
backend::a11y_unix runs accesskit_unix in the Wayland shell behind the new
`a11y` feature, for an app that returns true from
Application::publishes_accessibility (or any app under CCE_A11Y=1). The
adapter's callbacks only post into the calloop loop, which publishes the
whole tree when a reader connects and after every frame (nothing is built
while none is connected), answers Focus on a widget and Click on a context
menu row, and forwards the keyboard's enter/leave as window focus.
Widgets parked off-screen or without a size are left out of the tree.
CCE_A11Y_DEBUG=1 logs connections, actions, publishes and focus changes.
Proven on cce-data-editor in a shadow session through Atspi: roles,
values, grab_focus moving the app's focus and reading back FOCUSED, Tab
steps republished; a 21-node tree builds in 50-100 us (debug build).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 18 +++--
Cargo.toml | 6 ++
docs/rfc-accessibility-locale.md | 24 ++++++
src/a11y.rs | 48 ++++++++++-
src/backend/a11y_unix.rs | 171 +++++++++++++++++++++++++++++++++++++++
src/backend/app.rs | 7 ++
src/backend/mod.rs | 2 +
src/backend/window_runner.rs | 50 +++++++++++-
8 files changed, 319 insertions(+), 7 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 000efd1..964c7e6 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1564,10 +1564,12 @@ the ladder should be supplying, and the audit counts them.
## Accessibility and locale are on the roadmap (read `docs/rfc-accessibility-locale.md`)
-There is no accessibility support yet (no AT-SPI, AccessKit, NSAccessibility or ARIA), no
-message catalogue, and the editors assume left-to-right text (`TextBox`
-assumes monospace too). The RFC has the measured state and a phased plan. Until it lands,
-two rules keep the retrofit cheap:
+Accessibility reaches Linux screen readers only, and only for an app that opts in: the
+widget tree is published over AT-SPI through AccessKit (`backend::a11y_unix`, the `a11y`
+feature, `Application::publishes_accessibility` or `CCE_A11Y=1`; phase 2, proven on
+cce-data-editor). There is nothing yet on macOS or in the browser, no message catalogue,
+and the editors assume left-to-right text (`TextBox` assumes monospace too). The RFC has
+the measured state and a phased plan. Until it lands, two rules keep the retrofit cheap:
- **A new widget declares what it is**: its `focus_role`, and a label that names it to a
person (not to a host).
@@ -1711,7 +1713,8 @@ cce-system-interface) to confirm behavior, not just the test suite.
window's `TreeUpdate` — its registered widgets (role, name, value, bounds, actions, focus;
`WidgetHost::a11y_role` / `a11y_value` are what a widget says about itself), the nodes an
app without widgets declares (`Application::accessibility`, `AppNodes`), and an open context
- menu. No platform adapter yet (`docs/rfc-accessibility-locale.md`, phase 2).
+ menu. `backend::a11y_unix` publishes it over AT-SPI (the `a11y` feature; see
+ `docs/rfc-accessibility-locale.md`, phase 2).
- `ime.rs` — input-method composition shared between the editing widget and the shell:
`Preedit`, the composition and its generation, the reported caret, the reset request
(see "Input-method composition is one model for every shell").
@@ -2192,6 +2195,11 @@ All opt-in, all read once, all quiet when unset — set one and run any client.
its grouping window (correctly — the carve's shading is baked into the plate's earlier
draw).
- `CCE_PRESENT_DEBUG=1` — swapchain present/acquire tracing.
+- `CCE_A11Y=1` — publish the accessibility tree over AT-SPI for an app that has not opted in
+ (`Application::publishes_accessibility`; needs the `a11y` feature). `CCE_A11Y_DEBUG=1` logs
+ each reader connection, action, publish (node count, focus, build time) and window-focus
+ change. A shadow window holds no keyboard until `ctl focus-window <app_id>`, and nothing
+ reads FOCUSED until it does. See `docs/rfc-accessibility-locale.md`, phase 2.
- `CCE_UI_MENU_POPUP=0` — keep the context menu in the window instead of its popup
surface (see "The context menu draws in its own popup surface").
- `CCE_VK_DEVICE=<substring>` — force a physical device; `CCE_VK_RT=0` disables ray tracing.
diff --git a/Cargo.toml b/Cargo.toml
index c95df34..9dd2fbf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -15,6 +15,11 @@ markdown = ["dep:cce-vault"]
# `widget::doc_editor` (DocEditor): the line-buffer document editor with
# Markdown live preview (cce-notes' editing mode). No extra dependencies.
doc_editor = []
+# Publish each window's accessibility tree to screen readers through AT-SPI
+# (accesskit_unix, in the Wayland shell; rfc-accessibility-locale phase 2). An
+# app must still opt in at run time (`Application::publishes_accessibility`, or
+# CCE_A11Y=1), since a workspace build turns a feature on for every app at once.
+a11y = ["dep:accesskit_unix"]
[dependencies]
# The GUI-free half: config, input, motion, units, ipc and the spec parsers,
@@ -78,6 +83,7 @@ wayland-protocols = { version = "0.31", features = ["client", "unstable"] }
wayland-backend = "0.3"
wayland-scanner = "0.31"
xkeysym = "0.2"
+accesskit_unix = { version = "0.24", optional = true }
# The AppKit shell (`src/mac`): the window, its CAMetalLayer, the run loop.
[target.'cfg(target_os = "macos")'.dependencies]
diff --git a/docs/rfc-accessibility-locale.md b/docs/rfc-accessibility-locale.md
index ab76d79..d67d609 100644
--- a/docs/rfc-accessibility-locale.md
+++ b/docs/rfc-accessibility-locale.md
@@ -117,6 +117,30 @@ focus and click actions, children in reading order, and the context's focus.
### Phase 2 — speak to the platform
+**Progress (2026-10-08): Linux works, on one app.** `backend::a11y_unix` runs `accesskit_unix`
+in the Wayland shell behind the `a11y` feature (23 crates, zbus among them; nothing without
+it), for an app that returns true from `Application::publishes_accessibility` or any app run
+with `CCE_A11Y=1`. The adapter's callbacks only post into the runner's calloop loop; the
+loop publishes the whole tree when a reader connects (an idle window renders nothing, and
+AccessKit wants it by the next refresh), after every frame (`update_if_active`: nothing is
+built while no reader is connected), and answers `Focus` on a widget (`UiContext::
+set_focused_id`) and `Click` on a context-menu row (a press where it is drawn). The window's
+keyboard enter / leave is AccessKit's window focus, so a node reads FOCUSED only while the
+window has the keyboard. Widgets parked off-screen or without a size are left out.
+`CCE_A11Y_DEBUG=1` logs each connection, action, publish (with its build time) and focus change.
+
+Proven on cce-data-editor in a shadow session through `pyatspi`'s `Atspi` (no screen reader
+is installed), with the session's `org.a11y.Status IsEnabled` set for the run: the app is
+listed with toolkit `cce-ui`; its frame holds buttons, combo boxes, entries, spin buttons
+with values, a check box, the menu bar and the tree with their roles; `grab_focus` on a
+button focuses it in the window (its ring drawn) and reads back FOCUSED; Tab steps are
+republished. A tree of 21 nodes builds in 50–100 µs in a debug build.
+
+What it showed is the apps' to fix, not the adapter's: most of the data editor's controls
+have no label (an unlabelled field is an empty name to a reader), and its inactive editors
+are registered and visible though not drawn. Next: a widget `Click` (activate), the macOS
+adapter onto the AppKit view, and listening with Orca.
+
- **Wayland / Linux:** AT-SPI over D-Bus. AccessKit's Unix adapter is the likely carrier
(evaluate it first; the alternative is a small AT-SPI server of our own). The compositor
needs nothing new: AT-SPI is a session-bus protocol between the app and the reader.
diff --git a/src/a11y.rs b/src/a11y.rs
index e1975f7..cb1e3ca 100644
--- a/src/a11y.rs
+++ b/src/a11y.rs
@@ -39,6 +39,16 @@ use crate::widget::{FocusRole, WidgetHost, WidgetId};
/// The window's node, the root every widget hangs from.
pub const WINDOW: NodeId = NodeId(0);
+/// The widget a node is, when it is a widget's ([`node_id`]'s inverse).
+pub fn widget_of(node: NodeId) -> Option<WidgetId> {
+ (node.0 > 0 && node.0 < APP_BASE).then(|| WidgetId((node.0 - 1) as usize))
+}
+
+/// The context-menu row a node is, when it is one ([`menu_row_id`]'s inverse).
+pub fn menu_row_of(node: NodeId) -> Option<usize> {
+ (node.0 > MENU.0).then(|| (node.0 - MENU.0 - 1) as usize)
+}
+
/// The open context menu's node, and the first of its rows' (`MENU + 1 + row`): far above any
/// widget's id.
pub const MENU: NodeId = NodeId(1 << 62);
@@ -160,6 +170,15 @@ pub fn widget_node(w: &dyn WidgetHost, children: Vec<NodeId>) -> Node {
node
}
+/// Whether a widget is on screen: visible, with a size, and not parked. Apps park a widget
+/// they are not showing far off the window (x or y below -9000, the toolkit's sentinel —
+/// cce-data-editor's per-type value editors) instead of hiding it; a reader must not see
+/// those either.
+fn shown_on_screen(w: &dyn WidgetHost) -> bool {
+ let (x, y, width, height) = w.rect();
+ w.visible() && width > 0.0 && height > 0.0 && x > -9000.0 && y > -9000.0
+}
+
/// A check box's or switch's value string as a state: what `value_string` writes for one.
fn truth(value: &str) -> Option<bool> {
match value.trim().to_ascii_lowercase().as_str() {
@@ -203,7 +222,7 @@ pub fn window_tree(ctx: Option<&UiContext>, app: AppNodes, title: &str, scale: f
.tree
.iter_registered()
.map(|(id, ptr)| (id, unsafe { &*ptr } as &dyn WidgetHost))
- .filter(|(_, w)| w.visible())
+ .filter(|(_, w)| shown_on_screen(*w))
.collect();
let shown: std::collections::HashSet<WidgetId> = widgets.iter().map(|(id, _)| *id).collect();
@@ -434,6 +453,33 @@ mod tests {
assert!(bar != WINDOW && bar != MENU && bar.0 > node_id(WidgetId(usize::MAX >> 4)).0, "its own id range");
}
+ #[test]
+ fn parked_and_sizeless_widgets_are_not_on_screen() {
+ let mut ctx = UiContext::new();
+ let mut parked = Owned::new(Button::new(-10_000.0, 0.0, 80.0, 24.0).with_label("Parked"));
+ let mut empty = Owned::new(Button::new(10.0, 10.0, 0.0, 0.0).with_label("Empty"));
+ let mut shown = Owned::new(Button::new(10.0, 10.0, 80.0, 24.0).with_label("Shown"));
+ ctx.register_host(&mut parked);
+ ctx.register_host(&mut empty);
+ ctx.register_host(&mut shown);
+ let update = tree_update(&ctx, "", 1.0);
+ assert_eq!(node(&update, WINDOW).children(), &[node_id(shown.base().id())][..]);
+ }
+
+ #[test]
+ fn a_node_id_names_back_what_it_was_made_from() {
+ for id in [WidgetId(0), WidgetId(1), WidgetId(123_456)] {
+ assert_eq!(widget_of(node_id(id)), Some(id));
+ }
+ for row in [0, 1, 40] {
+ assert_eq!(menu_row_of(menu_row_id(row)), Some(row));
+ assert_eq!(widget_of(menu_row_id(row)), None, "a menu row is no widget");
+ }
+ assert_eq!(widget_of(WINDOW), None);
+ assert_eq!((widget_of(MENU), menu_row_of(MENU)), (None, None));
+ assert_eq!((widget_of(AppNodes::id(5)), menu_row_of(AppNodes::id(5))), (None, None), "an app's own node is neither");
+ }
+
#[test]
fn a_widget_that_says_what_it_is_is_believed() {
assert_eq!(role_for("Button", FocusRole::Plate, Some(Role::Tab)), Role::Tab);
diff --git a/src/backend/a11y_unix.rs b/src/backend/a11y_unix.rs
new file mode 100644
index 0000000..bff4428
--- /dev/null
+++ b/src/backend/a11y_unix.rs
@@ -0,0 +1,171 @@
+//! The accessibility tree, published to screen readers on Linux: AT-SPI through
+//! `accesskit_unix` (`docs/rfc-accessibility-locale.md`, phase 2).
+//!
+//! The Wayland shell owns one [`Publisher`] per session for an app that asks for one
+//! ([`wanted`]: `Application::publishes_accessibility`, or `CCE_A11Y=1` for any app). The
+//! adapter calls back on ITS thread — a screen reader arrived, asked for something, or left —
+//! and each callback only posts an [`Event`] into the runner's loop, as an `AppSender` does;
+//! the loop does the work on its own thread:
+//!
+//! - **arrived** ([`Event::Activated`]): publish the whole tree NOW. AccessKit requires it "no
+//! later than the next display refresh, even if a frame would not normally be rendered", and
+//! an idle window renders nothing, so the runner publishes from the event itself.
+//! - **after every frame**: publish again ([`Publisher::publish`] builds nothing while no
+//! screen reader is connected — `update_if_active`).
+//! - **asked** ([`Event::Action`]): [`act`] — focus a widget, or press a context-menu row.
+//! - **the window's keyboard focus**: [`Publisher::window_focus`].
+//!
+//! Without the `a11y` feature the same API compiles to a stub whose [`Publisher::start`] is
+//! `None`, so the runner carries no `cfg`.
+
+use accesskit::{Action, ActionRequest};
+
+use crate::backend::app::Application;
+
+/// `CCE_A11Y_DEBUG=1`: log each activation, action, publish and window-focus change to stderr.
+pub fn debug() -> bool {
+ static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
+ *ON.get_or_init(|| std::env::var_os("CCE_A11Y_DEBUG").is_some())
+}
+
+/// What the adapter's thread tells the runner's loop.
+#[derive(Debug)]
+pub enum Event {
+ /// A screen reader is connected and wants the tree.
+ Activated,
+ /// It left; the adapter builds nothing until it is back.
+ Deactivated,
+ /// It asked for something.
+ Action(ActionRequest),
+}
+
+/// Whether `app` publishes its tree: it says so, or `CCE_A11Y=1` says so for any app (to
+/// try one that has not opted in).
+pub fn wanted<A: Application>(app: &A) -> bool {
+ app.publishes_accessibility() || std::env::var("CCE_A11Y").is_ok_and(|v| v == "1")
+}
+
+/// Carry out an assistive tool's request on `app`; true when something changed (redraw).
+///
+/// - **Focus** on a widget's node focuses it, through the app's `UiContext` like a Tab step.
+/// - **Click** on an open context menu's row presses it where it is drawn, so the menu runs
+/// the row's action exactly as a pointer would.
+///
+/// Anything else is not supported yet and does nothing, as AccessKit requires of an action
+/// the app cannot perform (a click on a widget is the next step).
+pub fn act<A: Application>(app: &mut A, request: &ActionRequest) -> bool {
+ use crate::widget::context_menu as cm;
+ if debug() {
+ eprintln!("[a11y] action {:?} on {:?}", request.action, request.target_node);
+ }
+ match request.action {
+ Action::Focus => {
+ let (Some(id), Some(ctx)) = (crate::a11y::widget_of(request.target_node), app.ui_context_mut()) else {
+ return false;
+ };
+ if ctx.tree.get_ptr(id).is_none() {
+ return false;
+ }
+ ctx.set_focused_id(id);
+ true
+ }
+ Action::Click => {
+ let Some(row) = crate::a11y::menu_row_of(request.target_node) else { return false };
+ if !cm::is_visible() || row >= cm::options().len() {
+ return false;
+ }
+ let (x, y) = (cm::x() + cm::w() * 0.5, cm::row_y(row) + cm::ROW_H * 0.5);
+ cm::mouse_input(crate::widget::MouseButton::Left, crate::widget::ElementState::Pressed, x, y, app.ui_context_mut());
+ true
+ }
+ _ => false,
+ }
+}
+
+#[cfg(feature = "a11y")]
+mod imp {
+ use super::Event;
+ use accesskit::{ActionHandler, ActionRequest, ActivationHandler, DeactivationHandler, TreeUpdate};
+
+ use crate::backend::app::Application;
+
+ /// The session's adapter.
+ pub struct Publisher {
+ adapter: accesskit_unix::Adapter,
+ }
+
+ struct Activation(calloop::channel::Sender<Event>);
+ impl ActivationHandler for Activation {
+ fn request_initial_tree(&mut self) -> Option<TreeUpdate> {
+ // The tree is the loop's to build; it publishes as this event arrives.
+ let _ = self.0.send(Event::Activated);
+ None
+ }
+ }
+
+ struct Actions(calloop::channel::Sender<Event>);
+ impl ActionHandler for Actions {
+ fn do_action(&mut self, request: ActionRequest) {
+ let _ = self.0.send(Event::Action(request));
+ }
+ }
+
+ struct Deactivation(calloop::channel::Sender<Event>);
+ impl DeactivationHandler for Deactivation {
+ fn deactivate_accessibility(&mut self) {
+ let _ = self.0.send(Event::Deactivated);
+ }
+ }
+
+ impl Publisher {
+ /// Register with the accessibility bus; the adapter's callbacks post to `tx`.
+ pub fn start(tx: calloop::channel::Sender<Event>) -> Option<Publisher> {
+ let adapter = accesskit_unix::Adapter::new(Activation(tx.clone()), Actions(tx.clone()), Deactivation(tx));
+ log::info!("[a11y] publishing the accessibility tree over AT-SPI");
+ Some(Publisher { adapter })
+ }
+
+ /// Hand the adapter `app`'s whole tree, if a screen reader is connected.
+ pub fn publish<A: Application>(&mut self, app: &mut A, scale: f64) {
+ self.adapter.update_if_active(|| {
+ let t0 = web_time::Instant::now();
+ let tree = crate::a11y::app_tree(app, scale);
+ if super::debug() {
+ eprintln!("[a11y] publish: {} nodes, focus {:?}, built in {:?}", tree.nodes.len(), tree.focus, t0.elapsed());
+ }
+ tree
+ });
+ }
+
+ /// The window gained or lost the keyboard.
+ pub fn window_focus(&mut self, focused: bool) {
+ if super::debug() {
+ eprintln!("[a11y] window focus: {focused}");
+ }
+ self.adapter.update_window_focus_state(focused);
+ }
+ }
+}
+
+#[cfg(not(feature = "a11y"))]
+mod imp {
+ use super::Event;
+ use crate::backend::app::Application;
+
+ /// Built without the `a11y` feature: nothing is published.
+ pub enum Publisher {}
+
+ impl Publisher {
+ pub fn start(_tx: calloop::channel::Sender<Event>) -> Option<Publisher> {
+ None
+ }
+ pub fn publish<A: Application>(&mut self, _app: &mut A, _scale: f64) {
+ match *self {}
+ }
+ pub fn window_focus(&mut self, _focused: bool) {
+ match *self {}
+ }
+ }
+}
+
+pub use imp::Publisher;
diff --git a/src/backend/app.rs b/src/backend/app.rs
index cf5ff20..4916dd8 100644
--- a/src/backend/app.rs
+++ b/src/backend/app.rs
@@ -334,6 +334,13 @@ pub trait Application: Sized + 'static {
None
}
+ /// Publish this app's accessibility tree to screen readers (AT-SPI on Wayland), with
+ /// cce-ui built with its `a11y` feature. Default false while the adapter is proven
+ /// (`docs/rfc-accessibility-locale.md`, phase 2); `CCE_A11Y=1` turns it on for any app.
+ fn publishes_accessibility(&self) -> bool {
+ false
+ }
+
/// What the app shows that its [`ui_context`](Self::ui_context) does not: the nodes of
/// an accessibility tree, for an app that draws without widgets (a status bar module, a
/// terminal, a map) or draws parts of its window itself. Pushed as AccessKit nodes into
diff --git a/src/backend/mod.rs b/src/backend/mod.rs
index 9462179..d56ac90 100644
--- a/src/backend/mod.rs
+++ b/src/backend/mod.rs
@@ -9,6 +9,8 @@ pub mod text;
pub mod touch;
// The Wayland shell: native, but for macOS.
#[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
+pub mod a11y_unix;
+#[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
pub mod dnd;
#[cfg(not(any(target_arch = "wasm32", target_os = "macos")))]
pub mod menu_popup;
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index b6df778..693787c 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -63,6 +63,9 @@ fn debug_clock_ms() -> u128 {
}
pub struct EngineState<A: Application> {
+ /// The session's accessibility publisher, for an app that publishes its tree
+ /// (`backend::a11y_unix`).
+ pub a11y: Option<crate::backend::a11y_unix::Publisher>,
pub registry_state: RegistryState,
pub compositor_state: CompositorState,
pub xdg_shell_state: XdgShell,
@@ -556,6 +559,11 @@ impl<A: Application> EngineState<A> {
self.damage_owed = false;
}
self.sync_text_input();
+ // The tree after the frame that may have changed it; nothing is built while no
+ // screen reader is connected.
+ if let (Some(publisher), Some(app)) = (self.a11y.as_mut(), self.inner.as_mut()) {
+ publisher.publish(app, crate::scale::scale_factor() as f64);
+ }
}
/// Bring the text input in step with the frame just built: enabled at
@@ -1324,6 +1332,9 @@ impl<A: Application> KeyboardHandler for EngineState<A> {
_raw_modifiers: &[u32],
_keysyms: &[xkeysym::Keysym],
) {
+ if let Some(publisher) = self.a11y.as_mut() {
+ publisher.window_focus(true);
+ }
let (driver, t) = self.turn();
driver.keyboard_focus(t, true);
}
@@ -1336,6 +1347,9 @@ impl<A: Application> KeyboardHandler for EngineState<A> {
_surface: &wl_surface::WlSurface,
_serial: u32,
) {
+ if let Some(publisher) = self.a11y.as_mut() {
+ publisher.window_focus(false);
+ }
let (driver, t) = self.turn();
driver.keyboard_focus(t, false);
}
@@ -1892,6 +1906,9 @@ pub fn run<A: Application>() {
// loop outlives a reconnect while the EngineState does not.
let (drop_tx, drop_rx) =
calloop::channel::channel::<crate::backend::dnd::DroppedData>();
+ // The accessibility adapter's callbacks (`backend::a11y_unix`), from its own thread;
+ // registered once, as the loop outlives a reconnect.
+ let (a11y_tx, a11y_rx) = calloop::channel::channel::<crate::backend::a11y_unix::Event>();
let mut event_loop = match EventLoop::try_new() {
Ok(l) => l,
Err(e) => {
@@ -1932,6 +1949,32 @@ pub fn run<A: Application>() {
})
.unwrap();
+ event_loop
+ .handle()
+ .insert_source(a11y_rx, |event, _metadata, app_state: &mut EngineState<A>| {
+ use crate::backend::a11y_unix::{act, Event};
+ let calloop::channel::Event::Msg(event) = event else { return };
+ let (Some(publisher), Some(app)) = (app_state.a11y.as_mut(), app_state.inner.as_mut()) else { return };
+ match event {
+ // Published from here, not at the next frame: an idle window renders none,
+ // and AccessKit wants the tree by the next refresh.
+ Event::Activated => {
+ if crate::backend::a11y_unix::debug() {
+ eprintln!("[a11y] a screen reader connected");
+ }
+ publisher.publish(app, crate::scale::scale_factor() as f64)
+ }
+ Event::Deactivated => {}
+ Event::Action(request) => {
+ if act(app, &request) {
+ app_state.redraw = true;
+ publisher.publish(app, crate::scale::scale_factor() as f64);
+ }
+ }
+ }
+ })
+ .unwrap();
+
let mut app: Option<A> = None;
let mut sources_registered = false;
let mut attempt: u32 = 0;
@@ -1939,7 +1982,7 @@ pub fn run<A: Application>() {
loop {
let started = std::time::Instant::now();
let (returned_app, end) =
- run_session(&mut event_loop, sender.clone(), drop_tx.clone(), app.take(), !sources_registered);
+ run_session(&mut event_loop, sender.clone(), drop_tx.clone(), a11y_tx.clone(), app.take(), !sources_registered);
app = returned_app;
sources_registered = true;
@@ -1990,6 +2033,7 @@ fn run_session<'l, A: Application>(
event_loop: &mut EventLoop<'l, EngineState<A>>,
sender: calloop::channel::Sender<A::Message>,
drop_tx: calloop::channel::Sender<crate::backend::dnd::DroppedData>,
+ a11y_tx: calloop::channel::Sender<crate::backend::a11y_unix::Event>,
existing_app: Option<A>,
register_app_sources: bool,
) -> (Option<A>, SessionEnd) {
@@ -2020,6 +2064,7 @@ fn run_session<'l, A: Application>(
let text_input_manager: Option<ZwpTextInputManagerV3> = globals.bind(&qh, 1..=1, ()).ok();
let mut engine_state = EngineState {
+ a11y: None,
data_device_manager: DataDeviceManagerState::bind(&globals, &qh).ok(),
data_devices: Vec::new(),
drag_mime: None,
@@ -2107,6 +2152,9 @@ fn run_session<'l, A: Application>(
Some(app) => app,
None => A::create(AppSender::from(engine_state.sender.clone())),
};
+ if crate::backend::a11y_unix::wanted(&inner) {
+ engine_state.a11y = crate::backend::a11y_unix::Publisher::start(a11y_tx);
+ }
let settings = inner.settings();
crate::scale::set_app_id(settings.app_id.clone());
engine_state.is_status_bar = settings.app_id.starts_with("cce-status");