bevy_ui_navigation/
lib.rs

1/*!
2[`ButtonBundle`]: bevy::prelude::ButtonBundle
3[Changed]: bevy::prelude::Changed
4[doc-root]: ./index.html
5[`Entity`]: bevy::prelude::Entity
6[entity-id]: bevy::ecs::system::EntityCommands::id
7[`FocusableButtonBundle`]: components::FocusableButtonBundle
8[`Focusable::cancel`]: resolve::Focusable::cancel
9[`Focusable::block`]: resolve::Focusable::block
10[`Focusable::dormant`]: resolve::Focusable::dormant
11[`Focusable`]: resolve::Focusable
12[`Focusable::lock`]: resolve::Focusable::lock
13[`generic_default_mouse_input`]: systems::generic_default_mouse_input
14[`InputMapping`]: systems::InputMapping
15[`InputMapping::keyboard_navigation`]: systems::InputMapping::keyboard_navigation
16[module-event_helpers]: events::NavEventReaderExt
17[module-marking]: mark
18[module-systems]: systems
19[Name]: bevy::core::Name
20[`NavEvent::FocusChanged`]: events::NavEvent::FocusChanged
21[`NavEvent`]: events::NavEvent
22[`NavEvent::InitiallyFocused`]: events::NavEvent::InitiallyFocused
23[`MenuSetting`]: menu::MenuSetting
24[`NavMenu`]: menu::MenuSetting
25[`MenuBuilder`]: menu::MenuBuilder
26[MenuBuilder::reachable_from]: menu::MenuBuilder::EntityParent
27[MenuBuilder::reachable_from_named]: menu::MenuBuilder::from_named
28[`NavRequest`]: events::NavRequest
29[`NavRequest::Action`]: events::NavRequest::Action
30[`NavRequest::FocusOn`]: events::NavRequest::FocusOn
31[`NavRequest::Free`]: events::NavRequest::Unlock
32[`NavRequest::Unlock`]: events::NavRequest::Unlock
33[`NavRequest::ScopeMove`]: events::NavRequest::ScopeMove
34[`NavRequestSystem`]: NavRequestSystem
35*/
36#![doc = include_str!("../Readme.md")]
37#![forbid(missing_docs)]
38#![allow(clippy::unnecessary_lazy_evaluations)]
39
40mod commands;
41#[cfg(feature = "bevy_ui")]
42pub mod components;
43#[cfg(feature = "cuicui_dsl")]
44mod dsl;
45pub mod events;
46mod marker;
47pub mod menu;
48mod named;
49mod resolve;
50pub mod systems;
51
52use std::marker::PhantomData;
53
54use bevy::ecs::system::{SystemParam, SystemParamItem};
55use bevy::prelude::*;
56
57pub use non_empty_vec::NonEmpty;
58
59#[cfg(feature = "bevy_ui")]
60use resolve::UiProjectionQuery;
61
62/// Default imports for `bevy_ui_navigation`.
63pub mod prelude {
64    #[cfg(feature = "cuicui_dsl")]
65    pub use crate::dsl::NavigationDsl;
66    pub use crate::events::{NavEvent, NavEventReaderExt, NavRequest};
67    pub use crate::menu::{MenuBuilder, MenuSetting};
68    pub use crate::resolve::{
69        FocusAction, FocusState, Focusable, Focused, MenuNavigationStrategy, NavLock,
70    };
71    pub use crate::NavRequestSystem;
72    #[cfg(feature = "bevy_ui")]
73    pub use crate::{DefaultNavigationPlugins, NavigationPlugin};
74}
75/// Utilities to mark focusables within a menu with a specific component.
76pub mod mark {
77    pub use crate::menu::NavMarker;
78    pub use crate::NavMarkerPropagationPlugin;
79}
80/// Types useful to define your own custom navigation inputs.
81pub mod custom {
82    #[cfg(feature = "bevy_ui")]
83    pub use crate::resolve::UiProjectionQuery;
84    pub use crate::resolve::{Rect, ScreenBoundaries};
85    pub use crate::GenericNavigationPlugin;
86}
87
88/// Plugin for menu marker propagation.
89///
90/// For a marker of type `T` to be propagated when using
91/// [`mark::NavMarker`], you need to add a
92/// `NavMarkerPropagationPlugin<T>` to your bevy app. It is possible to add any
93/// amount of `NavMarkerPropagationPlugin<T>` for as many `T` you need to
94/// propagate through the menu system.
95pub struct NavMarkerPropagationPlugin<T>(PhantomData<T>);
96impl<T> NavMarkerPropagationPlugin<T> {
97    #[allow(clippy::new_without_default)]
98    /// Create a new [`NavMarkerPropagationPlugin`].
99    pub fn new() -> Self {
100        NavMarkerPropagationPlugin(PhantomData)
101    }
102}
103
104impl<T: 'static + Sync + Send + Component + Clone> Plugin for NavMarkerPropagationPlugin<T> {
105    fn build(&self, app: &mut App) {
106        app.add_systems(
107            Update,
108            (
109                marker::mark_new_menus::<T>,
110                marker::mark_new_focusables::<T>,
111            ),
112        );
113    }
114}
115
116/// The label of the system in which the [`NavRequest`] events are handled, the
117/// focus state of the [`Focusable`]s is updated and the [`NavEvent`] events
118/// are sent.
119///
120/// Systems updating visuals of UI elements should run _after_ the `NavRequestSystem`,
121/// while systems that emit [`NavRequest`] should run _before_ it.
122/// For example, an input system should run before the `NavRequestSystem`.
123///
124/// Failing to do so won't cause logical errors, but will make the UI feel more slugish
125/// than necessary. This is especially critical of you are running on low framerate.
126///
127/// # Example
128///
129/// ```rust, no_run
130/// use bevy_ui_navigation::prelude::*;
131/// use bevy_ui_navigation::events::Direction;
132/// use bevy_ui_navigation::custom::GenericNavigationPlugin;
133/// use bevy::prelude::*;
134/// # use std::marker::PhantomData;
135/// # use bevy::ecs::system::SystemParam;
136/// # #[derive(SystemParam)] struct MoveCursor3d<'w, 's> {
137/// #   #[system_param(ignore)] _foo: PhantomData<(&'w (), &'s ())>
138/// # }
139/// # impl<'w, 's> MenuNavigationStrategy for MoveCursor3d<'w, 's> {
140/// #   fn resolve_2d<'a>(
141/// #       &self,
142/// #       focused: Entity,
143/// #       direction: Direction,
144/// #       cycles: bool,
145/// #       siblings: &'a [Entity],
146/// #   ) -> Option<&'a Entity> { None }
147/// # }
148/// # fn button_system() {}
149/// fn main() {
150///     App::new()
151///         .add_plugins(GenericNavigationPlugin::<MoveCursor3d>::new())
152///         // ...
153///         // Add the button color update system after the focus update system
154///         .add_systems(Update, button_system.after(NavRequestSystem))
155///         // ...
156///         .run();
157/// }
158/// ```
159///
160/// [`NavRequest`]: prelude::NavRequest
161/// [`NavEvent`]: prelude::NavEvent
162/// [`Focusable`]: prelude::Focusable
163#[derive(Clone, Debug, Hash, PartialEq, Eq, SystemSet)]
164pub struct NavRequestSystem;
165
166/// The navigation plugin.
167///
168/// Add it to your app with `.add_plugins(NavigationPlugin::new())` and send
169/// [`NavRequest`]s to move focus within declared [`Focusable`] entities.
170///
171/// You should prefer `bevy_ui` provided defaults
172/// if you don't want to bother with that.
173///
174/// # Note on generic parameters
175///
176/// The `STGY` type parameter might seem complicated, but all you have to do
177/// is for your type to implement [`SystemParam`] and [`MenuNavigationStrategy`].
178///
179/// [`MenuNavigationStrategy`]: resolve::MenuNavigationStrategy
180/// [`Focusable`]: prelude::Focusable
181/// [`NavRequest`]: prelude::NavRequest
182#[derive(Default)]
183pub struct GenericNavigationPlugin<STGY>(PhantomData<fn() -> STGY>);
184#[cfg(feature = "bevy_ui")]
185/// A default [`GenericNavigationPlugin`] for `bevy_ui`.
186pub type NavigationPlugin<'w, 's> = GenericNavigationPlugin<UiProjectionQuery<'w, 's>>;
187
188impl<STGY: resolve::MenuNavigationStrategy> GenericNavigationPlugin<STGY> {
189    /// Create a new [`GenericNavigationPlugin`] with the provided `STGY`,
190    /// see also [`resolve::MenuNavigationStrategy`].
191    pub fn new() -> Self {
192        Self(PhantomData)
193    }
194}
195impl<STGY: SystemParam + 'static> Plugin for GenericNavigationPlugin<STGY>
196where
197    for<'w, 's> SystemParamItem<'w, 's, STGY>: resolve::MenuNavigationStrategy,
198{
199    fn build(&self, app: &mut App) {
200        #[cfg(feature = "bevy_reflect")]
201        app.register_type::<menu::MenuBuilder>()
202            .register_type::<menu::MenuSetting>()
203            .register_type::<resolve::Focusable>()
204            .register_type::<resolve::FocusAction>()
205            .register_type::<resolve::FocusState>()
206            .register_type::<resolve::LockReason>()
207            .register_type::<resolve::NavLock>()
208            .register_type::<resolve::Rect>()
209            .register_type::<resolve::ScreenBoundaries>()
210            .register_type::<resolve::TreeMenu>()
211            .register_type::<systems::InputMapping>();
212
213        app.add_event::<events::NavRequest>()
214            .add_event::<events::NavEvent>()
215            .insert_resource(resolve::NavLock::new())
216            .add_systems(
217                Update,
218                (
219                    (resolve::set_first_focused, resolve::consistent_menu),
220                    resolve::listen_nav_requests::<STGY>.in_set(NavRequestSystem),
221                )
222                    .chain(),
223            )
224            .add_systems(
225                PreUpdate,
226                (named::resolve_named_menus, resolve::insert_tree_menus).chain(),
227            );
228    }
229}
230
231/// The navigation plugin and the default input scheme.
232///
233/// Add it to your app with `.add_plugins(DefaultNavigationPlugins)`.
234///
235/// This provides default implementations for input handling, if you want
236/// your own custom input handling, you should use [`NavigationPlugin`] and
237/// provide your own input handling systems.
238#[cfg(feature = "bevy_ui")]
239pub struct DefaultNavigationPlugins;
240#[cfg(feature = "bevy_ui")]
241impl PluginGroup for DefaultNavigationPlugins {
242    fn build(self) -> bevy::app::PluginGroupBuilder {
243        bevy::app::PluginGroupBuilder::start::<Self>()
244            .add(NavigationPlugin::new())
245            .add(systems::DefaultNavigationSystems)
246    }
247}
248
249#[cfg(test)]
250mod test {
251    use crate::prelude::*;
252    use bevy::{
253        ecs::{event::Event, world::EntityWorldMut},
254        prelude::*,
255    };
256
257    use super::*;
258    // Why things might fail?
259    // -> State becomes inconsistent, assumptions are broken
260    // How would assumptions be broken?
261    // -> The ECS hierarchy changed under our feet
262    // -> state was modified by users and we didn't expect it
263    // -> internal state is not updated correctly to reflect the actual state
264    // Consistency design:
265    // - Strong dependency on bevy hierarchy not being mucked with
266    //   (doesn't handle changed parents well)
267    // - Need to get rid of TreeMenu::active_child probably
268    // - Possible to "check and fix" the state in a system that accepts
269    //   Changed<Parent> + RemovedComponent<Focusable | TreeMenu | Parent>
270    // - But the check cannot anticipate when the hierarchy is changed,
271    //   so we are doomed to expose to users inconsistent states
272    //   -> implication: we don't need to maintain it in real time, since
273    //      after certain hierarchy manipulations, it will be inconsistent either way.
274    //      So we could do with only checking and updating when receiving
275    //      NavRequest (sounds like good use case for system chaining)
276
277    /// Define a menu structure to spawn.
278    ///
279    /// This just describes the menu structure,  use [`SpawnHierarchy::spawn`],
280    /// to spawn the entities in the world,.
281    enum SpawnHierarchy {
282        Rootless(SpawnRootless),
283        Menu(SpawnMenu),
284    }
285    impl SpawnHierarchy {
286        fn spawn(self, world: &mut World) {
287            match self {
288                Self::Rootless(menu) => menu.spawn(world),
289                Self::Menu(menu) => menu.spawn(&mut world.spawn_empty()),
290            };
291        }
292    }
293    struct SpawnFocusable {
294        name: &'static str,
295        prioritized: bool,
296        child_menu: Option<SpawnMenu>,
297    }
298
299    impl SpawnFocusable {
300        fn spawn(self, mut entity: EntityWorldMut) {
301            let SpawnFocusable {
302                name,
303                prioritized,
304                child_menu,
305            } = self;
306            entity.insert(Name::new(name));
307            let focusable = if prioritized {
308                Focusable::new().prioritized()
309            } else {
310                Focusable::new()
311            };
312            entity.insert(focusable);
313            if let Some(child_menu) = child_menu {
314                // SAFETY: we do not call any methods on `entity` after `world_mut()`
315                unsafe {
316                    child_menu.spawn(&mut entity.world_mut().spawn_empty());
317                };
318                std::mem::drop(entity);
319            }
320        }
321    }
322    struct SpawnMenu {
323        name: &'static str,
324        children: Vec<SpawnFocusable>,
325    }
326    impl SpawnMenu {
327        fn spawn(self, entity: &mut EntityWorldMut) {
328            let SpawnMenu { name, children } = self;
329            let parent_focusable = name.strip_suffix(" Menu");
330            let menu_builder = match parent_focusable {
331                Some(name) => MenuBuilder::from_named(name),
332                None => MenuBuilder::Root,
333            };
334            entity.insert((Name::new(name), menu_builder, MenuSetting::new()));
335            entity.with_children(|commands| {
336                for child in children.into_iter() {
337                    child.spawn(commands.spawn_empty());
338                }
339            });
340        }
341    }
342    struct SpawnRootless {
343        focusables: Vec<SpawnFocusable>,
344    }
345    impl SpawnRootless {
346        fn spawn(self, world: &mut World) {
347            for focusable in self.focusables.into_iter() {
348                focusable.spawn(world.spawn_empty())
349            }
350        }
351    }
352    /// Define a `SpawnHierarchy`.
353    ///
354    /// Syntax:
355    /// - `spawn_hierarchy![ <focus_kind>, ... ]`:
356    ///   A hierarchy of focusable components with a root menu.
357    /// - `spawn_hierarchy!(@rootless [ <focus_kind> , ...] )`:
358    ///   A hierarchy of focusable components **without** a root menu.
359    /// - `<focus_kind>` is one of the following:
360    ///   - `focusable("Custom")`: a focusable with the `Name::new("Custom")` component
361    ///   - `focusable_to("Custom" [ <focus_kind> , ...] )`:
362    ///     a focusable with the `Name::new("Custom")` component, parent of a menu (`MenuBuilder`)
363    ///     marked with the `Name::new("Custom Menu")` component. The menu content is the
364    ///     content of the provided list
365    ///   - `prioritized("Custom")`: a focusable with the `Name::new("Custom")` component,
366    ///     spawned with `Focusable::new().prioritized()`.
367    macro_rules! spawn_hierarchy {
368        ( @rootless [ $( $elem_kind:ident $elem_args:tt ),* $(,)? ] ) => (
369            SpawnHierarchy::Rootless(SpawnRootless {
370                focusables: vec![ $(
371                    spawn_hierarchy!(@elem $elem_kind $elem_args),
372                )* ],
373            })
374        );
375        ( @menu $name:expr, $( $elem_name:ident $elem_args:tt ),* $(,)? ) => (
376            SpawnMenu {
377                name: $name,
378                children: vec![ $(
379                    spawn_hierarchy!(@elem $elem_name $elem_args),
380                )* ],
381            }
382        );
383        ( @elem prioritized ( $name:literal ) ) => (
384            SpawnFocusable {
385                name: $name,
386                prioritized: true,
387                child_menu: None,
388            }
389        );
390        ( @elem focusable ( $name:literal ) ) => (
391            SpawnFocusable {
392                name: $name,
393                prioritized: false,
394                child_menu: None,
395            }
396        );
397        ( @elem focusable_to ( $name:literal [ $( $submenu:tt )* ] ) ) => (
398            SpawnFocusable {
399                name: $name,
400                prioritized: false,
401                child_menu: Some( spawn_hierarchy!(@menu concat!( $name , " Menu"),  $( $submenu )* ) ),
402            }
403        );
404        ($( $elem_name:ident $elem_args:tt ),* $(,)? ) => (
405            SpawnHierarchy::Menu(spawn_hierarchy!(@menu "Root", $( $elem_name $elem_args ),*))
406        );
407    }
408
409    /// Assert identity of a list of entities by their `Name` component
410    /// (makes understanding test failures easier)
411    ///
412    /// This is a macro, so that when there is an assert failure or panic,
413    /// the line of code it points to is the calling site,
414    /// rather than the function body.
415    ///
416    /// There is nothing beside that that would prevent converting this into a function.
417    macro_rules! assert_expected_focus_change {
418        ($app:expr, $events:expr, $expected_from:expr, $expected_to:expr $(,)?) => {
419            if let [NavEvent::FocusChanged { to, from }] = $events {
420                let actual_from = $app.name_list(&*from);
421                assert_eq!(&*actual_from, $expected_from);
422
423                let actual_to = $app.name_list(&*to);
424                assert_eq!(&*actual_to, $expected_to);
425            } else {
426                panic!(
427                    "Expected a signle FocusChanged NavEvent, got: {:#?}",
428                    $events
429                );
430            }
431        };
432    }
433
434    // A navigation strategy that does nothing, useful for testing.
435    #[derive(SystemParam)]
436    struct MockNavigationStrategy<'w, 's> {
437        #[system_param(ignore)]
438        _f: PhantomData<fn() -> (&'w (), &'s ())>,
439    }
440    // Just to make the next `impl` block shorter, unused otherwise.
441    use events::Direction as D;
442    impl<'w, 's> MenuNavigationStrategy for MockNavigationStrategy<'w, 's> {
443        fn resolve_2d<'a>(&self, _: Entity, _: D, _: bool, _: &'a [Entity]) -> Option<&'a Entity> {
444            None
445        }
446    }
447    fn receive_events<E: Event + Clone>(world: &World) -> Vec<E> {
448        let events = world.resource::<Events<E>>();
449        events.iter_current_update_events().cloned().collect()
450    }
451
452    /// Wrapper around `App` to make it easier to test the navigation systems.
453    struct NavEcsMock {
454        app: App,
455    }
456    impl NavEcsMock {
457        fn currently_focused(&mut self) -> &str {
458            let mut query = self.app.world.query_filtered::<&Name, With<Focused>>();
459            &**query.iter(&self.app.world).next().unwrap()
460        }
461        fn kill_named(&mut self, to_kill: &str) -> Vec<NavEvent> {
462            let mut query = self.app.world.query::<(Entity, &Name)>();
463            let requested = query
464                .iter(&self.app.world)
465                .find_map(|(e, name)| (&**name == to_kill).then(|| e));
466            if let Some(to_kill) = requested {
467                self.app.world.despawn(to_kill);
468            }
469            self.app.update();
470            receive_events(&mut self.app.world)
471        }
472        fn name_list(&mut self, entity_list: &[Entity]) -> Vec<&str> {
473            let mut query = self.app.world.query::<&Name>();
474            entity_list
475                .iter()
476                .filter_map(|e| query.get(&self.app.world, *e).ok())
477                .map(|name| &**name)
478                .collect()
479        }
480        fn new(hierarchy: SpawnHierarchy) -> Self {
481            let mut app = App::new();
482            app.add_plugins(GenericNavigationPlugin::<MockNavigationStrategy>::new());
483            hierarchy.spawn(&mut app.world);
484            // Run once to convert the `MenuSetting` and `MenuBuilder` into `TreeMenu`.
485            app.update();
486
487            Self { app }
488        }
489        fn run_focus_on(&mut self, entity_name: &str) -> Vec<NavEvent> {
490            let mut query = self.app.world.query::<(Entity, &Name)>();
491            let requested = query
492                .iter(&self.app.world)
493                .find_map(|(e, name)| (&**name == entity_name).then(|| e))
494                .unwrap();
495            self.app.world.send_event(NavRequest::FocusOn(requested));
496            self.app.update();
497            receive_events(&mut self.app.world)
498        }
499        fn run_request(&mut self, request: NavRequest) -> Vec<NavEvent> {
500            self.app.world.send_event(request);
501            self.app.update();
502            receive_events(&mut self.app.world)
503        }
504        fn state_of(&mut self, requested: &str) -> FocusState {
505            let mut query = self.app.world.query::<(&Focusable, &Name)>();
506            let requested = query
507                .iter(&self.app.world)
508                .find_map(|(focus, name)| (&**name == requested).then(|| focus));
509            requested.unwrap().state()
510        }
511    }
512
513    // ====
514    // Expected basic functionalities
515    // ====
516
517    #[test]
518    fn move_in_menuless() {
519        let mut app = NavEcsMock::new(spawn_hierarchy!(@rootless [
520            prioritized("Initial"),
521            focusable("Left"),
522            focusable("Right"),
523        ]));
524        assert_eq!(app.currently_focused(), "Initial");
525        app.run_focus_on("Left");
526        assert_eq!(app.currently_focused(), "Left");
527    }
528
529    #[test]
530    fn deep_initial_focusable() {
531        let mut app = NavEcsMock::new(spawn_hierarchy![
532            focusable("Middle"),
533            focusable_to("Left" [
534                focusable("LCenter1"),
535                focusable("LCenter2"),
536                focusable_to("LTop" [
537                    prioritized("LTopForward"),
538                    focusable("LTopBackward"),
539                ]),
540                focusable("LCenter3"),
541                focusable("LBottom"),
542            ]),
543            focusable("Right"),
544        ]);
545        use FocusState::{Active, Inert};
546        assert_eq!(app.currently_focused(), "LTopForward");
547        assert_eq!(app.state_of("Left"), Active);
548        assert_eq!(app.state_of("Right"), Inert);
549        assert_eq!(app.state_of("Middle"), Inert);
550        assert_eq!(app.state_of("LTop"), Active);
551        assert_eq!(app.state_of("LCenter1"), Inert);
552        assert_eq!(app.state_of("LTopBackward"), Inert);
553    }
554
555    #[test]
556    fn move_in_complex_menu_hierarchy() {
557        let mut app = NavEcsMock::new(spawn_hierarchy![
558            prioritized("Initial"),
559            focusable_to("Left" [
560                focusable_to("LTop" [
561                    focusable("LTopForward"),
562                    focusable("LTopBackward"),
563                ]),
564                focusable_to("LBottom" [
565                    focusable("LBottomForward"),
566                    focusable("LBottomForward1"),
567                    focusable("LBottomForward2"),
568                    prioritized("LBottomBackward"),
569                    focusable("LBottomForward3"),
570                    focusable("LBottomForward4"),
571                    focusable("LBottomForward5"),
572                ]),
573            ]),
574            focusable_to("Right" [
575                focusable_to("RTop" [
576                    focusable("RTopForward"),
577                    focusable("RTopBackward"),
578                ]),
579                focusable("RBottom"),
580            ]),
581        ]);
582        assert_eq!(app.currently_focused(), "Initial");
583
584        // Move deep into a menu
585        let events = app.run_focus_on("RBottom");
586        assert_expected_focus_change!(app, &events[..], ["Initial"], ["RBottom", "Right"]);
587
588        // Go up and back down several layers of menus
589        let events = app.run_focus_on("LTopForward");
590        assert_expected_focus_change!(
591            app,
592            &events[..],
593            ["RBottom", "Right"],
594            ["LTopForward", "LTop", "Left"],
595        );
596        // See if cancel event works
597        let events = app.run_request(NavRequest::Cancel);
598        assert_expected_focus_change!(app, &events[..], ["LTopForward", "LTop"], ["LTop"]);
599
600        // Move to sibling within menu
601        let events = app.run_focus_on("LBottom");
602        assert_expected_focus_change!(app, &events[..], ["LTop"], ["LBottom"]);
603
604        // Move down into menu by activating a focusable
605        // (also make sure `prioritized` works)
606        let events = app.run_request(NavRequest::Action);
607        assert_expected_focus_change!(
608            app,
609            &events[..],
610            ["LBottom"],
611            ["LBottomBackward", "LBottom"]
612        );
613    }
614
615    // ====
616    // What happens when Focused element is killed
617    // ====
618
619    // Select a new focusable in the same menu (or anything if no menus exist)
620    #[test]
621    fn focus_rootless_kill_robust() {
622        let mut app = NavEcsMock::new(spawn_hierarchy!(@rootless [
623            prioritized("Initial"),
624            focusable("Right"),
625        ]));
626        assert_eq!(app.currently_focused(), "Initial");
627        app.kill_named("Initial");
628        assert_eq!(app.currently_focused(), "Right");
629
630        app.kill_named("Right");
631        let events = app.run_request(NavRequest::Action);
632        assert_eq!(events.len(), 0, "{:#?}", events);
633    }
634
635    // Go up the menu tree if it was the last focusable in the menu
636    // And swap to something in the same menu if focusable killed in it.
637    #[test]
638    fn menu_elem_kill_robust() {
639        let mut app = NavEcsMock::new(spawn_hierarchy![
640            focusable_to("Left" [
641                focusable("LTop"),
642                focusable("LBottom"),
643            ]),
644            focusable_to("Antony" [
645                prioritized("Caesar"),
646                focusable("Brutus"),
647            ]),
648            focusable_to("Octavian" [
649                focusable("RTop"),
650                focusable("RBottom"),
651            ]),
652        ]);
653        // NOTE: was broken because didn't properly set
654        // active_child and Active when initial focus was given to
655        // a deep element.
656        assert_eq!(app.currently_focused(), "Caesar");
657        assert_eq!(app.state_of("Antony"), FocusState::Active);
658        app.kill_named("Caesar");
659        assert_eq!(app.currently_focused(), "Brutus");
660        app.kill_named("Brutus");
661        assert_eq!(app.currently_focused(), "Antony");
662    }
663
664    // ====
665    // removal of parent menu and focusables
666    // ====
667
668    // Relink the child menu to the removed parent's parents
669    // Make sure this works with root as well
670    // Relink when the focusable parent of a menu is killed
671    // NOTE: user is warned against engaging in such operations, implementation can wait
672
673    // ====
674    // some reparenting potential problems
675    // ====
676
677    // Focused element is reparented to a new menu
678    // Active element is reparented to a new menu
679    // NOTE: those are not expected to work. Currently considered a user error.
680}