Skip to content

Commit

Permalink
Polish and improve docs for bevy_input_focus (#16887)
Browse files Browse the repository at this point in the history
# 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.
  • Loading branch information
alice-i-cecile authored Dec 18, 2024
1 parent 20049d4 commit d8796ae
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 60 deletions.
20 changes: 20 additions & 0 deletions crates/bevy_input_focus/src/autofocus.rs
Original file line number Diff line number Diff line change
@@ -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);
}
82 changes: 62 additions & 20 deletions crates/bevy_input_focus/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)]
Expand All @@ -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.
///
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(
Expand All @@ -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>,
Expand Down
68 changes: 28 additions & 40 deletions crates/bevy_input_focus/src/tab_navigation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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.
///
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.
Expand All @@ -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");
Expand All @@ -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| {
Expand All @@ -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)
Expand All @@ -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.
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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;
Expand All @@ -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));
}
}

0 comments on commit d8796ae

Please sign in to comment.