Skip to main content

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//! * Events for when entities gain or lose focus: [`FocusGained`] and [`FocusLost`].
15//! * A generic [`FocusedInput`] event to send input events which bubble up from the focused entity.
16//! * Various navigation frameworks for moving input focus between entities based on user input, such as [`tab_navigation`] and [`directional_navigation`].
17#![cfg_attr(
18    feature = "bevy_picking",
19    doc = "* [`pointer_focus`], which focuses (or blurs) entities in response to pointer clicks, independently of any navigation framework (requires the `bevy_picking` feature)."
20)]
21//!
22//! This crate does *not* provide any integration with UI widgets: this is the responsibility of the widget crate,
23//! which should depend on [`bevy_input_focus`](crate).
24
25#[cfg(feature = "std")]
26extern crate std;
27
28extern crate alloc;
29
30pub mod directional_navigation;
31pub mod navigator;
32#[cfg(feature = "bevy_picking")]
33pub mod pointer_focus;
34pub mod tab_navigation;
35
36// These modules are too small / specific to be exported by the crate,
37// but it's nice to have them separate for code organization.
38mod autofocus;
39pub use autofocus::*;
40
41mod gained_and_lost;
42pub use gained_and_lost::*;
43
44use alloc::vec;
45use alloc::vec::Vec;
46#[cfg(any(feature = "keyboard", feature = "gamepad", feature = "mouse"))]
47use bevy_app::PreUpdate;
48use bevy_app::{App, Plugin, PostStartup, PostUpdate};
49use bevy_ecs::{
50    entity::Entities, prelude::*, query::QueryData, system::SystemParam, traversal::Traversal,
51};
52#[cfg(feature = "gamepad")]
53use bevy_input::gamepad::GamepadButtonChangedEvent;
54#[cfg(feature = "keyboard")]
55use bevy_input::keyboard::KeyboardInput;
56#[cfg(feature = "mouse")]
57use bevy_input::mouse::MouseWheel;
58use bevy_window::{PrimaryWindow, Window};
59use core::fmt::Debug;
60
61#[cfg(feature = "bevy_reflect")]
62use bevy_reflect::{prelude::*, Reflect};
63
64/// Resource representing which entity has input focus, if any. Input events (other than pointer-like inputs) will be
65/// dispatched to the current focus entity, or to the primary window if no entity has focus.
66///
67/// Changing the input focus is as easy as modifying this resource.
68///
69/// To detect when an entity gains or loses focus, listen for the [`FocusGained`] and [`FocusLost`] events.
70///
71/// # Examples
72///
73/// From within a system:
74///
75/// ```rust
76/// use bevy_ecs::prelude::*;
77/// use bevy_input_focus::{FocusCause, InputFocus};
78///
79/// fn clear_focus(mut input_focus: ResMut<InputFocus>) {
80///   input_focus.clear();
81/// }
82/// ```
83///
84/// With exclusive (or deferred) world access:
85///
86/// ```rust
87/// use bevy_ecs::prelude::*;
88/// use bevy_input_focus::{FocusCause, InputFocus};
89///
90/// fn set_focus_from_world(world: &mut World) {
91///     let entity = world.spawn_empty().id();
92///
93///     // Fetch the resource from the world
94///     let mut input_focus = world.resource_mut::<InputFocus>();
95///     // Then mutate it!
96///     input_focus.set(entity, FocusCause::Navigated);
97///
98///     // Or you can just insert a fresh copy of the resource
99///     // which will overwrite the existing one.
100///     world.insert_resource(InputFocus::from_entity(entity));
101/// }
102/// ```
103#[derive(Clone, Debug, Default, Resource, PartialEq)]
104#[cfg_attr(
105    feature = "bevy_reflect",
106    derive(Reflect),
107    reflect(Debug, Default, Resource, Clone)
108)]
109pub struct InputFocus {
110    /// The entity that currently has input focus, if any.
111    current_focus: Option<Entity>,
112    /// The set of input focus changes that have been recorded since the last time [`FocusGained`] and [`FocusLost`] events were sent.
113    ///
114    /// These are stored in a first-in-first-out manner, so the most recent change is at the end of the vector.
115    recorded_changes: Vec<Option<(Entity, FocusCause)>>,
116    /// The entity that had input focus at the time of the last sent [`FocusGained`] or [`FocusLost`] event, if any.
117    ///
118    /// This is used to determine which events to send when processing recorded focus changes.
119    original_focus: Option<Entity>,
120}
121
122impl InputFocus {
123    /// Create a new [`InputFocus`] resource with the given entity.
124    ///
125    /// This is mostly useful for tests.
126    ///
127    /// WARNING: this will clear any buffered focus changes,
128    /// so it may cause missed [`FocusGained`] and [`FocusLost`] events.
129    ///
130    /// Prefer the [`set`](InputFocus::set) method for normal use, which will preserve buffered changes.
131    pub fn from_entity(entity: Entity) -> Self {
132        Self {
133            current_focus: Some(entity),
134            recorded_changes: vec![Some((entity, FocusCause::Navigated))],
135            original_focus: None,
136        }
137    }
138
139    /// Set the entity with input focus.
140    ///
141    /// When spawning entities, you may want to use the [`AutoFocus`] component instead,
142    /// which will automatically set focus to the entity when it is spawned.
143    ///
144    /// This is particularly useful when working with bsn! scenes, where spawning may be delayed.
145    pub fn set(&mut self, entity: Entity, cause: FocusCause) {
146        self.current_focus = Some(entity);
147        self.recorded_changes.push(Some((entity, cause)));
148    }
149
150    /// Returns the entity with input focus, if any.
151    pub const fn get(&self) -> Option<Entity> {
152        self.current_focus
153    }
154
155    /// Clears input focus.
156    pub fn clear(&mut self) {
157        self.current_focus = None;
158        self.recorded_changes.push(None);
159    }
160}
161
162/// Resource representing whether the input focus indicator should be visible on UI elements.
163///
164/// Note that this resource is not used by [`bevy_input_focus`](crate) itself, but is provided for
165/// convenience to UI widgets or frameworks that want to display a focus indicator.
166/// [`InputFocus`] may still be `Some` even if the focus indicator is not visible.
167///
168/// The value of this resource should be set by your focus navigation solution.
169/// For a desktop/web style of user interface this would be set to true when the user presses the tab key,
170/// and set to false when the user clicks on a different element.
171/// By contrast, a console-style UI intended to be navigated with a gamepad may always have the focus indicator visible.
172///
173/// To easily access information about whether focus indicators should be shown for a given entity, use the [`IsFocused`] trait.
174///
175/// By default, this resource is set to `false`.
176#[derive(Clone, Debug, Resource, Default)]
177#[cfg_attr(
178    feature = "bevy_reflect",
179    derive(Reflect),
180    reflect(Debug, Resource, Clone)
181)]
182pub struct InputFocusVisible(pub bool);
183
184/// A bubble-able user input event that starts at the currently focused entity.
185///
186/// This event is normally dispatched to the current input focus entity, if any.
187/// If no entity has input focus, then the event is dispatched to the main window.
188///
189/// To set up your own bubbling input event, add the [`dispatch_focused_input::<MyEvent>`](dispatch_focused_input) system to your app,
190/// in the [`InputFocusSystems::Dispatch`] system set during [`PreUpdate`].
191#[derive(EntityEvent, Clone, Debug, Component)]
192#[entity_event(propagate = WindowTraversal, auto_propagate)]
193#[cfg_attr(
194    feature = "bevy_reflect",
195    derive(Reflect),
196    reflect(Event, Component, Clone)
197)]
198pub struct FocusedInput<M: Message + Clone> {
199    /// The entity that has received focused input.
200    #[event_target]
201    pub focused_entity: Entity,
202    /// The underlying input message.
203    pub input: M,
204    /// The primary window entity.
205    window: Entity,
206}
207
208impl<M: Message + Clone> FocusedInput<M> {
209    /// Create a new focused input event.
210    pub fn new(focused_entity: Entity, input: M, window: Entity) -> Self {
211        Self {
212            focused_entity,
213            input,
214            window,
215        }
216    }
217}
218
219/// An event which is used to set input focus. Trigger this on an entity, and it will bubble
220/// until it finds a focusable entity, and then set focus to it.
221#[derive(EntityEvent, Debug, Clone)]
222#[entity_event(propagate = WindowTraversal, auto_propagate)]
223#[cfg_attr(
224    feature = "bevy_reflect",
225    derive(Reflect),
226    reflect(Event, Clone, Debug)
227)]
228pub struct AcquireFocus {
229    /// The entity that has acquired focus.
230    #[event_target]
231    pub focused_entity: Entity,
232    /// The primary window entity.
233    pub window: Entity,
234}
235
236#[derive(QueryData)]
237/// These are for accessing components defined on the targeted entity
238pub struct WindowTraversal {
239    child_of: Option<&'static ChildOf>,
240    window: Option<&'static Window>,
241}
242
243impl<M: Message + Clone> Traversal<FocusedInput<M>> for WindowTraversal {
244    fn traverse(item: Self::Item<'_, '_>, event: &FocusedInput<M>) -> Option<Entity> {
245        let WindowTraversalItem { child_of, window } = item;
246
247        // Send event to parent, if it has one.
248        if let Some(child_of) = child_of {
249            return Some(child_of.parent());
250        };
251
252        // Otherwise, send it to the window entity (unless this is a window entity).
253        if window.is_none() {
254            return Some(event.window);
255        }
256
257        None
258    }
259}
260
261impl Traversal<AcquireFocus> for WindowTraversal {
262    fn traverse(item: Self::Item<'_, '_>, event: &AcquireFocus) -> Option<Entity> {
263        let WindowTraversalItem { child_of, window } = item;
264
265        // Send event to parent, if it has one.
266        if let Some(child_of) = child_of {
267            return Some(child_of.parent());
268        };
269
270        // Otherwise, send it to the window entity (unless this is a window entity).
271        if window.is_none() {
272            return Some(event.window);
273        }
274
275        None
276    }
277}
278
279/// Observer which clears input focus when an [`AcquireFocus`] request bubbles all the way up
280/// to a [`Window`] without finding a focusable target.
281///
282/// This is the generalized half of focus acquisition: it has no knowledge of any specific
283/// focus-navigation scheme. It is what makes "click outside to unfocus" work even when no
284/// navigation framework (e.g. [`tab_navigation`]) is installed — an [`AcquireFocus`] triggered
285/// on a non-focusable entity bubbles via [`WindowTraversal`] until it reaches the window, where
286/// this observer clears focus. Actually *focusing* a target is the responsibility of a
287/// navigation module: for tab navigation, that is
288/// [`acquire_focus_tab_index`](tab_navigation::acquire_focus_tab_index), which stops the request
289/// (and focuses the target) before it can reach the window.
290///
291/// Clearing focus relies on the request actually *reaching* the window via [`WindowTraversal`].
292/// That traversal walks up [`ChildOf`] relationships, and — as a fallback — routes to
293/// [`AcquireFocus::window`] once it runs off the end of the ancestor chain. Because of this
294/// window fallback, the request target does *not* need to be a descendant of the window entity:
295/// any non-window entity eventually reaches the window, where focus is cleared.
296///
297/// The `focus.get()` guard avoids spurious mutations so change detection only fires on real changes.
298/// Verify against the `acquire_focus_*` tests before altering any of this.
299pub fn on_window_acquire_focus_clear(
300    mut acquire_focus: On<AcquireFocus>,
301    windows: Query<(), With<Window>>,
302    mut focus: ResMut<InputFocus>,
303) {
304    if windows.contains(acquire_focus.focused_entity) {
305        // Stop and clear focus
306        acquire_focus.propagate(false);
307        // Don't mutate unless we need to, for change detection
308        if focus.get().is_some() {
309            focus.clear();
310        }
311    }
312}
313
314/// Plugin which sets up the core input focus system.
315///
316/// This includes the[`InputFocus`] and [`InputFocusVisible`] resources,
317/// [`set_initial_focus`] system to initialize the focus to the primary window, if any, at startup,
318/// and [`process_recorded_focus_changes`] to send [`FocusGained`] and [`FocusLost`] events when the focused entity changes.
319#[derive(Default)]
320pub struct InputFocusPlugin;
321
322impl Plugin for InputFocusPlugin {
323    fn build(&self, app: &mut App) {
324        app.add_systems(PostStartup, set_initial_focus)
325            .init_resource::<InputFocus>()
326            .init_resource::<InputFocusVisible>()
327            .add_observer(on_window_acquire_focus_clear)
328            .add_systems(
329                PostUpdate,
330                process_recorded_focus_changes.in_set(InputFocusSystems::FocusChangeEvents),
331            );
332    }
333}
334
335/// Plugin which sets up systems for dispatching bubbling keyboard and gamepad button events to the focused entity.
336///
337/// To add bubbling to your own input events, add the [`dispatch_focused_input::<MyEvent>`](dispatch_focused_input) system to your app,
338/// as described in the docs for [`FocusedInput`].
339#[derive(Default)]
340pub struct InputDispatchPlugin;
341
342impl Plugin for InputDispatchPlugin {
343    fn build(&self, app: &mut App) {
344        #[cfg(not(any(feature = "keyboard", feature = "gamepad", feature = "mouse")))]
345        let _ = app;
346        #[cfg(any(feature = "keyboard", feature = "gamepad", feature = "mouse"))]
347        app.add_systems(
348            PreUpdate,
349            (
350                #[cfg(feature = "keyboard")]
351                dispatch_focused_input::<KeyboardInput>,
352                #[cfg(feature = "gamepad")]
353                dispatch_focused_input::<GamepadButtonChangedEvent>,
354                #[cfg(feature = "mouse")]
355                dispatch_focused_input::<MouseWheel>,
356            )
357                .chain()
358                .in_set(InputFocusSystems::Dispatch)
359                .after(bevy_input::InputSystems),
360        );
361    }
362}
363
364/// System sets for [`bevy_input_focus`](crate).
365///
366/// These systems run in the [`PreUpdate`] schedule.
367#[derive(SystemSet, Debug, PartialEq, Eq, Hash, Clone)]
368pub enum InputFocusSystems {
369    /// System which dispatches bubbled input events to the focused entity, or to the primary window.
370    ///
371    /// Occurs in the [`PreUpdate`] schedule, after [`InputSystems`](bevy_input::InputSystems).
372    Dispatch,
373    /// System which processes recorded focus changes and sends the appropriate [`FocusGained`] and [`FocusLost`] events.
374    ///
375    /// Occurs in the [`PostUpdate`] schedule.
376    FocusChangeEvents,
377}
378
379/// If no entity is focused, sets the focus to the primary window, if any.
380pub fn set_initial_focus(
381    mut input_focus: ResMut<InputFocus>,
382    window: Single<Entity, With<PrimaryWindow>>,
383) {
384    if input_focus.get().is_none() {
385        input_focus.set(*window, FocusCause::Navigated);
386    }
387}
388
389/// System which dispatches bubbled input events to the focused entity, or to the primary window
390/// if no entity has focus.
391///
392/// If the currently focused entity no longer exists (has been despawned), this system will
393/// automatically clear the focus and dispatch events to the primary window instead.
394pub fn dispatch_focused_input<M: Message + Clone>(
395    mut input_reader: MessageReader<M>,
396    mut focus: ResMut<InputFocus>,
397    windows: Query<Entity, With<PrimaryWindow>>,
398    entities: &Entities,
399    mut commands: Commands,
400) {
401    if let Ok(window) = windows.single() {
402        // If an element has keyboard focus, then dispatch the input event to that element.
403        if let Some(focused_entity) = focus.get() {
404            // Check if the focused entity is still alive
405            if entities.contains(focused_entity) {
406                for ev in input_reader.read() {
407                    commands.trigger(FocusedInput {
408                        focused_entity,
409                        input: ev.clone(),
410                        window,
411                    });
412                }
413            } else {
414                // If the focused entity no longer exists, clear focus and dispatch to window
415                focus.clear();
416                for ev in input_reader.read() {
417                    commands.trigger(FocusedInput {
418                        focused_entity: window,
419                        input: ev.clone(),
420                        window,
421                    });
422                }
423            }
424        } else {
425            // If no element has input focus, then dispatch the input event to the primary window.
426            // There should be only one primary window.
427            for ev in input_reader.read() {
428                commands.trigger(FocusedInput {
429                    focused_entity: window,
430                    input: ev.clone(),
431                    window,
432                });
433            }
434        }
435    }
436}
437
438/// Trait which defines methods to check if an entity currently has focus.
439///
440/// This is implemented for [`World`] and [`IsFocusedHelper`].
441/// [`DeferredWorld`](bevy_ecs::world::DeferredWorld) indirectly implements it through [`Deref`].
442///
443/// For use within systems, use [`IsFocusedHelper`].
444///
445/// Modify the [`InputFocus`] resource to change the focused entity.
446///
447/// [`Deref`]: std::ops::Deref
448pub trait IsFocused {
449    /// Returns true if the given entity has input focus.
450    fn is_focused(&self, entity: Entity) -> bool;
451
452    /// Returns true if the given entity or any of its descendants has input focus.
453    ///
454    /// Note that for unusual layouts, the focus may not be within the entity's visual bounds.
455    fn is_focus_within(&self, entity: Entity) -> bool;
456
457    /// Returns true if the given entity has input focus and the focus indicator should be visible.
458    fn is_focus_visible(&self, entity: Entity) -> bool;
459
460    /// Returns true if the given entity, or any descendant, has input focus and the focus
461    /// indicator should be visible.
462    fn is_focus_within_visible(&self, entity: Entity) -> bool;
463}
464
465/// A system param that helps get information about the current focused entity.
466///
467/// When working with the entire [`World`], consider using the [`IsFocused`] instead.
468#[derive(SystemParam)]
469pub struct IsFocusedHelper<'w, 's> {
470    parent_query: Query<'w, 's, &'static ChildOf>,
471    input_focus: Option<Res<'w, InputFocus>>,
472    input_focus_visible: Option<Res<'w, InputFocusVisible>>,
473}
474
475impl IsFocused for IsFocusedHelper<'_, '_> {
476    fn is_focused(&self, entity: Entity) -> bool {
477        self.input_focus
478            .as_deref()
479            .and_then(InputFocus::get)
480            .is_some_and(|e| e == entity)
481    }
482
483    fn is_focus_within(&self, entity: Entity) -> bool {
484        let Some(focus) = self.input_focus.as_deref().and_then(InputFocus::get) else {
485            return false;
486        };
487        if focus == entity {
488            return true;
489        }
490        self.parent_query.iter_ancestors(focus).any(|e| e == entity)
491    }
492
493    fn is_focus_visible(&self, entity: Entity) -> bool {
494        self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focused(entity)
495    }
496
497    fn is_focus_within_visible(&self, entity: Entity) -> bool {
498        self.input_focus_visible.as_deref().is_some_and(|vis| vis.0) && self.is_focus_within(entity)
499    }
500}
501
502impl IsFocused for World {
503    fn is_focused(&self, entity: Entity) -> bool {
504        self.get_resource::<InputFocus>()
505            .and_then(InputFocus::get)
506            .is_some_and(|f| f == entity)
507    }
508
509    fn is_focus_within(&self, entity: Entity) -> bool {
510        let Some(focus) = self.get_resource::<InputFocus>().and_then(InputFocus::get) else {
511            return false;
512        };
513        let mut e = focus;
514        loop {
515            if e == entity {
516                return true;
517            }
518            if let Some(parent) = self.entity(e).get::<ChildOf>().map(ChildOf::parent) {
519                e = parent;
520            } else {
521                return false;
522            }
523        }
524    }
525
526    fn is_focus_visible(&self, entity: Entity) -> bool {
527        self.get_resource::<InputFocusVisible>()
528            .is_some_and(|vis| vis.0)
529            && self.is_focused(entity)
530    }
531
532    fn is_focus_within_visible(&self, entity: Entity) -> bool {
533        self.get_resource::<InputFocusVisible>()
534            .is_some_and(|vis| vis.0)
535            && self.is_focus_within(entity)
536    }
537}
538
539#[cfg(test)]
540mod tests {
541    use super::*;
542
543    use alloc::string::String;
544    use bevy_app::Startup;
545    use bevy_ecs::{observer::On, system::RunSystemOnce, world::DeferredWorld};
546    use bevy_input::{
547        keyboard::{Key, KeyCode},
548        ButtonState, InputPlugin,
549    };
550
551    #[derive(Component, Default)]
552    struct GatherKeyboardEvents(String);
553
554    fn gather_keyboard_events(
555        event: On<FocusedInput<KeyboardInput>>,
556        mut query: Query<&mut GatherKeyboardEvents>,
557    ) {
558        if let Ok(mut gather) = query.get_mut(event.focused_entity)
559            && let Key::Character(c) = &event.input.logical_key
560        {
561            gather.0.push_str(c.as_str());
562        }
563    }
564
565    fn key_a_message(window: Entity) -> KeyboardInput {
566        KeyboardInput {
567            key_code: KeyCode::KeyA,
568            logical_key: Key::Character("A".into()),
569            state: ButtonState::Pressed,
570            text: Some("A".into()),
571            repeat: false,
572            window,
573        }
574    }
575
576    #[test]
577    fn test_no_panics_if_resource_missing() {
578        let mut app = App::new();
579        // Note that we do not insert InputFocus here!
580
581        let entity = app.world_mut().spawn_empty().id();
582
583        assert!(!app.world().is_focused(entity));
584
585        app.world_mut()
586            .run_system_once(move |helper: IsFocusedHelper| {
587                assert!(!helper.is_focused(entity));
588                assert!(!helper.is_focus_within(entity));
589                assert!(!helper.is_focus_visible(entity));
590                assert!(!helper.is_focus_within_visible(entity));
591            })
592            .unwrap();
593
594        app.world_mut()
595            .run_system_once(move |world: DeferredWorld| {
596                assert!(!world.is_focused(entity));
597                assert!(!world.is_focus_within(entity));
598                assert!(!world.is_focus_visible(entity));
599                assert!(!world.is_focus_within_visible(entity));
600            })
601            .unwrap();
602    }
603
604    #[test]
605    fn initial_focus_unset_if_no_primary_window() {
606        let mut app = App::new();
607        app.add_plugins((InputPlugin, InputFocusPlugin));
608
609        app.update();
610
611        assert_eq!(app.world().resource::<InputFocus>().get(), None);
612    }
613
614    #[test]
615    fn initial_focus_set_to_primary_window() {
616        let mut app = App::new();
617        app.add_plugins((InputPlugin, InputFocusPlugin));
618
619        let entity_window = app
620            .world_mut()
621            .spawn((Window::default(), PrimaryWindow))
622            .id();
623        app.update();
624
625        assert_eq!(
626            app.world().resource::<InputFocus>().get(),
627            Some(entity_window)
628        );
629    }
630
631    #[test]
632    fn initial_focus_not_overridden() {
633        let mut app = App::new();
634        app.add_plugins((InputPlugin, InputFocusPlugin));
635
636        app.world_mut().spawn((Window::default(), PrimaryWindow));
637
638        app.add_systems(Startup, |mut commands: Commands| {
639            commands.spawn(AutoFocus);
640        });
641
642        app.update();
643
644        let autofocus_entity = app
645            .world_mut()
646            .query_filtered::<Entity, With<AutoFocus>>()
647            .single(app.world())
648            .unwrap();
649
650        assert_eq!(
651            app.world().resource::<InputFocus>().get(),
652            Some(autofocus_entity)
653        );
654    }
655
656    #[test]
657    fn test_keyboard_events() {
658        fn get_gathered(app: &App, entity: Entity) -> &str {
659            app.world()
660                .entity(entity)
661                .get::<GatherKeyboardEvents>()
662                .unwrap()
663                .0
664                .as_str()
665        }
666
667        let mut app = App::new();
668
669        app.add_plugins((InputPlugin, InputFocusPlugin, InputDispatchPlugin))
670            .add_observer(gather_keyboard_events);
671
672        let window = app
673            .world_mut()
674            .spawn((Window::default(), PrimaryWindow))
675            .id();
676
677        // Run the world for a single frame to set up the initial focus
678        app.update();
679
680        let entity_a = app
681            .world_mut()
682            .spawn((GatherKeyboardEvents::default(), AutoFocus))
683            .id();
684
685        let child_of_b = app
686            .world_mut()
687            .spawn((GatherKeyboardEvents::default(),))
688            .id();
689
690        let entity_b = app
691            .world_mut()
692            .spawn((GatherKeyboardEvents::default(),))
693            .add_child(child_of_b)
694            .id();
695
696        assert!(app.world().is_focused(entity_a));
697        assert!(!app.world().is_focused(entity_b));
698        assert!(!app.world().is_focused(child_of_b));
699        assert!(!app.world().is_focus_visible(entity_a));
700        assert!(!app.world().is_focus_visible(entity_b));
701        assert!(!app.world().is_focus_visible(child_of_b));
702
703        // entity_a should receive this event
704        app.world_mut().write_message(key_a_message(window));
705        app.update();
706
707        assert_eq!(get_gathered(&app, entity_a), "A");
708        assert_eq!(get_gathered(&app, entity_b), "");
709        assert_eq!(get_gathered(&app, child_of_b), "");
710
711        app.world_mut().insert_resource(InputFocus::default());
712
713        assert!(!app.world().is_focused(entity_a));
714        assert!(!app.world().is_focus_visible(entity_a));
715
716        // This event should be lost
717        app.world_mut().write_message(key_a_message(window));
718        app.update();
719
720        assert_eq!(get_gathered(&app, entity_a), "A");
721        assert_eq!(get_gathered(&app, entity_b), "");
722        assert_eq!(get_gathered(&app, child_of_b), "");
723
724        app.world_mut()
725            .insert_resource(InputFocus::from_entity(entity_b));
726        assert!(app.world().is_focused(entity_b));
727        assert!(!app.world().is_focused(child_of_b));
728
729        app.world_mut()
730            .run_system_once(move |mut input_focus: ResMut<InputFocus>| {
731                input_focus.set(child_of_b, FocusCause::Navigated);
732            })
733            .unwrap();
734        assert!(app.world().is_focus_within(entity_b));
735
736        // These events should be received by entity_b and child_of_b
737        app.world_mut()
738            .write_message_batch(core::iter::repeat_n(key_a_message(window), 4));
739        app.update();
740
741        assert_eq!(get_gathered(&app, entity_a), "A");
742        assert_eq!(get_gathered(&app, entity_b), "AAAA");
743        assert_eq!(get_gathered(&app, child_of_b), "AAAA");
744
745        app.world_mut().resource_mut::<InputFocusVisible>().0 = true;
746
747        app.world_mut()
748            .run_system_once(move |helper: IsFocusedHelper| {
749                assert!(!helper.is_focused(entity_a));
750                assert!(!helper.is_focus_within(entity_a));
751                assert!(!helper.is_focus_visible(entity_a));
752                assert!(!helper.is_focus_within_visible(entity_a));
753
754                assert!(!helper.is_focused(entity_b));
755                assert!(helper.is_focus_within(entity_b));
756                assert!(!helper.is_focus_visible(entity_b));
757                assert!(helper.is_focus_within_visible(entity_b));
758
759                assert!(helper.is_focused(child_of_b));
760                assert!(helper.is_focus_within(child_of_b));
761                assert!(helper.is_focus_visible(child_of_b));
762                assert!(helper.is_focus_within_visible(child_of_b));
763            })
764            .unwrap();
765
766        app.world_mut()
767            .run_system_once(move |world: DeferredWorld| {
768                assert!(!world.is_focused(entity_a));
769                assert!(!world.is_focus_within(entity_a));
770                assert!(!world.is_focus_visible(entity_a));
771                assert!(!world.is_focus_within_visible(entity_a));
772
773                assert!(!world.is_focused(entity_b));
774                assert!(world.is_focus_within(entity_b));
775                assert!(!world.is_focus_visible(entity_b));
776                assert!(world.is_focus_within_visible(entity_b));
777
778                assert!(world.is_focused(child_of_b));
779                assert!(world.is_focus_within(child_of_b));
780                assert!(world.is_focus_visible(child_of_b));
781                assert!(world.is_focus_within_visible(child_of_b));
782            })
783            .unwrap();
784    }
785
786    #[test]
787    fn dispatch_clears_focus_when_focused_entity_despawned() {
788        let mut app = App::new();
789        app.add_plugins((InputPlugin, InputFocusPlugin, InputDispatchPlugin));
790
791        let window = app
792            .world_mut()
793            .spawn((Window::default(), PrimaryWindow))
794            .id();
795        app.update();
796
797        let entity = app.world_mut().spawn_empty().id();
798        app.world_mut()
799            .insert_resource(InputFocus::from_entity(entity));
800        app.world_mut().entity_mut(entity).despawn();
801
802        assert_eq!(app.world().resource::<InputFocus>().get(), Some(entity));
803
804        // Send input event - this should clear focus instead of panicking
805        app.world_mut().write_message(key_a_message(window));
806        app.update();
807
808        assert_eq!(app.world().resource::<InputFocus>().get(), None);
809    }
810
811    /// Sets up an app with a primary window and the input-focus plugins, with initial focus resolved.
812    fn acquire_focus_app() -> (App, Entity) {
813        let mut app = App::new();
814        app.add_plugins((InputPlugin, InputFocusPlugin));
815        let window = app
816            .world_mut()
817            .spawn((Window::default(), PrimaryWindow))
818            .id();
819        // Resolve initial focus (focus goes to the primary window).
820        app.update();
821        (app, window)
822    }
823
824    #[test]
825    fn acquire_focus_clears_focus_when_reaching_window() {
826        let (mut app, window) = acquire_focus_app();
827
828        // Start with some entity focused.
829        let previously_focused = app.world_mut().spawn_empty().id();
830        app.world_mut()
831            .insert_resource(InputFocus::from_entity(previously_focused));
832
833        // Click away onto a non-focusable child of the window: the request bubbles up through
834        // `WindowTraversal` to the window, which clears focus. This is the behavior that
835        // "click outside to unfocus" depends on, and it works without any navigation framework
836        // (e.g. `tab_navigation`) installed.
837        let non_focusable = app.world_mut().spawn(ChildOf(window)).id();
838        app.world_mut().trigger(AcquireFocus {
839            focused_entity: non_focusable,
840            window,
841        });
842        app.update();
843
844        assert_eq!(app.world().resource::<InputFocus>().get(), None);
845    }
846}