From 1407b8a69f6440640f35d9215fe4799348b6193e Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 9 Jan 2025 11:35:09 -0800 Subject: [PATCH 1/7] First pass at `Activate` event --- crates/bevy_ui/Cargo.toml | 5 ++++ crates/bevy_ui/src/actions.rs | 48 +++++++++++++++++++++++++++++++++++ crates/bevy_ui/src/lib.rs | 1 + 3 files changed, 54 insertions(+) create mode 100644 crates/bevy_ui/src/actions.rs diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index 5cad6baa4a473..8768db4d054d5 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -43,6 +43,11 @@ smallvec = "1.11" accesskit = "0.17" tracing = { version = "0.1", default-features = false, features = ["std"] } +[dev-dependencies] +# This is currently a dev dependency for documentation purposes, +# but it will likely be used as a full dependency in the future. +bevy_input_focus = { path = "../bevy_input_focus", version = "0.16.0-dev" } + [features] default = [] serialize = ["serde", "smallvec/serde", "bevy_math/serialize"] diff --git a/crates/bevy_ui/src/actions.rs b/crates/bevy_ui/src/actions.rs new file mode 100644 index 0000000000000..3f3d86b163a2f --- /dev/null +++ b/crates/bevy_ui/src/actions.rs @@ -0,0 +1,48 @@ +//! 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. + +use bevy_ecs::event::Event; + +/// 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. +/// +/// Buttons should respond to this action via an observer to perform their primary action. +/// +/// # Example +/// +/// ```rust +/// use bevy_input_focus::InputFocus; +/// +/// fn send_activate_event_to_input_focus(keyboard_input: Res>, input_focus: Res, 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) { +/// let button_entity = trigger.target(); +/// println!("The button with the entity ID {button_entity} was activated!"); +/// } +/// +/// # assert_is_system!(send_activate_event_to_input_focus); +/// # assert_is_system!(spawn_my_button); +/// ``` +#[derive(Debug, Event, Copy, Clone, PartialEq, Eq, Hash)] +pub struct Activate; diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 49ca8ab54febe..65672b89977f8 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -26,6 +26,7 @@ pub mod picking_backend; use bevy_derive::{Deref, DerefMut}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; mod accessibility; +mod actions; // This module is not re-exported, but is instead made public. // This is intended to discourage accidental use of the experimental API. pub mod experimental; From 1e89dca4ad9dea6702975d966a70f35a62098a05 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 9 Jan 2025 11:46:23 -0800 Subject: [PATCH 2/7] Make events bubble --- crates/bevy_ui/src/actions.rs | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/actions.rs b/crates/bevy_ui/src/actions.rs index 3f3d86b163a2f..42574727adf8d 100644 --- a/crates/bevy_ui/src/actions.rs +++ b/crates/bevy_ui/src/actions.rs @@ -6,8 +6,19 @@ //! //! 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. -use bevy_ecs::event::Event; +use bevy_ecs::prelude::*; +use bevy_hierarchy::Parent; +use bevy_reflect::prelude::*; /// Activate a UI element. /// @@ -17,6 +28,11 @@ use bevy_ecs::event::Event; /// /// Buttons 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 @@ -39,10 +55,18 @@ use bevy_ecs::event::Event; /// fn activate_my_button(trigger: Trigger) { /// 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!(send_activate_event_to_input_focus); /// # assert_is_system!(spawn_my_button); /// ``` -#[derive(Debug, Event, Copy, Clone, PartialEq, Eq, Hash)] +#[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; +} From 1c6f4f0d881b0b42b87a06befd7f135baa6540c5 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 9 Jan 2025 12:01:10 -0800 Subject: [PATCH 3/7] Fix export strategy --- crates/bevy_ui/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 65672b89977f8..0655c5a9223f5 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -37,6 +37,7 @@ mod render; mod stack; mod ui_node; +pub use actions::*; pub use focus::*; pub use geometry::*; pub use layout::*; From a8dfdf6ca8f57c24d203b742c4a98f98c8d41839 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 9 Jan 2025 12:06:41 -0800 Subject: [PATCH 4/7] Link to and from `Button` docs --- crates/bevy_ui/src/actions.rs | 2 +- crates/bevy_ui/src/widget/button.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_ui/src/actions.rs b/crates/bevy_ui/src/actions.rs index 42574727adf8d..96dcaa474bbaf 100644 --- a/crates/bevy_ui/src/actions.rs +++ b/crates/bevy_ui/src/actions.rs @@ -26,7 +26,7 @@ use bevy_reflect::prelude::*; /// the enter key press on the focused element, /// or the "A" button on a gamepad. /// -/// Buttons should respond to this action via an observer to perform their primary action. +/// [`Button`](crate::widget::Button)s should respond to this action via an observer to perform their primary action. /// /// # Bubbling /// diff --git a/crates/bevy_ui/src/widget/button.rs b/crates/bevy_ui/src/widget/button.rs index 8445a4ad6266b..4daad802668f9 100644 --- a/crates/bevy_ui/src/widget/button.rs +++ b/crates/bevy_ui/src/widget/button.rs @@ -5,7 +5,9 @@ use bevy_ecs::{ }; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; -/// Marker struct for buttons +/// A marker struct for buttons. +/// +/// Buttons should use an observer to listen for the [`Activate`](crate::Activate) action to perform their primary action. #[derive(Component, Debug, Default, Clone, Copy, PartialEq, Eq, Reflect)] #[reflect(Component, Default, Debug, PartialEq)] #[require(Node, FocusPolicy(|| FocusPolicy::Block), Interaction)] From 13e04cce999b412d30f511976c76e3b218c4ce03 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 9 Jan 2025 12:07:26 -0800 Subject: [PATCH 5/7] Register type --- crates/bevy_ui/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 0655c5a9223f5..fc3a7d2b93fcd 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -151,6 +151,7 @@ impl Plugin for UiPlugin { app.init_resource::() .init_resource::() .init_resource::() + .register_type::() .register_type::() .register_type::() .register_type::() From 6ca5e55a63cc002d45f44cf7699c0394e3b27bfc Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 9 Jan 2025 12:31:42 -0800 Subject: [PATCH 6/7] Add systems --- crates/bevy_ui/Cargo.toml | 6 +-- crates/bevy_ui/src/actions.rs | 84 ++++++++++++++++++++++++++++++++++- crates/bevy_ui/src/lib.rs | 27 +++++++++++ 3 files changed, 110 insertions(+), 7 deletions(-) diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index 8768db4d054d5..919d3f097c5a0 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -20,6 +20,7 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.16.0-dev" } bevy_image = { path = "../bevy_image", version = "0.16.0-dev" } bevy_input = { path = "../bevy_input", version = "0.16.0-dev" } +bevy_input_focus = { path = "../bevy_input_focus", version = "0.16.0-dev" } bevy_math = { path = "../bevy_math", version = "0.16.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ "bevy", @@ -43,11 +44,6 @@ smallvec = "1.11" accesskit = "0.17" tracing = { version = "0.1", default-features = false, features = ["std"] } -[dev-dependencies] -# This is currently a dev dependency for documentation purposes, -# but it will likely be used as a full dependency in the future. -bevy_input_focus = { path = "../bevy_input_focus", version = "0.16.0-dev" } - [features] default = [] serialize = ["serde", "smallvec/serde", "bevy_math/serialize"] diff --git a/crates/bevy_ui/src/actions.rs b/crates/bevy_ui/src/actions.rs index 96dcaa474bbaf..84c53b30cf407 100644 --- a/crates/bevy_ui/src/actions.rs +++ b/crates/bevy_ui/src/actions.rs @@ -15,11 +15,87 @@ //! //! 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>, + node_query: Query<(), With>, + 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>, + input_focus: Res, + mut commands: Commands, +) { + if keyboard_input.just_pressed(KeyCode::Enter) { + 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, + 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), @@ -36,9 +112,13 @@ use bevy_reflect::prelude::*; /// # Example /// /// ```rust +/// use bevy_ecs::prelude::*; /// use bevy_input_focus::InputFocus; +/// use bevy_ui::Activate; +/// use bevy_input::keyboard::KeyCode; /// -/// fn send_activate_event_to_input_focus(keyboard_input: Res>, input_focus: Res, mut commands: Commands) { +/// // This system is already added to the `UiPlugin` by default. +/// fn activate_focus_on_enter(keyboard_input: Res>, input_focus: Res, mut commands: Commands) { /// if keyboard_input.just_pressed(KeyCode::Enter) { /// if let Some(focused_entity) = input_focus.get() { /// commands.trigger_targets(Activate, focused_entity); @@ -59,7 +139,7 @@ use bevy_reflect::prelude::*; /// trigger.propagate(false); /// } /// -/// # assert_is_system!(send_activate_event_to_input_focus); +/// # assert_is_system!(activate_focus_on_enter); /// # assert_is_system!(spawn_my_button); /// ``` #[derive(Component, Debug, Default, Copy, Clone, PartialEq, Eq, Hash, Reflect)] diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index fc3a7d2b93fcd..f16d42267cb6c 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -85,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 { @@ -97,6 +105,7 @@ impl Default for UiPlugin { enable_rendering: true, #[cfg(feature = "bevy_ui_picking_backend")] add_picking: true, + actions: true, } } } @@ -108,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. @@ -190,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, + ( + activate_focus_on_enter, + activate_ui_elements_on_click, + activate_focus_on_gamepad_south, + ) + .in_set(UiSystem::Actions), + ); + } + let ui_layout_system_config = ui_layout_system .in_set(UiSystem::Layout) .before(TransformSystem::TransformPropagate); From 0c706a6ec3a324db4b223d9027ffa132fddcde07 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Thu, 9 Jan 2025 12:35:20 -0800 Subject: [PATCH 7/7] Don't use glob imports into the crate root to preserve module docs --- crates/bevy_ui/src/lib.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index f16d42267cb6c..0ab9e36c4ed13 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -26,7 +26,6 @@ pub mod picking_backend; use bevy_derive::{Deref, DerefMut}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; mod accessibility; -mod actions; // This module is not re-exported, but is instead made public. // This is intended to discourage accidental use of the experimental API. pub mod experimental; @@ -37,7 +36,8 @@ mod render; mod stack; mod ui_node; -pub use actions::*; +pub mod actions; + pub use focus::*; pub use geometry::*; pub use layout::*; @@ -164,7 +164,7 @@ impl Plugin for UiPlugin { app.init_resource::() .init_resource::() .init_resource::() - .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -209,9 +209,9 @@ impl Plugin for UiPlugin { app.add_systems( PreUpdate, ( - activate_focus_on_enter, - activate_ui_elements_on_click, - activate_focus_on_gamepad_south, + actions::activate_focus_on_enter, + actions::activate_ui_elements_on_click, + actions::activate_focus_on_gamepad_south, ) .in_set(UiSystem::Actions), );