-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Adding interaction states for headless widgets. (ui only) #19349
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
Open
viridia
wants to merge
5
commits into
bevyengine:main
Choose a base branch
from
viridia:widget_interactions
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 2 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
68c2c50
Adding interaction states for headless widgets. (ui only)
viridia 35cd0e0
Updated list of PRs in release notes.
viridia 2237abe
Typo (review feedback).
viridia 5205c5c
Renamed ButtonPressed to Depressed.
viridia ca7ac75
Updated doc.
viridia File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 entity = world.entity_mut(context.entity); | ||
if let Some(mut accessibility) = entity.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 entity = world.entity_mut(context.entity); | ||
if let Some(mut accessibility) = entity.get_mut::<AccessibilityNode>() { | ||
accessibility.clear_disabled(); | ||
} | ||
} | ||
|
||
/// Component that indicates whether a button is currently pressed. | ||
#[derive(Component, Default, Debug)] | ||
#[component(immutable)] | ||
pub struct ButtonPressed(pub bool); | ||
|
||
/// 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 entity = world.entity_mut(context.entity); | ||
let checked = entity.get::<Checked>().unwrap().0; | ||
let mut accessibility = entity.get_mut::<AccessibilityNode>().unwrap(); | ||
accessibility.set_toggled(match checked { | ||
true => accesskit::Toggled::True, | ||
false => accesskit::Toggled::False, | ||
}); | ||
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
--- | ||
title: Headless Widgets | ||
authors: ["@viridia"] | ||
pull_requests: [19238, 19349] | ||
--- | ||
|
||
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 | ||
[headlessui](https://headlessui.com/) and [reakit](https://reakit.io/) 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 their 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` fulfills 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). |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.