bevy_ui 0.19.0-rc.1

A custom ECS-driven UI framework built specifically for Bevy Engine
Documentation
use crate::{
    experimental::UiChildren,
    prelude::{Button, Label},
    ui_transform::UiGlobalTransform,
    widget::{ImageNode, TextUiReader},
    ComputedNode, UiSystems,
};
use bevy_a11y::{AccessibilityNode, AccessibilitySystems};
use bevy_app::{App, Plugin, PostUpdate};
use bevy_ecs::{
    change_detection::DetectChanges,
    hierarchy::ChildOf,
    prelude::Entity,
    query::{Changed, With, Without},
    schedule::IntoScheduleConfigs,
    system::{Commands, Query},
    world::Ref,
};
use bevy_math::Affine2;

use accesskit::{Affine, Node, Rect, Role};

fn calc_label(
    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 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, 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();
        }
    }
}

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 label = calc_label(&mut text_reader, ui_children.iter_ui_children(entity));
        if let Some(mut accessible) = accessible {
            accessible.set_role(Role::Button);
            if let Some(name) = label {
                accessible.set_label(name);
            } else {
                accessible.clear_label();
            }
        } else {
            let mut node = Node::new(Role::Button);
            if let Some(label) = label {
                node.set_label(label);
            }
            commands
                .entity(entity)
                .try_insert(AccessibilityNode::from(node));
        }
    }
}

fn image_changed(
    mut commands: Commands,
    mut query: Query<
        (Entity, Option<&mut AccessibilityNode>),
        (Changed<ImageNode>, Without<Button>),
    >,
    ui_children: UiChildren,
    mut text_reader: TextUiReader,
) {
    for (entity, accessible) in &mut query {
        let label = calc_label(&mut text_reader, ui_children.iter_ui_children(entity));
        if let Some(mut accessible) = accessible {
            accessible.set_role(Role::Image);
            if let Some(label) = label {
                accessible.set_label(label);
            } else {
                accessible.clear_label();
            }
        } else {
            let mut node = Node::new(Role::Image);
            if let Some(label) = label {
                node.set_label(label);
            }
            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 label = Some(values.join(" ").into_boxed_str());
        if let Some(mut accessible) = accessible {
            accessible.set_role(Role::Label);
            if let Some(label) = label {
                accessible.set_value(label);
            } else {
                accessible.clear_value();
            }
        } else {
            let mut node = Node::new(Role::Label);
            if let Some(label) = label {
                node.set_value(label);
            }
            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,
            (
                button_changed,
                image_changed,
                label_changed,
                sync_bounds_and_transforms
                    .after(button_changed)
                    .after(image_changed)
                    .after(label_changed),
            )
                .in_set(UiSystems::PostLayout)
                .before(AccessibilitySystems::Update),
        );
    }
}