Crate bevy_ui_navigation
source · [−]Expand description
Bevy UI navigation
A generic UI navigation algorithm for the Bevy engine default UI library.
[dependencies]
bevy-ui-navigation = "0.13.1"The in-depth design specification is available here.
Examples
Check out the examples directory for bevy examples.

Cargo Features
This crate exposes the bevy-ui feature. It is enabled by default. Toggling
off this feature let you compile this crate without requiring the bevy render
feature. But you won’t be able to use FocusableButtonBundle, and you’ll have
to use generic_default_mouse_input for mouse input and define special spacial
components to get it working.
Usage
See this example for a quick start guide.
The crate documentation is extensive, but for practical reason doesn’t include many examples. This page contains most of the doc examples, you should check the examples directory for examples showcasing all features of this crate.
Simple case
To create a simple menu with navigation between buttons, simply replace usages
of ButtonBundle
with FocusableButtonBundle.
You will need to create your own system to change the color of focused elements, and add manually the input systems, but with that setup you get: Complete physical position based navigation with controller, mouse and keyboard. Including rebindable mapping.
use bevy::prelude::*;
use bevy_ui_navigation::systems::{
default_gamepad_input, default_keyboard_input, default_mouse_input, InputMapping,
};
use bevy_ui_navigation::NavigationPlugin;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(NavigationPlugin)
.init_resource::<InputMapping>()
.add_system(default_keyboard_input)
.add_system(default_mouse_input)
.add_system(default_gamepad_input)
.run();
}Use the InputMapping
resource to change keyboard and gamepad button mapping.
Check the examples directory
for more example code.
To respond to relevant user input, for example when the player pressed the
“Action” button when focusing start_game_button, you should read the
NavEvent event queue:
use bevy::prelude::*;
use bevy_ui_navigation::{NavEvent, NavRequest};
struct Gameui {
start_game_button: Entity,
}
fn handle_nav_events(mut events: EventReader<NavEvent>, game: Res<Gameui>) {
use bevy_ui_navigation::{NavEvent::NoChanges, NavRequest::Action};
for event in events.iter() {
match event {
NoChanges { from, request: Action } if *from.first() == game.start_game_button => {
// Start the game on "A" or "ENTER" button press
}
_ => {}
}
}
}The focus navigation works across the whole UI tree, regardless of how or where you’ve put your focusable entities. You just move in the direction you want to go, and you get there.
Any Entity
can be converted into a focusable entity by adding the
Focusable
component to it. To do so, just:
fn system(mut cmds: Commands, my_entity: Entity) {
cmds.entity(my_entity).insert(Focusable::default());
}That’s it! Now my_entity is part of the navigation tree. The player can
select it with their controller the same way as any other
Focusable
element.
You probably want to render the focused button differently than other buttons,
this can be done with the
Changed<Focusable>
query parameter as follow:
use bevy::prelude::*;
use bevy_ui_navigation::{FocusState, Focusable};
fn button_system(
mut focusables: Query<(&Focusable, &mut UiColor), Changed<Focusable>>,
) {
for (focus, mut color) in focusables.iter_mut() {
let new_color = if let FocusState::Focused = focus.state() {
Color::RED
} else {
Color::BLACK
};
*color = new_color.into();
}
}More complex use cases
Locking
If you need to supress the navigation algorithm temporarily, you can declare a
Focusable as
Focusable::lock.
This is useful for example if you want to implement custom widget with their
own controls, or if you want to disable menu navigation while in game. To
resume the navigation system, you’ll need to send a
NavRequest::Free.
NavRequest::FocusOn
You can’t directly manipulate which entity is focused, because we need to keep
track of a lot of thing on the backend to make the navigation work as expected.
But you can set the focused element to any arbitrary Focusable entity with
NavRequest::FocusOn.
use bevy::prelude::*;
use bevy_ui_navigation::NavRequest;
fn set_focus_to_arbitrary_focusable(
entity: Entity,
mut requests: EventWriter<NavRequest>,
) {
requests.send(NavRequest::FocusOn(entity));
}NavMenus
Suppose you have a more complex game with menus sub-menus and sub-sub-menus etc. For example, in your everyday 2021 AAA game, to change the antialiasing you would go through a few menus:
game menu → options menu → graphics menu → custom graphics menu → AAIn this case, you need to be capable of specifying which button in the previous menu leads to the next menu (for example, you would press the “Options” button in the game menu to access the options menu).
For that, you need to use
NavMenu.
The high level usage of NavMenu is as follow:
- First you need a “root”
NavMenu. - You need to spawn into the ECS your “options” button with a
Focusablecomponent. To link the button to your options menu, you need to do one of the following:- Add a
Name("opt_btn_name")component in addition to theFocusablecomponent to your options button. - Pre-spawn the options button and store somewhere it’s
Entityid (let opt_btn = commands.spawn_bundle(FocusableButtonBundle).id();)
- Add a
- to the
NodeBundlecontaining all the options menuFocusableentities, you add the following bundle:NavMenu::Bound2d.reachable_from_named("opt_btn_name")if you opted for adding theNamecomponent.NavMenu::Bound2d.reachable_from(opt_btn)if you have theEntityid.
In code, This will look like this:
use bevy::prelude::*;
use bevy_ui_navigation::{Focusable, NavMenu};
use bevy_ui_navigation::components::FocusableButtonBundle;
struct SaveFile;
impl SaveFile {
fn bundle(&self) -> impl Bundle {
// UI bundle to show this in game
NodeBundle::default()
}
}
fn spawn_menu(mut cmds: Commands, save_files: Vec<SaveFile>) {
let menu_node = NodeBundle {
style: Style { flex_direction: FlexDirection::Column, ..Default::default()},
..Default::default()
};
let button = FocusableButtonBundle::from(ButtonBundle {
color: Color::rgb(1.0, 0.3, 1.0).into(),
..Default::default()
});
let mut spawn = |bundle: &FocusableButtonBundle, name: &'static str| {
cmds.spawn_bundle(bundle.clone()).insert(Name::new(name)).id()
};
let options = spawn(&button, "options");
let graphics_option = spawn(&button, "graphics");
let audio_options = spawn(&button, "audio");
let input_options = spawn(&button, "input");
let game = spawn(&button, "game");
let quit = spawn(&button, "quit");
let load = spawn(&button, "load");
// Spawn the game menu
cmds.spawn_bundle(menu_node.clone())
// Root NavMenu vvvvvvvvvvvvvv
.insert_bundle(NavMenu::Bound2d.root())
.push_children(&[options, game, quit, load]);
// Spawn the load menu
cmds.spawn_bundle(menu_node.clone())
// Sub menu accessible through the load button
// vvvvvvvvvvvvvvvvvvvv
.insert_bundle(NavMenu::Bound2d.reachable_from(load))
.with_children(|cmds| {
// can only access the save file UI nodes from the load menu
for file in save_files.iter() {
cmds.spawn_bundle(file.bundle()).insert(Focusable::default());
}
});
// Spawn the options menu
cmds.spawn_bundle(menu_node)
// Sub menu accessible through the "options" button
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
.insert_bundle(NavMenu::Bound2d.reachable_from_named("options"))
.push_children(&[graphics_option, audio_options, input_options]);
}With this, your game menu will be isolated from your options menu, you can only
access it by sending NavRequest::Action
when options_button is focused, or by sending a
NavRequest::FocusOn(entity) where entity is any of graphics_option, audio_options or input_options.
Note that you won’t need to manually send the NavRequest if you are using one
of the default input systems provided in the systems
module.
Specifically, navigation between
Focusable
entities will be constrained to other
Focusable
that are children of the same
NavMenu.
It creates a self-contained menu.
Types of NavMenus
A NavMenu
doesn’t only define menu-to-menu navigation, but it also gives you
finner-grained control on how navigation is handled within a menu:
NavMenu::Wrapping*(as opposed toNavMenu::Bound*) enables looping navigation, where going offscreen in one direction “wraps” to the opposite screen edge.NavMenu::*Scopecreates a “scope” menu that catchesNavRequest::ScopeMoverequests even when the focused entity is in another sub-menu reachable from this menu. This behaves like you would expect a tabbed menu to behave.
See the NavMenu
documentation or the “ultimate” menu navigation
example
for details.
Marking
If you need to know from which menu a
NavEvent::FocusChanged
originated, you can use one of the marking methods on the NavMenu seeds.
A usage demo is available in the marking.rs example.
Changelog
0.8.2: Fix offsetting of mouse focus withUiCameras with a transform set to anything else than zero.0.9.0: AddFocusable::cancel(see documentation for details); Add warning message rather than do dumb things when there is more than a singleNavRequestper frame0.9.1: Fix #8, Panic on diagonal gamepad input0.10.0: Add thebevy-uifeature, technically this includes breaking changes, but it is very unlikely you need to change your code to get it working- Breaking: if you were manually calling
default_mouse_input, it now has additional parameters - Breaking:
ui_focusable_atandNodePosQuerynow have type parameters
- Breaking: if you were manually calling
0.11.0: Add theFocusable::lockfeature. A focusable now can be declared as “lock” and block the ui navigation systems until the user sends aNavRequest::Free. See thelocking.rsexample for illustration.- Breaking: New enum variants on
NavRequestandNavEvent
- Breaking: New enum variants on
0.11.1: Add themarkermodule, enabling propagation of user-specified components toFocusablechildren of aNavMenu.0.12.0: RemoveNavMenumethods fromMarkingMenuand make themenufield public instead. Internally, this represented too much duplicate code.0.12.1: Add by-name menus, making declaring complex menus in one go much easier.0.13.0: Complete rewrite of theNavMenudeclaration system:- Add automatic submenu access for
scopemenus. - Rename examples, they were named weirdly.
- Breaking: Replace
NavMenuconstructor API with an enum (KISS) and a set of methods that return various types ofBundles. Each variant does what thecycleandscopemethods used to do. - Breaking:
NavMenuis not a component anymore, the one used in the navigation algorithm is now private, you can’t match onNavMenuin query parameters anymore. If you need that functionality, create your own marker component and add it manually to your menu entities. - Breaking: Remove
is_*methods fromFocusable. Please use thestatemethod instead. The resulting program will be more correct. If you are only worried about discriminating theFocusedelement from others, just use aif let Focused = focus.state() {} else {}. Please see the examples directory for usage patterns. - Breaking: A lot of struct and module reordering to make documentation
more discoverable. Notably
DirectionandScopeDirectionare now in theeventsmodule.
- Add automatic submenu access for
0.13.1: Fix broken URLs in Readme.md
Notes on API Stability
In the 4th week of January, there has been 5 breaking version changes. 0.13.0
marks the end of this wave of API changes. And things should go much slower in
the future.
The new NavMenu construction system helps adding orthogonal features to the
library without breaking API changes. However, since bevy is still in 0.*
territory, it doesn’t make sense for this library to leave the 0.* range.
License
Copyright © 2022 Nicola Papale
This software is licensed under either MIT or Apache 2.0 at your leisure. See LICENSE file for details.
Re-exports
pub use events::NavEvent;pub use events::NavRequest;Modules
Focusable components and bundles to ease navigable UI declaration
Navigation events and requests
System for the navigation tree and default input systems to get started
Structs
The navigation system’s lock.
Plugin for menu marker propagation.
The navigation plugin
Non empty vector, ensure non empty by construction.
Inherits Vec’s methods through Deref trait, not implement DerefMut.
Overridden these methods: