diff --git a/Cargo.toml b/Cargo.toml
index ba1947661a0784..2a5af0a877a13b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1427,6 +1427,49 @@ description = "Illustrates various features of Bevy UI"
category = "UI (User Interface)"
wasm = true
+
+# UI navigation
+[[example]]
+name = "locking"
+path = "examples/ui_navigation/locking.rs"
+
+[package.metadata.example.locking]
+name = "UI locking"
+description = "Illustrates UI locking"
+category = "UI navigation"
+wasm = true
+
+[[example]]
+name = "menu_navigation"
+path = "examples/ui_navigation/menu_navigation.rs"
+
+[package.metadata.example.menu_navigation]
+name = "menu navigation"
+description = "Illustrates movement between menus"
+category = "UI navigation"
+wasm = true
+
+[[example]]
+name = "nav_3d"
+path = "examples/ui_navigation/nav_3d.rs"
+
+[package.metadata.example.nav_3d]
+name = "UI in 3d world"
+description = "Illustrates using Focusable on 3d entities"
+category = "UI navigation"
+wasm = true
+
+[[example]]
+name = "off_screen_focusables"
+path = "examples/ui_navigation/off_screen_focusables.rs"
+
+[package.metadata.example.off_screen_focusables]
+name = "Focusable wrapping"
+description = "Illustrates wrapping within a menu"
+category = "UI navigation"
+wasm = true
+
+
# Window
[[example]]
name = "clear_color"
diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml
index 7fccda37863d09..4c72884e3a64d8 100644
--- a/crates/bevy_internal/Cargo.toml
+++ b/crates/bevy_internal/Cargo.toml
@@ -94,6 +94,7 @@ bevy_dynamic_plugin = { path = "../bevy_dynamic_plugin", optional = true, versio
bevy_sprite = { path = "../bevy_sprite", optional = true, version = "0.8.0-dev" }
bevy_text = { path = "../bevy_text", optional = true, version = "0.8.0-dev" }
bevy_ui = { path = "../bevy_ui", optional = true, version = "0.8.0-dev" }
+bevy_ui_navigation = { path = "../bevy_ui_navigation", optional = true, version = "0.8.0-dev" }
bevy_winit = { path = "../bevy_winit", optional = true, version = "0.8.0-dev" }
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.8.0-dev" }
diff --git a/crates/bevy_internal/src/lib.rs b/crates/bevy_internal/src/lib.rs
index cc63909ab3cb81..1a05145b15d2c6 100644
--- a/crates/bevy_internal/src/lib.rs
+++ b/crates/bevy_internal/src/lib.rs
@@ -159,6 +159,12 @@ pub mod ui {
pub use bevy_ui::*;
}
+#[cfg(feature = "bevy_ui")]
+pub mod ui_navigation {
+ //! User interface navigation.
+ pub use bevy_ui_navigation::*;
+}
+
#[cfg(feature = "bevy_winit")]
pub mod winit {
//! Window creation, configuration, and handling
diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml
index 170b26b0ff0d5c..4da6129558285b 100644
--- a/crates/bevy_ui/Cargo.toml
+++ b/crates/bevy_ui/Cargo.toml
@@ -27,6 +27,7 @@ bevy_sprite = { path = "../bevy_sprite", version = "0.8.0-dev" }
bevy_text = { path = "../bevy_text", version = "0.8.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.8.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.8.0-dev" }
+bevy_ui_navigation = { path = "../bevy_ui_navigation", version = "0.8.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.8.0-dev" }
# other
diff --git a/crates/bevy_ui/src/entity.rs b/crates/bevy_ui/src/entity.rs
index 5345ee3dd67264..5cddfeb92f3c88 100644
--- a/crates/bevy_ui/src/entity.rs
+++ b/crates/bevy_ui/src/entity.rs
@@ -15,6 +15,7 @@ use bevy_render::{
};
use bevy_text::Text;
use bevy_transform::prelude::{GlobalTransform, Transform};
+use bevy_ui_navigation::Focusable;
/// The basic UI node
#[derive(Bundle, Clone, Debug, Default)]
@@ -106,7 +107,7 @@ impl Default for TextBundle {
}
/// A UI node that is a button
-#[derive(Bundle, Clone, Debug)]
+#[derive(Bundle, Clone, Debug, Default)]
pub struct ButtonBundle {
/// Describes the size of the node
pub node: Node,
@@ -114,10 +115,8 @@ pub struct ButtonBundle {
pub button: Button,
/// Describes the style including flexbox settings
pub style: Style,
- /// Describes whether and how the button has been interacted with by the input
- pub interaction: Interaction,
- /// Whether this node should block interaction with lower nodes
- pub focus_policy: FocusPolicy,
+ /// How this button fits with other ones
+ pub focusable: Focusable,
/// The color of the node
pub color: UiColor,
/// The image of the node
@@ -132,23 +131,6 @@ pub struct ButtonBundle {
pub computed_visibility: ComputedVisibility,
}
-impl Default for ButtonBundle {
- fn default() -> Self {
- ButtonBundle {
- button: Button,
- interaction: Default::default(),
- focus_policy: Default::default(),
- node: Default::default(),
- style: Default::default(),
- color: Default::default(),
- image: Default::default(),
- transform: Default::default(),
- global_transform: Default::default(),
- visibility: Default::default(),
- computed_visibility: Default::default(),
- }
- }
-}
/// Configuration for cameras related to UI.
///
/// When a [`Camera`] doesn't have the [`UiCameraConfig`] component,
diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs
index 54a0e75189f3bf..b527d82dbc8c67 100644
--- a/crates/bevy_ui/src/lib.rs
+++ b/crates/bevy_ui/src/lib.rs
@@ -3,8 +3,9 @@
//! Spawn UI elements with [`entity::ButtonBundle`], [`entity::ImageBundle`], [`entity::TextBundle`] and [`entity::NodeBundle`]
//! This UI is laid out with the Flexbox paradigm (see ) except the vertical axis is inverted
mod flex;
-mod focus;
+// mod focus;
mod geometry;
+mod navigation;
mod render;
mod ui_node;
diff --git a/crates/bevy_ui/src/navigation/mod.rs b/crates/bevy_ui/src/navigation/mod.rs
new file mode 100644
index 00000000000000..d283fd01a7ce77
--- /dev/null
+++ b/crates/bevy_ui/src/navigation/mod.rs
@@ -0,0 +1,2 @@
+mod queries;
+mod systems;
diff --git a/crates/bevy_ui/src/navigation/queries.rs b/crates/bevy_ui/src/navigation/queries.rs
new file mode 100644
index 00000000000000..f47b2e22e69514
--- /dev/null
+++ b/crates/bevy_ui/src/navigation/queries.rs
@@ -0,0 +1,105 @@
+#[derive(Debug, Clone, Copy)]
+pub struct Rect {
+ pub min: Vec2,
+ pub max: Vec2,
+}
+/// Camera modifiers for movement cycling.
+///
+/// This is only used by the cycling routine to find [`Focusable`]s at the
+/// opposite side of the screen. It's expected to contain the ui camera
+/// projection screen boundaries and position. See implementation of
+/// [`systems::update_boundaries`](crate::systems::update_boundaries) to
+/// see how to implement it yourself.
+///
+/// # Note
+///
+/// This is a [resource](Res). It is optional and will log a warning if
+/// a cycling request is made and it does not exist.
+#[derive(Debug)]
+pub struct ScreenBoundaries {
+ pub position: Vec2,
+ pub screen_edge: Rect,
+ pub scale: f32,
+}
+
+/// System parameter for the default cursor navigation system.
+///
+/// It uses the bevy [`GlobalTransform`] to compute relative positions
+/// and change focus to the correct entity.
+/// It uses the [`ScreenBoundaries`] resource to compute screen boundaries
+/// and move the cursor accordingly when it reaches a screen border
+/// in a cycling menu.
+#[derive(SystemParam)]
+pub struct UiProjectionQuery<'w, 's> {
+ boundaries: Option>,
+ transforms: Query<'w, 's, &'static GlobalTransform>,
+}
+impl<'w, 's> MoveParam for UiProjectionQuery<'w, 's> {
+ fn resolve_2d<'a>(
+ &self,
+ focused: Entity,
+ direction: events::Direction,
+ cycles: bool,
+ siblings: &'a [Entity],
+ ) -> Option<&'a Entity> {
+ use events::Direction::*;
+
+ let pos_of = |entity: Entity| {
+ self.transforms
+ .get(entity)
+ .expect("Focusable entities must have a GlobalTransform component")
+ .translation()
+ .xy()
+ };
+ let focused_pos = pos_of(focused);
+ let closest = siblings.iter().filter(|sibling| {
+ direction.is_in(focused_pos, pos_of(**sibling)) && **sibling != focused
+ });
+ let closest = max_by_in_iter(closest, |s| -focused_pos.distance_squared(pos_of(**s)));
+ match (closest, self.boundaries.as_ref()) {
+ (None, None) if cycles => {
+ warn!(
+ "Tried to move in {direction:?} from Focusable {focused:?} while no other \
+ Focusables were there. There were no `Res`, so we couldn't \
+ compute the screen edges for cycling. Make sure you either add the \
+ bevy_ui_navigation::systems::update_boundaries system to your app or implement \
+ your own routine to manage a `Res`."
+ );
+ None
+ }
+ (None, Some(boundaries)) if cycles => {
+ let (x, y) = (boundaries.position.x, boundaries.position.y);
+ let edge = boundaries.screen_edge;
+ let scale = boundaries.scale;
+ let focused_pos = match direction {
+ // NOTE: up/down axises are inverted in bevy
+ North => Vec2::new(focused_pos.x, y - scale * edge.min.y),
+ South => Vec2::new(focused_pos.x, y + scale * edge.max.y),
+ East => Vec2::new(x - edge.min.x * scale, focused_pos.y),
+ West => Vec2::new(x + edge.max.x * scale, focused_pos.y),
+ };
+ max_by_in_iter(siblings.iter(), |s| {
+ -focused_pos.distance_squared(pos_of(**s))
+ })
+ }
+ (anyelse, _) => anyelse,
+ }
+ }
+}
+
+pub type NavigationPlugin<'w, 's> = GenericNavigationPlugin>;
+
+/// The navigation plugin and the default input scheme.
+///
+/// Add it to your app with `.add_plugins(DefaultNavigationPlugins)`.
+///
+/// This provides default implementations for input handling, if you want
+/// your own custom input handling, you should use [`NavigationPlugin`] and
+/// provide your own input handling systems.
+pub struct DefaultNavigationPlugins;
+impl PluginGroup for DefaultNavigationPlugins {
+ fn build(&mut self, group: &mut bevy::app::PluginGroupBuilder) {
+ group.add(GenericNavigationPlugin::::new());
+ group.add(DefaultNavigationSystems);
+ }
+}
diff --git a/crates/bevy_ui/src/navigation/systems.rs b/crates/bevy_ui/src/navigation/systems.rs
new file mode 100644
index 00000000000000..df8da5fc0161c5
--- /dev/null
+++ b/crates/bevy_ui/src/navigation/systems.rs
@@ -0,0 +1,438 @@
+//! System for the navigation tree and default input systems to get started.
+
+use bevy_ecs::system::SystemParam;
+use bevy_input::prelude::*;
+use bevy_ui_navigation::{
+ events::{Direction, NavRequest, ScopeDirection},
+ resolve::{max_by_in_iter, ScreenBoundaries},
+ Focusable, Focused, Rect,
+};
+
+/// Control default ui navigation input buttons
+pub struct InputMapping {
+ /// Whether to use keybaord keys for navigation (instead of just actions).
+ pub keyboard_navigation: bool,
+ /// The gamepads to use for the UI. If empty, default to gamepad 0
+ pub gamepads: Vec,
+ /// Deadzone on the gamepad left stick for ui navigation
+ pub joystick_ui_deadzone: f32,
+ /// X axis of gamepad stick
+ pub move_x: GamepadAxisType,
+ /// Y axis of gamepad stick
+ pub move_y: GamepadAxisType,
+ /// Gamepad button for [`Direction::West`] [`NavRequest::Move`]
+ pub left_button: GamepadButtonType,
+ /// Gamepad button for [`Direction::East`] [`NavRequest::Move`]
+ pub right_button: GamepadButtonType,
+ /// Gamepad button for [`Direction::North`] [`NavRequest::Move`]
+ pub up_button: GamepadButtonType,
+ /// Gamepad button for [`Direction::South`] [`NavRequest::Move`]
+ pub down_button: GamepadButtonType,
+ /// Gamepad button for [`NavRequest::Action`]
+ pub action_button: GamepadButtonType,
+ /// Gamepad button for [`NavRequest::Cancel`]
+ pub cancel_button: GamepadButtonType,
+ /// Gamepad button for [`ScopeDirection::Previous`] [`NavRequest::ScopeMove`]
+ pub previous_button: GamepadButtonType,
+ /// Gamepad button for [`ScopeDirection::Next`] [`NavRequest::ScopeMove`]
+ pub next_button: GamepadButtonType,
+ /// Gamepad button for [`NavRequest::Free`]
+ pub free_button: GamepadButtonType,
+ /// Keyboard key for [`Direction::West`] [`NavRequest::Move`]
+ pub key_left: KeyCode,
+ /// Keyboard key for [`Direction::East`] [`NavRequest::Move`]
+ pub key_right: KeyCode,
+ /// Keyboard key for [`Direction::North`] [`NavRequest::Move`]
+ pub key_up: KeyCode,
+ /// Keyboard key for [`Direction::South`] [`NavRequest::Move`]
+ pub key_down: KeyCode,
+ /// Alternative keyboard key for [`Direction::West`] [`NavRequest::Move`]
+ pub key_left_alt: KeyCode,
+ /// Alternative keyboard key for [`Direction::East`] [`NavRequest::Move`]
+ pub key_right_alt: KeyCode,
+ /// Alternative keyboard key for [`Direction::North`] [`NavRequest::Move`]
+ pub key_up_alt: KeyCode,
+ /// Alternative keyboard key for [`Direction::South`] [`NavRequest::Move`]
+ pub key_down_alt: KeyCode,
+ /// Keyboard key for [`NavRequest::Action`]
+ pub key_action: KeyCode,
+ /// Keyboard key for [`NavRequest::Cancel`]
+ pub key_cancel: KeyCode,
+ /// Keyboard key for [`ScopeDirection::Next`] [`NavRequest::ScopeMove`]
+ pub key_next: KeyCode,
+ /// Alternative keyboard key for [`ScopeDirection::Next`] [`NavRequest::ScopeMove`]
+ pub key_next_alt: KeyCode,
+ /// Keyboard key for [`ScopeDirection::Previous`] [`NavRequest::ScopeMove`]
+ pub key_previous: KeyCode,
+ /// Keyboard key for [`NavRequest::Free`]
+ pub key_free: KeyCode,
+ /// Mouse button for [`NavRequest::Action`]
+ pub mouse_action: MouseButton,
+}
+impl Default for InputMapping {
+ fn default() -> Self {
+ InputMapping {
+ keyboard_navigation: false,
+ gamepads: vec![Gamepad { id: 0 }],
+ joystick_ui_deadzone: 0.36,
+ move_x: GamepadAxisType::LeftStickX,
+ move_y: GamepadAxisType::LeftStickY,
+ left_button: GamepadButtonType::DPadLeft,
+ right_button: GamepadButtonType::DPadRight,
+ up_button: GamepadButtonType::DPadUp,
+ down_button: GamepadButtonType::DPadDown,
+ action_button: GamepadButtonType::South,
+ cancel_button: GamepadButtonType::East,
+ previous_button: GamepadButtonType::LeftTrigger,
+ next_button: GamepadButtonType::RightTrigger,
+ free_button: GamepadButtonType::Start,
+ key_left: KeyCode::A,
+ key_right: KeyCode::D,
+ key_up: KeyCode::W,
+ key_down: KeyCode::S,
+ key_left_alt: KeyCode::Left,
+ key_right_alt: KeyCode::Right,
+ key_up_alt: KeyCode::Up,
+ key_down_alt: KeyCode::Down,
+ key_action: KeyCode::Space,
+ key_cancel: KeyCode::Back,
+ key_next: KeyCode::E,
+ key_next_alt: KeyCode::Tab,
+ key_previous: KeyCode::Q,
+ key_free: KeyCode::Escape,
+ mouse_action: MouseButton::Left,
+ }
+ }
+}
+
+/// `mapping { XYZ::X => ABC::A, XYZ::Y => ABC::B, XYZ::Z => ABC::C }: [(XYZ, ABC)]`
+macro_rules! mapping {
+ ($($from:expr => $to:expr),* ) => ([$( ( $from, $to ) ),*])
+}
+
+/// A system to send gamepad control events to the focus system
+///
+/// Dpad and left stick for movement, `LT` and `RT` for scopped menus, `A` `B`
+/// for selection and cancel.
+///
+/// The button mapping may be controlled through the [`InputMapping`] resource.
+/// You may however need to customize the behavior of this system (typically
+/// when integrating in the game) in this case, you should write your own
+/// system that sends [`NavRequest`](crate::NavRequest) events
+pub fn default_gamepad_input(
+ mut nav_cmds: EventWriter,
+ has_focused: Query>,
+ input_mapping: Res,
+ buttons: Res>,
+ axis: Res>,
+ mut ui_input_status: Local,
+) {
+ use Direction::*;
+ use NavRequest::{Action, Cancel, Free, Move, ScopeMove};
+
+ if has_focused.is_empty() {
+ // Do not compute navigation if there is no focus to change
+ return;
+ }
+
+ for &gamepad in &input_mapping.gamepads {
+ macro_rules! axis_delta {
+ ($dir:ident, $axis:ident) => {{
+ let axis_type = input_mapping.$axis;
+ axis.get(GamepadAxis { gamepad, axis_type })
+ .map_or(Vec2::ZERO, |v| Vec2::$dir * v)
+ }};
+ }
+
+ let delta = axis_delta!(Y, move_y) + axis_delta!(X, move_x);
+ if delta.length_squared() > input_mapping.joystick_ui_deadzone && !*ui_input_status {
+ let direction = match () {
+ () if delta.y < delta.x && delta.y < -delta.x => South,
+ () if delta.y < delta.x => East,
+ () if delta.y >= delta.x && delta.y > -delta.x => North,
+ () => West,
+ };
+ nav_cmds.send(Move(direction));
+ *ui_input_status = true;
+ } else if delta.length_squared() <= input_mapping.joystick_ui_deadzone {
+ *ui_input_status = false;
+ }
+
+ let command_mapping = mapping! {
+ input_mapping.action_button => Action,
+ input_mapping.cancel_button => Cancel,
+ input_mapping.left_button => Move(Direction::West),
+ input_mapping.right_button => Move(Direction::East),
+ input_mapping.up_button => Move(Direction::North),
+ input_mapping.down_button => Move(Direction::South),
+ input_mapping.next_button => ScopeMove(ScopeDirection::Next),
+ input_mapping.free_button => Free,
+ input_mapping.previous_button => ScopeMove(ScopeDirection::Previous)
+ };
+ for (button_type, request) in command_mapping {
+ let button = GamepadButton {
+ gamepad,
+ button_type,
+ };
+ if buttons.just_pressed(button) {
+ nav_cmds.send(request)
+ }
+ }
+ }
+}
+
+/// A system to send keyboard control events to the focus system.
+///
+/// supports `WASD` and arrow keys for the directions, `E`, `Q` and `Tab` for
+/// scopped menus, `Backspace` and `Enter` for cancel and selection.
+///
+/// The button mapping may be controlled through the [`InputMapping`] resource.
+/// You may however need to customize the behavior of this system (typically
+/// when integrating in the game) in this case, you should write your own
+/// system that sends [`NavRequest`](crate::NavRequest) events.
+pub fn default_keyboard_input(
+ has_focused: Query<(), With>,
+ keyboard: Res>,
+ input_mapping: Res,
+ mut nav_cmds: EventWriter,
+) {
+ use Direction::*;
+ use NavRequest::*;
+
+ if has_focused.is_empty() {
+ // Do not compute navigation if there is no focus to change
+ return;
+ }
+
+ let with_movement = mapping! {
+ input_mapping.key_up => Move(North),
+ input_mapping.key_down => Move(South),
+ input_mapping.key_left => Move(West),
+ input_mapping.key_right => Move(East),
+ input_mapping.key_up_alt => Move(North),
+ input_mapping.key_down_alt => Move(South),
+ input_mapping.key_left_alt => Move(West),
+ input_mapping.key_right_alt => Move(East)
+ };
+ let without_movement = mapping! {
+ input_mapping.key_action => Action,
+ input_mapping.key_cancel => Cancel,
+ input_mapping.key_next => ScopeMove(ScopeDirection::Next),
+ input_mapping.key_next_alt => ScopeMove(ScopeDirection::Next),
+ input_mapping.key_free => Free,
+ input_mapping.key_previous => ScopeMove(ScopeDirection::Previous)
+ };
+ let mut send_command = |&(key, request)| {
+ if keyboard.just_pressed(key) {
+ nav_cmds.send(request)
+ }
+ };
+ if input_mapping.keyboard_navigation {
+ with_movement.iter().for_each(&mut send_command);
+ }
+ without_movement.iter().for_each(send_command);
+}
+
+/// [`SystemParam`](https://docs.rs/bevy/0.7.0/bevy/ecs/system/trait.SystemParam.html)
+/// used to compute UI focusable physical positions in mouse input systems.
+#[derive(SystemParam)]
+pub struct NodePosQuery<'w, 's, T: Component> {
+ entities: Query<'w, 's, (Entity, &'static T, &'static GlobalTransform), With>,
+ boundaries: Option>,
+}
+impl<'w, 's, T: Component> NodePosQuery<'w, 's, T> {
+ fn cursor_pos(&self, at: Vec2) -> Option {
+ let boundaries = self.boundaries.as_ref()?;
+ Some(at * boundaries.scale + boundaries.position)
+ }
+}
+
+fn is_in_node(at: Vec2, (_, node, trans): &(Entity, &T, &GlobalTransform)) -> bool {
+ let ui_pos = trans.translation().truncate();
+ let node_half_size = node.size() / 2.0;
+ let min = ui_pos - node_half_size;
+ let max = ui_pos + node_half_size;
+ (min.x..max.x).contains(&at.x) && (min.y..max.y).contains(&at.y)
+}
+
+/// Check which [`Focusable`] is at position `at` if any.
+///
+/// NOTE: returns `None` if there is no [`ScreenBoundaries`] resource.
+pub fn ui_focusable_at(at: Vec2, query: &NodePosQuery) -> Option
+where
+ T: ScreenSize + Component,
+{
+ let world_at = query.cursor_pos(at)?;
+ let under_mouse = query
+ .entities
+ .iter()
+ .filter(|query_elem| is_in_node(world_at, query_elem));
+ max_by_in_iter(under_mouse, |elem| elem.2.translation().z).map(|elem| elem.0)
+}
+
+fn cursor_pos(windows: &Windows) -> Option {
+ windows.get_primary().and_then(|w| w.cursor_position())
+}
+
+pub trait ScreenSize {
+ fn size(&self) -> Vec2;
+}
+
+impl ScreenSize for Node {
+ fn size(&self) -> Vec2 {
+ self.size
+ }
+}
+
+/// A system to send mouse control events to the focus system
+///
+/// Unlike [`generic_default_mouse_input`], this system is gated by the
+/// `bevy::render::Camera` and `bevy::ui::Node`.
+///
+/// Which button to press to cause an action event is specified in the
+/// [`InputMapping`] resource.
+///
+/// You may however need to customize the behavior of this system (typically
+/// when integrating in the game) in this case, you should write your own
+/// system that sends [`NavRequest`](crate::NavRequest) events. You may use
+/// [`ui_focusable_at`] to tell which focusable is currently being hovered.
+pub fn default_mouse_input(
+ input_mapping: Res,
+ windows: Res,
+ mouse: Res>,
+ focusables: NodePosQuery,
+ focused: Query>,
+ nav_cmds: EventWriter,
+ last_pos: Local,
+) {
+ generic_default_mouse_input(
+ input_mapping,
+ windows,
+ mouse,
+ focusables,
+ focused,
+ nav_cmds,
+ last_pos,
+ );
+}
+
+/// A generic system to send mouse control events to the focus system
+///
+/// `T` must be a component assigned to `Focusable` elements that implements
+/// the [`ScreenSize`] trait.
+///
+/// Which button to press to cause an action event is specified in the
+/// [`InputMapping`] resource.
+///
+/// You may however need to customize the behavior of this system (typically
+/// when integrating in the game) in this case, you should write your own
+/// system that sends [`NavRequest`](crate::NavRequest) events. You may use
+/// [`ui_focusable_at`] to tell which focusable is currently being hovered.
+pub fn generic_default_mouse_input(
+ input_mapping: Res,
+ windows: Res,
+ mouse: Res>,
+ focusables: NodePosQuery,
+ focused: Query>,
+ mut nav_cmds: EventWriter,
+ mut last_pos: Local,
+) {
+ let cursor_pos = match cursor_pos(&windows) {
+ Some(c) => c,
+ None => return,
+ };
+ let world_cursor_pos = match focusables.cursor_pos(cursor_pos) {
+ Some(c) => c,
+ None => return,
+ };
+ let released = mouse.just_released(input_mapping.mouse_action);
+ let focused = focused.get_single();
+ // Return early if cursor didn't move since last call
+ let camera_moved = focusables.boundaries.map_or(false, |b| b.is_changed());
+ if !released && *last_pos == cursor_pos && !camera_moved {
+ return;
+ } else {
+ *last_pos = cursor_pos;
+ }
+ let not_hovering_focused = |focused: &Entity| {
+ let focused = focusables
+ .entities
+ .get(*focused)
+ .expect("Entity with `Focused` component must also have a `Focusable` component");
+ !is_in_node(world_cursor_pos, &focused)
+ };
+ // If the currently hovered node is the focused one, there is no need to
+ // find which node we are hovering and to switch focus to it (since we are
+ // already focused on it)
+ if focused.iter().all(not_hovering_focused) {
+ // We only run this code when we really need it because we iterate over all
+ // focusables, which can eat a lot of CPU.
+ let under_mouse = focusables
+ .entities
+ .iter()
+ .filter(|query_elem| is_in_node(world_cursor_pos, query_elem));
+ let under_mouse =
+ max_by_in_iter(under_mouse, |elem| elem.2.translation().z).map(|elem| elem.0);
+ let to_target = match under_mouse {
+ Some(c) => c,
+ None => return,
+ };
+ nav_cmds.send(NavRequest::FocusOn(to_target));
+ } else if released {
+ nav_cmds.send(NavRequest::Action);
+ }
+}
+
+/// Update [`ScreenBoundaries`] resource when the UI camera change
+/// (assuming there is a unique one).
+///
+/// See [`ScreenBoundaries`] doc for details.
+#[allow(clippy::type_complexity)]
+pub fn update_boundaries(
+ mut commands: Commands,
+ mut boundaries: Option>,
+ cam: Query<(&Camera, Option<&UiCameraConfig>), Or<(Changed, Changed)>>,
+ windows: Res,
+ images: Res>,
+) {
+ // TODO: this assumes there is only a single camera with activated UI.
+ let first_visible_ui_cam = |(cam, config): (_, Option<&UiCameraConfig>)| {
+ config.map_or(true, |c| c.show_ui).then_some(cam)
+ };
+ let mut update_boundaries = || {
+ let cam = cam.iter().find_map(first_visible_ui_cam)?;
+ let target_info = cam.target.get_render_target_info(&windows, &images)?;
+ let new_boundaries = ScreenBoundaries {
+ position: Vec2::ZERO,
+ screen_edge: Rect {
+ max: target_info.physical_size.as_vec2(),
+ min: Vec2::ZERO,
+ },
+ scale: 1.0,
+ };
+ if let Some(boundaries) = boundaries.as_mut() {
+ **boundaries = new_boundaries;
+ } else {
+ commands.insert_resource(new_boundaries);
+ }
+ Some(())
+ };
+ update_boundaries();
+}
+
+/// Default input systems for ui navigation.
+pub struct DefaultNavigationSystems;
+impl Plugin for DefaultNavigationSystems {
+ fn build(&self, app: &mut App) {
+ use crate::NavRequestSystem;
+ app.init_resource::()
+ .add_system(default_mouse_input.before(NavRequestSystem))
+ .add_system(default_gamepad_input.before(NavRequestSystem))
+ .add_system(default_keyboard_input.before(NavRequestSystem))
+ .add_system(
+ update_boundaries
+ .before(NavRequestSystem)
+ .before(default_mouse_input),
+ );
+ }
+}
diff --git a/crates/bevy_ui_navigation/Cargo.toml b/crates/bevy_ui_navigation/Cargo.toml
new file mode 100644
index 00000000000000..d8e31d886642f6
--- /dev/null
+++ b/crates/bevy_ui_navigation/Cargo.toml
@@ -0,0 +1,24 @@
+[package]
+name = "bevy_ui_navigation"
+version = "0.8.0-dev"
+edition = "2021"
+description = "A ECS-driven generic UI navigation for Bevy engine"
+homepage = "https://bevyengine.org"
+repository = "https://github.com/bevyengine/bevy"
+license = "MIT OR Apache-2.0"
+keywords = ["bevy"]
+
+[dependencies]
+# bevy
+bevy_app = { path = "../bevy_app", version = "0.8.0-dev" }
+bevy_core = { path = "../bevy_core", version = "0.8.0-dev" }
+bevy_derive = { path = "../bevy_derive", version = "0.8.0-dev" }
+bevy_ecs = { path = "../bevy_ecs", version = "0.8.0-dev" }
+bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.8.0-dev" }
+bevy_log = { path = "../bevy_log", version = "0.8.0-dev" }
+bevy_reflect = { path = "../bevy_reflect", version = "0.8.0-dev", features = [
+ "bevy",
+] }
+
+# other
+non-empty-vec = { version = "0.2.2", default-features = false }
diff --git a/crates/bevy_ui_navigation/README.md b/crates/bevy_ui_navigation/README.md
new file mode 100644
index 00000000000000..a5f6ac5b0812d2
--- /dev/null
+++ b/crates/bevy_ui_navigation/README.md
@@ -0,0 +1,513 @@
+# Bevy UI navigation
+
+The in-depth design specification is [available here][rfc41].
+
+### Examples
+
+Check out the [`examples`][examples] directory for bevy examples.
+
+![Demonstration of "Ultimate navigation" example](https://user-images.githubusercontent.com/26321040/141612751-ba0e62b2-23d6-429a-b5d1-48b09c10d526.gif)
+
+
+## Usage
+
+See [this example][example-simple] for a quick start guide.
+
+[The crate documentation is extensive][doc-root], but for practical reason
+doesn't include many examples. This page contains most of the doc examples,
+you should check the [examples directory][examples] 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::DefaultNavigationPlugins;
+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`]:
+
+```rust, no_run
+use bevy::prelude::*;
+use bevy_ui_navigation::{NavigationPlugin, NavRequest};
+
+fn custom_input_system_emitting_nav_requests(mut events: EventWriter) {
+ // 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`][examples] for more example code.
+
+`bevy-ui-navigation` provides a variety of ways to handle navigation actions.
+Check out the [`event_helpers`][module-event_helpers] module for more examples.
+
+```rust
+use bevy::{app::AppExit, prelude::*};
+use bevy_ui_navigation::event_helpers::NavEventQuery;
+
+#[derive(Component)]
+enum MenuButton {
+ StartGame,
+ ToggleFullscreen,
+ ExitGame,
+ Counter(i32),
+ //.. etc.
+}
+
+fn handle_nav_events(mut buttons: NavEventQuery<&mut MenuButton>, mut exit: EventWriter) {
+ // Do something when player activates (click, press "A" etc.) a `Focusable` button.
+ match buttons.single_activated_mut().deref_mut().ignore_remaining() {
+ Some(MenuButton::StartGame) => {
+ // start the game
+ }
+ Some(MenuButton::ToggleFullscreen) => {
+ // toggle fullscreen here
+ }
+ Some(MenuButton::ExitGame) => {
+ exit.send(AppExit);
+ }
+ Some(MenuButton::Counter(count)) => {
+ *count += 1;
+ }
+ //.. etc.
+ None => {}
+ }
+}
+```
+
+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 bevy_ui_navigation::Focusable;
+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`][Changed] query parameter as follow:
+```rust
+use bevy::prelude::*;
+use bevy_ui_navigation::{FocusState, Focusable};
+
+fn button_system(
+ mut focusables: Query<(&Focusable, &mut UiColor), Changed>,
+) {
+ 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:
+```rust, no_run
+use bevy::prelude::*;
+use bevy_ui_navigation::{NavRequestSystem, NavRequest, NavigationPlugin};
+
+fn custom_mouse_input(mut events: EventWriter) {
+ // 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`].
+
+```rust
+use bevy::prelude::*;
+use bevy_ui_navigation::NavRequest;
+
+fn set_focus_to_arbitrary_focusable(
+ 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 dormant [`Focusable`] with [`Focusable::dormant`].
+
+### `NavMenu`s
+
+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 [`NavMenu`].
+
+The high level usage of [`NavMenu`] is as follow:
+1. First you need a "root" [`NavMenu`].
+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")`][Name] component in addition to the
+ [`Focusable`] component to your options button.
+ * Pre-spawn the options button and store somewhere it's [`Entity` id][entity-id]
+ (`let opt_btn = commands.spawn_bundle(FocusableButtonBundle).id();`)
+3. to the `NodeBundle` containing all the options menu [`Focusable`] entities,
+ you add the following bundle:
+ * [`NavMenu::Bound2d.reachable_from_named("opt_btn_name")`][NavMenu::reachable_from_named]
+ if you opted for adding the `Name` component.
+ * [`NavMenu::Bound2d.reachable_from(opt_btn)`][NavMenu::reachable_from]
+ if you have the [`Entity`] id.
+
+In code, This will look like this:
+```rust
+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) {
+ 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)`][`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][module-systems].
+
+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 `NavMenu`s
+
+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 to `NavMenu::Bound*`) enables looping
+ navigation, where going offscreen in one direction "wraps" to the opposite
+ screen edge.
+* `NavMenu::*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 [`NavMenu`] documentation or the ["ultimate" menu navigation
+example][example-ultimate] for details.
+
+
+#### Marking
+
+If you need to know from which menu a [`NavEvent::FocusChanged`] originated, you
+can use one of the [`marking`][module-marking] methods on the `NavMenu` seeds.
+
+A usage demo is available in [the `marking.rs` example][example-marking].
+
+
+## Changelog
+
+* `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 frame
+* `0.9.1`: Fix #8, Panic on diagonal gamepad input
+* `0.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
+ * **Breaking**: if you were manually calling `default_mouse_input`, it now has
+ additional parameters
+ * **Breaking**: `ui_focusable_at` and `NodePosQuery` now have type parameters
+* `0.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.
+ * Breaking: New enum variants on `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:
+ * Add automatic submenu access for `scope` menus.
+ * Rename examples, they were named weirdly.
+ * **Breaking**: Replace `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.
+ * **Breaking**: `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.
+ * **Breaking**: Remove `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.
+ * **Breaking**: A lot of struct and module reordering to make documentation
+ more discoverable. Notably `Direction` and `ScopeDirection` are now in the
+ `events` module.
+* `0.13.1`: Fix broken URLs in Readme.md
+* `0.14.0`: Some important changes, and a bunch of new very useful features.
+ * Add a [`Focusable::dormant`] constructor to specify which focusable you want
+ to be the first to focus, this also works for [`Focusable`]s within
+ [`NavMenu`]s.
+ * **Important**: This changes the library behavior, now there will
+ automatically be a `Focused` entity set. Add a system to set the first
+ `Focused` whenever `Focusable`s are added to the world.
+ * Add [`NavEvent::InitiallyFocused`] to handle this first `Focused` event.
+ * Early-return in `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.
+ * Do not crash when resolving `NavRequest`s while no `Focusable`s exists in
+ the world. Instead, it now prints a warning message.
+ * **Important**: Now the focus handling algorithm supports multiple `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.
+ * This also means the focus changes are visible as soon as the system ran,
+ the new `NavRequestSystem` label 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.rs` without the `build_ui!` macro
+ because we shouldn't expect users to be familiar with my personal weird
+ macro.
+ * **Breaking**: Remove `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`:
+ * Cycling now wraps around the screen properly, regardless of UI camera
+ position and scale. See the new `off_screen_focusables.rs` example for a demo.
+ * Fix the `default_mouse_input` system not accounting for camera scale.
+ * Update examples to make use of `NavRequestSystem` label, 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 `Overflow` feature), 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 `UiCamera` marker component, please
+ use the bevy native `bevy::ui::entity::CameraUi` instead. Furthermore, the
+ `default_mouse_input` system 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.
+* `0.17.0`: Non-breaking, but due to cargo semver handling is a minor bump.
+ * Add the `event_helpers` module to simplify ui event handling
+ * Fix README and crate-level documentation links
+* `0.18.0`:
+ * **Breaking**: Remove marker generic type parameter from `systems::NodePosQuery`.
+ The [`generic_default_mouse_input`] system now relies on the newly introduced
+ [`ScreenBoundaries`] that abstracts camera offset and zoom settings.
+ * **Important**: Introduced new plugins to make it even simpler to get started.
+ * Add `event_helpers` module introduction to README.
+ * Fix `bevy-ui` feature not building. This issue was introduced in `0.16.0`.
+* `0.19.0`: **Breaking**: Update to bevy 0.8.0
+ * **Warning**: 0.8.0 removed the ability for the user to change the ui camera position
+ and perspective, see
+ 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_navigation`] field.
+ * 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 using `DefaultNavigationPlugins` instead,
+ if it is not possible, then use `NavigationPlugin::new()`.
+
+[`ButtonBundle`]: https://docs.rs/bevy/0.8.0/bevy/ui/entity/struct.ButtonBundle.html
+[Changed]: https://docs.rs/bevy/0.8.0/bevy/ecs/prelude/struct.Changed.html
+[doc-root]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/
+[`Entity`]: https://docs.rs/bevy/0.8.0/bevy/ecs/entity/struct.Entity.html
+[entity-id]: https://docs.rs/bevy/0.8.0/bevy/ecs/system/struct.EntityCommands.html#method.id
+[example-marking]: https://github.com/nicopap/ui-navigation/tree/v0.19.0/examples/marking.rs
+[examples]: https://github.com/nicopap/ui-navigation/tree/v0.19.0/examples
+[example-simple]: https://github.com/nicopap/ui-navigation/tree/v0.19.0/examples/simple.rs
+[example-ultimate]: https://github.com/nicopap/ui-navigation/blob/v0.19.0/examples/ultimate_menu_navigation.rs
+[`FocusableButtonBundle`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/components/struct.FocusableButtonBundle.html
+[`Focusable::cancel`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/struct.Focusable.html#method.cancel
+[`Focusable::dormant`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/struct.Focusable.html#method.dormant
+[`Focusable`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/struct.Focusable.html
+[`Focusable::lock`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/struct.Focusable.html#method.lock
+[`generic_default_mouse_input`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/systems/fn.generic_default_mouse_input.html
+[`InputMapping`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/systems/struct.InputMapping.html
+[module-event_helpers]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/event_helpers/index.html
+[module-marking]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/bundles/struct.MenuSeed.html#method.marking
+[module-systems]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/systems/index.html
+[Name]: https://docs.rs/bevy/0.8.0/bevy/core/enum.Name.html
+[`NavEvent::FocusChanged`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/events/enum.NavEvent.html#variant.FocusChanged
+[`NavEvent`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/events/enum.NavEvent.html
+[`NavEvent::InitiallyFocused`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/events/enum.NavEvent.html#variant.InitiallyFocused
+[`NavMenu`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/enum.NavMenu.html
+[NavMenu::reachable_from]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/enum.NavMenu.html#method.reachable_from
+[NavMenu::reachable_from_named]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/enum.NavMenu.html#method.reachable_from_named
+[`NavRequest`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/events/enum.NavRequest.html
+[`NavRequest::Action`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/events/enum.NavRequest.html#variant.Action
+[`NavRequest::FocusOn`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/events/enum.NavRequest.html#variant.FocusOn
+[`NavRequest::Free`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/events/enum.NavRequest.html#variant.Free
+[`NavRequest::ScopeMove`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/events/enum.NavRequest.html#variant.ScopeMove
+[`NavRequestSystem`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/struct.NavRequestSystem.html
+[rfc41]: https://github.com/nicopap/rfcs/blob/ui-navigation/rfcs/41-ui-navigation.md
+[`ScreenBoundaries`]: https://docs.rs/bevy-ui-navigation/0.19.0/bevy_ui_navigation/struct.ScreenBoundaries.html
+
+
+### Version matrix
+
+| bevy | latest supporting version |
+|------|--------|
+| 0.8 | 0.19.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.
diff --git a/crates/bevy_ui_navigation/src/commands.rs b/crates/bevy_ui_navigation/src/commands.rs
new file mode 100644
index 00000000000000..3f104300ae6616
--- /dev/null
+++ b/crates/bevy_ui_navigation/src/commands.rs
@@ -0,0 +1,21 @@
+use bevy_ecs::{entity::Entity, prelude::World, system::Command};
+
+use crate::{FocusState, Focused};
+
+pub(crate) fn set_focus_state(entity: Entity, new_state: FocusState) -> UpdateFocusable {
+ UpdateFocusable { entity, new_state }
+}
+pub(crate) struct UpdateFocusable {
+ entity: Entity,
+ new_state: FocusState,
+}
+impl Command for UpdateFocusable {
+ fn write(self, world: &mut World) {
+ let mut entity = world.entity_mut(self.entity);
+ if matches!(self.new_state, FocusState::Focused) {
+ entity.insert(Focused);
+ } else {
+ entity.remove::();
+ }
+ }
+}
diff --git a/crates/bevy_ui_navigation/src/event_helpers.rs b/crates/bevy_ui_navigation/src/event_helpers.rs
new file mode 100644
index 00000000000000..41a080ac646744
--- /dev/null
+++ b/crates/bevy_ui_navigation/src/event_helpers.rs
@@ -0,0 +1,485 @@
+//! Helpers for reacting to [`NavEvent`]s.
+//!
+//! This module defines [`SystemParam`] with methods that helps structure code
+//! reacting to [`NavEvent`]s.
+//!
+//! The goal is to provide a conceptually simple abstraction over the specifics
+//! of `bevy_ui_navigation` while preserving access to the specifics for the
+//! rare occurences when it is needed.
+//!
+//! # Motivation
+//!
+//! The default way of hooking your UI to your code in `bevy_ui_navigation` is
+//! a bit rough:
+//! * You need to listen for [`NavEvent`],
+//! * filter for the ones you care about (typically [`NavEvent::NoChanges`]),
+//! * check what [`NavRequest`] triggered it,
+//! * retrieve the focused entity from the event,
+//! * check against your own queries what entity it is,
+//! * write code for each case you want to handle
+//!
+//! It is not _awful_, but it requires a deep familiarity with this crates'
+//! way of doing things:
+//! ```rust
+//! use bevy::prelude::*;
+//! use bevy_ui_navigation::events::{NavEvent, NavRequest};
+//!
+//! #[derive(Component)]
+//! enum MainMenuButton { Start, Options, Exit }
+//! /// Marker component
+//! #[derive(Component)] struct ActiveButton;
+//!
+//! fn handle_ui(
+//! mut events: EventReader,
+//! buttons: Query<&MainMenuButton, With>,
+//! ) {
+//! // iterate NavEvents
+//! for event in events.iter() {
+//! // Check for a `NoChanges` event with Action
+//! if let NavEvent::NoChanges { from, request: NavRequest::Action } = event {
+//! // Get the focused entity (from.first()), check the button for it.
+//! match buttons.get(*from.first()) {
+//! // Do things when specific button is activated
+//! Ok(MainMenuButton::Start) => {}
+//! Ok(MainMenuButton::Options) => {}
+//! Ok(MainMenuButton::Exit) => {}
+//! Err(_) => {}
+//! }
+//! }
+//! }
+//! }
+//! ```
+//!
+//! This module defines two [`SystemParam`]:
+//! * [`NavEventQuery`]: A wrapper around a query to limit iteration to
+//! item activated this cycle.
+//! * [`NavEventReader`]: A wrapper around `EventReader` that
+//! add extra methods specific to iterating `NavEvent`s.
+//!
+//! Those `SystemParam` are accessed by specifying them as system arguments.
+//!
+//! ## [`NavEventReader`]
+//!
+//! [`NavEventReader`] works exactly like `EventReader` (you can even access
+//! the inner reader) but gives you choice on how to iterate the `NavEvent`s.
+//!
+//! Check the [`NavEventReader`] docs for usage examples and use cases.
+//!
+//! ## [`NavEventQuery`]
+//!
+//! [`NavEventQuery`] works exactly like a query, appart that it doesn't define
+//! `iter()`, but [`iter_requested()`](NavEventQuery::iter_requested) and
+//! [`iter_activated()`](NavEventQuery::iter_activated). You use it exactly
+//! like the bevy `Query` system parameter. This let you just iterate over
+//! the entities that got activated since last frame.
+//!
+//! Check the [`NavEventQuery`] docs for usage examples and use cases.
+//!
+//! # Examples
+//!
+//! Those `SystemParam` should let you react to `NavEvent`s under your own terms.
+//! The simplest way to handle `NavEvent`s is with the `NavEventQuery` parameter:
+//!
+//! ```rust
+//! use bevy::prelude::*;
+//! use bevy_ui_navigation::event_helpers::NavEventQuery;
+//!
+//! #[derive(Component)]
+//! enum MainMenuButton { Start, Options, Exit }
+//! /// Marker component
+//! #[derive(Component)] struct ActiveButton;
+//!
+//! fn handle_ui(mut button_events: NavEventQuery<&MainMenuButton, With>) {
+//! // NOTE: this will silently ignore multiple navigation event at the same frame.
+//! // It should be a very rare occurance.
+//! match button_events.single_activated().ignore_remaining() {
+//! // Do things when specific button is activated
+//! Some(MainMenuButton::Start) => {}
+//! Some(MainMenuButton::Options) => {}
+//! Some(MainMenuButton::Exit) => {}
+//! None => {}
+//! }
+//! }
+//! ```
+//!
+//! # Disclaimer
+//!
+//! This is a very new API and it is likely to be too awkward to use in a real-world
+//! use case. But the aim is to make it possible to use this API in 90% of cases. I
+//! don't think it's currently the case. Feel free to open an issue discussing possible
+//! improvements.
+use std::ops::{Deref, DerefMut};
+
+use bevy_ecs::{
+ entity::Entity,
+ event::EventReader,
+ query::{QueryItem, ROQueryItem, WorldQuery},
+ system::{Query, SystemParam},
+};
+
+use crate::events::{NavEvent, NavRequest};
+
+/// A thing that should exist or not, but possibly could erroneously be multiple.
+pub enum SingleResult {
+ /// There is exactly one `T`.
+ One(T),
+ /// There is exactly no `T`.
+ None,
+ /// There is more than one `T`, holding the first `T`.
+ MoreThanOne(T),
+}
+impl SingleResult {
+ /// Assume the [`SingleResult::MoreThanOne`] case doesn't exist.
+ ///
+ /// # Panics
+ ///
+ /// When `self` is [`SingleResult::MoreThanOne`].
+ pub fn unwrap_opt(self) -> Option {
+ match self {
+ Self::MoreThanOne(_) => panic!("There was more than one `SingleResult`"),
+ Self::One(t) => Some(t),
+ Self::None => None,
+ }
+ }
+ /// Return contained value, not caring even if there is more than one result.
+ pub fn ignore_remaining(self) -> Option {
+ match self {
+ Self::MoreThanOne(t) | Self::One(t) => Some(t),
+ Self::None => None,
+ }
+ }
+ pub fn deref_mut(&mut self) -> SingleResult<&mut ::Target>
+ where
+ T: DerefMut,
+ {
+ match self {
+ Self::MoreThanOne(t) => SingleResult::MoreThanOne(t.deref_mut()),
+ Self::One(t) => SingleResult::One(t.deref_mut()),
+ Self::None => SingleResult::None,
+ }
+ }
+ fn new(from: Option, is_multiple: bool) -> Self {
+ match (from, is_multiple) {
+ (Some(t), false) => Self::One(t),
+ (Some(t), true) => Self::MoreThanOne(t),
+ (None, _) => Self::None,
+ }
+ }
+}
+
+/// Types of [`NavEvent`] that can be "emitted" by focused elements.
+pub enum NavEventType {
+ /// [`NavEvent::Locked`].
+ Locked,
+ /// [`NavEvent::Unlocked`].
+ Unlocked,
+ /// [`NavEvent::FocusChanged`].
+ FocusChanged,
+ /// [`NavEvent::NoChanges`].
+ NoChanges(NavRequest),
+}
+
+/// An [`EventReader`] with methods to filter for meaningful events.
+/// Use this like an `EventReader`, but with extra functionalities:
+/// ```rust
+/// # use bevy::prelude::*;
+/// use bevy_ui_navigation::event_helpers::{NavEventReader, NavEventType};
+/// use bevy_ui_navigation::events::NavRequest;
+///
+/// # #[derive(Component)] enum MainMenuButton { Start, Options, Exit }
+/// # #[derive(Component)] struct ActiveButton;
+/// fn handle_ui(
+/// mut events: NavEventReader,
+/// buttons: Query<&MainMenuButton, With>,
+/// ) {
+/// for (event_type, from) in events.type_iter() {
+/// match (buttons.get(from), event_type) {
+/// (Ok(MainMenuButton::Start), NavEventType::NoChanges(NavRequest::Action)) => {}
+/// (Ok(MainMenuButton::Start), NavEventType::Locked) => {}
+/// (Ok(MainMenuButton::Start), _) => {}
+/// (Ok(MainMenuButton::Options), _) => {}
+/// _ => {}
+/// // etc..
+/// }
+/// }
+/// }
+/// ```
+/// See methods documentation for what type of iterators you can get.
+#[derive(SystemParam)]
+pub struct NavEventReader<'w, 's> {
+ pub events: EventReader<'w, 's, NavEvent>,
+}
+impl<'w, 's> NavEventReader<'w, 's> {
+ /// Event reader of [`events`](NavEventReader::events) filtered
+ /// to only keep [`NavEvent::NoChanges`].
+ ///
+ /// Note that this iterator usually has 0 or 1 element.
+ pub fn unchanged(&mut self) -> impl DoubleEndedIterator- + '_ {
+ self.events.iter().filter_map(|event| {
+ if let NavEvent::NoChanges { from, request } = event {
+ Some((*request, *from.first()))
+ } else {
+ None
+ }
+ })
+ }
+
+ /// Iterate [`NavEvent`] by type.
+ ///
+ /// Note that this iterator usually has 0 or 1 element.
+ pub fn type_iter(&mut self) -> impl DoubleEndedIterator
- + '_ {
+ use NavEventType::{FocusChanged, Locked, NoChanges, Unlocked};
+ self.events.iter().filter_map(|event| match event {
+ NavEvent::FocusChanged { from, .. } => Some((FocusChanged, *from.first())),
+ NavEvent::Locked(from) => Some((Locked, *from)),
+ NavEvent::Unlocked(from) => Some((Unlocked, *from)),
+ NavEvent::NoChanges { from, request } => Some((NoChanges(*request), *from.first())),
+ _ => None,
+ })
+ }
+
+ /// The entities that got an unhandled `request` [`NavRequest`] this frame.
+ ///
+ /// Note that this iterator usually has 0 or 1 element.
+ pub fn caught(&mut self, request: NavRequest) -> impl DoubleEndedIterator
- + '_ {
+ self.unchanged()
+ .filter_map(move |(req, entity)| (req == request).then(|| entity))
+ }
+
+ /// The entities that got an unhandled [`NavRequest::Action`] this frame.
+ ///
+ /// An unhandled `Action` happens typically when the user presses the action
+ /// key on a focused entity. The [`Entity`] returned here will be the focused
+ /// entity.
+ ///
+ /// Typically there will be 0 or 1 such entity. It's technically possible to
+ /// have more than one activated entity in the same frame, but extremely
+ /// unlikely. Please account for that case.
+ pub fn activated(&mut self) -> impl DoubleEndedIterator
- + '_ {
+ self.caught(NavRequest::Action)
+ }
+
+ /// Variation of [`NavEventReader::caught`] where more than one `NavEvent` is
+ /// explicitly excluded.
+ pub fn single_caught(&mut self, request: NavRequest) -> SingleResult {
+ let mut iterated = self.caught(request);
+ let first = iterated.next();
+ let one_more = iterated.next().is_some();
+ SingleResult::new(first, one_more)
+ }
+
+ /// Variation of [`NavEventReader::activated`] where more than one `NavEvent` is
+ /// explicitly excluded.
+ pub fn single_activated(&mut self) -> SingleResult {
+ self.single_caught(NavRequest::Action)
+ }
+}
+
+/// Convinient wrapper around a query for a quick way of handling UI events.
+///
+/// See [the module level doc](crate::event_helpers) for justification and
+/// use case, see the following methods documentation for specifics on how
+/// to use this [`SystemParam`].
+///
+/// This is a bevy [`SystemParam`], you access it by specifying it as parameter
+/// to your systems:
+///
+/// ```rust
+/// # use bevy::prelude::*;
+/// use bevy_ui_navigation::event_helpers::NavEventQuery;
+/// # #[derive(Component)] enum MenuButton { StartGame, Quit, JoinFriend }
+///
+/// fn handle_ui(mut button_events: NavEventQuery<&MenuButton>) {}
+/// ```
+#[derive(SystemParam)]
+pub struct NavEventQuery<'w, 's, Q: WorldQuery + 'static, F: 'static + WorldQuery = ()> {
+ query: Query<'w, 's, Q, F>,
+ events: NavEventReader<'w, 's>,
+}
+impl<'w, 's, Q: WorldQuery, F: WorldQuery> NavEventQuery<'w, 's, Q, F> {
+ /// Like [`NavEventQuery::iter_activated`], but for mutable queries.
+ ///
+ /// You can use this method to get mutable access to items from the `Q`
+ /// `WorldQuery`.
+ ///
+ /// It is however recommended that you use a [`NavEventReader`] method
+ /// with your own queries if you have any kind of complex access pattern
+ /// to your mutable data.
+ ///
+ /// With the `MenuButton` example from [`NavEventQuery::iter_activated`],
+ /// you could use this method as follow:
+ ///
+ /// ```rust
+ /// use bevy::prelude::*;
+ /// use bevy::app::AppExit;
+ /// use bevy_ui_navigation::event_helpers::NavEventQuery;
+ /// # use bevy::ecs::system::SystemParam;
+ /// # #[derive(SystemParam)] struct StartGameQuery<'w, 's> { foo: Query<'w, 's, ()> }
+ /// #[derive(Component)]
+ /// enum MenuButton {
+ /// StartGame,
+ /// Quit,
+ /// Counter(i64),
+ /// }
+ ///
+ /// fn start_game(queries: &mut StartGameQuery) { /* ... */ }
+ ///
+ /// fn handle_menu_button(
+ /// mut buttons: NavEventQuery<&mut MenuButton>,
+ /// mut app_evs: EventWriter,
+ /// mut start_game_query: StartGameQuery,
+ /// ) {
+ /// // WARNING: using `deref_mut` here triggers change detection regardless
+ /// // of whether we changed anything, but there is no other ways to
+ /// // pattern-match on `MenuButton` in rust in this case.
+ /// match buttons.single_activated_mut().deref_mut().unwrap_opt() {
+ /// Some(MenuButton::Counter(i)) => *i += 1,
+ /// Some(MenuButton::StartGame) => start_game(&mut start_game_query),
+ /// Some(MenuButton::Quit) => app_evs.send(AppExit),
+ /// None => {},
+ /// }
+ /// }
+ /// ```
+ pub fn single_activated_mut(&mut self) -> SingleResult> {
+ self.single_caught_mut(NavRequest::Action)
+ }
+ /// Like [`NavEventQuery::single_activated_mut`] but for non-mutable queries.
+ ///
+ /// ```rust
+ /// use bevy::prelude::*;
+ /// use bevy::app::AppExit;
+ /// use bevy_ui_navigation::event_helpers::NavEventQuery;
+ /// # use bevy::ecs::system::SystemParam;
+ /// # #[derive(SystemParam)] struct StartGameQuery<'w, 's> { foo: Query<'w, 's, ()> }
+ /// #[derive(Component)]
+ /// enum MenuButton {
+ /// StartGame,
+ /// Quit,
+ /// Options,
+ /// }
+ ///
+ /// fn start_game(queries: &mut StartGameQuery) { /* ... */ }
+ ///
+ /// fn handle_menu_button(
+ /// mut buttons: NavEventQuery<&MenuButton>,
+ /// mut app_evs: EventWriter,
+ /// mut start_game_query: StartGameQuery,
+ /// ) {
+ /// match buttons.single_activated().unwrap_opt() {
+ /// Some(MenuButton::Options) => {/* do something optiony */},
+ /// Some(MenuButton::StartGame) => start_game(&mut start_game_query),
+ /// Some(MenuButton::Quit) => app_evs.send(AppExit),
+ /// None => {},
+ /// }
+ /// }
+ /// ```
+ pub fn single_activated(&mut self) -> SingleResult> {
+ match self.events.single_caught(NavRequest::Action) {
+ SingleResult::MoreThanOne(e) => SingleResult::new(self.query.get(e).ok(), true),
+ SingleResult::One(e) => SingleResult::new(self.query.get(e).ok(), false),
+ SingleResult::None => SingleResult::None,
+ }
+ }
+
+ /// Like [`NavEventQuery::single_activated_mut`] but for arbitrary
+ /// `react_to` [`NavRequest`].
+ pub fn single_caught_mut(&mut self, react_to: NavRequest) -> SingleResult> {
+ match self.events.single_caught(react_to) {
+ SingleResult::MoreThanOne(e) => SingleResult::new(self.query.get_mut(e).ok(), true),
+ SingleResult::One(e) => SingleResult::new(self.query.get_mut(e).ok(), false),
+ SingleResult::None => SingleResult::None,
+ }
+ }
+
+ /// Iterate over items of `Q` that received a `react_to` [`NavRequest`]
+ /// not handled by the navigation engine.
+ ///
+ /// This method is used like [`NavEventQuery::iter_activated`], but with
+ /// the additional `react_to` argument.
+ ///
+ /// Note that this iterator is usualy of length 0 or 1.
+ ///
+ /// See the [`NavEventQuery::iter_activated`] documentation for details.
+ pub fn iter_requested(&mut self, react_to: NavRequest) -> impl Iterator
- > {
+ let Self { events, query } = self;
+ events.caught(react_to).filter_map(|e| query.get(e).ok())
+ }
+
+ /// Iterate over received [`NavEvent`] types associated with the
+ /// corresponding result from `Q`.
+ ///
+ /// Note that this iterator is usualy of length 0 or 1.
+ ///
+ /// This is similar to [`NavEventReader::type_iter`], but instead of
+ /// returning an entity, it returns the query item of `Q`
+ pub fn iter_types(
+ &mut self,
+ ) -> impl DoubleEndedIterator
- )> {
+ let Self { events, query } = self;
+ events
+ .type_iter()
+ .filter_map(|(t, e)| query.get(e).ok().map(|e| (t, e)))
+ }
+
+ /// Iterate over items of `Q` that received a [`NavRequest::Action`]
+ /// not handled by the navigation engine.
+ ///
+ /// This method is very useful for UI logic. You typically want to react
+ /// to [`NavEvent::NoChanges`] where the `request` field is
+ /// [`NavRequest::Action`]. This happens when the user clicks a button or
+ /// when they press the `Action` button on their controller.
+ ///
+ /// Note that this iterator is usualy of length 0 or 1.
+ ///
+ /// # Mutable queries
+ ///
+ /// This method cannot be used with `WorldQueries` with mutable
+ /// world access. You should use [`NavEventQuery::single_activated_mut`] instead.
+ ///
+ /// # Example
+ ///
+ /// We have a menu where each button has a `MenuButton` component. We want
+ /// to do something special when the player clicks a specific button, to
+ /// react to specific buttons we would create a system that accepts a
+ /// `NavEventQuery<&MenuButton>` and uses it as follow:
+ ///
+ /// ```rust
+ /// use bevy::prelude::*;
+ /// use bevy::app::AppExit;
+ /// # use bevy::ecs::system::SystemParam;
+ /// # #[derive(SystemParam)] struct StartGameQuery<'w, 's> { foo: Query<'w, 's, ()> }
+ /// # #[derive(SystemParam)] struct JoinFriendQuery<'w, 's> { foo: Query<'w, 's, ()> }
+ /// use bevy_ui_navigation::event_helpers::NavEventQuery;
+ ///
+ /// #[derive(Component)]
+ /// enum MenuButton {
+ /// StartGame,
+ /// Quit,
+ /// JoinFriend,
+ /// }
+ ///
+ /// fn start_game(queries: &mut StartGameQuery) { /* ... */ }
+ /// fn join_friend(queries: &mut JoinFriendQuery) { /* ... */ }
+ ///
+ /// fn handle_menu_button(
+ /// mut buttons: NavEventQuery<&MenuButton>,
+ /// mut app_evs: EventWriter,
+ /// mut start_game_query: StartGameQuery,
+ /// mut join_friend_query: JoinFriendQuery,
+ /// ) {
+ /// for activated_button in buttons.iter_activated() {
+ /// match activated_button {
+ /// MenuButton::StartGame => start_game(&mut start_game_query),
+ /// MenuButton::Quit => app_evs.send(AppExit),
+ /// MenuButton::JoinFriend => join_friend(&mut join_friend_query),
+ /// }
+ /// }
+ /// }
+ /// ```
+ ///
+ /// If you are curious how `StartGameQuery` was defined, check out the bevy
+ /// [`SystemParam`] trait!
+ pub fn iter_activated(&mut self) -> impl Iterator
- > {
+ self.iter_requested(NavRequest::Action)
+ }
+}
diff --git a/crates/bevy_ui_navigation/src/events.rs b/crates/bevy_ui_navigation/src/events.rs
new file mode 100644
index 00000000000000..faa346c41c66a3
--- /dev/null
+++ b/crates/bevy_ui_navigation/src/events.rs
@@ -0,0 +1,118 @@
+//! Navigation events and requests.
+//!
+//! The navigation system works through bevy's `Events` system. Basically, it is
+//! a system with one input and two outputs:
+//! * Input [`Events`](https://docs.rs/bevy/0.8.0/bevy/app/struct.Events.html),
+//! tells the navigation system what to do. Your app should have a system
+//! that writes to a [`EventWriter`](https://docs.rs/bevy/0.8.0/bevy/app/struct.EventWriter.html)
+//! based on inputs or internal game state. Usually, the default input systems specified
+//! in [`crate::systems`] do that for you. But you can add your own requests
+//! on top of the ones the default systems send. For example to unlock the UI with
+//! [`NavRequest::Free`].
+//! * Output [`Focusable`](crate::Focusable) components. The navigation system
+//! updates the focusables component according to the focus state of the
+//! navigation system. See examples directory for how to read those
+//! * Output [`EventReader`](https://docs.rs/bevy/0.8.0/bevy/app/struct.EventReader.html),
+//! contains specific information about what the navigation system is doing.
+use bevy_ecs::entity::Entity;
+use non_empty_vec::NonEmpty;
+
+/// Requests to send to the navigation system to update focus.
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum NavRequest {
+ /// Move in 2d in provided direction.
+ Move(Direction),
+ /// Move within the encompassing [`NavMenu::BoundScope`](crate::NavMenu::BoundScope).
+ ScopeMove(ScopeDirection),
+ /// Enter submenu if any [`NavMenu::reachable_from`](crate::NavMenu::reachable_from)
+ /// the currently focused entity.
+ Action,
+ /// Leave this submenu to enter the one it is [`reachable_from`](crate::NavMenu::reachable_from).
+ Cancel,
+ /// Move the focus to any arbitrary [`Focusable`](crate::Focusable) entity.
+ FocusOn(Entity),
+ /// Unlocks the navigation system.
+ ///
+ /// A [`NavEvent::Unlocked`] will be emitted as a response if the
+ /// navigation system was indeed locked.
+ Free,
+}
+
+/// Direction for movement in [`NavMenu::BoundScope`](crate::NavMenu::BoundScope) menus.
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum ScopeDirection {
+ Next,
+ Previous,
+}
+
+/// 2d direction to move in normal menus
+#[derive(Debug, PartialEq, Clone, Copy)]
+pub enum Direction {
+ South,
+ North,
+ East,
+ West,
+}
+
+/// Events emitted by the navigation system.
+///
+/// Useful if you want to react to [`NavEvent::NoChanges`] event, for example
+/// when a "start game" button is focused and the [`NavRequest::Action`] is
+/// pressed.
+#[derive(Debug, Clone)]
+pub enum NavEvent {
+ /// Tells the app which element is the first one to be focused.
+ ///
+ /// This will be sent whenever the number of focused elements go from 0 to 1.
+ /// Meaning: whenever you spawn a new UI with [`crate::Focusable`] elements.
+ InitiallyFocused(Entity),
+
+ /// Focus changed
+ ///
+ /// ## Notes
+ ///
+ /// Both `to` and `from` are ascending, meaning that the focused and newly
+ /// focused elements are the first of their respective vectors.
+ ///
+ /// [`NonEmpty`] enables you to safely check `to.first()` or `from.first()`
+ /// without returning an option. It is guaranteed that there is at least
+ /// one element.
+ FocusChanged {
+ /// The list of elements that has become active after the focus
+ /// change
+ to: NonEmpty,
+ /// The list of active elements from the focused one to the last
+ /// active which is affected by the focus change
+ from: NonEmpty,
+ },
+ /// The [`NavRequest`] didn't lead to any change in focus.
+ NoChanges {
+ /// The list of active elements from the focused one to the last
+ /// active which is affected by the focus change
+ from: NonEmpty,
+ /// The [`NavRequest`] that didn't do anything
+ request: NavRequest,
+ },
+ /// A [lock focusable](crate::Focusable::lock) has been triggered
+ ///
+ /// Once the navigation plugin enters a locked state, the only way to exit
+ /// it is to send a [`NavRequest::Free`].
+ Locked(Entity),
+
+ /// A [lock focusable](crate::Focusable::lock) has been triggered
+ ///
+ /// Once the navigation plugin enters a locked state, the only way to exit
+ /// it is to send a [`NavRequest::Free`].
+ Unlocked(Entity),
+}
+impl NavEvent {
+ /// Convenience function to construct a `FocusChanged` with a single `to`
+ ///
+ /// Usually the `NavEvent::FocusChanged.to` field has a unique value.
+ pub(crate) fn focus_changed(to: Entity, from: NonEmpty) -> NavEvent {
+ NavEvent::FocusChanged {
+ from,
+ to: NonEmpty::new(to),
+ }
+ }
+}
diff --git a/crates/bevy_ui_navigation/src/lib.rs b/crates/bevy_ui_navigation/src/lib.rs
new file mode 100644
index 00000000000000..643e7391d7136b
--- /dev/null
+++ b/crates/bevy_ui_navigation/src/lib.rs
@@ -0,0 +1,94 @@
+mod commands;
+pub mod event_helpers;
+pub mod events;
+mod named;
+mod resolve;
+mod seeds;
+
+use std::marker::PhantomData;
+
+use bevy_app::prelude::*;
+use bevy_ecs::{
+ schedule::{ParallelSystemDescriptorCoercion, SystemLabel},
+ system::{SystemParam, SystemParamItem},
+};
+
+pub use events::{NavEvent, NavRequest};
+pub use non_empty_vec::NonEmpty;
+pub use resolve::{FocusAction, FocusState, Focusable, Focused, MoveParam, NavLock};
+pub use seeds::NavMenu;
+
+/// The [`Bundle`](bevy::prelude::Bundle)s
+/// returned by the [`NavMenu`] methods.
+pub mod bundles {
+ pub use crate::seeds::{MenuSeed, NamedMenuSeed};
+}
+
+/// The label of the system in which the [`NavRequest`] events are handled, the
+/// focus state of the [`Focusable`]s is updated and the [`NavEvent`] events
+/// are sent.
+///
+/// Systems updating visuals of UI elements should run _after_ the `NavRequestSystem`,
+/// while systems that emit [`NavRequest`] should run _before_ it. For example, the
+/// [`systems::default_mouse_input`] systems should run before the `NavRequestSystem`.
+///
+/// Failing to do so won't cause logical errors, but will make the UI feel more slugish
+/// than necessary. This is especially critical of you are running on low framerate.
+///
+/// # Example
+///
+/// ```rust, no_run
+/// use bevy::prelude::*;
+/// use bevy_ui_navigation::{NavRequestSystem, DefaultNavigationPlugins};
+/// # fn button_system() {}
+/// fn main() {
+/// App::new()
+/// .add_plugins(DefaultPlugins)
+/// .add_plugins(DefaultNavigationPlugins)
+/// // ...
+/// // Add the button color update system after the focus update system
+/// .add_system(button_system.after(NavRequestSystem))
+/// // ...
+/// .run();
+/// }
+/// ```
+#[derive(Clone, Debug, Hash, PartialEq, Eq, SystemLabel)]
+pub struct NavRequestSystem;
+
+/// The navigation plugin.
+///
+/// Add it to your app with `.add_plugin(NavigationPlugin)` and send
+/// [`NavRequest`]s to move focus within declared [`Focusable`] entities.
+///
+/// This means you'll also have to add manaully the systems from [`systems`]
+/// and [`systems::InputMapping`]. You should prefer [`DefaultNavigationPlugins`]
+/// if you don't want to bother with that.
+///
+/// # Note on generic parameters
+///
+/// The `MP` type parameter might seem complicated, but all you have to do
+/// is for your type to implement [`SystemParam`] and [`MoveParam`].
+/// See the [`resolve::UiProjectionQuery`] source code for implementation hints.
+pub struct GenericNavigationPlugin(PhantomData);
+unsafe impl Send for GenericNavigationPlugin {}
+unsafe impl Sync for GenericNavigationPlugin {}
+
+impl GenericNavigationPlugin {
+ pub fn new() -> Self {
+ Self(PhantomData)
+ }
+}
+impl Plugin for GenericNavigationPlugin
+where
+ for<'w, 's> SystemParamItem<'w, 's, MP>: MoveParam,
+{
+ fn build(&self, app: &mut App) {
+ app.add_event::()
+ .add_event::()
+ .insert_resource(NavLock::new())
+ .add_system(resolve::listen_nav_requests::.label(NavRequestSystem))
+ .add_system(resolve::set_first_focused)
+ .add_system(resolve::insert_tree_menus)
+ .add_system(named::resolve_named_menus.before(resolve::insert_tree_menus));
+ }
+}
diff --git a/crates/bevy_ui_navigation/src/named.rs b/crates/bevy_ui_navigation/src/named.rs
new file mode 100644
index 00000000000000..954a8e1725cace
--- /dev/null
+++ b/crates/bevy_ui_navigation/src/named.rs
@@ -0,0 +1,45 @@
+//! Declare menu navigation through
+//! [`Name`](https://docs.rs/bevy/0.8.0/bevy/core/struct.Name.html).
+//!
+//! The most difficult part of the API to deal with was giving
+//! [`NavMenu::reachable_from`](crate::NavMenu::reachable_from) the `Entity` for of
+//! the button used to reach it.
+//!
+//! This forced you to divide the whole menu construction in multiple
+//! parts and keep track of intermediary values if you want to make multple menus.
+//!
+//! *By-name declaration* let you simply add a label to your `Focusable` and
+//! refer to it in [`NavMenu::reachable_from_named`](crate::NavMenu::reachable_from_named).
+//! The runtime then detects labelled stuff and replace the partial
+//! [`NavMenu`](crate::NavMenu) with the full [`TreeMenu`](crate::resolve::TreeMenu)
+//! with the proper entity id reference. This saves you from pre-spawning your
+//! buttons so that you can associate their `id` with the proper submenu.
+
+use bevy_core::Name;
+use bevy_ecs::{entity::Entity, prelude::With, system::Query};
+
+use crate::{
+ seeds::{FailableOption, ParentName, TreeMenuSeed},
+ Focusable,
+};
+
+pub(crate) fn resolve_named_menus(
+ mut unresolved: Query<(&mut TreeMenuSeed, &ParentName)>,
+ named: Query<(Entity, &Name), With>,
+) {
+ for (mut seed, ParentName(parent_name)) in unresolved.iter_mut() {
+ match named.iter().find(|(_, n)| *n == parent_name) {
+ Some((focus_parent, _)) => {
+ seed.focus_parent = FailableOption::Some(focus_parent);
+ }
+ None => {
+ let name = parent_name.as_str();
+ bevy_log::warn!(
+ "Tried to spawn a `NavMenu` with parent focusable {name}, but no\
+ `Focusable` has a `Name` component with that value."
+ );
+ continue;
+ }
+ }
+ }
+}
diff --git a/crates/bevy_ui_navigation/src/resolve.rs b/crates/bevy_ui_navigation/src/resolve.rs
new file mode 100644
index 00000000000000..3f89dc39239af8
--- /dev/null
+++ b/crates/bevy_ui_navigation/src/resolve.rs
@@ -0,0 +1,695 @@
+//! The resolution algorithm for the navigation system.
+//!
+//! # Overview
+//!
+//! This module defines two systems:
+//! 1. [`listen_nav_requests`]: the system gathering [`NavRequest`] and running
+//! the [`resolve`] algorithm on them, updating the [`Focusable`] states and
+//! sending [`NavEvent`] as result.
+//! 2. [`insert_tree_menus`]: system responsible to convert seed bundles defined
+//! in [`crate::seeds`] into [`TreeMenu`], which is the component used by
+//! the resolution algorithm.
+//!
+//! The module also defines the [`Focusable`] component (also used in the
+//! resolution algorithm) and its fields.
+//!
+//! The bulk of the resolution algorithm is implemented in [`resolve`],
+//! delegating some abstract tasks to helper functions, of which:
+//! * [`parent_menu`]
+//! * [`ChildQueries::focusables_of`]
+//! * [`child_menu`]
+//! * [`focus_deep`]
+//! * [`root_path`]
+//! * [`MoveParam::resolve_2d`]
+//! * [`resolve_scope`]
+//!
+//! A trait [`MoveParam`] allows user-defined movements through a custom system parameter
+//! by implementing `resolve_2d`.
+//!
+//! We define some `SystemParam`:
+//! * [`ChildQueries`]: queries used to find the focusable children of a given entity.
+//! * [`NavQueries`]: All **immutable** queries used by the resolution algorithm.
+//! * [`MutQueries`]: Queries with mutable access to [`Focusable`] and [`TreeMenu`]
+//! for updating them in [`listen_nav_requests`].
+//!
+//! [`listen_nav_requests`] uses a `ParamSet` to access the focusables immutably for
+//! navigation resolution and mutably for updating them with the new navigation state.
+use std::cmp::Ordering;
+use std::num::NonZeroUsize;
+
+use bevy_ecs::{
+ entity::Entity,
+ event::{EventReader, EventWriter},
+ prelude::{Component, Or, With, Without},
+ system::{Commands, ParamSet, Query, ResMut, StaticSystemParam, SystemParam, SystemParamItem},
+};
+use bevy_hierarchy::{Children, Parent};
+use bevy_log::warn;
+
+use non_empty_vec::NonEmpty;
+
+use crate::{
+ commands::set_focus_state,
+ events::{self, NavEvent, NavRequest},
+ seeds::{self, NavMenu},
+};
+
+/// System parameter used to resolve movement and cycling focus updates.
+///
+/// This is useful if you don't want to depend on bevy's [`GlobalTransform`]
+/// for your UI, or want to implement your own navigation algorithm. For example,
+/// if you want your ui to be 3d elements in the world.
+///
+/// See the [`UiProjectionQuery`] source code for implementation hints.
+pub trait MoveParam {
+ /// Which `Entity` in `siblings` can be reached from `focused` in
+ /// `direction` if any, otherwise `None`.
+ ///
+ /// * `focused`: The currently focused entity in the menu
+ /// * `direction`: The direction in which the focus should move
+ /// * `cycles`: Whether the navigation should loop
+ /// * `sibligns`: All the other focusable entities in this menu
+ ///
+ /// Note that `focused` appears once in `siblings`.
+ fn resolve_2d<'a>(
+ &self,
+ focused: Entity,
+ direction: events::Direction,
+ cycles: bool,
+ siblings: &'a [Entity],
+ ) -> Option<&'a Entity>;
+}
+
+#[derive(SystemParam)]
+pub(crate) struct ChildQueries<'w, 's> {
+ children: Query<'w, 's, &'static Children>,
+ is_focusable: Query<'w, 's, With>,
+ is_menu: Query<'w, 's, Or<(With, With)>>,
+}
+
+/// Collection of queries to manage the navigation tree.
+#[allow(clippy::type_complexity)]
+#[derive(SystemParam)]
+pub(crate) struct NavQueries<'w, 's> {
+ pub(crate) children: ChildQueries<'w, 's>,
+ parents: Query<'w, 's, &'static Parent>,
+ focusables: Query<'w, 's, (Entity, &'static Focusable), Without>,
+ menus: Query<'w, 's, (Entity, &'static TreeMenu), Without>,
+}
+impl<'w, 's> NavQueries<'w, 's> {
+ fn focused(&self) -> Option {
+ use FocusState::{Dormant, Focused};
+ let menu_dormant =
+ |menu: &TreeMenu| menu.focus_parent.is_none().then_some(menu.active_child);
+ let any_dormant = |(e, focus): (Entity, &Focusable)| (focus.state == Dormant).then_some(e);
+ let any_dormant = || self.focusables.iter().find_map(any_dormant);
+ let root_dormant = || self.menus.iter().find_map(|(_, menu)| menu_dormant(menu));
+ let fallback = || self.focusables.iter().next().map(|(entity, _)| entity);
+ self.focusables
+ .iter()
+ .find_map(|(e, focus)| (focus.state == Focused).then_some(e))
+ .or_else(root_dormant)
+ .or_else(any_dormant)
+ .or_else(fallback)
+ }
+}
+
+/// Queries [`Focusable`] and [`TreeMenu`] in a mutable way.
+///
+/// NOTE: This is separate from [`NavQueries`] to work around
+/// [bevy's query exclusion rules][bevy-exclusion] in the `marker.rs`
+/// module.
+///
+/// [bevy-exclusion]: https://github.com/bevyengine/bevy/issues/4624
+#[derive(SystemParam)]
+pub(crate) struct MutQueries<'w, 's> {
+ parents: Query<'w, 's, &'static Parent>,
+ focusables: Query<'w, 's, &'static mut Focusable, Without>,
+ menus: Query<'w, 's, &'static mut TreeMenu, Without>,
+}
+impl<'w, 's> MutQueries<'w, 's> {
+ /// Set the [`active_child`](TreeMenu::active_child) field of the enclosing
+ /// [`TreeMenu`] and disables the previous one.
+ fn set_active_child(&mut self, cmds: &mut Commands, child: Entity) {
+ let mut focusable = child;
+ let mut nav_menu = loop {
+ // Find the enclosing parent menu.
+ if let Ok(parent) = self.parents.get(focusable) {
+ let parent = parent.get();
+ focusable = parent;
+ if let Ok(menu) = self.menus.get_mut(parent) {
+ break menu;
+ }
+ } else {
+ return;
+ }
+ };
+ let entity = nav_menu.active_child;
+ nav_menu.active_child = child;
+ self.set_entity_focus(cmds, entity, FocusState::Inert);
+ }
+
+ fn set_entity_focus(&mut self, cmds: &mut Commands, entity: Entity, state: FocusState) {
+ if let Ok(mut focusable) = self.focusables.get_mut(entity) {
+ focusable.state = state;
+ cmds.add(set_focus_state(entity, state));
+ }
+ }
+}
+
+/// State of a [`Focusable`].
+#[derive(Clone, Debug, Copy, PartialEq)]
+pub enum FocusState {
+ /// An entity that was previously [`Active`](FocusState::Active) from a branch of
+ /// the menu tree that is currently not _focused_. When focus comes back to
+ /// the [`NavMenu`](NavMenu) containing this [`Focusable`], the `Dormant` element
+ /// will be the [`Focused`](FocusState::Focused) entity.
+ Dormant,
+ /// The currently highlighted/used entity, there is only a signle _focused_
+ /// entity.
+ ///
+ /// All navigation requests start from it.
+ ///
+ /// To set an arbitrary [`Focusable`] to _focused_, you should send a
+ /// [`NavRequest::FocusOn`] request.
+ Focused,
+ /// This [`Focusable`] is on the path in the menu tree to the current
+ /// [`Focused`](FocusState::Focused) entity.
+ ///
+ /// [`FocusState::Active`] focusables are the [`Focusable`]s from
+ /// previous menus that were activated in order to reach the
+ /// [`NavMenu`](NavMenu) containing the currently _focused_ element.
+ Active,
+ /// None of the above: This [`Focusable`] is neither `Dormant`, `Focused`
+ /// or `Active`.
+ Inert,
+}
+
+/// The navigation system's lock.
+///
+/// When locked, the navigation system doesn't process any [`NavRequest`].
+/// It only waits on a [`NavRequest::Free`] event. It will then continue
+/// processing new requests.
+pub struct NavLock {
+ entity: Option,
+}
+impl NavLock {
+ pub(crate) fn new() -> Self {
+ Self { entity: None }
+ }
+ /// The [`Entity`](https://docs.rs/bevy/0.8.0/bevy/ecs/entity/struct.Entity.html)
+ /// that triggered the lock.
+ pub fn entity(&self) -> Option {
+ self.entity
+ }
+ /// Whether the navigation system is locked.
+ pub fn is_locked(&self) -> bool {
+ self.entity.is_some()
+ }
+}
+
+/// A menu that isolate children [`Focusable`]s from other focusables and
+/// specify navigation method within itself.
+///
+/// The user can't create a `TreeMenu`, they will use the
+/// [`NavMenu`](NavMenu) API and the `TreeMenu` component will be inserted
+/// by the [`insert_tree_menus`] system.
+#[derive(Debug, Component, Clone)]
+pub(crate) struct TreeMenu {
+ /// The [`Focusable`] that sends to this `NavMenu` when recieving
+ /// [`NavRequest::Action`].
+ pub(crate) focus_parent: Option,
+ /// How we want the user to move between [`Focusable`]s within this menu.
+ pub(crate) setting: NavMenu,
+ /// The currently dormant or active focusable in this menu.
+ pub(crate) active_child: Entity,
+}
+
+/// The actions triggered by a [`Focusable`].
+#[derive(Clone, Copy, PartialEq, Debug)]
+#[non_exhaustive]
+pub enum FocusAction {
+ /// Acts like a standard navigation node.
+ ///
+ /// Goes into relevant menu if any [`NavMenu`](NavMenu) is
+ /// [`reachable_from`](NavMenu::reachable_from) this [`Focusable`].
+ Normal,
+
+ /// If we receive [`NavRequest::Action`] while this [`Focusable`] is
+ /// focused, it will act as a [`NavRequest::Cancel`] (leaving submenu to
+ /// enter the parent one).
+ Cancel,
+
+ /// If we receive [`NavRequest::Action`] while this [`Focusable`] is
+ /// focused, the navigation system will freeze until [`NavRequest::Free`]
+ /// is received, sending a [`NavEvent::Unlocked`].
+ ///
+ /// This is useful to implement widgets with complex controls you don't
+ /// want to accidentally unfocus, or suspending the navigation system while
+ /// in-game.
+ Lock,
+}
+
+/// An [`Entity`] that can be navigated to using the ui navigation system.
+///
+/// It is in one of multiple [`FocusState`], you can check its state with
+/// the [`Focusable::state`] method.
+///
+/// A `Focusable` can execute a variety of [`FocusAction`] when receiving
+/// [`NavRequest::Action`], the default one is [`FocusAction::Normal`]
+#[derive(Component, Clone, Debug)]
+pub struct Focusable {
+ pub(crate) state: FocusState,
+ action: FocusAction,
+}
+impl Default for Focusable {
+ fn default() -> Self {
+ Focusable {
+ state: FocusState::Inert,
+ action: FocusAction::Normal,
+ }
+ }
+}
+impl Focusable {
+ /// Default Focusable
+ pub fn new() -> Self {
+ Self::default()
+ }
+ /// The [`FocusState`] of this `Focusable`.
+ pub fn state(&self) -> FocusState {
+ self.state
+ }
+ /// The [`FocusAction`] of this `Focusable`.
+ pub fn action(&self) -> FocusAction {
+ self.action
+ }
+ /// Spawn a "cancel" focusable, see [`FocusAction::Cancel`].
+ pub fn cancel() -> Self {
+ Focusable {
+ state: FocusState::Inert,
+ action: FocusAction::Cancel,
+ }
+ }
+ /// Spawn a "lock" focusable, see [`FocusAction::Lock`].
+ pub fn lock() -> Self {
+ Focusable {
+ state: FocusState::Inert,
+ action: FocusAction::Lock,
+ }
+ }
+ /// Spawn a focusable that will get highlighted in priority when none are set yet.
+ ///
+ /// **WARNING**: Only use this to spawn the UI. Any of the following state is
+ /// unspecified and will likely result in broken behavior:
+ /// * Having multiple dormant `Focusable`s in the same menu.
+ /// * Updating an already existing `Focusable` with this.
+ pub fn dormant(self) -> Self {
+ Self {
+ state: FocusState::Dormant,
+ ..self
+ }
+ }
+}
+
+/// The currently _focused_ [`Focusable`].
+///
+/// You cannot edit it or create new `Focused` component. To set an arbitrary
+/// [`Focusable`] to _focused_, you should send [`NavRequest::FocusOn`].
+///
+/// This [`Component`](https://docs.rs/bevy/0.8.0/bevy/ecs/component/trait.Component.html)
+/// is useful if you need to query for the _currently focused_ element using a
+/// `Query>` for example.
+///
+/// If a [`Focusable`] is focused, its [`Focusable::state()`] will be
+/// [`FocusState::Focused`], if you have a [`Focusable`] but can't query
+/// filter on [`Focused`], you can check for equality.
+///
+/// # Notes
+///
+/// The `Focused` marker component is only updated at the end of the
+/// `CoreStage::Update` stage. This means it might lead to a single frame of
+/// latency compared to using [`Focusable::state()`].
+#[derive(Component)]
+#[non_exhaustive]
+pub struct Focused;
+
+pub(crate) fn max_by_in_iter(
+ iter: impl Iterator
- ,
+ f: impl Fn(&U) -> T,
+) -> Option {
+ iter.max_by(|s1, s2| {
+ let s1_val = f(s1);
+ let s2_val = f(s2);
+ s1_val.partial_cmp(&s2_val).unwrap_or(Ordering::Equal)
+ })
+}
+
+/// Returns the next or previous entity based on `direction`
+fn resolve_scope(
+ focused: Entity,
+ direction: events::ScopeDirection,
+ cycles: bool,
+ siblings: &NonEmpty,
+) -> Option<&Entity> {
+ let focused_index = siblings.iter().position(|e| *e == focused)?;
+ let new_index = resolve_index(focused_index, cycles, direction, siblings.len().get() - 1);
+ new_index.and_then(|i| siblings.get(i))
+}
+
+/// Resolve `request` where the focused element is `focused`
+fn resolve(
+ focused: Entity,
+ request: NavRequest,
+ queries: &NavQueries,
+ lock: &mut NavLock,
+ from: Vec,
+ mquery: &MP,
+) -> NavEvent {
+ use NavRequest::*;
+
+ assert!(
+ queries.focusables.get(focused).is_ok(),
+ "The resolution algorithm MUST go from a focusable element"
+ );
+ assert!(
+ !from.contains(&focused),
+ "Navigation graph cycle detected! This panic has prevented a stack overflow, \
+ please check usages of `NavMenu::reachable_from`"
+ );
+
+ let mut from = (from, focused).into();
+
+ macro_rules! or_none {
+ ($to_match:expr) => {
+ match $to_match {
+ Some(x) => x,
+ None => return NavEvent::NoChanges { from, request },
+ }
+ };
+ }
+ match request {
+ Move(direction) => {
+ let (parent, cycles) = match parent_menu(focused, queries) {
+ Some(val) if !val.1.setting.is_2d() => {
+ return NavEvent::NoChanges { from, request }
+ }
+ Some(val) => (Some(val.0), !val.1.setting.bound()),
+ None => (None, true),
+ };
+ let siblings = match parent {
+ Some(parent) => queries.children.focusables_of(parent),
+ None => {
+ let focusables: Vec<_> = queries.focusables.iter().map(|tpl| tpl.0).collect();
+ NonEmpty::try_from(focusables).expect(
+ "There must be at least one `Focusable` when sending a `NavRequest`!",
+ )
+ }
+ };
+ let to = mquery.resolve_2d(focused, direction, cycles, &siblings);
+ NavEvent::focus_changed(*or_none!(to), from)
+ }
+ Cancel => {
+ let to = or_none!(parent_menu(focused, queries));
+ let to = or_none!(to.1.focus_parent);
+ from.push(to);
+ NavEvent::focus_changed(to, from)
+ }
+ Action => {
+ if let Ok((_, focusable)) = queries.focusables.get(focused) {
+ match focusable.action {
+ FocusAction::Cancel => {
+ let mut from = from.to_vec();
+ from.truncate(from.len() - 1);
+ return resolve(focused, NavRequest::Cancel, queries, lock, from, mquery);
+ }
+ FocusAction::Lock => {
+ lock.entity = Some(focused);
+ return NavEvent::Locked(focused);
+ }
+ FocusAction::Normal => {}
+ }
+ }
+ let child_menu = child_menu(focused, queries);
+ let (_, menu) = or_none!(child_menu);
+ let to = (menu.active_child, from.clone().into()).into();
+ NavEvent::FocusChanged { to, from }
+ }
+ ScopeMove(scope_dir) => {
+ let (parent, menu) = or_none!(parent_menu(focused, queries));
+ let siblings = queries.children.focusables_of(parent);
+ if !menu.setting.is_scope() {
+ let focused = or_none!(menu.focus_parent);
+ resolve(focused, request, queries, lock, from.into(), mquery)
+ } else {
+ let cycles = !menu.setting.bound();
+ let to = or_none!(resolve_scope(focused, scope_dir, cycles, &siblings));
+ let extra = match child_menu(*to, queries) {
+ Some((_, menu)) => focus_deep(menu, queries),
+ None => Vec::new(),
+ };
+ let to = (extra, *to).into();
+ NavEvent::FocusChanged { to, from }
+ }
+ }
+ FocusOn(new_to_focus) => {
+ let mut from = root_path(focused, queries);
+ let mut to = root_path(new_to_focus, queries);
+ trim_common_tail(&mut from, &mut to);
+ if from == to {
+ NavEvent::NoChanges { from, request }
+ } else {
+ NavEvent::FocusChanged { from, to }
+ }
+ }
+ Free => {
+ if let Some(lock_entity) = lock.entity.take() {
+ NavEvent::Unlocked(lock_entity)
+ } else {
+ warn!("Received a NavRequest::Free while not locked");
+ NavEvent::NoChanges { from, request }
+ }
+ }
+ }
+}
+
+/// Replaces [`seeds::TreeMenuSeed`]s with proper [`TreeMenu`]s.
+pub(crate) fn insert_tree_menus(
+ mut cmds: Commands,
+ seeds: Query<(Entity, &seeds::TreeMenuSeed)>,
+ queries: NavQueries,
+) {
+ use FocusState::{Active, Dormant, Focused};
+ let mut inserts = Vec::new();
+ for (entity, seed) in seeds.iter() {
+ let children = queries.children.focusables_of(entity);
+ let child = children
+ .iter()
+ .find_map(|e| {
+ let (_, focusable) = queries.focusables.get(*e).ok()?;
+ matches!(focusable.state, Dormant | Active | Focused).then_some(e)
+ })
+ .unwrap_or_else(|| children.first());
+ let menu = seed.clone().with_child(*child);
+ inserts.push((entity, (menu,)));
+ cmds.entity(entity).remove::();
+ }
+ cmds.insert_or_spawn_batch(inserts)
+}
+
+/// System to set the first [`Focusable`] to [`FocusState::Focused`] when no
+/// navigation has been done yet.
+pub(crate) fn set_first_focused(
+ has_focused: Query<(), With>,
+ mut queries: ParamSet<(NavQueries, MutQueries)>,
+ mut cmds: Commands,
+ mut events: EventWriter,
+) {
+ use FocusState::Focused;
+ if has_focused.is_empty() {
+ if let Some(to_focus) = queries.p0().focused() {
+ queries.p1().set_entity_focus(&mut cmds, to_focus, Focused);
+ events.send(NavEvent::InitiallyFocused(to_focus));
+ }
+ }
+}
+
+/// Listen to [`NavRequest`] and update the state of [`Focusable`] entities if
+/// relevant.
+pub(crate) fn listen_nav_requests(
+ mut cmds: Commands,
+ mut queries: ParamSet<(NavQueries, MutQueries)>,
+ mquery: StaticSystemParam,
+ mut lock: ResMut,
+ mut requests: EventReader,
+ mut events: EventWriter,
+) where
+ for<'w, 's> SystemParamItem<'w, 's, MP>: MoveParam,
+{
+ use FocusState as Fs;
+
+ let no_focused = "Tried to execute a NavRequest when no focusables exist, NavRequest does nothing if there isn't any navigation to do.";
+ for request in requests.iter() {
+ if lock.is_locked() && *request != NavRequest::Free {
+ continue;
+ }
+ // TODO: ensure no multiple Focused entities
+ let focused = if let Some(e) = queries.p0().focused() {
+ e
+ } else {
+ warn!(no_focused);
+ continue;
+ };
+ let from = Vec::new();
+ let event = resolve(focused, *request, &queries.p0(), &mut lock, from, &*mquery);
+ // Change focus state of relevant entities
+ if let NavEvent::FocusChanged { to, from } = &event {
+ let mut mut_queries = queries.p1();
+ if to == from {
+ continue;
+ }
+ let (&disable, put_to_sleep) = from.split_last();
+ mut_queries.set_entity_focus(&mut cmds, disable, Fs::Inert);
+ for &entity in put_to_sleep {
+ mut_queries.set_entity_focus(&mut cmds, entity, Fs::Dormant);
+ }
+ let (&focus, activate) = to.split_first();
+ mut_queries.set_active_child(&mut cmds, focus);
+ mut_queries.set_entity_focus(&mut cmds, focus, Fs::Focused);
+ for &entity in activate {
+ mut_queries.set_active_child(&mut cmds, entity);
+ mut_queries.set_entity_focus(&mut cmds, entity, Fs::Active);
+ }
+ };
+ events.send(event);
+ }
+}
+
+/// The child [`TreeMenu`] of `focusable`.
+fn child_menu<'a>(focusable: Entity, queries: &'a NavQueries) -> Option<(Entity, &'a TreeMenu)> {
+ queries
+ .menus
+ .iter()
+ .find(|e| e.1.focus_parent == Some(focusable))
+}
+
+/// The [`TreeMenu`] containing `focusable`, if any.
+pub(crate) fn parent_menu(focusable: Entity, queries: &NavQueries) -> Option<(Entity, TreeMenu)> {
+ let parent = queries.parents.get(focusable).ok()?.get();
+ match queries.menus.get(parent) {
+ Ok(menu) => Some((parent, menu.1.clone())),
+ Err(_) => parent_menu(parent, queries),
+ }
+}
+
+impl<'w, 's> ChildQueries<'w, 's> {
+ /// All sibling [`Focusable`]s within a single [`TreeMenu`].
+ pub(crate) fn focusables_of(&self, menu: Entity) -> NonEmpty {
+ let ret = self.focusables_of_helper(menu);
+ NonEmpty::try_from(ret)
+ .expect("A NavMenu MUST AT LEAST HAVE ONE Focusable child, found one without")
+ }
+
+ fn focusables_of_helper(&self, menu: Entity) -> Vec {
+ match self.children.get(menu).ok() {
+ Some(direct_children) => {
+ let focusables = direct_children
+ .iter()
+ .filter(|e| self.is_focusable.get(**e).is_ok())
+ .cloned();
+ let transitive_focusables = direct_children
+ .iter()
+ .filter(|e| self.is_focusable.get(**e).is_err())
+ .filter(|e| self.is_menu.get(**e).is_err())
+ .flat_map(|e| self.focusables_of_helper(*e));
+ focusables.chain(transitive_focusables).collect()
+ }
+ None => Vec::new(),
+ }
+ }
+}
+
+/// Remove all mutually identical elements at the end of `v1` and `v2`.
+fn trim_common_tail(v1: &mut NonEmpty, v2: &mut NonEmpty) {
+ let mut i1 = v1.len().get() - 1;
+ let mut i2 = v2.len().get() - 1;
+ loop {
+ if v1[i1] != v2[i2] {
+ // unwraps: any usize + 1 (saturating) is NonZero
+ let l1 = NonZeroUsize::new(i1.saturating_add(1)).unwrap();
+ let l2 = NonZeroUsize::new(i2.saturating_add(1)).unwrap();
+ v1.truncate(l1);
+ v2.truncate(l2);
+ return;
+ } else if i1 != 0 && i2 != 0 {
+ i1 -= 1;
+ i2 -= 1;
+ } else {
+ // There is no changes to be made to the input vectors
+ return;
+ }
+ }
+}
+
+fn root_path(mut from: Entity, queries: &NavQueries) -> NonEmpty {
+ let mut ret = NonEmpty::new(from);
+ loop {
+ from = match parent_menu(from, queries) {
+ // purely personal preference over deeply nested pattern match
+ Some((_, menu)) if menu.focus_parent.is_some() => menu.focus_parent.unwrap(),
+ _ => return ret,
+ };
+ assert!(
+ !ret.contains(&from),
+ "Navigation graph cycle detected! This panic has prevented a stack \
+ overflow, please check usages of `NavMenu::reachable_from`"
+ );
+ ret.push(from);
+ }
+}
+
+/// Navigate downward the menu hierarchy, traversing all dormant children.
+fn focus_deep<'a>(mut menu: &'a TreeMenu, queries: &'a NavQueries) -> Vec {
+ let mut ret = Vec::with_capacity(4);
+ loop {
+ let last = menu.active_child;
+ ret.insert(0, last);
+ menu = match child_menu(last, queries) {
+ Some((_, menu)) => menu,
+ None => return ret,
+ };
+ }
+}
+
+/// Cycle through a [scoped menu](NavMenu::BoundScope) according to menu settings
+///
+/// Returns the index of the element to focus according to `direction`. Cycles
+/// if `cycles` and goes over `max_value` or goes bellow 0. `None` if the
+/// direction is a dead end.
+fn resolve_index(
+ from: usize,
+ cycles: bool,
+ direction: events::ScopeDirection,
+ max_value: usize,
+) -> Option {
+ use events::ScopeDirection::*;
+ match (direction, from) {
+ (Previous, 0) => cycles.then_some(max_value),
+ (Previous, from) => Some(from - 1),
+ (Next, from) if from == max_value => cycles.then_some(0),
+ (Next, from) => Some(from + 1),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::trim_common_tail;
+ #[test]
+ fn test_trim_common_tail() {
+ use non_empty_vec::ne_vec;
+ let mut v1 = ne_vec![1, 2, 3, 4, 5, 6, 7];
+ let mut v2 = ne_vec![3, 2, 1, 4, 5, 6, 7];
+ trim_common_tail(&mut v1, &mut v2);
+ assert_eq!(v1, ne_vec![1, 2, 3]);
+ assert_eq!(v2, ne_vec![3, 2, 1]);
+ }
+}
diff --git a/crates/bevy_ui_navigation/src/seeds.rs b/crates/bevy_ui_navigation/src/seeds.rs
new file mode 100644
index 00000000000000..728b825df15eed
--- /dev/null
+++ b/crates/bevy_ui_navigation/src/seeds.rs
@@ -0,0 +1,288 @@
+//! [`NavMenu`] builders to convert into [`TreeMenu`].
+//!
+//! This module defines a bunch of "seed" bundles. Systems in [`crate::named`],
+//! and [`crate::resolve`] will take the components
+//! defined in those seeds and replace them by [`NavMenu`]s. It is necessary
+//! for a few things:
+//! * The [`active_child`](TreeMenu::active_child) field of `NavMenu`, which
+//! cannot be inferred without the [`Focusable`](crate::Focusable)s children of that menu
+//! * Finding the [`Focusable`](crate::Focusable) specified in [`ParentName`]
+//!
+//! # Seed bundles
+//!
+//! Seed bundles are collections of components that will trigger various
+//! pre-processing to create a [`TreeMenu`]. They are a combination of those
+//! components:
+//! * [`TreeMenuSeed`]: the base seed, which will be converted into a [`TreeMenu`]
+//! in [`crate::resolve::insert_tree_menus`].
+//! * [`ParentName`], the *by-name* marker: marks a [`TreeMenuSeed`] as needing
+//! its [`focus_parent`](TreeMenuSeed::focus_parent) to be updated by
+//! [`crate::named::resolve_named_menus`] with the [`Focusable`](crate::Focusable) which
+//! [`Name`](https://docs.rs/bevy/0.8.0/bevy/core/struct.Name.html) matches
+//! the one in [`ParentName`]. If for whatever reason that update doesn't
+//! happen, [`crate::resolve::insert_tree_menus`] will panic.
+//!
+//! Those components are combined in the seed bundles. Which processing step is
+//! applied to the [`TreeMenuSeed`] depends on which components it was inserted
+//! with. The bundles are:
+//! * [`MenuSeed`]: Creates a [`NavMenu`].
+//! * [`NamedMenuSeed`]: Creates a [`NavMenu`] "reachable from" the
+//! [`Focusable`](crate::Focusable) named in the [`ParentName`].
+//!
+//! # Ordering
+//!
+//! In order to correctly create the [`TreeMenu`] specified with the bundles
+//! declared int this module, the systems need to be ran in this order:
+//!
+//! ```text
+//! named::resolve_named_menus → resolve::insert_tree_menus
+//! ```
+//!
+//! The resolve_named/insert relationship should be upheld. Otherwise, root
+//! NavMenus will spawn instead of NavMenus with parent with given name.
+#![allow(unused_parens)]
+
+use std::borrow::Cow;
+
+use bevy_core::Name;
+use bevy_ecs::{
+ entity::Entity,
+ prelude::{Bundle, Component},
+};
+
+use crate::resolve::TreeMenu;
+
+/// Option of an option.
+///
+/// It's really just `Option