Vendor dependencies for 0.3.0 release

This commit is contained in:
2025-09-27 10:29:08 -05:00
parent 0c8d39d483
commit 82ab7f317b
26803 changed files with 16134934 additions and 0 deletions

View File

@@ -0,0 +1,30 @@
//! Contains the [`AutoFocus`] component and related machinery.
use bevy_ecs::{component::HookContext, prelude::*, world::DeferredWorld};
use crate::InputFocus;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::{prelude::*, Reflect};
/// Indicates that this widget should automatically receive [`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)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, Default, Component, Clone)
)]
#[component(on_add = on_auto_focus_added)]
pub struct AutoFocus;
fn on_auto_focus_added(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
if let Some(mut input_focus) = world.get_resource_mut::<InputFocus>() {
input_focus.set(entity);
}
}

View File

@@ -0,0 +1,433 @@
//! A navigation framework for moving between focusable elements based on directional input.
//!
//! While virtual cursors are a common way to navigate UIs with a gamepad (or arrow keys!),
//! they are generally both slow and frustrating to use.
//! Instead, directional inputs should provide a direct way to snap between focusable elements.
//!
//! Like the rest of this crate, the [`InputFocus`] resource is manipulated to track
//! the current focus.
//!
//! Navigating between focusable entities (commonly UI nodes) is done by
//! passing a [`CompassOctant`] into the [`navigate`](DirectionalNavigation::navigate) method
//! from the [`DirectionalNavigation`] system parameter.
//!
//! Under the hood, the [`DirectionalNavigationMap`] stores a directed graph of focusable entities.
//! Each entity can have up to 8 neighbors, one for each [`CompassOctant`], balancing flexibility and required precision.
//! For now, this graph must be built manually, but in the future, it could be generated automatically.
use bevy_app::prelude::*;
use bevy_ecs::{
entity::{EntityHashMap, EntityHashSet},
prelude::*,
system::SystemParam,
};
use bevy_math::CompassOctant;
use thiserror::Error;
use crate::InputFocus;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::{prelude::*, Reflect};
/// A plugin that sets up the directional navigation systems and resources.
#[derive(Default)]
pub struct DirectionalNavigationPlugin;
impl Plugin for DirectionalNavigationPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<DirectionalNavigationMap>();
#[cfg(feature = "bevy_reflect")]
app.register_type::<NavNeighbors>()
.register_type::<DirectionalNavigationMap>();
}
}
/// The up-to-eight neighbors of a focusable entity, one for each [`CompassOctant`].
#[derive(Default, Debug, Clone, PartialEq)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Default, Debug, PartialEq, Clone)
)]
pub struct NavNeighbors {
/// The array of neighbors, one for each [`CompassOctant`].
/// The mapping between array elements and directions is determined by [`CompassOctant::to_index`].
///
/// If no neighbor exists in a given direction, the value will be [`None`].
/// In most cases, using [`NavNeighbors::set`] and [`NavNeighbors::get`]
/// will be more ergonomic than directly accessing this array.
pub neighbors: [Option<Entity>; 8],
}
impl NavNeighbors {
/// An empty set of neighbors.
pub const EMPTY: NavNeighbors = NavNeighbors {
neighbors: [None; 8],
};
/// Get the neighbor for a given [`CompassOctant`].
pub const fn get(&self, octant: CompassOctant) -> Option<Entity> {
self.neighbors[octant.to_index()]
}
/// Set the neighbor for a given [`CompassOctant`].
pub const fn set(&mut self, octant: CompassOctant, entity: Entity) {
self.neighbors[octant.to_index()] = Some(entity);
}
}
/// A resource that stores the traversable graph of focusable entities.
///
/// Each entity can have up to 8 neighbors, one for each [`CompassOctant`].
///
/// To ensure that your graph is intuitive to navigate and generally works correctly, it should be:
///
/// - **Connected**: Every focusable entity should be reachable from every other focusable entity.
/// - **Symmetric**: If entity A is a neighbor of entity B, then entity B should be a neighbor of entity A, ideally in the reverse direction.
/// - **Physical**: The direction of navigation should match the layout of the entities when possible,
/// although looping around the edges of the screen is also acceptable.
/// - **Not self-connected**: An entity should not be a neighbor of itself; use [`None`] instead.
///
/// For now, this graph must be built manually, and the developer is responsible for ensuring that it meets the above criteria.
#[derive(Resource, Debug, Default, Clone, PartialEq)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Resource, Debug, Default, PartialEq, Clone)
)]
pub struct DirectionalNavigationMap {
/// A directed graph of focusable entities.
///
/// Pass in the current focus as a key, and get back a collection of up to 8 neighbors,
/// each keyed by a [`CompassOctant`].
pub neighbors: EntityHashMap<NavNeighbors>,
}
impl DirectionalNavigationMap {
/// Adds a new entity to the navigation map, overwriting any existing neighbors for that entity.
///
/// Removes an entity from the navigation map, including all connections to and from it.
///
/// Note that this is an O(n) operation, where n is the number of entities in the map,
/// as we must iterate over each entity to check for connections to the removed entity.
///
/// If you are removing multiple entities, consider using [`remove_multiple`](Self::remove_multiple) instead.
pub fn remove(&mut self, entity: Entity) {
self.neighbors.remove(&entity);
for node in self.neighbors.values_mut() {
for neighbor in node.neighbors.iter_mut() {
if *neighbor == Some(entity) {
*neighbor = None;
}
}
}
}
/// Removes a collection of entities from the navigation map.
///
/// While this is still an O(n) operation, where n is the number of entities in the map,
/// it is more efficient than calling [`remove`](Self::remove) multiple times,
/// as we can check for connections to all removed entities in a single pass.
///
/// An [`EntityHashSet`] must be provided as it is noticeably faster than the standard hasher or a [`Vec`](`alloc::vec::Vec`).
pub fn remove_multiple(&mut self, entities: EntityHashSet) {
for entity in &entities {
self.neighbors.remove(entity);
}
for node in self.neighbors.values_mut() {
for neighbor in node.neighbors.iter_mut() {
if let Some(entity) = *neighbor {
if entities.contains(&entity) {
*neighbor = None;
}
}
}
}
}
/// Completely clears the navigation map, removing all entities and connections.
pub fn clear(&mut self) {
self.neighbors.clear();
}
/// Adds an edge between two entities in the navigation map.
/// Any existing edge from A in the provided direction will be overwritten.
///
/// The reverse edge will not be added, so navigation will only be possible in one direction.
/// If you want to add a symmetrical edge, use [`add_symmetrical_edge`](Self::add_symmetrical_edge) instead.
pub fn add_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {
self.neighbors
.entry(a)
.or_insert(NavNeighbors::EMPTY)
.set(direction, b);
}
/// Adds a symmetrical edge between two entities in the navigation map.
/// The A -> B path will use the provided direction, while B -> A will use the [`CompassOctant::opposite`] variant.
///
/// Any existing connections between the two entities will be overwritten.
pub fn add_symmetrical_edge(&mut self, a: Entity, b: Entity, direction: CompassOctant) {
self.add_edge(a, b, direction);
self.add_edge(b, a, direction.opposite());
}
/// Add symmetrical edges between each consecutive pair of entities in the provided slice.
///
/// Unlike [`add_looping_edges`](Self::add_looping_edges), this method does not loop back to the first entity.
pub fn add_edges(&mut self, entities: &[Entity], direction: CompassOctant) {
for pair in entities.windows(2) {
self.add_symmetrical_edge(pair[0], pair[1], direction);
}
}
/// Add symmetrical edges between each consecutive pair of entities in the provided slice, looping back to the first entity at the end.
///
/// This is useful for creating a circular navigation path between a set of entities, such as a menu.
pub fn add_looping_edges(&mut self, entities: &[Entity], direction: CompassOctant) {
self.add_edges(entities, direction);
if let Some((first_entity, rest)) = entities.split_first() {
if let Some(last_entity) = rest.last() {
self.add_symmetrical_edge(*last_entity, *first_entity, direction);
}
}
}
/// Gets the entity in a given direction from the current focus, if any.
pub fn get_neighbor(&self, focus: Entity, octant: CompassOctant) -> Option<Entity> {
self.neighbors
.get(&focus)
.and_then(|neighbors| neighbors.get(octant))
}
/// Looks up the neighbors of a given entity.
///
/// If the entity is not in the map, [`None`] will be returned.
/// Note that the set of neighbors is not guaranteed to be non-empty though!
pub fn get_neighbors(&self, entity: Entity) -> Option<&NavNeighbors> {
self.neighbors.get(&entity)
}
}
/// A system parameter for navigating between focusable entities in a directional way.
#[derive(SystemParam, Debug)]
pub struct DirectionalNavigation<'w> {
/// The currently focused entity.
pub focus: ResMut<'w, InputFocus>,
/// The navigation map containing the connections between entities.
pub map: Res<'w, DirectionalNavigationMap>,
}
impl DirectionalNavigation<'_> {
/// Navigates to the neighbor in a given direction from the current focus, if any.
///
/// Returns the new focus if successful.
/// Returns an error if there is no focus set or if there is no neighbor in the requested direction.
///
/// If the result was `Ok`, the [`InputFocus`] resource is updated to the new focus as part of this method call.
pub fn navigate(
&mut self,
direction: CompassOctant,
) -> Result<Entity, DirectionalNavigationError> {
if let Some(current_focus) = self.focus.0 {
if let Some(new_focus) = self.map.get_neighbor(current_focus, direction) {
self.focus.set(new_focus);
Ok(new_focus)
} else {
Err(DirectionalNavigationError::NoNeighborInDirection {
current_focus,
direction,
})
}
} else {
Err(DirectionalNavigationError::NoFocus)
}
}
}
/// An error that can occur when navigating between focusable entities using [directional navigation](crate::directional_navigation).
#[derive(Debug, PartialEq, Clone, Error)]
pub enum DirectionalNavigationError {
/// No focusable entity is currently set.
#[error("No focusable entity is currently set.")]
NoFocus,
/// No neighbor in the requested direction.
#[error("No neighbor from {current_focus} in the {direction:?} direction.")]
NoNeighborInDirection {
/// The entity that was the focus when the error occurred.
current_focus: Entity,
/// The direction in which the navigation was attempted.
direction: CompassOctant,
},
}
#[cfg(test)]
mod tests {
use bevy_ecs::system::RunSystemOnce;
use super::*;
#[test]
fn setting_and_getting_nav_neighbors() {
let mut neighbors = NavNeighbors::EMPTY;
assert_eq!(neighbors.get(CompassOctant::SouthEast), None);
neighbors.set(CompassOctant::SouthEast, Entity::PLACEHOLDER);
for i in 0..8 {
if i == CompassOctant::SouthEast.to_index() {
assert_eq!(
neighbors.get(CompassOctant::SouthEast),
Some(Entity::PLACEHOLDER)
);
} else {
assert_eq!(neighbors.get(CompassOctant::from_index(i).unwrap()), None);
}
}
}
#[test]
fn simple_set_and_get_navmap() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
let mut map = DirectionalNavigationMap::default();
map.add_edge(a, b, CompassOctant::SouthEast);
assert_eq!(map.get_neighbor(a, CompassOctant::SouthEast), Some(b));
assert_eq!(
map.get_neighbor(b, CompassOctant::SouthEast.opposite()),
None
);
}
#[test]
fn symmetrical_edges() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
let mut map = DirectionalNavigationMap::default();
map.add_symmetrical_edge(a, b, CompassOctant::North);
assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b));
assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a));
}
#[test]
fn remove_nodes() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
let mut map = DirectionalNavigationMap::default();
map.add_edge(a, b, CompassOctant::North);
map.add_edge(b, a, CompassOctant::South);
assert_eq!(map.get_neighbor(a, CompassOctant::North), Some(b));
assert_eq!(map.get_neighbor(b, CompassOctant::South), Some(a));
map.remove(b);
assert_eq!(map.get_neighbor(a, CompassOctant::North), None);
assert_eq!(map.get_neighbor(b, CompassOctant::South), None);
}
#[test]
fn remove_multiple_nodes() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
let c = world.spawn_empty().id();
let mut map = DirectionalNavigationMap::default();
map.add_edge(a, b, CompassOctant::North);
map.add_edge(b, a, CompassOctant::South);
map.add_edge(b, c, CompassOctant::East);
map.add_edge(c, b, CompassOctant::West);
let mut to_remove = EntityHashSet::default();
to_remove.insert(b);
to_remove.insert(c);
map.remove_multiple(to_remove);
assert_eq!(map.get_neighbor(a, CompassOctant::North), None);
assert_eq!(map.get_neighbor(b, CompassOctant::South), None);
assert_eq!(map.get_neighbor(b, CompassOctant::East), None);
assert_eq!(map.get_neighbor(c, CompassOctant::West), None);
}
#[test]
fn edges() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
let c = world.spawn_empty().id();
let mut map = DirectionalNavigationMap::default();
map.add_edges(&[a, b, c], CompassOctant::East);
assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b));
assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c));
assert_eq!(map.get_neighbor(c, CompassOctant::East), None);
assert_eq!(map.get_neighbor(a, CompassOctant::West), None);
assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a));
assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b));
}
#[test]
fn looping_edges() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
let c = world.spawn_empty().id();
let mut map = DirectionalNavigationMap::default();
map.add_looping_edges(&[a, b, c], CompassOctant::East);
assert_eq!(map.get_neighbor(a, CompassOctant::East), Some(b));
assert_eq!(map.get_neighbor(b, CompassOctant::East), Some(c));
assert_eq!(map.get_neighbor(c, CompassOctant::East), Some(a));
assert_eq!(map.get_neighbor(a, CompassOctant::West), Some(c));
assert_eq!(map.get_neighbor(b, CompassOctant::West), Some(a));
assert_eq!(map.get_neighbor(c, CompassOctant::West), Some(b));
}
#[test]
fn nav_with_system_param() {
let mut world = World::new();
let a = world.spawn_empty().id();
let b = world.spawn_empty().id();
let c = world.spawn_empty().id();
let mut map = DirectionalNavigationMap::default();
map.add_looping_edges(&[a, b, c], CompassOctant::East);
world.insert_resource(map);
let mut focus = InputFocus::default();
focus.set(a);
world.insert_resource(focus);
assert_eq!(world.resource::<InputFocus>().get(), Some(a));
fn navigate_east(mut nav: DirectionalNavigation) {
nav.navigate(CompassOctant::East).unwrap();
}
world.run_system_once(navigate_east).unwrap();
assert_eq!(world.resource::<InputFocus>().get(), Some(b));
world.run_system_once(navigate_east).unwrap();
assert_eq!(world.resource::<InputFocus>().get(), Some(c));
world.run_system_once(navigate_east).unwrap();
assert_eq!(world.resource::<InputFocus>().get(), Some(a));
}
}

