mirror of
https://github.com/bevyengine/bevy
synced 2024-11-28 15:40:21 +00:00
Various accessibility API updates. (#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>
This commit is contained in:
parent
44a9a4cc86
commit
73e0ac26ca
8 changed files with 108 additions and 86 deletions
|
@ -14,4 +14,4 @@ bevy_app = { path = "../bevy_app", version = "0.12.0-dev" }
|
|||
bevy_derive = { path = "../bevy_derive", version = "0.12.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.12.0-dev" }
|
||||
|
||||
accesskit = "0.11"
|
||||
accesskit = "0.12"
|
||||
|
|
|
@ -4,17 +4,18 @@
|
|||
#![allow(clippy::type_complexity)]
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use std::{
|
||||
num::NonZeroU128,
|
||||
sync::{atomic::AtomicBool, Arc},
|
||||
use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
|
||||
pub use accesskit;
|
||||
use accesskit::{NodeBuilder, NodeId};
|
||||
use accesskit::NodeBuilder;
|
||||
use bevy_app::Plugin;
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::{
|
||||
prelude::{Component, Entity, Event},
|
||||
schedule::SystemSet,
|
||||
system::Resource,
|
||||
};
|
||||
|
||||
|
@ -30,6 +31,46 @@ pub struct ActionRequest(pub accesskit::ActionRequest);
|
|||
#[derive(Resource, Default, Clone, Debug, Deref, DerefMut)]
|
||||
pub struct AccessibilityRequested(Arc<AtomicBool>);
|
||||
|
||||
impl AccessibilityRequested {
|
||||
/// Returns `true` if an access technology is active and accessibility tree
|
||||
/// updates should be sent.
|
||||
pub fn get(&self) -> bool {
|
||||
self.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// Sets whether accessibility updates were requested by an access technology.
|
||||
pub fn set(&self, value: bool) {
|
||||
self.store(value, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
/// Resource whose value determines whether the accessibility tree is updated
|
||||
/// via the ECS.
|
||||
///
|
||||
/// Set to `false` in cases where an external GUI library is sending
|
||||
/// accessibility updates instead. Without this, the external library and ECS
|
||||
/// will generate conflicting updates.
|
||||
#[derive(Resource, Clone, Debug, Deref, DerefMut)]
|
||||
pub struct ManageAccessibilityUpdates(bool);
|
||||
|
||||
impl Default for ManageAccessibilityUpdates {
|
||||
fn default() -> Self {
|
||||
Self(true)
|
||||
}
|
||||
}
|
||||
|
||||
impl ManageAccessibilityUpdates {
|
||||
/// Returns `true` if the ECS should update the accessibility tree.
|
||||
pub fn get(&self) -> bool {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Sets whether the ECS should update the accessibility tree.
|
||||
pub fn set(&mut self, value: bool) {
|
||||
self.0 = value;
|
||||
}
|
||||
}
|
||||
|
||||
/// Component to wrap a [`accesskit::Node`], representing this entity to the platform's
|
||||
/// accessibility API.
|
||||
///
|
||||
|
@ -47,22 +88,16 @@ impl From<NodeBuilder> for AccessibilityNode {
|
|||
}
|
||||
}
|
||||
|
||||
/// Extensions to ease integrating entities with [`AccessKit`](https://accesskit.dev).
|
||||
pub trait AccessKitEntityExt {
|
||||
/// Convert an entity to a stable [`NodeId`].
|
||||
fn to_node_id(&self) -> NodeId;
|
||||
}
|
||||
|
||||
impl AccessKitEntityExt for Entity {
|
||||
fn to_node_id(&self) -> NodeId {
|
||||
let id = NonZeroU128::new(self.to_bits() as u128 + 1);
|
||||
NodeId(id.unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
/// Resource representing which entity has keyboard focus, if any.
|
||||
#[derive(Resource, Default, Deref, DerefMut)]
|
||||
pub struct Focus(Option<Entity>);
|
||||
pub struct Focus(pub Option<Entity>);
|
||||
|
||||
/// Set enum for the systems relating to accessibility
|
||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
|
||||
pub enum AccessibilitySystem {
|
||||
/// Update the accessibility tree
|
||||
Update,
|
||||
}
|
||||
|
||||
/// Plugin managing non-GUI aspects of integrating with accessibility APIs.
|
||||
pub struct AccessibilityPlugin;
|
||||
|
@ -70,6 +105,7 @@ pub struct AccessibilityPlugin;
|
|||
impl Plugin for AccessibilityPlugin {
|
||||
fn build(&self, app: &mut bevy_app::App) {
|
||||
app.init_resource::<AccessibilityRequested>()
|
||||
.init_resource::<ManageAccessibilityUpdates>()
|
||||
.init_resource::<Focus>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -124,14 +124,14 @@ fn label_changed(
|
|||
.collect::<Vec<String>>();
|
||||
let name = Some(values.join(" ").into_boxed_str());
|
||||
if let Some(mut accessible) = accessible {
|
||||
accessible.set_role(Role::LabelText);
|
||||
accessible.set_role(Role::StaticText);
|
||||
if let Some(name) = name {
|
||||
accessible.set_name(name);
|
||||
} else {
|
||||
accessible.clear_name();
|
||||
}
|
||||
} else {
|
||||
let mut node = NodeBuilder::new(Role::LabelText);
|
||||
let mut node = NodeBuilder::new(Role::StaticText);
|
||||
if let Some(name) = name {
|
||||
node.set_name(name);
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ serialize = ["serde"]
|
|||
|
||||
[dependencies]
|
||||
# bevy
|
||||
bevy_a11y = { path = "../bevy_a11y", version = "0.12.0-dev" }
|
||||
bevy_app = { path = "../bevy_app", version = "0.12.0-dev" }
|
||||
bevy_ecs = { path = "../bevy_ecs", version = "0.12.0-dev" }
|
||||
bevy_math = { path = "../bevy_math", version = "0.12.0-dev" }
|
||||
|
|
|
@ -7,6 +7,8 @@
|
|||
//! The [`WindowPlugin`] sets up some global window-related parameters and
|
||||
//! is part of the [`DefaultPlugins`](https://docs.rs/bevy/latest/bevy/struct.DefaultPlugins.html).
|
||||
|
||||
use bevy_a11y::Focus;
|
||||
|
||||
mod cursor;
|
||||
mod event;
|
||||
mod raw_handle;
|
||||
|
@ -99,9 +101,14 @@ impl Plugin for WindowPlugin {
|
|||
.add_event::<WindowThemeChanged>();
|
||||
|
||||
if let Some(primary_window) = &self.primary_window {
|
||||
app.world
|
||||
let initial_focus = app
|
||||
.world
|
||||
.spawn(primary_window.clone())
|
||||
.insert(PrimaryWindow);
|
||||
.insert(PrimaryWindow)
|
||||
.id();
|
||||
if let Some(mut focus) = app.world.get_resource_mut::<Focus>() {
|
||||
**focus = Some(initial_focus);
|
||||
}
|
||||
}
|
||||
|
||||
match self.exit_condition {
|
||||
|
|
|
@ -29,7 +29,7 @@ bevy_tasks = { path = "../bevy_tasks", version = "0.12.0-dev" }
|
|||
|
||||
# other
|
||||
winit = { version = "0.28.7", default-features = false }
|
||||
accesskit_winit = { version = "0.14", default-features = false }
|
||||
accesskit_winit = { version = "0.15", default-features = false }
|
||||
approx = { version = "0.5", default-features = false }
|
||||
raw-window-handle = "0.5"
|
||||
|
||||
|
|
|
@ -2,25 +2,28 @@
|
|||
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
sync::{atomic::Ordering, Arc, Mutex},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
use accesskit_winit::Adapter;
|
||||
use bevy_a11y::ActionRequest as ActionRequestWrapper;
|
||||
use bevy_a11y::{
|
||||
accesskit::{ActionHandler, ActionRequest, NodeBuilder, NodeClassSet, Role, TreeUpdate},
|
||||
AccessKitEntityExt, AccessibilityNode, AccessibilityRequested, Focus,
|
||||
accesskit::{
|
||||
ActionHandler, ActionRequest, NodeBuilder, NodeClassSet, NodeId, Role, TreeUpdate,
|
||||
},
|
||||
AccessibilityNode, AccessibilityRequested, AccessibilitySystem, Focus,
|
||||
};
|
||||
use bevy_a11y::{ActionRequest as ActionRequestWrapper, ManageAccessibilityUpdates};
|
||||
use bevy_app::{App, Plugin, PostUpdate};
|
||||
use bevy_derive::{Deref, DerefMut};
|
||||
use bevy_ecs::{
|
||||
prelude::{DetectChanges, Entity, EventReader, EventWriter},
|
||||
query::With,
|
||||
schedule::IntoSystemConfigs,
|
||||
system::{NonSend, NonSendMut, Query, Res, ResMut, Resource},
|
||||
};
|
||||
use bevy_hierarchy::{Children, Parent};
|
||||
use bevy_utils::{default, HashMap};
|
||||
use bevy_window::{PrimaryWindow, Window, WindowClosed, WindowFocused};
|
||||
use bevy_utils::HashMap;
|
||||
use bevy_window::{PrimaryWindow, Window, WindowClosed};
|
||||
|
||||
/// Maps window entities to their `AccessKit` [`Adapter`]s.
|
||||
#[derive(Default, Deref, DerefMut)]
|
||||
|
@ -35,34 +38,12 @@ pub struct WinitActionHandlers(pub HashMap<Entity, WinitActionHandler>);
|
|||
pub struct WinitActionHandler(pub Arc<Mutex<VecDeque<ActionRequest>>>);
|
||||
|
||||
impl ActionHandler for WinitActionHandler {
|
||||
fn do_action(&self, request: ActionRequest) {
|
||||
fn do_action(&mut self, request: ActionRequest) {
|
||||
let mut requests = self.0.lock().unwrap();
|
||||
requests.push_back(request);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_window_focus(
|
||||
focus: Res<Focus>,
|
||||
adapters: NonSend<AccessKitAdapters>,
|
||||
mut focused: EventReader<WindowFocused>,
|
||||
) {
|
||||
for event in focused.read() {
|
||||
if let Some(adapter) = adapters.get(&event.window) {
|
||||
adapter.update_if_active(|| {
|
||||
let focus_id = (*focus).unwrap_or_else(|| event.window);
|
||||
TreeUpdate {
|
||||
focus: if event.focused {
|
||||
Some(focus_id.to_node_id())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
..default()
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn window_closed(
|
||||
mut adapters: NonSendMut<AccessKitAdapters>,
|
||||
mut receivers: ResMut<WinitActionHandlers>,
|
||||
|
@ -86,10 +67,16 @@ fn poll_receivers(
|
|||
}
|
||||
}
|
||||
|
||||
fn should_update_accessibility_nodes(
|
||||
accessibility_requested: Res<AccessibilityRequested>,
|
||||
manage_accessibility_updates: Res<ManageAccessibilityUpdates>,
|
||||
) -> bool {
|
||||
accessibility_requested.get() && manage_accessibility_updates.get()
|
||||
}
|
||||
|
||||
fn update_accessibility_nodes(
|
||||
adapters: NonSend<AccessKitAdapters>,
|
||||
focus: Res<Focus>,
|
||||
accessibility_requested: Res<AccessibilityRequested>,
|
||||
primary_window: Query<(Entity, &Window), With<PrimaryWindow>>,
|
||||
nodes: Query<(
|
||||
Entity,
|
||||
|
@ -99,46 +86,37 @@ fn update_accessibility_nodes(
|
|||
)>,
|
||||
node_entities: Query<Entity, With<AccessibilityNode>>,
|
||||
) {
|
||||
if !accessibility_requested.load(Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
if let Ok((primary_window_id, primary_window)) = primary_window.get_single() {
|
||||
if let Some(adapter) = adapters.get(&primary_window_id) {
|
||||
let should_run = focus.is_changed() || !nodes.is_empty();
|
||||
if should_run {
|
||||
adapter.update_if_active(|| {
|
||||
let mut to_update = vec![];
|
||||
let mut has_focus = false;
|
||||
let mut name = None;
|
||||
if primary_window.focused {
|
||||
has_focus = true;
|
||||
let title = primary_window.title.clone();
|
||||
name = Some(title.into_boxed_str());
|
||||
}
|
||||
let focus_id = if has_focus {
|
||||
(*focus).or_else(|| Some(primary_window_id))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let focus_id = (*focus).unwrap_or_else(|| primary_window_id).to_bits();
|
||||
let mut root_children = vec![];
|
||||
for (entity, node, children, parent) in &nodes {
|
||||
let mut node = (**node).clone();
|
||||
if let Some(parent) = parent {
|
||||
if node_entities.get(**parent).is_err() {
|
||||
root_children.push(entity.to_node_id());
|
||||
if !node_entities.contains(**parent) {
|
||||
root_children.push(NodeId(entity.to_bits()));
|
||||
}
|
||||
} else {
|
||||
root_children.push(entity.to_node_id());
|
||||
root_children.push(NodeId(entity.to_bits()));
|
||||
}
|
||||
if let Some(children) = children {
|
||||
for child in children {
|
||||
if node_entities.get(*child).is_ok() {
|
||||
node.push_child(child.to_node_id());
|
||||
if node_entities.contains(*child) {
|
||||
node.push_child(NodeId(child.to_bits()));
|
||||
}
|
||||
}
|
||||
}
|
||||
to_update.push((
|
||||
entity.to_node_id(),
|
||||
NodeId(entity.to_bits()),
|
||||
node.build(&mut NodeClassSet::lock_global()),
|
||||
));
|
||||
}
|
||||
|
@ -148,12 +126,12 @@ fn update_accessibility_nodes(
|
|||
}
|
||||
root.set_children(root_children);
|
||||
let root = root.build(&mut NodeClassSet::lock_global());
|
||||
let window_update = (primary_window_id.to_node_id(), root);
|
||||
let window_update = (NodeId(primary_window_id.to_bits()), root);
|
||||
to_update.insert(0, window_update);
|
||||
TreeUpdate {
|
||||
nodes: to_update,
|
||||
focus: focus_id.map(|v| v.to_node_id()),
|
||||
..default()
|
||||
tree: None,
|
||||
focus: NodeId(focus_id),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -171,12 +149,13 @@ impl Plugin for AccessibilityPlugin {
|
|||
.add_event::<ActionRequestWrapper>()
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
handle_window_focus,
|
||||
window_closed,
|
||||
poll_receivers,
|
||||
update_accessibility_nodes,
|
||||
),
|
||||
(window_closed, poll_receivers).in_set(AccessibilitySystem::Update),
|
||||
)
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
update_accessibility_nodes
|
||||
.run_if(should_update_accessibility_nodes)
|
||||
.in_set(AccessibilitySystem::Update),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
#![warn(missing_docs)]
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use accesskit_winit::Adapter;
|
||||
use bevy_a11y::{
|
||||
accesskit::{NodeBuilder, NodeClassSet, Role, Tree, TreeUpdate},
|
||||
AccessKitEntityExt, AccessibilityRequested,
|
||||
accesskit::{NodeBuilder, NodeClassSet, NodeId, Role, Tree, TreeUpdate},
|
||||
AccessibilityRequested,
|
||||
};
|
||||
use bevy_ecs::entity::Entity;
|
||||
|
||||
|
@ -151,17 +150,17 @@ impl WinitWindows {
|
|||
root_builder.set_name(name.into_boxed_str());
|
||||
let root = root_builder.build(&mut NodeClassSet::lock_global());
|
||||
|
||||
let accesskit_window_id = entity.to_node_id();
|
||||
let accesskit_window_id = NodeId(entity.to_bits());
|
||||
let handler = WinitActionHandler::default();
|
||||
let accessibility_requested = (*accessibility_requested).clone();
|
||||
let accessibility_requested = accessibility_requested.clone();
|
||||
let adapter = Adapter::with_action_handler(
|
||||
&winit_window,
|
||||
move || {
|
||||
accessibility_requested.store(true, Ordering::SeqCst);
|
||||
accessibility_requested.set(true);
|
||||
TreeUpdate {
|
||||
nodes: vec![(accesskit_window_id, root)],
|
||||
tree: Some(Tree::new(accesskit_window_id)),
|
||||
focus: None,
|
||||
focus: accesskit_window_id,
|
||||
}
|
||||
},
|
||||
Box::new(handler.clone()),
|
||||
|
|
Loading…
Reference in a new issue