bevy/crates/bevy_ui/src/accessibility.rs

165 lines
5 KiB
Rust
Raw Normal View History

use crate::{
prelude::{Button, Label},
Node, UiImage,
};
use bevy_a11y::{
accesskit::{NodeBuilder, Rect, Role},
AccessibilityNode,
};
use bevy_app::{App, Plugin, PostUpdate};
use bevy_ecs::{
prelude::{DetectChanges, Entity},
query::{Changed, Without},
schedule::IntoSystemConfigs,
system::{Commands, Query},
world::Ref,
};
use bevy_hierarchy::Children;
resolve all internal ambiguities (#10411) - ignore all ambiguities that are not a problem - remove `.before(Assets::<Image>::track_assets),` that points into a different schedule (-> should this be caught?) - add some explicit orderings: - run `poll_receivers` and `update_accessibility_nodes` after `window_closed` in `bevy_winit::accessibility` - run `bevy_ui::accessibility::calc_bounds` after `CameraUpdateSystem` - run ` bevy_text::update_text2d_layout` and `bevy_ui::text_system` after `font_atlas_set::remove_dropped_font_atlas_sets` - add `app.ignore_ambiguity(a, b)` function for cases where you want to ignore an ambiguity between two independent plugins `A` and `B` - add `IgnoreAmbiguitiesPlugin` in `DefaultPlugins` that allows cross-crate ambiguities like `bevy_animation`/`bevy_ui` - Fixes https://github.com/bevyengine/bevy/issues/9511 ## Before **Render** ![render_schedule_Render dot](https://github.com/bevyengine/bevy/assets/22177966/1c677968-7873-40cc-848c-91fca4c8e383) **PostUpdate** ![schedule_PostUpdate dot](https://github.com/bevyengine/bevy/assets/22177966/8fc61304-08d4-4533-8110-c04113a7367a) ## After **Render** ![render_schedule_Render dot](https://github.com/bevyengine/bevy/assets/22177966/462f3b28-cef7-4833-8619-1f5175983485) **PostUpdate** ![schedule_PostUpdate dot](https://github.com/bevyengine/bevy/assets/22177966/8cfb3d83-7842-4a84-9082-46177e1a6c70) --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Alice Cecile <alice.i.cecil@gmail.com> Co-authored-by: François <mockersf@gmail.com>
2024-01-09 19:08:15 +00:00
use bevy_render::{camera::CameraUpdateSystem, prelude::Camera};
use bevy_text::Text;
use bevy_transform::prelude::GlobalTransform;
fn calc_name(texts: &Query<&Text>, children: &Children) -> Option<Box<str>> {
let mut name = None;
for child in children {
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(String::into_boxed_str)
}
fn calc_bounds(
camera: Query<(&Camera, &GlobalTransform)>,
mut nodes: Query<(&mut AccessibilityNode, Ref<Node>, Ref<GlobalTransform>)>,
) {
if let Ok((camera, camera_transform)) = camera.get_single() {
for (mut accessible, node, transform) in &mut nodes {
if node.is_changed() || transform.is_changed() {
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)
fix: use try_insert instead of insert in bevy_ui to prevent panics when despawning ui nodes (#13000) # Objective Sometimes when despawning a ui node in the PostUpdate schedule it panics. This is because both a despawn command and insert command are being run on the same entity. See this example code: ```rs use bevy::{prelude::*, ui::UiSystem}; #[derive(Resource)] struct SliceSquare(Handle<Image>); fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, create_ui) .add_systems(PostUpdate, despawn_nine_slice.after(UiSystem::Layout)) .run(); } fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { commands.spawn(Camera2dBundle::default()); commands.insert_resource(SliceSquare(asset_server.load("textures/slice_square.png"))); } fn create_ui(mut commands: Commands, slice_square: Res<SliceSquare>) { commands.spawn(( NodeBundle { style: Style { width: Val::Px(200.), height: Val::Px(200.), ..default() }, background_color: Color::WHITE.into(), ..default() }, UiImage::new(slice_square.0.clone()), ImageScaleMode::Sliced(TextureSlicer { border: BorderRect::square(220.), center_scale_mode: SliceScaleMode::Stretch, sides_scale_mode: SliceScaleMode::Stretch, max_corner_scale: 1., }), )); } fn despawn_nine_slice(mut commands: Commands, mut slices: Query<Entity, With<ImageScaleMode>>) { for entity in slices.iter_mut() { commands.entity(entity).despawn_recursive(); } } ``` This code spawns a UiNode with a sliced image scale mode, and despawns it in the same frame. The bevy_ui::texture_slice::compute_slices_on_image_change system tries to insert the ComputedTextureSlices component on that node, but that entity is already despawned causing this error: ```md error[B0003]: Could not insert a bundle (of type `bevy_ui::texture_slice::ComputedTextureSlices`) for entity Entity { index: 2, generation: 3 } because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/#b0003 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace Encountered a panic when applying buffers for system `bevy_ui::texture_slice::compute_slices_on_image_change`! Encountered a panic in system `bevy_ecs::schedule::executor::apply_deferred`! Encountered a panic in system `bevy_app::main_schedule::Main::run_main`! ``` Note that you might have to run the code a few times before this error appears. ## Solution Use try_insert instead of insert for non critical inserts in the bevy_ui crate. ## Some notes In a lot of cases it does not makes much sense to despawn ui nodes after the layout system has finished. Except maybe if you delete the root ui node of a tree. I personally encountered this issue in bevy `0.13.2` with a system that was running before the layout system. And in `0.13.2` the `compute_slices_on_image_change` system was also running before the layout system. But now it runs after the layout system. So the only way that this bug appears now is if you despawn ui nodes after the layout system. So I am not 100% sure if using try_insert in this system is the best option. But personally I still think it is better then the program panicking. However the `update_children_target_camera` system does still run before the layout system. So I do think it might still be able to panic when ui nodes are despawned before the layout system. Though I haven't been able to verify that.
2024-04-19 18:12:08 +00:00
.try_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)
fix: use try_insert instead of insert in bevy_ui to prevent panics when despawning ui nodes (#13000) # Objective Sometimes when despawning a ui node in the PostUpdate schedule it panics. This is because both a despawn command and insert command are being run on the same entity. See this example code: ```rs use bevy::{prelude::*, ui::UiSystem}; #[derive(Resource)] struct SliceSquare(Handle<Image>); fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, create_ui) .add_systems(PostUpdate, despawn_nine_slice.after(UiSystem::Layout)) .run(); } fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { commands.spawn(Camera2dBundle::default()); commands.insert_resource(SliceSquare(asset_server.load("textures/slice_square.png"))); } fn create_ui(mut commands: Commands, slice_square: Res<SliceSquare>) { commands.spawn(( NodeBundle { style: Style { width: Val::Px(200.), height: Val::Px(200.), ..default() }, background_color: Color::WHITE.into(), ..default() }, UiImage::new(slice_square.0.clone()), ImageScaleMode::Sliced(TextureSlicer { border: BorderRect::square(220.), center_scale_mode: SliceScaleMode::Stretch, sides_scale_mode: SliceScaleMode::Stretch, max_corner_scale: 1., }), )); } fn despawn_nine_slice(mut commands: Commands, mut slices: Query<Entity, With<ImageScaleMode>>) { for entity in slices.iter_mut() { commands.entity(entity).despawn_recursive(); } } ``` This code spawns a UiNode with a sliced image scale mode, and despawns it in the same frame. The bevy_ui::texture_slice::compute_slices_on_image_change system tries to insert the ComputedTextureSlices component on that node, but that entity is already despawned causing this error: ```md error[B0003]: Could not insert a bundle (of type `bevy_ui::texture_slice::ComputedTextureSlices`) for entity Entity { index: 2, generation: 3 } because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/#b0003 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace Encountered a panic when applying buffers for system `bevy_ui::texture_slice::compute_slices_on_image_change`! Encountered a panic in system `bevy_ecs::schedule::executor::apply_deferred`! Encountered a panic in system `bevy_app::main_schedule::Main::run_main`! ``` Note that you might have to run the code a few times before this error appears. ## Solution Use try_insert instead of insert for non critical inserts in the bevy_ui crate. ## Some notes In a lot of cases it does not makes much sense to despawn ui nodes after the layout system has finished. Except maybe if you delete the root ui node of a tree. I personally encountered this issue in bevy `0.13.2` with a system that was running before the layout system. And in `0.13.2` the `compute_slices_on_image_change` system was also running before the layout system. But now it runs after the layout system. So the only way that this bug appears now is if you despawn ui nodes after the layout system. So I am not 100% sure if using try_insert in this system is the best option. But personally I still think it is better then the program panicking. However the `update_children_target_camera` system does still run before the layout system. So I do think it might still be able to panic when ui nodes are despawned before the layout system. Though I haven't been able to verify that.
2024-04-19 18:12:08 +00:00
.try_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::Label);
if let Some(name) = name {
accessible.set_name(name);
} else {
accessible.clear_name();
}
} else {
let mut node = NodeBuilder::new(Role::Label);
if let Some(name) = name {
node.set_name(name);
}
commands
.entity(entity)
fix: use try_insert instead of insert in bevy_ui to prevent panics when despawning ui nodes (#13000) # Objective Sometimes when despawning a ui node in the PostUpdate schedule it panics. This is because both a despawn command and insert command are being run on the same entity. See this example code: ```rs use bevy::{prelude::*, ui::UiSystem}; #[derive(Resource)] struct SliceSquare(Handle<Image>); fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) .add_systems(Update, create_ui) .add_systems(PostUpdate, despawn_nine_slice.after(UiSystem::Layout)) .run(); } fn setup(mut commands: Commands, asset_server: Res<AssetServer>) { commands.spawn(Camera2dBundle::default()); commands.insert_resource(SliceSquare(asset_server.load("textures/slice_square.png"))); } fn create_ui(mut commands: Commands, slice_square: Res<SliceSquare>) { commands.spawn(( NodeBundle { style: Style { width: Val::Px(200.), height: Val::Px(200.), ..default() }, background_color: Color::WHITE.into(), ..default() }, UiImage::new(slice_square.0.clone()), ImageScaleMode::Sliced(TextureSlicer { border: BorderRect::square(220.), center_scale_mode: SliceScaleMode::Stretch, sides_scale_mode: SliceScaleMode::Stretch, max_corner_scale: 1., }), )); } fn despawn_nine_slice(mut commands: Commands, mut slices: Query<Entity, With<ImageScaleMode>>) { for entity in slices.iter_mut() { commands.entity(entity).despawn_recursive(); } } ``` This code spawns a UiNode with a sliced image scale mode, and despawns it in the same frame. The bevy_ui::texture_slice::compute_slices_on_image_change system tries to insert the ComputedTextureSlices component on that node, but that entity is already despawned causing this error: ```md error[B0003]: Could not insert a bundle (of type `bevy_ui::texture_slice::ComputedTextureSlices`) for entity Entity { index: 2, generation: 3 } because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/#b0003 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace Encountered a panic when applying buffers for system `bevy_ui::texture_slice::compute_slices_on_image_change`! Encountered a panic in system `bevy_ecs::schedule::executor::apply_deferred`! Encountered a panic in system `bevy_app::main_schedule::Main::run_main`! ``` Note that you might have to run the code a few times before this error appears. ## Solution Use try_insert instead of insert for non critical inserts in the bevy_ui crate. ## Some notes In a lot of cases it does not makes much sense to despawn ui nodes after the layout system has finished. Except maybe if you delete the root ui node of a tree. I personally encountered this issue in bevy `0.13.2` with a system that was running before the layout system. And in `0.13.2` the `compute_slices_on_image_change` system was also running before the layout system. But now it runs after the layout system. So the only way that this bug appears now is if you despawn ui nodes after the layout system. So I am not 100% sure if using try_insert in this system is the best option. But personally I still think it is better then the program panicking. However the `update_children_target_camera` system does still run before the layout system. So I do think it might still be able to panic when ui nodes are despawned before the layout system. Though I haven't been able to verify that.
2024-04-19 18:12:08 +00:00
.try_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_systems(
PostUpdate,
(
resolve all internal ambiguities (#10411) - ignore all ambiguities that are not a problem - remove `.before(Assets::<Image>::track_assets),` that points into a different schedule (-> should this be caught?) - add some explicit orderings: - run `poll_receivers` and `update_accessibility_nodes` after `window_closed` in `bevy_winit::accessibility` - run `bevy_ui::accessibility::calc_bounds` after `CameraUpdateSystem` - run ` bevy_text::update_text2d_layout` and `bevy_ui::text_system` after `font_atlas_set::remove_dropped_font_atlas_sets` - add `app.ignore_ambiguity(a, b)` function for cases where you want to ignore an ambiguity between two independent plugins `A` and `B` - add `IgnoreAmbiguitiesPlugin` in `DefaultPlugins` that allows cross-crate ambiguities like `bevy_animation`/`bevy_ui` - Fixes https://github.com/bevyengine/bevy/issues/9511 ## Before **Render** ![render_schedule_Render dot](https://github.com/bevyengine/bevy/assets/22177966/1c677968-7873-40cc-848c-91fca4c8e383) **PostUpdate** ![schedule_PostUpdate dot](https://github.com/bevyengine/bevy/assets/22177966/8fc61304-08d4-4533-8110-c04113a7367a) ## After **Render** ![render_schedule_Render dot](https://github.com/bevyengine/bevy/assets/22177966/462f3b28-cef7-4833-8619-1f5175983485) **PostUpdate** ![schedule_PostUpdate dot](https://github.com/bevyengine/bevy/assets/22177966/8cfb3d83-7842-4a84-9082-46177e1a6c70) --------- Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com> Co-authored-by: Alice Cecile <alice.i.cecil@gmail.com> Co-authored-by: François <mockersf@gmail.com>
2024-01-09 19:08:15 +00:00
calc_bounds
.after(bevy_transform::TransformSystem::TransformPropagate)
.after(CameraUpdateSystem)
// the listed systems do not affect calculated size
.ambiguous_with(crate::ui_stack_system),
button_changed,
image_changed,
label_changed,
),
);
}
}