A generic UI navigation algorithm for the Bevy engine default UI library.
toml
[dependencies]
bevy-ui-navigation = "0.25.0"
The in-depth design specification is available here.
Check out the examples
directory for bevy examples.
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.
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.
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.
rust, no_run
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>
][NavRequest
]:
```rust, norun use bevy::prelude::*; use bevyui_navigation::prelude::*;
fn custominputsystememittingnav_requests(mut events: EventWriter
fn main() { App::new() .addplugins((DefaultPlugins, NavigationPlugin::new())) .addsystems(Update, custominputsystememittingnav_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.
```rust use bevy::{app::AppExit, prelude::}; use bevy_ui_navigation::prelude::;
enum MenuButton { StartGame, ToggleFullscreen, ExitGame, Counter(i32), //.. etc. }
fn handlenavevents(
mut buttons: Query<&mut MenuButton>,
mut events: EventReaderbuttons
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.naviter().activatedinqueryforeach_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:
```rust
fn system(mut cmds: Commands, myentity: Entity) {
cmds.entity(myentity).insert(Focusable::default());
}
``
That's it! Now
my_entityis 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:
```rust
use bevy::prelude::*;
use bevyuinavigation::prelude::{FocusState, Focusable};
fn buttonsystem(
mut focusables: Query<(&Focusable, &mut BackgroundColor), Changed
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:
```rust, norun
use bevy::prelude::*;
use bevyui_navigation::prelude::{NavRequestSystem, NavRequest, NavigationPlugin};
fn custommouseinput(mut events: EventWriter
fn main() { App::new() .addplugins((DefaultPlugins, NavigationPlugin::new())) // ... .addsystems(Update, ( // Add input systems before the focus update system custommouseinput.before(NavRequestSystem), // Add the button color update system after the focus update system buttonsystem.after(NavRequestSystem), )) // ... .run(); } // Implementation from earlier fn buttonsystem() {} ```
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
].
```rust use bevy::prelude::*; use bevyuinavigation::prelude::NavRequest;
fn setfocustoarbitraryfocusable(
entity: Entity,
mut requests: EventWriter
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:
text
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:
1. First you need a "root" menu using MenuBuilder::Root
.
2. You need to spawn into the ECS your "options" button with a [Focusable
]
component. 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 the
[Focusable
] component to your options button.
* Pre-spawn the options button and store somewhere it's Entity
id
(let opt_btn = commands.spawn(FocusableButtonBundle).id();
)
3. to the NodeBundle
containing all the options menu [Focusable
] entities,
you add the following component:
* MenuBuilder::from_named("opt_btn_name")
if you opted for adding the Name
component.
* MenuBuilder::EntityParent(opt_btn)
if you have an [Entity
] id.
In code, This will look like this: ```rust use bevy::prelude::*; use bevyuinavigation::prelude::{Focusable, MenuSetting, MenuBuilder}; use bevyuinavigation::components::FocusableButtonBundle;
struct SaveFile;
impl SaveFile {
fn bundle(&self) -> impl Bundle {
// UI bundle to show this in game
NodeBundle::default()
}
}
fn spawnmenu(mut cmds: Commands, savefiles: Vec
// 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)
][NavRequest::FocusOn
] 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.
MenuSetting
sTo 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 catches [NavRequest::ScopeMove
]
requests 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.
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.
0.8.2
: Fix offsetting of mouse focus with UiCamera
s with a transform set
to anything else than zero.0.9.0
: Add [Focusable::cancel
] (see documentation for details); Add warning
message rather than do dumb things when there is more than a single [NavRequest
]
per frame0.9.1
: Fix #8, Panic on diagonal gamepad input0.10.0
: Add the bevy-ui
feature, technically this includes breaking
changes, but it is very unlikely you need to change your code to get it
working
default_mouse_input
, it now has
additional parametersui_focusable_at
and NodePosQuery
now have type parameters0.11.0
: Add the Focusable::lock
feature. A focusable now can be declared
as "lock" and block the ui navigation systems until the user sends a
NavRequest::Free
. See the locking.rs
example for illustration.
NavRequest
and NavEvent
0.11.1
: Add the marker
module, enabling propagation of user-specified
components to Focusable
children of a NavMenu
.0.12.0
: Remove NavMenu
methods from MarkingMenu
and make the menu
field 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 the [NavMenu
] declaration system:
scope
menus.NavMenu
constructor API with an enum (KISS) and a
set of methods that return various types of Bundle
s. Each variant does
what the cycle
and scope
methods used to do.NavMenu
is not a component anymore, the one used in the
navigation algorithm is now private, you can't match on NavMenu
in query
parameters anymore. If you need that functionality, create your own marker
component and add it manually to your menu entities.is_*
methods from Focusable
. Please use the
state
method instead. The resulting program will be more correct. If you
are only worried about discriminating the Focused
element from others,
just use a if let Focused = focus.state() {} else {}
. Please see the
examples directory for usage patterns.Direction
and ScopeDirection
are now in the
events
module.0.13.1
: Fix broken URLs in Readme.md0.14.0
: Some important changes, and a bunch of new very useful features.
Focusable::dormant
constructor to specify which focusable you want
to be the first to focus, this also works for [Focusable
]s within
[NavMenu
]s.Focused
entity set. Add a system to set the first
Focused
whenever Focusable
s are added to the world.NavEvent::InitiallyFocused
] to handle this first Focused
event.default_gamepad_input
and default_keyboard_input
when
there are no Focusable
elements in the world. This saves your precious
CPU cycles. And prevents spurious warn
log messages.NavRequest
s while no Focusable
s exists in
the world. Instead, it now prints a warning message.NavRequest
s
per frame. If previously you erroneously sent multiple NavRequest
per
update and relied on the ignore mechanism, you'll have a bad time.NavRequestSystem
label can be used to order your system in
relation to the focus update system. This makes the focus change much
snappier.ultimate_menu_navigation.rs
without the build_ui!
macro
because we shouldn't expect users to be familiar with my personal weird
macro.Default
impl on NavLock
. The user shouldn't be
able to mutate it, you could technically overwrite the NavLock
resource
by using insert_resource(NavLock::default())
.0.15.0
: Breaking: bump bevy version to 0.7
(you should be able to
upgrade from 0.14.0
without changing your code)0.15.1
: Fix the marker
systems panicking at startup.0.16.0
:
off_screen_focusables.rs
example for a demo.default_mouse_input
system not accounting for camera scale.NavRequestSystem
label, add more recommendations
with regard to system ordering.Overflow
feature), this might result
in unexpected behavior. Please fill a bug if you hit that limitation.UiCamera
marker component, please
use the bevy native bevy::ui::entity::CameraUi
instead. Furthermore, the
default_mouse_input
system has one less parameter.0.17.0
: Non-breaking, but due to cargo semver handling is a minor bump.
event_helpers
module to simplify ui event handling0.18.0
:
systems::NodePosQuery
.
The [generic_default_mouse_input
] system now relies on the newly introduced
[ScreenBoundaries
] that abstracts camera offset and zoom settings.event_helpers
module introduction to README.bevy-ui
feature not building. This issue was introduced in 0.16.0
.0.19.0
: Breaking: Update to bevy 0.8.0
examples
directory for help on migration.event_helpers
module, use instead the .nav_iter
method
on EventReader<NavEvent>
. You should import the NavEventReaderExt
trait for .nav_iter
to be available on EventReader<NavEvent>
.dormant
→ prioritized
NavMenu
→ MenuSetting
NavMenu
is now a struct with two boolean fieldsbundles
→ menu
MenuBuilder
] docsMenuBuilder
is likely to be renamed in the future.prelude
module, 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 "bevyuinavigation doesn't export this symbol", try importing
prelude
instead.InputMapping::keyboard_navigation
] field.NavigationPlugin
, please consider using DefaultNavigationPlugins
instead,
if it is not possible, then use NavigationPlugin::new()
.insert_tree_menus
and resolve_named_menus
systems to
CoreStage::PostUpdate
, which fixes a variety of bugs and unspecified behaviors with
regard to adding menus and selecting the first focused element.0.20.0
: Improve lock system
NavRequest::Free
→ NavRequest::Unlock
for consistency.NavEvent::Unlocked
now contains a [LockReason
] rather than an Entity
.NavRequest::Lock
request to block navigation through a request.Focusable::block
].InputMapping.focus_follows_mouse
field to true
.
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.0.21.0
: Add the [NavEventReader::types
] method0.22.0
: Update to bevy 0.9.00.23.0
: Start porting back to this crate all the changes made in [the RFC PR]
Reflect
derive to all navigation components, it's on by default,
disable it using --no-default-features --features "bevy-ui-navigation/bevy_ui"
TreeMenu
insertion, the transformation from MenuBuilder
to the
internally used component (TreeMenu
) is now done in PreUpdate
instead of
PostUpdate
. This fixes a potential frame lag.0.23.1
: Fix docs.rs rustdoc-scrape-examples
flags.0.24.0
:
NavEventReader::activated_in_query_foreach_mut
0.24.1
:
ultimate_menu_navigation.rs
exampletoo_many_focusables.rs
, menu_navigation.rs
and simple.rs
.simple.rs
and ultimate_menu_navigation.rs
bevy_framepace
to all examples.#[bundle]
attribute from navigation bundles (it's now useless)0.25.0
: BREAKING: Update ot bevy 0.11.00.26.0
: BREAKING: Fix the bevy_ui
feature. Ooops sorry.0.26.0
The 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:
| bevy | latest supporting version | |------|--------| | 0.11 | 0.26.0 | | 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 |
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).
Copyright © 2022 Nicola Papale
This software is licensed under either MIT or Apache 2.0 at your leisure. See licenses directory for details.
The font in font.ttf
is derived from Adobe SourceSans, licensed
under the SIL OFL. see file at licenses/SIL Open Font License.txt
.