bevy/crates/bevy_ui/src/accessibility.rs
Carter Anderson 015f2c69ca
Merge Style properties into Node. Use ComputedNode for computed properties. (#15975)
# Objective

Continue improving the user experience of our UI Node API in the
direction specified by [Bevy's Next Generation Scene / UI
System](https://github.com/bevyengine/bevy/discussions/14437)

## Solution

As specified in the document above, merge `Style` fields into `Node`,
and move "computed Node fields" into `ComputedNode` (I chose this name
over something like `ComputedNodeLayout` because it currently contains
more than just layout info. If we want to break this up / rename these
concepts, lets do that in a separate PR). `Style` has been removed.

This accomplishes a number of goals:

## Ergonomics wins

Specifying both `Node` and `Style` is now no longer required for
non-default styles

Before:
```rust
commands.spawn((
    Node::default(),
    Style {
        width:  Val::Px(100.),
        ..default()
    },
));
```

After:

```rust
commands.spawn(Node {
    width:  Val::Px(100.),
    ..default()
});
```

## Conceptual clarity

`Style` was never a comprehensive "style sheet". It only defined "core"
style properties that all `Nodes` shared. Any "styled property" that
couldn't fit that mold had to be in a separate component. A "real" style
system would style properties _across_ components (`Node`, `Button`,
etc). We have plans to build a true style system (see the doc linked
above).

By moving the `Style` fields to `Node`, we fully embrace `Node` as the
driving concept and remove the "style system" confusion.

## Next Steps

* Consider identifying and splitting out "style properties that aren't
core to Node". This should not happen for Bevy 0.15.

---

## Migration Guide

Move any fields set on `Style` into `Node` and replace all `Style`
component usage with `Node`.

Before:
```rust
commands.spawn((
    Node::default(),
    Style {
        width:  Val::Px(100.),
        ..default()
    },
));
```

After:

```rust
commands.spawn(Node {
    width:  Val::Px(100.),
    ..default()
});
```

For any usage of the "computed node properties" that used to live on
`Node`, use `ComputedNode` instead:

Before:
```rust
fn system(nodes: Query<&Node>) {
    for node in &nodes {
        let computed_size = node.size();
    }
}
```

After:
```rust
fn system(computed_nodes: Query<&ComputedNode>) {
    for computed_node in &computed_nodes {
        let computed_size = computed_node.size();
    }
}
```
2024-10-18 22:25:33 +00:00

170 lines
5.2 KiB
Rust

use crate::{
experimental::UiChildren,
prelude::{Button, Label},
widget::TextUiReader,
ComputedNode, 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_render::{camera::CameraUpdateSystem, prelude::Camera};
use bevy_transform::prelude::GlobalTransform;
fn calc_name(
text_reader: &mut TextUiReader,
children: impl Iterator<Item = Entity>,
) -> Option<Box<str>> {
let mut name = None;
for child in children {
let values = text_reader
.iter(child)
.map(|(_, _, text, _, _)| text.into())
.collect::<Vec<String>>();
if !values.is_empty() {
name = Some(values.join(" "));
}
}
name.map(String::into_boxed_str)
}
fn calc_bounds(
camera: Query<(&Camera, &GlobalTransform)>,
mut nodes: Query<(
&mut AccessibilityNode,
Ref<ComputedNode>,
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 Ok(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, Option<&mut AccessibilityNode>), Changed<Button>>,
ui_children: UiChildren,
mut text_reader: TextUiReader,
) {
for (entity, accessible) in &mut query {
let name = calc_name(&mut text_reader, ui_children.iter_ui_children(entity));
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)
.try_insert(AccessibilityNode::from(node));
}
}
}
fn image_changed(
mut commands: Commands,
mut query: Query<(Entity, Option<&mut AccessibilityNode>), (Changed<UiImage>, Without<Button>)>,
ui_children: UiChildren,
mut text_reader: TextUiReader,
) {
for (entity, accessible) in &mut query {
let name = calc_name(&mut text_reader, ui_children.iter_ui_children(entity));
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)
.try_insert(AccessibilityNode::from(node));
}
}
}
fn label_changed(
mut commands: Commands,
mut query: Query<(Entity, Option<&mut AccessibilityNode>), Changed<Label>>,
mut text_reader: TextUiReader,
) {
for (entity, accessible) in &mut query {
let values = text_reader
.iter(entity)
.map(|(_, _, text, _, _)| text.into())
.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)
.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,
(
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,
),
);
}
}