Skip to content

Commit bb1d516

Browse files
authored
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.
1 parent 7a42034 commit bb1d516

File tree

2 files changed

+59
-22
lines changed

2 files changed

+59
-22
lines changed

crates/bevy_ui/src/accessibility.rs

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,20 @@ use crate::{
55
widget::{ImageNode, TextUiReader},
66
ComputedNode, UiSystems,
77
};
8-
use bevy_a11y::AccessibilityNode;
8+
use bevy_a11y::{AccessibilityNode, AccessibilitySystems};
99
use bevy_app::{App, Plugin, PostUpdate};
1010
use bevy_ecs::{
11-
prelude::{DetectChanges, Entity},
12-
query::{Changed, Without},
11+
change_detection::DetectChanges,
12+
hierarchy::ChildOf,
13+
prelude::Entity,
14+
query::{Changed, With, Without},
1315
schedule::IntoScheduleConfigs,
1416
system::{Commands, Query},
1517
world::Ref,
1618
};
19+
use bevy_math::Affine2;
1720

18-
use accesskit::{Node, Rect, Role};
19-
use bevy_camera::CameraUpdateSystems;
21+
use accesskit::{Affine, Node, Rect, Role};
2022

2123
fn calc_label(
2224
text_reader: &mut TextUiReader,
@@ -35,21 +37,43 @@ fn calc_label(
3537
name.map(String::into_boxed_str)
3638
}
3739

38-
fn calc_bounds(
39-
mut nodes: Query<(
40+
fn sync_bounds_and_transforms(
41+
mut accessible_nodes_query: Query<(
4042
&mut AccessibilityNode,
4143
Ref<ComputedNode>,
4244
Ref<UiGlobalTransform>,
45+
Option<&ChildOf>,
4346
)>,
47+
accessible_transform_query: Query<Ref<UiGlobalTransform>, With<AccessibilityNode>>,
4448
) {
45-
for (mut accessible, node, transform) in &mut nodes {
46-
if node.is_changed() || transform.is_changed() {
47-
let center = transform.translation;
48-
let half_size = 0.5 * node.size;
49-
let min = center - half_size;
50-
let max = center + half_size;
51-
let bounds = Rect::new(min.x as f64, min.y as f64, max.x as f64, max.y as f64);
52-
accessible.set_bounds(bounds);
49+
for (mut accessible, node, ui_transform, maybe_child_of) in &mut accessible_nodes_query {
50+
let maybe_parent_transform = maybe_child_of
51+
.and_then(|child_of| accessible_transform_query.get(child_of.parent()).ok());
52+
53+
if !(node.is_changed()
54+
|| ui_transform.is_changed()
55+
|| maybe_parent_transform.is_some_and(|transform| transform.is_changed()))
56+
{
57+
continue;
58+
}
59+
60+
accessible.set_bounds(Rect::new(
61+
-0.5 * node.size.x as f64,
62+
-0.5 * node.size.y as f64,
63+
0.5 * node.size.x as f64,
64+
0.5 * node.size.y as f64,
65+
));
66+
67+
// If the node has an accessible parent, its transform in the accessibility tree must be relative to the parent.
68+
let transform = maybe_parent_transform
69+
.and_then(|transform| transform.try_inverse())
70+
.unwrap_or_default()
71+
* ui_transform.affine();
72+
73+
if transform.is_finite() && transform != Affine2::IDENTITY {
74+
accessible.set_transform(Affine::new(transform.to_cols_array().map(f64::from)));
75+
} else {
76+
accessible.clear_transform();
5377
}
5478
}
5579
}
@@ -149,16 +173,18 @@ impl Plugin for AccessibilityPlugin {
149173
app.add_systems(
150174
PostUpdate,
151175
(
152-
calc_bounds
153-
.after(bevy_transform::TransformSystems::Propagate)
154-
.after(CameraUpdateSystems)
155-
// the listed systems do not affect calculated size
156-
.ambiguous_with(crate::ui_stack_system)
157-
.before(UiSystems::PostLayout),
158176
button_changed,
159177
image_changed,
160178
label_changed,
161-
),
179+
sync_bounds_and_transforms
180+
.after(button_changed)
181+
.after(image_changed)
182+
.after(label_changed)
183+
// the listed systems do not affect calculated size
184+
.ambiguous_with(crate::ui_stack_system),
185+
)
186+
.in_set(UiSystems::PostLayout)
187+
.before(AccessibilitySystems::Update),
162188
);
163189
}
164190
}

examples/ui/widgets/button.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ fn main() {
88
.add_plugins(DefaultPlugins)
99
// `InputFocus` must be set for accessibility to recognize the button.
1010
.init_resource::<InputFocus>()
11+
.add_systems(
12+
Startup,
13+
|requested: Res<bevy::a11y::AccessibilityRequested>| {
14+
requested.set(true);
15+
},
16+
)
1117
.add_systems(Startup, setup)
1218
.add_systems(Update, button_system)
1319
.run();
@@ -92,6 +98,11 @@ fn button(asset_server: &AssetServer) -> impl Bundle {
9298
border_radius: BorderRadius::MAX,
9399
..default()
94100
},
101+
UiTransform {
102+
translation: default(),
103+
scale: 2. * Vec2::ONE,
104+
rotation: Rot2::degrees(45.)
105+
},
95106
BorderColor::all(Color::WHITE),
96107
BackgroundColor(Color::BLACK),
97108
children![(

0 commit comments

Comments
 (0)