-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Add an Activate
triggered event to bevy_ui
#17267
Changes from all commits
1407b8a
1e89dca
1c6f4f0
a8dfdf6
13e04cc
6ca5e55
0c706a6
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 |
---|---|---|
@@ -0,0 +1,152 @@ | ||
//! Semantically meaningful actions that can be performed on UI elements. | ||
//! | ||
//! Rather than listening for raw keyboard or picking events, UI elements should listen to these actions | ||
//! for all functional behavior. This allows for more consistent behavior across different input devices | ||
//! and makes it easier to customize input mappings. | ||
//! | ||
//! By contrast, cosmetic behavior like hover effects should generally be implemented by reading the [`Interaction`](crate::focus::Interaction) component, | ||
//! the [`InputFocus`](bevy_input_focus::InputFocus) resource or in response to various [`Pointer`](bevy_picking::events::Pointer) events. | ||
//! | ||
//! # Event bubbling | ||
//! | ||
//! All of the events in this module are will automatically bubble up the entity hierarchy. | ||
//! This allows for more responsiveness to the users' input, as the event will be | ||
//! consumed by the first entity that cares about it. | ||
//! | ||
//! When responding to these events, make sure to call [`Trigger::propagate`] with `false` | ||
//! to prevent the event from being consumed by other later entities. | ||
//! | ||
//! # Systems | ||
//! | ||
//! Various public systems are provided to trigger these actions in response to raw input events. | ||
//! These systems run in [`PreUpdate`](bevy_app::main_schedule::PreUpdate) as part of [`UiSystem::Actions`](crate::UiSystem::Actions). | ||
//! They are all enabled by default in the [`UiPlugin`](crate::UiPlugin), | ||
//! but are split apart for more control over when / if they are run via run conditions. | ||
//! | ||
//! To disable them entirely, set [`UiPlugin::actions`](crate::UiPlugin::actions) to `false`. | ||
|
||
use bevy_ecs::prelude::*; | ||
use bevy_hierarchy::Parent; | ||
use bevy_input::{ | ||
gamepad::{Gamepad, GamepadButton}, | ||
keyboard::KeyCode, | ||
ButtonInput, | ||
}; | ||
use bevy_input_focus::InputFocus; | ||
use bevy_picking::events::{Click, Pointer}; | ||
use bevy_reflect::prelude::*; | ||
|
||
use crate::Node; | ||
|
||
/// A system which triggers the [`Activate`](crate::Activate) action | ||
/// when an entity with the [`Node`] component is clicked. | ||
pub fn activate_ui_elements_on_click( | ||
mut click_events: EventReader<Pointer<Click>>, | ||
node_query: Query<(), With<Node>>, | ||
mut commands: Commands, | ||
) { | ||
for click in click_events.read() { | ||
if node_query.contains(click.target) { | ||
commands.trigger_targets(Activate, click.target); | ||
} | ||
} | ||
} | ||
|
||
/// A system which activates the [`Activate`](crate::Activate) action | ||
/// when [`KeyCode::Enter`] is first pressed. | ||
pub fn activate_focus_on_enter( | ||
keyboard_input: Res<ButtonInput<KeyCode>>, | ||
input_focus: Res<InputFocus>, | ||
mut commands: Commands, | ||
) { | ||
if keyboard_input.just_pressed(KeyCode::Enter) { | ||
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. Hard-coding these to specific inputs isn't ideal. Seems like this is begging for an input-manager-defined remappable action 😄 |
||
if let Some(focused_entity) = input_focus.get() { | ||
commands.trigger_targets(Activate, focused_entity); | ||
} | ||
} | ||
} | ||
|
||
/// A system which activates the [`Activate`](crate::Activate) action | ||
/// when [`GamepadButton::South`] is first pressed on any controller. | ||
/// | ||
/// This system is generally not suitable for local co-op games, | ||
/// as *any* gamepad can activate the focused element. | ||
/// | ||
/// # Warning | ||
/// | ||
/// Note that for Nintendo Switch controllers, the "A" button (commonly used as "activate"), | ||
/// is *not* the South button. It's instead the [`GamepadButton::East`]. | ||
pub fn activate_focus_on_gamepad_south( | ||
input_focus: Res<InputFocus>, | ||
gamepads: Query<&Gamepad>, | ||
mut commands: Commands, | ||
) { | ||
for gamepad in gamepads.iter() { | ||
if gamepad.just_pressed(GamepadButton::South) { | ||
if let Some(focused_entity) = input_focus.get() { | ||
commands.trigger_targets(Activate, focused_entity); | ||
// Only send one activate event per frame, | ||
// even if multiple gamepads pressed the button. | ||
return; | ||
} | ||
} | ||
} | ||
} | ||
|
||
/// A system which activates the [`Activate`](crate::Activate) action | ||
/// when the [`GamepadButtonType::South`] is pressed. | ||
|
||
/// Activate a UI element. | ||
/// | ||
/// This is typically triggered by a mouse click (or press), | ||
/// the enter key press on the focused element, | ||
/// or the "A" button on a gamepad. | ||
/// | ||
/// [`Button`](crate::widget::Button)s should respond to this action via an observer to perform their primary action. | ||
/// | ||
/// # Bubbling | ||
/// | ||
/// This event will bubble up the entity hierarchy. | ||
/// Make sure to call [`Trigger::propagate`] with `false` to prevent the event from being consumed by other later entities. | ||
/// | ||
/// # Example | ||
/// | ||
/// ```rust | ||
/// use bevy_ecs::prelude::*; | ||
/// use bevy_input_focus::InputFocus; | ||
/// use bevy_ui::Activate; | ||
/// use bevy_input::keyboard::KeyCode; | ||
/// | ||
/// // This system is already added to the `UiPlugin` by default. | ||
/// fn activate_focus_on_enter(keyboard_input: Res<ButtonInput<KeyCode>>, input_focus: Res<InputFocus>, mut commands: Commands) { | ||
/// if keyboard_input.just_pressed(KeyCode::Enter) { | ||
/// if let Some(focused_entity) = input_focus.get() { | ||
/// commands.trigger_targets(Activate, focused_entity); | ||
/// } | ||
/// } | ||
/// } | ||
/// | ||
/// fn spawn_my_button(mut commands: Commands) { | ||
/// // This observer will only watch this entity; | ||
/// // use a global observer to respond to *any* Activate event. | ||
/// commands.spawn(Button).observe(activate_my_button); | ||
/// } | ||
/// | ||
/// fn activate_my_button(trigger: Trigger<Activate>) { | ||
/// let button_entity = trigger.target(); | ||
/// println!("The button with the entity ID {button_entity} was activated!"); | ||
/// // We've handled the event, so don't let it bubble up. | ||
/// trigger.propagate(false); | ||
/// } | ||
/// | ||
/// # assert_is_system!(activate_focus_on_enter); | ||
/// # assert_is_system!(spawn_my_button); | ||
/// ``` | ||
#[derive(Component, Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Reflect)] | ||
#[reflect(Component, Default, PartialEq, Hash)] | ||
pub struct Activate; | ||
|
||
impl Event for Activate { | ||
type Traversal = &'static Parent; | ||
const AUTO_PROPAGATE: bool = true; | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -36,6 +36,8 @@ mod render; | |
mod stack; | ||
mod ui_node; | ||
|
||
pub mod actions; | ||
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. The "pub use module::*`" style below is pretty terrible for documentation: both because "pile of stuff" and because it breaks module docs. I've broken from convention here: I would like to change the other modules in this crate in another PR. |
||
|
||
pub use focus::*; | ||
pub use geometry::*; | ||
pub use layout::*; | ||
|
@@ -83,10 +85,18 @@ use update::{update_clipping_system, update_target_camera_system}; | |
pub struct UiPlugin { | ||
/// If set to false, the UI's rendering systems won't be added to the `RenderApp` and no UI elements will be drawn. | ||
/// The layout and interaction components will still be updated as normal. | ||
/// | ||
/// Default is true. | ||
pub enable_rendering: bool, | ||
/// Whether to add the UI picking backend to the app. | ||
/// | ||
/// Default is true. | ||
#[cfg(feature = "bevy_ui_picking_backend")] | ||
pub add_picking: bool, | ||
/// If set to true, bevy_ui will listen for input events and send them to UI entities. | ||
/// | ||
/// Default is true. | ||
pub actions: bool, | ||
} | ||
|
||
impl Default for UiPlugin { | ||
|
@@ -95,6 +105,7 @@ impl Default for UiPlugin { | |
enable_rendering: true, | ||
#[cfg(feature = "bevy_ui_picking_backend")] | ||
add_picking: true, | ||
actions: true, | ||
} | ||
} | ||
} | ||
|
@@ -106,6 +117,10 @@ pub enum UiSystem { | |
/// | ||
/// Runs in [`PreUpdate`]. | ||
Focus, | ||
/// Various UI-centric actions, such as activating buttons, are computed from input. | ||
/// | ||
/// Runs in [`PreUpdate`], after [`InputSystem`]. | ||
Actions, | ||
/// All UI systems in [`PostUpdate`] will run in or after this label. | ||
Prepare, | ||
/// After this label, the ui layout state has been updated. | ||
|
@@ -149,6 +164,7 @@ impl Plugin for UiPlugin { | |
app.init_resource::<UiSurface>() | ||
.init_resource::<UiScale>() | ||
.init_resource::<UiStack>() | ||
.register_type::<actions::Activate>() | ||
.register_type::<BackgroundColor>() | ||
.register_type::<CalculatedClip>() | ||
.register_type::<ComputedNode>() | ||
|
@@ -187,6 +203,20 @@ impl Plugin for UiPlugin { | |
ui_focus_system.in_set(UiSystem::Focus).after(InputSystem), | ||
); | ||
|
||
if self.actions { | ||
app.configure_sets(PreUpdate, UiSystem::Actions.after(InputSystem)); | ||
|
||
app.add_systems( | ||
PreUpdate, | ||
( | ||
actions::activate_focus_on_enter, | ||
actions::activate_ui_elements_on_click, | ||
actions::activate_focus_on_gamepad_south, | ||
) | ||
.in_set(UiSystem::Actions), | ||
); | ||
} | ||
|
||
let ui_layout_system_config = ui_layout_system | ||
.in_set(UiSystem::Layout) | ||
.before(TransformSystem::TransformPropagate); | ||
|
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 see why you're adding this, but it feels like it might be too opinionated. A generic "activate" action seems like it would be context specific. For example, I can easily imagine a button that wants to "activate" on release, that only "activates" while held, or only if it is held for a period of time, or that wants to activate even if it is not focused, or a widget that cannot be "activated" at all.
If we're going to go this route, I think we should embrace it as a "high level focused button system" that uses familiar and less opinionated terminology and encapsulates the whole lifecycle (ex: something like Pressed / Released or FocusedPress/FocusedRelease), and makes that state poll-able (to allow people to distinguish between "just pressed" and "currently pressed" each frame). And it would need to be able to accommodate things like "firing the release event for an entity if focus changes while pressed".
I also think that a lot of the utility / UX improvement of this impl comes from performing an action based on multiple input types (aka this is providing the same utility that an action manager would provide).
Investing in an upstream action system would let each widget express things like:
From there, we could build the "high level entity focus based virtual button system" on top:
However I think introducing this at all seriously complicates the "event handling story", as users now need to decide to listen for either
Pointer<Pressed>
orFocusedPress
. These are two separate generic input event systems that both perform very similar roles (andPointer<Pressed>
directly drives the focus system). It also means that people that want pointer information in the context ofFocusedPress
would need to grab that from somewhere else. I'm thinking a "virtual input-action-driven Pointer", as you used #17224 is probably the better move, as it allows everyone to snap to the single, already-generic pointer API. All of the boilerplate of producing the generic pointer info would be internal bevy logic (so not user facing), and it would all serve a purpose (people want to able to write logic about pointer position, read hit data, etc).Imagine a button that when pressed, applies a visual effect at the point it was pressed at. If a context-free event like
FocusedPress
is used, they would need to somehow grab the current pointer state separately (if it was driven by a pointer) and if it was driven by a button, pick some fall back logic. By embracing Pointer as the "one api" and implementing a virtual pointer system, we can make an opinionated choice about where it exists on screen (ex: the "middle" of the focused UI element). Then consumers of the API can blissfully code to the Pointer api (and receive all of its benefits). This also allows the "virtual pointer" behavior to be user-configurable. Ex: maybe they want to trigger the "middle pointer click" on a given input action. And by nature of supporting "multiple pointers", we get all of the same behaviors "for free" / they behave consistently (ex: the most recent pointer change "stealing" focus), without users needing to think about supporting both use cases.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'm in agreement with you here, at least on the first part. I'm OK with the concept of an
Activate
event, but I think that the decision as to when it should be triggered is widget-specific.Consider the case of a checkbox: emitting an
Activate
event is not all that useful, what you really want is aValueChanged(bool)
event. A slider emits aValueChanged(f32)
and so on. Different widgets will emit different "semantic" events which are up-conversions of low-level "physical" events.Different button behaviors that you describe (activate on middle button and so on) could easily be implemented as distinct widget types - it's just a set of observers. However, we don't want developers to have to constantly re-implement
Button
orCheckbox
behaviors, because the "correct" behaviors are subtle and easy to get wrong, especially when we add a11y into the mix.This is why I have been lobbying for a collection of "core widgets" in Bevy. These would be "headless" (unstyled) widgets that implement PushButton, Checkbox, Slider, Spinbox and other behaviors, and would include the proper focus handling and accessibility goodness.
These headless widgets would be designed to work in conjunction with BSN or whatever templating system - they are just components after all, with a handful of registered observers to update the component state and re-emit events.
Such widgets could use
Activate
andValueChanged<T>
, so I think it's helpful to define these events in a standard way; but I don't like the idea of triggering them broadly.