Skip to main content

bevy_ui/
accessibility.rs

1use crate::{
2    experimental::UiChildren,
3    prelude::{Button, Label},
4    ui_transform::UiGlobalTransform,
5    widget::{ImageNode, TextUiReader},
6    ComputedNode, UiSystems,
7};
8use bevy_a11y::{AccessibilityNode, AccessibilitySystems};
9use bevy_app::{App, Plugin, PostUpdate};
10use bevy_ecs::{
11    change_detection::DetectChanges,
12    component::Component,
13    hierarchy::ChildOf,
14    lifecycle::HookContext,
15    prelude::Entity,
16    query::{Changed, With, Without},
17    reflect::ReflectComponent,
18    schedule::IntoScheduleConfigs,
19    system::{Commands, Query},
20    world::{DeferredWorld, Ref},
21};
22use bevy_math::Affine2;
23use bevy_reflect::prelude::ReflectDefault;
24
25use accesskit::{Affine, Node, Rect, Role};
26use bevy_reflect::Reflect;
27
28fn calc_label(
29    text_reader: &mut TextUiReader,
30    children: impl Iterator<Item = Entity>,
31) -> Option<Box<str>> {
32    let mut name = None;
33    for child in children {
34        let values = text_reader
35            .iter(child)
36            .map(|(_, _, text, _, _, _, _)| text.into())
37            .collect::<Vec<String>>();
38        if !values.is_empty() {
39            name = Some(values.join(" "));
40        }
41    }
42    name.map(String::into_boxed_str)
43}
44
45fn sync_bounds_and_transforms(
46    mut accessible_nodes_query: Query<(
47        &mut AccessibilityNode,
48        Ref<ComputedNode>,
49        Ref<UiGlobalTransform>,
50        Option<&ChildOf>,
51    )>,
52    accessible_transform_query: Query<Ref<UiGlobalTransform>, With<AccessibilityNode>>,
53) {
54    for (mut accessible, node, ui_transform, maybe_child_of) in &mut accessible_nodes_query {
55        let maybe_parent_transform = maybe_child_of
56            .and_then(|child_of| accessible_transform_query.get(child_of.parent()).ok());
57
58        if !(node.is_changed()
59            || ui_transform.is_changed()
60            || maybe_parent_transform.is_some_and(|transform| transform.is_changed()))
61        {
62            continue;
63        }
64
65        accessible.set_bounds(Rect::new(
66            -0.5 * node.size.x as f64,
67            -0.5 * node.size.y as f64,
68            0.5 * node.size.x as f64,
69            0.5 * node.size.y as f64,
70        ));
71
72        // If the node has an accessible parent, its transform in the accessibility tree must be relative to the parent.
73        let transform = maybe_parent_transform
74            .and_then(|transform| transform.try_inverse())
75            .unwrap_or_default()
76            * ui_transform.affine();
77
78        if transform.is_finite() && transform != Affine2::IDENTITY {
79            accessible.set_transform(Affine::new(transform.to_cols_array().map(f64::from)));
80        } else {
81            accessible.clear_transform();
82        }
83    }
84}
85
86fn button_changed(
87    mut commands: Commands,
88    mut query: Query<(Entity, Option<&mut AccessibilityNode>), Changed<Button>>,
89    ui_children: UiChildren,
90    mut text_reader: TextUiReader,
91) {
92    for (entity, accessible) in &mut query {
93        let label = calc_label(&mut text_reader, ui_children.iter_ui_children(entity));
94        if let Some(mut accessible) = accessible {
95            accessible.set_role(Role::Button);
96            if let Some(name) = label {
97                accessible.set_label(name);
98            } else {
99                accessible.clear_label();
100            }
101        } else {
102            let mut node = Node::new(Role::Button);
103            if let Some(label) = label {
104                node.set_label(label);
105            }
106            commands
107                .entity(entity)
108                .try_insert(AccessibilityNode::from(node));
109        }
110    }
111}
112
113fn image_changed(
114    mut commands: Commands,
115    mut query: Query<
116        (Entity, Option<&mut AccessibilityNode>),
117        (Changed<ImageNode>, Without<Button>),
118    >,
119    ui_children: UiChildren,
120    mut text_reader: TextUiReader,
121) {
122    for (entity, accessible) in &mut query {
123        let label = calc_label(&mut text_reader, ui_children.iter_ui_children(entity));
124        if let Some(mut accessible) = accessible {
125            accessible.set_role(Role::Image);
126            if let Some(label) = label {
127                accessible.set_label(label);
128            } else {
129                accessible.clear_label();
130            }
131        } else {
132            let mut node = Node::new(Role::Image);
133            if let Some(label) = label {
134                node.set_label(label);
135            }
136            commands
137                .entity(entity)
138                .try_insert(AccessibilityNode::from(node));
139        }
140    }
141}
142
143fn label_changed(
144    mut commands: Commands,
145    mut query: Query<(Entity, Option<&mut AccessibilityNode>), Changed<Label>>,
146    mut text_reader: TextUiReader,
147) {
148    for (entity, accessible) in &mut query {
149        let values = text_reader
150            .iter(entity)
151            .map(|(_, _, text, _, _, _, _)| text.into())
152            .collect::<Vec<String>>();
153        let label = Some(values.join(" ").into_boxed_str());
154        if let Some(mut accessible) = accessible {
155            accessible.set_role(Role::Label);
156            if let Some(label) = label {
157                accessible.set_value(label);
158            } else {
159                accessible.clear_value();
160            }
161        } else {
162            let mut node = Node::new(Role::Label);
163            if let Some(label) = label {
164                node.set_value(label);
165            }
166            commands
167                .entity(entity)
168                .try_insert(AccessibilityNode::from(node));
169        }
170    }
171}
172
173/// A component which permits the a11y label to be specified independently from other a11y
174/// attributes.
175///
176/// The content of the `label` attribute is typically application-specific, and frequently
177/// originates in application code rather than library code. Because the primary mechanism of entity
178/// composition in Bevy is component insertion (especially in BSN scenes), and because ``accesskit``
179/// mandates that all a11y properties be stored in a single data structure, it can be cumbersome
180/// to combine together a11y properties coming from different parts of the code; making the label
181/// its own component makes it possible to specify the label as a mixin.
182///
183/// Internally, what this does is update the [`AccessibilityNode`] component, using component hooks
184/// which are automatically registered when this component is used.
185#[derive(#[doc =
"**Required Components**: [`AccessibilityNode`]. \n\n A component's Required Components are inserted whenever it is inserted. Note that this will also insert the required components _of_ the required components, recursively, in depth-first order."]
impl bevy_ecs::component::Component for AccessibleLabel where
    Self: ::core::marker::Send + ::core::marker::Sync + 'static {
    const STORAGE_TYPE: bevy_ecs::component::StorageType =
        bevy_ecs::component::StorageType::Table;
    type Mutability = bevy_ecs::component::Immutable;
    fn register_required_components(_requiree:
            bevy_ecs::component::ComponentId,
        required_components:
            &mut bevy_ecs::component::RequiredComponentsRegistrator) {
        required_components.register_required::<AccessibilityNode>(<AccessibilityNode
                as ::core::default::Default>::default);
    }
    fn on_insert()
        -> ::core::option::Option<bevy_ecs::lifecycle::ComponentHook> {
        ::core::option::Option::Some(on_label_inserted)
    }
    fn on_remove()
        -> ::core::option::Option<bevy_ecs::lifecycle::ComponentHook> {
        ::core::option::Option::Some(on_label_removed)
    }
    fn clone_behavior() -> bevy_ecs::component::ComponentCloneBehavior {
        use bevy_ecs::component::{
            DefaultCloneBehaviorBase, DefaultCloneBehaviorViaClone,
        };
        (&&&bevy_ecs::component::DefaultCloneBehaviorSpecialization::<Self>::default()).default_clone_behavior()
    }
    fn relationship_accessor()
        ->
            ::core::option::Option<bevy_ecs::relationship::ComponentRelationshipAccessor<Self>> {
        ::core::option::Option::None
    }
}Component, #[automatically_derived]
impl ::core::fmt::Debug for AccessibleLabel {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::debug_tuple_field1_finish(f,
            "AccessibleLabel", &&self.0)
    }
}Debug, #[automatically_derived]
impl ::core::default::Default for AccessibleLabel {
    #[inline]
    fn default() -> AccessibleLabel {
        AccessibleLabel(::core::default::Default::default())
    }
}Default, #[automatically_derived]
impl ::core::clone::Clone for AccessibleLabel {
    #[inline]
    fn clone(&self) -> AccessibleLabel {
        AccessibleLabel(::core::clone::Clone::clone(&self.0))
    }
}Clone, const _: () =
    {
        impl bevy_reflect::GetTypeRegistration for AccessibleLabel where  {
            fn get_type_registration() -> bevy_reflect::TypeRegistration {
                let mut registration =
                    bevy_reflect::TypeRegistration::of::<Self>();
                registration.insert::<bevy_reflect::ReflectFromPtr>(bevy_reflect::FromType::<Self>::from_type());
                registration.insert::<bevy_reflect::ReflectFromReflect>(bevy_reflect::FromType::<Self>::from_type());
                registration.register_type_data::<ReflectComponent, Self>();
                registration.register_type_data::<ReflectDefault, Self>();
                registration
            }
            #[inline(never)]
            fn register_type_dependencies(registry:
                    &mut bevy_reflect::TypeRegistry) {
                <String as
                        bevy_reflect::__macro_exports::RegisterForReflection>::__register(registry);
            }
        }
        impl bevy_reflect::Typed for AccessibleLabel where  {
            #[inline]
            fn type_info() -> &'static bevy_reflect::TypeInfo {
                static CELL: bevy_reflect::utility::NonGenericTypeInfoCell =
                    bevy_reflect::utility::NonGenericTypeInfoCell::new();
                CELL.get_or_set(||
                        {
                            bevy_reflect::TypeInfo::TupleStruct(bevy_reflect::tuple_struct::TupleStructInfo::new::<Self>(&[bevy_reflect::UnnamedField::new::<String>(0usize)]))
                        })
            }
        }
        #[allow(deprecated, reason =
        "derives on a deprecated type shouldn't be considered a usage")]
        impl bevy_reflect::TypePath for AccessibleLabel where  {
            fn type_path() -> &'static str {
                "bevy_ui::accessibility::AccessibleLabel"
            }
            fn short_type_path() -> &'static str { "AccessibleLabel" }
            fn type_ident() -> ::core::option::Option<&'static str> {
                ::core::option::Option::Some("AccessibleLabel")
            }
            fn crate_name() -> ::core::option::Option<&'static str> {
                ::core::option::Option::Some("bevy_ui::accessibility".split(':').next().unwrap())
            }
            fn module_path() -> ::core::option::Option<&'static str> {
                ::core::option::Option::Some("bevy_ui::accessibility")
            }
        }
        impl bevy_reflect::Reflect for AccessibleLabel where  {
            #[inline]
            fn into_any(self:
                    bevy_reflect::__macro_exports::alloc_utils::Box<Self>)
                ->
                    bevy_reflect::__macro_exports::alloc_utils::Box<dyn ::core::any::Any> {
                self
            }
            #[inline]
            fn as_any(&self) -> &dyn ::core::any::Any { self }
            #[inline]
            fn as_any_mut(&mut self) -> &mut dyn ::core::any::Any { self }
            #[inline]
            fn into_reflect(self:
                    bevy_reflect::__macro_exports::alloc_utils::Box<Self>)
                ->
                    bevy_reflect::__macro_exports::alloc_utils::Box<dyn bevy_reflect::Reflect> {
                self
            }
            #[inline]
            fn as_reflect(&self) -> &dyn bevy_reflect::Reflect { self }
            #[inline]
            fn as_reflect_mut(&mut self) -> &mut dyn bevy_reflect::Reflect {
                self
            }
            #[inline]
            fn set(&mut self,
                value:
                    bevy_reflect::__macro_exports::alloc_utils::Box<dyn bevy_reflect::Reflect>)
                ->
                    ::core::result::Result<(),
                    bevy_reflect::__macro_exports::alloc_utils::Box<dyn bevy_reflect::Reflect>> {
                *self = <dyn bevy_reflect::Reflect>::take(value)?;
                ::core::result::Result::Ok(())
            }
        }
        #[allow(non_upper_case_globals)]
        const _: () =
            {
                static __INVENTORY: ::inventory::Node =
                    ::inventory::Node {
                        value: &{
                                bevy_reflect::__macro_exports::auto_register::AutomaticReflectRegistrations(<AccessibleLabel
                                        as
                                        bevy_reflect::__macro_exports::auto_register::RegisterForReflection>::__register)
                            },
                        next: ::inventory::__private::UnsafeCell::new(::inventory::__private::Option::None),
                    };
                #[link_section = ".text.startup"]
                unsafe extern "C" fn __ctor() {
                    unsafe {
                        ::inventory::ErasedNode::submit(__INVENTORY.value,
                            &__INVENTORY)
                    }
                }
                #[used]
                #[link_section = ".init_array"]
                static __CTOR: unsafe extern "C" fn() = __ctor;
            };
        impl bevy_reflect::tuple_struct::TupleStruct for AccessibleLabel where
             {
            fn field(&self, index: usize)
                -> ::core::option::Option<&dyn bevy_reflect::PartialReflect> {
                match index {
                    0usize => ::core::option::Option::Some(&self.0),
                    _ => ::core::option::Option::None,
                }
            }
            fn field_mut(&mut self, index: usize)
                ->
                    ::core::option::Option<&mut dyn bevy_reflect::PartialReflect> {
                match index {
                    0usize => ::core::option::Option::Some(&mut self.0),
                    _ => ::core::option::Option::None,
                }
            }
            #[inline]
            fn field_len(&self) -> usize { 1usize }
            #[inline]
            fn iter_fields(&self)
                -> bevy_reflect::tuple_struct::TupleStructFieldIter {
                bevy_reflect::tuple_struct::TupleStructFieldIter::new(self)
            }
            fn to_dynamic_tuple_struct(&self)
                -> bevy_reflect::tuple_struct::DynamicTupleStruct {
                let mut dynamic:
                        bevy_reflect::tuple_struct::DynamicTupleStruct =
                    ::core::default::Default::default();
                dynamic.set_represented_type(bevy_reflect::PartialReflect::get_represented_type_info(self));
                dynamic.insert_boxed(bevy_reflect::PartialReflect::to_dynamic(&self.0));
                dynamic
            }
        }
        impl bevy_reflect::PartialReflect for AccessibleLabel where  {
            #[inline]
            fn get_represented_type_info(&self)
                -> ::core::option::Option<&'static bevy_reflect::TypeInfo> {
                ::core::option::Option::Some(<Self as
                            bevy_reflect::Typed>::type_info())
            }
            #[inline]
            fn try_apply(&mut self, value: &dyn bevy_reflect::PartialReflect)
                -> ::core::result::Result<(), bevy_reflect::ApplyError> {
                if let bevy_reflect::ReflectRef::TupleStruct(struct_value) =
                        bevy_reflect::PartialReflect::reflect_ref(value) {
                    for (i, value) in
                        ::core::iter::Iterator::enumerate(bevy_reflect::tuple_struct::TupleStruct::iter_fields(struct_value))
                        {
                        if let ::core::option::Option::Some(v) =
                                bevy_reflect::tuple_struct::TupleStruct::field_mut(self, i)
                            {
                            bevy_reflect::PartialReflect::try_apply(v, value)?;
                        }
                    }
                } else {
                    return ::core::result::Result::Err(bevy_reflect::ApplyError::MismatchedKinds {
                                from_kind: bevy_reflect::PartialReflect::reflect_kind(value),
                                to_kind: bevy_reflect::ReflectKind::TupleStruct,
                            });
                }
                ::core::result::Result::Ok(())
            }
            #[inline]
            fn reflect_kind(&self) -> bevy_reflect::ReflectKind {
                bevy_reflect::ReflectKind::TupleStruct
            }
            #[inline]
            fn reflect_ref(&self) -> bevy_reflect::ReflectRef {
                bevy_reflect::ReflectRef::TupleStruct(self)
            }
            #[inline]
            fn reflect_mut(&mut self) -> bevy_reflect::ReflectMut {
                bevy_reflect::ReflectMut::TupleStruct(self)
            }
            #[inline]
            fn reflect_owned(self:
                    bevy_reflect::__macro_exports::alloc_utils::Box<Self>)
                -> bevy_reflect::ReflectOwned {
                bevy_reflect::ReflectOwned::TupleStruct(self)
            }
            #[inline]
            fn try_into_reflect(self:
                    bevy_reflect::__macro_exports::alloc_utils::Box<Self>)
                ->
                    ::core::result::Result<bevy_reflect::__macro_exports::alloc_utils::Box<dyn bevy_reflect::Reflect>,
                    bevy_reflect::__macro_exports::alloc_utils::Box<dyn bevy_reflect::PartialReflect>> {
                ::core::result::Result::Ok(self)
            }
            #[inline]
            fn try_as_reflect(&self)
                -> ::core::option::Option<&dyn bevy_reflect::Reflect> {
                ::core::option::Option::Some(self)
            }
            #[inline]
            fn try_as_reflect_mut(&mut self)
                -> ::core::option::Option<&mut dyn bevy_reflect::Reflect> {
                ::core::option::Option::Some(self)
            }
            #[inline]
            fn into_partial_reflect(self:
                    bevy_reflect::__macro_exports::alloc_utils::Box<Self>)
                ->
                    bevy_reflect::__macro_exports::alloc_utils::Box<dyn bevy_reflect::PartialReflect> {
                self
            }
            #[inline]
            fn as_partial_reflect(&self)
                -> &dyn bevy_reflect::PartialReflect {
                self
            }
            #[inline]
            fn as_partial_reflect_mut(&mut self)
                -> &mut dyn bevy_reflect::PartialReflect {
                self
            }
            fn reflect_partial_eq(&self,
                value: &dyn bevy_reflect::PartialReflect)
                -> ::core::option::Option<bool> {
                (bevy_reflect::tuple_struct::tuple_struct_partial_eq)(self,
                    value)
            }
            fn reflect_partial_cmp(&self,
                value: &dyn bevy_reflect::PartialReflect)
                -> ::core::option::Option<::core::cmp::Ordering> {
                (bevy_reflect::tuple_struct::tuple_struct_partial_cmp)(self,
                    value)
            }
            fn debug(&self, f: &mut ::core::fmt::Formatter<'_>)
                -> ::core::fmt::Result {
                ::core::fmt::Debug::fmt(self, f)
            }
            #[inline]
            fn reflect_clone(&self)
                ->
                    ::core::result::Result<bevy_reflect::__macro_exports::alloc_utils::Box<dyn bevy_reflect::Reflect>,
                    bevy_reflect::ReflectCloneError> {
                ::core::result::Result::Ok(bevy_reflect::__macro_exports::alloc_utils::Box::new(::core::clone::Clone::clone(self)))
            }
        }
        impl bevy_reflect::FromReflect for AccessibleLabel where  {
            fn from_reflect(reflect: &dyn bevy_reflect::PartialReflect)
                -> ::core::option::Option<Self> {
                if let bevy_reflect::ReflectRef::TupleStruct(__ref_struct) =
                        bevy_reflect::PartialReflect::reflect_ref(reflect) {
                    let mut __this =
                        <Self as ::core::default::Default>::default();
                    if let ::core::option::Option::Some(__field) =
                            (||
                                        <String as
                                                bevy_reflect::FromReflect>::from_reflect(bevy_reflect::tuple_struct::TupleStruct::field(__ref_struct,
                                                    0)?))() {
                        __this.0 = __field;
                    }
                    ::core::option::Option::Some(__this)
                } else { ::core::option::Option::None }
            }
        }
    };Reflect)]
