Skip to content

Commit e0e6895

Browse files
ndarilekDimchikkk
authored andcommitted
Various accessibility API updates. (bevyengine#9989)
# Objective `bevy_a11y` was impossible to integrate into some third-party projects in part because it insisted on managing the accessibility tree on its own. ## Solution The changes in this PR were necessary to get `bevy_egui` working with Bevy's AccessKit integration. They were tested on a fork of 0.11, developed against `bevy_egui`, then ported to main and tested against the `ui` example. ## Changelog ### Changed * Add `bevy_a11y::ManageAccessibilityUpdates` to indicate whether the ECS should manage accessibility tree updates. * Add getter/setter to `bevy_a11y::AccessibilityRequested`. * Add `bevy_a11y::AccessibilitySystem` `SystemSet` for ordering relative to accessibility tree updates. * Upgrade `accesskit` to v0.12.0. ### Fixed * Correctly set initial accessibility focus to new windows on creation. ## Migration Guide ### Change direct accesses of `AccessibilityRequested` to use `AccessibilityRequested.::get()`/`AccessibilityRequested::set()` #### Before ``` use std::sync::atomic::Ordering; // To access accessibility_requested.load(Ordering::SeqCst) // To update accessibility_requested.store(true, Ordering::SeqCst); ``` #### After ``` // To access accessibility_requested.get() // To update accessibility_requested.set(true); ``` --------- Co-authored-by: StaffEngineer <111751109+StaffEngineer@users.noreply.github.com>
1 parent 46c7b5e commit e0e6895

File tree

8 files changed

+108
-86
lines changed

8 files changed

+108
-86
lines changed

crates/bevy_a11y/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@ bevy_app = { path = "../bevy_app", version = "0.12.0-dev" }
1414
bevy_derive = { path = "../bevy_derive", version = "0.12.0-dev" }
1515
bevy_ecs = { path = "../bevy_ecs", version = "0.12.0-dev" }
1616

17-
accesskit = "0.11"
17+
accesskit = "0.12"

crates/bevy_a11y/src/lib.rs

Lines changed: 54 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@
44
#![allow(clippy::type_complexity)]
55
#![forbid(unsafe_code)]
66

7-
use std::{
8-
num::NonZeroU128,
9-
sync::{atomic::AtomicBool, Arc},
7+
use std::sync::{
8+
atomic::{AtomicBool, Ordering},
9+
Arc,
1010
};
1111

1212
pub use accesskit;
13-
use accesskit::{NodeBuilder, NodeId};
13+
use accesskit::NodeBuilder;
1414
use bevy_app::Plugin;
1515
use bevy_derive::{Deref, DerefMut};
1616
use bevy_ecs::{
1717
prelude::{Component, Entity, Event},
18+
schedule::SystemSet,
1819
system::Resource,
1920
};
2021

@@ -30,6 +31,46 @@ pub struct ActionRequest(pub accesskit::ActionRequest);
3031
#[derive(Resource, Default, Clone, Debug, Deref, DerefMut)]
3132
pub struct AccessibilityRequested(Arc<AtomicBool>);
3233

34+
impl AccessibilityRequested {
35+
/// Returns `true` if an access technology is active and accessibility tree
36+
/// updates should be sent.
37+
pub fn get(&self) -> bool {
38+
self.load(Ordering::SeqCst)
39+
}
40+
41+
/// Sets whether accessibility updates were requested by an access technology.
42+
pub fn set(&self, value: bool) {
43+
self.store(value, Ordering::SeqCst);
44+
}
45+
}
46+
47+
/// Resource whose value determines whether the accessibility tree is updated
48+
/// via the ECS.
49+
///
50+
/// Set to `false` in cases where an external GUI library is sending
51+
/// accessibility updates instead. Without this, the external library and ECS
52+
/// will generate conflicting updates.
53+
#[derive(Resource, Clone, Debug, Deref, DerefMut)]
54+
pub struct ManageAccessibilityUpdates(bool);
55+
56+
impl Default for ManageAccessibilityUpdates {
57+
fn default() -> Self {
58+
Self(true)
59+
}
60+
}
61+
62+
impl ManageAccessibilityUpdates {
63+
/// Returns `true` if the ECS should update the accessibility tree.
64+
pub fn get(&self) -> bool {
65+
self.0
66+
}
67+
68+
/// Sets whether the ECS should update the accessibility tree.
69+
pub fn set(&mut self, value: bool) {
70+
self.0 = value;
71+
}
72+
}
73+
3374
/// Component to wrap a [`accesskit::Node`], representing this entity to the platform's
3475
/// accessibility API.
3576
///
@@ -47,29 +88,24 @@ impl From<NodeBuilder> for AccessibilityNode {
4788
}
4889
}
4990

50-
/// Extensions to ease integrating entities with [`AccessKit`](https://accesskit.dev).
51-
pub trait AccessKitEntityExt {
52-
/// Convert an entity to a stable [`NodeId`].
53-
fn to_node_id(&self) -> NodeId;
54-
}
55-
56-
impl AccessKitEntityExt for Entity {
57-
fn to_node_id(&self) -> NodeId {
58-
let id = NonZeroU128::new(self.to_bits() as u128 + 1);
59-
NodeId(id.unwrap())
60-
}
61-
}
62-
6391
/// Resource representing which entity has keyboard focus, if any.
6492
#[derive(Resource, Default, Deref, DerefMut)]
65-
pub struct Focus(Option<Entity>);
93+
pub struct Focus(pub Option<Entity>);
94+
95+
/// Set enum for the systems relating to accessibility
96+
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
97+
pub enum AccessibilitySystem {
98+
/// Update the accessibility tree
99+
Update,
100+
}
66101

67102
/// Plugin managing non-GUI aspects of integrating with accessibility APIs.
68103
pub struct AccessibilityPlugin;
69104

70105
impl Plugin for AccessibilityPlugin {
71106
fn build(&self, app: &mut bevy_app::App) {
72107
app.init_resource::<AccessibilityRequested>()
108+
.init_resource::<ManageAccessibilityUpdates>()
73109
.init_resource::<Focus>();
74110
}
75111
}

crates/bevy_ui/src/accessibility.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,14 @@ fn label_changed(
124124
.collect::<Vec<String>>();
125125
let name = Some(values.join(" ").into_boxed_str());
126126
if let Some(mut accessible) = accessible {
127-
accessible.set_role(Role::LabelText);
127+
accessible.set_role(Role::StaticText);
128128
if let Some(name) = name {
129129
accessible.set_name(name);
130130
} else {
131131
accessible.clear_name();
132132
}
133133
} else {
134-
let mut node = NodeBuilder::new(Role::LabelText);
134+
let mut node = NodeBuilder::new(Role::StaticText);
135135
if let Some(name) = name {
136136
node.set_name(name);
137137
}

crates/bevy_window/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ serialize = ["serde"]
1414

1515
[dependencies]
1616
# bevy
17+
bevy_a11y = { path = "../bevy_a11y", version = "0.12.0-dev" }
1718
bevy_app = { path = "../bevy_app", version = "0.12.0-dev" }
1819
bevy_ecs = { path = "../bevy_ecs", version = "0.12.0-dev" }
1920
bevy_math = { path = "../bevy_math", version = "0.12.0-dev" }

crates/bevy_window/src/lib.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
//! The [`WindowPlugin`] sets up some global window-related parameters and
88
//! is part of the [`DefaultPlugins`](https://docs.rs/bevy/latest/bevy/struct.DefaultPlugins.html).
99
10+
use bevy_a11y::Focus;
11+
1012
mod cursor;
1113
mod event;
1214
mod raw_handle;
@@ -99,9 +101,14 @@ impl Plugin for WindowPlugin {
99101
.add_event::<WindowThemeChanged>();
100102

101103
if let Some(primary_window) = &self.primary_window {
102-
app.world
104+
let initial_focus = app
105+
.world
103106
.spawn(primary_window.clone())
104-
.insert(PrimaryWindow);
107+
.insert(PrimaryWindow)
108+
.id();
109+
if let Some(mut focus) = app.world.get_resource_mut::<Focus>() {
110+
**focus = Some(initial_focus);
111+
}
105112
}
106113

107114
match self.exit_condition {

crates/bevy_winit/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ bevy_tasks = { path = "../bevy_tasks", version = "0.12.0-dev" }
2929

3030
# other
3131
winit = { version = "0.28.7", default-features = false }
32-
accesskit_winit = { version = "0.14", default-features = false }
32+
accesskit_winit = { version = "0.15", default-features = false }
3333
approx = { version = "0.5", default-features = false }
3434
raw-window-handle = "0.5"
3535

crates/bevy_winit/src/accessibility.rs

Lines changed: 34 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,28 @@
22
33
use std::{
44
collections::VecDeque,
5-
sync::{atomic::Ordering, Arc, Mutex},
5+
sync::{Arc, Mutex},
66
};
77

88
use accesskit_winit::Adapter;
9-
use bevy_a11y::ActionRequest as ActionRequestWrapper;
109
use bevy_a11y::{
11-
accesskit::{ActionHandler, ActionRequest, NodeBuilder, NodeClassSet, Role, TreeUpdate},
12-
AccessKitEntityExt, AccessibilityNode, AccessibilityRequested, Focus,
10+
accesskit::{
11+
ActionHandler, ActionRequest, NodeBuilder, NodeClassSet, NodeId, Role, TreeUpdate,
12+
},
13+
AccessibilityNode, AccessibilityRequested, AccessibilitySystem, Focus,
1314
};
15+
use bevy_a11y::{ActionRequest as ActionRequestWrapper, ManageAccessibilityUpdates};
1416
use bevy_app::{App, Plugin, PostUpdate};
1517
use bevy_derive::{Deref, DerefMut};
1618
use bevy_ecs::{
1719
prelude::{DetectChanges, Entity, EventReader, EventWriter},
1820
query::With,
21+
schedule::IntoSystemConfigs,
1922
system::{NonSend, NonSendMut, Query, Res, ResMut, Resource},
2023
};
2124
use bevy_hierarchy::{Children, Parent};
22-
use bevy_utils::{default, HashMap};
23-
use bevy_window::{PrimaryWindow, Window, WindowClosed, WindowFocused};
25+
use bevy_utils::HashMap;
26+
use bevy_window::{PrimaryWindow, Window, WindowClosed};
2427

2528
/// Maps window entities to their `AccessKit` [`Adapter`]s.
2629
#[derive(Default, Deref, DerefMut)]
@@ -35,34 +38,12 @@ pub struct WinitActionHandlers(pub HashMap<Entity, WinitActionHandler>);
3538
pub struct WinitActionHandler(pub Arc<Mutex<VecDeque<ActionRequest>>>);
3639

3740
impl ActionHandler for WinitActionHandler {
38-
fn do_action(&self, request: ActionRequest) {
41+
fn do_action(&mut self, request: ActionRequest) {
3942
let mut requests = self.0.lock().unwrap();
4043
requests.push_back(request);
4144
}
4245
}
4346

44-
fn handle_window_focus(
45-
focus: Res<Focus>,
46-
adapters: NonSend<AccessKitAdapters>,
47-
mut focused: EventReader<WindowFocused>,
48-
) {
49-
for event in focused.read() {
50-
if let Some(adapter) = adapters.get(&event.window) {
51-
adapter.update_if_active(|| {
52-
let focus_id = (*focus).unwrap_or_else(|| event.window);
53-
TreeUpdate {
54-
focus: if event.focused {
55-
Some(focus_id.to_node_id())
56-
} else {
57-
None
58-
},
59-
..default()
60-
}
61-
});
62-
}
63-
}
64-
}
65-
6647
fn window_closed(
6748
mut adapters: NonSendMut<AccessKitAdapters>,
6849
mut receivers: ResMut<WinitActionHandlers>,
@@ -86,10 +67,16 @@ fn poll_receivers(
8667
}
8768
}
8869

70+
fn should_update_accessibility_nodes(
71+
accessibility_requested: Res<AccessibilityRequested>,
72+
manage_accessibility_updates: Res<ManageAccessibilityUpdates>,
73+
) -> bool {
74+
accessibility_requested.get() && manage_accessibility_updates.get()
75+
}
76+
8977
fn update_accessibility_nodes(
9078
adapters: NonSend<AccessKitAdapters>,
9179
focus: Res<Focus>,
92-
accessibility_requested: Res<AccessibilityRequested>,
9380
primary_window: Query<(Entity, &Window), With<PrimaryWindow>>,
9481
nodes: Query<(
9582
Entity,
@@ -99,46 +86,37 @@ fn update_accessibility_nodes(
9986
)>,
10087
node_entities: Query<Entity, With<AccessibilityNode>>,
10188
) {
102-
if !accessibility_requested.load(Ordering::SeqCst) {
103-
return;
104-
}
10589
if let Ok((primary_window_id, primary_window)) = primary_window.get_single() {
10690
if let Some(adapter) = adapters.get(&primary_window_id) {
10791
let should_run = focus.is_changed() || !nodes.is_empty();
10892
if should_run {
10993
adapter.update_if_active(|| {
11094
let mut to_update = vec![];
111-
let mut has_focus = false;
11295
let mut name = None;
11396
if primary_window.focused {
114-
has_focus = true;
11597
let title = primary_window.title.clone();
11698
name = Some(title.into_boxed_str());
11799
}
118-
let focus_id = if has_focus {
119-
(*focus).or_else(|| Some(primary_window_id))
120-
} else {
121-
None
122-
};
100+
let focus_id = (*focus).unwrap_or_else(|| primary_window_id).to_bits();
123101
let mut root_children = vec![];
124102
for (entity, node, children, parent) in &nodes {
125103
let mut node = (**node).clone();
126104
if let Some(parent) = parent {
127-
if node_entities.get(**parent).is_err() {
128-
root_children.push(entity.to_node_id());
105+
if !node_entities.contains(**parent) {
106+
root_children.push(NodeId(entity.to_bits()));
129107
}
130108
} else {
131-
root_children.push(entity.to_node_id());
109+
root_children.push(NodeId(entity.to_bits()));
132110
}
133111
if let Some(children) = children {
134112
for child in children {
135-
if node_entities.get(*child).is_ok() {
136-
node.push_child(child.to_node_id());
113+
if node_entities.contains(*child) {
114+
node.push_child(NodeId(child.to_bits()));
137115
}
138116
}
139117
}
140118
to_update.push((
141-
entity.to_node_id(),
119+
NodeId(entity.to_bits()),
142120
node.build(&mut NodeClassSet::lock_global()),
143121
));
144122
}
@@ -148,12 +126,12 @@ fn update_accessibility_nodes(
148126
}
149127
root.set_children(root_children);
150128
let root = root.build(&mut NodeClassSet::lock_global());
151-
let window_update = (primary_window_id.to_node_id(), root);
129+
let window_update = (NodeId(primary_window_id.to_bits()), root);
152130
to_update.insert(0, window_update);
153131
TreeUpdate {
154132
nodes: to_update,
155-
focus: focus_id.map(|v| v.to_node_id()),
156-
..default()
133+
tree: None,
134+
focus: NodeId(focus_id),
157135
}
158136
});
159137
}
@@ -171,12 +149,13 @@ impl Plugin for AccessibilityPlugin {
171149
.add_event::<ActionRequestWrapper>()
172150
.add_systems(
173151
PostUpdate,
174-
(
175-
handle_window_focus,
176-
window_closed,
177-
poll_receivers,
178-
update_accessibility_nodes,
179-
),
152+
(window_closed, poll_receivers).in_set(AccessibilitySystem::Update),
153+
)
154+
.add_systems(
155+
PostUpdate,
156+
update_accessibility_nodes
157+
.run_if(should_update_accessibility_nodes)
158+
.in_set(AccessibilitySystem::Update),
180159
);
181160
}
182161
}

crates/bevy_winit/src/winit_windows.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
#![warn(missing_docs)]
2-
use std::sync::atomic::Ordering;
32

43
use accesskit_winit::Adapter;
54
use bevy_a11y::{
6-
accesskit::{NodeBuilder, NodeClassSet, Role, Tree, TreeUpdate},
7-
AccessKitEntityExt, AccessibilityRequested,
5+
accesskit::{NodeBuilder, NodeClassSet, NodeId, Role, Tree, TreeUpdate},
6+
AccessibilityRequested,
87
};
98
use bevy_ecs::entity::Entity;
109

@@ -151,17 +150,17 @@ impl WinitWindows {
151150
root_builder.set_name(name.into_boxed_str());
152151
let root = root_builder.build(&mut NodeClassSet::lock_global());
153152

154-
let accesskit_window_id = entity.to_node_id();
153+
let accesskit_window_id = NodeId(entity.to_bits());
155154
let handler = WinitActionHandler::default();
156-
let accessibility_requested = (*accessibility_requested).clone();
155+
let accessibility_requested = accessibility_requested.clone();
157156
let adapter = Adapter::with_action_handler(
158157
&winit_window,
159158
move || {
160-
accessibility_requested.store(true, Ordering::SeqCst);
159+
accessibility_requested.set(true);
161160
TreeUpdate {
162161
nodes: vec![(accesskit_window_id, root)],
163162
tree: Some(Tree::new(accesskit_window_id)),
164-
focus: None,
163+
focus: accesskit_window_id,
165164
}
166165
},
167166
Box::new(handler.clone()),

0 commit comments

Comments
 (0)