Integrate AccessKit (#6874)
# 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.
This commit is contained in:
parent
abcb0661e3
commit
8d1f6ff7fa
@ -209,6 +209,9 @@ detailed_trace = ["bevy_internal/detailed_trace"]
|
|||||||
# Include tonemapping Look Up Tables KTX2 files
|
# Include tonemapping Look Up Tables KTX2 files
|
||||||
tonemapping_luts = ["bevy_internal/tonemapping_luts"]
|
tonemapping_luts = ["bevy_internal/tonemapping_luts"]
|
||||||
|
|
||||||
|
# Enable AccessKit on Unix backends (currently only works with experimental screen readers and forks.)
|
||||||
|
accesskit_unix = ["bevy_internal/accesskit_unix"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bevy_dylib = { path = "crates/bevy_dylib", version = "0.9.0", default-features = false, optional = true }
|
bevy_dylib = { path = "crates/bevy_dylib", version = "0.9.0", default-features = false, optional = true }
|
||||||
bevy_internal = { path = "crates/bevy_internal", version = "0.9.0", default-features = false }
|
bevy_internal = { path = "crates/bevy_internal", version = "0.9.0", default-features = false }
|
||||||
|
17
crates/bevy_a11y/Cargo.toml
Normal file
17
crates/bevy_a11y/Cargo.toml
Normal file
@ -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"
|
70
crates/bevy_a11y/src/lib.rs
Normal file
70
crates/bevy_a11y/src/lib.rs
Normal file
@ -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>();
|
||||||
|
}
|
||||||
|
}
|
@ -82,10 +82,15 @@ dynamic_linking = ["bevy_diagnostic/dynamic_linking"]
|
|||||||
# Enable using a shared stdlib for cxx on Android.
|
# Enable using a shared stdlib for cxx on Android.
|
||||||
android_shared_stdcxx = ["bevy_audio/android_shared_stdcxx"]
|
android_shared_stdcxx = ["bevy_audio/android_shared_stdcxx"]
|
||||||
|
|
||||||
|
# Enable AccessKit on Unix backends (currently only works with experimental
|
||||||
|
# screen readers and forks.)
|
||||||
|
accesskit_unix = ["bevy_winit/accesskit_unix"]
|
||||||
|
|
||||||
bevy_text = ["dep:bevy_text", "bevy_ui?/bevy_text"]
|
bevy_text = ["dep:bevy_text", "bevy_ui?/bevy_text"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# bevy
|
# bevy
|
||||||
|
bevy_a11y = { path = "../bevy_a11y", version = "0.9.0" }
|
||||||
bevy_app = { path = "../bevy_app", version = "0.9.0" }
|
bevy_app = { path = "../bevy_app", version = "0.9.0" }
|
||||||
bevy_core = { path = "../bevy_core", version = "0.9.0" }
|
bevy_core = { path = "../bevy_core", version = "0.9.0" }
|
||||||
bevy_derive = { path = "../bevy_derive", version = "0.9.0" }
|
bevy_derive = { path = "../bevy_derive", version = "0.9.0" }
|
||||||
|
@ -50,7 +50,8 @@ impl PluginGroup for DefaultPlugins {
|
|||||||
.add(bevy_hierarchy::HierarchyPlugin::default())
|
.add(bevy_hierarchy::HierarchyPlugin::default())
|
||||||
.add(bevy_diagnostic::DiagnosticsPlugin::default())
|
.add(bevy_diagnostic::DiagnosticsPlugin::default())
|
||||||
.add(bevy_input::InputPlugin::default())
|
.add(bevy_input::InputPlugin::default())
|
||||||
.add(bevy_window::WindowPlugin::default());
|
.add(bevy_window::WindowPlugin::default())
|
||||||
|
.add(bevy_a11y::AccessibilityPlugin);
|
||||||
|
|
||||||
#[cfg(feature = "bevy_asset")]
|
#[cfg(feature = "bevy_asset")]
|
||||||
{
|
{
|
||||||
|
@ -7,6 +7,11 @@ pub mod prelude;
|
|||||||
mod default_plugins;
|
mod default_plugins;
|
||||||
pub use default_plugins::*;
|
pub use default_plugins::*;
|
||||||
|
|
||||||
|
pub mod a11y {
|
||||||
|
//! Integrate with platform accessibility APIs.
|
||||||
|
pub use bevy_a11y::*;
|
||||||
|
}
|
||||||
|
|
||||||
pub mod app {
|
pub mod app {
|
||||||
//! Build bevy apps, create plugins, and read events.
|
//! Build bevy apps, create plugins, and read events.
|
||||||
pub use bevy_app::*;
|
pub use bevy_app::*;
|
||||||
|
@ -10,6 +10,7 @@ keywords = ["bevy"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# bevy
|
# bevy
|
||||||
|
bevy_a11y = { path = "../bevy_a11y", version = "0.9.0" }
|
||||||
bevy_app = { path = "../bevy_app", version = "0.9.0" }
|
bevy_app = { path = "../bevy_app", version = "0.9.0" }
|
||||||
bevy_asset = { path = "../bevy_asset", version = "0.9.0" }
|
bevy_asset = { path = "../bevy_asset", version = "0.9.0" }
|
||||||
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.9.0" }
|
bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.9.0" }
|
||||||
|
157
crates/bevy_ui/src/accessibility.rs
Normal file
157
crates/bevy_ui/src/accessibility.rs
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -9,6 +9,7 @@ mod render;
|
|||||||
mod stack;
|
mod stack;
|
||||||
mod ui_node;
|
mod ui_node;
|
||||||
|
|
||||||
|
mod accessibility;
|
||||||
pub mod camera_config;
|
pub mod camera_config;
|
||||||
pub mod node_bundles;
|
pub mod node_bundles;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
@ -27,8 +28,7 @@ pub use ui_node::*;
|
|||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub use crate::{
|
pub use crate::{
|
||||||
camera_config::*, geometry::*, node_bundles::*, ui_node::*, widget::Button, Interaction,
|
camera_config::*, geometry::*, node_bundles::*, ui_node::*, widget::*, Interaction, UiScale,
|
||||||
UiScale,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +102,8 @@ impl Plugin for UiPlugin {
|
|||||||
.register_type::<UiImage>()
|
.register_type::<UiImage>()
|
||||||
.register_type::<Val>()
|
.register_type::<Val>()
|
||||||
.register_type::<widget::Button>()
|
.register_type::<widget::Button>()
|
||||||
|
.register_type::<widget::Label>()
|
||||||
|
.add_plugin(accessibility::AccessibilityPlugin)
|
||||||
.configure_set(UiSystem::Focus.in_base_set(CoreSet::PreUpdate))
|
.configure_set(UiSystem::Focus.in_base_set(CoreSet::PreUpdate))
|
||||||
.configure_set(UiSystem::Flex.in_base_set(CoreSet::PostUpdate))
|
.configure_set(UiSystem::Flex.in_base_set(CoreSet::PostUpdate))
|
||||||
.configure_set(UiSystem::Stack.in_base_set(CoreSet::PostUpdate))
|
.configure_set(UiSystem::Stack.in_base_set(CoreSet::PostUpdate))
|
||||||
|
9
crates/bevy_ui/src/widget/label.rs
Normal file
9
crates/bevy_ui/src/widget/label.rs
Normal file
@ -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;
|
@ -2,10 +2,12 @@
|
|||||||
|
|
||||||
mod button;
|
mod button;
|
||||||
mod image;
|
mod image;
|
||||||
|
mod label;
|
||||||
#[cfg(feature = "bevy_text")]
|
#[cfg(feature = "bevy_text")]
|
||||||
mod text;
|
mod text;
|
||||||
|
|
||||||
pub use button::*;
|
pub use button::*;
|
||||||
pub use image::*;
|
pub use image::*;
|
||||||
|
pub use label::*;
|
||||||
#[cfg(feature = "bevy_text")]
|
#[cfg(feature = "bevy_text")]
|
||||||
pub use text::*;
|
pub use text::*;
|
||||||
|
@ -12,11 +12,15 @@ keywords = ["bevy"]
|
|||||||
trace = []
|
trace = []
|
||||||
wayland = ["winit/wayland"]
|
wayland = ["winit/wayland"]
|
||||||
x11 = ["winit/x11"]
|
x11 = ["winit/x11"]
|
||||||
|
accesskit_unix = ["accesskit_winit/accesskit_unix"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# bevy
|
# bevy
|
||||||
|
bevy_a11y = { path = "../bevy_a11y", version = "0.9.0" }
|
||||||
bevy_app = { path = "../bevy_app", version = "0.9.0" }
|
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" }
|
bevy_ecs = { path = "../bevy_ecs", version = "0.9.0" }
|
||||||
|
bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.9.0" }
|
||||||
bevy_input = { path = "../bevy_input", version = "0.9.0" }
|
bevy_input = { path = "../bevy_input", version = "0.9.0" }
|
||||||
bevy_math = { path = "../bevy_math", version = "0.9.0" }
|
bevy_math = { path = "../bevy_math", version = "0.9.0" }
|
||||||
bevy_window = { path = "../bevy_window", version = "0.9.0" }
|
bevy_window = { path = "../bevy_window", version = "0.9.0" }
|
||||||
@ -24,6 +28,7 @@ bevy_utils = { path = "../bevy_utils", version = "0.9.0" }
|
|||||||
|
|
||||||
# other
|
# other
|
||||||
winit = { version = "0.28", default-features = false }
|
winit = { version = "0.28", default-features = false }
|
||||||
|
accesskit_winit = { version = "0.12", default-features = false }
|
||||||
approx = { version = "0.5", default-features = false }
|
approx = { version = "0.5", default-features = false }
|
||||||
raw-window-handle = "0.5"
|
raw-window-handle = "0.5"
|
||||||
|
|
||||||
|
171
crates/bevy_winit/src/accessibility.rs
Normal file
171
crates/bevy_winit/src/accessibility.rs
Normal file
@ -0,0 +1,171 @@
|
|||||||
|
use std::{
|
||||||
|
collections::VecDeque,
|
||||||
|
sync::{atomic::Ordering, Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
use accesskit_winit::Adapter;
|
||||||
|
use bevy_a11y::{
|
||||||
|
accesskit::{ActionHandler, ActionRequest, NodeBuilder, NodeClassSet, Role, TreeUpdate},
|
||||||
|
AccessKitEntityExt, AccessibilityNode, AccessibilityRequested, Focus,
|
||||||
|
};
|
||||||
|
use bevy_app::{App, Plugin};
|
||||||
|
use bevy_derive::{Deref, DerefMut};
|
||||||
|
use bevy_ecs::{
|
||||||
|
prelude::{DetectChanges, Entity, EventReader, EventWriter},
|
||||||
|
query::With,
|
||||||
|
system::{NonSend, NonSendMut, Query, Res, ResMut, Resource},
|
||||||
|
};
|
||||||
|
use bevy_hierarchy::{Children, Parent};
|
||||||
|
use bevy_utils::{default, HashMap};
|
||||||
|
use bevy_window::{PrimaryWindow, Window, WindowClosed, WindowFocused};
|
||||||
|
|
||||||
|
/// Maps window entities to their `AccessKit` [`Adapter`]s.
|
||||||
|
#[derive(Default, Deref, DerefMut)]
|
||||||
|
pub struct AccessKitAdapters(pub HashMap<Entity, Adapter>);
|
||||||
|
|
||||||
|
/// Maps window entities to their respective [`WinitActionHandler`]s.
|
||||||
|
#[derive(Resource, Default, Deref, DerefMut)]
|
||||||
|
pub struct WinitActionHandlers(pub HashMap<Entity, WinitActionHandler>);
|
||||||
|
|
||||||
|
/// Forwards `AccessKit` [`ActionRequest`]s from winit to an event channel.
|
||||||
|
#[derive(Clone, Default, Deref, DerefMut)]
|
||||||
|
pub struct WinitActionHandler(pub Arc<Mutex<VecDeque<ActionRequest>>>);
|
||||||
|
|
||||||
|
impl ActionHandler for WinitActionHandler {
|
||||||
|
fn do_action(&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.iter() {
|
||||||
|
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>,
|
||||||
|
mut events: EventReader<WindowClosed>,
|
||||||
|
) {
|
||||||
|
for WindowClosed { window, .. } in events.iter() {
|
||||||
|
adapters.remove(window);
|
||||||
|
receivers.remove(window);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_receivers(handlers: Res<WinitActionHandlers>, mut actions: EventWriter<ActionRequest>) {
|
||||||
|
for (_id, handler) in handlers.iter() {
|
||||||
|
let mut handler = handler.lock().unwrap();
|
||||||
|
while let Some(event) = handler.pop_front() {
|
||||||
|
actions.send(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_accessibility_nodes(
|
||||||
|
adapters: NonSend<AccessKitAdapters>,
|
||||||
|
focus: Res<Focus>,
|
||||||
|
accessibility_requested: Res<AccessibilityRequested>,
|
||||||
|
primary_window: Query<(Entity, &Window), With<PrimaryWindow>>,
|
||||||
|
nodes: Query<(
|
||||||
|
Entity,
|
||||||
|
&AccessibilityNode,
|
||||||
|
Option<&Children>,
|
||||||
|
Option<&Parent>,
|
||||||
|
)>,
|
||||||
|
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 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());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
root_children.push(entity.to_node_id());
|
||||||
|
}
|
||||||
|
if let Some(children) = children {
|
||||||
|
for child in children.iter() {
|
||||||
|
if node_entities.get(*child).is_ok() {
|
||||||
|
node.push_child(child.to_node_id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
to_update.push((
|
||||||
|
entity.to_node_id(),
|
||||||
|
node.build(&mut NodeClassSet::lock_global()),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let mut root = NodeBuilder::new(Role::Window);
|
||||||
|
if let Some(name) = name {
|
||||||
|
root.set_name(name);
|
||||||
|
}
|
||||||
|
root.set_children(root_children);
|
||||||
|
let root = root.build(&mut NodeClassSet::lock_global());
|
||||||
|
let window_update = (primary_window_id.to_node_id(), root);
|
||||||
|
to_update.insert(0, window_update);
|
||||||
|
TreeUpdate {
|
||||||
|
nodes: to_update,
|
||||||
|
focus: focus_id.map(|v| v.to_node_id()),
|
||||||
|
..default()
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implements winit-specific `AccessKit` functionality.
|
||||||
|
pub struct AccessibilityPlugin;
|
||||||
|
|
||||||
|
impl Plugin for AccessibilityPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.init_non_send_resource::<AccessKitAdapters>()
|
||||||
|
.init_resource::<WinitActionHandlers>()
|
||||||
|
.add_event::<ActionRequest>()
|
||||||
|
.add_system(handle_window_focus)
|
||||||
|
.add_system(window_closed)
|
||||||
|
.add_system(poll_receivers)
|
||||||
|
.add_system(update_accessibility_nodes);
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
pub mod accessibility;
|
||||||
mod converters;
|
mod converters;
|
||||||
mod system;
|
mod system;
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@ -5,6 +6,7 @@ mod web_resize;
|
|||||||
mod winit_config;
|
mod winit_config;
|
||||||
mod winit_windows;
|
mod winit_windows;
|
||||||
|
|
||||||
|
use bevy_a11y::AccessibilityRequested;
|
||||||
use bevy_ecs::system::{SystemParam, SystemState};
|
use bevy_ecs::system::{SystemParam, SystemState};
|
||||||
use system::{changed_window, create_window, despawn_window, CachedWindow};
|
use system::{changed_window, create_window, despawn_window, CachedWindow};
|
||||||
|
|
||||||
@ -39,6 +41,8 @@ use winit::{
|
|||||||
event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopWindowTarget},
|
event_loop::{ControlFlow, EventLoop, EventLoopBuilder, EventLoopWindowTarget},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::accessibility::{AccessKitAdapters, AccessibilityPlugin, WinitActionHandlers};
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use crate::web_resize::{CanvasParentResizeEventChannel, CanvasParentResizePlugin};
|
use crate::web_resize::{CanvasParentResizeEventChannel, CanvasParentResizePlugin};
|
||||||
|
|
||||||
@ -80,6 +84,8 @@ impl Plugin for WinitPlugin {
|
|||||||
.in_base_set(CoreSet::Last),
|
.in_base_set(CoreSet::Last),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
app.add_plugin(AccessibilityPlugin);
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
app.add_plugin(CanvasParentResizePlugin);
|
app.add_plugin(CanvasParentResizePlugin);
|
||||||
|
|
||||||
@ -90,6 +96,9 @@ impl Plugin for WinitPlugin {
|
|||||||
Query<(Entity, &mut Window)>,
|
Query<(Entity, &mut Window)>,
|
||||||
EventWriter<WindowCreated>,
|
EventWriter<WindowCreated>,
|
||||||
NonSendMut<WinitWindows>,
|
NonSendMut<WinitWindows>,
|
||||||
|
NonSendMut<AccessKitAdapters>,
|
||||||
|
ResMut<WinitActionHandlers>,
|
||||||
|
ResMut<AccessibilityRequested>,
|
||||||
)> = SystemState::from_world(&mut app.world);
|
)> = SystemState::from_world(&mut app.world);
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@ -99,6 +108,9 @@ impl Plugin for WinitPlugin {
|
|||||||
Query<(Entity, &mut Window)>,
|
Query<(Entity, &mut Window)>,
|
||||||
EventWriter<WindowCreated>,
|
EventWriter<WindowCreated>,
|
||||||
NonSendMut<WinitWindows>,
|
NonSendMut<WinitWindows>,
|
||||||
|
NonSendMut<AccessKitAdapters>,
|
||||||
|
ResMut<WinitActionHandlers>,
|
||||||
|
ResMut<AccessibilityRequested>,
|
||||||
ResMut<CanvasParentResizeEventChannel>,
|
ResMut<CanvasParentResizeEventChannel>,
|
||||||
)> = SystemState::from_world(&mut app.world);
|
)> = SystemState::from_world(&mut app.world);
|
||||||
|
|
||||||
@ -107,12 +119,29 @@ impl Plugin for WinitPlugin {
|
|||||||
#[cfg(not(any(target_os = "android", target_os = "ios", target_os = "macos")))]
|
#[cfg(not(any(target_os = "android", target_os = "ios", target_os = "macos")))]
|
||||||
{
|
{
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let (commands, event_loop, mut new_windows, event_writer, winit_windows) =
|
let (
|
||||||
create_window_system_state.get_mut(&mut app.world);
|
commands,
|
||||||
|
event_loop,
|
||||||
|
mut new_windows,
|
||||||
|
event_writer,
|
||||||
|
winit_windows,
|
||||||
|
adapters,
|
||||||
|
handlers,
|
||||||
|
accessibility_requested,
|
||||||
|
) = create_window_system_state.get_mut(&mut app.world);
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
let (commands, event_loop, mut new_windows, event_writer, winit_windows, event_channel) =
|
let (
|
||||||
create_window_system_state.get_mut(&mut app.world);
|
commands,
|
||||||
|
event_loop,
|
||||||
|
mut new_windows,
|
||||||
|
event_writer,
|
||||||
|
winit_windows,
|
||||||
|
adapters,
|
||||||
|
handlers,
|
||||||
|
accessibility_requested,
|
||||||
|
event_channel,
|
||||||
|
) = create_window_system_state.get_mut(&mut app.world);
|
||||||
|
|
||||||
// Here we need to create a winit-window and give it a WindowHandle which the renderer can use.
|
// Here we need to create a winit-window and give it a WindowHandle which the renderer can use.
|
||||||
// It needs to be spawned before the start of the startup-stage, so we cannot use a regular system.
|
// It needs to be spawned before the start of the startup-stage, so we cannot use a regular system.
|
||||||
@ -123,6 +152,9 @@ impl Plugin for WinitPlugin {
|
|||||||
new_windows.iter_mut(),
|
new_windows.iter_mut(),
|
||||||
event_writer,
|
event_writer,
|
||||||
winit_windows,
|
winit_windows,
|
||||||
|
adapters,
|
||||||
|
handlers,
|
||||||
|
accessibility_requested,
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
event_channel,
|
event_channel,
|
||||||
);
|
);
|
||||||
@ -264,6 +296,9 @@ pub fn winit_runner(mut app: App) {
|
|||||||
Query<(Entity, &mut Window), Added<Window>>,
|
Query<(Entity, &mut Window), Added<Window>>,
|
||||||
EventWriter<WindowCreated>,
|
EventWriter<WindowCreated>,
|
||||||
NonSendMut<WinitWindows>,
|
NonSendMut<WinitWindows>,
|
||||||
|
NonSendMut<AccessKitAdapters>,
|
||||||
|
ResMut<WinitActionHandlers>,
|
||||||
|
ResMut<AccessibilityRequested>,
|
||||||
)> = SystemState::from_world(&mut app.world);
|
)> = SystemState::from_world(&mut app.world);
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@ -272,6 +307,9 @@ pub fn winit_runner(mut app: App) {
|
|||||||
Query<(Entity, &mut Window), Added<Window>>,
|
Query<(Entity, &mut Window), Added<Window>>,
|
||||||
EventWriter<WindowCreated>,
|
EventWriter<WindowCreated>,
|
||||||
NonSendMut<WinitWindows>,
|
NonSendMut<WinitWindows>,
|
||||||
|
NonSendMut<AccessKitAdapters>,
|
||||||
|
ResMut<WinitActionHandlers>,
|
||||||
|
ResMut<AccessibilityRequested>,
|
||||||
ResMut<CanvasParentResizeEventChannel>,
|
ResMut<CanvasParentResizeEventChannel>,
|
||||||
)> = SystemState::from_world(&mut app.world);
|
)> = SystemState::from_world(&mut app.world);
|
||||||
|
|
||||||
@ -646,8 +684,15 @@ pub fn winit_runner(mut app: App) {
|
|||||||
|
|
||||||
if winit_state.active {
|
if winit_state.active {
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let (commands, mut new_windows, created_window_writer, winit_windows) =
|
let (
|
||||||
create_window_system_state.get_mut(&mut app.world);
|
commands,
|
||||||
|
mut new_windows,
|
||||||
|
created_window_writer,
|
||||||
|
winit_windows,
|
||||||
|
adapters,
|
||||||
|
handlers,
|
||||||
|
accessibility_requested,
|
||||||
|
) = create_window_system_state.get_mut(&mut app.world);
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
let (
|
let (
|
||||||
@ -655,6 +700,9 @@ pub fn winit_runner(mut app: App) {
|
|||||||
mut new_windows,
|
mut new_windows,
|
||||||
created_window_writer,
|
created_window_writer,
|
||||||
winit_windows,
|
winit_windows,
|
||||||
|
adapters,
|
||||||
|
handlers,
|
||||||
|
accessibility_requested,
|
||||||
canvas_parent_resize_channel,
|
canvas_parent_resize_channel,
|
||||||
) = create_window_system_state.get_mut(&mut app.world);
|
) = create_window_system_state.get_mut(&mut app.world);
|
||||||
|
|
||||||
@ -665,6 +713,9 @@ pub fn winit_runner(mut app: App) {
|
|||||||
new_windows.iter_mut(),
|
new_windows.iter_mut(),
|
||||||
created_window_writer,
|
created_window_writer,
|
||||||
winit_windows,
|
winit_windows,
|
||||||
|
adapters,
|
||||||
|
handlers,
|
||||||
|
accessibility_requested,
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
canvas_parent_resize_channel,
|
canvas_parent_resize_channel,
|
||||||
);
|
);
|
||||||
|
@ -1,9 +1,10 @@
|
|||||||
|
use bevy_a11y::AccessibilityRequested;
|
||||||
use bevy_ecs::{
|
use bevy_ecs::{
|
||||||
entity::Entity,
|
entity::Entity,
|
||||||
event::EventWriter,
|
event::EventWriter,
|
||||||
prelude::{Changed, Component, Resource},
|
prelude::{Changed, Component, Resource},
|
||||||
removal_detection::RemovedComponents,
|
removal_detection::RemovedComponents,
|
||||||
system::{Commands, NonSendMut, Query},
|
system::{Commands, NonSendMut, Query, ResMut},
|
||||||
world::Mut,
|
world::Mut,
|
||||||
};
|
};
|
||||||
use bevy_utils::{
|
use bevy_utils::{
|
||||||
@ -21,22 +22,25 @@ use winit::{
|
|||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
use crate::web_resize::{CanvasParentResizeEventChannel, WINIT_CANVAS_SELECTOR};
|
use crate::web_resize::{CanvasParentResizeEventChannel, WINIT_CANVAS_SELECTOR};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
accessibility::{AccessKitAdapters, WinitActionHandlers},
|
||||||
converters::{self, convert_window_level},
|
converters::{self, convert_window_level},
|
||||||
get_best_videomode, get_fitting_videomode, WinitWindows,
|
get_best_videomode, get_fitting_videomode, WinitWindows,
|
||||||
};
|
};
|
||||||
#[cfg(target_arch = "wasm32")]
|
|
||||||
use bevy_ecs::system::ResMut;
|
|
||||||
|
|
||||||
/// System responsible for creating new windows whenever a `Window` component is added
|
/// System responsible for creating new windows whenever a `Window` component is added
|
||||||
/// to an entity.
|
/// to an entity.
|
||||||
///
|
///
|
||||||
/// This will default any necessary components if they are not already added.
|
/// This will default any necessary components if they are not already added.
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
pub(crate) fn create_window<'a>(
|
pub(crate) fn create_window<'a>(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
event_loop: &EventLoopWindowTarget<()>,
|
event_loop: &EventLoopWindowTarget<()>,
|
||||||
created_windows: impl Iterator<Item = (Entity, Mut<'a, Window>)>,
|
created_windows: impl Iterator<Item = (Entity, Mut<'a, Window>)>,
|
||||||
mut event_writer: EventWriter<WindowCreated>,
|
mut event_writer: EventWriter<WindowCreated>,
|
||||||
mut winit_windows: NonSendMut<WinitWindows>,
|
mut winit_windows: NonSendMut<WinitWindows>,
|
||||||
|
mut adapters: NonSendMut<AccessKitAdapters>,
|
||||||
|
mut handlers: ResMut<WinitActionHandlers>,
|
||||||
|
mut accessibility_requested: ResMut<AccessibilityRequested>,
|
||||||
#[cfg(target_arch = "wasm32")] event_channel: ResMut<CanvasParentResizeEventChannel>,
|
#[cfg(target_arch = "wasm32")] event_channel: ResMut<CanvasParentResizeEventChannel>,
|
||||||
) {
|
) {
|
||||||
for (entity, mut window) in created_windows {
|
for (entity, mut window) in created_windows {
|
||||||
@ -50,7 +54,14 @@ pub(crate) fn create_window<'a>(
|
|||||||
entity
|
entity
|
||||||
);
|
);
|
||||||
|
|
||||||
let winit_window = winit_windows.create_window(event_loop, entity, &window);
|
let winit_window = winit_windows.create_window(
|
||||||
|
event_loop,
|
||||||
|
entity,
|
||||||
|
&window,
|
||||||
|
&mut adapters,
|
||||||
|
&mut handlers,
|
||||||
|
&mut accessibility_requested,
|
||||||
|
);
|
||||||
window
|
window
|
||||||
.resolution
|
.resolution
|
||||||
.set_scale_factor(winit_window.scale_factor());
|
.set_scale_factor(winit_window.scale_factor());
|
||||||
|
@ -1,3 +1,10 @@
|
|||||||
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
use accesskit_winit::Adapter;
|
||||||
|
use bevy_a11y::{
|
||||||
|
accesskit::{NodeBuilder, NodeClassSet, Role, Tree, TreeUpdate},
|
||||||
|
AccessKitEntityExt, AccessibilityRequested,
|
||||||
|
};
|
||||||
use bevy_ecs::entity::Entity;
|
use bevy_ecs::entity::Entity;
|
||||||
|
|
||||||
use bevy_utils::{tracing::warn, HashMap};
|
use bevy_utils::{tracing::warn, HashMap};
|
||||||
@ -8,7 +15,10 @@ use winit::{
|
|||||||
monitor::MonitorHandle,
|
monitor::MonitorHandle,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::converters::convert_window_level;
|
use crate::{
|
||||||
|
accessibility::{AccessKitAdapters, WinitActionHandler, WinitActionHandlers},
|
||||||
|
converters::convert_window_level,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct WinitWindows {
|
pub struct WinitWindows {
|
||||||
@ -27,9 +37,16 @@ impl WinitWindows {
|
|||||||
event_loop: &winit::event_loop::EventLoopWindowTarget<()>,
|
event_loop: &winit::event_loop::EventLoopWindowTarget<()>,
|
||||||
entity: Entity,
|
entity: Entity,
|
||||||
window: &Window,
|
window: &Window,
|
||||||
|
adapters: &mut AccessKitAdapters,
|
||||||
|
handlers: &mut WinitActionHandlers,
|
||||||
|
accessibility_requested: &mut AccessibilityRequested,
|
||||||
) -> &winit::window::Window {
|
) -> &winit::window::Window {
|
||||||
let mut winit_window_builder = winit::window::WindowBuilder::new();
|
let mut winit_window_builder = winit::window::WindowBuilder::new();
|
||||||
|
|
||||||
|
// Due to a UIA limitation, winit windows need to be invisible for the
|
||||||
|
// AccessKit adapter is initialized.
|
||||||
|
winit_window_builder = winit_window_builder.with_visible(false);
|
||||||
|
|
||||||
winit_window_builder = match window.mode {
|
winit_window_builder = match window.mode {
|
||||||
WindowMode::BorderlessFullscreen => winit_window_builder.with_fullscreen(Some(
|
WindowMode::BorderlessFullscreen => winit_window_builder.with_fullscreen(Some(
|
||||||
winit::window::Fullscreen::Borderless(event_loop.primary_monitor()),
|
winit::window::Fullscreen::Borderless(event_loop.primary_monitor()),
|
||||||
@ -118,6 +135,30 @@ impl WinitWindows {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let winit_window = winit_window_builder.build(event_loop).unwrap();
|
let winit_window = winit_window_builder.build(event_loop).unwrap();
|
||||||
|
let name = window.title.clone();
|
||||||
|
|
||||||
|
let mut root_builder = NodeBuilder::new(Role::Window);
|
||||||
|
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 handler = WinitActionHandler::default();
|
||||||
|
let accessibility_requested = (*accessibility_requested).clone();
|
||||||
|
let adapter = Adapter::with_action_handler(
|
||||||
|
&winit_window,
|
||||||
|
move || {
|
||||||
|
accessibility_requested.store(true, Ordering::SeqCst);
|
||||||
|
TreeUpdate {
|
||||||
|
nodes: vec![(accesskit_window_id, root)],
|
||||||
|
tree: Some(Tree::new(accesskit_window_id)),
|
||||||
|
focus: None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Box::new(handler.clone()),
|
||||||
|
);
|
||||||
|
adapters.insert(entity, adapter);
|
||||||
|
handlers.insert(entity, handler);
|
||||||
|
winit_window.set_visible(true);
|
||||||
|
|
||||||
// Do not set the grab mode on window creation if it's none, this can fail on mobile
|
// Do not set the grab mode on window creation if it's none, this can fail on mobile
|
||||||
if window.cursor.grab_mode != CursorGrabMode::None {
|
if window.cursor.grab_mode != CursorGrabMode::None {
|
||||||
|
@ -39,6 +39,7 @@ The default feature set enables most of the expected features of a game engine,
|
|||||||
|
|
||||||
|feature name|description|
|
|feature name|description|
|
||||||
|-|-|
|
|-|-|
|
||||||
|
|accesskit_unix|Enable AccessKit on Unix backends (currently only works with experimental screen readers and forks.)|
|
||||||
|basis-universal|Basis Universal compressed texture support|
|
|basis-universal|Basis Universal compressed texture support|
|
||||||
|bevy_ci_testing|Enable systems that allow for automated testing on CI|
|
|bevy_ci_testing|Enable systems that allow for automated testing on CI|
|
||||||
|bevy_dynamic_plugin|Plugin for dynamic loading (using [libloading](https://crates.io/crates/libloading))|
|
|bevy_dynamic_plugin|Plugin for dynamic loading (using [libloading](https://crates.io/crates/libloading))|
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
//! This example illustrates the various features of Bevy UI.
|
//! This example illustrates the various features of Bevy UI.
|
||||||
|
|
||||||
use bevy::{
|
use bevy::{
|
||||||
|
a11y::{
|
||||||
|
accesskit::{NodeBuilder, Role},
|
||||||
|
AccessibilityNode,
|
||||||
|
},
|
||||||
input::mouse::{MouseScrollUnit, MouseWheel},
|
input::mouse::{MouseScrollUnit, MouseWheel},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
winit::WinitSettings,
|
winit::WinitSettings,
|
||||||
@ -55,7 +59,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
})
|
})
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
// text
|
// text
|
||||||
parent.spawn(
|
parent.spawn((
|
||||||
TextBundle::from_section(
|
TextBundle::from_section(
|
||||||
"Text Example",
|
"Text Example",
|
||||||
TextStyle {
|
TextStyle {
|
||||||
@ -68,7 +72,11 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
margin: UiRect::all(Val::Px(5.0)),
|
margin: UiRect::all(Val::Px(5.0)),
|
||||||
..default()
|
..default()
|
||||||
}),
|
}),
|
||||||
);
|
// Because this is a distinct label widget and
|
||||||
|
// not button/list item text, this is necessary
|
||||||
|
// for accessibility to treat the text accordingly.
|
||||||
|
Label,
|
||||||
|
));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
// right vertical fill
|
// right vertical fill
|
||||||
@ -86,7 +94,7 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
})
|
})
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
// Title
|
// Title
|
||||||
parent.spawn(
|
parent.spawn((
|
||||||
TextBundle::from_section(
|
TextBundle::from_section(
|
||||||
"Scrolling list",
|
"Scrolling list",
|
||||||
TextStyle {
|
TextStyle {
|
||||||
@ -99,7 +107,8 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
size: Size::height(Val::Px(25.)),
|
size: Size::height(Val::Px(25.)),
|
||||||
..default()
|
..default()
|
||||||
}),
|
}),
|
||||||
);
|
Label,
|
||||||
|
));
|
||||||
// List with hidden overflow
|
// List with hidden overflow
|
||||||
parent
|
parent
|
||||||
.spawn(NodeBundle {
|
.spawn(NodeBundle {
|
||||||
@ -128,11 +137,12 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
..default()
|
..default()
|
||||||
},
|
},
|
||||||
ScrollingList::default(),
|
ScrollingList::default(),
|
||||||
|
AccessibilityNode(NodeBuilder::new(Role::List)),
|
||||||
))
|
))
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
// List items
|
// List items
|
||||||
for i in 0..30 {
|
for i in 0..30 {
|
||||||
parent.spawn(
|
parent.spawn((
|
||||||
TextBundle::from_section(
|
TextBundle::from_section(
|
||||||
format!("Item {i}"),
|
format!("Item {i}"),
|
||||||
TextStyle {
|
TextStyle {
|
||||||
@ -147,7 +157,9 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
size: Size::new(Val::Undefined, Val::Px(20.)),
|
size: Size::new(Val::Undefined, Val::Px(20.)),
|
||||||
..default()
|
..default()
|
||||||
}),
|
}),
|
||||||
);
|
Label,
|
||||||
|
AccessibilityNode(NodeBuilder::new(Role::ListItem)),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -275,14 +287,19 @@ fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
|
|||||||
})
|
})
|
||||||
.with_children(|parent| {
|
.with_children(|parent| {
|
||||||
// bevy logo (image)
|
// bevy logo (image)
|
||||||
parent.spawn(ImageBundle {
|
parent
|
||||||
style: Style {
|
.spawn(ImageBundle {
|
||||||
size: Size::width(Val::Px(500.0)),
|
style: Style {
|
||||||
|
size: Size::width(Val::Px(500.0)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
..default()
|
..default()
|
||||||
},
|
})
|
||||||
image: asset_server.load("branding/bevy_logo_dark_big.png").into(),
|
.with_children(|parent| {
|
||||||
..default()
|
// alt text
|
||||||
});
|
parent
|
||||||
|
.spawn(TextBundle::from_section("Bevy logo", TextStyle::default()));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -34,6 +34,7 @@ crates=(
|
|||||||
bevy_scene
|
bevy_scene
|
||||||
bevy_sprite
|
bevy_sprite
|
||||||
bevy_text
|
bevy_text
|
||||||
|
bevy_a11y
|
||||||
bevy_ui
|
bevy_ui
|
||||||
bevy_winit
|
bevy_winit
|
||||||
bevy_internal
|
bevy_internal
|
||||||
|
Loading…
Reference in New Issue
Block a user