566
vendor/bevy_input_focus/src/lib.rs vendored Normal file
View File

@@ -0,0 +1,566 @@
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png"
)]
#![no_std]
//! A UI-centric focus system for Bevy.
//!
//! This crate provides a system for managing input focus in Bevy applications, including:
//! * [`InputFocus`], a resource for tracking which entity has input focus.
//! * Methods for getting and setting input focus via [`InputFocus`] and [`IsFocusedHelper`].
//! * A generic [`FocusedInput`] event for input events which bubble up from the focused entity.
//! * Various navigation frameworks for moving input focus between entities based on user input, such as [`tab_navigation`] and [`directional_navigation`].
//!
//! 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).
#[cfg(feature = "std")]
extern crate std;
extern crate alloc;
pub mod directional_navigation;
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};
use bevy_input::{gamepad::GamepadButtonChangedEvent, keyboard::KeyboardInput, mouse::MouseWheel};
use bevy_window::{PrimaryWindow, Window};
use core::fmt::Debug;
#[cfg(feature = "bevy_reflect")]
use bevy_reflect::{prelude::*, Reflect};
/// Resource representing which entity has input focus, if any. Input events (other than pointer-like inputs) will be
/// dispatched to the current focus entity, or to the primary window if no entity has focus.
///
/// Changing the input focus is as easy as modifying this resource.
///
/// # Examples
///
/// From within a system:
///
/// ```rust
/// use bevy_ecs::prelude::*;
/// use bevy_input_focus::InputFocus;
///
/// fn clear_focus(mut input_focus: ResMut<InputFocus>) {
/// input_focus.clear();
/// }
/// ```
///
/// With exclusive (or deferred) world access:
///
/// ```rust
/// use bevy_ecs::prelude::*;
/// use bevy_input_focus::InputFocus;
///
/// fn set_focus_from_world(world: &mut World) {
/// let entity = world.spawn_empty().id();
///
/// // Fetch the resource from the world
/// let mut input_focus = world.resource_mut::<InputFocus>();
/// // Then mutate it!
/// input_focus.set(entity);
///
/// // Or you can just insert a fresh copy of the resource
/// // which will overwrite the existing one.
/// world.insert_resource(InputFocus::from_entity(entity));
/// }
/// ```
#[derive(Clone, Debug, Default, Resource)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, Default, Resource, Clone)
)]
pub struct InputFocus(pub Option<Entity>);
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.
///
/// Note that this resource is not used by [`bevy_input_focus`](crate) itself, but is provided for
/// convenience to UI widgets or frameworks that want to display a focus indicator.
/// [`InputFocus`] may still be `Some` even if the focus indicator is not visible.
///
/// The value of this resource should be set by your focus navigation solution.
/// 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.
/// By contrast, a console-style UI intended to be navigated with a gamepad may always have the focus indicator visible.
///
/// To easily access information about whether focus indicators should be shown for a given entity, use the [`IsFocused`] trait.
///
/// By default, this resource is set to `false`.
#[derive(Clone, Debug, Resource, Default)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, Resource, Clone)
)]
pub struct InputFocusVisible(pub bool);
/// 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::<MyEvent>`](dispatch_focused_input) system to your app,
/// in the [`InputFocusSet::Dispatch`] system set during [`PreUpdate`].
#[derive(Clone, Debug, Component)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))]
pub struct FocusedInput<E: Event + Clone> {
/// The underlying input event.
pub input: E,
/// The primary window entity.
window: Entity,
}
impl<E: Event + Clone> Event for FocusedInput<E> {
type Traversal = WindowTraversal;
const AUTO_PROPAGATE: bool = true;
}
#[derive(QueryData)]
/// These are for accessing components defined on the targeted entity
pub struct WindowTraversal {
child_of: Option<&'static ChildOf>,
window: Option<&'static Window>,
}
impl<E: Event + Clone> Traversal<FocusedInput<E>> for WindowTraversal {
fn traverse(item: Self::Item<'_>, event: &FocusedInput<E>) -> Option<Entity> {
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::<MyEvent>`](dispatch_focused_input) system to your app,
/// as described in the docs for [`FocusedInput`].
pub struct InputDispatchPlugin;
impl Plugin for InputDispatchPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Startup, set_initial_focus)
.init_resource::<InputFocus>()
.init_resource::<InputFocusVisible>()
.add_systems(
PreUpdate,
(
dispatch_focused_input::<KeyboardInput>,
dispatch_focused_input::<GamepadButtonChangedEvent>,
dispatch_focused_input::<MouseWheel>,
)
.in_set(InputFocusSet::Dispatch),
);
#[cfg(feature = "bevy_reflect")]
app.register_type::<AutoFocus>()
.register_type::<InputFocus>()
.register_type::<InputFocusVisible>();
}
}
/// 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<InputFocus>,
window: Single<Entity, With<PrimaryWindow>>,
) {
input_focus.0 = Some(*window);
}
/// System which dispatches bubbled input events to the focused entity, or to the primary window
/// if no entity has focus.
pub fn dispatch_focused_input<E: Event + Clone>(
mut key_events: EventReader<E>,
focus: Res<InputFocus>,
windows: Query<Entity, With<PrimaryWindow>>,
mut commands: Commands,
) {
if let Ok(window) = windows.single() {
// 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,
},
focused_entity,
);
}
} else {
// 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(
FocusedInput {
input: ev.clone(),
window,
},
window,
);
}
}
}
}
/// Trait which defines methods to check if an entity currently has focus.
///
/// This is implemented for [`World`] and [`IsFocusedHelper`].
/// [`DeferredWorld`](bevy_ecs::world::DeferredWorld) indirectly implements it through [`Deref`].
///
/// For use within systems, use [`IsFocusedHelper`].
///
/// Modify the [`InputFocus`] resource to change the focused entity.
///
/// [`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 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 should be visible.
fn is_focus_within_visible(&self, entity: Entity) -> bool;
}
/// 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 ChildOf>,
input_focus: Option<Res<'w, InputFocus>>,
input_focus_visible: Option<Res<'w, InputFocusVisible>>,
}
impl IsFocused for IsFocusedHelper<'_, '_> {
fn is_focused(&self, entity: Entity) -> bool {
self.input_focus
.as_deref()
.and_then(|f| f.0)
.is_some_and(|e| e == entity)
}
fn is_focus_within(&self, entity: Entity) -> bool {
let Some(focus) = self.input_focus.as_deref().and_then(|f| f.0) else {
return false;
};
if focus == entity {
return true;
}
self.parent_query.iter_ancestors(focus).any(|e| e == entity)
}
fn is_focus_visible(&self, entity: Entity) -> bool {
self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focused(entity)
}
fn is_focus_within_visible(&self, entity: Entity) -> bool {
self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focus_within(entity)
}
}
impl IsFocused for World {
fn is_focused(&self, entity: Entity) -> bool {
self.get_resource::<InputFocus>()
.and_then(|f| f.0)
.is_some_and(|f| f == entity)
}
fn is_focus_within(&self, entity: Entity) -> bool {
let Some(focus) = self.get_resource::<InputFocus>().and_then(|f| f.0) else {
return false;
};
let mut e = focus;
loop {
if e == entity {
return true;
}
if let Some(parent) = self.entity(e).get::<ChildOf>().map(ChildOf::parent) {
e = parent;
} else {
return false;
}
}
}
fn is_focus_visible(&self, entity: Entity) -> bool {
self.get_resource::<InputFocusVisible>()
.is_some_and(|vis| vis.0)
&& self.is_focused(entity)
}
fn is_focus_within_visible(&self, entity: Entity) -> bool {
self.get_resource::<InputFocusVisible>()
.is_some_and(|vis| vis.0)
&& self.is_focus_within(entity)
}
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::String;
use bevy_ecs::{
component::HookContext, observer::Trigger, system::RunSystemOnce, world::DeferredWorld,
};
use bevy_input::{
keyboard::{Key, KeyCode},
ButtonState, InputPlugin,
};
use bevy_window::WindowResolution;
use smol_str::SmolStr;
#[derive(Component)]
#[component(on_add = set_focus_on_add)]
struct SetFocusOnAdd;
fn set_focus_on_add(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
let mut input_focus = world.resource_mut::<InputFocus>();
input_focus.set(entity);
}
#[derive(Component, Default)]
struct GatherKeyboardEvents(String);
fn gather_keyboard_events(
trigger: Trigger<FocusedInput<KeyboardInput>>,
mut query: Query<&mut GatherKeyboardEvents>,
) {
if let Ok(mut gather) = query.get_mut(trigger.target()) {
if let Key::Character(c) = &trigger.input.logical_key {
gather.0.push_str(c.as_str());
}
}
}
const KEY_A_EVENT: KeyboardInput = KeyboardInput {
key_code: KeyCode::KeyA,
logical_key: Key::Character(SmolStr::new_static("A")),
state: ButtonState::Pressed,
text: Some(SmolStr::new_static("A")),
repeat: false,
window: Entity::PLACEHOLDER,
};
#[test]
fn test_no_panics_if_resource_missing() {
let mut app = App::new();
// Note that we do not insert InputFocus here!
let entity = app.world_mut().spawn_empty().id();
assert!(!app.world().is_focused(entity));
app.world_mut()
.run_system_once(move |helper: IsFocusedHelper| {
assert!(!helper.is_focused(entity));
assert!(!helper.is_focus_within(entity));
assert!(!helper.is_focus_visible(entity));
assert!(!helper.is_focus_within_visible(entity));
})
.unwrap();
app.world_mut()
.run_system_once(move |world: DeferredWorld| {
assert!(!world.is_focused(entity));
assert!(!world.is_focus_within(entity));
assert!(!world.is_focus_visible(entity));
assert!(!world.is_focus_within_visible(entity));
})
.unwrap();
}
#[test]
fn test_keyboard_events() {
fn get_gathered(app: &App, entity: Entity) -> &str {
app.world()
.entity(entity)
.get::<GatherKeyboardEvents>()
.unwrap()
.0
.as_str()
}
let mut app = App::new();
app.add_plugins((InputPlugin, InputDispatchPlugin))
.add_observer(gather_keyboard_events);
let window = Window {
resolution: WindowResolution::new(800., 600.),
..Default::default()
};
app.world_mut().spawn((window, PrimaryWindow));
// Run the world for a single frame to set up the initial focus
app.update();
let entity_a = app
.world_mut()
.spawn((GatherKeyboardEvents::default(), SetFocusOnAdd))
.id();
let child_of_b = app
.world_mut()
.spawn((GatherKeyboardEvents::default(),))
.id();
let entity_b = app
.world_mut()
.spawn((GatherKeyboardEvents::default(),))
.add_child(child_of_b)
.id();
assert!(app.world().is_focused(entity_a));
assert!(!app.world().is_focused(entity_b));
assert!(!app.world().is_focused(child_of_b));
assert!(!app.world().is_focus_visible(entity_a));
assert!(!app.world().is_focus_visible(entity_b));
assert!(!app.world().is_focus_visible(child_of_b));
// entity_a should receive this event
app.world_mut().send_event(KEY_A_EVENT);
app.update();
assert_eq!(get_gathered(&app, entity_a), "A");
assert_eq!(get_gathered(&app, entity_b), "");
assert_eq!(get_gathered(&app, child_of_b), "");
app.world_mut().insert_resource(InputFocus(None));
assert!(!app.world().is_focused(entity_a));
assert!(!app.world().is_focus_visible(entity_a));
// This event should be lost
app.world_mut().send_event(KEY_A_EVENT);
app.update();
assert_eq!(get_gathered(&app, entity_a), "A");
assert_eq!(get_gathered(&app, entity_b), "");
assert_eq!(get_gathered(&app, child_of_b), "");
app.world_mut()
.insert_resource(InputFocus::from_entity(entity_b));
assert!(app.world().is_focused(entity_b));
assert!(!app.world().is_focused(child_of_b));
app.world_mut()
.run_system_once(move |mut input_focus: ResMut<InputFocus>| {
input_focus.set(child_of_b);
})
.unwrap();
assert!(app.world().is_focus_within(entity_b));
// These events should be received by entity_b and child_of_b
app.world_mut().send_event_batch([KEY_A_EVENT; 4]);
app.update();
assert_eq!(get_gathered(&app, entity_a), "A");
assert_eq!(get_gathered(&app, entity_b), "AAAA");
assert_eq!(get_gathered(&app, child_of_b), "AAAA");
app.world_mut().resource_mut::<InputFocusVisible>().0 = true;
app.world_mut()
.run_system_once(move |helper: IsFocusedHelper| {
assert!(!helper.is_focused(entity_a));
assert!(!helper.is_focus_within(entity_a));
assert!(!helper.is_focus_visible(entity_a));
assert!(!helper.is_focus_within_visible(entity_a));
assert!(!helper.is_focused(entity_b));
assert!(helper.is_focus_within(entity_b));
assert!(!helper.is_focus_visible(entity_b));
assert!(helper.is_focus_within_visible(entity_b));
assert!(helper.is_focused(child_of_b));
assert!(helper.is_focus_within(child_of_b));
assert!(helper.is_focus_visible(child_of_b));
assert!(helper.is_focus_within_visible(child_of_b));
})
.unwrap();
app.world_mut()
.run_system_once(move |world: DeferredWorld| {
assert!(!world.is_focused(entity_a));
assert!(!world.is_focus_within(entity_a));
assert!(!world.is_focus_visible(entity_a));
assert!(!world.is_focus_within_visible(entity_a));
assert!(!world.is_focused(entity_b));
assert!(world.is_focus_within(entity_b));
assert!(!world.is_focus_visible(entity_b));
assert!(world.is_focus_within_visible(entity_b));
assert!(world.is_focused(child_of_b));
assert!(world.is_focus_within(child_of_b));
assert!(world.is_focus_visible(child_of_b));
assert!(world.is_focus_within_visible(child_of_b));
})
.unwrap();
}
}

