From b9123e74b6838b58c33badff73d176441f8a33cc Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Tue, 17 Dec 2024 17:04:50 -0800 Subject: [PATCH] Generalize bubbling focus input events to other kinds of input (#16876) # Objective The new `bevy_input_focus` crates has a tool to bubble input events up the entity hierarchy, ending with the window, based on the currently focused entity. Right now though, this only works for keyboard events! Both `bevy_ui` buttons and `bevy_egui` should hook into this system (primarily for contextual hotkeys), and we would like to drive `leafwing_input_manager` via these events, to help resolve longstanding pain around "absorbing" / "consuming" inputs based on focus. In order to make that work properly though, we need gamepad support! ## Solution The logic backing this has been changed to be generic for any cloneable event types, and the machinery to make use of this externally has been made `pub`. Within the engine itself, I've added support for gamepad button and scroll events, but nothing else. Mouse button / touch bubbling is handled via bevy_picking, and mouse / gamepad motion doesn't really make sense to bubble. ## Testing The `tab_navigation` example continues to work, and CI is green. ## Future Work I would like to add more complex UI examples to stress test this, but not here please. We should take advantage of the bubbled mouse scrolling when defining scrolled widgets. --- crates/bevy_input_focus/src/lib.rs | 64 ++++++++++++------- crates/bevy_input_focus/src/tab_navigation.rs | 12 +++- 2 files changed, 49 insertions(+), 27 deletions(-) diff --git a/crates/bevy_input_focus/src/lib.rs b/crates/bevy_input_focus/src/lib.rs index 400f36986f..3233fa3e78 100644 --- a/crates/bevy_input_focus/src/lib.rs +++ b/crates/bevy_input_focus/src/lib.rs @@ -20,16 +20,10 @@ pub mod tab_navigation; use bevy_app::{App, Plugin, PreUpdate, Startup}; use bevy_ecs::{ - component::Component, - entity::Entity, - event::{Event, EventReader}, - query::{QueryData, With}, - system::{Commands, Query, Res, ResMut, Resource, Single, SystemParam}, - traversal::Traversal, - world::{Command, DeferredWorld, World}, + prelude::*, query::QueryData, system::SystemParam, traversal::Traversal, world::DeferredWorld, }; use bevy_hierarchy::{HierarchyQueryExt, Parent}; -use bevy_input::keyboard::KeyboardInput; +use bevy_input::{gamepad::GamepadButtonChangedEvent, keyboard::KeyboardInput, mouse::MouseWheel}; use bevy_window::{PrimaryWindow, Window}; use core::fmt::Debug; @@ -111,17 +105,22 @@ impl SetInputFocus for Commands<'_, '_> { } } -/// A bubble-able event for keyboard input. This event is normally dispatched to the current -/// input focus entity, if any. If no entity has input focus, then the event is dispatched to -/// the main window. +/// A bubble-able user input event that starts at the currently focused entity. +/// +/// This event is normally dispatched to the current input focus entity, if any. +/// If no entity has input focus, then the event is dispatched to the main window. +/// +/// To set up your own bubbling input event, add the [`dispatch_focused_input::`](dispatch_focused_input) system to your app, +/// in the [`InputFocusSet::Dispatch`] system set during [`PreUpdate`]. #[derive(Clone, Debug, Component)] -pub struct FocusKeyboardInput { - /// The keyboard input event. - pub input: KeyboardInput, +pub struct FocusedInput { + /// The underlying input event. + pub input: E, + /// The primary window entity. window: Entity, } -impl Event for FocusKeyboardInput { +impl Event for FocusedInput { type Traversal = WindowTraversal; const AUTO_PROPAGATE: bool = true; @@ -134,8 +133,8 @@ pub struct WindowTraversal { window: Option<&'static Window>, } -impl Traversal for WindowTraversal { - fn traverse(item: Self::Item<'_>, event: &FocusKeyboardInput) -> Option { +impl Traversal> for WindowTraversal { + fn traverse(item: Self::Item<'_>, event: &FocusedInput) -> Option { let WindowTraversalItem { parent, window } = item; // Send event to parent, if it has one. @@ -161,10 +160,27 @@ impl Plugin for InputDispatchPlugin { app.add_systems(Startup, set_initial_focus) .insert_resource(InputFocus(None)) .insert_resource(InputFocusVisible(false)) - .add_systems(PreUpdate, dispatch_keyboard_input); + .add_systems( + PreUpdate, + ( + dispatch_focused_input::, + dispatch_focused_input::, + dispatch_focused_input::, + ) + .in_set(InputFocusSet::Dispatch), + ); } } +/// System sets for [`bevy_input_focus`](crate). +/// +/// These systems run in the [`PreUpdate`] schedule. +#[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)] +pub enum InputFocusSet { + /// System which dispatches bubbled input events to the focused entity, or to the primary window. + Dispatch, +} + /// Sets the initial focus to the primary window, if any. pub fn set_initial_focus( mut input_focus: ResMut, @@ -173,10 +189,10 @@ pub fn set_initial_focus( input_focus.0 = Some(*window); } -/// System which dispatches keyboard input events to the focused entity, or to the primary window +/// System which dispatches bubbled input events to the focused entity, or to the primary window /// if no entity has focus. -fn dispatch_keyboard_input( - mut key_events: EventReader, +pub fn dispatch_focused_input( + mut key_events: EventReader, focus: Res, windows: Query>, mut commands: Commands, @@ -186,7 +202,7 @@ fn dispatch_keyboard_input( if let Some(focus_elt) = focus.0 { for ev in key_events.read() { commands.trigger_targets( - FocusKeyboardInput { + FocusedInput { input: ev.clone(), window, }, @@ -198,7 +214,7 @@ fn dispatch_keyboard_input( // There should be only one primary window. for ev in key_events.read() { commands.trigger_targets( - FocusKeyboardInput { + FocusedInput { input: ev.clone(), window, }, @@ -326,7 +342,7 @@ mod tests { struct GatherKeyboardEvents(String); fn gather_keyboard_events( - trigger: Trigger, + trigger: Trigger>, mut query: Query<&mut GatherKeyboardEvents>, ) { if let Ok(mut gather) = query.get_mut(trigger.target()) { diff --git a/crates/bevy_input_focus/src/tab_navigation.rs b/crates/bevy_input_focus/src/tab_navigation.rs index 1bc7d300e7..00706fdba7 100644 --- a/crates/bevy_input_focus/src/tab_navigation.rs +++ b/crates/bevy_input_focus/src/tab_navigation.rs @@ -36,11 +36,14 @@ use bevy_ecs::{ world::DeferredWorld, }; use bevy_hierarchy::{Children, HierarchyQueryExt, Parent}; -use bevy_input::{keyboard::KeyCode, ButtonInput, ButtonState}; +use bevy_input::{ + keyboard::{KeyCode, KeyboardInput}, + ButtonInput, ButtonState, +}; use bevy_utils::tracing::warn; use bevy_window::PrimaryWindow; -use crate::{FocusKeyboardInput, InputFocus, InputFocusVisible, SetInputFocus}; +use crate::{FocusedInput, InputFocus, InputFocusVisible, SetInputFocus}; /// A component which indicates that an entity wants to participate in tab navigation. /// @@ -263,8 +266,11 @@ fn setup_tab_navigation(mut commands: Commands, window: Query, + mut trigger: Trigger>, nav: TabNavigation, mut focus: ResMut, mut visible: ResMut,