Path: blob/main/crates/bevy_ui/src/auto_directional_navigation.rs
9356 views
//! An automatic directional navigation system, powered by the [`AutoDirectionalNavigation`]1//! component.2//!3//! Unlike the navigation system provided by `bevy_input_focus`, the automatic directional4//! navigation system does not require specifying navigation edges. Just simply add the5//! [`AutoDirectionalNavigation`] component to your entities, and the system will automatically6//! calculate the navigation edges between entities based on screen position.7//!8//! [`AutoDirectionalNavigator`] replaces the manual directional navigation system9//! provided by the [`DirectionalNavigation`] system parameter from `bevy_input_focus`. The10//! [`AutoDirectionalNavigator`] will first navigate using manual override edges defined in the11//! [`DirectionalNavigationMap`](bevy_input_focus::directional_navigation::DirectionalNavigationMap).12//! If no manual overrides are defined, automatic navigation will occur between entities based on13//! screen position.14//!15//! If any resulting navigation behavior is undesired, [`AutoNavigationConfig`] can be tweaked or16//! manual overrides can be specified using the17//! [`DirectionalNavigationMap`](bevy_input_focus::directional_navigation::DirectionalNavigationMap).1819use crate::{ComputedNode, ComputedUiTargetCamera, UiGlobalTransform};20use bevy_camera::visibility::InheritedVisibility;21use bevy_ecs::{prelude::*, system::SystemParam};22use bevy_math::{ops, CompassOctant, Vec2};2324use bevy_input_focus::{25directional_navigation::{26AutoNavigationConfig, DirectionalNavigation, DirectionalNavigationError, FocusableArea,27},28navigator::find_best_candidate,29};3031use bevy_reflect::{prelude::*, Reflect};3233/// Marker component to enable automatic directional navigation to and from the entity.34///35/// Simply add this component to your UI entities so that the navigation algorithm will36/// consider this entity in its calculations:37///38/// ```rust39/// # use bevy_ecs::prelude::*;40/// # use bevy_ui::auto_directional_navigation::AutoDirectionalNavigation;41/// fn spawn_auto_nav_button(mut commands: Commands) {42/// commands.spawn((43/// // ... Button, Node, etc. ...44/// AutoDirectionalNavigation::default(), // That's it!45/// ));46/// }47/// ```48///49/// # Multi-Layer UIs and Z-Index50///51/// **Important**: Automatic navigation is currently **z-index agnostic** and treats52/// all entities with `AutoDirectionalNavigation` as a flat set, regardless of which UI layer53/// or z-index they belong to. This means navigation may jump between different layers (e.g.,54/// from a background menu to an overlay popup).55///56/// **Workarounds** for multi-layer UIs:57///58/// 1. **Per-layer manual edge generation**: Query entities by layer and call59/// [`auto_generate_navigation_edges()`](bevy_input_focus::directional_navigation::auto_generate_navigation_edges)60/// separately for each layer:61/// ```rust,ignore62/// for layer in &layers {63/// let nodes: Vec<FocusableArea> = query_layer(layer).collect();64/// auto_generate_navigation_edges(&mut nav_map, &nodes, &config);65/// }66/// ```67///68/// 2. **Manual cross-layer navigation**: Use69/// [`DirectionalNavigationMap::add_edge()`](bevy_input_focus::directional_navigation::DirectionalNavigationMap::add_edge)70/// to define explicit connections between layers (e.g., "Back" button to main menu).71///72/// 3. **Remove component when layer is hidden**: Dynamically add/remove73/// [`AutoDirectionalNavigation`] based on which layers are currently active.74///75/// See issue [#21679](https://github.com/bevyengine/bevy/issues/21679) for planned76/// improvements to layer-aware automatic navigation.77///78/// # Opting Out79///80/// To disable automatic navigation for specific entities:81///82/// - **Remove the component**: Simply don't add [`AutoDirectionalNavigation`] to entities83/// that should only use manual navigation edges.84/// - **Dynamically toggle**: Remove/insert the component at runtime to enable/disable85/// automatic navigation as needed.86///87/// Manual edges defined via [`DirectionalNavigationMap`](bevy_input_focus::directional_navigation::DirectionalNavigationMap)88/// will override any automatically calculated edges.89///90/// # Additional Requirements91///92/// Entities must also have:93/// - [`ComputedNode`] - for size information94/// - [`UiGlobalTransform`] - for position information95///96/// These are automatically added by `bevy_ui` when you spawn UI entities.97///98/// # Custom UI Systems99///100/// For custom UI frameworks, you can call101/// [`auto_generate_navigation_edges`](bevy_input_focus::directional_navigation::auto_generate_navigation_edges)102/// directly in your own system instead of using this component.103#[derive(Component, Default, Debug, Clone, Copy, PartialEq, Reflect)]104#[reflect(Component, Default, Debug, PartialEq, Clone)]105pub struct AutoDirectionalNavigation {106/// Whether to also consider `TabIndex` for navigation order hints.107/// Currently unused but reserved for future functionality.108pub respect_tab_order: bool,109}110111/// A system parameter for combining manual and auto navigation between focusable entities in a directional way.112/// This wraps the [`DirectionalNavigation`] system parameter provided by `bevy_input_focus` and113/// augments it with auto directional navigation.114/// To use, the [`DirectionalNavigationPlugin`](bevy_input_focus::directional_navigation::DirectionalNavigationPlugin)115/// must be added to the app.116#[derive(SystemParam, Debug)]117pub struct AutoDirectionalNavigator<'w, 's> {118/// A system parameter for the manual directional navigation system provided by `bevy_input_focus`119pub manual_directional_navigation: DirectionalNavigation<'w>,120/// Configuration for the automated portion of the navigation algorithm.121pub config: Res<'w, AutoNavigationConfig>,122/// The entities which can possibly be navigated to automatically.123navigable_entities_query: Query<124'w,125's,126(127Entity,128&'static ComputedUiTargetCamera,129&'static ComputedNode,130&'static UiGlobalTransform,131&'static InheritedVisibility,132),133With<AutoDirectionalNavigation>,134>,135/// A query used to get the target camera and the [`FocusableArea`] for a given entity to be used in automatic navigation.136camera_and_focusable_area_query: Query<137'w,138's,139(140Entity,141&'static ComputedUiTargetCamera,142&'static ComputedNode,143&'static UiGlobalTransform,144),145With<AutoDirectionalNavigation>,146>,147}148149impl<'w, 's> AutoDirectionalNavigator<'w, 's> {150/// Returns the current input focus151pub fn input_focus(&mut self) -> Option<Entity> {152self.manual_directional_navigation.focus.0153}154155/// Tries to find the neighbor in a given direction from the given entity. Assumes the entity is valid.156///157/// Returns a neighbor if successful.158/// Returns None if there is no neighbor in the requested direction.159pub fn navigate(160&mut self,161direction: CompassOctant,162) -> Result<Entity, DirectionalNavigationError> {163if let Some(current_focus) = self.input_focus() {164// Respect manual edges first165if let Ok(new_focus) = self.manual_directional_navigation.navigate(direction) {166self.manual_directional_navigation.focus.set(new_focus);167Ok(new_focus)168} else if let Some((target_camera, origin)) =169self.entity_to_camera_and_focusable_area(current_focus)170&& let Some(new_focus) = find_best_candidate(171&origin,172direction,173&self.get_navigable_nodes(target_camera),174&self.config,175)176{177self.manual_directional_navigation.focus.set(new_focus);178Ok(new_focus)179} else {180Err(DirectionalNavigationError::NoNeighborInDirection {181current_focus,182direction,183})184}185} else {186Err(DirectionalNavigationError::NoFocus)187}188}189190/// Returns a vec of [`FocusableArea`] representing nodes that are eligible to be automatically navigated to.191/// The camera of any navigable nodes will equal the desired `target_camera`.192fn get_navigable_nodes(&self, target_camera: Entity) -> Vec<FocusableArea> {193self.navigable_entities_query194.iter()195.filter_map(196|(entity, computed_target_camera, computed, transform, inherited_visibility)| {197// Skip hidden or zero-size nodes198if computed.is_empty() || !inherited_visibility.get() {199return None;200}201// Accept nodes that have the same target camera as the desired target camera202if let Some(tc) = computed_target_camera.get()203&& tc == target_camera204{205let (scale, rotation, translation) = transform.to_scale_angle_translation();206let scaled_size = computed.size() * computed.inverse_scale_factor() * scale;207let rotated_size = get_rotated_bounds(scaled_size, rotation);208Some(FocusableArea {209entity,210position: translation * computed.inverse_scale_factor(),211size: rotated_size,212})213} else {214// The node either does not have a target camera or it is not the same as the desired one.215None216}217},218)219.collect()220}221222/// Gets the target camera and the [`FocusableArea`] of the provided entity, if it exists.223///224/// Returns None if there was a [`QueryEntityError`](bevy_ecs::query::QueryEntityError) or225/// if the entity does not have a target camera.226fn entity_to_camera_and_focusable_area(227&self,228entity: Entity,229) -> Option<(Entity, FocusableArea)> {230self.camera_and_focusable_area_query.get(entity).map_or(231None,232|(entity, computed_target_camera, computed, transform)| {233if let Some(target_camera) = computed_target_camera.get() {234let (scale, rotation, translation) = transform.to_scale_angle_translation();235let scaled_size = computed.size() * computed.inverse_scale_factor() * scale;236let rotated_size = get_rotated_bounds(scaled_size, rotation);237Some((238target_camera,239FocusableArea {240entity,241position: translation * computed.inverse_scale_factor(),242size: rotated_size,243},244))245} else {246None247}248},249)250}251}252253/// Util used to get the resulting bounds of a UI entity after applying its rotation.254///255/// This is necessary to apply because navigation should only use the final screen position256/// of an entity in automatic navigation calculations. These bounds are used as the entity's size in257/// [`FocusableArea`].258fn get_rotated_bounds(size: Vec2, rotation: f32) -> Vec2 {259if rotation == 0.0 {260return size;261}262let cos_r = ops::cos(rotation).abs();263let sin_r = ops::sin(rotation).abs();264Vec2::new(265size.x * cos_r + size.y * sin_r,266size.x * sin_r + size.y * cos_r,267)268}269270271