-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Adding interaction states for headless widgets. #19238
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 5 commits
5e153ed
743ca0d
cdc2b04
508fffd
a98547b
11aaf8a
6e4fc7f
58fa91a
50f0da6
48c98d9
c38af19
090307a
5547e8e
25d9d44
fc3943f
7d83ebc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -14,7 +14,7 @@ use crate::{ | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
use bevy_derive::{Deref, DerefMut}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
use bevy_ecs::prelude::*; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
use bevy_ecs::{entity::EntityHashSet, prelude::*}; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
use bevy_math::FloatOrd; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
use bevy_platform::collections::HashMap; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
use bevy_reflect::prelude::*; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -275,3 +275,116 @@ fn merge_interaction_states( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
new_interaction_state.insert(*hovered_entity, new_interaction); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// A component that allows users to use regular Bevy change detection to determine when the pointer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// enters or leaves an entity. Users should insert this component on an entity to indicate interest | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// in knowing about hover state changes. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// This is similar to the old Bevy [`Interaction`] component, except that it only tracks hover | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// state, not button presses or other interactions. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// The component's boolean value will be `true` whenever the pointer is currently hovering over the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// entity, or any of the entity's children. This value is guaranteed to only be mutated when the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// pointer enters or leaves the entity, allowing Bevy change detection to be used efficiently. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// This is in contrast to the [`HoverMap`] resource, which is updated every frame. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// Typically, a simple hoverable entity or widget will have this component added to it. More | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// complex widgets can have this component added to each hoverable part. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// The computational cost of keeping the `Hovering` components up to date is relatively cheap, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// and linear in the number of entities that have the `Hovering` component inserted. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#[derive(Component, Copy, Clone, Default, Eq, PartialEq, Debug, Reflect)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#[reflect(Component, Default, PartialEq, Debug, Clone)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pub struct Hovering(pub bool); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/// Uses [`HoverMap`] changes to update [`Hovering`] components. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
pub fn update_hovering_states( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
hover_map: Option<Res<HoverMap>>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
mut hovers: Query<(Entity, &mut Hovering)>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
parent_query: Query<&ChildOf>, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Don't bother collecting ancestors if there are no hovers. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if hovers.is_empty() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let Some(hover_map) = hover_map else { return }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This unwrap is probably cheaper than the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Set which contains the hovered for the current pointer entity and its ancestors. The capacity | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// is based on the likely tree depth of the hierarchy, which is typically greater for UI (because | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// of layout issues) than for 3D scenes. A depth of 32 is a reasonable upper bound for most use | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// cases. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let mut hover_ancestors = EntityHashSet::with_capacity(32); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
if let Some(map) = hover_map.get(&PointerId::Mouse) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
for (hovered_entity, _) in map.iter() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
hover_ancestors.insert(*hovered_entity); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
hover_ancestors.extend(parent_query.iter_ancestors(*hovered_entity)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// For each hovered entity, it is considered "hovering" if it's in the set of hovered ancestors. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
for (entity, mut hoverable) in hovers.iter_mut() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let is_hovering = hover_ancestors.contains(&entity); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
hoverable.set_if_neq(Hovering(is_hovering)); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If no hover map is found or it's empty, then we can just set all the flags to false I think:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the hover map is empty, then the set of ancestors will be empty as well, which means that the flags will get set to false naturally. Note that in practice the hover map will rarely be empty, unless the mouse is outside the window bounds, or there are no hoverable entities under the pointer. If there's no hover map at all - well. This is kinda sorta undefined behavior. I mean, it shouldn't crash. But there's really no point in having That being said, if you really feel strongly about it, I could change it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No don't worry about it, I'm being too fussy. It's fine as it is. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This seems wrong, hovered state isn't strictly transitive from child to parent since a child node can be positioned outside of its parents bounds. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe this is how the CSS Note that in CSS, the overflow property of the parent can affect whether the child is hoverable. However, the picking system should already handle this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah thinking about it there are circumstances where it makes sense that way. |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#[cfg(test)] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
mod tests { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
use bevy_render::camera::Camera; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
use super::*; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
#[test] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
fn update_hovering_is_memoized() { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let mut world = World::default(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let camera = world.spawn(Camera::default()).id(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Setup entities | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let hovered_child = world.spawn_empty().id(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let hovered_entity = world.spawn(Hovering(false)).add_child(hovered_child).id(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Setup hover map with hovered_entity hovered by mouse | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let mut hover_map = HoverMap::default(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let mut entity_map = HashMap::new(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
entity_map.insert( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
hovered_child, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
HitData { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
depth: 0.0, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
camera, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
position: None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
normal: None, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
hover_map.insert(PointerId::Mouse, entity_map); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
world.insert_resource(hover_map); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Run the system | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
assert!(world.run_system_cached(update_hovering_states).is_ok()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Check to insure that the hovered entity has the Hovering component set to true | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let hover = world.get_mut::<Hovering>(hovered_entity).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
assert!(hover.0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
assert!(hover.is_changed()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Now do it again, but don't change the hover map. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
world.increment_change_tick(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
assert!(world.run_system_cached(update_hovering_states).is_ok()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let hover = world.get_mut::<Hovering>(hovered_entity).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
assert!(hover.0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Should not be changed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// NOTE: Test doesn't work - thinks it is always changed | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// assert!(!hover.is_changed()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
// Clear the hover map and run again. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
world.insert_resource(HoverMap::default()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
world.increment_change_tick(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
assert!(world.run_system_cached(update_hovering_states).is_ok()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
let hover = world.get_mut::<Hovering>(hovered_entity).unwrap(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
assert!(!hover.0); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
assert!(hover.is_changed()); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
/// This module contains components that are used to track the interaction state of UI widgets. | ||
/// | ||
// Note to implementers: This uses a combination of both marker components and newtype components | ||
// containing a bool. Markers are used for one-way binding, "write-only" components | ||
// (like `InteractionDisabled`) that relay instructions from the user to the framework, whereas | ||
// newtype components are used to request state updates from the framework, which mutates the | ||
// content of those components on update. | ||
use bevy_a11y::AccessibilityNode; | ||
use bevy_ecs::{ | ||
component::{Component, HookContext}, | ||
world::DeferredWorld, | ||
}; | ||
|
||
/// A marker component to indicate that a widget is disabled and should be "grayed out". | ||
/// This is used to prevent user interaction with the widget. It should not, however, prevent | ||
/// the widget from being updated or rendered, or from acquiring keyboard focus. | ||
/// | ||
/// For apps which support a11y: if a widget (such as a slider) contains multiple entities, | ||
/// the `InteractionDisabled` component should be added to the root entity of the widget - the | ||
/// same entity that contains the `AccessibilityNode` component. This will ensure that | ||
/// the a11y tree is updated correctly. | ||
#[derive(Component, Debug, Clone, Copy)] | ||
#[component(on_add = on_add_disabled, on_remove = on_remove_disabled)] | ||
pub struct InteractionDisabled; | ||
|
||
// Hook to set the a11y "disabled" state when the widget is disabled. | ||
fn on_add_disabled(mut world: DeferredWorld, context: HookContext) { | ||
let mut entt = world.entity_mut(context.entity); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. BTW the name "entt" (pronounced "en-tee-tee") is one of the "short local variable names" that I often use in my code, I picked up the name from some Bevy code example, don't remember where. |
||
if let Some(mut accessibility) = entt.get_mut::<AccessibilityNode>() { | ||
accessibility.set_disabled(); | ||
} | ||
} | ||
|
||
// Hook to remove the a11y "disabled" state when the widget is enabled. | ||
fn on_remove_disabled(mut world: DeferredWorld, context: HookContext) { | ||
let mut entt = world.entity_mut(context.entity); | ||
if let Some(mut accessibility) = entt.get_mut::<AccessibilityNode>() { | ||
accessibility.clear_disabled(); | ||
} | ||
} | ||
|
||
/// Component that indicates whether a button is currently pressed. This will be true while | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is nothing enforcing this behavior at the moment. If this is a "universal" state intended to be shared and not something specific to, say, the upcoming "bevy core widgets" (which is where this behavior is implemented in your prototype), I think it should call out explicitly that this is a contract that implementers are expected to fulfill. That being said, maybe whether or not a button is pressed on drag should be a widget-specific behavior? (aka maybe we should delete this line, then document the drag behavior in the relevant core widget docs?) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also note that we can't call this It's an open question how universal we want this to be. Some widgets are pressable and others are not. But "pressing" has uses outside of UI - for example There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah this does feel a little awkward as we already have a universal "pressing" system ( Part of me does want to embrace an "uncontrolled" model here, as that would allow us to more directly unify the bevy_picking concepts/events/components with bevy_ui. When considering both this case and the hovered case, it feels like we aren't building a cohesive framework of concepts. Users will be confused. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nit: Buttons are neither controlled or uncontrolled because they don't have values - that is, while buttons do have state (hover and pressed), that state is meant for internal use only. Yes, the button state is accessible to allow for custom visual styling, but that state is not part of the protocol by which the button communicates with the rest of the app - it's not like a slider or checkbox which controls a value. What this means is that controlled widgets are only "controlled" with respect to the primary value being edited. All other state variables (like drag mode and offset) are managed by the widget internally, without the need for cooperation from the app. Or to put it even simpler: classic MVC. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ok yeah that makes sense. I was imagining the pressed state as similar to the "checked" state, in that a developer might want to gate whether or not it is actually "pressed" based on their own criteria. But that seems less likely. |
||
/// a drag action is in progress. | ||
#[derive(Component, Default, Debug)] | ||
pub struct ButtonPressed(pub bool); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Given that the other "bool state" components are immutable, maybe we should make this immutable too for consistency / to enable reliable observer-driven management? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ok |
||
|
||
/// Component that indicates whether a checkbox or radio button is in a checked state. | ||
#[derive(Component, Default, Debug)] | ||
#[component(immutable, on_add = on_add_checked, on_replace = on_add_checked)] | ||
pub struct Checked(pub bool); | ||
|
||
// Hook to set the a11y "checked" state when the checkbox is added. | ||
fn on_add_checked(mut world: DeferredWorld, context: HookContext) { | ||
let mut entt = world.entity_mut(context.entity); | ||
let checked = entt.get::<Checked>().unwrap().0; | ||
let mut accessibility = entt.get_mut::<AccessibilityNode>().unwrap(); | ||
accessibility.set_toggled(match checked { | ||
true => accesskit::Toggled::True, | ||
false => accesskit::Toggled::False, | ||
}); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
--- | ||
title: Headless Widgets | ||
authors: ["@viridia"] | ||
pull_requests: [19238] | ||
--- | ||
|
||
Bevy's `Button` and `Interaction` components have been around for a long time. Unfortunately | ||
these components have a number of shortcomings, such as the fact that they don't use the new | ||
`bevy_picking` framework, or the fact that they are really only useful for creating buttons | ||
and not other kinds of widgets like sliders. | ||
|
||
As an art form, games thrive on novelty: the typical game doesn't have boring, standardize controls | ||
reminiscent of a productivity app, but instead will have beautiful, artistic widgets that are | ||
in harmony with the game's overall visual theme. But writing new and unique widgets requires | ||
skill and subtlety, particularly if we want first-class accessibility support. It's not a burden we | ||
want to put on the average indie developer. | ||
|
||
In the web development world, "headless" widget libraries, such as react-headless and reakit have | ||
become popular. These provide standardized widgets that implement all of the correct interactions and | ||
behavioral logic, including integration with screen readers, but which are unstyled. It's the | ||
responsibility of the game developer to provide the visual style and animation for the widgets, | ||
which can fit the overall style of the game. | ||
|
||
With this release, Bevy introduces a collection of headless or "core" widgets. These are components | ||
which can be added to any UI Node to get widget-like behavior. The core widget set includes buttons, | ||
sliders, scrollbars, checkboxes, radio buttons, and more. This set will likely be expanded in | ||
future releases. | ||
|
||
## Widget Interaction States | ||
|
||
Many of the core widgets will define supplementary ECS components that are used to store the widget's | ||
state, similar to how the old `Interaction` component worked, but in a way that is more flexible. | ||
These components include: | ||
|
||
- `InteractionDisabled` - a marker component used to indicate that a component should be | ||
"grayed out" and non-interactive. Note that these disabled widgets are still visible and can | ||
have keyboard focus (otherwise the user would have no way to discover them). | ||
- `Hovering` is a simple boolean component that allows detection of whether the widget is being | ||
hovered using regular Bevy change detection. | ||
- `Checked` is a boolean component that stores the checked state of a checkbox or radio button. | ||
- `ButtonPressed` is used for a button-like widget, and will be true while the button is held down. | ||
|
||
The combination of `Hovering` and `ButtonPressed` fulfils the same purpose as the old `Interaction` | ||
component, except that now we can also represent "roll-off" behavior (the state where you click | ||
on a button and then, while holding the mouse down, move the pointer out of the button's bounds). | ||
It also provides additional flexibility in cases where a widget has multiple hoverable parts, | ||
or cases where a widget is hoverable but doesn't have a pressed state (such as a tree-view expansion | ||
toggle). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The name
Hovering
seems a bit odd, isn'tHovered
more idiomatic? It doesn't fit withChecked
andButtonPressed
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have thought about this. In this context, the two words mean the same thing. Actually it's a bit more subtle than that: the
-ed
suffix has two interpretations in English, "checked" could mean "the past tense of the verb 'check'" (meaning that at some point in the past it was checked) or it could mean "currently in a state of checkness". "Hovering", OTOH, only has a single interpretation.In any case, I don't see a clear winner between the two, and don't feel super strongly about it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems wrong to me I think because the component belongs to the UI node entity and the node itself isn't
Hovering
, it's beingHovered
by the pointer.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Renamed