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