GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
docs/rfc-accessibility-locale.md (26.2K)
1 # RFC: Accessibility and locale — what the toolkit owes people who are not its author
2
3 **Status:** Draft / roadmap (2026-10-08)
4 **Scope:** Assistive-technology access (screen readers, switch and voice control),
5 keyboard-only use, the user's locale, right-to-left and non-monospace text editing, and
6 translatable UI strings — across `cce-ui` and the apps built on it.
7 **Appetite:** Phased. Phase 0 is a few lines; each later phase is its own piece of work
8 and leaves every app building and drawing as before until the step meant to change it.
9 **Reads before this:** `CLAUDE.md` § "Plates, wells and seams" (focus roles, the walk),
10 § "Input-method composition is one model for every shell" (the IME model this extends);
11 `docs/rfc-core-rebuild.md` (the widget tree the accessibility tree would mirror).
12
13 ---
14
15 ## 1. Why now
16
17 The toolkit audit of 2026-10-07 found no accessibility support and no locale handling at
18 all. Both are cheap to design for and expensive to retrofit: every widget added without a
19 role, every label used as an identifier, and every editor that assumes left-to-right
20 monospace text is a later rewrite. The widget set is still growing (the field family, the
21 spreadsheet's selection and the trackball all landed in the last two weeks), so this is
22 the cheapest moment there will be.
23
24 ## 2. Where things stand (measured 2026-10-08)
25
26 **Accessibility.**
27 - Nothing in the tree speaks to assistive technology: no AT-SPI, no AccessKit, no
28 NSAccessibility, no ARIA. A screen reader sees an untitled surface.
29 - What exists is a good seed. Every retained widget reports a `FocusRole` (`Plate`, `Well`
30 or `None`), a `label()`, a value (`Adapted::get_value_string`), its rect, its focus and
31 its `type_name()`, and `UiContext` holds them all by `WidgetId`. Since phase 2 of the
32 pointer work (`widget::Owned`) every registered app widget sits at a stable address.
33 - 11 of the 27 `Application` apps draw immediate-mode (no `UiContext` tree): calendar,
34 browser, grid, keyboard, screensaver, map, status bar, model, preview, terminal,
35 notifier. They have nothing for a tree to be built from.
36
37 **Keyboard.**
38 - Tab / Shift+Tab plate navigation exists (`UiContext::focus_step`, `focus_step_group`)
39 but is OPT-IN (`Application::plate_navigation`, default false; on by default since phase
40 3). Four apps turn it on:
41 cce-data-editor, cce-files, cce-gallery, cce-system-interface (plus the demo).
42 - There is no tooltip (where an accessible description usually lives), no modal dialog
43 widget (where focus must be trapped), and no radio group.
44
45 **Locale.**
46 - The locale is the literal `"en-US"` in all four `FontSystem` constructors
47 (`lib.rs`, `backend/text.rs` twice, `backend/frame.rs`). cosmic-text uses it to order
48 its font fallback, so a Japanese user gets the Chinese variants of shared Han glyphs.
49 - No message catalogue. The toolkit itself ships about 20 English strings (the context
50 menu's Cut / Copy / Paste / Select All / Undo / Redo, Copy Path / Copy Key / Copy Value,
51 Expand / Collapse, Properties, Search…, OK / Cancel / Save / Delete).
52 - **Labels are identifiers.** A context-menu row's text is its identity (CLAUDE.md: "the
53 text stays the row's identity, so hosts that match their own labels still match"), and
54 hosts dispatch by comparing label strings. Translating a label today breaks the action
55 behind it.
56
57 **Text direction and script.**
58 - cosmic-text shapes bidirectional text and lays out right-to-left runs, so DRAWING Arabic
59 or Hebrew mostly works.
60 - EDITING does not. `ShapingMeasure::offsets` sorts glyph positions by byte and assumes x
61 grows with the text (false inside a right-to-left run); the `DocEditor` and `LineEdit`
62 carets are built on it. `TextBox` is a column editor: it measures one monospace advance
63 (`"MMMMMMMM"` / 8) and wraps by character count, which holds for neither proportional
64 fonts nor CJK, whose characters are two columns wide.
65 - The IME model (`crate::ime`) is direction-agnostic and needs no change.
66
67 ## 3. The plan
68
69 ### Phase 0 — the locale comes from the user (small; do first) — DONE 2026-10-08
70
71 Done as written below: `cce_core::locale` (re-exported as `cce_ui::locale`), used by all four
72 constructors, and the browser shell sets it from `navigator.language` before the first font
73 system. Tests: `locale::tests` in cce-core (the parsing and the environment's order) and
74 `every_font_system_is_built_with_the_users_locale` in cce-ui.
75
76 One `cce_core::locale()`: `LC_ALL`, else `LC_CTYPE`, else `LANG`, turned from POSIX form
77 (`ja_JP.UTF-8`) into a BCP 47 tag (`ja-JP`), falling back to `en-US`; in the browser,
78 `navigator.language`. The four `FontSystem` constructors take it. Nothing changes for an
79 `en_US` session, which is every session today.
80
81 ### Phase 1 — an accessibility tree the toolkit can produce
82
83 **Progress (2026-10-08):** the tree for retained widgets is `crate::a11y::tree_update`
84 (AccessKit's `TreeUpdate`, § 5): a window node carrying the HiDPI scale, and a node per
85 registered visible widget with its role (`WidgetHost::a11y_role`, else `a11y::role_for` from
86 type and focus role), label, value (`a11y_value`, the widget's `value_string`: toggled for a
87 check box or switch, numeric for a slider, spin button or progress bar), logical bounds,
88 focus and click actions, children in reading order, and the context's focus.
89
90 **Phase 1 is done (2026-10-08).** Also landed:
91 - **An open context menu is in the tree**: a `Menu` under the window, its rows items — `✓` a
92 checkable item, `●` / `○` a radio item (toggled as marked), a slider row a slider with its
93 range, a header a label, separators left out — and while it is open the focus is its
94 highlighted row, else the menu.
95 - **The hook for apps without widgets**: `Application::accessibility(&mut self, &mut
96 a11y::AppNodes)`, AccessKit nodes in an id range of the app's own (`AppNodes::id`), with
97 `push_top` / `push` / `set_focus`; `a11y::app_tree(&mut app, scale)` is an app's whole
98 tree (its widgets, its own nodes, the menu).
99 - **Menu rows carry their actions**: `context_menu::set_row_actions` /
100 `UiContext::show_context_menu_rows` give each row a `ContextAction`, and a press runs it;
101 matching the English label (`legacy_action_for_label`) is only a fallback for menus built
102 without. The toolkit's own menus (a widget's standard menu, the tree list's) set theirs, so
103 translating their labels changes nothing they do. Apps that match labels themselves (the
104 designer's menus) move when they are next touched.
105
106 - A node per registered widget: **role** (from `type_name` and `FocusRole` first, then an
107 explicit `Input::a11y_role()` a widget can override: button, checkbox, toggle-button,
108 slider, spin-button, text-input, combo-box, list, tree, table, menu), **name** (the
109 label), **value** (`get_value_string`), **bounds**, **focus**, and the **actions** it
110 takes (activate, increment / decrement, set value, focus). Parent and child links come
111 from `UiContext`'s tree.
112 - For the immediate-mode apps, an `Application::accessibility(&mut self, out)` hook where an
113 app declares its own nodes, the way it already declares `input_regions`.
114 - Incremental: a node is keyed by `WidgetId`, and only changed nodes are re-sent. The
115 per-frame damage diff (`backend::frame`) is the model for what "changed" means.
116 - **Rule from this phase on:** a new widget declares its role and name when it lands, and a
117 context-menu row or any other actionable label gets an ID separate from its text.
118
119 ### Phase 2 — speak to the platform
120
121 **Progress (2026-10-08): Linux works, on one app.** `backend::a11y_unix` runs `accesskit_unix`
122 in the Wayland shell behind the `a11y` feature (23 crates, zbus among them; nothing without
123 it), for an app that returns true from `Application::publishes_accessibility` or any app run
124 with `CCE_A11Y=1`. The adapter's callbacks only post into the runner's calloop loop; the
125 loop publishes the whole tree when a reader connects (an idle window renders nothing, and
126 AccessKit wants it by the next refresh), after every frame (`update_if_active`: nothing is
127 built while no reader is connected), and answers `Focus` on a widget (`UiContext::
128 set_focused_id`) and `Click` on a context-menu row (a press where it is drawn). The window's
129 keyboard enter / leave is AccessKit's window focus, so a node reads FOCUSED only while the
130 window has the keyboard. Widgets parked off-screen or without a size are left out.
131 `CCE_A11Y_DEBUG=1` logs each connection, action, publish (with its build time) and focus change.
132
133 Proven on cce-data-editor in a shadow session through `pyatspi`'s `Atspi` (no screen reader
134 is installed), with the session's `org.a11y.Status IsEnabled` set for the run: the app is
135 listed with toolkit `cce-ui`; its frame holds buttons, combo boxes, entries, spin buttons
136 with values, a check box, the menu bar and the tree with their roles; `grab_focus` on a
137 button focuses it in the window (its ring drawn) and reads back FOCUSED; Tab steps are
138 republished. A tree of 21 nodes builds in 50–100 µs in a debug build.
139
140 What it showed is the apps' to fix, not the adapter's: most of the data editor's controls
141 have no label (an unlabelled field is an empty name to a reader), and its inactive editors
142 are registered and visible though not drawn.
143
144 **Widget actions (2026-10-08).** A reader can now press and set widgets, not only focus
145 them. What AT-SPI reaches through AccessKit is narrower than AccessKit's action list: the
146 Action interface carries `click` alone, and a value is adjusted through the Value
147 interface's `SetCurrentValue` (`Action::SetValue` with a number); Increment / Decrement
148 exist for the macOS and Windows adapters. So:
149 - **Click** focuses the widget and presses Space through the app's own
150 `handle_key_input` (`Driver::press_named_key`), what activates a plate from the keyboard,
151 so the app hears of it as it hears of a key. **Increment / Decrement** are the arrows
152 (Right / Left on a slider or range, Up / Down on a spin button), ready for the other
153 adapters. `a11y::key_for` is the one table: a node offers exactly the actions it answers.
154 - **SetValue** sets the value on the widget (`Input::a11y_set_value`, Slider and Spinbox),
155 clamped and marked changed as a typed value is, without moving focus. Not by keys: a key
156 a widget does not take falls through to the app, where Backspace or Escape may mean
157 something else. An app that drains `take_change` in `tick` sees it this turn; one that
158 drains only in its input handlers, at the next input.
159 - A slider's or spin button's node carries its range and step (`Input::a11y_range`), what a
160 reader reads a percentage and a step from.
161 - A Spinbox now steps on Up / Down, and a focused box takes keys again after Enter (it
162 ignored every key until refocused).
163
164 Verified on cce-data-editor in a shadow: a check box clicked on and off (CHECKED read
165 back), spin buttons set to 7 and 3 with their ranges read.
166
167 **A text field is read and set as text (2026-10-09).** A field says what a reader reads and
168 edits (`Input::a11y_text` → `a11y::A11yText`: what it shows, its caret and selection while
169 it is edited, multi-line, password, editable, placeholder), and the tree publishes it as
170 TEXT RUNS — one per line, the line's break at its end, each character's UTF-8 length beside
171 it — with the selection in them, instead of a plain value. Runs are what AccessKit gives
172 AT-SPI's Text and EditableText interfaces for: a reader reads the field by character and
173 line, follows the caret, and sets it with `SetTextContents`, which arrives as `SetValue`
174 with text and is `Input::a11y_set_text` (`backend::a11y_unix::set_value`): the text
175 replaced as the user replacing it would — undoable, the caret at its end — and reported to
176 the app as a typed change. A multi-line box is a `MultilineTextInput`, a password box a
177 `PasswordInput` whose runs are bullets; until this its secret was the node's value, in the
178 clear. A disabled field is read-only. `TextBox` implements both; the placeholder rides as
179 AT-SPI's `placeholder-text`. Verified on cce-data-editor, on a private session bus with its
180 own accessibility bus (a plain `dbus-daemon`, `at-spi2-registryd`, and a stand-in
181 `org.a11y.Bus` reporting it enabled — so the desktop's own AT-SPI stays off): the source
182 editor's whole text and caret read back, the search box set to "grid" filtering the tree to
183 `grid_gap`, the source set to new KDL re-parsed into the tree. The add-key popover's box,
184 there whether its popover was open or not, is hidden while it is closed: it was a field a
185 reader found and a Tab stop nobody could see.
186
187 **And so is every field where a person types (2026-10-09).** A field is published as a
188 text input whatever the widget would be otherwise, since only a text input has
189 EditableText: the `ColorSelector` answers `a11y_text` with its hex and `a11y_set_text` with
190 what typing a hex does (a colour it parses, or nothing), described as a "colour"
191 (`A11yText::kind`). As a DESCRIPTION, not a role description: AccessKit gives a node with a
192 role description AT-SPI's `Extended` role, and such a node never registered on the bus —
193 the gallery's two colour selectors were simply missing from the reader until it moved. A
194 `Spinbox` stays a spin button: it is a number, which AT-SPI's Value interface sets
195 (`SetCurrentValue`), as it already did. A field an APP draws — a `LineEdit` — is
196 `AppNodes::text_field(n, label, &edit.a11y_text(has_keyboard), bounds)` among the app's own
197 nodes (`Application::accessibility`; AccessKit is re-exported as `cce_ui::accesskit` for
198 them), its runs in a range of their own (`APP_RUN_BASE`); a reader's edit, or its request
199 for the keyboard, arrives as `Application::accessibility_action(n, AppAction::SetText(..) /
200 Focus)` — the app's own nodes took no action at all until this — and `LineEdit::a11y_set_text`
201 replaces the text undoably (a masked field keeps no history). cce-browser publishes its
202 address bar, the bookmarks search, the vi command line and a dialog (modal, titled, its
203 message as description) with its fields, and builds cce-ui's `a11y` feature, publishing
204 with `CCE_A11Y=1` (not by default yet). Verified over AT-SPI on private buses: the
205 browser's address bar set from the reader — the bar unfolds, takes the keyboard and shows
206 it, the caret at its end — and the gallery's colour selector set to `#20c060`, its swatch
207 green, "not a colour" refused.
208
209 Next: the macOS adapter onto the AppKit view, and listening with Orca.
210
211 - **Wayland / Linux:** AT-SPI over D-Bus. AccessKit's Unix adapter is the likely carrier
212 (evaluate it first; the alternative is a small AT-SPI server of our own). The compositor
213 needs nothing new: AT-SPI is a session-bus protocol between the app and the reader.
214 - **macOS:** NSAccessibility through the same tree (AccessKit's macOS adapter, or direct).
215 - **Browser:** mirror the tree as hidden ARIA elements over the canvas — the same move the
216 IME already makes with its hidden `<textarea>`.
217
218 ### Phase 3 — keyboard first
219
220 **Progress (2026-10-08): the walk is on by default.** `Application::plate_navigation`
221 answers true. It changes nothing for an app without a `UiContext` (the walk finds no stops
222 and Tab reaches `handle_key_input` as before: the terminal, the browser, the calendar and
223 the other immediate-mode apps), so the apps it reached are the seven with a context that had
224 not opted in. Three give Tab a meaning of their own and opt out: the designer (the node
225 palette), the display manager (its username / password order) and cce-notes (accepting a
226 link completion). A focused widget that types Tab keeps it (`Input::keeps_tab`: a
227 multi-line `TextBox` while editing), and Ctrl+Tab still leaves it
228 (`tab_walks_the_stops_but_a_multi_line_box_keeps_it`). Checked in a shadow, two Tabs each:
229 cce-list, cce-weather, the demo and cce-relief ring their second stop; cce-fonts' first
230 two stops were its picker buttons, registered outside picker mode and never drawn, and are
231 now registered only in it; cce-text-editor has no stops and is unchanged.
232
233 **A modal dialog and a radio group (2026-10-08).** `widget::Dialog` is a raised plate (the
234 menu's material) around a lasso of members the host lays out; opening it makes it modal
235 through the context (`UiContext::open_modal` / `close_modal`): the Tab walk is trapped among
236 its members, every widget outside reads as covered so neither a press nor a hover reaches
237 it, focus moves in and is given back on close, and a reader sees a modal `Dialog` node
238 holding the members. `widget::RadioGroup` is one Tab stop whose arrows move the choice;
239 each option is the check box's well at a full corner, the chosen one holding a lit bead.
240 To a reader it is a radio group of radio buttons, each clickable, which needed a way for a
241 widget to show parts of itself as nodes (`Input::a11y_items`, `a11y::A11yItem`, clicked
242 through `Input::a11y_select_item`). The demo's Options… dialog holds a radio group and
243 OK / Cancel; in a shadow: Tab cycles its three stops only, a click behind it does nothing,
244 Escape cancels, Space on OK keeps the choice; over AT-SPI the dialog holds the group's
245 three radio buttons, and a click on one chooses it. The tooltip that doubles as the
246 accessible description is not done: no tooltips yet, by decision.
247
248 - `plate_navigation` defaults to true; an app that routes Tab itself (a terminal, a web
249 view) opts OUT.
250 - A modal dialog widget that traps focus, a tooltip that doubles as the accessible
251 description, a radio group.
252
253 ### Phase 4 — text that is not left-to-right monospace
254
255 **Progress (2026-10-08).** Carets, clicks and selections follow the text in either
256 direction, and a multiline `TextBox` wraps by shaped width:
257 - **`backend::text::shaped_run`** (`ShapingMeasure::shape`) is a line as an editor needs it:
258 the caret's x at every char boundary — a cluster's LEADING edge, a right-to-left
259 letter's right — the clusters in logical order with their boxes, the width, and the
260 paragraph's base direction, which cosmic-text already takes from the first strong
261 character. `index_at` is a click, `spans` a selection (two where it crosses a change of
262 direction). `ShapingMeasure::offsets` is its stops; `shaped_cluster_offsets`, what
263 hand-drawn fields (`LineEdit` hosts) read, gives leading edges too.
264 - **A fix underneath:** `normalized_glyph_starts` rebuilt glyph starts whenever they fell,
265 to repair cosmic-text 0.12's Basic shaping (span-relative starts) — but they also fall in
266 every right-to-left run, which it scrambled. Basic shaping is ASCII only, so the repair
267 now is too.
268 - **`DocEditor`**: runs carry their cluster boxes and the shaped width; a selection is the
269 boxes it covers (`LineLayout::selection_rects`), so it is drawn where its letters are.
270 - **`TextBox`**: its column offsets are the shaped stops (a right-to-left word is clicked and
271 selected where it is drawn), and a multiline box wraps by the summed shaped advances of
272 its chars against the width (`wrap_text(max_width)`, `wrap_width`), not a char count
273 against one monospace advance, so a proportional face and CJK wrap where they are drawn.
274 The advances are measured with the paint's font system in `prepare_text` and cached;
275 an edit that outruns the frame measures with the shared geometry font system, or a
276 thread's own when that one is held.
277
278 **And the rest (the same day):**
279 - **A right-to-left paragraph is set against the right.** `backend::text::paragraph_rtl` is
280 a paragraph's base direction (`unicode-bidi`, already in the tree through cosmic-text).
281 In a `TextBox` the shift is folded into the offsets, so caret, click and selection
282 follow, and added to where the text is drawn — a one-line box's text when it fits, each
283 wrapped line of a multiline box by its paragraph's direction. The shaping key now
284 carries the room the text is aligned in, which also re-shapes a box whose width was set
285 after its first shaping: cce-text-editor's highlight had stopped short on a CJK line
286 because its offsets were never re-shaped once the box had its size.
287 - **The `DocEditor` draws a line's styled runs in visual order.** The line's bidi levels
288 (`bidi_levels`, neutrals resolved) split every run where the level changes, so a plain
289 run holding an English word and a Hebrew one becomes two, and each row's runs are placed
290 in the order rule L2 gives (`visual_order`); a plain or heading line of a right-to-left
291 paragraph is set against the right edge (a list, quote or table keeps its markers at the
292 left and only reorders).
293 - **A selection crossing a change of direction is drawn as its pieces** in a `TextBox` too,
294 one-line and multiline, from the cluster boxes of each line's shaped run.
295
296 Tests: `carets_follow_the_text_in_either_direction`,
297 `right_to_left_words_are_laid_out_and_selected_where_they_are`,
298 `styled_runs_are_placed_in_visual_order`, `runs_are_reordered_as_the_bidi_algorithm_draws_them`,
299 `a_right_to_left_word_is_edited_where_it_is_drawn`,
300 `a_right_to_left_paragraph_is_set_against_the_right`, `a_multiline_box_wraps_where_its_text_is_wide`.
301 Checked in a shadow: cce-text-editor sets a Hebrew paragraph against the right with its
302 English and Arabic in bidi order. Still the shaping's own limit: a run's base direction is
303 cosmic-text's guess from the run's text, which a style split mid-paragraph can get wrong
304 for a run that starts with a neutral; arrow keys move the caret in logical order, as
305 editors on most platforms do.
306
307 - Carets and selection from shaped clusters in VISUAL order (cosmic-text's layout runs
308 carry direction), not byte-sorted x positions: `ShapingMeasure::offsets` grows a
309 direction-aware form, and `DocEditor` and `LineEdit` use it.
310 - `TextBox` moves from its column model to shaped wrapping (cosmic-text's own wrap), which
311 also makes it correct for proportional fonts and CJK.
312 - Base direction per paragraph from the first strong character, as the Unicode
313 bidirectional algorithm does.
314
315 ### Phase 5 — translatable strings
316
317 **Done (2026-10-08).** `cce_core::l10n` (the `l10n` feature, off by default so the compositor
318 does not carry it; fluent-bundle and its seven crates) is a catalogue per domain in Project
319 Fluent's format: the domain's English built in (`Catalog::new(domain, include_str!(…))`),
320 a translation found on first use for the user's locale chain (`ja-JP`, then `ja`) as
321 `<tag>/<domain>.ftl` under `CCE_LOCALE_DIR`, `$XDG_DATA_HOME/cce/locale` and each
322 `$XDG_DATA_DIRS/*/cce/locale`, and a message looked up most specific first, then English,
323 then its own id (a gap shows). Placeables are not wrapped in isolation marks, which the
324 renderer would draw. A page reads no files (`Catalog::add_translation`); a suite reads no
325 directory.
326
327 cce-ui's words are its `locale/en-US/cce-ui.ftl` (27 messages): the standard context menu
328 (Cut, Copy, Paste, Select All, Clear, Copy Path, the ramp's Collapse controls, the config
329 header's File / Key rows), the tree list's rows, the plate dock's, the search placeholder,
330 the copy and ramp-delete buttons, the keybind recorder's prompt and empty value, and the
331 `DocEditor`'s Properties table — through `crate::l10n::tr` / `tr_args`.
332 `every_message_the_toolkit_names_is_in_its_english` scans the source for every id looked up
333 and holds them to the file both ways. One identity-by-text went with it: the standard menu
334 knew a search box by its placeholder being "Search..."; a search box says so now
335 (`TextBox::with_search`), the English placeholder read only for the apps that set it
336 themselves. Checked in a shadow: the demo under `LANG=de_DE.UTF-8` with a German
337 `cce-ui.ftl` shows its text box's menu as Ausschneiden, Kopieren, Einfügen, Alles auswählen.
338 No translation ships yet; an app's own words are its own domain, adopted at its own pace.
339
340 - The toolkit's ~20 strings through a small catalogue keyed by message ID, in `cce-core`
341 so services can use it too. Fluent is the candidate format.
342 - Apps adopt it at their own pace. Only phase 1's rule — actions keyed by ID, never by
343 label — has to come first.
344
345 ## 4. Order and cost
346
347 Phase 0 is an afternoon. Phase 1 is the real design work, and everything after it stands
348 on it; it is additive, and the tree costs nothing for an app nobody inspects. Phases 3 and
349 4 are independent of 1 and 2 and can go in parallel. Phase 5 waits on phase 1's ID rule.
350
351 ## 5. Decision: AccessKit (2026-10-08)
352
353 The tree is AccessKit's, and so are the platform adapters. AccessKit is a schema for an
354 accessibility tree (the `accesskit` crate: pure data, one required dependency, `uuid`, so it
355 builds for every target cce-ui does) plus adapters that publish it, each pushed to by the
356 toolkit. Phase 1 produces its `TreeUpdate` directly; there is no schema of our own to
357 translate.
358
359 Why, against writing our own AT-SPI server:
360 - **It is the problem it was built for**: "toolkits that render their own user interface
361 elements". egui, Bevy, Slint, GPUI, Masonry/Xilem, Freya, Vizia, KAS and Servo use it.
362 - **Linux alone would be three crates' worth of work** (`accesskit_consumer`,
363 `accesskit_atspi_common`, `accesskit_unix`, ~115 KB compressed): the AT-SPI object
364 interfaces (accessible, component, action, value, text, editable text, selection, table),
365 their events and cache, tree diffing and text navigation.
366 - **macOS comes with it** (`accesskit_macos`, onto the AppKit shell's view). Our own server
367 would cover Linux only.
368 - **No cost without a screen reader**: `Adapter::update_if_active` builds nothing until an
369 assistive tool connects. Its handlers run on another thread, which `AppSender` already
370 serves.
371 - MIT or Apache-2.0, like this crate.
372
373 What it costs, accepted:
374 - `accesskit_unix` needs zbus 5.19 or later (cce-ui has none today), so it goes in the
375 Wayland shell and behind a feature if its weight shows.
376 - Pre-1.0: its crates release together with breaking minor versions every few months.
377 - Editable text is exposed in its model (text runs with per-character positions), which
378 phase 4's direction-aware carets must feed.
379
380 Unchanged by the choice: the browser still needs our hidden-ARIA mirror (AccessKit's web
381 adapter is planned, not released), and on Wayland a window's screen position is unknown to
382 any toolkit, so a screen reader's pointer features are approximate there for GTK as much as
383 for us.
384
385 How it is proven: phase 1's tree, then `accesskit_unix` in the Wayland shell for one app
386 (cce-data-editor: plate navigation on, a tree list, text fields, a menubar), measured in
387 dependencies and frame time and listened to with Orca (`at-spi2-core` is installed; a
388 screen reader is not).
389
390 ## 6. Open questions
391 - Should the compositor expose its own UI (window titles, overview, the grid) to AT-SPI?
392 It draws natively, so it would need its own tree.
393 - Do immediate-mode apps get the hook or get ported? The status bar and the notifier are
394 the most-used surfaces in the DE, and both are immediate.