#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![doc(
html_logo_url = "https://bevy.org/assets/icon.png",
html_favicon_url = "https://bevy.org/assets/icon.png"
)]
#![no_std]
#[cfg(feature = "std")]
extern crate std;
extern crate alloc;
pub mod directional_navigation;
pub mod tab_navigation;
mod autofocus;
pub use autofocus::*;
use bevy_app::{App, Plugin, PostStartup, PreUpdate};
use bevy_ecs::{
entity::Entities, 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};
#[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 {
pub const fn from_entity(entity: Entity) -> Self {
Self(Some(entity))
}
pub const fn set(&mut self, entity: Entity) {
self.0 = Some(entity);
}
pub const fn get(&self) -> Option<Entity> {
self.0
}
pub const fn clear(&mut self) {
self.0 = None;
}
}
#[derive(Clone, Debug, Resource, Default)]
#[cfg_attr(
feature = "bevy_reflect",
derive(Reflect),
reflect(Debug, Resource, Clone)
)]
pub struct InputFocusVisible(pub bool);
#[derive(EntityEvent, Clone, Debug, Component)]
#[entity_event(traversal = WindowTraversal, auto_propagate)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))]
pub struct FocusedInput<E: BufferedEvent + Clone> {
pub input: E,
window: Entity,
}
#[derive(Clone, EntityEvent)]
#[entity_event(traversal = WindowTraversal, auto_propagate)]
pub struct AcquireFocus {
window: Entity,
}
#[derive(QueryData)]
pub struct WindowTraversal {
child_of: Option<&'static ChildOf>,
window: Option<&'static Window>,
}
impl<E: BufferedEvent + Clone> Traversal<FocusedInput<E>> for WindowTraversal {
fn traverse(item: Self::Item<'_, '_>, event: &FocusedInput<E>) -> Option<Entity> {
let WindowTraversalItem { child_of, window } = item;
if let Some(child_of) = child_of {
return Some(child_of.parent());
};
if window.is_none() {
return Some(event.window);
}
None
}
}
impl Traversal<AcquireFocus> for WindowTraversal {
fn traverse(item: Self::Item<'_, '_>, event: &AcquireFocus) -> Option<Entity> {
let WindowTraversalItem { child_of, window } = item;
if let Some(child_of) = child_of {
return Some(child_of.parent());
};
if window.is_none() {
return Some(event.window);
}
None
}
}
pub struct InputDispatchPlugin;
impl Plugin for InputDispatchPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PostStartup, 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(InputFocusSystems::Dispatch),
);
}
}
#[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)]
pub enum InputFocusSystems {
Dispatch,
}
#[deprecated(since = "0.17.0", note = "Renamed to `InputFocusSystems`.")]
pub type InputFocusSet = InputFocusSystems;
pub fn set_initial_focus(
mut input_focus: ResMut<InputFocus>,
window: Single<Entity, With<PrimaryWindow>>,
) {
if input_focus.0.is_none() {
input_focus.0 = Some(*window);
}
}
pub fn dispatch_focused_input<E: BufferedEvent + Clone>(
mut key_events: EventReader<E>,
mut focus: ResMut<InputFocus>,
windows: Query<Entity, With<PrimaryWindow>>,
entities: &Entities,
mut commands: Commands,
) {
if let Ok(window) = windows.single() {
if let Some(focused_entity) = focus.0 {
if entities.contains(focused_entity) {
for ev in key_events.read() {
commands.trigger_targets(
FocusedInput {
input: ev.clone(),
window,
},
focused_entity,
);
}
} else {
focus.0 = None;
for ev in key_events.read() {
commands.trigger_targets(
FocusedInput {
input: ev.clone(),
window,
},
window,
);
}
}
} else {
for ev in key_events.read() {
commands.trigger_targets(
FocusedInput {
input: ev.clone(),
window,
},
window,
);
}
}
}
}
pub trait IsFocused {
fn is_focused(&self, entity: Entity) -> bool;
fn is_focus_within(&self, entity: Entity) -> bool;
fn is_focus_visible(&self, entity: Entity) -> bool;
fn is_focus_within_visible(&self, entity: Entity) -> bool;
}
#[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_app::Startup;
use bevy_ecs::{observer::On, system::RunSystemOnce, world::DeferredWorld};
use bevy_input::{
keyboard::{Key, KeyCode},
ButtonState, InputPlugin,
};
#[derive(Component, Default)]
struct GatherKeyboardEvents(String);
fn gather_keyboard_events(
event: On<FocusedInput<KeyboardInput>>,
mut query: Query<&mut GatherKeyboardEvents>,
) {
if let Ok(mut gather) = query.get_mut(event.entity()) {
if let Key::Character(c) = &event.input.logical_key {
gather.0.push_str(c.as_str());
}
}
}
fn key_a_event() -> KeyboardInput {
KeyboardInput {
key_code: KeyCode::KeyA,
logical_key: Key::Character("A".into()),
state: ButtonState::Pressed,
text: Some("A".into()),
repeat: false,
window: Entity::PLACEHOLDER,
}
}
#[test]
fn test_no_panics_if_resource_missing() {
let mut app = App::new();
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 initial_focus_unset_if_no_primary_window() {
let mut app = App::new();
app.add_plugins((InputPlugin, InputDispatchPlugin));
app.update();
assert_eq!(app.world().resource::<InputFocus>().0, None);
}
#[test]
fn initial_focus_set_to_primary_window() {
let mut app = App::new();
app.add_plugins((InputPlugin, InputDispatchPlugin));
let entity_window = app
.world_mut()
.spawn((Window::default(), PrimaryWindow))
.id();
app.update();
assert_eq!(app.world().resource::<InputFocus>().0, Some(entity_window));
}
#[test]
fn initial_focus_not_overridden() {
let mut app = App::new();
app.add_plugins((InputPlugin, InputDispatchPlugin));
app.world_mut().spawn((Window::default(), PrimaryWindow));
app.add_systems(Startup, |mut commands: Commands| {
commands.spawn(AutoFocus);
});
app.update();
let autofocus_entity = app
.world_mut()
.query_filtered::<Entity, With<AutoFocus>>()
.single(app.world())
.unwrap();
assert_eq!(
app.world().resource::<InputFocus>().0,
Some(autofocus_entity)
);
}
#[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);
app.world_mut().spawn((Window::default(), PrimaryWindow));
app.update();
let entity_a = app
.world_mut()
.spawn((GatherKeyboardEvents::default(), AutoFocus))
.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));
app.world_mut().write_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));
app.world_mut().write_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));
app.world_mut()
.write_event_batch(core::iter::repeat_n(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();
}
#[test]
fn dispatch_clears_focus_when_focused_entity_despawned() {
let mut app = App::new();
app.add_plugins((InputPlugin, InputDispatchPlugin));
app.world_mut().spawn((Window::default(), PrimaryWindow));
app.update();
let entity = app.world_mut().spawn_empty().id();
app.world_mut()
.insert_resource(InputFocus::from_entity(entity));
app.world_mut().entity_mut(entity).despawn();
assert_eq!(app.world().resource::<InputFocus>().0, Some(entity));
app.world_mut().write_event(key_a_event());
app.update();
assert_eq!(app.world().resource::<InputFocus>().0, None);
}
}