bevy_alt_ui_navigation_lite/
lib.rs

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