use crate::{
events::{Direction, NavRequest, ScopeDirection},
resolve::{FocusState, Focusable, Focused, ScreenBoundaries},
};
use bevy::utils::FloatOrd;
use bevy::{ecs::system::SystemParam, prelude::*};
#[derive(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 mouse_action: MouseButton,
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,
mouse_action: MouseButton::Left,
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);
}
#[derive(SystemParam)]
pub struct NodePosQuery<'w, 's, T: Component> {
entities: Query<
'w,
's,
(
Entity,
&'static T,
&'static GlobalTransform,
&'static Focusable,
),
>,
boundaries: Option<Res<'w, ScreenBoundaries>>,
}
impl<'w, 's, T: Component> NodePosQuery<'w, 's, T> {
fn cursor_pos(&self, at: Vec2) -> Option<Vec2> {
let boundaries = self.boundaries.as_ref()?;
Some(at * boundaries.scale + boundaries.position)
}
}
fn is_in_node<T: ScreenSize>(
at: Vec2,
(_, node, trans, _): &(Entity, &T, &GlobalTransform, &Focusable),
) -> bool {
let ui_pos = trans.translation().truncate();
let node_half_size = node.size() / 2.0;
let min = ui_pos - node_half_size;
let max = ui_pos + node_half_size;
(min.x..max.x).contains(&at.x) && (min.y..max.y).contains(&at.y)
}
pub fn ui_focusable_at<T>(at: Vec2, query: &NodePosQuery<T>) -> Option<Entity>
where
T: ScreenSize + Component,
{
let world_at = query.cursor_pos(at)?;
query
.entities
.iter()
.filter(|query_elem| is_in_node(world_at, query_elem))
.max_by_key(|elem| FloatOrd(elem.2.translation().z))
.map(|elem| elem.0)
}
fn cursor_pos(windows: &Windows) -> Option<Vec2> {
windows.get_primary().and_then(|window| {
let pos = window.cursor_position()?;
Some(Vec2 {
y: window.height() - pos.y,
..pos
})
})
}
pub trait ScreenSize {
fn size(&self) -> Vec2;
}
#[cfg(feature = "bevy_ui")]
impl ScreenSize for Node {
fn size(&self) -> Vec2 {
self.size()
}
}
#[cfg(feature = "bevy_ui")]
#[allow(clippy::too_many_arguments)]
pub fn default_mouse_input(
input_mapping: Res<InputMapping>,
windows: Res<Windows>,
mouse: Res<Input<MouseButton>>,
focusables: NodePosQuery<Node>,
focused: Query<Entity, With<Focused>>,
nav_cmds: EventWriter<NavRequest>,
last_pos: Local<Vec2>,
) {
generic_default_mouse_input(
input_mapping,
windows,
mouse,
focusables,
focused,
nav_cmds,
last_pos,
);
}
#[allow(clippy::too_many_arguments)]
pub fn generic_default_mouse_input<T: ScreenSize + Component>(
input_mapping: Res<InputMapping>,
windows: Res<Windows>,
mouse: Res<Input<MouseButton>>,
focusables: NodePosQuery<T>,
focused: Query<Entity, With<Focused>>,
mut nav_cmds: EventWriter<NavRequest>,
mut last_pos: Local<Vec2>,
) {
let no_focusable_msg = "Entity with `Focused` component must also have a `Focusable` component";
let cursor_pos = match cursor_pos(&windows) {
Some(c) => c,
None => return,
};
let world_cursor_pos = match focusables.cursor_pos(cursor_pos) {
Some(c) => c,
None => return,
};
let released = mouse.just_released(input_mapping.mouse_action);
let pressed = mouse.pressed(input_mapping.mouse_action);
let focused = focused.get_single();
let camera_moved = focusables.boundaries.map_or(false, |b| b.is_changed());
let mouse_moved = *last_pos != cursor_pos;
if (!released && !pressed) && !mouse_moved && !camera_moved {
return;
} else {
*last_pos = cursor_pos;
}
let pressed = input_mapping.focus_follows_mouse || pressed;
let hovering_focused = |focused| {
let focused = focusables.entities.get(focused).expect(no_focusable_msg);
is_in_node(world_cursor_pos, &focused)
};
let hovering = focused.map_or(false, hovering_focused);
let set_focused = (pressed || released) && !hovering;
if set_focused {
let under_mouse = focusables
.entities
.iter()
.filter(|query_elem| query_elem.3.state() != FocusState::Blocked)
.filter(|query_elem| is_in_node(world_cursor_pos, query_elem))
.max_by_key(|elem| FloatOrd(elem.2.translation().z))
.map(|elem| elem.0);
let to_target = match under_mouse {
Some(c) => c,
None => return,
};
nav_cmds.send(NavRequest::FocusOn(to_target));
}
if released && (set_focused || hovering) {
nav_cmds.send(NavRequest::Action);
}
}
#[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>)>>,
windows: Res<Windows>,
images: Res<Assets<Image>>,
) {
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 target_info = cam.target.get_render_target_info(&windows, &images)?;
let new_boundaries = ScreenBoundaries {
position: Vec2::ZERO,
screen_edge: crate::resolve::Rect {
max: target_info.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 = "bevy_ui")]
pub struct DefaultNavigationSystems;
#[cfg(feature = "bevy_ui")]
impl Plugin for DefaultNavigationSystems {
fn build(&self, app: &mut App) {
use crate::NavRequestSystem;
app.init_resource::<InputMapping>()
.add_system(default_mouse_input.before(NavRequestSystem))
.add_system(default_gamepad_input.before(NavRequestSystem))
.add_system(default_keyboard_input.before(NavRequestSystem))
.add_system(
update_boundaries
.before(NavRequestSystem)
.before(default_mouse_input),
);
}
}