Bevy UI navigation

Bevy tracking Latest version MIT/Apache 2.0 Documentation

A generic UI navigation algorithm meant to be adaptable to any UI library, but currently limiting itself to targeting the Bevy engine default UI library.

toml [dependencies] bevy-ui-navigation = "0.12.0"

The in-depth design specification is available here.

Examples

Check out the examples directory for bevy examples.

Ultimate navigation example demo 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. 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.

Simple case

You just have a collection of buttons displayed on screen and you want the player to be able to select between them with their controller? Simply use the bevy_ui_navigation::components::FocusableButtonBundle instead of ButtonBundle. The navigation system is capable of switching focus based on the 2D position of each focusable element.

This won't work out of the box though, you must wire the UI to the control scheme, by sending requests to the NavRequest event queue. We do not provide out of the box a way to do this, but we provide default input handling systems. Try this: rust, no_run use bevy::prelude::*; use bevy_ui_navigation::systems::{default_gamepad_input, default_keyboard_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_gamepad_input) .run(); } The default button mapping may not be what you want, or you may want to change it in-game (for example when the user is in an input mapping menu) The controls are modifiable with the InputMapping resource. Check out the doc for it for more details.

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: ```rust use bevy::prelude::*; use bevyuinavigation::{NavEvent, NavRequest};

struct Gameui { startgamebutton: Entity, }

fn handlenavevents(mut events: EventReader, game: Res) { use bevyuinavigation::{NavEvent::NoChanges, NavRequest::Action}; for event in events.iter() { match event { NoChanges { from, request: Action } if *from.first() == game.startgamebutton => { // 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.

In truth, FocusableButtonBundle is just a ButtonBundle with an extra Focusable component field.

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

fn system(mut cmds: Commands, myentity: Entity) { cmds.entity(myentity).insert(Focusable::default()); } `` That's it! Nowmyentityis part of the navigation tree. The player can select it with their controller the same way as any other [Focusable`](https://docs.rs/bevy-ui-navigation/latest/bevyui_navigation/struct.Focusable.html) 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::Focusable;

fn buttonsystem( mut focusables: Query<(&Focusable, &mut UiColor), Changed>, ) { for (focusstate, mut color) in focusables.itermut() { let newcolor = if focusstate.isfocused() { Color::RED } else { Color::BLUE }; *color = new_color.into(); } } ```

More complex use cases

With 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: 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 the NavMenu component. 1. First you need a "root" NavMenu. This is a limit of the navigation algorithm (which may be fixed in the future) 2. You also need the Entity value of your "Options" button 3. You then add a NavMenu::reachable_from(options_button) to the NodeBundle containing all the options menu Focusable entities. This may look like this: ```rust use bevy::prelude::*; use bevyuinavigation::NavMenu; use bevyuinavigation::components::FocusableButtonBundle;

fn spawnmenu(mut cmds: Commands) { let menu = NodeBundle { style: Style { flexdirection: 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| { cmds.spawnbundle(bundle.clone()).id() }; let options = spawn(&button); let graphicsoption = spawn(&button); let audiooptions = spawn(&button); let inputoptions = spawn(&button); let game = spawn(&button); let quit = spawn(&button); let load = spawn(&button);

// Spawn the game menu, with a `NavMenu`
let game_menu = cmds
    .spawn_bundle(menu.clone())
    //      vvvvvvvvvvvvv
    .insert(NavMenu::root())
    .push_children(&[options, game, quit, load])
    .id();

// Spawn the options menu
let options_menu = cmds
    .spawn_bundle(menu)
    //             !!vvvvvvvvvvvvvvvvvvvvvvv!!
    .insert(NavMenu::reachable_from(options))
    .push_children(&[graphics_option, audio_options, input_options])
    .id();

} ```

With this, your game menu will be isolated from your options menu, you can only access it by sending the NavRequest::Action when options_button is focused, or by sending a NavRequest::FocusOn(input_options_button).

Specifically, navigation between [Focusable] entities will be constrained to other Focusable that are children of the same NavMenu. It creates a self-contained menu.

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 entity with the NavRequest::FocusOn request.

NavMenu settings

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::{cycle, closed} controls whether or not going left from the leftmost element goes to the rightmost and vis-versa. NavMenu::scope sets the menu as a sort of global control menu. It 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 "ultimate" menu navigation example for a demonstration.

Marking

If you need to know from which menu a NavEvent::FocusChanged originated, you may want to use the marker module.

A usage demo is available in the marking.rs example

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.

Changelog

License

Copyright © 2021 Nicola Papale

This software is licensed under either MIT or Apache 2.0 at your leisure. See LICENSE file for details.