From 45a49c1b4e6751ae9a0d5987801f1b6ff9a11f61 Mon Sep 17 00:00:00 2001 From: Kevin Chen Date: Sun, 4 Jan 2026 16:22:22 -0500 Subject: [PATCH 01/25] docs: adds example combining auto and manual directional nav --- Cargo.toml | 12 + examples/ui/navigation.rs | 503 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 515 insertions(+) create mode 100644 examples/ui/navigation.rs diff --git a/Cargo.toml b/Cargo.toml index dd904e1217590..6709c4a3e03c9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5103,6 +5103,18 @@ description = "Demonstration of automatic directional navigation graph generatio category = "UI (User Interface)" wasm = true +[[example]] +name = "navigation" +path = "examples/ui/navigation.rs" +# Causes an ICE on docs.rs +doc-scrape-examples = false + +[package.metadata.example.navigation] +name = "Navigation" +description = "Demonstration of automatic and manual directional navigation between UI elements" +category = "UI (User Interface)" +wasm = true + [[example]] name = "clustered_decals" path = "examples/3d/clustered_decals.rs" diff --git a/examples/ui/navigation.rs b/examples/ui/navigation.rs new file mode 100644 index 0000000000000..5de059179bfb0 --- /dev/null +++ b/examples/ui/navigation.rs @@ -0,0 +1,503 @@ +//! Demonstrates a combination of automatic directional navigation and manual directional navigation. +//! +//! This example shows how to leverage both automatic navigation and manual navigation to create +//! a desired user navigation experience without much boilerplate code. The `AutoDirectionalNavigation` +//! component is used to add basic, intuitive navigation to UI elements. Manual edges between certain +//! UI elements are added to the `DirectionalNavigationMap` to override auto navigation and to add +//! special navigation rules. +//! +//! The directional navigation system provided by `AutoDirectionalNavigator` uses manually defined edges +//! first. If no manual edge is defined, automatic navigation is used. + +use core::time::Duration; + +use bevy::{ + camera::NormalizedRenderTarget, + input_focus::{ + directional_navigation::{ + AutoNavigationConfig, DirectionalNavigationMap, DirectionalNavigationPlugin, + }, + InputDispatchPlugin, InputFocus, InputFocusVisible, + }, + math::{CompassOctant, Dir2}, + picking::{ + backend::HitData, + pointer::{Location, PointerId}, + }, + platform::collections::HashSet, + prelude::*, + ui::auto_directional_navigation::{AutoDirectionalNavigation, AutoDirectionalNavigator} +}; + +fn main() { + App::new() + // Input focus is not enabled by default, so we need to add the corresponding plugins + // The navigation system's resources are initialized by the DirectionalNavigationPlugin. + .add_plugins(( + DefaultPlugins, + InputDispatchPlugin, + DirectionalNavigationPlugin, + )) + // This resource is canonically used to track whether or not to render a focus indicator + // It starts as false, but we set it to true here as we would like to see the focus indicator + .insert_resource(InputFocusVisible(true)) + // Configure auto-navigation behavior + .insert_resource(AutoNavigationConfig { + // Require at least 10% overlap in perpendicular axis for cardinal directions + min_alignment_factor: 0.1, + // Don't connect nodes more than 500 pixels apart + max_search_distance: Some(500.0), + // Do not prefer nodes that are well-aligned. In a cascading layout, nodes may not be well-aligned. + prefer_aligned: false, + }) + .init_resource::() + // For automatic navigation, UI entities will have the component `AutoDirectionalNavigation` + // and will be automatically connected by the navigation system. + // To override some automatically created navigation edges, manual edges are created + // in `setup_cascading_ui`. We will also add some new edges that the automatic navigation system + // does not create. + .add_systems(Startup, setup_cascading_ui) + // Input is generally handled during PreUpdate + .add_systems(PreUpdate, (process_inputs, navigate).chain()) + .add_systems( + Update, + ( + highlight_focused_element, + interact_with_focused_button, + reset_button_after_interaction, + update_focus_display, + update_key_display, + ), + ) + .add_observer(universal_button_click_behavior) + .run(); +} + +const NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_400; +const PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_500; +const FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::BLUE_50; + +/// Marker component for the text that displays the currently focused button +#[derive(Component)] +struct FocusDisplay; + +/// Marker component for the text that displays the last key pressed +#[derive(Component)] +struct KeyDisplay; + +// Observer for button clicks +fn universal_button_click_behavior( + mut click: On>, + mut button_query: Query<(&mut BackgroundColor, &mut ResetTimer)>, +) { + let button_entity = click.entity; + if let Ok((mut color, mut reset_timer)) = button_query.get_mut(button_entity) { + color.0 = PRESSED_BUTTON.into(); + reset_timer.0 = Timer::from_seconds(0.3, TimerMode::Once); + click.propagate(false); + } +} + +#[derive(Component, Default, Deref, DerefMut)] +struct ResetTimer(Timer); + +fn reset_button_after_interaction( + time: Res