diff --git a/crates/bevy_core_widgets/src/core_button.rs b/crates/bevy_core_widgets/src/core_button.rs index ec30b625f9..97b15b878d 100644 --- a/crates/bevy_core_widgets/src/core_button.rs +++ b/crates/bevy_core_widgets/src/core_button.rs @@ -2,7 +2,6 @@ use accesskit::Role; use bevy_a11y::AccessibilityNode; use bevy_app::{App, Plugin}; use bevy_ecs::query::Has; -use bevy_ecs::system::ResMut; use bevy_ecs::{ component::Component, entity::Entity, @@ -11,7 +10,8 @@ use bevy_ecs::{ system::{Commands, Query, SystemId}, }; use bevy_input::keyboard::{KeyCode, KeyboardInput}; -use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; +use bevy_input::ButtonState; +use bevy_input_focus::FocusedInput; use bevy_picking::events::{Cancel, Click, DragEnd, Pointer, Press, Release}; use bevy_ui::{InteractionDisabled, Pressed}; @@ -36,6 +36,7 @@ fn button_on_key_event( if !disabled { let event = &trigger.event().input; if !event.repeat + && event.state == ButtonState::Pressed && (event.key_code == KeyCode::Enter || event.key_code == KeyCode::Space) { if let Some(on_click) = bstate.on_click { @@ -65,24 +66,12 @@ fn button_on_pointer_click( fn button_on_pointer_down( mut trigger: On>, mut q_state: Query<(Entity, Has, Has), With>, - focus: Option>, - focus_visible: Option>, mut commands: Commands, ) { if let Ok((button, disabled, pressed)) = q_state.get_mut(trigger.target()) { trigger.propagate(false); - if !disabled { - if !pressed { - commands.entity(button).insert(Pressed); - } - // Clicking on a button makes it the focused input, - // and hides the focus ring if it was visible. - if let Some(mut focus) = focus { - focus.0 = (trigger.target() != Entity::PLACEHOLDER).then_some(trigger.target()); - } - if let Some(mut focus_visible) = focus_visible { - focus_visible.0 = false; - } + if !disabled && !pressed { + commands.entity(button).insert(Pressed); } } } diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index ecd6d52fbe..5a6b90636a 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -3,12 +3,11 @@ use core::ops::RangeInclusive; use accesskit::{Orientation, Role}; use bevy_a11y::AccessibilityNode; use bevy_app::{App, Plugin}; -use bevy_ecs::entity::Entity; use bevy_ecs::event::{EntityEvent, Event}; -use bevy_ecs::hierarchy::{ChildOf, Children}; +use bevy_ecs::hierarchy::Children; use bevy_ecs::lifecycle::Insert; use bevy_ecs::query::Has; -use bevy_ecs::system::{In, Res, ResMut}; +use bevy_ecs::system::{In, Res}; use bevy_ecs::world::DeferredWorld; use bevy_ecs::{ component::Component, @@ -18,7 +17,7 @@ use bevy_ecs::{ }; use bevy_input::keyboard::{KeyCode, KeyboardInput}; use bevy_input::ButtonState; -use bevy_input_focus::{FocusedInput, InputFocus, InputFocusVisible}; +use bevy_input_focus::FocusedInput; use bevy_log::warn_once; use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press}; use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale}; @@ -207,43 +206,18 @@ pub(crate) fn slider_on_pointer_down( )>, q_thumb: Query<&ComputedNode, With>, q_children: Query<&Children>, - q_parents: Query<&ChildOf>, - focus: Option>, - focus_visible: Option>, mut commands: Commands, ui_scale: Res, ) { if q_thumb.contains(trigger.target()) { // Thumb click, stop propagation to prevent track click. trigger.propagate(false); - - // Find the slider entity that's an ancestor of the thumb - if let Some(slider_entity) = q_parents - .iter_ancestors(trigger.target()) - .find(|entity| q_slider.contains(*entity)) - { - // Set focus to slider and hide focus ring - if let Some(mut focus) = focus { - focus.0 = Some(slider_entity); - } - if let Some(mut focus_visible) = focus_visible { - focus_visible.0 = false; - } - } } else if let Ok((slider, value, range, step, node, node_target, transform, disabled)) = q_slider.get(trigger.target()) { // Track click trigger.propagate(false); - // Set focus to slider and hide focus ring - if let Some(mut focus) = focus { - focus.0 = (trigger.target() != Entity::PLACEHOLDER).then_some(trigger.target()); - } - if let Some(mut focus_visible) = focus_visible { - focus_visible.0 = false; - } - if disabled { return; } diff --git a/crates/bevy_input_focus/Cargo.toml b/crates/bevy_input_focus/Cargo.toml index 49eea8dcea..f63d436af1 100644 --- a/crates/bevy_input_focus/Cargo.toml +++ b/crates/bevy_input_focus/Cargo.toml @@ -64,6 +64,7 @@ bevy_app = { path = "../bevy_app", version = "0.16.0-dev", default-features = fa bevy_ecs = { path = "../bevy_ecs", version = "0.16.0-dev", default-features = false } bevy_input = { path = "../bevy_input", version = "0.16.0-dev", default-features = false } bevy_math = { path = "../bevy_math", version = "0.16.0-dev", default-features = false } +bevy_picking = { path = "../bevy_picking", version = "0.16.0-dev", default-features = false } bevy_window = { path = "../bevy_window", version = "0.16.0-dev", default-features = false } bevy_reflect = { path = "../bevy_reflect", version = "0.16.0-dev", features = [ "glam", diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index cbf88740fd..df7690ef26 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -147,6 +147,15 @@ pub struct FocusedInput { window: Entity, } +/// An event which is used to set input focus. Trigger this on an entity, and it will bubble +/// until it finds a focusable entity, and then set focus to it. +#[derive(Clone, Event, EntityEvent)] +#[entity_event(traversal = WindowTraversal, auto_propagate)] +pub struct AcquireFocus { + /// The primary window entity. + window: Entity, +} + #[derive(QueryData)] /// These are for accessing components defined on the targeted entity pub struct WindowTraversal { @@ -172,6 +181,24 @@ impl Traversal> for WindowTraversal { } } +impl Traversal for WindowTraversal { + fn traverse(item: Self::Item<'_, '_>, event: &AcquireFocus) -> Option { + let WindowTraversalItem { child_of, window } = item; + + // Send event to parent, if it has one. + if let Some(child_of) = child_of { + return Some(child_of.parent()); + }; + + // Otherwise, send it to the window entity (unless this is a window entity). + if window.is_none() { + return Some(event.window); + } + + None + } +} + /// Plugin which sets up systems for dispatching bubbling keyboard and gamepad button events to the focused entity. /// /// To add bubbling to your own input events, add the [`dispatch_focused_input::`](dispatch_focused_input) system to your app, diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index 39c6e4ebcf..ef018c56fb 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -38,11 +38,12 @@ use bevy_input::{ keyboard::{KeyCode, KeyboardInput}, ButtonInput, ButtonState, }; -use bevy_window::PrimaryWindow; +use bevy_picking::events::{Pointer, Press}; +use bevy_window::{PrimaryWindow, Window}; use log::warn; use thiserror::Error; -use crate::{FocusedInput, InputFocus, InputFocusVisible}; +use crate::{AcquireFocus, FocusedInput, InputFocus, InputFocusVisible}; #[cfg(feature = "bevy_reflect")] use { @@ -312,6 +313,31 @@ impl TabNavigation<'_, '_> { } } +/// Observer which sets focus to the nearest ancestor that has tab index, using bubbling. +pub(crate) fn acquire_focus( + mut ev: On, + focusable: Query<(), With>, + windows: Query<(), With>, + mut focus: ResMut, +) { + // If the entity has a TabIndex + if focusable.contains(ev.target()) { + // Stop and focus it + ev.propagate(false); + // Don't mutate unless we need to, for change detection + if focus.0 != Some(ev.target()) { + focus.0 = Some(ev.target()); + } + } else if windows.contains(ev.target()) { + // Stop and clear focus + ev.propagate(false); + // Don't mutate unless we need to, for change detection + if focus.0.is_some() { + focus.clear(); + } + } +} + /// Plugin for navigating between focusable entities using keyboard input. pub struct TabNavigationPlugin; @@ -321,6 +347,8 @@ impl Plugin for TabNavigationPlugin { #[cfg(feature = "bevy_reflect")] app.register_type::().register_type::(); + app.add_observer(acquire_focus); + app.add_observer(click_to_focus); } } @@ -330,6 +358,30 @@ fn setup_tab_navigation(mut commands: Commands, window: Query>, + mut focus_visible: ResMut, + windows: Query>, + mut commands: Commands, +) { + // Because `Pointer` is a bubbling event, we don't want to trigger an `AcquireFocus` event + // for every ancestor, but only for the original entity. Also, users may want to stop + // propagation on the pointer event at some point along the bubbling chain, so we need our + // own dedicated event whose propagation we can control. + if ev.target() == ev.original_target() { + // Clicking hides focus + if focus_visible.0 { + focus_visible.0 = false; + } + // Search for a focusable parent entity, defaulting to window if none. + if let Ok(window) = windows.single() { + commands + .entity(ev.target()) + .trigger(AcquireFocus { window }); + } + } +} + /// Observer function which handles tab navigation. /// /// This observer responds to [`KeyCode::Tab`] events and Shift+Tab events, diff --git a/examples/ui/core_widgets.rs b/examples/ui/core_widgets.rs index fbd4dcd718..96959be5bc 100644 --- a/examples/ui/core_widgets.rs +++ b/examples/ui/core_widgets.rs @@ -8,7 +8,7 @@ use bevy::{ }, ecs::system::SystemId, input_focus::{ - tab_navigation::{TabGroup, TabIndex}, + tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin}, InputDispatchPlugin, }, picking::hover::Hovered, @@ -19,7 +19,12 @@ use bevy::{ fn main() { App::new() - .add_plugins((DefaultPlugins, CoreWidgetsPlugin, InputDispatchPlugin)) + .add_plugins(( + DefaultPlugins, + CoreWidgetsPlugin, + InputDispatchPlugin, + TabNavigationPlugin, + )) // Only run the app when there is user input. This will significantly reduce CPU/GPU use. .insert_resource(WinitSettings::desktop_app()) .insert_resource(DemoWidgetStates { slider_value: 50.0 }) diff --git a/examples/ui/core_widgets_observers.rs b/examples/ui/core_widgets_observers.rs index c3451fe700..4e24a646b2 100644 --- a/examples/ui/core_widgets_observers.rs +++ b/examples/ui/core_widgets_observers.rs @@ -8,7 +8,7 @@ use bevy::{ }, ecs::system::SystemId, input_focus::{ - tab_navigation::{TabGroup, TabIndex}, + tab_navigation::{TabGroup, TabIndex, TabNavigationPlugin}, InputDispatchPlugin, }, picking::hover::Hovered, @@ -19,7 +19,12 @@ use bevy::{ fn main() { App::new() - .add_plugins((DefaultPlugins, CoreWidgetsPlugin, InputDispatchPlugin)) + .add_plugins(( + DefaultPlugins, + CoreWidgetsPlugin, + InputDispatchPlugin, + TabNavigationPlugin, + )) // Only run the app when there is user input. This will significantly reduce CPU/GPU use. .insert_resource(WinitSettings::desktop_app()) .insert_resource(DemoWidgetStates { slider_value: 50.0 })