use std::time::Duration;
use bevy::{
input_focus::{
directional_navigation::{
DirectionalNavigation, DirectionalNavigationMap, DirectionalNavigationPlugin,
},
InputDispatchPlugin, InputFocus, InputFocusVisible,
},
math::{CompassOctant, FloatOrd},
picking::{
backend::HitData,
pointer::{Location, PointerId},
},
platform::collections::{HashMap, HashSet},
prelude::*,
render::camera::NormalizedRenderTarget,
};
fn main() {
App::new()
.add_plugins((
DefaultPlugins,
InputDispatchPlugin,
DirectionalNavigationPlugin,
))
.insert_resource(InputFocusVisible(true))
.init_resource::<ActionState>()
.add_systems(Startup, setup_ui)
.add_systems(PreUpdate, (process_inputs, navigate).chain())
.add_systems(
Update,
(
highlight_focused_element,
interact_with_focused_button,
reset_button_after_interaction,
),
)
.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;
fn universal_button_click_behavior(
mut trigger: Trigger<Pointer<Click>>,
mut button_query: Query<(&mut BackgroundColor, &mut ResetTimer)>,
) {
let button_entity = trigger.target();
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);
trigger.propagate(false);
}
}
#[derive(Component, Default, Deref, DerefMut)]
struct ResetTimer(Timer);
fn reset_button_after_interaction(
time: Res<Time>,
mut query: Query<(&mut ResetTimer, &mut BackgroundColor)>,
) {
for (mut reset_timer, mut color) in query.iter_mut() {
reset_timer.tick(time.delta());
if reset_timer.just_finished() {
color.0 = NORMAL_BUTTON.into();
}
}
}
fn setup_ui(
mut commands: Commands,
mut directional_nav_map: ResMut<DirectionalNavigationMap>,
mut input_focus: ResMut<InputFocus>,
) {
const N_ROWS: u16 = 5;
const N_COLS: u16 = 3;
commands.spawn(Camera2d);
let root_node = commands
.spawn(Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default()
})
.id();
let instructions = commands
.spawn((
Text::new("Use arrow keys or D-pad to navigate. \
Click the buttons, or press Enter / the South gamepad button to interact with the focused button."),
Node {
width: Val::Px(300.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
margin: UiRect::all(Val::Px(12.0)),
..default()
},
))
.id();
let grid_root_entity = commands
.spawn(Node {
display: Display::Grid,
width: Val::Percent(100.),
height: Val::Percent(100.),
grid_template_columns: RepeatedGridTrack::auto(N_COLS),
grid_template_rows: RepeatedGridTrack::auto(N_ROWS),
..default()
})
.id();
commands
.entity(root_node)
.add_children(&[instructions, grid_root_entity]);
let mut button_entities: HashMap<(u16, u16), Entity> = HashMap::default();
for row in 0..N_ROWS {
for col in 0..N_COLS {
let button_name = format!("Button {}-{}", row, col);
let button_entity = commands
.spawn((
Button,
Node {
width: Val::Px(200.0),
height: Val::Px(120.0),
border: UiRect::all(Val::Px(4.0)),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
align_self: AlignSelf::Center,
justify_self: JustifySelf::Center,
..default()
},
ResetTimer::default(),
BorderRadius::all(Val::Px(16.0)),
BackgroundColor::from(NORMAL_BUTTON),
Name::new(button_name.clone()),
))
.with_child((
Text::new(button_name),
TextLayout {
justify: JustifyText::Center,
..default()
},
))
.id();
commands.entity(grid_root_entity).add_child(button_entity);
button_entities.insert((row, col), button_entity);
}
}
for row in 0..N_ROWS {
let entities_in_row: Vec<Entity> = (0..N_COLS)
.map(|col| button_entities.get(&(row, col)).unwrap())
.copied()
.collect();
directional_nav_map.add_looping_edges(&entities_in_row, CompassOctant::East);
}
for col in 0..N_COLS {
let entities_in_column: Vec<Entity> = (0..N_ROWS)
.map(|row| button_entities.get(&(row, col)).unwrap())
.copied()
.collect();
directional_nav_map.add_edges(&entities_in_column, CompassOctant::South);
}
let top_left_entity = *button_entities.get(&(0, 0)).unwrap();
input_focus.set(top_left_entity);
}
#[derive(Debug, PartialEq, Eq, Hash)]
enum DirectionalNavigationAction {
Up,
Down,
Left,
Right,
Select,
}
impl DirectionalNavigationAction {
fn variants() -> Vec<Self> {
vec![
DirectionalNavigationAction::Up,
DirectionalNavigationAction::Down,
DirectionalNavigationAction::Left,
DirectionalNavigationAction::Right,
DirectionalNavigationAction::Select,
]
}
fn keycode(&self) -> KeyCode {
match self {
DirectionalNavigationAction::Up => KeyCode::ArrowUp,
DirectionalNavigationAction::Down => KeyCode::ArrowDown,
DirectionalNavigationAction::Left => KeyCode::ArrowLeft,
DirectionalNavigationAction::Right => KeyCode::ArrowRight,
DirectionalNavigationAction::Select => KeyCode::Enter,
}
}
fn gamepad_button(&self) -> GamepadButton {
match self {
DirectionalNavigationAction::Up => GamepadButton::DPadUp,
DirectionalNavigationAction::Down => GamepadButton::DPadDown,
DirectionalNavigationAction::Left => GamepadButton::DPadLeft,
DirectionalNavigationAction::Right => GamepadButton::DPadRight,
DirectionalNavigationAction::Select => GamepadButton::South,
}
}
}
#[derive(Default, Resource)]
struct ActionState {
pressed_actions: HashSet<DirectionalNavigationAction>,
}
fn process_inputs(
mut action_state: ResMut<ActionState>,
keyboard_input: Res<ButtonInput<KeyCode>>,
gamepad_input: Query<&Gamepad>,
) {
action_state.pressed_actions.clear();
for action in DirectionalNavigationAction::variants() {
if keyboard_input.just_pressed(action.keycode()) {
action_state.pressed_actions.insert(action);
}
}
for gamepad in gamepad_input.iter() {
for action in DirectionalNavigationAction::variants() {
if gamepad.just_pressed(action.gamepad_button()) {
action_state.pressed_actions.insert(action);
}
}
}
}
fn navigate(action_state: Res<ActionState>, mut directional_navigation: DirectionalNavigation) {
let net_east_west = action_state
.pressed_actions
.contains(&DirectionalNavigationAction::Right) as i8
- action_state
.pressed_actions
.contains(&DirectionalNavigationAction::Left) as i8;
let net_north_south = action_state
.pressed_actions
.contains(&DirectionalNavigationAction::Up) as i8
- action_state
.pressed_actions
.contains(&DirectionalNavigationAction::Down) as i8;
let maybe_direction = match (net_east_west, net_north_south) {
(0, 0) => None,
(0, 1) => Some(CompassOctant::North),
(1, 1) => Some(CompassOctant::NorthEast),
(1, 0) => Some(CompassOctant::East),
(1, -1) => Some(CompassOctant::SouthEast),
(0, -1) => Some(CompassOctant::South),
(-1, -1) => Some(CompassOctant::SouthWest),
(-1, 0) => Some(CompassOctant::West),
(-1, 1) => Some(CompassOctant::NorthWest),
_ => None,
};
if let Some(direction) = maybe_direction {
match directional_navigation.navigate(direction) {
Ok(entity) => {
println!("Navigated {direction:?} successfully. {entity} is now focused.");
}
Err(e) => println!("Navigation failed: {e}"),
}
}
}
fn highlight_focused_element(
input_focus: Res<InputFocus>,
input_focus_visible: Res<InputFocusVisible>,
mut query: Query<(Entity, &mut BorderColor)>,
) {
for (entity, mut border_color) in query.iter_mut() {
if input_focus.0 == Some(entity) && input_focus_visible.0 {
border_color.0 = FOCUSED_BORDER.into();
} else {
border_color.0 = Color::NONE;
}
}
}
fn interact_with_focused_button(
action_state: Res<ActionState>,
input_focus: Res<InputFocus>,
mut commands: Commands,
) {
if action_state
.pressed_actions
.contains(&DirectionalNavigationAction::Select)
{
if let Some(focused_entity) = input_focus.0 {
commands.trigger_targets(
Pointer::<Click> {
target: focused_entity,
pointer_id: PointerId::Mouse,
pointer_location: Location {
target: NormalizedRenderTarget::Image(
bevy_render::camera::ImageRenderTarget {
handle: Handle::default(),
scale_factor: FloatOrd(1.0),
},
),
position: Vec2::ZERO,
},
event: Click {
button: PointerButton::Primary,
hit: HitData {
camera: Entity::PLACEHOLDER,
depth: 0.0,
position: None,
normal: None,
},
duration: Duration::from_secs_f32(0.1),
},
},
focused_entity,
);
}
}
}