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}