View File

@@ -0,0 +1,400 @@
//! This module provides a framework for handling linear tab-key navigation in Bevy applications.
//!
//! The rules of tabbing are derived from the HTML specification, and are as follows:
//!
//! * An index >= 0 means that the entity is tabbable via sequential navigation.
//! The order of tabbing is determined by the index, with lower indices being tabbed first.
//! If two entities have the same index, then the order is determined by the order of
//! the entities in the ECS hierarchy (as determined by Parent/Child).
//! * 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
//! marks a tree of entities as containing tabbable elements. The order of 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.
//!
//! 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 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.
use alloc::vec::Vec;
use bevy_app::{App, Plugin, Startup};
use bevy_ecs::{
component::Component,
entity::Entity,
hierarchy::{ChildOf, Children},
observer::Trigger,
query::{With, Without},
system::{Commands, Query, Res, ResMut, SystemParam},
};
use bevy_input::{
keyboard::{KeyCode, KeyboardInput},
ButtonInput, ButtonState,
};
use bevy_window::PrimaryWindow;
use log::warn;
use thiserror::Error;
use crate::{FocusedInput, InputFocus, InputFocusVisible};
#[cfg(feature = "bevy_reflect")]
use {
bevy_ecs::prelude::ReflectComponent,
bevy_reflect::{prelude::*, Reflect},
};
/// A component which indicates that an entity wants to participate in tab navigation.
///
/// Note that you must also add the [`TabGroup`] component to the entity's ancestor in order
/// for this component to have any effect.
#[derive(Debug, Default, Component, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, Default, Component, PartialEq, Clone)
)]
pub struct TabIndex(pub i32);
/// A component used to mark a tree of entities as containing tabbable elements.
#[derive(Debug, Default, Component, Copy, Clone)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, Default, Component, Clone)
)]
pub struct TabGroup {
/// The order of the tab group relative to other tab groups.
pub order: i32,
/// Whether this is a 'modal' group. If true, then tabbing within the group (that is,
/// if the current focus entity is a child of this group) will cycle through the children
/// of this group. If false, then tabbing within the group will cycle through all non-modal
/// tab groups.
pub modal: bool,
}
impl TabGroup {
/// Create a new tab group with the given order.
pub fn new(order: i32) -> Self {
Self {
order,
modal: false,
}
}
/// Create a modal tab group.
pub fn modal() -> Self {
Self {
order: 0,
modal: true,
}
}
}
/// A navigation action that users might take to navigate your user interface in a cyclic fashion.
///
/// 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.
///
/// This is commonly triggered by pressing the Tab key.
Next,
/// Navigate to the previous focusable entity, wrapping around to the end if at the beginning.
///
/// This is commonly triggered by pressing Shift+Tab.
Previous,
/// Navigate to the first focusable entity.
///
/// This is commonly triggered by pressing Home.
First,
/// Navigate to the last focusable entity.
///
/// This is commonly triggered by pressing End.
Last,
}
/// An error that can occur during [tab navigation](crate::tab_navigation).
#[derive(Debug, Error, PartialEq, Eq, Clone)]
pub enum TabNavigationError {
/// No tab groups were found.
#[error("No tab groups found")]
NoTabGroups,
/// No focusable entities were found.
#[error("No focusable entities found")]
NoFocusableEntities,
/// Could not navigate to the next focusable entity.
///
/// This can occur if your tab groups are malformed.
#[error("Failed to navigate to next focusable entity")]
FailedToNavigateToNextFocusableEntity,
/// No tab group for the current focus entity was found.
#[error("No tab group found for currently focused entity {previous_focus}. Users will not be able to navigate back to this entity.")]
NoTabGroupForCurrentFocus {
/// The entity that was previously focused,
/// and is missing its tab group.
previous_focus: Entity,
/// The new entity that will be focused.
///
/// If you want to recover from this error, set [`InputFocus`] to this entity.
new_focus: Entity,
},
}
/// An injectable helper object that provides tab navigation functionality.
#[doc(hidden)]
#[derive(SystemParam)]
pub struct TabNavigation<'w, 's> {
// Query for tab groups.
tabgroup_query: Query<'w, 's, (Entity, &'static TabGroup, &'static Children)>,
// Query for tab indices.
tabindex_query: Query<
'w,
's,
(Entity, Option<&'static TabIndex>, Option<&'static Children>),
Without<TabGroup>,
>,
// Query for parents.
parent_query: Query<'w, 's, &'static ChildOf>,
}
impl TabNavigation<'_, '_> {
/// Navigate to the desired focusable entity.
///
/// Change the [`NavAction`] to navigate in a different direction.
/// Focusable entities are determined by the presence of the [`TabIndex`] component.
///
/// If no focusable entities are found, then this function will return either the first
/// 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: &InputFocus,
action: NavAction,
) -> Result<Entity, TabNavigationError> {
// If there are no tab groups, then there are no focusable entities.
if self.tabgroup_query.is_empty() {
return Err(TabNavigationError::NoTabGroups);
}
// 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.0.and_then(|focus_ent| {
self.parent_query
.iter_ancestors(focus_ent)
.find_map(|entity| {
self.tabgroup_query
.get(entity)
.ok()
.map(|(_, tg, _)| (entity, tg))
})
});
let navigation_result = self.navigate_in_group(tabgroup, focus, action);
match navigation_result {
Ok(entity) => {
if focus.0.is_some() && tabgroup.is_none() {
Err(TabNavigationError::NoTabGroupForCurrentFocus {
previous_focus: focus.0.unwrap(),
new_focus: entity,
})
} else {
Ok(entity)
}
}
Err(e) => Err(e),
}
}
fn navigate_in_group(
&self,
tabgroup: Option<(Entity, &TabGroup)>,
focus: &InputFocus,
action: NavAction,
) -> Result<Entity, TabNavigationError> {
// List of all focusable entities found.
let mut focusable: Vec<(Entity, TabIndex)> =
Vec::with_capacity(self.tabindex_query.iter().len());
match tabgroup {
Some((tg_entity, tg)) if tg.modal => {
// We're in a modal tab group, then gather all tab indices in that group.
if let Ok((_, _, children)) = self.tabgroup_query.get(tg_entity) {
for child in children.iter() {
self.gather_focusable(&mut focusable, *child);
}
}
}
_ => {
// Otherwise, gather all tab indices in all non-modal tab groups.
let mut tab_groups: Vec<(Entity, TabGroup)> = self
.tabgroup_query
.iter()
.filter(|(_, tg, _)| !tg.modal)
.map(|(e, tg, _)| (e, *tg))
.collect();
// Stable sort by group order
tab_groups.sort_by_key(|(_, tg)| tg.order);
// Search group descendants
tab_groups.iter().for_each(|(tg_entity, _)| {
self.gather_focusable(&mut focusable, *tg_entity);
});
}
}
if focusable.is_empty() {
return Err(TabNavigationError::NoFocusableEntities);
}
// Stable sort by tabindex
focusable.sort_by_key(|(_, idx)| *idx);
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),
(Some(idx), NavAction::Previous) => (idx + count - 1).rem_euclid(count),
(None, NavAction::Next) | (_, NavAction::First) => 0,
(None, NavAction::Previous) | (_, NavAction::Last) => count - 1,
};
match focusable.get(next) {
Some((entity, _)) => Ok(*entity),
None => Err(TabNavigationError::FailedToNavigateToNextFocusableEntity),
}
}
/// Gather all focusable entities in tree order.
fn gather_focusable(&self, out: &mut Vec<(Entity, TabIndex)>, parent: Entity) {
if let Ok((entity, tabindex, children)) = self.tabindex_query.get(parent) {
if let Some(tabindex) = tabindex {
if tabindex.0 >= 0 {
out.push((entity, *tabindex));
}
}
if let Some(children) = children {
for child in children.iter() {
// Don't traverse into tab groups, as they are handled separately.
if self.tabgroup_query.get(*child).is_err() {
self.gather_focusable(out, *child);
}
}
}
} else if let Ok((_, tabgroup, children)) = self.tabgroup_query.get(parent) {
if !tabgroup.modal {
for child in children.iter() {
self.gather_focusable(out, *child);
}
}
}
}
}
/// 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);
#[cfg(feature = "bevy_reflect")]
app.register_type::<TabIndex>().register_type::<TabGroup>();
}
}
fn setup_tab_navigation(mut commands: Commands, window: Query<Entity, With<PrimaryWindow>>) {
for window in window.iter() {
commands.entity(window).observe(handle_tab_navigation);
}
}
/// Observer function which handles tab navigation.
///
/// This observer responds to [`KeyCode::Tab`] events and Shift+Tab events,
/// cycling through focusable entities in the order determined by their tab index.
///
/// Any [`TabNavigationError`]s that occur during tab navigation are logged as warnings.
pub fn handle_tab_navigation(
mut trigger: Trigger<FocusedInput<KeyboardInput>>,
nav: TabNavigation,
mut focus: ResMut<InputFocus>,
mut visible: ResMut<InputFocusVisible>,
keys: Res<ButtonInput<KeyCode>>,
) {
// Tab navigation.
let key_event = &trigger.event().input;
if key_event.key_code == KeyCode::Tab
&& key_event.state == ButtonState::Pressed
&& !key_event.repeat
{
let maybe_next = nav.navigate(
&focus,
if keys.pressed(KeyCode::ShiftLeft) || keys.pressed(KeyCode::ShiftRight) {
NavAction::Previous
} else {
NavAction::Next
},
);
match maybe_next {
Ok(next) => {
trigger.propagate(false);
focus.set(next);
visible.0 = true;
}
Err(e) => {
warn!("Tab navigation error: {}", e);
// This failure mode is recoverable, but still indicates a problem.
if let TabNavigationError::NoTabGroupForCurrentFocus { new_focus, .. } = e {
trigger.propagate(false);
focus.set(new_focus);
visible.0 = true;
}
}
}
}
}
#[cfg(test)]
mod tests {
use bevy_ecs::system::SystemState;
use super::*;
#[test]
fn test_tab_navigation() {
let mut app = App::new();
let world = app.world_mut();
let tab_group_entity = world.spawn(TabGroup::new(0)).id();
let tab_entity_1 = world.spawn((TabIndex(0), ChildOf(tab_group_entity))).id();
let tab_entity_2 = world.spawn((TabIndex(1), ChildOf(tab_group_entity))).id();
let mut system_state: SystemState<TabNavigation> = SystemState::new(world);
let tab_navigation = system_state.get(world);
assert_eq!(tab_navigation.tabgroup_query.iter().count(), 1);
assert_eq!(tab_navigation.tabindex_query.iter().count(), 2);
let next_entity =
tab_navigation.navigate(&InputFocus::from_entity(tab_entity_1), NavAction::Next);
assert_eq!(next_entity, Ok(tab_entity_2));
let prev_entity =
tab_navigation.navigate(&InputFocus::from_entity(tab_entity_2), NavAction::Previous);
assert_eq!(prev_entity, Ok(tab_entity_1));
let first_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::First);
assert_eq!(first_entity, Ok(tab_entity_1));
let last_entity = tab_navigation.navigate(&InputFocus::default(), NavAction::Last);
assert_eq!(last_entity, Ok(tab_entity_2));
}
}