1use crate::{
3 events::{Direction, NavRequest, ScopeDirection},
4 resolve::Focused,
5};
6
7#[cfg(feature = "bevy_ui")]
8use crate::resolve::ScreenBoundaries;
9use bevy::prelude::*;
10#[cfg(feature = "bevy_reflect")]
11use bevy::{ecs::reflect::ReflectResource, reflect::Reflect};
12#[cfg(feature = "pointer_focus")]
13use bevy_mod_picking::prelude::*;
14
15#[derive(Resource)]
17#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Resource))]
18pub struct InputMapping {
19 pub keyboard_navigation: bool,
21 pub gamepads: Vec<Gamepad>,
23 pub joystick_ui_deadzone: f32,
25 pub move_x: GamepadAxisType,
27 pub move_y: GamepadAxisType,
29 pub left_button: GamepadButtonType,
31 pub right_button: GamepadButtonType,
33 pub up_button: GamepadButtonType,
35 pub down_button: GamepadButtonType,
37 pub action_button: GamepadButtonType,
39 pub cancel_button: GamepadButtonType,
41 pub previous_button: GamepadButtonType,
43 pub next_button: GamepadButtonType,
45 pub free_button: GamepadButtonType,
47 pub key_left: KeyCode,
49 pub key_right: KeyCode,
51 pub key_up: KeyCode,
53 pub key_down: KeyCode,
55 pub key_left_alt: KeyCode,
57 pub key_right_alt: KeyCode,
59 pub key_up_alt: KeyCode,
61 pub key_down_alt: KeyCode,
63 pub key_action: KeyCode,
65 pub key_cancel: KeyCode,
67 pub key_next: KeyCode,
69 pub key_next_alt: KeyCode,
71 pub key_previous: KeyCode,
73 pub key_free: KeyCode,
75 pub focus_follows_mouse: bool,
77}
78impl Default for InputMapping {
79 fn default() -> Self {
80 InputMapping {
81 keyboard_navigation: false,
82 gamepads: vec![Gamepad { id: 0 }],
83 joystick_ui_deadzone: 0.36,
84 move_x: GamepadAxisType::LeftStickX,
85 move_y: GamepadAxisType::LeftStickY,
86 left_button: GamepadButtonType::DPadLeft,
87 right_button: GamepadButtonType::DPadRight,
88 up_button: GamepadButtonType::DPadUp,
89 down_button: GamepadButtonType::DPadDown,
90 action_button: GamepadButtonType::South,
91 cancel_button: GamepadButtonType::East,
92 previous_button: GamepadButtonType::LeftTrigger,
93 next_button: GamepadButtonType::RightTrigger,
94 free_button: GamepadButtonType::Start,
95 key_left: KeyCode::A,
96 key_right: KeyCode::D,
97 key_up: KeyCode::W,
98 key_down: KeyCode::S,
99 key_left_alt: KeyCode::Left,
100 key_right_alt: KeyCode::Right,
101 key_up_alt: KeyCode::Up,
102 key_down_alt: KeyCode::Down,
103 key_action: KeyCode::Space,
104 key_cancel: KeyCode::Back,
105 key_next: KeyCode::E,
106 key_next_alt: KeyCode::Tab,
107 key_previous: KeyCode::Q,
108 key_free: KeyCode::Escape,
109 focus_follows_mouse: false,
110 }
111 }
112}
113
114macro_rules! mapping {
116 ($($from:expr => $to:expr),* ) => ([$( ( $from, $to ) ),*])
117}
118
119pub fn default_gamepad_input(
129 mut nav_cmds: EventWriter<NavRequest>,
130 has_focused: Query<(), With<Focused>>,
131 input_mapping: Res<InputMapping>,
132 buttons: Res<Input<GamepadButton>>,
133 axis: Res<Axis<GamepadAxis>>,
134 mut ui_input_status: Local<bool>,
135) {
136 use Direction::*;
137 use NavRequest::{Action, Cancel, Move, ScopeMove, Unlock};
138
139 if has_focused.is_empty() {
140 return;
142 }
143
144 for &gamepad in &input_mapping.gamepads {
145 macro_rules! axis_delta {
146 ($dir:ident, $axis:ident) => {{
147 let axis_type = input_mapping.$axis;
148 axis.get(GamepadAxis { gamepad, axis_type })
149 .map_or(Vec2::ZERO, |v| Vec2::$dir * v)
150 }};
151 }
152
153 let delta = axis_delta!(Y, move_y) + axis_delta!(X, move_x);
154 if delta.length_squared() > input_mapping.joystick_ui_deadzone && !*ui_input_status {
155 let direction = match () {
156 () if delta.y < delta.x && delta.y < -delta.x => South,
157 () if delta.y < delta.x => East,
158 () if delta.y >= delta.x && delta.y > -delta.x => North,
159 () => West,
160 };
161 nav_cmds.send(Move(direction));
162 *ui_input_status = true;
163 } else if delta.length_squared() <= input_mapping.joystick_ui_deadzone {
164 *ui_input_status = false;
165 }
166
167 let command_mapping = mapping! {
168 input_mapping.action_button => Action,
169 input_mapping.cancel_button => Cancel,
170 input_mapping.left_button => Move(Direction::West),
171 input_mapping.right_button => Move(Direction::East),
172 input_mapping.up_button => Move(Direction::North),
173 input_mapping.down_button => Move(Direction::South),
174 input_mapping.next_button => ScopeMove(ScopeDirection::Next),
175 input_mapping.free_button => Unlock,
176 input_mapping.previous_button => ScopeMove(ScopeDirection::Previous)
177 };
178 for (button_type, request) in command_mapping {
179 let button = GamepadButton {
180 gamepad,
181 button_type,
182 };
183 if buttons.just_pressed(button) {
184 nav_cmds.send(request)
185 }
186 }
187 }
188}
189
190pub fn default_keyboard_input(
200 has_focused: Query<(), With<Focused>>,
201 keyboard: Res<Input<KeyCode>>,
202 input_mapping: Res<InputMapping>,
203 mut nav_cmds: EventWriter<NavRequest>,
204) {
205 use Direction::*;
206 use NavRequest::*;
207
208 if has_focused.is_empty() {
209 return;
211 }
212
213 let with_movement = mapping! {
214 input_mapping.key_up => Move(North),
215 input_mapping.key_down => Move(South),
216 input_mapping.key_left => Move(West),
217 input_mapping.key_right => Move(East),
218 input_mapping.key_up_alt => Move(North),
219 input_mapping.key_down_alt => Move(South),
220 input_mapping.key_left_alt => Move(West),
221 input_mapping.key_right_alt => Move(East)
222 };
223 let without_movement = mapping! {
224 input_mapping.key_action => Action,
225 input_mapping.key_cancel => Cancel,
226 input_mapping.key_next => ScopeMove(ScopeDirection::Next),
227 input_mapping.key_next_alt => ScopeMove(ScopeDirection::Next),
228 input_mapping.key_free => Unlock,
229 input_mapping.key_previous => ScopeMove(ScopeDirection::Previous)
230 };
231 let mut send_command = |&(key, request)| {
232 if keyboard.just_pressed(key) {
233 nav_cmds.send(request)
234 }
235 };
236 if input_mapping.keyboard_navigation {
237 with_movement.iter().for_each(&mut send_command);
238 }
239 without_movement.iter().for_each(send_command);
240}
241
242#[cfg(feature = "bevy_ui")]
247#[allow(clippy::type_complexity)]
248pub fn update_boundaries(
249 mut commands: Commands,
250 mut boundaries: Option<ResMut<ScreenBoundaries>>,
251 cam: Query<(&Camera, Option<&UiCameraConfig>), Or<(Changed<Camera>, Changed<UiCameraConfig>)>>,
252) {
253 let first_visible_ui_cam = |(cam, config): (_, Option<&UiCameraConfig>)| {
255 config.map_or(true, |c| c.show_ui).then_some(cam)
256 };
257 let mut update_boundaries = || {
258 let cam = cam.iter().find_map(first_visible_ui_cam)?;
259 let physical_size = cam.physical_viewport_size()?;
260 let new_boundaries = ScreenBoundaries {
261 position: Vec2::ZERO,
262 screen_edge: crate::resolve::Rect {
263 max: physical_size.as_vec2(),
264 min: Vec2::ZERO,
265 },
266 scale: 1.0,
267 };
268 if let Some(boundaries) = boundaries.as_mut() {
269 **boundaries = new_boundaries;
270 } else {
271 commands.insert_resource(new_boundaries);
272 }
273 Some(())
274 };
275 update_boundaries();
276}
277
278#[cfg(feature = "pointer_focus")]
279fn send_request<E: EntityEvent>(
280 f: impl Fn(Query<&crate::resolve::Focusable>, Res<ListenerInput<E>>, EventWriter<NavRequest>)
281 + Send
282 + Sync
283 + Copy
284 + 'static,
285) -> impl Fn() -> On<E> {
286 move || On::<E>::run(f)
287}
288
289#[cfg(feature = "pointer_focus")]
310#[allow(clippy::type_complexity)]
311pub fn enable_click_request(
312 input_mapping: Res<InputMapping>,
313 to_add: Query<Entity, (With<crate::resolve::Focusable>, Without<On<Pointer<Click>>>)>,
314 mut commands: Commands,
315) {
316 use crate::prelude::FocusState::Blocked;
317
318 let on_click = send_request::<Pointer<Click>>(|q, e, mut evs| {
319 if matches!(q.get(e.listener()), Ok(f) if f.state() != Blocked) {
321 evs.send(NavRequest::FocusOn(e.listener()));
322 evs.send(NavRequest::Action);
323 }
324 });
325 let on_down = send_request::<Pointer<Down>>(|_, e, mut evs| {
326 evs.send(NavRequest::FocusOn(e.listener()));
327 });
328 let on_over = send_request::<Pointer<Over>>(|_, e, mut evs| {
329 evs.send(NavRequest::FocusOn(e.listener()));
330 });
331 if input_mapping.focus_follows_mouse {
332 let cmd_entry = |e| (e, (on_click(), on_down(), on_over()));
333 let batch_cmd: Vec<_> = to_add.iter().map(cmd_entry).collect();
334 if !batch_cmd.is_empty() {
335 commands.insert_or_spawn_batch(batch_cmd);
336 }
337 } else {
338 let cmd_entry = |e| (e, (on_click(), on_down()));
339 let batch_cmd: Vec<_> = to_add.iter().map(cmd_entry).collect();
340 if !batch_cmd.is_empty() {
341 commands.insert_or_spawn_batch(batch_cmd);
342 }
343 };
344}
345
346pub struct DefaultNavigationSystems;
348impl Plugin for DefaultNavigationSystems {
349 fn build(&self, app: &mut App) {
350 use crate::NavRequestSystem;
351 app.init_resource::<InputMapping>().add_systems(
352 Update,
353 (default_gamepad_input, default_keyboard_input).before(NavRequestSystem),
354 );
355
356 #[cfg(feature = "bevy_ui")]
357 app.add_systems(Update, update_boundaries.before(NavRequestSystem));
358
359 #[cfg(feature = "pointer_focus")]
360 app.add_plugins(DefaultPickingPlugins)
361 .add_systems(PostUpdate, enable_click_request);
362 }
363}