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.24.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, however, it requires implementing your own input handling. Check out
the source code for the systems module for leads on
implementing your own input handling.
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::prelude::*;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(DefaultNavigationPlugins)
.run();
}Use the InputMapping resource to change keyboard and gamepad button mapping.
If you want to change entirely how input is handled, you should do as follow. All
interaction with the navigation engine is done through
EventWriter<NavRequest>:
use bevy::prelude::*;
use bevy_ui_navigation::prelude::*;
fn custom_input_system_emitting_nav_requests(mut events: EventWriter<NavRequest>) {
// handle input and events.send(NavRequest::FooBar)
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(NavigationPlugin::new())
.add_system(custom_input_system_emitting_nav_requests)
.run();
}Check the examples directory for more example code.
bevy-ui-navigation provides a variety of ways to handle navigation actions.
Check out the NavEventReaderExt trait
(and the NavEventReader struct methods) for what you can do.
use bevy::{app::AppExit, prelude::*};
use bevy_ui_navigation::prelude::*;
#[derive(Component)]
enum MenuButton {
StartGame,
ToggleFullscreen,
ExitGame,
Counter(i32),
//.. etc.
}
fn handle_nav_events(
mut buttons: Query<&mut MenuButton>,
mut events: EventReader<NavEvent>,
mut exit: EventWriter<AppExit>
) {
// Note: we have a closure here because the `buttons` query is mutable.
// for immutable queries, you can use `.activated_in_query` which returns an iterator.
// Do something when player activates (click, press "A" etc.) a `Focusable` button.
events.nav_iter().activated_in_query_foreach_mut(&mut buttons, |mut button| match &mut *button {
MenuButton::StartGame => {
// start the game
}
MenuButton::ToggleFullscreen => {
// toggle fullscreen here
}
MenuButton::ExitGame => {
exit.send(AppExit);
}
MenuButton::Counter(count) => {
*count += 1;
}
//.. etc.
})
}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::prelude::{FocusState, Focusable};
fn button_system(
mut focusables: Query<(&Focusable, &mut BackgroundColor), Changed<Focusable>>,
) {
for (focus, mut color) in focusables.iter_mut() {
let new_color = if matches!(focus.state(), FocusState::Focused) {
Color::RED
} else {
Color::BLACK
};
*color = new_color.into();
}
}Snappy feedback
You will want the interaction feedback to be snappy. This means the
interaction feedback should run the same frame as the focus change. For this to
happen every frame, you should add button_system to your app using the
NavRequestSystem label like so:
use bevy::prelude::*;
use bevy_ui_navigation::prelude::{NavRequestSystem, NavRequest, NavigationPlugin};
fn custom_mouse_input(mut events: EventWriter<NavRequest>) {
// handle input and events.send(NavRequest::FooBar)
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugin(NavigationPlugin::new())
// ...
// Add the button color update system after the focus update system
.add_system(button_system.after(NavRequestSystem))
// Add input systems before the focus update system
.add_system(custom_mouse_input.before(NavRequestSystem))
// ...
.run();
}
// Implementation from earlier
fn button_system() {}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::prelude::NavRequest;
fn set_focus_to_arbitrary_focusable(
entity: Entity,
mut requests: EventWriter<NavRequest>,
) {
requests.send(NavRequest::FocusOn(entity));
}Set the first focused element
You probably want to be able to chose which element is the first one to gain
focus. By default, the system picks the first Focusable it finds. To change
this behavior, spawn a prioritized Focusable with Focusable::prioritized.
MenuBuilder
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 → AA
In 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 MenuBuilder.
The high level usage of MenuBuilder is as follow:
- First you need a “root” menu using
MenuBuilder::Root. - 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(FocusableButtonBundle).id();)
- Add a
- to the
NodeBundlecontaining all the options menuFocusableentities, you add the following component:MenuBuilder::from_named("opt_btn_name")if you opted for adding theNamecomponent.MenuBuilder::EntityParent(opt_btn)if you have anEntityid.
In code, This will look like this:
use bevy::prelude::*;
use bevy_ui_navigation::prelude::{Focusable, MenuSetting, MenuBuilder};
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 {
background_color: Color::rgb(1.0, 0.3, 1.0).into(),
..Default::default()
});
let mut spawn = |bundle: &FocusableButtonBundle, name: &'static str| {
cmds.spawn(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(menu_node.clone())
// Root Menu vvvvvvvvvvvvvvvvv
.insert((MenuSetting::new(), MenuBuilder::Root))
.push_children(&[options, game, quit, load]);
// Spawn the load menu
cmds.spawn(menu_node.clone())
// Sub menu accessible through the load button
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
.insert((MenuSetting::new(), MenuBuilder::EntityParent(load)))
.with_children(|cmds| {
// can only access the save file UI nodes from the load menu
for file in save_files.iter() {
cmds.spawn(file.bundle()).insert(Focusable::default());
}
});
// Spawn the options menu
cmds.spawn(menu_node)
// Sub menu accessible through the "options" button
// vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
.insert((MenuSetting::new(), MenuBuilder::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 MenuSetting. It creates a self-contained
menu.
Types of MenuSettings
To define a menu, you need both the MenuBuilder and MenuSetting components.
A MenuSetting gives you fine-grained control on how navigation is handled within a menu:
MenuSetting::new().wrapping()enables looping navigation, where going offscreen in one direction “wraps” to the opposite screen edge.MenuSetting::new().scope()creates 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 MenuSetting 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 NavMarker in the mark module.
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.md0.14.0: Some important changes, and a bunch of new very useful features.- Add a
Focusable::dormantconstructor to specify which focusable you want to be the first to focus, this also works forFocusables withinNavMenus. - Important: This changes the library behavior, now there will
automatically be a
Focusedentity set. Add a system to set the firstFocusedwheneverFocusables are added to the world. - Add
NavEvent::InitiallyFocusedto handle this firstFocusedevent. - Early-return in
default_gamepad_inputanddefault_keyboard_inputwhen there are noFocusableelements in the world. This saves your precious CPU cycles. And prevents spuriouswarnlog messages. - Do not crash when resolving
NavRequests while noFocusables exists in the world. Instead, it now prints a warning message. - Important: Now the focus handling algorithm supports multiple
NavRequests per frame. If previously you erroneously sent multipleNavRequestper update and relied on the ignore mechanism, you’ll have a bad time. - This also means the focus changes are visible as soon as the system ran,
the new
NavRequestSystemlabel can be used to order your system in relation to the focus update system. This makes the focus change much snappier. - Rewrite the
ultimate_menu_navigation.rswithout thebuild_ui!macro because we shouldn’t expect users to be familiar with my personal weird macro. - Breaking: Remove
Defaultimpl onNavLock. The user shouldn’t be able to mutate it, you could technically overwrite theNavLockresource by usinginsert_resource(NavLock::default()).
- Add a
0.15.0: Breaking: bump bevy version to0.7(you should be able to upgrade from0.14.0without changing your code)0.15.1: Fix themarkersystems panicking at startup.0.16.0:- Cycling now wraps around the screen properly, regardless of UI camera
position and scale. See the new
off_screen_focusables.rsexample for a demo. - Fix the
default_mouse_inputsystem not accounting for camera scale. - Update examples to make use of
NavRequestSystemlabel, add more recommendations with regard to system ordering. - Warning: if you have some funky UI that goes beyond the screen (which
you are likely to have if you use the
Overflowfeature), this might result in unexpected behavior. Please fill a bug if you hit that limitation. - Add a nice stress test example with 96000 focusable nodes. This crate is not particularly optimized, but it’s nice to see it holds up!
- Breaking: Removed the undocumented public
UiCameramarker component, please use the bevy nativebevy::ui::entity::CameraUiinstead. Furthermore, thedefault_mouse_inputsystem has one less parameter. - Warning: if you have multiple UI camera, things will definitively break. Please fill an issue if you stumble uppon that case.
- Cycling now wraps around the screen properly, regardless of UI camera
position and scale. See the new
0.17.0: Non-breaking, but due to cargo semver handling is a minor bump.- Add the
event_helpersmodule to simplify ui event handling - Fix README and crate-level documentation links
- Add the
0.18.0:- Breaking: Remove marker generic type parameter from
systems::NodePosQuery. Thegeneric_default_mouse_inputsystem now relies on the newly introducedScreenBoundariesthat abstracts camera offset and zoom settings. - Important: Introduced new plugins to make it even simpler to get started.
- Add
event_helpersmodule introduction to README. - Fix
bevy-uifeature not building. This issue was introduced in0.16.0.
- Breaking: Remove marker generic type parameter from
0.19.0: Breaking: Update to bevy 0.8.0- Please look at the diff for the
examplesdirectory for help on migration. - Important: Huge API modifications to comply with feedback from RFC 41.
- Breaking: Removed the
event_helpersmodule, use instead the.nav_itermethod onEventReader<NavEvent>. You should import theNavEventReaderExttrait for.nav_iterto be available onEventReader<NavEvent>. - Breaking: Renamed
dormant→prioritized - Breaking: Renamed
NavMenu→MenuSetting- Instead of an enum,
NavMenuis now a struct with two boolean fields
- Instead of an enum,
- Breaking: Renamed
bundles→menu- Note: Dealing with “seeds” (aka bundles) is now much simpler and similar to other bevy plugins
- This is serious breaking change, please check the
MenuBuilderdocs - Disclaimer:
MenuBuilderis likely to be renamed in the future.
- Breaking: Add a
preludemodule, for all your crazy folks who like to not name stuff they use (such as myself); this replaces the names being available at the top crate level, if your code breaks because “bevy_ui_navigation doesn’t export this symbol”, try importingpreludeinstead. - Warning: 0.8.0 removed the ability for the user to change the ui camera position and perspective, see https://github.com/bevyengine/bevy/pull/5252 Generic support for user-defined UIs still allows custom cropping, but it not a relevant use case to the default bevy_ui library.
- Keyboard navigation in the style of games pre-dating computer mouses is now disabled by default.
While you can still use the escape and tab keys for interaction, you cannot use keyboard keys
to navigate between focusables anymore, this prevents keyboard input conflicts.
You can enable keyboard movement using the
InputMapping::keyboard_navigationfield. - Improved the heuristic to set the first focused element, now it tries to find an element in root menus if there is such a thing.
- Touch input handling has been removed, it was untested and probably broken, better let the user who knows what they are doing do it.
- NEW: Add complete user-customizable focus movement. Now it should be possible to implement focus navigation in 3d space.
- Breaking: This requires making the plugin generic over the navigation system, if you were
manually adding
NavigationPlugin, please consider usingDefaultNavigationPluginsinstead, if it is not possible, then useNavigationPlugin::new(). - Breaking: moved the
insert_tree_menusandresolve_named_menussystems toCoreStage::PostUpdate, which fixes a variety of bugs and unspecified behaviors with regard to adding menus and selecting the first focused element.
- Please look at the diff for the
0.20.0: Improve lock system- Breaking: Rename
NavRequest::Free→NavRequest::Unlockfor consistency. - Breaking:
NavEvent::Unlockednow contains aLockReasonrather than anEntity. - Add
NavRequest::Lockrequest to block navigation through a request. - Add a way to spawn and set focusables as not focusable at all with
Focusable::block. - Breaking: The default mouse input system
now by default does not immediately focus on hovered elements.
This is more in line with conventional UI libraries.
To keep the old behavior, set the
InputMapping.focus_follows_mousefield totrue. If you want to have graphical effects on hover, please define your own hover system. Here is how it was done in the bevy merge PR.
- Breaking: Rename
0.21.0: Add theNavEventReader::typesmethod0.22.0: Update to bevy 0.9.00.23.0: Start porting back to this crate all the changes made in the RFC PR- Add (optional)
Reflectderive to all navigation components, it’s on by default, disable it using--no-default-features --features "bevy-ui-navigation/bevy_ui" - Add a bunch of tests
- Re-order
TreeMenuinsertion, the transformation fromMenuBuilderto the internally used component (TreeMenu) is now done inPreUpdateinstead ofPostUpdate. This fixes a potential frame lag.
- Add (optional)
0.23.1: Fix docs.rsrustdoc-scrape-examplesflags.0.24.0:- Improve performance on
NavEventReader::activated_in_query_foreach_mut - BREAKING: Update to bevy 0.10.0
- Improve performance on
0.24.1:- Fix the
ultimate_menu_navigation.rsexample - add keyboard navigation to it,
too_many_focusables.rs,menu_navigation.rsandsimple.rs. - Add focus follow mouse to
simple.rsandultimate_menu_navigation.rs - Add
bevy_framepaceto all examples. - Remove
#[bundle]attribute from navigation bundles (it’s now useless)
- Fix the
- FUTURE:
0.25.0The goal is to split this crate so that it fits better with the rest of the bevy ecosystem. Future Plans involve Split the crate in 3 sub-crate, as described in the now closed RFC:- A plugin to translate bevy_ui things into events
- A plugin for gamepad-based navigation system
- A plugin for complex hierarchical menu navigation
Version matrix
| bevy | latest supporting version |
|---|---|
| 0.10 | 0.24.1 |
| 0.9 | 0.23.1 |
| 0.8 | 0.21.0 |
| 0.7 | 0.18.0 |
| 0.6 | 0.14.0 |
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.
Also, the way cargo handles versioning for 0.* crates is in infraction of
the semver specification. Meaning that additional features without breakages
requires bumping the minor version rather than the patch version (as should
pre-1. versions do).
License
Copyright © 2022 Nicola Papale
This software is licensed under either MIT or Apache 2.0 at your leisure. See licenses directory for details.
Font
The font in font.ttf is derived from Adobe SourceSans, licensed
under the SIL OFL. see file at licenses/SIL Open Font License.txt.
Modules
- Focusable components and bundles to ease navigable UI declaration.
- Types useful to define your own custom navigation inputs.
- Navigation events and requests.
- Utilities to mark focusables within a menu with a specific component.
- Contains menu-related components.
- Default imports for
bevy_ui_navigation. - System for the navigation tree and default input systems to get started.
Structs
- The navigation plugin and the default input scheme.
- The navigation plugin.
- Plugin for menu marker propagation.
- The label of the system in which the
NavRequestevents are handled, the focus state of theFocusables is updated and theNavEventevents are sent. - Non empty vector, ensure non empty by construction. Inherits
Vec’s methods throughDereftrait, not implementDerefMut. Overridden these methods:
Type Definitions
- A default
GenericNavigationPluginforbevy_ui.