use crate::{
events::{Direction, NavRequest, ScopeDirection},
resolve::Focused,
};
#[cfg(feature = "bevy_ui")]
use crate::resolve::ScreenBoundaries;
use bevy::prelude::*;
#[cfg(feature = "bevy_reflect")]
use bevy::{ecs::reflect::ReflectResource, reflect::Reflect};
#[cfg(feature = "pointer_focus")]
use bevy_mod_picking::prelude::*;
#[derive(Resource)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Resource))]
pub struct InputMapping {
pub keyboard_navigation: bool,
pub gamepads: Vec<Gamepad>,
pub joystick_ui_deadzone: f32,
pub move_x: GamepadAxisType,
pub move_y: GamepadAxisType,
pub left_button: GamepadButtonType,
pub right_button: GamepadButtonType,
pub up_button: GamepadButtonType,
pub down_button: GamepadButtonType,
pub action_button: GamepadButtonType,
pub cancel_button: GamepadButtonType,
pub previous_button: GamepadButtonType,
pub next_button: GamepadButtonType,
pub free_button: GamepadButtonType,
pub key_left: KeyCode,
pub key_right: KeyCode,
pub key_up: KeyCode,
pub key_down: KeyCode,
pub key_left_alt: KeyCode,
pub key_right_alt: KeyCode,
pub key_up_alt: KeyCode,
pub key_down_alt: KeyCode,
pub key_action: KeyCode,
pub key_cancel: KeyCode,
pub key_next: KeyCode,
pub key_next_alt: KeyCode,
pub key_previous: KeyCode,
pub key_free: KeyCode,
pub focus_follows_mouse: bool,
}
impl Default for InputMapping {
fn default() -> Self {
InputMapping {
keyboard_navigation: false,
gamepads: vec![Gamepad { id: 0 }],
joystick_ui_deadzone: 0.36,
move_x: GamepadAxisType::LeftStickX,
move_y: GamepadAxisType::LeftStickY,
left_button: GamepadButtonType::DPadLeft,
right_button: GamepadButtonType::DPadRight,
up_button: GamepadButtonType::DPadUp,
down_button: GamepadButtonType::DPadDown,
action_button: GamepadButtonType::South,
cancel_button: GamepadButtonType::East,
previous_button: GamepadButtonType::LeftTrigger,
next_button: GamepadButtonType::RightTrigger,
free_button: GamepadButtonType::Start,
key_left: KeyCode::A,
key_right: KeyCode::D,
key_up: KeyCode::W,
key_down: KeyCode::S,
key_left_alt: KeyCode::Left,
key_right_alt: KeyCode::Right,
key_up_alt: KeyCode::Up,
key_down_alt: KeyCode::Down,
key_action: KeyCode::Space,
key_cancel: KeyCode::Back,
key_next: KeyCode::E,
key_next_alt: KeyCode::Tab,
key_previous: KeyCode::Q,
key_free: KeyCode::Escape,
focus_follows_mouse: false,
}
}
}
macro_rules! mapping {
($($from:expr => $to:expr),* ) => ([$( ( $from, $to ) ),*])
}
pub fn default_gamepad_input(
mut nav_cmds: EventWriter<NavRequest>,
has_focused: Query<(), With<Focused>>,
input_mapping: Res<InputMapping>,
buttons: Res<Input<GamepadButton>>,
axis: Res<Axis<GamepadAxis>>,
mut ui_input_status: Local<bool>,
) {
use Direction::*;
use NavRequest::{Action, Cancel, Move, ScopeMove, Unlock};
if has_focused.is_empty() {
return;
}
for &gamepad in &input_mapping.gamepads {
macro_rules! axis_delta {
($dir:ident, $axis:ident) => {{
let axis_type = input_mapping.$axis;
axis.get(GamepadAxis { gamepad, axis_type })
.map_or(Vec2::ZERO, |v| Vec2::$dir * v)
}};
}
let delta = axis_delta!(Y, move_y) + axis_delta!(X, move_x);
if delta.length_squared() > input_mapping.joystick_ui_deadzone && !*ui_input_status {
let direction = match () {
() if delta.y < delta.x && delta.y < -delta.x => South,
() if delta.y < delta.x => East,
() if delta.y >= delta.x && delta.y > -delta.x => North,
() => West,
};
nav_cmds.send(Move(direction));
*ui_input_status = true;
} else if delta.length_squared() <= input_mapping.joystick_ui_deadzone {
*ui_input_status = false;
}
let command_mapping = mapping! {
input_mapping.action_button => Action,
input_mapping.cancel_button => Cancel,
input_mapping.left_button => Move(Direction::West),
input_mapping.right_button => Move(Direction::East),
input_mapping.up_button => Move(Direction::North),
input_mapping.down_button => Move(Direction::South),
input_mapping.next_button => ScopeMove(ScopeDirection::Next),
input_mapping.free_button => Unlock,
input_mapping.previous_button => ScopeMove(ScopeDirection::Previous)
};
for (button_type, request) in command_mapping {
let button = GamepadButton {
gamepad,
button_type,
};
if buttons.just_pressed(button) {
nav_cmds.send(request)
}
}
}
}
pub fn default_keyboard_input(
has_focused: Query<(), With<Focused>>,
keyboard: Res<Input<KeyCode>>,
input_mapping: Res<InputMapping>,
mut nav_cmds: EventWriter<NavRequest>,
) {
use Direction::*;
use NavRequest::*;
if has_focused.is_empty() {
return;
}
let with_movement = mapping! {
input_mapping.key_up => Move(North),
input_mapping.key_down => Move(South),
input_mapping.key_left => Move(West),
input_mapping.key_right => Move(East),
input_mapping.key_up_alt => Move(North),
input_mapping.key_down_alt => Move(South),
input_mapping.key_left_alt => Move(West),
input_mapping.key_right_alt => Move(East)
};
let without_movement = mapping! {
input_mapping.key_action => Action,
input_mapping.key_cancel => Cancel,
input_mapping.key_next => ScopeMove(ScopeDirection::Next),
input_mapping.key_next_alt => ScopeMove(ScopeDirection::Next),
input_mapping.key_free => Unlock,
input_mapping.key_previous => ScopeMove(ScopeDirection::Previous)
};
let mut send_command = |&(key, request)| {
if keyboard.just_pressed(key) {
nav_cmds.send(request)
}
};
if input_mapping.keyboard_navigation {
with_movement.iter().for_each(&mut send_command);
}
without_movement.iter().for_each(send_command);
}
#[cfg(feature = "bevy_ui")]
#[allow(clippy::type_complexity)]
pub fn update_boundaries(
mut commands: Commands,
mut boundaries: Option<ResMut<ScreenBoundaries>>,
cam: Query<(&Camera, Option<&UiCameraConfig>), Or<(Changed<Camera>, Changed<UiCameraConfig>)>>,
) {
let first_visible_ui_cam = |(cam, config): (_, Option<&UiCameraConfig>)| {
config.map_or(true, |c| c.show_ui).then_some(cam)
};
let mut update_boundaries = || {
let cam = cam.iter().find_map(first_visible_ui_cam)?;
let physical_size = cam.physical_viewport_size()?;
let new_boundaries = ScreenBoundaries {
position: Vec2::ZERO,
screen_edge: crate::resolve::Rect {
max: physical_size.as_vec2(),
min: Vec2::ZERO,
},
scale: 1.0,
};
if let Some(boundaries) = boundaries.as_mut() {
**boundaries = new_boundaries;
} else {
commands.insert_resource(new_boundaries);
}
Some(())
};
update_boundaries();
}
#[cfg(feature = "pointer_focus")]
fn send_request<E: EntityEvent>(
f: impl Fn(Query<&crate::resolve::Focusable>, Res<ListenerInput<E>>, EventWriter<NavRequest>)
+ Send
+ Sync
+ Copy
+ 'static,
) -> impl Fn() -> On<E> {
move || On::<E>::run(f)
}
#[cfg(feature = "pointer_focus")]
#[allow(clippy::type_complexity)]
pub fn enable_click_request(
input_mapping: Res<InputMapping>,
to_add: Query<Entity, (With<crate::resolve::Focusable>, Without<On<Pointer<Click>>>)>,
mut commands: Commands,
) {
use crate::prelude::FocusState::Blocked;
let on_click = send_request::<Pointer<Click>>(|q, e, mut evs| {
if matches!(q.get(e.listener()), Ok(f) if f.state() != Blocked) {
evs.send(NavRequest::FocusOn(e.listener()));
evs.send(NavRequest::Action);
}
});
let on_down = send_request::<Pointer<Down>>(|_, e, mut evs| {
evs.send(NavRequest::FocusOn(e.listener()));
});
let on_over = send_request::<Pointer<Over>>(|_, e, mut evs| {
evs.send(NavRequest::FocusOn(e.listener()));
});
if input_mapping.focus_follows_mouse {
let cmd_entry = |e| (e, (on_click(), on_down(), on_over()));
let batch_cmd: Vec<_> = to_add.iter().map(cmd_entry).collect();
if !batch_cmd.is_empty() {
commands.insert_or_spawn_batch(batch_cmd);
}
} else {
let cmd_entry = |e| (e, (on_click(), on_down()));
let batch_cmd: Vec<_> = to_add.iter().map(cmd_entry).collect();
if !batch_cmd.is_empty() {
commands.insert_or_spawn_batch(batch_cmd);
}
};
}
pub struct DefaultNavigationSystems;
impl Plugin for DefaultNavigationSystems {
fn build(&self, app: &mut App) {
use crate::NavRequestSystem;
app.init_resource::<InputMapping>().add_systems(
Update,
(default_gamepad_input, default_keyboard_input).before(NavRequestSystem),
);
#[cfg(feature = "bevy_ui")]
app.add_systems(Update, update_boundaries.before(NavRequestSystem));
#[cfg(feature = "pointer_focus")]
app.add_plugins(DefaultPickingPlugins)
.add_systems(PostUpdate, enable_click_request);
}
}