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}