Skip to main content

bevy_ui/
accessibility.rs

1#[expect(
2    deprecated,
3    reason = "Should be removed after 0.20 is released when Button is removed."
4)]
5use crate::prelude::Button;
6use crate::{
7    experimental::UiChildren,
8    prelude::Label,
9    ui_transform::UiGlobalTransform,
10    widget::{ImageNode, TextUiReader},
11    ComputedNode, UiSystems,
12};
13use bevy_a11y::{AccessibilityNode, AccessibilitySystems};
14use bevy_app::{App, Plugin, PostUpdate};
15use bevy_ecs::{
16    change_detection::DetectChanges,
17    component::Component,
18    hierarchy::ChildOf,
19    lifecycle::HookContext,
20    prelude::Entity,
21    query::{Changed, With, Without},
22    reflect::ReflectComponent,
23    schedule::{IntoScheduleConfigs, SystemSet},
24    system::{Commands, Query},
25    world::{DeferredWorld, Ref},
26};
27use bevy_math::Affine2;
28use bevy_reflect::prelude::ReflectDefault;
29
30use accesskit::{Affine, Node, Rect, Role};
31use bevy_reflect::Reflect;
32
33fn calc_label(
34    text_reader: &mut TextUiReader,
35    children: impl Iterator<Item = Entity>,
36) -> Option<Box<str>> {
37    let mut name = None;
38    for child in children {
39        let values = text_reader
40            .iter(child)
41            .filter_map(|(_, _, item)| match item {
42                bevy_text::TextElement::Text { text, .. } => Some(text.into()),
43                bevy_text::TextElement::Box(_) => None,
44            })
45            .collect::<Vec<String>>();
46        if !values.is_empty() {
47            name = Some(values.join(" "));
48        }
49    }
50    name.map(String::into_boxed_str)
51}
52
53fn sync_bounds_and_transforms(
54    mut accessible_nodes_query: Query<(
55        &mut AccessibilityNode,
56        Ref<ComputedNode>,
57        Ref<UiGlobalTransform>,
58        Option<&ChildOf>,
59    )>,
60    accessible_transform_query: Query<Ref<UiGlobalTransform>, With<AccessibilityNode>>,
61) {
62    for (mut accessible, node, ui_transform, maybe_child_of) in &mut accessible_nodes_query {
63        let maybe_parent_transform = maybe_child_of
64            .and_then(|child_of| accessible_transform_query.get(child_of.parent()).ok());
65
66        if !(node.is_changed()
67            || ui_transform.is_changed()
68            || maybe_parent_transform.is_some_and(|transform| transform.is_changed()))
69        {
70            continue;
71        }
72
73        accessible.set_bounds(Rect::new(
74            -0.5 * node.size.x as f64,
75            -0.5 * node.size.y as f64,
76            0.5 * node.size.x as f64,
77            0.5 * node.size.y as f64,
78        ));
79
80        // If the node has an accessible parent, its transform in the accessibility tree must be relative to the parent.
81        let transform = maybe_parent_transform
82            .and_then(|transform| transform.try_inverse())
83            .unwrap_or_default()
84            * ui_transform.affine();
85
86        if transform.is_finite() && transform != Affine2::IDENTITY {
87            accessible.set_transform(Affine::new(transform.to_cols_array().map(f64::from)));
88        } else {
89            accessible.clear_transform();
90        }
91    }
92}
93
94#[expect(
95    deprecated,
96    reason = "Should be removed after 0.20 is released when Button is removed."
97)]
98fn button_changed(
99    mut commands: Commands,
100    mut query: Query<(Entity, Option<&mut AccessibilityNode>), Changed<Button>>,
101    ui_children: UiChildren,
102    mut text_reader: TextUiReader,
103) {
104    for (entity, accessible) in &mut query {
105        let label = calc_label(&mut text_reader, ui_children.iter_ui_children(entity));
106        if let Some(mut accessible) = accessible {
107            accessible.set_role(Role::Button);
108            if let Some(name) = label {
109                accessible.set_label(name);
110            } else {
111                accessible.clear_label();
112            }
113        } else {
114            let mut node = Node::new(Role::Button);
115            if let Some(label) = label {
116                node.set_label(label);
117            }
118            commands
119                .entity(entity)
120                .try_insert(AccessibilityNode::from(node));
121        }
122    }
123}
124
125#[expect(
126    deprecated,
127    reason = "Should remove the `Without<Button>` after 0.20 is released when Button is removed."
128)]
129fn image_changed(
130    mut commands: Commands,
131    mut query: Query<
132        (Entity, Option<&mut AccessibilityNode>),
133        (Changed<ImageNode>, Without<Button>),
134    >,
135    ui_children: UiChildren,
136    mut text_reader: TextUiReader,
137) {
138    for (entity, accessible) in &mut query {
139        let label = calc_label(&mut text_reader, ui_children.iter_ui_children(entity));
140        if let Some(mut accessible) = accessible {
141            accessible.set_role(Role::Image);
142            if let Some(label) = label {
143                accessible.set_label(label);
144            } else {
145                accessible.clear_label();
146            }
147        } else {
148            let mut node = Node::new(Role::Image);
149            if let Some(label) = label {
150                node.set_label(label);
151            }
152            commands
153                .entity(entity)
154                .try_insert(AccessibilityNode::from(node));
155        }
156    }
157}
158
159fn label_changed(
160    mut commands: Commands,
161    mut query: Query<(Entity, Option<&mut AccessibilityNode>), Changed<Label>>,
162    mut text_reader: TextUiReader,
163) {
164    for (entity, accessible) in &mut query {
165        let values = text_reader
166            .iter(entity)
167            .filter_map(|(_, _, item)| match item {
168                bevy_text::TextElement::Text { text, .. } => Some(text.into()),
169                bevy_text::TextElement::Box(_) => None,
170            })
171            .collect::<Vec<String>>();
172        let label = Some(values.join(" ").into_boxed_str());
173        if let Some(mut accessible) = accessible {
174            accessible.set_role(Role::Label);
175            if let Some(label) = label {
176                accessible.set_value(label);
177            } else {
178                accessible.clear_value();
179            }
180        } else {
181            let mut node = Node::new(Role::Label);
182            if let Some(label) = label {
183                node.set_value(label);
184            }
185            commands
186                .entity(entity)
187                .try_insert(AccessibilityNode::from(node));
188        }
189    }
190}
191
192/// System set for UI Systems that update the [`AccessibilityNode`] information
193/// of UI-related entities
194#[derive(const _: () =
    {
        extern crate alloc;
        impl bevy_ecs::schedule::SystemSet for AccessibilityUiSystems where
            Self: 'static + ::core::marker::Send + ::core::marker::Sync +
            ::core::clone::Clone + ::core::cmp::Eq + ::core::fmt::Debug +
            ::core::hash::Hash {
            fn dyn_clone(&self)
                -> alloc::boxed::Box<dyn bevy_ecs::schedule::SystemSet> {
                alloc::boxed::Box::new(::core::clone::Clone::clone(self))
            }
        }
    };SystemSet, #[automatically_derived]impl ::core::fmt::Debug for AccessibilityUiSystems {
    #[inline]
    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
        ::core::fmt::Formatter::write_str(f, "AccessibilityUiSystems")
    }
}Debug, #[automatically_derived]#[doc(hidden)]unsafe impl ::core::clone::TrivialClone
    for AccessibilityUiSystems {
}
#[automatically_derived]impl ::core::clone::Clone for AccessibilityUiSystems {
    #[inline]
    fn clone(&self) -> Self { *self }
}Clone, #[automatically_derived]impl ::core::marker::Copy for AccessibilityUiSystems {
}Copy, #[automatically_derived]impl ::core::marker::StructuralPartialEq for
    AccessibilityUiSystems {
}
#[automatically_derived]impl ::core::cmp::PartialEq for AccessibilityUiSystems
    {
    #[inline]
    fn eq(&self, other: &Self) -> bool { true }
}PartialEq, #[automatically_derived]impl ::core::cmp::Eq for AccessibilityUiSystems { }Eq, #[automatically_derived]impl ::core::hash::Hash for AccessibilityUiSystems {
    #[inline]
    fn hash<__H: ::core::hash::Hasher>(&self, state: &mut __H) {}
}Hash)]
195pub struct AccessibilityUiSystems;
196
197/// A component which permits the a11y label to be specified independently from other a11y
198/// attributes.
199///
200/// The content of the `label` attribute is typically application-specific, and frequently
201/// originates in application code rather than library code. Because the primary mechanism of entity
202/// composition in Bevy is component insertion (especially in BSN scenes), and because ``accesskit``
203/// mandates that all a11y properties be stored in a single data structure, it can be cumbersome
204/// to combine together a11y properties coming from different parts of the code; making the label
205/// its own component makes it possible to specify the label as a mixin.
206///
207/// Internally, what this does is update the [`AccessibilityNode`] component, using component hooks
208/// which are automatically registered when this component is used.
209#[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() -> Self {
        AccessibleLabel(::core::default::Default::default())
    }
}Default, #[automatically_derived]impl ::core::clone::Clone for AccessibleLabel {
    #[inline]
    fn clone(&self) -> Self {
        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 as
                            bevy_reflect::CreateTypeData<Self>>::create_type_data(()));
                registration.insert(<bevy_reflect::ReflectFromReflect as
                            bevy_reflect::CreateTypeData<Self>>::create_type_data(()));
                registration.register_type_data_with::<ReflectComponent, Self,
                    _>(());
                registration.register_type_data_with::<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(never)]
            fn type_info() -> &'static bevy_reflect::TypeInfo {
                static CELL: bevy_reflect::utility::NonGenericTypeInfoCell =
                    bevy_reflect::utility::NonGenericTypeInfoCell::new();
                CELL.get_or_set(#[inline(never)] ||
                        {
                            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)
                ->
                    ::core::result::Result<bevy_reflect::tuple_struct::DynamicTupleStruct,
                    bevy_reflect::ReflectCloneError> {
                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)?);
                ::core::result::Result::Ok(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)]
