use std::any::Any;
use crate::egui;
use bevy::prelude::*;
use bevy::utils::HashMap;
use crate::KbgpCommon;
const INPUT_MASK_UP: u8 = 1;
const INPUT_MASK_DOWN: u8 = 2;
const INPUT_MASK_VERTICAL: u8 = INPUT_MASK_UP | INPUT_MASK_DOWN;
const INPUT_MASK_LEFT: u8 = 4;
const INPUT_MASK_RIGHT: u8 = 8;
const INPUT_MASK_HORIZONTAL: u8 = INPUT_MASK_LEFT | INPUT_MASK_RIGHT;
const INPUT_MASK_CLICK: u8 = 16;
const INPUT_MASK_USER_ACTION: u8 = 32;
#[derive(Default)]
pub(crate) struct KbgpNavigationState {
pub(crate) prev_input: u8,
pub(crate) next_navigation: f64,
pub(crate) user_action: Option<Box<dyn Any + Send + Sync>>,
pub(crate) focus_label: Option<Box<dyn Any + Send + Sync>>,
pub(crate) next_frame_focus_label: Option<Box<dyn Any + Send + Sync>>,
pub(crate) focus_on: Option<egui::Id>,
pub(crate) last_focus: Option<egui::Id>,
pub(crate) mouse_was_last_on: Option<egui::Id>,
}
/// An option of [`KbgpPrepare`](crate::KbgpPrepare).
pub struct KbgpPrepareNavigation {
/// When the player holds a key/button, KBGP will wait `secs_after_first_input` seconds before
/// starting to rapidly apply the action.
///
/// Default: 0.6 seconds.
pub secs_after_first_input: f64,
/// When the player holds a key/button, after
/// [`secs_after_first_input`](crate::KbgpPrepareNavigation::secs_after_first_input), KBGP
/// will apply the action every `secs_between_inputs` seconds.
///
/// Default: 0.04 seconds.
pub secs_between_inputs: f64,
input: u8,
user_action: Option<Box<dyn Any + Send + Sync>>,
}
impl KbgpPrepareNavigation {
pub fn apply_action(&mut self, command: &KbgpNavCommand) {
match command {
KbgpNavCommand::NavigateUp => {
self.input |= INPUT_MASK_UP;
}
KbgpNavCommand::NavigateDown => {
self.input |= INPUT_MASK_DOWN;
}
KbgpNavCommand::NavigateLeft => {
self.input |= INPUT_MASK_LEFT;
}
KbgpNavCommand::NavigateRight => {
self.input |= INPUT_MASK_RIGHT;
}
KbgpNavCommand::Click => {
self.input |= INPUT_MASK_CLICK;
}
KbgpNavCommand::User(action) => {
self.user_action = Some(action());
self.input |= INPUT_MASK_USER_ACTION;
}
}
}
/// Navigate the UI with the keyboard.
pub fn navigate_keyboard_by_binding(
&mut self,
keys: &Input<KeyCode>,
binding: &HashMap<KeyCode, KbgpNavCommand>,
) {
for key in keys.get_pressed() {
if let Some(action) = binding.get(key) {
self.apply_action(action);
}
}
}
/// Navigate the UI with gamepads.
///
/// * Use both left stick and d-pad for navigation.
pub fn navigate_gamepad_by_binding(
&mut self,
gamepads: &Gamepads,
axes: &Axis<GamepadAxis>,
buttons: &Input<GamepadButton>,
binding: &HashMap<GamepadButtonType, KbgpNavCommand>,
) {
for gamepad in gamepads.iter() {
for (axis_type, action_for_negative, action_for_positive) in [
(
GamepadAxisType::LeftStickX,
KbgpNavCommand::NavigateLeft,
KbgpNavCommand::NavigateRight,
),
(
GamepadAxisType::LeftStickY,
KbgpNavCommand::NavigateDown,
KbgpNavCommand::NavigateUp,
),
] {
if let Some(axis_value) = axes.get(GamepadAxis { gamepad, axis_type }) {
if axis_value < -0.5 {
self.apply_action(&action_for_negative)
} else if 0.5 < axis_value {
self.apply_action(&action_for_positive)
}
}
}
}
for GamepadButton {
gamepad: _,
button_type,
} in buttons.get_pressed()
{
if let Some(action) = binding.get(button_type) {
self.apply_action(action);
}
}
}
}
impl KbgpNavigationState {
pub(crate) fn prepare(
&mut self,
common: &KbgpCommon,
egui_ctx: &egui::Context,
prepare_dlg: impl FnOnce(&mut KbgpPrepareNavigation),
) {
let mut handle = KbgpPrepareNavigation {
secs_after_first_input: 0.6,
secs_between_inputs: 0.04,
input: 0,
user_action: None,
};
prepare_dlg(&mut handle);
self.user_action = None;
if handle.input != 0 {
let mut effective_input = handle.input;
let current_time = egui_ctx.input().time;
if self.prev_input != handle.input {
effective_input &= !self.prev_input;
self.next_navigation = current_time + handle.secs_after_first_input;
} else if current_time < self.next_navigation {
effective_input = 0;
} else {
self.next_navigation = current_time + handle.secs_between_inputs;
}
if effective_input & INPUT_MASK_CLICK != 0 {
egui_ctx.input_mut().events.push(egui::Event::Key {
key: egui::Key::Enter,
pressed: true,
modifiers: Default::default(),
});
}
if effective_input & INPUT_MASK_USER_ACTION != 0 {
self.user_action = handle.user_action;
}
let mut move_focus_to = None;
match effective_input & INPUT_MASK_VERTICAL {
INPUT_MASK_UP => {
move_focus_to =
self.move_focus(common, egui_ctx, None, |egui::Pos2 { x, y }| egui::Pos2 {
x: -x,
y: -y,
});
}
INPUT_MASK_DOWN => {
move_focus_to = self.move_focus(common, egui_ctx, None, |p| p);
}
_ => {}
}
// Note: Doing transpose instead of rotation so that starting navigation without
// anything focused will make left similar to up and right similar to down.
match effective_input & INPUT_MASK_HORIZONTAL {
INPUT_MASK_LEFT => {
move_focus_to =
self.move_focus(common, egui_ctx, move_focus_to, |egui::Pos2 { x, y }| {
egui::Pos2 { x: -y, y: -x }
});
}
INPUT_MASK_RIGHT => {
move_focus_to =
self.move_focus(common, egui_ctx, move_focus_to, |egui::Pos2 { x, y }| {
egui::Pos2 { x: y, y: x }
});
}
_ => {}
}
if let Some(move_focus) = move_focus_to {
egui_ctx.memory().request_focus(move_focus);
}
}
self.prev_input = handle.input;
}
fn move_focus(
&mut self,
common: &KbgpCommon,
egui_ctx: &egui::Context,
move_from: Option<egui::Id>,
transform_pos_downward: impl Fn(egui::Pos2) -> egui::Pos2,
) -> Option<egui::Id> {
let transform_rect_downward = |rect: egui::Rect| -> egui::Rect {
let egui::Pos2 {
x: mut left,
y: mut top,
} = transform_pos_downward(rect.min);
let egui::Pos2 {
x: mut right,
y: mut bottom,
} = transform_pos_downward(rect.max);
if right < left {
std::mem::swap(&mut left, &mut right);
}
if bottom < top {
std::mem::swap(&mut top, &mut bottom);
}
egui::Rect {
min: egui::Pos2 { x: left, y: top },
max: egui::Pos2 {
x: right,
y: bottom,
},
}
};
let transformed_nodes = common
.nodes
.iter()
.map(|(id, data)| (id, transform_rect_downward(data.rect)));
let focused_node_id = move_from.or_else(|| egui_ctx.memory().focus());
if let Some(focused_node_id) = focused_node_id {
let focused_node_rect = if let Some(data) = common.nodes.get(&focused_node_id) {
transform_rect_downward(data.rect)
} else {
return Some(focused_node_id);
};
#[derive(Debug)]
struct InfoForComparison {
min_y: f32,
max_y: f32,
x_drift: f32,
}
transformed_nodes
.filter_map(|(id, rect)| {
if *id == focused_node_id {
return None;
}
let min_y_diff = rect.min.y - focused_node_rect.max.y;
if min_y_diff < 0.0 {
return None;
}
Some((
id,
InfoForComparison {
min_y: min_y_diff,
max_y: rect.max.y - focused_node_rect.max.y,
x_drift: {
if focused_node_rect.max.x < rect.min.x {
rect.max.x - focused_node_rect.min.x
} else if rect.max.x < focused_node_rect.min.x {
focused_node_rect.max.x - rect.min.x
} else {
0.0
}
},
},
))
})
.min_by(|(_, a), (_, b)| {
if a.max_y < b.min_y && b.max_y < a.min_y {
a.x_drift.partial_cmp(&b.x_drift).unwrap()
} else {
(a.min_y + a.x_drift)
.partial_cmp(&(b.min_y + b.x_drift))
.unwrap()
}
})
.map(|(id, _)| *id)
} else {
transformed_nodes
.map(|(id, rect)| (id, (rect.min.y, rect.min.x)))
.min_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap())
.map(|(id, _)| *id)
}
}
}
pub enum KbgpNavCommand {
/// Move the focus one widget up. If no widget has the focus - move up from the bottom.
///
/// Will only work if [`kbgp_navigation`](crate::KbgpEguiResponseExt::kbgp_navigation) was
/// called on the currently focused widget, and can only target widgets marked
/// `kbgp_navigation` was called on.
NavigateUp,
/// Move the focus one widget down. If no widget has the focus - move down from the top.
///
/// Will only work if [`kbgp_navigation`](crate::KbgpEguiResponseExt::kbgp_navigation) was
/// called on the currently focused widget, and can only target widgets marked
/// `kbgp_navigation` was called on.
NavigateDown,
/// Move the focus one widget left. If no widget has the focus - move left from the right.
///
/// Will only work if [`kbgp_navigation`](crate::KbgpEguiResponseExt::kbgp_navigation) was
/// called on the currently focused widget, and can only target widgets marked
/// `kbgp_navigation` was called on.
NavigateLeft,
/// Move the focus one widget right. If no widget has the focus - move right from the left.
///
/// Will only work if [`kbgp_navigation`](crate::KbgpEguiResponseExt::kbgp_navigation) was
/// called on the currently focused widget, and can only target widgets marked
/// `kbgp_navigation` was called on.
NavigateRight,
/// Make egui think the player clicked on the focused widget.
Click,
/// Activeate a user defined command.
///
/// This variant is tricky to construct directly - use [`KbgpNavCommand::user`] instead.
///
/// User commands can be checked by using
/// [`kbgp_user_action`](crate::KbgpEguiResponseExt::kbgp_user_action) or
/// [`kbgp_activated`](crate::KbgpEguiResponseExt::kbgp_activated) on the widget and
/// [`kbgp_user_action`](crate::KbgpEguiUiCtxExt::kbgp_user_action) on the UI handle.
User(Box<dyn 'static + Send + Sync + Fn() -> Box<dyn Any + Send + Sync>>),
}
impl KbgpNavCommand {
/// Used to define user-commands.
///
/// To define a user command, the result of this function should be bound to a key or a gamepad
/// button in [`KbgpNavBindings`] which in turn should be placed inside the
/// [`KbgpSettings`](crate::KbgpSettings) resource. Then a widget's
/// [`kbgp_user_action`](crate::KbgpEguiResponseExt::kbgp_user_action) or
/// [`kbgp_activated`](crate::KbgpEguiResponseExt::kbgp_activated) or the
/// [`kbgp_user_action`](crate::KbgpEguiUiCtxExt::kbgp_user_action) on the UI handle can be
/// used to determine if the player activated this action.
///
/// ```no_run
/// use bevy::prelude::*;
/// use bevy_egui::{EguiContext, EguiPlugin};
/// use bevy_egui_kbgp::{egui, bevy_egui};
/// use bevy_egui_kbgp::prelude::*;
/// fn main() {
/// App::new()
/// .add_plugins(DefaultPlugins)
/// .add_plugin(EguiPlugin)
/// .add_plugin(KbgpPlugin)
/// .add_system(ui_system)
/// .insert_resource(KbgpSettings {
/// bindings: bevy_egui_kbgp::KbgpNavBindings::default()
/// .with_key(KeyCode::Escape, KbgpNavCommand::user(UserAction::Exit))
/// .with_key(KeyCode::Z, KbgpNavCommand::user(UserAction::Special1))
/// .with_key(KeyCode::X, KbgpNavCommand::user(UserAction::Special2)),
/// ..Default::default()
/// })
/// .run();
/// }
///
/// #[derive(Clone)]
/// enum UserAction {
/// Exit,
/// Special1,
/// Special2,
/// }
///
/// fn ui_system(
/// mut egui_context: ResMut<EguiContext>,
/// ) {
/// egui::CentralPanel::default().show(egui_context.ctx_mut(), |ui| {
/// if matches!(ui.kbgp_user_action(), Some(UserAction::Exit)) {
/// println!("User wants to exit");
/// }
/// match ui.button("Button").kbgp_activated() {
/// KbgpNavActivation::Clicked => {
/// println!("Regular button activation");
/// }
/// KbgpNavActivation::ClickedSecondary | KbgpNavActivation::User(UserAction::Special1) => {
/// println!("Special button activation 1");
/// }
/// KbgpNavActivation::ClickedMiddle | KbgpNavActivation::User(UserAction::Special2) => {
/// println!("Special button activation 2");
/// }
/// _ => {}
/// }
/// });
/// }
/// ```
pub fn user<T: 'static + Clone + Send + Sync>(value: T) -> Self {
Self::User(Box::new(move || Box::new(value.clone())))
}
}
/// Input mapping for navigation.
pub struct KbgpNavBindings {
/// The configured keyboard bindings.
pub keyboard: HashMap<KeyCode, KbgpNavCommand>,
/// The configured gamepad bindings.
///
/// These are not limited to a specific gamepad, and are for buttons only - the axis behavior
/// is hard coded. Note that in some environments the d-pad is treated as an axis.
pub gamepad_buttons: HashMap<GamepadButtonType, KbgpNavCommand>,
}
impl Default for KbgpNavBindings {
/// Create bindings with the default mappings.
///
/// * Navigation: arrow keys, d-pad, left stick.
/// * Activateion: Enter (egui builtin), Spacebar (also egui builtin), gamepad south button.
fn default() -> Self {
Self::empty()
.with_arrow_keys_navigation()
.with_gamepad_dpad_navigation_and_south_button_activation()
}
}
impl KbgpNavBindings {
/// Create empty bindings with no mapping.
///
/// Gamepad axes will still be mapped, because their handling is hard coded. Note that in some
/// environments the d-pad is treated as an axis.
pub fn empty() -> Self {
Self {
keyboard: Default::default(),
gamepad_buttons: Default::default(),
}
}
/// Bind the arrow keys for navigation.
///
/// [`KbgpNavBindings::default`] already contains these mappings.
pub fn bind_arrow_keys_navigation(&mut self) {
self.bind_key(KeyCode::Up, KbgpNavCommand::NavigateUp);
self.bind_key(KeyCode::Down, KbgpNavCommand::NavigateDown);
self.bind_key(KeyCode::Left, KbgpNavCommand::NavigateLeft);
self.bind_key(KeyCode::Right, KbgpNavCommand::NavigateRight);
}
/// Bind the arrow keys for navigation.
///
/// [`KbgpNavBindings::default`] already contains these mappings.
pub fn with_arrow_keys_navigation(mut self) -> Self {
self.bind_arrow_keys_navigation();
self
}
/// Bind the gamepad's d-pad for navigation and south button for activation.
///
/// [`KbgpNavBindings::default`] already contains these mappings.
pub fn bind_gamepad_dpad_navigation_and_south_button_activation(&mut self) {
self.bind_gamepad_button(GamepadButtonType::DPadUp, KbgpNavCommand::NavigateUp);
self.bind_gamepad_button(GamepadButtonType::DPadDown, KbgpNavCommand::NavigateDown);
self.bind_gamepad_button(GamepadButtonType::DPadLeft, KbgpNavCommand::NavigateLeft);
self.bind_gamepad_button(GamepadButtonType::DPadRight, KbgpNavCommand::NavigateRight);
self.bind_gamepad_button(GamepadButtonType::South, KbgpNavCommand::Click);
}
/// Bind the gamepad's d-pad for navigation and south button for activation.
///
/// [`KbgpNavBindings::default`] already contains these mappings.
pub fn with_gamepad_dpad_navigation_and_south_button_activation(mut self) -> Self {
self.bind_gamepad_dpad_navigation_and_south_button_activation();
self
}
/// Bind WASD for navigation.
pub fn bind_wasd_navigation(&mut self) {
self.bind_key(KeyCode::W, KbgpNavCommand::NavigateUp);
self.bind_key(KeyCode::S, KbgpNavCommand::NavigateDown);
self.bind_key(KeyCode::A, KbgpNavCommand::NavigateLeft);
self.bind_key(KeyCode::D, KbgpNavCommand::NavigateRight);
}
/// Bind WASD for navigation.
pub fn with_wasd_navigation(mut self) -> Self {
self.bind_wasd_navigation();
self
}
/// Bind a command to a keyboard key.
pub fn bind_key(&mut self, key: KeyCode, command: KbgpNavCommand) {
self.keyboard.insert(key, command);
}
/// Bind a command to a keyboard key.
pub fn with_key(mut self, key: KeyCode, command: KbgpNavCommand) -> Self {
self.bind_key(key, command);
self
}
/// Bind a command to a gamepad button.
pub fn bind_gamepad_button(
&mut self,
gamepad_button: GamepadButtonType,
command: KbgpNavCommand,
) {
self.gamepad_buttons.insert(gamepad_button, command);
}
/// Bind a command to a gamepad button.
pub fn with_gamepad_button(
mut self,
gamepad_button: GamepadButtonType,
command: KbgpNavCommand,
) -> Self {
self.bind_gamepad_button(gamepad_button, command);
self
}
}
pub enum KbgpNavActivation<T> {
/// The widget was not actiated this frame.
None,
/// The widget's primary function was activated.
///
/// This means it was either left-clicked, or it was focused and the player pressed on Enter,
/// Spacebar, the gamepad's south button (unless overriden), or some other key or button set in
/// [`KbgpNavBindings`].
Clicked,
/// The widget was right-clicked.
ClickedSecondary,
/// The widget was middle-clicked.
ClickedMiddle,
/// A user action was activated when the focus was on this widget.
User(T),
}