Path: blob/main/examples/ui/navigation/directional_navigation.rs
9354 views
//! Demonstrates automatic directional navigation.1//!2//! This shows how to use automatic navigation by simply adding the [`AutoDirectionalNavigation`]3//! component to UI elements. Navigation is automatically calculated based on screen positions.4//!5//! This is especially useful for:6//! - Dynamic UIs where elements may be added, removed, or repositioned7//! - Irregular layouts that don't fit a simple grid pattern8//! - Prototyping where you want navigation without tedious manual setup9//!10//! The automatic system finds the nearest neighbor in each compass direction for every node,11//! completely eliminating the need to manually specify navigation relationships.12//!13//! For an example that demonstrates automatic directional navigation with manual overrides,14//! refer to the `directional_navigation_overrides` example.1516use core::time::Duration;1718use bevy::{19camera::NormalizedRenderTarget,20input_focus::{21directional_navigation::{AutoNavigationConfig, DirectionalNavigationPlugin},22InputDispatchPlugin, InputFocus, InputFocusVisible,23},24math::{CompassOctant, Dir2, Rot2},25picking::{26backend::HitData,27pointer::{Location, PointerId},28},29platform::collections::HashSet,30prelude::*,31ui::auto_directional_navigation::{AutoDirectionalNavigation, AutoDirectionalNavigator},32};3334fn main() {35App::new()36// Input focus is not enabled by default, so we need to add the corresponding plugins37.add_plugins((38DefaultPlugins,39InputDispatchPlugin,40DirectionalNavigationPlugin,41))42// This resource is canonically used to track whether or not to render a focus indicator43// It starts as false, but we set it to true here as we would like to see the focus indicator44.insert_resource(InputFocusVisible(true))45// Configure auto-navigation behavior46.insert_resource(AutoNavigationConfig {47// Require at least 10% overlap in perpendicular axis for cardinal directions48min_alignment_factor: 0.1,49// Don't connect nodes more than 500 pixels apart between their closest edges50max_search_distance: Some(500.0),51// Prefer nodes that are well-aligned52prefer_aligned: true,53})54.init_resource::<ActionState>()55.add_systems(Startup, setup_scattered_ui)56// No manual system needed - just add AutoDirectionalNavigation to entities.57// Input is generally handled during PreUpdate58.add_systems(PreUpdate, (process_inputs, navigate).chain())59.add_systems(60Update,61(62highlight_focused_element,63interact_with_focused_button,64reset_button_after_interaction,65update_focus_display66.run_if(|input_focus: Res<InputFocus>| input_focus.is_changed()),67update_key_display,68),69)70.add_observer(universal_button_click_behavior)71.run();72}7374const NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_400;75const PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_500;76const FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::BLUE_50;7778/// Marker component for the text that displays the currently focused button79#[derive(Component)]80struct FocusDisplay;8182/// Marker component for the text that displays the last key pressed83#[derive(Component)]84struct KeyDisplay;8586// Observer for button clicks87fn universal_button_click_behavior(88mut click: On<Pointer<Click>>,89mut button_query: Query<(&mut BackgroundColor, &mut ResetTimer)>,90) {91let button_entity = click.entity;92if let Ok((mut color, mut reset_timer)) = button_query.get_mut(button_entity) {93color.0 = PRESSED_BUTTON.into();94reset_timer.0 = Timer::from_seconds(0.3, TimerMode::Once);95click.propagate(false);96}97}9899#[derive(Component, Default, Deref, DerefMut)]100struct ResetTimer(Timer);101102fn reset_button_after_interaction(103time: Res<Time>,104mut query: Query<(&mut ResetTimer, &mut BackgroundColor)>,105) {106for (mut reset_timer, mut color) in query.iter_mut() {107reset_timer.tick(time.delta());108if reset_timer.just_finished() {109color.0 = NORMAL_BUTTON.into();110}111}112}113114/// Spawn a scattered layout of buttons to demonstrate automatic navigation.115///116/// Unlike a regular grid, these buttons are irregularly positioned,117/// but auto-navigation will still figure out the correct connections!118fn setup_scattered_ui(mut commands: Commands, mut input_focus: ResMut<InputFocus>) {119commands.spawn(Camera2d);120121// Create a full-screen background node122let root_node = commands123.spawn(Node {124width: percent(100),125height: percent(100),126..default()127})128.id();129130// Instructions131let instructions = commands132.spawn((133Text::new(134"Directional Navigation Demo\n\n\135Use arrow keys or D-pad to navigate.\n\136Press Enter or A button to interact.\n\n\137Buttons are scattered irregularly,\n\138but navigation is automatic!",139),140Node {141position_type: PositionType::Absolute,142left: px(20),143top: px(20),144width: px(280),145padding: UiRect::all(px(12)),146border_radius: BorderRadius::all(px(8)),147..default()148},149BackgroundColor(Color::srgba(0.1, 0.1, 0.1, 0.8)),150))151.id();152153// Focus display - shows which button is currently focused154commands.spawn((155Text::new("Focused: None"),156FocusDisplay,157Node {158position_type: PositionType::Absolute,159left: px(20),160bottom: px(80),161width: px(280),162padding: UiRect::all(px(12)),163border_radius: BorderRadius::all(px(8)),164..default()165},166BackgroundColor(Color::srgba(0.1, 0.5, 0.1, 0.8)),167TextFont {168font_size: FontSize::Px(20.0),169..default()170},171));172173// Key display - shows the last key pressed174commands.spawn((175Text::new("Last Key: None"),176KeyDisplay,177Node {178position_type: PositionType::Absolute,179left: px(20),180bottom: px(20),181width: px(280),182padding: UiRect::all(px(12)),183border_radius: BorderRadius::all(px(8)),184..default()185},186BackgroundColor(Color::srgba(0.5, 0.1, 0.5, 0.8)),187TextFont {188font_size: FontSize::Px(20.0),189..default()190},191));192193// Spawn buttons in a scattered/irregular pattern194// The auto-navigation system will figure out the connections!195let button_positions = [196// Top row (irregular spacing)197(350.0, 100.0),198(520.0, 120.0),199(700.0, 90.0),200// Middle-top row201(380.0, 220.0),202(600.0, 240.0),203// Center204(450.0, 340.0),205(620.0, 360.0),206// Lower row207(360.0, 480.0),208(540.0, 460.0),209(720.0, 490.0),210];211212let mut first_button = None;213for (i, (x, y)) in button_positions.iter().enumerate() {214let transform = if i == 4 {215UiTransform {216scale: Vec2::splat(1.2),217rotation: Rot2::FRAC_PI_2,218..default()219}220} else {221UiTransform::IDENTITY222};223let button_entity = commands224.spawn((225Button,226Node {227position_type: PositionType::Absolute,228left: px(*x),229top: px(*y),230width: px(140),231height: px(80),232border: UiRect::all(px(4)),233justify_content: JustifyContent::Center,234align_items: AlignItems::Center,235border_radius: BorderRadius::all(px(12)),236..default()237},238transform,239// This is the key: just add this component for automatic navigation!240AutoDirectionalNavigation::default(),241ResetTimer::default(),242BackgroundColor::from(NORMAL_BUTTON),243Name::new(format!("Button {}", i + 1)),244))245.with_child((246Text::new(format!("Button {}", i + 1)),247TextLayout {248justify: Justify::Center,249..default()250},251))252.id();253254if first_button.is_none() {255first_button = Some(button_entity);256}257}258259commands.entity(root_node).add_children(&[instructions]);260261// Set initial focus262if let Some(button) = first_button {263input_focus.set(button);264}265}266267// Action state and input handling268#[derive(Debug, PartialEq, Eq, Hash)]269enum DirectionalNavigationAction {270Up,271Down,272Left,273Right,274Select,275}276277impl DirectionalNavigationAction {278fn variants() -> Vec<Self> {279vec![280DirectionalNavigationAction::Up,281DirectionalNavigationAction::Down,282DirectionalNavigationAction::Left,283DirectionalNavigationAction::Right,284DirectionalNavigationAction::Select,285]286}287288fn keycode(&self) -> KeyCode {289match self {290DirectionalNavigationAction::Up => KeyCode::ArrowUp,291DirectionalNavigationAction::Down => KeyCode::ArrowDown,292DirectionalNavigationAction::Left => KeyCode::ArrowLeft,293DirectionalNavigationAction::Right => KeyCode::ArrowRight,294DirectionalNavigationAction::Select => KeyCode::Enter,295}296}297298fn gamepad_button(&self) -> GamepadButton {299match self {300DirectionalNavigationAction::Up => GamepadButton::DPadUp,301DirectionalNavigationAction::Down => GamepadButton::DPadDown,302DirectionalNavigationAction::Left => GamepadButton::DPadLeft,303DirectionalNavigationAction::Right => GamepadButton::DPadRight,304DirectionalNavigationAction::Select => GamepadButton::South,305}306}307}308309#[derive(Default, Resource)]310struct ActionState {311pressed_actions: HashSet<DirectionalNavigationAction>,312}313314fn process_inputs(315mut action_state: ResMut<ActionState>,316keyboard_input: Res<ButtonInput<KeyCode>>,317gamepad_input: Query<&Gamepad>,318) {319action_state.pressed_actions.clear();320321for action in DirectionalNavigationAction::variants() {322if keyboard_input.just_pressed(action.keycode()) {323action_state.pressed_actions.insert(action);324}325}326327for gamepad in gamepad_input.iter() {328for action in DirectionalNavigationAction::variants() {329if gamepad.just_pressed(action.gamepad_button()) {330action_state.pressed_actions.insert(action);331}332}333}334}335336fn navigate(337action_state: Res<ActionState>,338mut auto_directional_navigator: AutoDirectionalNavigator,339) {340let net_east_west = action_state341.pressed_actions342.contains(&DirectionalNavigationAction::Right) as i8343- action_state344.pressed_actions345.contains(&DirectionalNavigationAction::Left) as i8;346347let net_north_south = action_state348.pressed_actions349.contains(&DirectionalNavigationAction::Up) as i8350- action_state351.pressed_actions352.contains(&DirectionalNavigationAction::Down) as i8;353354// Use Dir2::from_xy to convert input to direction, then convert to CompassOctant355let maybe_direction = Dir2::from_xy(net_east_west as f32, net_north_south as f32)356.ok()357.map(CompassOctant::from);358359if let Some(direction) = maybe_direction {360match auto_directional_navigator.navigate(direction) {361Ok(_entity) => {362// Successfully navigated363}364Err(_e) => {365// Navigation failed (no neighbor in that direction)366}367}368}369}370371fn update_focus_display(372input_focus: Res<InputFocus>,373button_query: Query<&Name, With<Button>>,374mut display_query: Query<&mut Text, With<FocusDisplay>>,375) {376if let Ok(mut text) = display_query.single_mut() {377if let Some(focused_entity) = input_focus.0 {378if let Ok(name) = button_query.get(focused_entity) {379**text = format!("Focused: {}", name);380} else {381**text = "Focused: Unknown".to_string();382}383} else {384**text = "Focused: None".to_string();385}386}387}388389fn update_key_display(390keyboard_input: Res<ButtonInput<KeyCode>>,391gamepad_input: Query<&Gamepad>,392mut display_query: Query<&mut Text, With<KeyDisplay>>,393) {394if let Ok(mut text) = display_query.single_mut() {395// Check for keyboard inputs396for action in DirectionalNavigationAction::variants() {397if keyboard_input.just_pressed(action.keycode()) {398let key_name = match action {399DirectionalNavigationAction::Up => "Up Arrow",400DirectionalNavigationAction::Down => "Down Arrow",401DirectionalNavigationAction::Left => "Left Arrow",402DirectionalNavigationAction::Right => "Right Arrow",403DirectionalNavigationAction::Select => "Enter",404};405**text = format!("Last Key: {}", key_name);406return;407}408}409410// Check for gamepad inputs411for gamepad in gamepad_input.iter() {412for action in DirectionalNavigationAction::variants() {413if gamepad.just_pressed(action.gamepad_button()) {414let button_name = match action {415DirectionalNavigationAction::Up => "D-Pad Up",416DirectionalNavigationAction::Down => "D-Pad Down",417DirectionalNavigationAction::Left => "D-Pad Left",418DirectionalNavigationAction::Right => "D-Pad Right",419DirectionalNavigationAction::Select => "A Button",420};421**text = format!("Last Key: {}", button_name);422return;423}424}425}426}427}428429fn highlight_focused_element(430input_focus: Res<InputFocus>,431input_focus_visible: Res<InputFocusVisible>,432mut query: Query<(Entity, &mut BorderColor)>,433) {434for (entity, mut border_color) in query.iter_mut() {435if input_focus.0 == Some(entity) && input_focus_visible.0 {436*border_color = BorderColor::all(FOCUSED_BORDER);437} else {438*border_color = BorderColor::DEFAULT;439}440}441}442443fn interact_with_focused_button(444action_state: Res<ActionState>,445input_focus: Res<InputFocus>,446mut commands: Commands,447) {448if action_state449.pressed_actions450.contains(&DirectionalNavigationAction::Select)451&& let Some(focused_entity) = input_focus.0452{453commands.trigger(Pointer::<Click> {454entity: focused_entity,455pointer_id: PointerId::Mouse,456pointer_location: Location {457target: NormalizedRenderTarget::None {458width: 0,459height: 0,460},461position: Vec2::ZERO,462},463event: Click {464button: PointerButton::Primary,465hit: HitData {466camera: Entity::PLACEHOLDER,467depth: 0.0,468position: None,469normal: None,470},471duration: Duration::from_secs_f32(0.1),472},473});474}475}476477478