Polish and improve docs for bevy_input_focus
(#16887)
# Objective `bevy_input_focus` needs some love before we ship it to users. There's a few missing helper methods, the docs could be improved, and `AutoFocus` should be more generally available. ## Solution The changes here are broken down by commit, and should generally be uncontroversial. The ones to focus on during review are: - Make navigate take a & InputFocus argument: this makes the intended pattern clearer to users - Remove TabGroup requirement from `AutoFocus`: I want auto-focusing even with gamepad-style focus navigation! - Handle case where tab group is None more gracefully: I think we can try harder to provide something usable, and shouldn't just fail to navigate ## Testing The `tab_navigation` example continues to work.
This commit is contained in:
parent
20049d4c34
commit
d8796ae8b6
20
crates/bevy_input_focus/src/autofocus.rs
Normal file
20
crates/bevy_input_focus/src/autofocus.rs
Normal file
@ -0,0 +1,20 @@
|
||||
//! Contains the [`AutoFocus`] component and related machinery.
|
||||
|
||||
use bevy_ecs::{component::ComponentId, prelude::*, world::DeferredWorld};
|
||||
|
||||
use crate::SetInputFocus;
|
||||
|
||||
/// Indicates that this widget should automatically receive [`InputFocus`](crate::InputFocus).
|
||||
///
|
||||
/// This can be useful for things like dialog boxes, the first text input in a form,
|
||||
/// or the first button in a game menu.
|
||||
///
|
||||
/// The focus is swapped when this component is added
|
||||
/// or an entity with this component is spawned.
|
||||
#[derive(Debug, Default, Component, Copy, Clone)]
|
||||
#[component(on_add = on_auto_focus_added)]
|
||||
pub struct AutoFocus;
|
||||
|
||||
fn on_auto_focus_added(mut world: DeferredWorld, entity: Entity, _: ComponentId) {
|
||||
world.set_input_focus(entity);
|
||||
}
|
@ -8,16 +8,20 @@
|
||||
//! Keyboard focus system for Bevy.
|
||||
//!
|
||||
//! This crate provides a system for managing input focus in Bevy applications, including:
|
||||
//! * A resource for tracking which entity has input focus.
|
||||
//! * Methods for getting and setting input focus.
|
||||
//! * Event definitions for triggering bubble-able keyboard input events to the focused entity.
|
||||
//! * A system for dispatching keyboard input events to the focused entity.
|
||||
//! * [`InputFocus`], a resource for tracking which entity has input focus.
|
||||
//! * Methods for getting and setting input focus via [`SetInputFocus`], [`InputFocus`] and [`IsFocusedHelper`].
|
||||
//! * A generic [`FocusedInput`] event for input events which bubble up from the focused entity.
|
||||
//!
|
||||
//! This crate does *not* provide any integration with UI widgets, or provide functions for
|
||||
//! tab navigation or gamepad-based focus navigation, as those are typically application-specific.
|
||||
//! This crate does *not* provide any integration with UI widgets: this is the responsibility of the widget crate,
|
||||
//! which should depend on [`bevy_input_focus`](crate).
|
||||
|
||||
pub mod tab_navigation;
|
||||
|
||||
// This module is too small / specific to be exported by the crate,
|
||||
// but it's nice to have it separate for code organization.
|
||||
mod autofocus;
|
||||
pub use autofocus::*;
|
||||
|
||||
use bevy_app::{App, Plugin, PreUpdate, Startup};
|
||||
use bevy_ecs::{
|
||||
prelude::*, query::QueryData, system::SystemParam, traversal::Traversal, world::DeferredWorld,
|
||||
@ -29,11 +33,36 @@ use core::fmt::Debug;
|
||||
|
||||
/// Resource representing which entity has input focus, if any. Keyboard events will be
|
||||
/// dispatched to the current focus entity, or to the primary window if no entity has focus.
|
||||
#[derive(Clone, Debug, Resource)]
|
||||
#[derive(Clone, Debug, Default, Resource)]
|
||||
pub struct InputFocus(pub Option<Entity>);
|
||||
|
||||
/// Resource representing whether the input focus indicator should be visible. It's up to the
|
||||
/// current focus navigation system to set this resource. For a desktop/web style of user interface
|
||||
impl InputFocus {
|
||||
/// Create a new [`InputFocus`] resource with the given entity.
|
||||
///
|
||||
/// This is mostly useful for tests.
|
||||
pub const fn from_entity(entity: Entity) -> Self {
|
||||
Self(Some(entity))
|
||||
}
|
||||
|
||||
/// Set the entity with input focus.
|
||||
pub const fn set(&mut self, entity: Entity) {
|
||||
self.0 = Some(entity);
|
||||
}
|
||||
|
||||
/// Returns the entity with input focus, if any.
|
||||
pub const fn get(&self) -> Option<Entity> {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Clears input focus.
|
||||
pub const fn clear(&mut self) {
|
||||
self.0 = None;
|
||||
}
|
||||
}
|
||||
|
||||
/// Resource representing whether the input focus indicator should be visible on UI elements.
|
||||
///
|
||||
/// It's up to the current focus navigation system to set this resource. For a desktop/web style of user interface
|
||||
/// this would be set to true when the user presses the tab key, and set to false when the user
|
||||
/// clicks on a different element.
|
||||
#[derive(Clone, Debug, Resource)]
|
||||
@ -43,6 +72,8 @@ pub struct InputFocusVisible(pub bool);
|
||||
///
|
||||
/// These methods are equivalent to modifying the [`InputFocus`] resource directly,
|
||||
/// but only take effect when commands are applied.
|
||||
///
|
||||
/// See [`IsFocused`] for methods to check if an entity has focus.
|
||||
pub trait SetInputFocus {
|
||||
/// Set input focus to the given entity.
|
||||
///
|
||||
@ -151,8 +182,10 @@ impl<E: Event + Clone> Traversal<FocusedInput<E>> for WindowTraversal {
|
||||
}
|
||||
}
|
||||
|
||||
/// Plugin which registers the system for dispatching keyboard events based on focus and
|
||||
/// hover state.
|
||||
/// 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::<MyEvent>`](dispatch_focused_input) system to your app,
|
||||
/// as described in the docs for [`FocusedInput`].
|
||||
pub struct InputDispatchPlugin;
|
||||
|
||||
impl Plugin for InputDispatchPlugin {
|
||||
@ -198,19 +231,19 @@ pub fn dispatch_focused_input<E: Event + Clone>(
|
||||
mut commands: Commands,
|
||||
) {
|
||||
if let Ok(window) = windows.get_single() {
|
||||
// If an element has keyboard focus, then dispatch the key event to that element.
|
||||
if let Some(focus_elt) = focus.0 {
|
||||
// If an element has keyboard focus, then dispatch the input event to that element.
|
||||
if let Some(focused_entity) = focus.0 {
|
||||
for ev in key_events.read() {
|
||||
commands.trigger_targets(
|
||||
FocusedInput {
|
||||
input: ev.clone(),
|
||||
window,
|
||||
},
|
||||
focus_elt,
|
||||
focused_entity,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// If no element has input focus, then dispatch the key event to the primary window.
|
||||
// If no element has input focus, then dispatch the input event to the primary window.
|
||||
// There should be only one primary window.
|
||||
for ev in key_events.read() {
|
||||
commands.trigger_targets(
|
||||
@ -225,27 +258,36 @@ pub fn dispatch_focused_input<E: Event + Clone>(
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait which defines methods to check if an entity currently has focus. This is implemented
|
||||
/// for [`World`] and [`IsFocusedHelper`].
|
||||
/// Trait which defines methods to check if an entity currently has focus.
|
||||
///
|
||||
/// This is implemented for [`World`] and [`IsFocusedHelper`].
|
||||
/// [`DeferredWorld`] indirectly implements it through [`Deref`].
|
||||
///
|
||||
/// For use within systems, use [`IsFocusedHelper`].
|
||||
///
|
||||
/// See [`SetInputFocus`] for methods to set and clear input focus.
|
||||
///
|
||||
/// [`Deref`]: std::ops::Deref
|
||||
pub trait IsFocused {
|
||||
/// Returns true if the given entity has input focus.
|
||||
fn is_focused(&self, entity: Entity) -> bool;
|
||||
|
||||
/// Returns true if the given entity or any of its descendants has input focus.
|
||||
///
|
||||
/// Note that for unusual layouts, the focus may not be within the entity's visual bounds.
|
||||
fn is_focus_within(&self, entity: Entity) -> bool;
|
||||
|
||||
/// Returns true if the given entity has input focus and the focus indicator is visible.
|
||||
/// Returns true if the given entity has input focus and the focus indicator should be visible.
|
||||
fn is_focus_visible(&self, entity: Entity) -> bool;
|
||||
|
||||
/// Returns true if the given entity, or any descendant, has input focus and the focus
|
||||
/// indicator is visible.
|
||||
/// indicator should be visible.
|
||||
fn is_focus_within_visible(&self, entity: Entity) -> bool;
|
||||
}
|
||||
|
||||
/// System param that helps get information about the current focused entity.
|
||||
/// A system param that helps get information about the current focused entity.
|
||||
///
|
||||
/// When working with the entire [`World`], consider using the [`IsFocused`] instead.
|
||||
#[derive(SystemParam)]
|
||||
pub struct IsFocusedHelper<'w, 's> {
|
||||
parent_query: Query<'w, 's, &'static Parent>,
|
||||
|
@ -9,31 +9,27 @@
|
||||
//! * An index < 0 means that the entity is not focusable via sequential navigation, but
|
||||
//! can still be focused via direct selection.
|
||||
//!
|
||||
//! Tabbable entities must be descendants of a `TabGroup` entity, which is a component that
|
||||
//! Tabbable entities must be descendants of a [`TabGroup`] entity, which is a component that
|
||||
//! marks a tree of entities as containing tabbable elements. The order of tab groups
|
||||
//! is determined by the `order` field, with lower orders being tabbed first. Modal tab groups
|
||||
//! is determined by the [`TabGroup::order`] field, with lower orders being tabbed first. Modal tab groups
|
||||
//! are used for ui elements that should only tab within themselves, such as modal dialog boxes.
|
||||
//!
|
||||
//! There are several different ways to use this module. To enable automatic tabbing, add the
|
||||
//! `TabNavigationPlugin` to your app. (Make sure you also have `InputDispatchPlugin` installed).
|
||||
//! To enable automatic tabbing, add the
|
||||
//! [`TabNavigationPlugin`] and [`InputDispatchPlugin`](crate::InputDispatchPlugin) to your app.
|
||||
//! This will install a keyboard event observer on the primary window which automatically handles
|
||||
//! tab navigation for you.
|
||||
//!
|
||||
//! Alternatively, if you want to have more control over tab navigation, or are using an event
|
||||
//! mapping framework such as LWIM, you can use the `TabNavigation` helper object directly instead.
|
||||
//! This object can be injected into your systems, and provides a `navigate` method which can be
|
||||
//! Alternatively, if you want to have more control over tab navigation, or are using an input-action-mapping framework,
|
||||
//! you can use the [`TabNavigation`] system parameter directly instead.
|
||||
//! This object can be injected into your systems, and provides a [`navigate`](`TabNavigation::navigate`) method which can be
|
||||
//! used to navigate between focusable entities.
|
||||
//!
|
||||
//! This module also provides `AutoFocus`, a component which can be added to an entity to
|
||||
//! automatically focus it when it is added to the world.
|
||||
use bevy_app::{App, Plugin, Startup};
|
||||
use bevy_ecs::{
|
||||
component::{Component, ComponentId},
|
||||
component::Component,
|
||||
entity::Entity,
|
||||
observer::Trigger,
|
||||
query::{With, Without},
|
||||
system::{Commands, Query, Res, ResMut, SystemParam},
|
||||
world::DeferredWorld,
|
||||
};
|
||||
use bevy_hierarchy::{Children, HierarchyQueryExt, Parent};
|
||||
use bevy_input::{
|
||||
@ -43,7 +39,7 @@ use bevy_input::{
|
||||
use bevy_utils::tracing::warn;
|
||||
use bevy_window::PrimaryWindow;
|
||||
|
||||
use crate::{FocusedInput, InputFocus, InputFocusVisible, SetInputFocus};
|
||||
use crate::{FocusedInput, InputFocus, InputFocusVisible};
|
||||
|
||||
/// A component which indicates that an entity wants to participate in tab navigation.
|
||||
///
|
||||
@ -52,10 +48,6 @@ use crate::{FocusedInput, InputFocus, InputFocusVisible, SetInputFocus};
|
||||
#[derive(Debug, Default, Component, Copy, Clone)]
|
||||
pub struct TabIndex(pub i32);
|
||||
|
||||
/// Indicates that this widget should automatically receive focus when it's added.
|
||||
#[derive(Debug, Default, Component, Copy, Clone)]
|
||||
pub struct AutoFocus;
|
||||
|
||||
/// A component used to mark a tree of entities as containing tabbable elements.
|
||||
#[derive(Debug, Default, Component, Copy, Clone)]
|
||||
pub struct TabGroup {
|
||||
@ -87,7 +79,9 @@ impl TabGroup {
|
||||
}
|
||||
}
|
||||
|
||||
/// Navigation action for tabbing.
|
||||
/// A navigation action for tabbing.
|
||||
///
|
||||
/// These values are consumed by the [`TabNavigation`] system param.
|
||||
pub enum NavAction {
|
||||
/// Navigate to the next focusable entity, wrapping around to the beginning if at the end.
|
||||
Next,
|
||||
@ -120,6 +114,8 @@ pub struct TabNavigation<'w, 's> {
|
||||
impl TabNavigation<'_, '_> {
|
||||
/// Navigate to the next focusable entity.
|
||||
///
|
||||
/// Focusable entities are determined by the presence of the [`TabIndex`] component.
|
||||
///
|
||||
/// Arguments:
|
||||
/// * `focus`: The current focus entity, or `None` if no entity has focus.
|
||||
/// * `action`: Whether to select the next, previous, first, or last focusable entity.
|
||||
@ -128,7 +124,7 @@ impl TabNavigation<'_, '_> {
|
||||
/// or last focusable entity, depending on the direction of navigation. For example, if
|
||||
/// `action` is `Next` and no focusable entities are found, then this function will return
|
||||
/// the first focusable entity.
|
||||
pub fn navigate(&self, focus: Option<Entity>, action: NavAction) -> Option<Entity> {
|
||||
pub fn navigate(&self, focus: &InputFocus, action: NavAction) -> Option<Entity> {
|
||||
// If there are no tab groups, then there are no focusable entities.
|
||||
if self.tabgroup_query.is_empty() {
|
||||
warn!("No tab groups found");
|
||||
@ -137,7 +133,7 @@ impl TabNavigation<'_, '_> {
|
||||
|
||||
// Start by identifying which tab group we are in. Mainly what we want to know is if
|
||||
// we're in a modal group.
|
||||
let tabgroup = focus.and_then(|focus_ent| {
|
||||
let tabgroup = focus.0.and_then(|focus_ent| {
|
||||
self.parent_query
|
||||
.iter_ancestors(focus_ent)
|
||||
.find_map(|entity| {
|
||||
@ -148,9 +144,8 @@ impl TabNavigation<'_, '_> {
|
||||
})
|
||||
});
|
||||
|
||||
if focus.is_some() && tabgroup.is_none() {
|
||||
warn!("No tab group found for focus entity");
|
||||
return None;
|
||||
if focus.0.is_some() && tabgroup.is_none() {
|
||||
warn!("No tab group found for focus entity. Users will not be able to navigate back to this entity.");
|
||||
}
|
||||
|
||||
self.navigate_in_group(tabgroup, focus, action)
|
||||
@ -159,7 +154,7 @@ impl TabNavigation<'_, '_> {
|
||||
fn navigate_in_group(
|
||||
&self,
|
||||
tabgroup: Option<(Entity, &TabGroup)>,
|
||||
focus: Option<Entity>,
|
||||
focus: &InputFocus,
|
||||
action: NavAction,
|
||||
) -> Option<Entity> {
|
||||
// List of all focusable entities found.
|
||||
@ -201,7 +196,7 @@ impl TabNavigation<'_, '_> {
|
||||
// Stable sort by tabindex
|
||||
focusable.sort_by(compare_tab_indices);
|
||||
|
||||
let index = focusable.iter().position(|e| Some(e.0) == focus);
|
||||
let index = focusable.iter().position(|e| Some(e.0) == focus.0);
|
||||
let count = focusable.len();
|
||||
let next = match (index, action) {
|
||||
(Some(idx), NavAction::Next) => (idx + 1).rem_euclid(count),
|
||||
@ -247,15 +242,12 @@ fn compare_tab_indices(a: &(Entity, TabIndex), b: &(Entity, TabIndex)) -> core::
|
||||
a.1 .0.cmp(&b.1 .0)
|
||||
}
|
||||
|
||||
/// Plugin for handling keyboard input.
|
||||
/// Plugin for navigating between focusable entities using keyboard input.
|
||||
pub struct TabNavigationPlugin;
|
||||
|
||||
impl Plugin for TabNavigationPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(Startup, setup_tab_navigation);
|
||||
app.world_mut()
|
||||
.register_component_hooks::<AutoFocus>()
|
||||
.on_add(on_auto_focus_added);
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,7 +275,7 @@ pub fn handle_tab_navigation(
|
||||
&& !key_event.repeat
|
||||
{
|
||||
let next = nav.navigate(
|
||||
focus.0,
|
||||
&focus,
|
||||
if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
|
||||
NavAction::Previous
|
||||
} else {
|
||||
@ -298,12 +290,6 @@ pub fn handle_tab_navigation(
|
||||
}
|
||||
}
|
||||
|
||||
fn on_auto_focus_added(mut world: DeferredWorld, entity: Entity, _: ComponentId) {
|
||||
if world.entity(entity).contains::<TabIndex>() {
|
||||
world.set_input_focus(entity);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bevy_ecs::system::SystemState;
|
||||
@ -326,16 +312,18 @@ mod tests {
|
||||
assert_eq!(tab_navigation.tabgroup_query.iter().count(), 1);
|
||||
assert_eq!(tab_navigation.tabindex_query.iter().count(), 2);
|
||||
|
||||
let next_entity = tab_navigation.navigate(Some(tab_entity_1), NavAction::Next);
|
||||
let next_entity =
|
||||
tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next);
|
||||
assert_eq!(next_entity, Some(tab_entity_2));
|
||||
|
||||
let prev_entity = tab_navigation.navigate(Some(tab_entity_2), NavAction::Previous);
|
||||
let prev_entity =
|
||||
tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous);
|
||||
assert_eq!(prev_entity, Some(tab_entity_1));
|
||||
|
||||
let first_entity = tab_navigation.navigate(None, NavAction::First);
|
||||
let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First);
|
||||
assert_eq!(first_entity, Some(tab_entity_1));
|
||||
|
||||
let last_entity = tab_navigation.navigate(None, NavAction::Last);
|
||||
let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last);
|
||||
assert_eq!(last_entity, Some(tab_entity_2));
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user