diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index 5f24d3069f..4f84dd170e 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -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, Ref, + Option<&ChildOf>, )>, + accessible_transform_query: Query, With>, ) { - 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), ); } } diff --git a/examples/ui/widgets/button.rs b/examples/ui/widgets/button.rs index 6d7d42d891..d74aef2444 100644 --- a/examples/ui/widgets/button.rs +++ b/examples/ui/widgets/button.rs @@ -8,6 +8,12 @@ fn main() { .add_plugins(DefaultPlugins) // `InputFocus` must be set for accessibility to recognize the button. .init_resource::() + .add_systems( + Startup, + |requested: Res| { + 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![(