Bevy UI navigation

Bevy tracking Latest version MIT/Apache 2.0 Documentation

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.

Examples

Check out the examples directory for bevy examples.

Demonstration of "Ultimate navigation" example

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.

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) { // handle input and events.send(NavRequest::FooBar) }

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::;

[derive(Component)]

enum MenuButton { StartGame, ToggleFullscreen, ExitGame, Counter(i32), //.. etc. }

fn handlenavevents( mut buttons: Query<&mut MenuButton>, mut events: EventReader, mut exit: EventWriter ) { // 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.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

use bevy::prelude::*;

use bevyuinavigation::prelude::Focusable;

fn system(mut cmds: Commands, myentity: Entity) { cmds.entity(myentity).insert(Focusable::default()); } `` That's it! Nowmy_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>, ) { for (focus, mut color) in focusables.itermut() { let newcolor = if matches!(focus.state(), FocusState::Focused) { Color::RED } else { Color::BLACK }; *color = newcolor.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: ```rust, norun use bevy::prelude::*; use bevyui_navigation::prelude::{NavRequestSystem, NavRequest, NavigationPlugin};

fn custommouseinput(mut events: EventWriter) { // handle input and events.send(NavRequest::FooBar) }

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() {} ```

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].

```rust use bevy::prelude::*; use bevyuinavigation::prelude::NavRequest;

fn setfocustoarbitraryfocusable( entity: Entity, mut requests: EventWriter, ) { 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: 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) { let menunode = NodeBundle { style: Style { flexdirection: FlexDirection::Column, ..Default::default()}, ..Default::default() }; let button = FocusableButtonBundle::from(ButtonBundle { backgroundcolor: 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 graphicsoption = spawn(&button, "graphics"); let audiooptions = spawn(&button, "audio"); let inputoptions = 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)][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.

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 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.

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

Version matrix

| 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 |

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.