210#[reflect(Component, Default, Debug, Clone)]
211#[require(AccessibilityNode)]
212#[component(immutable, on_insert = on_label_inserted, on_remove = on_label_removed)]
213pub struct AccessibleLabel(pub String);
214
215impl AccessibleLabel {
216    /// Makes a new [`AccessibleLabel`] component.
217    pub fn new(text: impl Into<String>) -> Self {
218        Self(text.into())
219    }
220}
221
222fn on_label_inserted(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
223    if let Some(label) = world.get::<AccessibleLabel>(entity) {
224        let label_text = label.0.clone().into_boxed_str();
225        if let Some(mut accessible) = world.get_mut::<AccessibilityNode>(entity) {
226            accessible.set_label(label_text);
227        }
228    }
229}
230
231fn on_label_removed(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
232    if let Some(mut accessible) = world.get_mut::<AccessibilityNode>(entity) {
233        accessible.clear_label();
234    }
235}
236
237/// `AccessKit` integration for `bevy_ui`.
238pub(crate) struct AccessibilityPlugin;
239
240impl Plugin for AccessibilityPlugin {
241    fn build(&self, app: &mut App) {
242        app.add_systems(
243            PostUpdate,
244            (
245                button_changed,
246                image_changed,
247                label_changed,
248                sync_bounds_and_transforms
249                    .after(button_changed)
250                    .after(image_changed)
251                    .after(label_changed),
252            )
253                .in_set(UiSystems::PostLayout)
254                .in_set(AccessibilityUiSystems)
255                .before(AccessibilitySystems::Update),
256        );
257    }
258}