-
-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
# Objective UIs created for Bevy cannot currently be made accessible. This PR aims to address that. ## Solution Integrate AccessKit as a dependency, adding accessibility support to existing bevy_ui widgets. ## Changelog ### Added * Integrate with and expose [AccessKit](https://accesskit.dev) for platform accessibility. * Add `Label` for marking text specifically as a label for UI controls.
- Loading branch information
Showing
19 changed files
with
597 additions
and
27 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
[package] | ||
name = "bevy_a11y" | ||
version = "0.9.0" | ||
edition = "2021" | ||
description = "Provides accessibility support for Bevy Engine" | ||
homepage = "https://bevyengine.org" | ||
repository = "https://github.com/bevyengine/bevy" | ||
license = "MIT OR Apache-2.0" | ||
keywords = ["bevy", "accessibility", "a11y"] | ||
|
||
[dependencies] | ||
# bevy | ||
bevy_app = { path = "../bevy_app", version = "0.9.0" } | ||
bevy_derive = { path = "../bevy_derive", version = "0.9.0" } | ||
bevy_ecs = { path = "../bevy_ecs", version = "0.9.0" } | ||
|
||
accesskit = "0.10" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,70 @@ | ||
//! Accessibility for Bevy | ||
|
||
#![warn(missing_docs)] | ||
#![forbid(unsafe_code)] | ||
|
||
use std::{ | ||
num::NonZeroU128, | ||
sync::{atomic::AtomicBool, Arc}, | ||
}; | ||
|
||
pub use accesskit; | ||
use accesskit::{NodeBuilder, NodeId}; | ||
use bevy_app::Plugin; | ||
use bevy_derive::{Deref, DerefMut}; | ||
use bevy_ecs::{ | ||
prelude::{Component, Entity}, | ||
system::Resource, | ||
}; | ||
|
||
/// Resource that tracks whether an assistive technology has requested | ||
/// accessibility information. | ||
/// | ||
/// Useful if a third-party plugin needs to conditionally integrate with | ||
/// `AccessKit` | ||
#[derive(Resource, Default, Clone, Debug, Deref, DerefMut)] | ||
pub struct AccessibilityRequested(Arc<AtomicBool>); | ||
|
||
/// Component to wrap a [`accesskit::Node`], representing this entity to the platform's | ||
/// accessibility API. | ||
/// | ||
/// If an entity has a parent, and that parent also has an `AccessibilityNode`, | ||
/// the entity's node will be a child of the parent's node. | ||
/// | ||
/// If the entity doesn't have a parent, or if the immediate parent doesn't have | ||
/// an `AccessibilityNode`, its node will be an immediate child of the primary window. | ||
#[derive(Component, Clone, Deref, DerefMut)] | ||
pub struct AccessibilityNode(pub NodeBuilder); | ||
|
||
impl From<NodeBuilder> for AccessibilityNode { | ||
fn from(node: NodeBuilder) -> Self { | ||
Self(node) | ||
} | ||
} | ||
|
||
/// 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>); | ||
|
||
/// Plugin managing non-GUI aspects of integrating with accessibility APIs. | ||
pub struct AccessibilityPlugin; | ||
|
||
impl Plugin for AccessibilityPlugin { | ||
fn build(&self, app: &mut bevy_app::App) { | ||
app.init_resource::<AccessibilityRequested>() | ||
.init_resource::<Focus>(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
use bevy_a11y::{ | ||
accesskit::{NodeBuilder, Rect, Role}, | ||
AccessibilityNode, | ||
}; | ||
use bevy_app::{App, Plugin}; | ||
|
||
use bevy_ecs::{ | ||
prelude::Entity, | ||
query::{Changed, Or, Without}, | ||
system::{Commands, Query}, | ||
}; | ||
use bevy_hierarchy::Children; | ||
|
||
use bevy_render::prelude::Camera; | ||
use bevy_text::Text; | ||
use bevy_transform::prelude::GlobalTransform; | ||
|
||
use crate::{ | ||
prelude::{Button, Label}, | ||
Node, UiImage, | ||
}; | ||
|
||
fn calc_name(texts: &Query<&Text>, children: &Children) -> Option<Box<str>> { | ||
let mut name = None; | ||
for child in children.iter() { | ||
if let Ok(text) = texts.get(*child) { | ||
let values = text | ||
.sections | ||
.iter() | ||
.map(|v| v.value.to_string()) | ||
.collect::<Vec<String>>(); | ||
name = Some(values.join(" ")); | ||
} | ||
} | ||
name.map(|v| v.into_boxed_str()) | ||
} | ||
|
||
fn calc_bounds( | ||
camera: Query<(&Camera, &GlobalTransform)>, | ||
mut nodes: Query< | ||
(&mut AccessibilityNode, &Node, &GlobalTransform), | ||
Or<(Changed<Node>, Changed<GlobalTransform>)>, | ||
>, | ||
) { | ||
if let Ok((camera, camera_transform)) = camera.get_single() { | ||
for (mut accessible, node, transform) in &mut nodes { | ||
if let Some(translation) = | ||
camera.world_to_viewport(camera_transform, transform.translation()) | ||
{ | ||
let bounds = Rect::new( | ||
translation.x.into(), | ||
translation.y.into(), | ||
(translation.x + node.calculated_size.x).into(), | ||
(translation.y + node.calculated_size.y).into(), | ||
); | ||
accessible.set_bounds(bounds); | ||
} | ||
} | ||
} | ||
} | ||
|
||
fn button_changed( | ||
mut commands: Commands, | ||
mut query: Query<(Entity, &Children, Option<&mut AccessibilityNode>), Changed<Button>>, | ||
texts: Query<&Text>, | ||
) { | ||
for (entity, children, accessible) in &mut query { | ||
let name = calc_name(&texts, children); | ||
if let Some(mut accessible) = accessible { | ||
accessible.set_role(Role::Button); | ||
if let Some(name) = name { | ||
accessible.set_name(name); | ||
} else { | ||
accessible.clear_name(); | ||
} | ||
} else { | ||
let mut node = NodeBuilder::new(Role::Button); | ||
if let Some(name) = name { | ||
node.set_name(name); | ||
} | ||
commands | ||
.entity(entity) | ||
.insert(AccessibilityNode::from(node)); | ||
} | ||
} | ||
} | ||
|
||
fn image_changed( | ||
mut commands: Commands, | ||
mut query: Query< | ||
(Entity, &Children, Option<&mut AccessibilityNode>), | ||
(Changed<UiImage>, Without<Button>), | ||
>, | ||
texts: Query<&Text>, | ||
) { | ||
for (entity, children, accessible) in &mut query { | ||
let name = calc_name(&texts, children); | ||
if let Some(mut accessible) = accessible { | ||
accessible.set_role(Role::Image); | ||
if let Some(name) = name { | ||
accessible.set_name(name); | ||
} else { | ||
accessible.clear_name(); | ||
} | ||
} else { | ||
let mut node = NodeBuilder::new(Role::Image); | ||
if let Some(name) = name { | ||
node.set_name(name); | ||
} | ||
commands | ||
.entity(entity) | ||
.insert(AccessibilityNode::from(node)); | ||
} | ||
} | ||
} | ||
|
||
fn label_changed( | ||
mut commands: Commands, | ||
mut query: Query<(Entity, &Text, Option<&mut AccessibilityNode>), Changed<Label>>, | ||
) { | ||
for (entity, text, accessible) in &mut query { | ||
let values = text | ||
.sections | ||
.iter() | ||
.map(|v| v.value.to_string()) | ||
.collect::<Vec<String>>(); | ||
let name = Some(values.join(" ").into_boxed_str()); | ||
if let Some(mut accessible) = accessible { | ||
accessible.set_role(Role::LabelText); | ||
if let Some(name) = name { | ||
accessible.set_name(name); | ||
} else { | ||
accessible.clear_name(); | ||
} | ||
} else { | ||
let mut node = NodeBuilder::new(Role::LabelText); | ||
if let Some(name) = name { | ||
node.set_name(name); | ||
} | ||
commands | ||
.entity(entity) | ||
.insert(AccessibilityNode::from(node)); | ||
} | ||
} | ||
} | ||
|
||
/// `AccessKit` integration for `bevy_ui`. | ||
pub(crate) struct AccessibilityPlugin; | ||
|
||
impl Plugin for AccessibilityPlugin { | ||
fn build(&self, app: &mut App) { | ||
app.add_system(calc_bounds) | ||
.add_system(button_changed) | ||
.add_system(image_changed) | ||
.add_system(label_changed); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
use bevy_ecs::prelude::Component; | ||
use bevy_ecs::reflect::ReflectComponent; | ||
use bevy_reflect::std_traits::ReflectDefault; | ||
use bevy_reflect::Reflect; | ||
|
||
/// Marker struct for labels | ||
#[derive(Component, Debug, Default, Clone, Copy, Reflect)] | ||
#[reflect(Component, Default)] | ||
pub struct Label; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.