Skip to repository content296 lines · 11.0 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:43:44.913Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
_accessibility.rs
1//! # Accessibility in GPUI
2//!
3//! "Accessibility" refers to the ability of your application to be used by all
4//! users, regardless of disability status. There are many aspects, all important, including:
5//! - Ensuring sufficient text contrast.
6//! - Providing a mechanism to disable animations.
7//! - Providing a mechanism to increase text sizes.
8//! - etc.
9//!
10//! This guide is focused on **programmatic accessibility**. This allows
11//! assistive technology, such as screen readers or Braille displays, to inspect
12//! and interact with your app. Docs for contributors working on accessibility
13//! support can be found in the `a11y` module's doc comment.
14//!
15//! GPUI integrates with [AccessKit] to provide programmatic accessibility
16//! features (referred to as simply "accessibility" for the rest of this guide).
17//!
18//! A minimal example can be found in the `examples/a11y` directory.
19//!
20//! ## Background
21//!
22//! Accessibility support is based on two key capabilities:
23//! - Exposing information about the current UI state to assistive technology.
24//! - Responding to actions requested by assistive technology.
25//!
26//! For example, a screen reader might want to announce to the user that a new
27//! button has appeared. The user may then want to use a voice control program
28//! to press that button.
29//!
30//! ### IDs in GPUI - [`ElementId`] and [`GlobalElementId`]
31//!
32//! In GPUI, each [`Element`] can have an [`id`][Element::id]:
33//! ```rust
34//! # use gpui::*;
35//! let div_with_id = div().id("my-id").child(text!("hello"));
36//!
37//! // IDs are optional
38//! let div_without_id = div().child(text!("hello"));
39//! ```
40//!
41//! [`Element`]s with IDs are also assigned a [`GlobalElementId`]. This global
42//! ID is formed by composing all the non-`None` IDs of its ancestors. For
43//! example:
44//! ```rust
45//! # use gpui::*;
46//! let inner = div().id("inner-id");
47//! let middle = div().child(inner); // no ID
48//! let outer = div().id("outer-id").child(middle);
49//! ```
50//! In this example, `inner`s global ID is (roughly speaking) `["outer-id",
51//! "inner-id"]`.
52//!
53//! Since `middle` doesn't have an ID itself, it has no global ID.
54//!
55//! [`GlobalElementId`]s should be unique per-frame. Duplicate global IDs in the
56//! same frame will likely cause bugs.
57//!
58//! ### IDs and accessibility
59//!
60//! When GPUI renders a frame, it walks your UI tree, and finds nodes with
61//! global IDs, and informs assistive technology about this node.
62//!
63//! In order for nodes to be reported, they must also have a non-`None`
64//! [`role`][Element::a11y_role]. This is used to inform assistive technology
65//! what *sort* of node it is (button, label, table, etc.). You can use
66//! [`div().id(...).role()`][StatefulInteractiveElement::role] to set the role.
67//!
68//! Nodes with the same global ID *across frames* are considered to be "the
69//! same" node. For example:
70//! ```rust
71//! # use gpui::*;
72//! // The UI in frame 1
73//! let frame_1 = div()
74//! .id("parent")
75//! .role(Role::Button)
76//! .child(
77//! div()
78//! .id("id-1")
79//! .role(Role::Label)
80//! .child(text!("hello"))
81//! );
82//!
83//! // The UI on the next frame
84//! let frame_2 = div()
85//! .id("parent")
86//! .role(Role::Button)
87//! .child(
88//! div()
89//! .id("id-2") // <- different ID
90//! .role(Role::Label)
91//! .child(text!("hello"))
92//! );
93//! ```
94//! Logically, the UI has not changed. But the screen reader has no way of
95//! knowing that both child [`div`]s are "the same". So assistive technology
96//! will interpret this as one node being removed, and another node being added.
97//! This can be very disorienting for users, since announcements typically only
98//! happen when something has *meaningfully* changed.
99//!
100//! In other words, by controlling the ID of an element, you can control whether
101//! a change to a UI element is considered meaningful. You can also control
102//! whether elements are reported to assistive technology *at all* by setting
103//! the [`role`][Element::a11y_role], since nodes with no role are not reported.
104//!
105//! #### IDs and text
106//!
107//! Special care must be taken when dealing with text.
108//!
109//! GPUI provides the [`text!`] macro, which wraps strings in the [`Text`] type,
110//! but automatically derives an ID. Usually, this is what you want. However,
111//! the way it generates its ID is subtle and perhaps surprising.
112//!
113//! The ID of an invocation of the [`text!`] macro is derived from the
114//! **location in the source code of that invocation**. For example:
115//!
116//! ```rust
117//! # use gpui::*;
118//! let a = text!("a");
119//! let b = text!("b");
120//!
121//! // Different source locations, different IDs
122//! assert_ne!(a.id(), b.id());
123//!
124//! // However:
125//!
126//! fn make_text(s: &str) -> Text { text!(s) }
127//!
128//! let a = make_text("a");
129//! let b = make_text("b");
130//!
131//! // Both `a` and `b` are produced by the same `text!` invocation, so the IDs
132//! // are the same
133//! assert_eq!(a.id(), b.id());
134//! ```
135//! This can produce surprising behaviour. For example, this footgun:
136//! ```rust
137//! # use gpui::*;
138//! let todos = vec!["eat lunch", "drink water", "go to gym"];
139//! let todo_divs = todos.into_iter().map(|todo| {
140//! text!(todo)
141//! });
142//!
143//! div()
144//! .id("todo-list")
145//! .role(Role::Document)
146//! .children(todo_divs); // ERROR: multiple nodes with the same global ID
147//! ```
148//!
149//! Here, when we map the iterator, since we have only written [`text!`] once,
150//! there is only one ID. And since they have the same ancestors and the same
151//! ID, they will have the same global ID. In release builds, this will mean
152//! some nodes get silently dropped!
153//!
154//! To fix this, you can set an ID:
155//! ```rust
156//! # use gpui::*;
157//! let todos = vec!["eat lunch", "drink water", "go to gym"];
158//! let todo_divs = todos.into_iter().enumerate().map(|(index, todo)| {
159//! text!(todo).with_id(index) // OR `text(id = index, todo)`
160//! });
161//!
162//! div()
163//! .id("todo-list")
164//! .role(Role::Document)
165//! .children(todo_divs);
166//! ```
167//! Another possible solution is to wrap the [`text!`] in another node that
168//! *does* have a unique global ID. For example:
169//! ```rust
170//! # use gpui::*;
171//! let todos = vec!["eat lunch", "drink water", "go to gym"];
172//! let todo_divs = todos.into_iter().enumerate().map(|(index, todo)| {
173//! div().id(index).child(text!(todo))
174//! });
175//!
176//! div()
177//! .id("todo-list")
178//! .role(Role::Document)
179//! .children(todo_divs);
180//! ```
181//! Since the AccessKit [`NodeId`][accesskit::NodeId] is derived from the global
182//! ID, and the global ID takes into account the IDs of all ancestors, this
183//! works too.
184//!
185//! Occasionally, you will need to create a [`Text`] element with *no* ID. You
186//! can achieve this with [`Text::new_inaccessible`]. If you are creating a
187//! custom UI component (e.g. a button), you may want this so that you can set a
188//! label property on a parent [`div`] without duplicating the text in the
189//! accessibility tree.
190//!
191//! ### Handling actions
192//!
193//! Assistive technology can dispatch actions to the UI. While many users of
194//! assistive technology use traditional input devices (e.g. a keyboard), some
195//! use more specialized systems. For example, users with limited mobility may
196//! use voice control to interact with your app.
197//!
198//! When a user dispatches an action, it is dispatched *to a specific node*. It
199//! is your responsibility to tell the UI elements how they should respond when
200//! a request comes in.
201//!
202//! Note, these actions are **totally unrelated** to GPUI's [`Action`] trait.
203//! AccessKit exposes [`accesskit::Action`]. In GPUI, this is re-exported as
204//! [`AccessibleAction`].
205//!
206//! To respond to an accessible action, use
207//! [`div().on_a11y_action()`][InteractiveElement::on_a11y_action]:
208//! ```rust,ignore
209//! div()
210//! .id("my-slider")
211//! .role(Role::Slider)
212//! .on_a11y_action(AccessibleAction::Increment, |_extra, _window, _cx| {
213//! position += 1;
214//! cx.notify();
215//! })
216//! .child(my_cool_slider());
217//! ```
218//!
219//! Note that some common actions are automatically registered. For example,
220//! [`.on_click()`][StatefulInteractiveElement::on_click] adds an
221//! [`AccessibleAction::Click`] handler that calls the click handler.
222//!
223//! ## Synthetic children
224//!
225//! Sometimes, a custom [`Element`] may want to appear as if it is really made
226//! of multiple nodes. For example, a totally hypothetical custom text editor
227//! element may want to have [`Role::TextInput`], while presenting children
228//! consisting of [`Role::TextRun`]s.
229//!
230//! This is possible using [`Element::a11y_synthetic_children`]. For example:
231//! ```rust,ignore
232//! # use gpui::*;
233//! impl Element for MyCustomTextField {
234//!
235//! // ...
236//!
237//! fn a11y_role(&self) -> Option<Role> {
238//! Some(Role::TextInput)
239//! }
240//!
241//! fn a11y_synthetic_children(
242//! &mut self,
243//! _prepaint: &mut Self::PrepaintState,
244//! builder: &mut A11ySubtreeBuilder,
245//! ) {
246//! // Create the synthetic child node
247//! let mut run = accesskit::Node::new(Role::TextRun);
248//! run.set_value(self.text.clone());
249//! run.set_character_lengths(
250//! self.text.chars().map(|c| c.len_utf8() as u8).collect::<Vec<_>>(),
251//! );
252//!
253//! // Insert it as a child of `MyCustomTextField`
254//! let run_id = builder.synthetic_node_id(0);
255//! builder.push_child(run_id, run);
256//!
257//! // You can also mutate the parent (i.e. the `MyCustomTextField`)
258//! let caret = accesskit::TextPosition {
259//! node: run_id,
260//! character_index: self.cursor,
261//! };
262//! builder.parent_node().set_text_selection(accesskit::TextSelection {
263//! anchor: caret,
264//! focus: caret,
265//! });
266//! }
267//! }
268//! ```
269//!
270//! Notably, synthetic children are added *after* an element is
271//! [prepainted][Element::prepaint], so prepaint state can be used (for example,
272//! to determine what is visible on screen).
273//!
274//! ## Further reading
275//!
276//! Designing high-quality accessible interfaces can be challenging, in the same
277//! way that designing high-quality traditional interfaces can be. The
278//! following pages have useful information:
279//!
280//! - [AccessKit]: The cross-platform accessibility toolkit GPUI uses
281//! internally.
282//! - [MDN WAI-ARIA basics][mdn-aria]: Introduction to roles, properties, and
283//! states.
284//! - [ARIA Authoring Practices Guide][apg]: W3C patterns for accessible
285//! widgets.
286//!
287//! Note that, while GPUI mimics web APIs, it doesn't necessarily behave
288//! *exactly* as a web browser would with the same attributes.
289//!
290//! [AccessKit]: https://accesskit.dev/
291//! [mdn-aria]: https://developer.mozilla.org/en-US/docs/Learn_web_development/Core/Accessibility/WAI-ARIA_basics
292//! [apg]: https://www.w3.org/WAI/ARIA/apg/
293
294#[cfg(doc)]
295use crate::*; // so I don't have to qualify every type :)
296