bevy_egui_kbgp/
lib.rs

1//! Improve the keyboard and gamepads usage for egui in Bevy.
2//!
3//! Usage:
4//! * Add [`KbgpPlugin`].
5//! * Use [the extension methods](crate::KbgpEguiResponseExt) on the egui widgets to add KBGP's
6//!   functionality.
7//! * Call [`ui.kbgp_clear_input`](crate::KbgpEguiUiCtxExt::kbgp_clear_input) when triggering a
8//!   state transition as a response to a click on an egui widget. To control the focus in the new
9//!   state, use [`kbgp_focus_label`](KbgpEguiResponseExt::kbgp_focus_label) (and
10//!   [`kbgp_set_focus_label`](KbgpEguiUiCtxExt::kbgp_set_focus_label)) - otherwise egui will pick
11//!   the widget to focus on (or elect to drop the focus entirely)
12//! * To set special actions, see [the example here](crate::KbgpNavCommand::user). To avoid having
13//!   to deal with both Bevy's input methods and KBGP's input, it's better to use these actions for
14//!   entering the pause menu from within the game.
15//! * Use [`kbgp_click_released`](KbgpEguiResponseExt::kbgp_click_released) instead of egui's
16//!   `clicked` to register the button presss only when the user releases the key/button. This is
17//!   useful for exiting menus, to avoid having the same key/button that was used to exit the menu
18//!   registered as actual game input.
19//!
20//! ```no_run
21//! use bevy_egui_kbgp::{egui, bevy_egui};
22//! use bevy::prelude::*;
23//! use bevy_egui::{EguiPrimaryContextPass, EguiContexts, EguiPlugin};
24//! use bevy_egui_kbgp::prelude::*;
25//!
26//! fn main() {
27//!     App::new()
28//!         .add_plugins(DefaultPlugins)
29//!         .add_plugins(EguiPlugin::default())
30//!         .add_plugins(KbgpPlugin)
31//!         .add_systems(EguiPrimaryContextPass, ui_system)
32//!         .run();
33//! }
34//!
35//! fn ui_system(
36//!     mut egui_context: EguiContexts,
37//!     keys: Res<ButtonInput<KeyCode>>,
38//! ) -> Result {
39//!     egui::CentralPanel::default().show(egui_context.ctx_mut()?, |ui| {
40//!         if ui
41//!             .button("Button")
42//!             .kbgp_initial_focus()
43//!             .kbgp_navigation()
44//!             .clicked()
45//!         {
46//!             // Button action
47//!         }
48//!
49//!         if let Some(input_selected_by_player) = ui
50//!             .button("Set Input")
51//!             .kbgp_navigation()
52//!             .kbgp_pending_input()
53//!         {
54//!             // Do something with the input
55//!         }
56//!     });
57//!     Ok(())
58//! }
59//! ```
60//!
61//! ## Creating Key-Setting UI
62//!
63//! Use functions like [`kbgp_pending_input`](crate::KbgpEguiResponseExt::kbgp_pending_input) to
64//! convert a regular button to a key-setting button. When the players presses that button, they'll
65//! be prompted to enter input from the keyboard, the mouse, or a gamepad. That input will be
66//! returned as a [`KbgpInput`].
67//!
68//! [`kbgp_pending_chord`](crate::KbgpEguiResponseExt::kbgp_pending_chord) is similar, but prompts
69//! the player to enter multiple keys instead of just one.
70//!
71//! Both functions have several variants that allow limiting the chords/keys accepted by that
72//! button.
73//!
74//! By default, mouse wheel input is disabled. The reason is that mouse wheel events are a pain to
75//! deal with, and most third party crates that ease input handling don't support them - so it's
76//! better not to let the player select input that the game is unable to deal with.
77
78pub use bevy_egui;
79pub use bevy_egui::egui;
80
81use bevy::platform::collections::{HashMap, HashSet};
82use bevy::prelude::*;
83use bevy_egui::input::EguiInputEvent;
84use bevy_egui::{EguiContext, EguiContextSettings, PrimaryEguiContext};
85
86use self::navigation::KbgpPrepareNavigation;
87pub use self::navigation::{KbgpNavActivation, KbgpNavBindings, KbgpNavCommand};
88use self::navigation::{KbgpNavigationState, PendingReleaseState};
89use self::pending_input::KbgpPendingInputState;
90pub use self::pending_input::{KbgpInputManualHandle, KbgpPreparePendingInput};
91
92mod navigation;
93mod pending_input;
94
95pub mod prelude {
96    pub use crate::kbgp_prepare;
97    pub use crate::KbgpEguiResponseExt;
98    pub use crate::KbgpEguiUiCtxExt;
99    pub use crate::KbgpInput;
100    pub use crate::KbgpInputSource;
101    pub use crate::KbgpNavActivation;
102    pub use crate::KbgpNavBindings;
103    pub use crate::KbgpNavCommand;
104    pub use crate::KbgpPlugin;
105    pub use crate::KbgpSettings;
106}
107
108/// Adds KBGP input handling system and [`KbgpSettings`].
109pub struct KbgpPlugin;
110
111impl Plugin for KbgpPlugin {
112    fn build(&self, app: &mut App) {
113        app.insert_resource(KbgpSettings::default());
114        app.add_systems(
115            Update,
116            kbgp_system_default_input, //.after(bevy_egui::EguiPreUpdateSet::BeginPass),
117        );
118    }
119}
120
121/// General configuration resource for KBGP.
122///
123/// Note: [`KbgpPlugin`] will add the default settings, so custom settings should either be added
124/// after the plugin or modified with a system. The default is to enable everything except the
125/// mouse wheel.
126#[derive(Resource)]
127pub struct KbgpSettings {
128    /// Whether or not egui's tab navigation should work
129    pub disable_default_navigation: bool,
130    /// Whether or not egui's Enter and Space should work. These keys can still be assigned with
131    /// [`bindings`](KbgpSettings::bindings):
132    ///
133    /// ```no_run
134    /// # use bevy::prelude::*;
135    /// # use bevy_egui_kbgp::prelude::*;
136    /// App::new()
137    ///     // ...
138    ///     .insert_resource(KbgpSettings {
139    ///         disable_default_activation: true,
140    ///         bindings: KbgpNavBindings::default()
141    ///             .with_key(KeyCode::Space, KbgpNavCommand::Click)
142    ///             .with_key(KeyCode::NumpadEnter, KbgpNavCommand::Click)
143    ///             .with_key(KeyCode::Enter, KbgpNavCommand::Click),
144    ///         ..Default::default()
145    ///     })
146    ///     // ...
147    /// # ;
148    /// ```
149    pub disable_default_activation: bool,
150    /// Whether or not to force that there is always an egui widget that has the focus.
151    pub prevent_loss_of_focus: bool,
152    /// Whether or not to transfer focus when the mouse moves into a widget.
153    ///
154    /// Only works for widgets marked with [`kbgp_navigation`](crate::KbgpEguiResponseExt::kbgp_navigation).
155    pub focus_on_mouse_movement: bool,
156    /// Whether or not keyboard input is accepted for navigation and for chords.
157    pub allow_keyboard: bool,
158    /// Whether or not mouse buttons are accepted for chords.
159    pub allow_mouse_buttons: bool,
160    /// Whether or not mouse wheel is accepted for chords. Defaults to `false`.
161    pub allow_mouse_wheel: bool,
162    /// Whether or not mouse wheel sideways scrolling is accepted for chords. Defaults to `false`.
163    pub allow_mouse_wheel_sideways: bool,
164    /// Whether or not gamepads input is accepted for navigation and for chords.
165    pub allow_gamepads: bool,
166    /// Input mapping for navigation.
167    pub bindings: KbgpNavBindings,
168}
169
170impl Default for KbgpSettings {
171    fn default() -> Self {
172        Self {
173            disable_default_navigation: false,
174            disable_default_activation: false,
175            prevent_loss_of_focus: false,
176            focus_on_mouse_movement: false,
177            allow_keyboard: true,
178            allow_mouse_buttons: true,
179            allow_mouse_wheel: false,
180            allow_mouse_wheel_sideways: false,
181            allow_gamepads: true,
182            bindings: Default::default(),
183        }
184    }
185}
186
187/// Object used to configure KBGP's behavior in [`kbgp_prepare`].
188pub enum KbgpPrepare<'a> {
189    Navigation(&'a mut KbgpPrepareNavigation),
190    PendingInput(&'a mut KbgpPreparePendingInput),
191}
192
193#[derive(Default)]
194struct Kbgp {
195    common: KbgpCommon,
196    state: KbgpState,
197}
198
199fn kbgp_get(egui_ctx: &egui::Context) -> std::sync::Arc<egui::mutex::Mutex<Kbgp>> {
200    egui_ctx.memory_mut(|memory| {
201        memory
202            .data
203            .get_temp_mut_or_default::<std::sync::Arc<egui::mutex::Mutex<Kbgp>>>(egui::Id::NULL)
204            .clone()
205    })
206}
207
208/// Must be called every frame, either manually or by using [`KbgpPlugin`].
209///
210/// Should be called between bevy_egui's input handling system and the system that generates the
211/// UI - so in the `CoreStage::PreUpdate` stage after the `EguiSystem::ProcessInput` label.
212///
213/// The `prepare_dlg` argument is a closure that accepts a [`KbgpPrepare`], and used to:
214///
215/// * Register the input from the keyboard and the gamepads.
216/// * Set preferences.
217///
218/// Typical usage:
219///
220/// ```no_run
221/// # use bevy_egui_kbgp::bevy_egui;
222/// # use bevy::prelude::*;
223/// # use bevy_egui::input::EguiInputEvent;
224/// # use bevy_egui::{EguiContext, PrimaryEguiContext};
225/// # use bevy_egui_kbgp::prelude::*;
226/// # use bevy_egui_kbgp::KbgpPrepare;
227/// fn custom_kbgp_system(
228///     mut egui_context_query: Query<
229///         (Entity, &mut EguiContext),
230///         With<PrimaryEguiContext>,
231///     >,
232///     mut egui_input_writer: MessageWriter<EguiInputEvent>,
233///     keys: Res<ButtonInput<KeyCode>>,
234///     gamepads: Query<(Entity, &Gamepad)>,
235///     mouse_buttons: Res<ButtonInput<MouseButton>>,
236///     settings: Res<KbgpSettings>,
237/// ) -> Result {
238///     let (egui_ctx_entity, mut egui_ctx) = egui_context_query.single_mut()?;
239///     kbgp_prepare(
240///         egui_ctx_entity,
241///         egui_ctx.get_mut(),
242///         &mut egui_input_writer,
243///         |prp| {
244///             match prp {
245///                 KbgpPrepare::Navigation(prp) => {
246///                     prp.navigate_keyboard_by_binding(&keys, &settings.bindings.keyboard, true);
247///                     for (_, gamepad) in gamepads.iter() {
248///                         prp.navigate_gamepad_by_binding(gamepad, &settings.bindings.gamepad_buttons);
249///                     }
250///                 }
251///                 KbgpPrepare::PendingInput(prp) => {
252///                     prp.accept_keyboard_input(&keys);
253///                     prp.accept_mouse_buttons_input(&mouse_buttons);
254///                     for (gamepad_entity, gamepad) in gamepads.iter() {
255///                         prp.accept_gamepad_input(gamepad_entity, gamepad);
256///                     }
257///                 }
258///             }
259///         },
260///     );
261///     Ok(())
262/// }
263/// ```
264pub fn kbgp_prepare(
265    egui_ctx_entity: Entity,
266    egui_ctx: &egui::Context,
267    egui_input_writer: &mut MessageWriter<EguiInputEvent>,
268    prepare_dlg: impl FnOnce(KbgpPrepare<'_>),
269) {
270    let kbgp = kbgp_get(egui_ctx);
271    let mut kbgp = kbgp.lock();
272    // Since Bevy is allow to reorder systems mid-run, there is a risk that the KBGP prepare system
273    // run twice between egui drawing systems. The stale counter allows up to two such invocations
274    // - after that it assumes the widget is no longer drawn.
275    kbgp.common.nodes.retain(|_, data| data.seen_this_frame);
276    for node_data in kbgp.common.nodes.values_mut() {
277        node_data.seen_this_frame = false;
278    }
279    let Kbgp { common, state } = &mut *kbgp;
280    match state {
281        KbgpState::Navigation(state) => {
282            state.prepare(
283                common,
284                egui_ctx_entity,
285                egui_ctx,
286                egui_input_writer,
287                |prp| prepare_dlg(KbgpPrepare::Navigation(prp)),
288            );
289            if let Some(focus_on) = state.focus_on.take() {
290                egui_ctx.memory_mut(|memory| memory.request_focus(focus_on));
291            }
292            state.focus_label = state.next_frame_focus_label.take();
293            if common.nodes.is_empty() && state.focus_label.is_none() {
294                state.focus_label = Some(Box::new(KbgpInitialFocusLabel));
295            }
296        }
297        KbgpState::PendingInput(state) => {
298            state.prepare(common, egui_ctx, |prp| {
299                prepare_dlg(KbgpPrepare::PendingInput(prp))
300            });
301            if common.nodes.is_empty() {
302                kbgp.state = KbgpState::Navigation(Default::default());
303            }
304        }
305    }
306}
307
308/// Cancel's any tab-based navigation egui did in its `BeginFrame`.
309pub fn kbgp_intercept_default_navigation(egui_ctx: &egui::Context) {
310    egui_ctx.memory_mut(|memory| {
311        if let Some(focus) = memory.focused() {
312            memory.set_focus_lock_filter(
313                focus,
314                egui::EventFilter {
315                    tab: true,
316                    horizontal_arrows: true,
317                    vertical_arrows: true,
318                    escape: true,
319                },
320            );
321        }
322    });
323}
324
325/// Make sure there is always an egui widget that has the focus.
326pub fn kbgp_prevent_loss_of_focus(egui_ctx: &egui::Context) {
327    let kbgp = kbgp_get(egui_ctx);
328    let mut kbgp = kbgp.lock();
329
330    match &mut kbgp.state {
331        KbgpState::PendingInput(_) => {}
332        KbgpState::Navigation(state) => {
333            let current_focus = egui_ctx.memory(|memory| memory.focused());
334            if let Some(current_focus) = current_focus {
335                state.last_focus = Some(current_focus);
336            } else if let Some(last_focus) = state.last_focus.take() {
337                egui_ctx.memory_mut(|memory| memory.request_focus(last_focus));
338            }
339        }
340    }
341}
342
343/// Transfer focus when the mouse moves into a widget.
344///
345/// Only works for widgets marked with [`kbgp_navigation`](crate::KbgpEguiResponseExt::kbgp_navigation).
346pub fn kbgp_focus_on_mouse_movement(egui_ctx: &egui::Context) {
347    let kbgp = kbgp_get(egui_ctx);
348    let mut kbgp = kbgp.lock();
349
350    let Kbgp { common, state } = &mut *kbgp;
351
352    match state {
353        KbgpState::PendingInput(_) => {}
354        KbgpState::Navigation(state) => {
355            let node_at_pos = egui_ctx.input(|input| {
356                input.pointer.interact_pos().and_then(|pos| {
357                    common.nodes.iter().find_map(|(node_id, node_data)| {
358                        node_data.rect.contains(pos).then_some(*node_id)
359                    })
360                })
361            });
362            if node_at_pos != state.mouse_was_last_on {
363                state.mouse_was_last_on = node_at_pos;
364                if let Some(node_at_pos) = node_at_pos {
365                    egui_ctx.memory_mut(|memory| memory.request_focus(node_at_pos));
366                }
367            }
368        }
369    }
370}
371
372/// System that operates KBGP with the default input scheme.
373///
374/// * Keyboard:
375///   * Arrow keys - navigation.
376///   * egui already uses Space and Enter for widget activation.
377/// * Gamepad:
378///   * DPad - navigation.
379///   * Left stick - navigation.
380///   * South face button (depends on model - usually X or A): widget activation.
381#[allow(clippy::too_many_arguments)]
382fn kbgp_system_default_input(
383    mut egui_context_query: Query<
384        (Entity, &mut EguiContext, &mut EguiContextSettings),
385        With<PrimaryEguiContext>,
386    >,
387    mut egui_input_writer: MessageWriter<EguiInputEvent>,
388    settings: Res<KbgpSettings>,
389    keys: Res<ButtonInput<KeyCode>>,
390    mouse_buttons: Res<ButtonInput<MouseButton>>,
391    mut mouse_wheel_events: MessageReader<bevy::input::mouse::MouseWheel>,
392    gamepads: Query<(Entity, &Gamepad)>,
393) -> Result {
394    let (egui_ctx_entity, mut egui_ctx, mut egui_ctx_settings) = egui_context_query.single_mut()?;
395    let egui_ctx = egui_ctx.get_mut();
396    if settings.disable_default_navigation {
397        kbgp_intercept_default_navigation(egui_ctx);
398    }
399    if settings.disable_default_activation {
400        egui_ctx_settings
401            .input_system_settings
402            .run_write_keyboard_input_messages_system = false;
403    }
404    if settings.prevent_loss_of_focus {
405        kbgp_prevent_loss_of_focus(egui_ctx);
406    }
407    if settings.focus_on_mouse_movement {
408        kbgp_focus_on_mouse_movement(egui_ctx);
409    }
410
411    kbgp_prepare(
412        egui_ctx_entity,
413        egui_ctx,
414        &mut egui_input_writer,
415        |prp| match prp {
416            KbgpPrepare::Navigation(prp) => {
417                if settings.allow_keyboard {
418                    prp.navigate_keyboard_by_binding(
419                        &keys,
420                        &settings.bindings.keyboard,
421                        !settings.disable_default_activation,
422                    );
423                }
424                if settings.allow_gamepads {
425                    for (_, gamepad) in gamepads.iter() {
426                        prp.navigate_gamepad_by_binding(
427                            gamepad,
428                            &settings.bindings.gamepad_buttons,
429                        );
430                    }
431                }
432            }
433            KbgpPrepare::PendingInput(prp) => {
434                if settings.allow_keyboard {
435                    prp.accept_keyboard_input(&keys);
436                }
437                if settings.allow_mouse_buttons {
438                    prp.accept_mouse_buttons_input(&mouse_buttons);
439                }
440                if settings.allow_mouse_wheel || settings.allow_mouse_wheel_sideways {
441                    for event in mouse_wheel_events.read() {
442                        prp.accept_mouse_wheel_event(
443                            event,
444                            settings.allow_mouse_wheel,
445                            settings.allow_mouse_wheel_sideways,
446                        );
447                    }
448                }
449                if settings.allow_gamepads {
450                    for (gamepad_entity, gamepad) in gamepads.iter() {
451                        prp.accept_gamepad_input(gamepad_entity, gamepad);
452                    }
453                }
454            }
455        },
456    );
457    Ok(())
458}
459
460#[derive(Default)]
461struct KbgpCommon {
462    nodes: HashMap<egui::Id, NodeData>,
463}
464
465enum KbgpState {
466    Navigation(KbgpNavigationState),
467    PendingInput(KbgpPendingInputState),
468}
469
470impl Default for KbgpState {
471    fn default() -> Self {
472        Self::Navigation(Default::default())
473    }
474}
475
476#[derive(Debug)]
477struct NodeData {
478    rect: egui::Rect,
479    seen_this_frame: bool,
480}
481
482#[derive(PartialEq)]
483struct KbgpInitialFocusLabel;
484
485/// Extensions for egui's `Response` to activate KBGP's functionality.
486///
487/// ```no_run
488/// # use bevy_egui_kbgp::egui;
489/// # use bevy::prelude::*;
490/// # use bevy_egui_kbgp::prelude::*;
491/// # let ui: egui::Ui = todo!();
492/// if ui
493///     .button("My Button")
494///     .kbgp_initial_focus() // focus on this button when starting the UI
495///     .kbgp_navigation() // navigate to and from this button with keyboard/gamepad
496///     .clicked()
497/// {
498///     // ...
499/// }
500/// ```
501pub trait KbgpEguiResponseExt: Sized {
502    /// Focus on this widget when [`kbgp_set_focus_label`](KbgpEguiUiCtxExt::kbgp_set_focus_label)
503    /// is called with the same label.
504    ///
505    /// This will only happen if `kbgp_set_focus_label` was called in the previous frame. A single
506    /// widget can be marked with multiple labels by calling `kbgp_focus_label` multiple times.
507    ///
508    /// ```no_run
509    /// # use bevy_egui_kbgp::egui;
510    /// # use bevy_egui_kbgp::prelude::*;
511    /// # let ui: egui::Ui = todo!();
512    /// #[derive(PartialEq)]
513    /// enum FocusLabel {
514    ///     Left,
515    ///     Right,
516    /// }
517    /// if ui
518    ///     .button("Focus >")
519    ///     .kbgp_navigation()
520    ///     .kbgp_focus_label(FocusLabel::Left)
521    ///     .clicked()
522    /// {
523    ///     ui.kbgp_set_focus_label(FocusLabel::Right);
524    /// }
525    /// if ui
526    ///     .button("< Focus")
527    ///     .kbgp_navigation()
528    ///     .kbgp_focus_label(FocusLabel::Right)
529    ///     .clicked()
530    /// {
531    ///     ui.kbgp_set_focus_label(FocusLabel::Left);
532    /// }
533    /// ```
534    fn kbgp_focus_label<T: 'static + PartialEq<T>>(self, label: T) -> Self;
535
536    /// When the UI is first created, focus on this widget.
537    ///
538    /// Note that if [`kbgp_set_focus_label`](KbgpEguiUiCtxExt::kbgp_set_focus_label) was called in
539    /// the previous frame the widget marked with
540    /// [`kbgp_focus_label`](KbgpEguiResponseExt::kbgp_focus_label) will receive focus instead. A
541    /// single widget can be marked with both `kbgp_focus_label` and `kbgp_initial_focus`.
542    fn kbgp_initial_focus(self) -> Self {
543        self.kbgp_focus_label(KbgpInitialFocusLabel)
544    }
545
546    /// Navigate to and from this widget.
547    fn kbgp_navigation(self) -> Self;
548
549    /// Check if the player pressed a user action button while focused on this widget.
550    ///
551    /// ```no_run
552    /// # use bevy_egui_kbgp::egui;
553    /// # use bevy_egui_kbgp::prelude::*;
554    /// # let ui: egui::Ui = todo!();
555    /// # #[derive(Clone)]
556    /// # enum MyUserAction { Action1, Action2 }
557    /// match ui.button("Button").kbgp_user_action() {
558    ///     None => {}
559    ///     Some(MyUserAction::Action1) => println!("User action 1"),
560    ///     Some(MyUserAction::Action2) => println!("User action 2"),
561    /// }
562    /// ```
563    fn kbgp_user_action<T: 'static + Clone>(&self) -> Option<T>;
564
565    /// Check if the player activated this widget or pressed a user action button while focused on
566    /// it.
567    ///
568    /// ```no_run
569    /// # use bevy_egui_kbgp::egui;
570    /// # use bevy_egui_kbgp::prelude::*;
571    /// # let ui: egui::Ui = todo!();
572    /// # #[derive(Clone)]
573    /// # enum SpecialAction { Special1, Special2 }
574    /// match ui.button("Button").kbgp_activated() {
575    ///     KbgpNavActivation::Clicked => println!("Regular activateion"),
576    ///     KbgpNavActivation::ClickedSecondary | KbgpNavActivation::User(SpecialAction::Special1) => println!("Special activateion 1"),
577    ///     KbgpNavActivation::ClickedMiddle | KbgpNavActivation::User(SpecialAction::Special2) => println!("Special activateion 2"),
578    ///     _ => {}
579    /// }
580    /// ```
581    fn kbgp_activated<T: 'static + Clone>(&self) -> KbgpNavActivation<T>;
582
583    /// Similar to [`kbgp_activated`](Self::kbgp_activated), but only returns a
584    /// non-[`KbgpNavActivation::None`] value when the key/button is released.
585    fn kbgp_activate_released<T: 'static + Clone>(&self) -> KbgpNavActivation<T>;
586
587    /// Similar to egui's `clicked`, but only returns `true` when the key/button is released.
588    fn kbgp_click_released(&self) -> bool;
589
590    /// Similar to [`kbgp_user_action`](Self::kbgp_user_action), but only returns `Some` when the
591    /// key/button is released.
592    fn kbgp_user_action_released<T: 'static + Clone>(&self) -> Option<T>;
593
594    /// Accept a single key/button input from this widget.
595    ///
596    /// Must be called on widgets that had
597    /// [`kbgp_navigation`](crate::KbgpEguiResponseExt::kbgp_navigation) called on them.
598    ///
599    /// ```no_run
600    /// use bevy::prelude::*;
601    /// use bevy_egui::{EguiPrimaryContextPass, EguiContexts, EguiPlugin};
602    /// use bevy_egui_kbgp::{egui, bevy_egui};
603    /// use bevy_egui_kbgp::prelude::*;
604    /// fn main() {
605    ///     App::new()
606    ///         .add_plugins(DefaultPlugins)
607    ///         .add_plugins(EguiPlugin::default())
608    ///         .add_plugins(KbgpPlugin)
609    ///         .add_systems(EguiPrimaryContextPass, ui_system)
610    ///         .insert_resource(JumpInput(KbgpInput::Keyboard(KeyCode::Space)))
611    ///         .run();
612    /// }
613    ///
614    /// #[derive(Resource)]
615    /// struct JumpInput(KbgpInput);
616    ///
617    /// fn ui_system(
618    ///     mut egui_context: EguiContexts,
619    ///     mut jump_input: ResMut<JumpInput>,
620    /// ) -> Result {
621    ///     egui::CentralPanel::default().show(egui_context.ctx_mut()?, |ui| {
622    ///         ui.horizontal(|ui| {
623    ///             ui.label("Set button for jumping");
624    ///             if let Some(new_jump_input) = ui.button(format!("{}", jump_input.0))
625    ///                 .kbgp_navigation()
626    ///                 .kbgp_pending_input()
627    ///             {
628    ///                 jump_input.0 = new_jump_input;
629    ///             }
630    ///         });
631    ///     });
632    ///     Ok(())
633    /// }
634    fn kbgp_pending_input(&self) -> Option<KbgpInput>;
635
636    /// Accept a single key/button input from this widget, limited to a specific input source.
637    fn kbgp_pending_input_of_source(&self, source: KbgpInputSource) -> Option<KbgpInput>;
638
639    /// Accept a single key/button input from this widget, with the ability to filter which inputs
640    /// to accept.
641    fn kbgp_pending_input_vetted(&self, pred: impl FnMut(KbgpInput) -> bool) -> Option<KbgpInput>;
642
643    /// Accept a chord of key/button inputs from this widget.
644    ///
645    /// Must be called on widgets that had
646    /// [`kbgp_navigation`](crate::KbgpEguiResponseExt::kbgp_navigation) called on them.
647    ///
648    /// ```no_run
649    /// use bevy::prelude::*;
650    /// use bevy_egui::{EguiPrimaryContextPass, EguiContexts, EguiPlugin};
651    /// use bevy_egui_kbgp::{egui, bevy_egui};
652    /// use bevy_egui_kbgp::prelude::*;
653    /// fn main() {
654    ///     App::new()
655    ///         .add_plugins(DefaultPlugins)
656    ///         .add_plugins(EguiPlugin::default())
657    ///         .add_plugins(KbgpPlugin)
658    ///         .add_systems(EguiPrimaryContextPass, ui_system)
659    ///         .insert_resource(JumpChord(vec![KbgpInput::Keyboard(KeyCode::Space)]))
660    ///         .run();
661    /// }
662    ///
663    /// #[derive(Resource)]
664    /// struct JumpChord(Vec<KbgpInput>);
665    ///
666    /// fn ui_system(
667    ///     mut egui_context: EguiContexts,
668    ///     mut jump_chord: ResMut<JumpChord>,
669    /// ) -> Result {
670    ///     egui::CentralPanel::default().show(egui_context.ctx_mut()?, |ui| {
671    ///         ui.horizontal(|ui| {
672    ///             ui.label("Set chord of buttons for jumping");
673    ///             if let Some(new_jump_chord) = ui
674    ///                 .button(KbgpInput::format_chord(jump_chord.0.iter().cloned()))
675    ///                 .kbgp_navigation()
676    ///                 .kbgp_pending_chord()
677    ///             {
678    ///                 jump_chord.0 = new_jump_chord.into_iter().collect();
679    ///             }
680    ///         });
681    ///     });
682    ///     Ok(())
683    /// }
684    fn kbgp_pending_chord(&self) -> Option<HashSet<KbgpInput>>;
685
686    /// Accept a chord of key/button inputs from this widget, limited to a specific input source.
687    fn kbgp_pending_chord_of_source(&self, source: KbgpInputSource) -> Option<HashSet<KbgpInput>>;
688
689    /// Accept a chord of key/button inputs from this widget, where all inputs are from the same
690    /// source.
691    ///
692    /// "Same source" means either all the inputs are from the same gamepad, or all the inputs are
693    /// from the keyboard and the mouse.
694    fn kbgp_pending_chord_same_source(&self) -> Option<HashSet<KbgpInput>>;
695
696    /// Accept a chord of key/button inputs from this widget, with the ability to filter which
697    /// inputs to accept.
698    ///
699    /// The predicate accepts as a first argument the inputs that already participate in the chord,
700    /// to allow vetting the new input based on them.
701    fn kbgp_pending_chord_vetted(
702        &self,
703        pred: impl FnMut(&HashSet<KbgpInput>, KbgpInput) -> bool,
704    ) -> Option<HashSet<KbgpInput>>;
705
706    /// Helper for manually implementing custom methods for input-setting
707    ///
708    /// Inside the delegate, one would usually:
709    /// * Call
710    ///   [`process_new_input`](crate::pending_input::KbgpInputManualHandle::process_new_input) to
711    ///   decide which new input to register.
712    /// * Call
713    ///   [`show_current_chord`](crate::pending_input::KbgpInputManualHandle::show_current_chord)
714    ///   to show the tooltip, or generate some other visual cue.
715    /// * Return `None` if the player did not finish entering the input.
716    fn kbgp_pending_input_manual<T>(
717        &self,
718        dlg: impl FnOnce(&Self, KbgpInputManualHandle) -> Option<T>,
719    ) -> Option<T>;
720}
721
722impl KbgpEguiResponseExt for egui::Response {
723    fn kbgp_focus_label<T: 'static + PartialEq<T>>(self, label: T) -> Self {
724        let kbgp = kbgp_get(&self.ctx);
725        let mut kbgp = kbgp.lock();
726        match &mut kbgp.state {
727            KbgpState::Navigation(state) => {
728                if let Some(focus_label) = &state.focus_label {
729                    if let Some(focus_label) = focus_label.downcast_ref::<T>() {
730                        if focus_label == &label {
731                            state.focus_label = None;
732                            state.focus_on = Some(self.id);
733                        }
734                    }
735                }
736            }
737            KbgpState::PendingInput(_) => {}
738        }
739        self
740    }
741
742    //fn kbgp_initial_focus(self) -> Self {
743    //let kbgp = kbgp_get(&self.ctx);
744    //let mut kbgp = kbgp.lock();
745    //match &mut kbgp.state {
746    //KbgpState::Inactive(state) => {
747    //state.focus_on = Some(self.id);
748    //}
749    //KbgpState::Navigation(_) => {}
750    //KbgpState::PendingInput(_) => {}
751    //}
752    //self
753    //}
754
755    fn kbgp_navigation(self) -> Self {
756        let kbgp = kbgp_get(&self.ctx);
757        let mut kbgp = kbgp.lock();
758        kbgp.common.nodes.insert(
759            self.id,
760            NodeData {
761                rect: self.rect,
762                seen_this_frame: true,
763            },
764        );
765        self
766    }
767
768    fn kbgp_user_action<T: 'static + Clone>(&self) -> Option<T> {
769        if self.has_focus() {
770            self.ctx.kbgp_user_action()
771        } else {
772            None
773        }
774    }
775
776    fn kbgp_activated<T: 'static + Clone>(&self) -> KbgpNavActivation<T> {
777        if self.clicked() {
778            KbgpNavActivation::Clicked
779        } else if self.secondary_clicked() {
780            KbgpNavActivation::ClickedSecondary
781        } else if self.middle_clicked() {
782            KbgpNavActivation::ClickedMiddle
783        } else if let Some(action) = self.kbgp_user_action() {
784            KbgpNavActivation::User(action)
785        } else {
786            KbgpNavActivation::None
787        }
788    }
789
790    fn kbgp_activate_released<T: 'static + Clone>(&self) -> KbgpNavActivation<T> {
791        if self.kbgp_click_released() {
792            KbgpNavActivation::Clicked
793        } else if self.secondary_clicked() {
794            KbgpNavActivation::ClickedSecondary
795        } else if self.middle_clicked() {
796            KbgpNavActivation::ClickedMiddle
797        } else if let Some(action) = self.kbgp_user_action_released() {
798            KbgpNavActivation::User(action)
799        } else {
800            KbgpNavActivation::None
801        }
802    }
803
804    fn kbgp_click_released(&self) -> bool {
805        let kbgp = kbgp_get(&self.ctx);
806        let kbgp = kbgp.lock();
807        if let KbgpState::Navigation(state) = &kbgp.state {
808            if let navigation::PendingReleaseState::NodeHoldReleased {
809                id,
810                user_action: None,
811            } = &state.pending_release_state
812            {
813                return *id == self.id;
814            }
815        }
816        // Otherwise it would not accept mouse clicks
817        if self.hovered() && self.ctx.input(|input| input.pointer.primary_released()) {
818            return true;
819        }
820        false
821    }
822
823    fn kbgp_user_action_released<T: 'static + Clone>(&self) -> Option<T> {
824        if self.has_focus() {
825            let kbgp = kbgp_get(&self.ctx);
826            let kbgp = kbgp.lock();
827            if let KbgpState::Navigation(state) = &kbgp.state {
828                if let navigation::PendingReleaseState::NodeHoldReleased {
829                    id,
830                    user_action: Some(user_action),
831                } = &state.pending_release_state
832                {
833                    if *id == self.id {
834                        return user_action.downcast_ref().cloned();
835                    }
836                }
837            }
838        }
839        None
840    }
841
842    fn kbgp_pending_input_manual<T>(
843        &self,
844        dlg: impl FnOnce(&Self, KbgpInputManualHandle) -> Option<T>,
845    ) -> Option<T> {
846        let kbgp = kbgp_get(&self.ctx);
847        let mut kbgp = kbgp.lock();
848        match &mut kbgp.state {
849            KbgpState::Navigation(_) => {
850                if self.clicked() {
851                    kbgp.state = KbgpState::PendingInput(KbgpPendingInputState::new(self.id));
852                }
853                None
854            }
855            KbgpState::PendingInput(state) => {
856                if state.acceptor_id != self.id {
857                    return None;
858                }
859                self.request_focus();
860                self.ctx.memory_mut(|memory| {
861                    memory.set_focus_lock_filter(
862                        self.id,
863                        egui::EventFilter {
864                            tab: true,
865                            horizontal_arrows: true,
866                            vertical_arrows: true,
867                            escape: true,
868                        },
869                    )
870                });
871                let handle = KbgpInputManualHandle { state };
872                let result = dlg(self, handle);
873                if result.is_some() {
874                    kbgp.state = KbgpState::Navigation(KbgpNavigationState::default());
875                }
876                result
877            }
878        }
879    }
880
881    fn kbgp_pending_input(&self) -> Option<KbgpInput> {
882        self.kbgp_pending_input_vetted(|_| true)
883    }
884
885    fn kbgp_pending_input_of_source(&self, source: KbgpInputSource) -> Option<KbgpInput> {
886        self.kbgp_pending_input_vetted(|input| input.get_source() == source)
887    }
888
889    fn kbgp_pending_input_vetted(
890        &self,
891        mut pred: impl FnMut(KbgpInput) -> bool,
892    ) -> Option<KbgpInput> {
893        self.kbgp_pending_input_manual(|response, mut hnd| {
894            hnd.process_new_input(|hnd, input| hnd.received_input().is_empty() && pred(input));
895            hnd.show_current_chord(response);
896            if hnd
897                .input_this_frame()
898                .any(|inp| hnd.received_input().contains(&inp))
899            {
900                None
901            } else {
902                let mut it = hnd.received_input().iter();
903                let single_input = it.next();
904                assert!(
905                    it.next().is_none(),
906                    "More than one input in chord, but limit is 1"
907                );
908                // This will not be empty and we'll return a value if and only if there was some
909                // input in received_input.
910                single_input.cloned()
911            }
912        })
913    }
914
915    fn kbgp_pending_chord(&self) -> Option<HashSet<KbgpInput>> {
916        self.kbgp_pending_chord_vetted(|_, _| true)
917    }
918
919    fn kbgp_pending_chord_of_source(&self, source: KbgpInputSource) -> Option<HashSet<KbgpInput>> {
920        self.kbgp_pending_chord_vetted(|_, input| input.get_source() == source)
921    }
922
923    fn kbgp_pending_chord_same_source(&self) -> Option<HashSet<KbgpInput>> {
924        self.kbgp_pending_chord_vetted(|existing, input| {
925            if let Some(existing_input) = existing.iter().next() {
926                input.get_source() == existing_input.get_source()
927            } else {
928                true
929            }
930        })
931    }
932
933    fn kbgp_pending_chord_vetted(
934        &self,
935        mut pred: impl FnMut(&HashSet<KbgpInput>, KbgpInput) -> bool,
936    ) -> Option<HashSet<KbgpInput>> {
937        self.kbgp_pending_input_manual(|response, mut hnd| {
938            hnd.process_new_input(|hnd, input| pred(hnd.received_input(), input));
939            hnd.show_current_chord(response);
940            if hnd.input_this_frame().any(|_| true) || hnd.received_input().is_empty() {
941                None
942            } else {
943                Some(hnd.received_input().clone())
944            }
945        })
946    }
947}
948
949/// Input from the keyboard or from a gamepad.
950#[derive(Hash, PartialEq, Eq, Debug, Clone)]
951pub enum KbgpInput {
952    Keyboard(KeyCode),
953    MouseButton(MouseButton),
954    MouseWheelUp,
955    MouseWheelDown,
956    MouseWheelLeft,
957    MouseWheelRight,
958    GamepadAxisPositive(Entity, GamepadAxis),
959    GamepadAxisNegative(Entity, GamepadAxis),
960    GamepadButton(Entity, GamepadButton),
961}
962
963impl core::fmt::Display for KbgpInput {
964    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
965        match self {
966            KbgpInput::Keyboard(key) => write!(f, "{key:?}")?,
967            KbgpInput::MouseButton(MouseButton::Other(button)) => {
968                write!(f, "MouseButton{button:?}")?
969            }
970            KbgpInput::MouseButton(button) => write!(f, "Mouse{button:?}")?,
971            KbgpInput::MouseWheelUp => write!(f, "MouseScrollUp")?,
972            KbgpInput::MouseWheelDown => write!(f, "MouseScrollDown")?,
973            KbgpInput::MouseWheelLeft => write!(f, "MouseScrollLeft")?,
974            KbgpInput::MouseWheelRight => write!(f, "MouseScrollRight")?,
975            KbgpInput::GamepadButton(entity, gamepad_button) => {
976                write!(f, "[{entity}]{gamepad_button:?}")?
977            }
978            KbgpInput::GamepadAxisPositive(entity, gamepad_axis) => {
979                write!(f, "[{entity}]{gamepad_axis:?}")?
980            }
981            KbgpInput::GamepadAxisNegative(entity, gamepad_axis) => {
982                write!(f, "[{entity}]-{gamepad_axis:?}")?
983            }
984        }
985        Ok(())
986    }
987}
988
989impl KbgpInput {
990    /// Create a string that describes a chord of multiple inputs.
991    pub fn format_chord(chord: impl Iterator<Item = Self>) -> String {
992        let mut chord_text = String::new();
993        for input in chord {
994            use std::fmt::Write;
995            if !chord_text.is_empty() {
996                write!(&mut chord_text, " & ").unwrap();
997            }
998            write!(&mut chord_text, "{input}").unwrap();
999        }
1000        chord_text
1001    }
1002
1003    /// Return the source responsible for this input.
1004    pub fn get_source(&self) -> KbgpInputSource {
1005        match self {
1006            KbgpInput::Keyboard(_) => KbgpInputSource::KeyboardAndMouse,
1007            KbgpInput::MouseButton(_) => KbgpInputSource::KeyboardAndMouse,
1008            KbgpInput::MouseWheelUp => KbgpInputSource::KeyboardAndMouse,
1009            KbgpInput::MouseWheelDown => KbgpInputSource::KeyboardAndMouse,
1010            KbgpInput::MouseWheelLeft => KbgpInputSource::KeyboardAndMouse,
1011            KbgpInput::MouseWheelRight => KbgpInputSource::KeyboardAndMouse,
1012            KbgpInput::GamepadAxisPositive(entity, _) => KbgpInputSource::Gamepad(*entity),
1013            KbgpInput::GamepadAxisNegative(entity, _) => KbgpInputSource::Gamepad(*entity),
1014            KbgpInput::GamepadButton(entity, _) => KbgpInputSource::Gamepad(*entity),
1015        }
1016    }
1017}
1018
1019/// Input from the keyboard or from a gamepad.
1020#[derive(Hash, PartialEq, Eq, Debug, Clone, Copy)]
1021pub enum KbgpInputSource {
1022    KeyboardAndMouse,
1023    Gamepad(Entity),
1024}
1025
1026/// A source of input for chords
1027impl core::fmt::Display for KbgpInputSource {
1028    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1029        match self {
1030            KbgpInputSource::KeyboardAndMouse => write!(f, "Keyboard&Mouse"),
1031            KbgpInputSource::Gamepad(entity) => write!(f, "Gamepad {entity}"),
1032        }
1033    }
1034}
1035
1036impl KbgpInputSource {
1037    /// The gamepad of the source, of `None` if the source is keyboard or mouse.
1038    pub fn gamepad(&self) -> Option<Entity> {
1039        match self {
1040            KbgpInputSource::KeyboardAndMouse => None,
1041            KbgpInputSource::Gamepad(entity) => Some(*entity),
1042        }
1043    }
1044}
1045
1046/// Extensions for egui's `UI` and Context to activate KBGP's functionality.
1047pub trait KbgpEguiUiCtxExt {
1048    /// Needs to be called when triggering state transition from egui.
1049    ///
1050    /// Otherwise, the same player input that triggered the transition will be applied again to the
1051    /// GUI in the new state.
1052    fn kbgp_clear_input(&self);
1053
1054    /// Focus on the widget that called [`kbgp_focus_label`](KbgpEguiResponseExt::kbgp_focus_label)
1055    /// with the same label.
1056    ///
1057    /// This will only happen on the next frame.
1058    fn kbgp_set_focus_label<T: 'static + Send + Sync>(&self, label: T);
1059
1060    /// Check if the player pressed a user action button.
1061    ///
1062    /// Note that if the focus is on a widget that handles a user action, it will be reported both
1063    /// by that widget's [`kbgp_user_action`](crate::KbgpEguiResponseExt::kbgp_user_action) or
1064    /// [`kbgp_activated`](crate::KbgpEguiResponseExt::kbgp_activated) and by this method.
1065    ///
1066    /// ```no_run
1067    /// # use bevy_egui_kbgp::egui;
1068    /// # use bevy_egui_kbgp::prelude::*;
1069    /// # let ui: egui::Ui = todo!();
1070    /// # #[derive(Clone)]
1071    /// # enum MyUserAction { Action1, Action2 }
1072    /// match ui.kbgp_user_action() {
1073    ///     None => {}
1074    ///     Some(MyUserAction::Action1) => println!("User action 1"),
1075    ///     Some(MyUserAction::Action2) => println!("User action 2"),
1076    /// }
1077    /// ```
1078    fn kbgp_user_action<T: 'static + Clone>(&self) -> Option<T>;
1079
1080    /// Similar to [`kbgp_user_action`](Self::kbgp_user_action), but only returns `Some` when the
1081    /// key/button is released.
1082    fn kbgp_user_action_released<T: 'static + Clone>(&self) -> Option<T>;
1083}
1084
1085impl KbgpEguiUiCtxExt for egui::Ui {
1086    fn kbgp_clear_input(&self) {
1087        self.ctx().kbgp_clear_input()
1088    }
1089
1090    fn kbgp_set_focus_label<T: 'static + Send + Sync>(&self, label: T) {
1091        self.ctx().kbgp_set_focus_label(label);
1092    }
1093
1094    fn kbgp_user_action<T: 'static + Clone>(&self) -> Option<T> {
1095        self.ctx().kbgp_user_action()
1096    }
1097
1098    fn kbgp_user_action_released<T: 'static + Clone>(&self) -> Option<T> {
1099        self.ctx().kbgp_user_action_released()
1100    }
1101}
1102
1103impl KbgpEguiUiCtxExt for egui::Context {
1104    fn kbgp_clear_input(&self) {
1105        let kbgp = kbgp_get(self);
1106        let mut kbgp = kbgp.lock();
1107        match &mut kbgp.state {
1108            KbgpState::PendingInput(_) => {}
1109            KbgpState::Navigation(state) => {
1110                state.user_action = None;
1111                state.pending_release_state = PendingReleaseState::Invalidated {
1112                    cooldown_frame: true,
1113                };
1114            }
1115        }
1116
1117        self.input_mut(|input| {
1118            input.pointer = Default::default();
1119            #[allow(clippy::match_like_matches_macro)]
1120            input.events.retain(|event| match event {
1121                egui::Event::Key {
1122                    key: egui::Key::Space | egui::Key::Enter,
1123                    physical_key: _,
1124                    pressed: true,
1125                    modifiers: _,
1126                    repeat: _,
1127                } => false,
1128                egui::Event::PointerButton {
1129                    pos: _,
1130                    button: egui::PointerButton::Primary,
1131                    pressed: true,
1132                    modifiers: _,
1133                } => false,
1134                _ => true,
1135            });
1136        });
1137    }
1138
1139    fn kbgp_set_focus_label<T: 'static + Send + Sync>(&self, label: T) {
1140        let kbgp = kbgp_get(self);
1141        let mut kbgp = kbgp.lock();
1142        match &mut kbgp.state {
1143            KbgpState::PendingInput(_) => {}
1144            KbgpState::Navigation(state) => {
1145                state.next_frame_focus_label = Some(Box::new(label));
1146            }
1147        }
1148    }
1149
1150    fn kbgp_user_action<T: 'static + Clone>(&self) -> Option<T> {
1151        let kbgp = kbgp_get(self);
1152        let kbgp = kbgp.lock();
1153        match &kbgp.state {
1154            KbgpState::PendingInput(_) => None,
1155            KbgpState::Navigation(state) => state.user_action.as_ref()?.downcast_ref().cloned(),
1156        }
1157    }
1158
1159    fn kbgp_user_action_released<T: 'static + Clone>(&self) -> Option<T> {
1160        let kbgp = kbgp_get(self);
1161        let kbgp = kbgp.lock();
1162        match &kbgp.state {
1163            KbgpState::PendingInput(_) => None,
1164            KbgpState::Navigation(state) => match &state.pending_release_state {
1165                navigation::PendingReleaseState::Idle => None,
1166                navigation::PendingReleaseState::NodeHeld { .. } => None,
1167                navigation::PendingReleaseState::NodeHoldReleased { id: _, user_action } => {
1168                    user_action.as_ref()?.downcast_ref().cloned()
1169                }
1170                navigation::PendingReleaseState::GloballyHeld { .. } => None,
1171                navigation::PendingReleaseState::GlobalHoldReleased { user_action } => {
1172                    user_action.downcast_ref().cloned()
1173                }
1174                navigation::PendingReleaseState::Invalidated { .. } => None,
1175            },
1176        }
1177    }
1178}