bevy_input_focus/
lib.rs

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