//! Demonstrates automatic directional navigation with manual navigation overrides. //! //! This example shows how to leverage both automatic navigation and manual overrides to create //! a desired user navigation experience without much boilerplate code. In this example, there are //! multiple pages of UI Buttons that depict different scenarios in which both automatic and manual //! navigation are leveraged to produce a desired navigation experience. //! //! Manual overrides can be used to define navigation in any situation where automatic //! navigation fails to create an edge due to lack of proximity. For example, when creating //! navigation that loops around to an opposite side, manual overrides should be used to define //! this behavior. If one input is too far away from the others and `AutoNavigationConfig` //! cannot be tweaked, manual overrides can connect that input to the others. Manual navigation //! can also be used to override any undesired automatic navigation. //! //! The [`AutoDirectionalNavigation`] component is used to create basic, intuitive navigation to UI //! elements within a page. Manual navigation edges are added to the [`DirectionalNavigationMap`] //! to create special navigation rules. The [`AutoDirectionalNavigator`] system parameter navigates //! using manual navigation rules/overrides first and automatic navigation second. use core::time::Duration; use bevy::{ camera::NormalizedRenderTarget, input_focus::{ directional_navigation::{ AutoNavigationConfig, DirectionalNavigationMap, DirectionalNavigationPlugin, }, tab_navigation::TabIndex, AutoFocus, InputFocus, InputFocusVisible, }, math::{CompassOctant, Dir2}, picking::{ backend::HitData, pointer::{Location, PointerId}, }, platform::collections::HashSet, prelude::*, ui::auto_directional_navigation::{AutoDirectionalNavigation, AutoDirectionalNavigator}, ui_widgets::Button, }; 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, 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 200 pixels apart between their closest edges max_search_distance: Some(200.0), // Prefer nodes that are well-aligned prefer_aligned: true, }) .init_resource::() // For automatic navigation, UI entities will have the component `AutoDirectionalNavigation` // and will be automatically connected by the navigation system. // We will also add some new edges that the automatic navigation system // cannot create by itself by inserting them into `DirectionalNavigationMap` .add_systems(Startup, setup_paged_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 .run_if(|input_focus: Res| input_focus.is_changed()), update_key_display, ), ) .add_observer(universal_button_click_behavior) .run(); } const PAGE_1_NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_400; const PAGE_1_PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::BLUE_500; const PAGE_1_FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::BLUE_50; const PAGE_2_NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::RED_400; const PAGE_2_PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::RED_500; const PAGE_2_FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::RED_50; const PAGE_3_NORMAL_BUTTON: Srgba = bevy::color::palettes::tailwind::GREEN_400; const PAGE_3_PRESSED_BUTTON: Srgba = bevy::color::palettes::tailwind::GREEN_500; const PAGE_3_FOCUSED_BORDER: Srgba = bevy::color::palettes::tailwind::GREEN_50; const NORMAL_BUTTON_COLORS: [Srgba; 3] = [ PAGE_1_NORMAL_BUTTON, PAGE_2_NORMAL_BUTTON, PAGE_3_NORMAL_BUTTON, ]; const PRESSED_BUTTON_COLORS: [Srgba; 3] = [ PAGE_1_PRESSED_BUTTON, PAGE_2_PRESSED_BUTTON, PAGE_3_PRESSED_BUTTON, ]; const FOCUSED_BORDER_COLORS: [Srgba; 3] = [ PAGE_1_FOCUSED_BORDER, PAGE_2_FOCUSED_BORDER, PAGE_3_FOCUSED_BORDER, ]; /// Marker component for the text that displays the currently focused button #[derive(Component, Clone, Copy, Default)] struct FocusDisplay; /// Marker component for the text that displays the last key pressed #[derive(Component, Clone, Copy, Default)] struct KeyDisplay; /// Component that stores which page a button is on #[derive(Component, Clone, Copy, Default)] struct Page(usize); /// Observer for button clicks. Clicking on a button is considered navigation to that button. /// This is also triggered when `interact_with_focused_button` simulates a button click with a /// manually sent pointer click event fn universal_button_click_behavior( mut click: On, mut button_query: Query<(&mut BackgroundColor, &Page, &mut ResetTimer)>, mut focus_visible: ResMut, ) { let button_entity = click.entity; if let Ok((mut color, page, mut reset_timer)) = button_query.get_mut(button_entity) { color.0 = PRESSED_BUTTON_COLORS[page.0].into(); reset_timer.0 = Timer::from_seconds(0.3, TimerMode::Once); click.propagate(false); // The focus should be visible because a button was clicked. // It may have been set to false by the `PointerFocusPlugin` focus_visible.0 = true; } } #[derive(Component, Clone, Default, Deref, DerefMut)] struct ResetTimer(Timer); fn reset_button_after_interaction( time: Res