bevy_ui_navigation/
systems.rs

1//! System for the navigation tree and default input systems to get started.
2use 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/// Control default ui navigation input buttons
16#[derive(Resource)]
17#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Resource))]
18pub struct InputMapping {
19    /// Whether to use keybaord keys for navigation (instead of just actions).
20    pub keyboard_navigation: bool,
21    /// The gamepads to use for the UI. If empty, default to gamepad 0
22    pub gamepads: Vec<Gamepad>,
23    /// Deadzone on the gamepad left stick for ui navigation
24    pub joystick_ui_deadzone: f32,
25    /// X axis of gamepad stick
26    pub move_x: GamepadAxisType,
27    /// Y axis of gamepad stick
28    pub move_y: GamepadAxisType,
29    /// Gamepad button for [`Direction::West`] [`NavRequest::Move`]
30    pub left_button: GamepadButtonType,
31    /// Gamepad button for [`Direction::East`] [`NavRequest::Move`]
32    pub right_button: GamepadButtonType,
33    /// Gamepad button for [`Direction::North`] [`NavRequest::Move`]
34    pub up_button: GamepadButtonType,
35    /// Gamepad button for [`Direction::South`] [`NavRequest::Move`]
36    pub down_button: GamepadButtonType,
37    /// Gamepad button for [`NavRequest::Action`]
38    pub action_button: GamepadButtonType,
39    /// Gamepad button for [`NavRequest::Cancel`]
40    pub cancel_button: GamepadButtonType,
41    /// Gamepad button for [`ScopeDirection::Previous`] [`NavRequest::ScopeMove`]
42    pub previous_button: GamepadButtonType,
43    /// Gamepad button for [`ScopeDirection::Next`] [`NavRequest::ScopeMove`]
44    pub next_button: GamepadButtonType,
45    /// Gamepad button for [`NavRequest::Unlock`]
46    pub free_button: GamepadButtonType,
47    /// Keyboard key for [`Direction::West`] [`NavRequest::Move`]
48    pub key_left: KeyCode,
49    /// Keyboard key for [`Direction::East`] [`NavRequest::Move`]
50    pub key_right: KeyCode,
51    /// Keyboard key for [`Direction::North`] [`NavRequest::Move`]
52    pub key_up: KeyCode,
53    /// Keyboard key for [`Direction::South`] [`NavRequest::Move`]
54    pub key_down: KeyCode,
55    /// Alternative keyboard key for [`Direction::West`] [`NavRequest::Move`]
56    pub key_left_alt: KeyCode,
57    /// Alternative keyboard key for [`Direction::East`] [`NavRequest::Move`]
58    pub key_right_alt: KeyCode,
59    /// Alternative keyboard key for [`Direction::North`] [`NavRequest::Move`]
60    pub key_up_alt: KeyCode,
61    /// Alternative keyboard key for [`Direction::South`] [`NavRequest::Move`]
62    pub key_down_alt: KeyCode,
63    /// Keyboard key for [`NavRequest::Action`]
64    pub key_action: KeyCode,
65    /// Keyboard key for [`NavRequest::Cancel`]
66    pub key_cancel: KeyCode,
67    /// Keyboard key for [`ScopeDirection::Next`] [`NavRequest::ScopeMove`]
68    pub key_next: KeyCode,
69    /// Alternative keyboard key for [`ScopeDirection::Next`] [`NavRequest::ScopeMove`]
70    pub key_next_alt: KeyCode,
71    /// Keyboard key for [`ScopeDirection::Previous`] [`NavRequest::ScopeMove`]
72    pub key_previous: KeyCode,
73    /// Keyboard key for [`NavRequest::Unlock`]
74    pub key_free: KeyCode,
75    /// Whether mouse hover gives focus to [`Focusable`](crate::resolve::Focusable) elements.
76    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
114/// `mapping { XYZ::X => ABC::A, XYZ::Y => ABC::B, XYZ::Z => ABC::C }: [(XYZ, ABC)]`
115macro_rules! mapping {
116    ($($from:expr => $to:expr),* ) => ([$( ( $from, $to ) ),*])
117}
118
119/// A system to send gamepad control events to the focus system
120///
121/// Dpad and left stick for movement, `LT` and `RT` for scopped menus, `A` `B`
122/// for selection and cancel.
123///
124/// The button mapping may be controlled through the [`InputMapping`] resource.
125/// You may however need to customize the behavior of this system (typically
126/// when integrating in the game) in this case, you should write your own
127/// system that sends [`NavRequest`] events
128pub 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        // Do not compute navigation if there is no focus to change
141        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
190/// A system to send keyboard control events to the focus system.
191///
192/// supports `WASD` and arrow keys for the directions, `E`, `Q` and `Tab` for
193/// scopped menus, `Backspace` and `Enter` for cancel and selection.
194///
195/// The button mapping may be controlled through the [`InputMapping`] resource.
196/// You may however need to customize the behavior of this system (typically
197/// when integrating in the game) in this case, you should write your own
198/// system that sends [`NavRequest`] events.
199pub 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        // Do not compute navigation if there is no focus to change
210        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/// Update [`ScreenBoundaries`] resource when the UI camera change
243/// (assuming there is a unique one).
244///
245/// See [`ScreenBoundaries`] doc for details.
246#[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    // TODO: this assumes there is only a single camera with activated UI.
254    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/// Send [`NavRequest`]s when an [`Entity`] is clicked, as defined by
290/// [`bevy_mod_picking`].
291///
292/// # `bevy_mod_picking` features
293///
294/// `bevy-ui-navigation` inserts the [`DefaultPickingPlugins`].
295/// This means you can control how mouse picking works by… picking the
296/// feature flags that are most relevant to you:
297///
298/// Check the [`bevy_mod_picking` feature flags docs.rs page][bmp-features]
299/// for a list of features.
300///
301/// `bevy-ui-navigation` only enables `backed_bevy_ui`, when the `bevy_ui` flag
302/// is enabled.
303///
304/// Depend explicitly on `bevy_mod_picking` and enable the flags you want to
305/// extend the picking functionality to, well, 3D objects, sprites, anything
306/// really.
307///
308/// [bmp-features]: https://docs.rs/crate/bevy_mod_picking/0.15.0/features
309#[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        // TODO(clean): This shouldn't be the responsability of the input system.
320        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
346/// Default input systems for ui navigation.
347pub 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}