186#[reflect(Component, Default, Debug, Clone)]
187#[require(AccessibilityNode)]
188#[component(immutable, on_insert = on_label_inserted, on_remove = on_label_removed)]
189pub struct AccessibleLabel(pub String);
190
191impl AccessibleLabel {
192    /// Makes a new [`AccessibleLabel`] component.
193    pub fn new(text: impl Into<String>) -> Self {
194        Self(text.into())
195    }
196}
197
198fn on_label_inserted(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
199    if let Some(label) = world.get::<AccessibleLabel>(entity) {
200        let label_text = label.0.clone().into_boxed_str();
201        if let Some(mut accessible) = world.get_mut::<AccessibilityNode>(entity) {
202            accessible.set_label(label_text);
203        }
204    }
205}
206
207fn on_label_removed(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
208    if let Some(mut accessible) = world.get_mut::<AccessibilityNode>(entity) {
209        accessible.clear_label();
210    }
211}
212
213/// `AccessKit` integration for `bevy_ui`.
214pub(crate) struct AccessibilityPlugin;
215
216impl Plugin for AccessibilityPlugin {
217    fn build(&self, app: &mut App) {
218        app.add_systems(
219            PostUpdate,
220            (
221                button_changed,
222                image_changed,
223                label_changed,
224                sync_bounds_and_transforms
225                    .after(button_changed)
226                    .after(image_changed)
227                    .after(label_changed),
228            )
229                .in_set(UiSystems::PostLayout)
230                .before(AccessibilitySystems::Update),
231        );
232    }
233}