bevy_input_focus/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![forbid(unsafe_code)]
3#![doc(
4    html_logo_url = "https://bevy.org/assets/icon.png",
5    html_favicon_url = "https://bevy.org/assets/icon.png"
6)]
7#![no_std]
8
9//! A UI-centric focus system for Bevy.
10//!
11//! This crate provides a system for managing input focus in Bevy applications, including:
12//! * [`InputFocus`], a resource for tracking which entity has input focus.
13//! * Methods for getting and setting input focus via [`InputFocus`] and [`IsFocusedHelper`].
14//! * A generic [`FocusedInput`] event for input events which bubble up from the focused entity.
15//! * Various navigation frameworks for moving input focus between entities based on user input, such as [`tab_navigation`] and [`directional_navigation`].
16//!
17//! This crate does *not* provide any integration with UI widgets: this is the responsibility of the widget crate,
18//! which should depend on [`bevy_input_focus`](crate).
19
20#[cfg(feature = "std")]
21extern crate std;
22
23extern crate alloc;
24
25pub mod directional_navigation;
26pub mod navigator;
27pub mod tab_navigation;
28
29// This module is too small / specific to be exported by the crate,
30// but it's nice to have it separate for code organization.
31mod autofocus;
32pub use autofocus::*;
33
34#[cfg(any(feature = "keyboard", feature = "gamepad", feature = "mouse"))]
35use bevy_app::PreUpdate;
36use bevy_app::{App, Plugin, PostStartup};
37use bevy_ecs::{
38    entity::Entities, prelude::*, query::QueryData, system::SystemParam, traversal::Traversal,
39};
40#[cfg(feature = "gamepad")]
41use bevy_input::gamepad::GamepadButtonChangedEvent;
42#[cfg(feature = "keyboard")]
43use bevy_input::keyboard::KeyboardInput;
44#[cfg(feature = "mouse")]
45use bevy_input::mouse::MouseWheel;
46use bevy_window::{PrimaryWindow, Window};
47use core::fmt::Debug;
48
49#[cfg(feature = "bevy_reflect")]
50use bevy_reflect::{prelude::*, Reflect};
51
52/// Resource representing which entity has input focus, if any. Input events (other than pointer-like inputs) will be
53/// dispatched to the current focus entity, or to the primary window if no entity has focus.
54///
55/// Changing the input focus is as easy as modifying this resource.
56///
57/// # Examples
58///
59/// From within a system:
60///
61/// ```rust
62/// use bevy_ecs::prelude::*;
63/// use bevy_input_focus::InputFocus;
64///
65/// fn clear_focus(mut input_focus: ResMut<InputFocus>) {
66///   input_focus.clear();
67/// }
68/// ```
69///
70/// With exclusive (or deferred) world access:
71///
72/// ```rust
73/// use bevy_ecs::prelude::*;
74/// use bevy_input_focus::InputFocus;
75///
76/// fn set_focus_from_world(world: &mut World) {
77///     let entity = world.spawn_empty().id();
78///
79///     // Fetch the resource from the world
80///     let mut input_focus = world.resource_mut::<InputFocus>();
81///     // Then mutate it!
82///     input_focus.set(entity);
83///
84///     // Or you can just insert a fresh copy of the resource
85///     // which will overwrite the existing one.
86///     world.insert_resource(InputFocus::from_entity(entity));
87/// }
88/// ```
89#[derive(Clone, Debug, Default, Resource, PartialEq)]
90#[cfg_attr(
91    feature = "bevy_reflect",
92    derive(Reflect),
93    reflect(Debug, Default, Resource, Clone)
94)]
95pub struct InputFocus(pub Option<Entity>);
96
97impl InputFocus {
98    /// Create a new [`InputFocus`] resource with the given entity.
99    ///
100    /// This is mostly useful for tests.
101    pub const fn from_entity(entity: Entity) -> Self {
102        Self(Some(entity))
103    }
104
105    /// Set the entity with input focus.
106    pub const fn set(&mut self, entity: Entity) {
107        self.0 = Some(entity);
108    }
109
110    /// Returns the entity with input focus, if any.
111    pub const fn get(&self) -> Option<Entity> {
112        self.0
113    }
114
115    /// Clears input focus.
116    pub const fn clear(&mut self) {
117        self.0 = None;
118    }
119}
120
121/// Resource representing whether the input focus indicator should be visible on UI elements.
122///
123/// Note that this resource is not used by [`bevy_input_focus`](crate) itself, but is provided for
124/// convenience to UI widgets or frameworks that want to display a focus indicator.
125/// [`InputFocus`] may still be `Some` even if the focus indicator is not visible.
126///
127/// The value of this resource should be set by your focus navigation solution.
128/// For a desktop/web style of user interface this would be set to true when the user presses the tab key,
129/// and set to false when the user clicks on a different element.
130/// By contrast, a console-style UI intended to be navigated with a gamepad may always have the focus indicator visible.
131///
132/// To easily access information about whether focus indicators should be shown for a given entity, use the [`IsFocused`] trait.
133///
134/// By default, this resource is set to `false`.
135#[derive(Clone, Debug, Resource, Default)]
136#[cfg_attr(
137    feature = "bevy_reflect",
138    derive(Reflect),
139    reflect(Debug, Resource, Clone)
140)]
141pub struct InputFocusVisible(pub bool);
142
143/// A bubble-able user input event that starts at the currently focused entity.
144///
145/// This event is normally dispatched to the current input focus entity, if any.
146/// If no entity has input focus, then the event is dispatched to the main window.
147///
148/// To set up your own bubbling input event, add the [`dispatch_focused_input::<MyEvent>`](dispatch_focused_input) system to your app,
149/// in the [`InputFocusSystems::Dispatch`] system set during [`PreUpdate`].
150#[derive(EntityEvent, Clone, Debug, Component)]
151#[entity_event(propagate = WindowTraversal, auto_propagate)]
152#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Component, Clone))]
153pub struct FocusedInput<M: Message + Clone> {
154    /// The entity that has received focused input.
155    #[event_target]
156    pub focused_entity: Entity,
157    /// The underlying input message.
158    pub input: M,
159    /// The primary window entity.
160    window: Entity,
161}
162
163/// An event which is used to set input focus. Trigger this on an entity, and it will bubble
164/// until it finds a focusable entity, and then set focus to it.
165#[derive(Clone, EntityEvent)]
166#[entity_event(propagate = WindowTraversal, auto_propagate)]
167pub struct AcquireFocus {
168    /// The entity that has acquired focus.
169    #[event_target]
170    pub focused_entity: Entity,
171    /// The primary window entity.
172    window: Entity,
173}
174
175#[derive(QueryData)]
176/// These are for accessing components defined on the targeted entity
177pub struct WindowTraversal {
178    child_of: Option<&'static ChildOf>,
179    window: Option<&'static Window>,
180}
181
182impl<M: Message + Clone> Traversal<FocusedInput<M>> for WindowTraversal {
183    fn traverse(item: Self::Item<'_, '_>, event: &FocusedInput<M>) -> Option<Entity> {
184        let WindowTraversalItem { child_of, window } = item;
185
186        // Send event to parent, if it has one.
187        if let Some(child_of) = child_of {
188            return Some(child_of.parent());
189        };
190
191        // Otherwise, send it to the window entity (unless this is a window entity).
192        if window.is_none() {
193            return Some(event.window);
194        }
195
196        None
197    }
198}
199
200impl Traversal<AcquireFocus> for WindowTraversal {
201    fn traverse(item: Self::Item<'_, '_>, event: &AcquireFocus) -> Option<Entity> {
202        let WindowTraversalItem { child_of, window } = item;
203
204        // Send event to parent, if it has one.
205        if let Some(child_of) = child_of {
206            return Some(child_of.parent());
207        };
208
209        // Otherwise, send it to the window entity (unless this is a window entity).
210        if window.is_none() {
211            return Some(event.window);
212        }
213
214        None
215    }
216}
217
218/// Plugin which sets up systems for dispatching bubbling keyboard and gamepad button events to the focused entity.
219///
220/// To add bubbling to your own input events, add the [`dispatch_focused_input::<MyEvent>`](dispatch_focused_input) system to your app,
221/// as described in the docs for [`FocusedInput`].
222pub struct InputDispatchPlugin;
223
224impl Plugin for InputDispatchPlugin {
225    fn build(&self, app: &mut App) {
226        app.add_systems(PostStartup, set_initial_focus)
227            .init_resource::<InputFocus>()
228            .init_resource::<InputFocusVisible>();
229
230        #[cfg(any(feature = "keyboard", feature = "gamepad", feature = "mouse"))]
231        app.add_systems(
232            PreUpdate,
233            (
234                #[cfg(feature = "keyboard")]
235                dispatch_focused_input::<KeyboardInput>,
236                #[cfg(feature = "gamepad")]
237                dispatch_focused_input::<GamepadButtonChangedEvent>,
238                #[cfg(feature = "mouse")]
239                dispatch_focused_input::<MouseWheel>,
240            )
241                .in_set(InputFocusSystems::Dispatch),
242        );
243    }
244}
245
246/// System sets for [`bevy_input_focus`](crate).
247///
248/// These systems run in the [`PreUpdate`] schedule.
249#[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)]
250pub enum InputFocusSystems {
251    /// System which dispatches bubbled input events to the focused entity, or to the primary window.
252    Dispatch,
253}
254
255/// If no entity is focused, sets the focus to the primary window, if any.
256pub fn set_initial_focus(
257    mut input_focus: ResMut<InputFocus>,
258    window: Single<Entity, With<PrimaryWindow>>,
259) {
260    if input_focus.0.is_none() {
261        input_focus.0 = Some(*window);
262    }
263}
264
265/// System which dispatches bubbled input events to the focused entity, or to the primary window
266/// if no entity has focus.
267///
268/// If the currently focused entity no longer exists (has been despawned), this system will
269/// automatically clear the focus and dispatch events to the primary window instead.
270pub fn dispatch_focused_input<M: Message + Clone>(
271    mut input_reader: MessageReader<M>,
272    mut focus: ResMut<InputFocus>,
273    windows: Query<Entity, With<PrimaryWindow>>,
274    entities: &Entities,
275    mut commands: Commands,
276) {
277    if let Ok(window) = windows.single() {
278        // If an element has keyboard focus, then dispatch the input event to that element.
279        if let Some(focused_entity) = focus.0 {
280            // Check if the focused entity is still alive
281            if entities.contains(focused_entity) {
282                for ev in input_reader.read() {
283                    commands.trigger(FocusedInput {
284                        focused_entity,
285                        input: ev.clone(),
286                        window,
287                    });
288                }
289            } else {
290                // If the focused entity no longer exists, clear focus and dispatch to window
291                focus.0 = None;
292                for ev in input_reader.read() {
293                    commands.trigger(FocusedInput {
294                        focused_entity: window,
295                        input: ev.clone(),
296                        window,
297                    });
298                }
299            }
300        } else {
301            // If no element has input focus, then dispatch the input event to the primary window.
302            // There should be only one primary window.
303            for ev in input_reader.read() {
304                commands.trigger(FocusedInput {
305                    focused_entity: window,
306                    input: ev.clone(),
307                    window,
308                });
309            }
310        }
311    }
312}
313
314/// Trait which defines methods to check if an entity currently has focus.
315///
316/// This is implemented for [`World`] and [`IsFocusedHelper`].
317/// [`DeferredWorld`](bevy_ecs::world::DeferredWorld) indirectly implements it through [`Deref`].
318///
319/// For use within systems, use [`IsFocusedHelper`].
320///
321/// Modify the [`InputFocus`] resource to change the focused entity.
322///
323/// [`Deref`]: std::ops::Deref
324pub trait IsFocused {
325    /// Returns true if the given entity has input focus.
326    fn is_focused(&self, entity: Entity) -> bool;
327
328    /// Returns true if the given entity or any of its descendants has input focus.
329    ///
330    /// Note that for unusual layouts, the focus may not be within the entity's visual bounds.
331    fn is_focus_within(&self, entity: Entity) -> bool;
332
333    /// Returns true if the given entity has input focus and the focus indicator should be visible.
334    fn is_focus_visible(&self, entity: Entity) -> bool;
335
336    /// Returns true if the given entity, or any descendant, has input focus and the focus
337    /// indicator should be visible.
338    fn is_focus_within_visible(&self, entity: Entity) -> bool;
339}
340
341/// A system param that helps get information about the current focused entity.
342///
343/// When working with the entire [`World`], consider using the [`IsFocused`] instead.
344#[derive(SystemParam)]
345pub struct IsFocusedHelper<'w, 's> {
346    parent_query: Query<'w, 's, &'static ChildOf>,
347    input_focus: Option<Res<'w, InputFocus>>,
348    input_focus_visible: Option<Res<'w, InputFocusVisible>>,
349}
350
351impl IsFocused for IsFocusedHelper<'_, '_> {
352    fn is_focused(&self, entity: Entity) -> bool {
353        self.input_focus
354            .as_deref()
355            .and_then(|f| f.0)
356            .is_some_and(|e| e == entity)
357    }
358
359    fn is_focus_within(&self, entity: Entity) -> bool {
360        let Some(focus) = self.input_focus.as_deref().and_then(|f| f.0) else {
361            return false;
362        };
363        if focus == entity {
364            return true;
365        }
366        self.parent_query.iter_ancestors(focus).any(|e| e == entity)
367    }
368
369    fn is_focus_visible(&self, entity: Entity) -> bool {
370        self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focused(entity)
371    }
372
373    fn is_focus_within_visible(&self, entity: Entity) -> bool {
374        self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focus_within(entity)
375    }
376}
377
378impl IsFocused for World {
379    fn is_focused(&self, entity: Entity) -> bool {
380        self.get_resource::<InputFocus>()
381            .and_then(|f| f.0)
382            .is_some_and(|f| f == entity)
383    }
384
385    fn is_focus_within(&self, entity: Entity) -> bool {
386        let Some(focus) = self.get_resource::<InputFocus>().and_then(|f| f.0) else {
387            return false;
388        };
389        let mut e = focus;
390        loop {
391            if e == entity {
392                return true;
393            }
394            if let Some(parent) = self.entity(e).get::<ChildOf>().map(ChildOf::parent) {
395                e = parent;
396            } else {
397                return false;
398            }
399        }
400    }
401
402    fn is_focus_visible(&self, entity: Entity) -> bool {
403        self.get_resource::<InputFocusVisible>()
404            .is_some_and(|vis| vis.0)
405            && self.is_focused(entity)
406    }
407
408    fn is_focus_within_visible(&self, entity: Entity) -> bool {
409        self.get_resource::<InputFocusVisible>()
410            .is_some_and(|vis| vis.0)
411            && self.is_focus_within(entity)
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    use alloc::string::String;
420    use bevy_app::Startup;
421    use bevy_ecs::{observer::On, system::RunSystemOnce, world::DeferredWorld};
422    use bevy_input::{
423        keyboard::{Key, KeyCode},
424        ButtonState, InputPlugin,
425    };
426
427    #[derive(Component, Default)]
428    struct GatherKeyboardEvents(String);
429
430    fn gather_keyboard_events(
431        event: On<FocusedInput<KeyboardInput>>,
432        mut query: Query<&mut GatherKeyboardEvents>,
433    ) {
434        if let Ok(mut gather) = query.get_mut(event.focused_entity) {
435            if let Key::Character(c) = &event.input.logical_key {
436                gather.0.push_str(c.as_str());
437            }
438        }
439    }
440
441    fn key_a_message() -> KeyboardInput {
442        KeyboardInput {
443            key_code: KeyCode::KeyA,
444            logical_key: Key::Character("A".into()),
445            state: ButtonState::Pressed,
446            text: Some("A".into()),
447            repeat: false,
448            window: Entity::PLACEHOLDER,
449        }
450    }
451
452    #[test]
453    fn test_no_panics_if_resource_missing() {
454        let mut app = App::new();
455        // Note that we do not insert InputFocus here!
456
457        let entity = app.world_mut().spawn_empty().id();
458
459        assert!(!app.world().is_focused(entity));
460
461        app.world_mut()
462            .run_system_once(move |helper: IsFocusedHelper| {
463                assert!(!helper.is_focused(entity));
464                assert!(!helper.is_focus_within(entity));
465                assert!(!helper.is_focus_visible(entity));
466                assert!(!helper.is_focus_within_visible(entity));
467            })
468            .unwrap();
469
470        app.world_mut()
471            .run_system_once(move |world: DeferredWorld| {
472                assert!(!world.is_focused(entity));
473                assert!(!world.is_focus_within(entity));
474                assert!(!world.is_focus_visible(entity));
475                assert!(!world.is_focus_within_visible(entity));
476            })
477            .unwrap();
478    }
479
480    #[test]
481    fn initial_focus_unset_if_no_primary_window() {
482        let mut app = App::new();
483        app.add_plugins((InputPlugin, InputDispatchPlugin));
484
485        app.update();
486
487        assert_eq!(app.world().resource::<InputFocus>().0, None);
488    }
489
490    #[test]
491    fn initial_focus_set_to_primary_window() {
492        let mut app = App::new();
493        app.add_plugins((InputPlugin, InputDispatchPlugin));
494
495        let entity_window = app
496            .world_mut()
497            .spawn((Window::default(), PrimaryWindow))
498            .id();
499        app.update();
500
501        assert_eq!(app.world().resource::<InputFocus>().0, Some(entity_window));
502    }
503
504    #[test]
505    fn initial_focus_not_overridden() {
506        let mut app = App::new();
507        app.add_plugins((InputPlugin, InputDispatchPlugin));
508
509        app.world_mut().spawn((Window::default(), PrimaryWindow));
510
511        app.add_systems(Startup, |mut commands: Commands| {
512            commands.spawn(AutoFocus);
513        });
514
515        app.update();
516
517        let autofocus_entity = app
518            .world_mut()
519            .query_filtered::<Entity, With<AutoFocus>>()
520            .single(app.world())
521            .unwrap();
522
523        assert_eq!(
524            app.world().resource::<InputFocus>().0,
525            Some(autofocus_entity)
526        );
527    }
528
529    #[test]
530    fn test_keyboard_events() {
531        fn get_gathered(app: &App, entity: Entity) -> &str {
532            app.world()
533                .entity(entity)
534                .get::<GatherKeyboardEvents>()
535                .unwrap()
536                .0
537                .as_str()
538        }
539
540        let mut app = App::new();
541
542        app.add_plugins((InputPlugin, InputDispatchPlugin))
543            .add_observer(gather_keyboard_events);
544
545        app.world_mut().spawn((Window::default(), PrimaryWindow));
546
547        // Run the world for a single frame to set up the initial focus
548        app.update();
549
550        let entity_a = app
551            .world_mut()
552            .spawn((GatherKeyboardEvents::default(), AutoFocus))
553            .id();
554
555        let child_of_b = app
556            .world_mut()
557            .spawn((GatherKeyboardEvents::default(),))
558            .id();
559
560        let entity_b = app
561            .world_mut()
562            .spawn((GatherKeyboardEvents::default(),))
563            .add_child(child_of_b)
564            .id();
565
566        assert!(app.world().is_focused(entity_a));
567        assert!(!app.world().is_focused(entity_b));
568        assert!(!app.world().is_focused(child_of_b));
569        assert!(!app.world().is_focus_visible(entity_a));
570        assert!(!app.world().is_focus_visible(entity_b));
571        assert!(!app.world().is_focus_visible(child_of_b));
572
573        // entity_a should receive this event
574        app.world_mut().write_message(key_a_message());
575        app.update();
576
577        assert_eq!(get_gathered(&app, entity_a), "A");
578        assert_eq!(get_gathered(&app, entity_b), "");
579        assert_eq!(get_gathered(&app, child_of_b), "");
580
581        app.world_mut().insert_resource(InputFocus(None));
582
583        assert!(!app.world().is_focused(entity_a));
584        assert!(!app.world().is_focus_visible(entity_a));
585
586        // This event should be lost
587        app.world_mut().write_message(key_a_message());
588        app.update();
589
590        assert_eq!(get_gathered(&app, entity_a), "A");
591        assert_eq!(get_gathered(&app, entity_b), "");
592        assert_eq!(get_gathered(&app, child_of_b), "");
593
594        app.world_mut()
595            .insert_resource(InputFocus::from_entity(entity_b));
596        assert!(app.world().is_focused(entity_b));
597        assert!(!app.world().is_focused(child_of_b));
598
599        app.world_mut()
600            .run_system_once(move |mut input_focus: ResMut<InputFocus>| {
601                input_focus.set(child_of_b);
602            })
603            .unwrap();
604        assert!(app.world().is_focus_within(entity_b));
605
606        // These events should be received by entity_b and child_of_b
607        app.world_mut()
608            .write_message_batch(core::iter::repeat_n(key_a_message(), 4));
609        app.update();
610
611        assert_eq!(get_gathered(&app, entity_a), "A");
612        assert_eq!(get_gathered(&app, entity_b), "AAAA");
613        assert_eq!(get_gathered(&app, child_of_b), "AAAA");
614
615        app.world_mut().resource_mut::<InputFocusVisible>().0 = true;
616
617        app.world_mut()
618            .run_system_once(move |helper: IsFocusedHelper| {
619                assert!(!helper.is_focused(entity_a));
620                assert!(!helper.is_focus_within(entity_a));
621                assert!(!helper.is_focus_visible(entity_a));
622                assert!(!helper.is_focus_within_visible(entity_a));
623
624                assert!(!helper.is_focused(entity_b));
625                assert!(helper.is_focus_within(entity_b));
626                assert!(!helper.is_focus_visible(entity_b));
627                assert!(helper.is_focus_within_visible(entity_b));
628
629                assert!(helper.is_focused(child_of_b));
630                assert!(helper.is_focus_within(child_of_b));
631                assert!(helper.is_focus_visible(child_of_b));
632                assert!(helper.is_focus_within_visible(child_of_b));
633            })
634            .unwrap();
635
636        app.world_mut()
637            .run_system_once(move |world: DeferredWorld| {
638                assert!(!world.is_focused(entity_a));
639                assert!(!world.is_focus_within(entity_a));
640                assert!(!world.is_focus_visible(entity_a));
641                assert!(!world.is_focus_within_visible(entity_a));
642
643                assert!(!world.is_focused(entity_b));
644                assert!(world.is_focus_within(entity_b));
645                assert!(!world.is_focus_visible(entity_b));
646                assert!(world.is_focus_within_visible(entity_b));
647
648                assert!(world.is_focused(child_of_b));
649                assert!(world.is_focus_within(child_of_b));
650                assert!(world.is_focus_visible(child_of_b));
651                assert!(world.is_focus_within_visible(child_of_b));
652            })
653            .unwrap();
654    }
655
656    #[test]
657    fn dispatch_clears_focus_when_focused_entity_despawned() {
658        let mut app = App::new();
659        app.add_plugins((InputPlugin, InputDispatchPlugin));
660
661        app.world_mut().spawn((Window::default(), PrimaryWindow));
662        app.update();
663
664        let entity = app.world_mut().spawn_empty().id();
665        app.world_mut()
666            .insert_resource(InputFocus::from_entity(entity));
667        app.world_mut().entity_mut(entity).despawn();
668
669        assert_eq!(app.world().resource::<InputFocus>().0, Some(entity));
670
671        // Send input event - this should clear focus instead of panicking
672        app.world_mut().write_message(key_a_message());
673        app.update();
674
675        assert_eq!(app.world().resource::<InputFocus>().0, None);
676    }
677}