|
| 1 | +# How AccessKit Works |
| 2 | + |
| 3 | +AccessKit provides a cross-platform abstraction over native accessibility APIs, so a UI toolkit can describe its UI tree once and let AccessKit expose that tree to every supported operating system. This document is the canonical source for the “How it works” narrative and a practical quick start. |
| 4 | + |
| 5 | +## Architecture at a glance |
| 6 | + |
| 7 | +### Data schema |
| 8 | + |
| 9 | +At the heart of AccessKit is a schema that expresses an accessibility tree. Each [`Node`](../common/src/lib.rs) has a stable [`NodeId`], a [`Role`] (for example `Button`, `Label`, or `TextInput`), optional attributes, and the set of actions it and its children support. Trees are delivered as [`TreeUpdate`] structs so the toolkit can stream incremental changes instead of rebuilding the entire tree. |
| 10 | + |
| 11 | +### Tree updates and actions |
| 12 | + |
| 13 | +The UI process (“provider”) pushes an initial tree to AccessKit and follows up with smaller updates whenever nodes change. Assistive technologies request interactions such as focus changes or button invocations through AccessKit actions; the toolkit responds by mutating its state and emitting another `TreeUpdate`. Only the platform adapter keeps a complete copy of the tree, which makes this model friendly to immediate-mode toolkits that can re-create nodes on demand as long as IDs stay stable. |
| 14 | + |
| 15 | +### Platform adapters |
| 16 | + |
| 17 | +Platform adapters translate the shared schema to the host accessibility API: |
| 18 | + |
| 19 | +- `accesskit_android` (Android accessibility bridge) |
| 20 | +- `accesskit_macos` (AppKit `NSAccessibility`) |
| 21 | +- `accesskit_unix` (AT-SPI over D-Bus) |
| 22 | +- `accesskit_windows` (UI Automation) |
| 23 | + |
| 24 | +Adapters are primarily written in Rust. The [winit adapter](../platforms/winit) wraps the appropriate per-platform adapter so Rust applications using the popular windowing library only need to depend on one crate. Planned adapters include iOS and a web/canvas bridge. |
| 25 | + |
| 26 | +### Language bindings |
| 27 | + |
| 28 | +In addition to the Rust API, AccessKit publishes C and Python bindings. Each binding exposes the same schema (generated from the canonical Rust definitions) and forwards platform events to the native adapters. |
| 29 | + |
| 30 | +### Consumer library |
| 31 | + |
| 32 | +Shared, platform-independent logic that adapters need lives in the [`accesskit_consumer`](../consumer) crate. Advanced use cases—such as embedded assistive technologies that run inside a game or appliance—can build on this crate directly. |
| 33 | + |
| 34 | +## Quick Start (Rust + winit) |
| 35 | + |
| 36 | +The snippet below mirrors the runnable sample in [`platforms/winit/examples/simple.rs`](../platforms/winit/examples/simple.rs) and shows the pieces you need in every application: describe the tree, initialize the adapter before the window becomes visible, forward window events, and react to AccessKit requests. |
| 37 | + |
| 38 | +### 1. Add the dependencies |
| 39 | + |
| 40 | +```toml |
| 41 | +[dependencies] |
| 42 | +accesskit = "0.21" |
| 43 | +accesskit_winit = "0.29" |
| 44 | +# Pick the winit backend features your app needs. |
| 45 | +winit = { version = "0.30", default-features = false, features = ["x11", "wayland"] } |
| 46 | +``` |
| 47 | + |
| 48 | +### 2. Describe your UI tree |
| 49 | + |
| 50 | +```rust |
| 51 | +use accesskit::{Action, Node, NodeId, Role, Tree, TreeUpdate}; |
| 52 | + |
| 53 | +const WINDOW_ID: NodeId = NodeId(0); |
| 54 | +const BUTTON_ID: NodeId = NodeId(1); |
| 55 | + |
| 56 | +fn build_initial_tree() -> TreeUpdate { |
| 57 | + let mut window = Node::new(Role::Window); |
| 58 | + window.set_children(vec![BUTTON_ID]); |
| 59 | + window.set_label("AccessKit demo"); |
| 60 | + |
| 61 | + let mut button = Node::new(Role::Button); |
| 62 | + button.set_label("Press me"); |
| 63 | + button.add_action(Action::Click); |
| 64 | + |
| 65 | + TreeUpdate { |
| 66 | + nodes: vec![(WINDOW_ID, window), (BUTTON_ID, button)], |
| 67 | + tree: Some(Tree::new(WINDOW_ID)), |
| 68 | + focus: BUTTON_ID, |
| 69 | + } |
| 70 | +} |
| 71 | + |
| 72 | +fn button_pressed_update() -> TreeUpdate { |
| 73 | + let mut button = Node::new(Role::Button); |
| 74 | + button.set_label("Clicked!"); |
| 75 | + button.add_action(Action::Click); |
| 76 | + |
| 77 | + TreeUpdate { |
| 78 | + nodes: vec![(BUTTON_ID, button)], |
| 79 | + tree: None, |
| 80 | + focus: BUTTON_ID, |
| 81 | + } |
| 82 | +} |
| 83 | +``` |
| 84 | + |
| 85 | +### 3. Create the adapter before showing your window |
| 86 | + |
| 87 | +```rust |
| 88 | +use accesskit_winit::{Adapter, Event as AccessKitEvent, WindowEvent as AccessKitWindowEvent}; |
| 89 | +use winit::{ |
| 90 | + application::ApplicationHandler, |
| 91 | + event::WindowEvent, |
| 92 | + event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy}, |
| 93 | + window::{Window, WindowId}, |
| 94 | +}; |
| 95 | + |
| 96 | +struct DemoApp { |
| 97 | + window: Option<Window>, |
| 98 | + adapter: Option<Adapter>, |
| 99 | + proxy: EventLoopProxy<AccessKitEvent>, |
| 100 | +} |
| 101 | + |
| 102 | +impl ApplicationHandler<AccessKitEvent> for DemoApp { |
| 103 | + fn resumed(&mut self, event_loop: &ActiveEventLoop) { |
| 104 | + let attrs = Window::default_attributes() |
| 105 | + .with_title("AccessKit demo") |
| 106 | + .with_visible(false); |
| 107 | + let window = event_loop.create_window(attrs).expect("window"); |
| 108 | + let adapter = Adapter::with_event_loop_proxy(event_loop, &window, self.proxy.clone()); |
| 109 | + window.set_visible(true); |
| 110 | + self.window = Some(window); |
| 111 | + self.adapter = Some(adapter); |
| 112 | + } |
| 113 | + |
| 114 | + fn window_event(&mut self, _: &ActiveEventLoop, _: WindowId, event: WindowEvent) { |
| 115 | + if let (Some(window), Some(adapter)) = (&self.window, &mut self.adapter) { |
| 116 | + adapter.process_event(window, &event); |
| 117 | + } |
| 118 | + } |
| 119 | + |
| 120 | + fn user_event(&mut self, _: &ActiveEventLoop, event: AccessKitEvent) { |
| 121 | + use accesskit::{Action, ActionRequest}; |
| 122 | + |
| 123 | + let adapter = match &mut self.adapter { |
| 124 | + Some(adapter) => adapter, |
| 125 | + None => return, |
| 126 | + }; |
| 127 | + |
| 128 | + match event.window_event { |
| 129 | + AccessKitWindowEvent::InitialTreeRequested => { |
| 130 | + adapter.update_if_active(build_initial_tree); |
| 131 | + } |
| 132 | + AccessKitWindowEvent::ActionRequested(ActionRequest { action, target, .. }) |
| 133 | + if target == BUTTON_ID && action == Action::Click => |
| 134 | + { |
| 135 | + adapter.update_if_active(button_pressed_update); |
| 136 | + } |
| 137 | + AccessKitWindowEvent::AccessibilityDeactivated => {} |
| 138 | + _ => {} |
| 139 | + } |
| 140 | + } |
| 141 | +} |
| 142 | + |
| 143 | +fn main() -> winit::error::EventLoopError { |
| 144 | + let event_loop = EventLoop::with_user_event().build()?; |
| 145 | + let proxy = event_loop.create_proxy(); |
| 146 | + let mut app = DemoApp { |
| 147 | + window: None, |
| 148 | + adapter: None, |
| 149 | + proxy, |
| 150 | + }; |
| 151 | + event_loop.run_app(&mut app) |
| 152 | +} |
| 153 | +``` |
| 154 | + |
| 155 | +### 4. Keep tree updates incremental |
| 156 | + |
| 157 | +- Call `adapter.process_event` for every winit `WindowEvent` so AccessKit can keep platform state in sync. |
| 158 | +- Whenever your UI changes, call `adapter.update_if_active` with a closure that returns only the nodes that changed plus the latest focus target. |
| 159 | +- Provide a full tree in the first update after `InitialTreeRequested`. Subsequent updates can set `tree: None`. |
| 160 | + |
| 161 | +For more advanced scenarios (direct handlers, custom runtimes, or adapters outside winit), start from this document and explore the crate-level documentation on [docs.rs](https://docs.rs/accesskit/). |
0 commit comments