Skip to content

Commit 5e153ed

Browse files
committed
Adding interaction states for headless widgets.
Part of bevyengine#19236
1 parent 1395152 commit 5e153ed

File tree

2 files changed

+96
-0
lines changed

2 files changed

+96
-0
lines changed
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
/// This module contains components that are used to track the interaction state of UI widgets.
2+
///
3+
// Note to implementers: This uses a combination of both marker components and newtype components
4+
// containing a bool. Markers are used for one-way binding, "write-only" components
5+
// (like `InteractionDisabled`) that relay instructions from the user to the framework, whereas
6+
// newtype components are used to request state updates from the framework, which mutates the
7+
// content of those components on update.
8+
use bevy_a11y::AccessibilityNode;
9+
use bevy_ecs::{
10+
component::{Component, HookContext},
11+
entity::Entity,
12+
hierarchy::ChildOf,
13+
system::{Query, Res},
14+
world::DeferredWorld,
15+
};
16+
use bevy_picking::{hover::HoverMap, pointer::PointerId};
17+
18+
/// A marker component to indicate that a widget is disabled and should be "grayed out".
19+
/// This is used to prevent user interaction with the widget. It should not, however, prevent
20+
/// the widget from being updated or rendered, or from acquiring keyboard focus.
21+
///
22+
/// For apps which support a11y: if a widget (such as a slider) contains multiple entities,
23+
/// the `InteractionDisabled` component should be added to the root entity of the widget - the
24+
/// same entity that contains the `AccessibilityNode` component. This will ensure that
25+
/// the a11y tree is updated correctly.
26+
#[derive(Component, Debug, Clone, Copy)]
27+
#[component(on_add = on_add_disabled, on_remove = on_remove_disabled)]
28+
pub struct InteractionDisabled;
29+
30+
// Hook to set the a11y "disabled" state when the widget is disabled.
31+
fn on_add_disabled(mut world: DeferredWorld, context: HookContext) {
32+
let mut entt = world.entity_mut(context.entity);
33+
if let Some(mut accessibility) = entt.get_mut::<AccessibilityNode>() {
34+
accessibility.set_disabled();
35+
}
36+
}
37+
38+
// Hook to remove the a11y "disabled" state when the widget is enabled.
39+
fn on_remove_disabled(mut world: DeferredWorld, context: HookContext) {
40+
let mut entt = world.entity_mut(context.entity);
41+
if let Some(mut accessibility) = entt.get_mut::<AccessibilityNode>() {
42+
accessibility.clear_disabled();
43+
}
44+
}
45+
46+
/// Component that indicates whether a button is currently pressed. This will be true while
47+
/// a drag action is in progress.
48+
#[derive(Component, Default, Debug)]
49+
pub struct ButtonPressed(pub bool);
50+
51+
/// Component that indicates whether a checkbox or radio button is in a checked state.
52+
#[derive(Component, Default, Debug)]
53+
#[component(immutable, on_add = on_add_checked, on_replace = on_add_checked)]
54+
pub struct Checked(pub bool);
55+
56+
// Hook to set the a11y "checked" state when the checkbox is added.
57+
fn on_add_checked(mut world: DeferredWorld, context: HookContext) {
58+
let mut entt = world.entity_mut(context.entity);
59+
let checked = entt.get::<Checked>().unwrap().0;
60+
let mut accessibility = entt.get_mut::<AccessibilityNode>().unwrap();
61+
accessibility.set_toggled(match checked {
62+
true => accesskit::Toggled::True,
63+
false => accesskit::Toggled::False,
64+
});
65+
}
66+
67+
/// Component which indicates that the entity is interested in knowing when the mouse is hovering
68+
/// over it or any of its children. Using this component lets users use regular Bevy change
69+
/// detection for hover enter and leave transitions instead of having to rely on observers or hooks.
70+
///
71+
/// TODO: This component and it's associated system isn't UI-specific, and could be moved to the
72+
/// bevy_picking crate.
73+
#[derive(Debug, Clone, Copy, Component, Default)]
74+
pub struct Hovering(pub bool);
75+
76+
// TODO: This should be registered as a system after the hover map is updated.
77+
pub(crate) fn update_hover_states(
78+
hover_map: Option<Res<HoverMap>>,
79+
mut hovers: Query<(Entity, &mut Hovering)>,
80+
parent_query: Query<&ChildOf>,
81+
) {
82+
let Some(hover_map) = hover_map else { return };
83+
let hover_set = hover_map.get(&PointerId::Mouse);
84+
for (entity, mut hoverable) in hovers.iter_mut() {
85+
let is_hovering = match hover_set {
86+
Some(map) => map.iter().any(|(ha, _)| {
87+
*ha == entity || parent_query.iter_ancestors(*ha).any(|e| e == entity)
88+
}),
89+
None => false,
90+
};
91+
if hoverable.0 != is_hovering {
92+
hoverable.0 = is_hovering;
93+
}
94+
}
95+
}

crates/bevy_ui/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//! Spawn UI elements with [`widget::Button`], [`ImageNode`], [`Text`](prelude::Text) and [`Node`]
1111
//! This UI is laid out with the Flexbox and CSS Grid layout models (see <https://cssreference.io/flexbox/>)
1212
13+
pub mod interaction_states;
1314
pub mod measurement;
1415
pub mod ui_material;
1516
pub mod update;

0 commit comments

Comments
 (0)