pub use bevy_egui;
pub use bevy_egui::egui;
use bevy::prelude::*;
use bevy::utils::{HashMap, HashSet};
use bevy_egui::EguiContext;
use self::navigation::KbgpNavigationState;
use self::navigation::KbgpPrepareNavigation;
pub use self::navigation::{KbgpNavActivation, KbgpNavBindings, KbgpNavCommand};
use self::pending_input::KbgpPendingInputState;
pub use self::pending_input::{KbgpInputManualHandle, KbgpPreparePendingInput};
mod navigation;
mod pending_input;
pub mod prelude {
pub use crate::kbgp_prepare;
pub use crate::KbgpEguiResponseExt;
pub use crate::KbgpEguiUiCtxExt;
pub use crate::KbgpInput;
pub use crate::KbgpInputSource;
pub use crate::KbgpNavActivation;
pub use crate::KbgpNavBindings;
pub use crate::KbgpNavCommand;
pub use crate::KbgpPlugin;
pub use crate::KbgpSettings;
}
pub struct KbgpPlugin;
impl Plugin for KbgpPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(KbgpSettings::default());
app.add_system_to_stage(
CoreStage::PreUpdate,
kbgp_system_default_input.after(bevy_egui::EguiSystem::BeginFrame),
);
}
}
#[derive(Resource)]
pub struct KbgpSettings {
pub disable_default_navigation: bool,
pub disable_default_activation: bool,
pub prevent_loss_of_focus: bool,
pub focus_on_mouse_movement: bool,
pub allow_keyboard: bool,
pub allow_mouse_buttons: bool,
pub allow_mouse_wheel: bool,
pub allow_mouse_wheel_sideways: bool,
pub allow_gamepads: bool,
pub bindings: KbgpNavBindings,
}
impl Default for KbgpSettings {
fn default() -> Self {
Self {
disable_default_navigation: false,
disable_default_activation: false,
prevent_loss_of_focus: false,
focus_on_mouse_movement: false,
allow_keyboard: true,
allow_mouse_buttons: true,
allow_mouse_wheel: false,
allow_mouse_wheel_sideways: false,
allow_gamepads: true,
bindings: Default::default(),
}
}
}
pub enum KbgpPrepare<'a> {
Navigation(&'a mut KbgpPrepareNavigation),
PendingInput(&'a mut KbgpPreparePendingInput),
}
#[derive(Default)]
struct Kbgp {
common: KbgpCommon,
state: KbgpState,
}
fn kbgp_get(egui_ctx: &egui::Context) -> std::sync::Arc<egui::mutex::Mutex<Kbgp>> {
egui_ctx
.memory()
.data
.get_temp_mut_or_default::<std::sync::Arc<egui::mutex::Mutex<Kbgp>>>(egui::Id::null())
.clone()
}
pub fn kbgp_prepare(egui_ctx: &egui::Context, prepare_dlg: impl FnOnce(KbgpPrepare<'_>)) {
let kbgp = kbgp_get(egui_ctx);
let mut kbgp = kbgp.lock();
kbgp.common.nodes.retain(|_, data| data.seen_this_frame);
for node_data in kbgp.common.nodes.values_mut() {
node_data.seen_this_frame = false;
}
let Kbgp { common, state } = &mut *kbgp;
match state {
KbgpState::Navigation(state) => {
state.prepare(common, egui_ctx, |prp| {
prepare_dlg(KbgpPrepare::Navigation(prp))
});
if let Some(focus_on) = state.focus_on.take() {
egui_ctx.memory().request_focus(focus_on);
}
state.focus_label = state.next_frame_focus_label.take();
if common.nodes.is_empty() && state.focus_label.is_none() {
state.focus_label = Some(Box::new(KbgpInitialFocusLabel));
}
}
KbgpState::PendingInput(state) => {
state.prepare(common, egui_ctx, |prp| {
prepare_dlg(KbgpPrepare::PendingInput(prp))
});
if common.nodes.is_empty() {
kbgp.state = KbgpState::Navigation(Default::default());
}
}
}
}
pub fn kbgp_intercept_default_navigation(egui_ctx: &egui::Context) {
let mut egui_memory = egui_ctx.memory();
if let Some(focus) = egui_memory.focus() {
egui_memory.lock_focus(focus, true);
}
}
pub fn kbgp_intercept_default_activation(egui_ctx: &egui::Context) {
egui_ctx.input_mut().events.retain(|evt| match evt {
egui::Event::Key {
key,
pressed: true,
modifiers: _,
} => !matches!(key, egui::Key::Enter | egui::Key::Space),
_ => true,
});
}
pub fn kbgp_prevent_loss_of_focus(egui_ctx: &egui::Context) {
let kbgp = kbgp_get(egui_ctx);
let mut kbgp = kbgp.lock();
match &mut kbgp.state {
KbgpState::PendingInput(_) => {}
KbgpState::Navigation(state) => {
let current_focus = egui_ctx.memory().focus();
if let Some(current_focus) = current_focus {
state.last_focus = Some(current_focus);
} else if let Some(last_focus) = state.last_focus.take() {
egui_ctx.memory().request_focus(last_focus);
}
}
}
}
pub fn kbgp_focus_on_mouse_movement(egui_ctx: &egui::Context) {
let kbgp = kbgp_get(egui_ctx);
let mut kbgp = kbgp.lock();
let Kbgp { common, state } = &mut *kbgp;
match state {
KbgpState::PendingInput(_) => {}
KbgpState::Navigation(state) => {
let node_at_pos = egui_ctx.input().pointer.interact_pos().and_then(|pos| {
common.nodes.iter().find_map(|(node_id, node_data)| {
node_data.rect.contains(pos).then_some(*node_id)
})
});
if node_at_pos != state.mouse_was_last_on {
state.mouse_was_last_on = node_at_pos;
if let Some(node_at_pos) = node_at_pos {
egui_ctx.memory().request_focus(node_at_pos);
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
fn kbgp_system_default_input(
mut egui_context: ResMut<EguiContext>,
settings: Res<KbgpSettings>,
keys: Res<Input<KeyCode>>,
mouse_buttons: Res<Input<MouseButton>>,
mut mouse_wheel_events: EventReader<bevy::input::mouse::MouseWheel>,
gamepads: Res<Gamepads>,
gamepad_axes: Res<Axis<GamepadAxis>>,
gamepad_buttons: Res<Input<GamepadButton>>,
) {
let egui_ctx = egui_context.ctx_mut();
if settings.disable_default_navigation {
kbgp_intercept_default_navigation(egui_ctx);
}
if settings.disable_default_activation {
kbgp_intercept_default_activation(egui_ctx);
}
if settings.prevent_loss_of_focus {
kbgp_prevent_loss_of_focus(egui_ctx);
}
if settings.focus_on_mouse_movement {
kbgp_focus_on_mouse_movement(egui_ctx);
}
kbgp_prepare(egui_ctx, |prp| match prp {
KbgpPrepare::Navigation(prp) => {
if settings.allow_keyboard {
prp.navigate_keyboard_by_binding(&keys, &settings.bindings.keyboard);
}
if settings.allow_gamepads {
prp.navigate_gamepad_by_binding(
&gamepads,
&gamepad_axes,
&gamepad_buttons,
&settings.bindings.gamepad_buttons,
);
}
}
KbgpPrepare::PendingInput(prp) => {
if settings.allow_keyboard {
prp.accept_keyboard_input(&keys);
}
if settings.allow_mouse_buttons {
prp.accept_mouse_buttons_input(&mouse_buttons);
}
if settings.allow_mouse_wheel || settings.allow_mouse_wheel_sideways {
for event in mouse_wheel_events.iter() {
prp.accept_mouse_wheel_event(
event,
settings.allow_mouse_wheel,
settings.allow_mouse_wheel_sideways,
);
}
}
if settings.allow_gamepads {
prp.accept_gamepad_input(&gamepads, &gamepad_axes, &gamepad_buttons);
}
}
});
}
#[derive(Default)]
struct KbgpCommon {
nodes: HashMap<egui::Id, NodeData>,
}
enum KbgpState {
Navigation(KbgpNavigationState),
PendingInput(KbgpPendingInputState),
}
impl Default for KbgpState {
fn default() -> Self {
Self::Navigation(Default::default())
}
}
#[derive(Debug)]
struct NodeData {
rect: egui::Rect,
seen_this_frame: bool,
}
#[derive(PartialEq)]
struct KbgpInitialFocusLabel;
pub trait KbgpEguiResponseExt: Sized {
fn kbgp_focus_label<T: 'static + PartialEq<T>>(self, label: T) -> Self;
fn kbgp_initial_focus(self) -> Self {
self.kbgp_focus_label(KbgpInitialFocusLabel)
}
fn kbgp_navigation(self) -> Self;
fn kbgp_user_action<T: 'static + Clone>(&self) -> Option<T>;
fn kbgp_activated<T: 'static + Clone>(&self) -> KbgpNavActivation<T>;
fn kbgp_pending_input(&self) -> Option<KbgpInput>;
fn kbgp_pending_input_of_source(&self, source: KbgpInputSource) -> Option<KbgpInput>;
fn kbgp_pending_input_vetted(&self, pred: impl FnMut(KbgpInput) -> bool) -> Option<KbgpInput>;
fn kbgp_pending_chord(&self) -> Option<HashSet<KbgpInput>>;
fn kbgp_pending_chord_of_source(&self, source: KbgpInputSource) -> Option<HashSet<KbgpInput>>;
fn kbgp_pending_chord_same_source(&self) -> Option<HashSet<KbgpInput>>;
fn kbgp_pending_chord_vetted(
&self,
pred: impl FnMut(&HashSet<KbgpInput>, KbgpInput) -> bool,
) -> Option<HashSet<KbgpInput>>;
fn kbgp_pending_input_manual<T>(
&self,
dlg: impl FnOnce(&Self, KbgpInputManualHandle) -> Option<T>,
) -> Option<T>;
}
impl KbgpEguiResponseExt for egui::Response {
fn kbgp_focus_label<T: 'static + PartialEq<T>>(self, label: T) -> Self {
let kbgp = kbgp_get(&self.ctx);
let mut kbgp = kbgp.lock();
match &mut kbgp.state {
KbgpState::Navigation(state) => {
if let Some(focus_label) = &state.focus_label {
if let Some(focus_label) = focus_label.downcast_ref::<T>() {
if focus_label == &label {
state.focus_label = None;
state.focus_on = Some(self.id);
}
}
}
}
KbgpState::PendingInput(_) => {}
}
self
}
fn kbgp_navigation(self) -> Self {
let kbgp = kbgp_get(&self.ctx);
let mut kbgp = kbgp.lock();
kbgp.common.nodes.insert(
self.id,
NodeData {
rect: self.rect,
seen_this_frame: true,
},
);
self
}
fn kbgp_user_action<T: 'static + Clone>(&self) -> Option<T> {
if self.has_focus() {
self.ctx.kbgp_user_action()
} else {
None
}
}
fn kbgp_activated<T: 'static + Clone>(&self) -> KbgpNavActivation<T> {
if self.clicked() {
KbgpNavActivation::Clicked
} else if self.secondary_clicked() {
KbgpNavActivation::ClickedSecondary
} else if self.middle_clicked() {
KbgpNavActivation::ClickedMiddle
} else if let Some(action) = self.kbgp_user_action() {
KbgpNavActivation::User(action)
} else {
KbgpNavActivation::None
}
}
fn kbgp_pending_input_manual<T>(
&self,
dlg: impl FnOnce(&Self, KbgpInputManualHandle) -> Option<T>,
) -> Option<T> {
let kbgp = kbgp_get(&self.ctx);
let mut kbgp = kbgp.lock();
match &mut kbgp.state {
KbgpState::Navigation(_) => {
if self.clicked() {
kbgp.state = KbgpState::PendingInput(KbgpPendingInputState::new(self.id));
}
None
}
KbgpState::PendingInput(state) => {
if state.acceptor_id != self.id {
return None;
}
self.request_focus();
self.ctx.memory().lock_focus(self.id, true);
let handle = KbgpInputManualHandle { state };
let result = dlg(self, handle);
if result.is_some() {
kbgp.state = KbgpState::Navigation(KbgpNavigationState::default());
}
result
}
}
}
fn kbgp_pending_input(&self) -> Option<KbgpInput> {
self.kbgp_pending_input_vetted(|_| true)
}
fn kbgp_pending_input_of_source(&self, source: KbgpInputSource) -> Option<KbgpInput> {
self.kbgp_pending_input_vetted(|input| input.get_source() == source)
}
fn kbgp_pending_input_vetted(
&self,
mut pred: impl FnMut(KbgpInput) -> bool,
) -> Option<KbgpInput> {
self.kbgp_pending_input_manual(|response, mut hnd| {
hnd.process_new_input(|hnd, input| hnd.received_input().is_empty() && pred(input));
hnd.show_current_chord(response);
if hnd
.input_this_frame()
.any(|inp| hnd.received_input().contains(&inp))
{
None
} else {
let mut it = hnd.received_input().iter();
let single_input = it.next();
assert!(
it.next().is_none(),
"More than one input in chord, but limit is 1"
);
single_input.cloned()
}
})
}
fn kbgp_pending_chord(&self) -> Option<HashSet<KbgpInput>> {
self.kbgp_pending_chord_vetted(|_, _| true)
}
fn kbgp_pending_chord_of_source(&self, source: KbgpInputSource) -> Option<HashSet<KbgpInput>> {
self.kbgp_pending_chord_vetted(|_, input| input.get_source() == source)
}
fn kbgp_pending_chord_same_source(&self) -> Option<HashSet<KbgpInput>> {
self.kbgp_pending_chord_vetted(|existing, input| {
if let Some(existing_input) = existing.iter().next() {
input.get_source() == existing_input.get_source()
} else {
true
}
})
}
fn kbgp_pending_chord_vetted(
&self,
mut pred: impl FnMut(&HashSet<KbgpInput>, KbgpInput) -> bool,
) -> Option<HashSet<KbgpInput>> {
self.kbgp_pending_input_manual(|response, mut hnd| {
hnd.process_new_input(|hnd, input| pred(hnd.received_input(), input));
hnd.show_current_chord(response);
if hnd.input_this_frame().any(|_| true) || hnd.received_input().is_empty() {
None
} else {
Some(hnd.received_input().clone())
}
})
}
}
#[derive(Hash, PartialEq, Eq, Debug, Clone)]
pub enum KbgpInput {
Keyboard(KeyCode),
MouseButton(MouseButton),
MouseWheelUp,
MouseWheelDown,
MouseWheelLeft,
MouseWheelRight,
GamepadAxisPositive(GamepadAxis),
GamepadAxisNegative(GamepadAxis),
GamepadButton(GamepadButton),
}
impl core::fmt::Display for KbgpInput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
KbgpInput::Keyboard(key) => write!(f, "{:?}", key)?,
KbgpInput::MouseButton(MouseButton::Other(button)) => {
write!(f, "MouseButton{:?}", button)?
}
KbgpInput::MouseButton(button) => write!(f, "Mouse{:?}", button)?,
KbgpInput::MouseWheelUp => write!(f, "MouseScrollUp")?,
KbgpInput::MouseWheelDown => write!(f, "MouseScrollDown")?,
KbgpInput::MouseWheelLeft => write!(f, "MouseScrollLeft")?,
KbgpInput::MouseWheelRight => write!(f, "MouseScrollRight")?,
KbgpInput::GamepadButton(GamepadButton {
gamepad: Gamepad { id },
button_type,
}) => write!(f, "[{}]{:?}", id, button_type)?,
KbgpInput::GamepadAxisPositive(GamepadAxis {
gamepad: Gamepad { id },
axis_type,
}) => write!(f, "[{}]{:?}", id, axis_type)?,
KbgpInput::GamepadAxisNegative(GamepadAxis {
gamepad: Gamepad { id },
axis_type,
}) => write!(f, "[{}]-{:?}", id, axis_type)?,
}
Ok(())
}
}
impl KbgpInput {
pub fn format_chord(chord: impl Iterator<Item = Self>) -> String {
let mut chord_text = String::new();
for input in chord {
use std::fmt::Write;
if !chord_text.is_empty() {
write!(&mut chord_text, " & ").unwrap();
}
write!(&mut chord_text, "{}", input).unwrap();
}
chord_text
}
pub fn get_source(&self) -> KbgpInputSource {
match self {
KbgpInput::Keyboard(_) => KbgpInputSource::KeyboardAndMouse,
KbgpInput::MouseButton(_) => KbgpInputSource::KeyboardAndMouse,
KbgpInput::MouseWheelUp => KbgpInputSource::KeyboardAndMouse,
KbgpInput::MouseWheelDown => KbgpInputSource::KeyboardAndMouse,
KbgpInput::MouseWheelLeft => KbgpInputSource::KeyboardAndMouse,
KbgpInput::MouseWheelRight => KbgpInputSource::KeyboardAndMouse,
KbgpInput::GamepadAxisPositive(GamepadAxis {
gamepad,
axis_type: _,
}) => KbgpInputSource::Gamepad(*gamepad),
KbgpInput::GamepadAxisNegative(GamepadAxis {
gamepad,
axis_type: _,
}) => KbgpInputSource::Gamepad(*gamepad),
KbgpInput::GamepadButton(GamepadButton {
gamepad,
button_type: _,
}) => KbgpInputSource::Gamepad(*gamepad),
}
}
}
#[derive(Hash, PartialEq, Eq, Debug, Clone, Copy)]
pub enum KbgpInputSource {
KeyboardAndMouse,
Gamepad(Gamepad),
}
impl core::fmt::Display for KbgpInputSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
KbgpInputSource::KeyboardAndMouse => write!(f, "Keyboard&Mouse"),
KbgpInputSource::Gamepad(Gamepad { id }) => write!(f, "Gamepad {}", id),
}
}
}
impl KbgpInputSource {
pub fn gamepad(&self) -> Option<Gamepad> {
match self {
KbgpInputSource::KeyboardAndMouse => None,
KbgpInputSource::Gamepad(gamepad) => Some(*gamepad),
}
}
}
pub trait KbgpEguiUiCtxExt {
fn kbgp_clear_input(&self);
fn kbgp_set_focus_label<T: 'static + Send + Sync>(&self, label: T);
fn kbgp_user_action<T: 'static + Clone>(&self) -> Option<T>;
}
impl KbgpEguiUiCtxExt for egui::Ui {
fn kbgp_clear_input(&self) {
self.ctx().kbgp_clear_input()
}
fn kbgp_set_focus_label<T: 'static + Send + Sync>(&self, label: T) {
self.ctx().kbgp_set_focus_label(label);
}
fn kbgp_user_action<T: 'static + Clone>(&self) -> Option<T> {
self.ctx().kbgp_user_action()
}
}
impl KbgpEguiUiCtxExt for egui::Context {
fn kbgp_clear_input(&self) {
let kbgp = kbgp_get(self);
let mut kbgp = kbgp.lock();
match &mut kbgp.state {
KbgpState::PendingInput(_) => {}
KbgpState::Navigation(state) => {
state.user_action = None;
}
}
let mut input = self.input_mut();
input.pointer = Default::default();
#[allow(clippy::match_like_matches_macro)]
input.events.retain(|event| match event {
egui::Event::Key {
key: egui::Key::Space | egui::Key::Enter,
pressed: true,
modifiers: _,
} => false,
egui::Event::PointerButton {
pos: _,
button: egui::PointerButton::Primary,
pressed: true,
modifiers: _,
} => false,
_ => true,
});
}
fn kbgp_set_focus_label<T: 'static + Send + Sync>(&self, label: T) {
let kbgp = kbgp_get(self);
let mut kbgp = kbgp.lock();
match &mut kbgp.state {
KbgpState::PendingInput(_) => {}
KbgpState::Navigation(state) => {
state.next_frame_focus_label = Some(Box::new(label));
}
}
}
fn kbgp_user_action<T: 'static + Clone>(&self) -> Option<T> {
let kbgp = kbgp_get(self);
let kbgp = kbgp.lock();
match &kbgp.state {
KbgpState::PendingInput(_) => None,
KbgpState::Navigation(state) => state.user_action.as_ref()?.downcast_ref().cloned(),
}
}
}