mirror of
https://github.com/bevyengine/bevy.git
synced 2026-05-06 06:06:42 -04:00
Fix UI accessibility transforms and update ordering (#23859)
# Objective * Bevy UI's accessibility module doesn't set the transform on accessiblity nodes, ignoring scaling and rotation. * UI accessibility nodes should be updated before `bevy_winit` updates the accesskit adaptors, otherwise there will be a frames delay. ## Solution Replaced the `calc_bounds` systems with a new system `sync_bounds_and_transforms`. Each accesskit `Node` corresponding to an `AccessibleNode` UI entity is now given object-centered coordinates for its bounding rect (instead of window coordinates) and a transform. Accesskit uses local transforms so if an accessible node also has an accessible parent, its transform has to be recomputed relative to its parent. ## Testing I modified the button example so that accesskit integration is enabled by default and the button is drawn at a 45 degrees angle. ``` cargo run --example button ``` Screen readers should only react when the pointer is directly over the rotated button if the changes are working. The example changes should be reverted before merging.
This commit is contained in:
@@ -5,18 +5,20 @@ use crate::{
|
||||
widget::{ImageNode, TextUiReader},
|
||||
ComputedNode, UiSystems,
|
||||
};
|
||||
use bevy_a11y::AccessibilityNode;
|
||||
use bevy_a11y::{AccessibilityNode, AccessibilitySystems};
|
||||
use bevy_app::{App, Plugin, PostUpdate};
|
||||
use bevy_ecs::{
|
||||
prelude::{DetectChanges, Entity},
|
||||
query::{Changed, Without},
|
||||
change_detection::DetectChanges,
|
||||
hierarchy::ChildOf,
|
||||
prelude::Entity,
|
||||
query::{Changed, With, Without},
|
||||
schedule::IntoScheduleConfigs,
|
||||
system::{Commands, Query},
|
||||
world::Ref,
|
||||
};
|
||||
use bevy_math::Affine2;
|
||||
|
||||
use accesskit::{Node, Rect, Role};
|
||||
use bevy_camera::CameraUpdateSystems;
|
||||
use accesskit::{Affine, Node, Rect, Role};
|
||||
|
||||
fn calc_label(
|
||||
text_reader: &mut TextUiReader,
|
||||
@@ -35,21 +37,43 @@ fn calc_label(
|
||||
name.map(String::into_boxed_str)
|
||||
}
|
||||
|
||||
fn calc_bounds(
|
||||
mut nodes: Query<(
|
||||
fn sync_bounds_and_transforms(
|
||||
mut accessible_nodes_query: Query<(
|
||||
&mut AccessibilityNode,
|
||||
Ref<ComputedNode>,
|
||||
Ref<UiGlobalTransform>,
|
||||
Option<&ChildOf>,
|
||||
)>,
|
||||
accessible_transform_query: Query<Ref<UiGlobalTransform>, With<AccessibilityNode>>,
|
||||
) {
|
||||
for (mut accessible, node, transform) in &mut nodes {
|
||||
if node.is_changed() || transform.is_changed() {
|
||||
let center = transform.translation;
|
||||
let half_size = 0.5 * node.size;
|
||||
let min = center - half_size;
|
||||
let max = center + half_size;
|
||||
let bounds = Rect::new(min.x as f64, min.y as f64, max.x as f64, max.y as f64);
|
||||
accessible.set_bounds(bounds);
|
||||
for (mut accessible, node, ui_transform, maybe_child_of) in &mut accessible_nodes_query {
|
||||
let maybe_parent_transform = maybe_child_of
|
||||
.and_then(|child_of| accessible_transform_query.get(child_of.parent()).ok());
|
||||
|
||||
if !(node.is_changed()
|
||||
|| ui_transform.is_changed()
|
||||
|| maybe_parent_transform.is_some_and(|transform| transform.is_changed()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
accessible.set_bounds(Rect::new(
|
||||
-0.5 * node.size.x as f64,
|
||||
-0.5 * node.size.y as f64,
|
||||
0.5 * node.size.x as f64,
|
||||
0.5 * node.size.y as f64,
|
||||
));
|
||||
|
||||
// If the node has an accessible parent, its transform in the accessibility tree must be relative to the parent.
|
||||
let transform = maybe_parent_transform
|
||||
.and_then(|transform| transform.try_inverse())
|
||||
.unwrap_or_default()
|
||||
* ui_transform.affine();
|
||||
|
||||
if transform.is_finite() && transform != Affine2::IDENTITY {
|
||||
accessible.set_transform(Affine::new(transform.to_cols_array().map(f64::from)));
|
||||
} else {
|
||||
accessible.clear_transform();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,16 +173,18 @@ impl Plugin for AccessibilityPlugin {
|
||||
app.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
calc_bounds
|
||||
.after(bevy_transform::TransformSystems::Propagate)
|
||||
.after(CameraUpdateSystems)
|
||||
// the listed systems do not affect calculated size
|
||||
.ambiguous_with(crate::ui_stack_system)
|
||||
.before(UiSystems::PostLayout),
|
||||
button_changed,
|
||||
image_changed,
|
||||
label_changed,
|
||||
),
|
||||
sync_bounds_and_transforms
|
||||
.after(button_changed)
|
||||
.after(image_changed)
|
||||
.after(label_changed)
|
||||
// the listed systems do not affect calculated size
|
||||
.ambiguous_with(crate::ui_stack_system),
|
||||
)
|
||||
.in_set(UiSystems::PostLayout)
|
||||
.before(AccessibilitySystems::Update),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@ fn main() {
|
||||
.add_plugins(DefaultPlugins)
|
||||
// `InputFocus` must be set for accessibility to recognize the button.
|
||||
.init_resource::<InputFocus>()
|
||||
.add_systems(
|
||||
Startup,
|
||||
|requested: Res<bevy::a11y::AccessibilityRequested>| {
|
||||
requested.set(true);
|
||||
},
|
||||
)
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(Update, button_system)
|
||||
.run();
|
||||
@@ -92,6 +98,11 @@ fn button(asset_server: &AssetServer) -> impl Bundle {
|
||||
border_radius: BorderRadius::MAX,
|
||||
..default()
|
||||
},
|
||||
UiTransform {
|
||||
translation: default(),
|
||||
scale: 2. * Vec2::ONE,
|
||||
rotation: Rot2::degrees(45.)
|
||||
},
|
||||
BorderColor::all(Color::WHITE),
|
||||
BackgroundColor(Color::BLACK),
|
||||
children![(
|
||||
|
||||
Reference in New Issue
Block a user