use bevy::input::ButtonState;
use bevy::input::gamepad::{Gamepad, GamepadButton};
use bevy::input::keyboard::{KeyCode, KeyboardInput};
use bevy::input::mouse::MouseButton;
use bevy::input_focus::{FocusCause, FocusedInput, InputFocus, InputFocusVisible};
use bevy::prelude::*;
use bevy::ui::InteractionDisabled;
use bevy::ui_widgets::{Activate, ValueChange};
use crate::drag::DragState;
use crate::feedback::{UiFeedback, UiFeedbackKind};
use crate::model::{PadStyle, is_modifier_key, key_name, mouse_name, pad_name};
use crate::nav::show_focus;
use crate::prompt::InputDevice;
use crate::shortcut::{Modifiers, Shortcut};
use crate::skin::Role;
const CATCHER_Z: i32 = 1000;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Reflect)]
#[reflect(Debug, PartialEq, Hash, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Binding {
Key(KeyCode),
Mouse(MouseButton),
Pad(GamepadButton),
Chord(Shortcut),
}
impl Binding {
pub fn label(&self, style: PadStyle) -> String {
match self {
Self::Key(code) => key_name(&format!("{code:?}")),
Self::Mouse(button) => mouse_name(&format!("{button:?}")),
Self::Pad(button) => pad_name(&format!("{button:?}"), style),
Self::Chord(chord) => chord.label(),
}
}
pub fn device(&self) -> InputDevice {
match self {
Self::Key(_) | Self::Mouse(_) | Self::Chord(_) => InputDevice::Keyboard,
Self::Pad(_) => InputDevice::Pad,
}
}
}
#[derive(Component, Clone, Copy, Debug, PartialEq, Eq, Reflect)]
#[reflect(Component, Debug, PartialEq, Clone)]
pub struct BindingValue(pub Binding);
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
pub struct Capturing;
#[derive(Component)]
pub struct Armed;
#[derive(Component)]
pub struct Catcher(Entity);
#[derive(Resource, Clone, Debug, PartialEq, Reflect)]
#[reflect(Resource, Default, Debug, PartialEq, Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(default)
)]
pub struct BindingSettings {
pub key_prompt: String,
pub pad_prompt: String,
pub chord_prompt: String,
#[reflect(ignore)]
pub pad_style: PadStyle,
pub cancel_key: KeyCode,
pub cancel_button: GamepadButton,
}
impl Default for BindingSettings {
fn default() -> Self {
Self {
key_prompt: "Press a key, {exit} exits".to_owned(),
pad_prompt: "Press a button, {exit} exits".to_owned(),
chord_prompt: "Press a shortcut, {exit} exits".to_owned(),
pad_style: PadStyle::default(),
cancel_key: KeyCode::Escape,
cancel_button: GamepadButton::Start,
}
}
}
impl BindingSettings {
pub fn key_prompt(&self) -> String {
let exit = Binding::Key(self.cancel_key).label(self.pad_style);
self.key_prompt.replace("{exit}", &exit)
}
pub fn pad_prompt(&self) -> String {
let exit = Binding::Pad(self.cancel_button).label(self.pad_style);
self.pad_prompt.replace("{exit}", &exit)
}
pub fn chord_prompt(&self) -> String {
let exit = Binding::Key(self.cancel_key).label(self.pad_style);
self.chord_prompt.replace("{exit}", &exit)
}
fn prompt(&self, binding: &Binding) -> String {
match binding {
Binding::Chord(_) => self.chord_prompt(),
other => match other.device() {
InputDevice::Keyboard => self.key_prompt(),
InputDevice::Pad => self.pad_prompt(),
},
}
}
}
pub fn not_capturing(waiting: Query<(), With<Capturing>>, drag: Option<Res<DragState>>) -> bool {
waiting.is_empty() && !drag.is_some_and(|drag| drag.carried_by_key())
}
pub(crate) fn no_row_waits(waiting: Query<(), With<Capturing>>) -> bool {
waiting.is_empty()
}
pub(crate) fn binding_self_update(
change: On<ValueChange<Binding>>,
mut rows: Query<&mut BindingValue>,
) {
if let Ok(mut value) = rows.get_mut(change.source) {
value.set_if_neq(BindingValue(change.value));
}
}
pub(crate) fn binding_on_activate(
event: On<Activate>,
rows: Query<Has<Capturing>, (With<BindingValue>, Without<InteractionDisabled>)>,
mut out: MessageWriter<UiFeedback>,
mut commands: Commands,
) {
let Ok(waiting) = rows.get(event.entity) else {
return;
};
if waiting {
return;
}
commands.entity(event.entity).insert((Capturing, Armed));
out.write(UiFeedback {
entity: event.entity,
kind: UiFeedbackKind::Activate,
});
}
pub(crate) fn binding_on_key_input(
mut input: On<FocusedInput<KeyboardInput>>,
waiting: Query<(), With<Capturing>>,
) {
if input.input.state == ButtonState::Pressed && waiting.contains(input.focused_entity) {
input.propagate(false);
}
}
enum Heard {
Nothing,
Exit,
Bind(Binding),
}
fn heard_by_keyboard_row(
settings: &BindingSettings,
keys: &ButtonInput<KeyCode>,
mouse: &ButtonInput<MouseButton>,
gamepads: &Query<&Gamepad>,
) -> Heard {
if keys.just_pressed(settings.cancel_key) {
return Heard::Exit;
}
if let Some(&key) = keys.get_just_pressed().min() {
return Heard::Bind(Binding::Key(key));
}
if let Some(&button) = mouse.get_just_pressed().next() {
return Heard::Bind(Binding::Mouse(button));
}
if gamepads
.iter()
.any(|pad| pad.get_just_pressed().next().is_some())
{
return Heard::Exit;
}
Heard::Nothing
}
fn heard_by_chord_row(
settings: &BindingSettings,
keys: &ButtonInput<KeyCode>,
mouse: &ButtonInput<MouseButton>,
gamepads: &Query<&Gamepad>,
) -> Heard {
if keys.just_pressed(settings.cancel_key) {
return Heard::Exit;
}
let chord = keys
.get_just_pressed()
.filter(|key| !is_modifier_key(&format!("{key:?}")))
.min();
if let Some(&key) = chord {
return Heard::Bind(Binding::Chord(Shortcut::new(Modifiers::held(keys), key)));
}
if mouse.get_just_pressed().next().is_some()
|| gamepads
.iter()
.any(|pad| pad.get_just_pressed().next().is_some())
{
return Heard::Exit;
}
Heard::Nothing
}
fn heard_by_pad_row(
settings: &BindingSettings,
keys: &ButtonInput<KeyCode>,
mouse: &ButtonInput<MouseButton>,
gamepads: &Query<&Gamepad>,
) -> Heard {
if gamepads
.iter()
.any(|pad| pad.just_pressed(settings.cancel_button))
{
return Heard::Exit;
}
if let Some(button) = gamepads
.iter()
.flat_map(|pad| pad.get_just_pressed().copied())
.min()
{
return Heard::Bind(Binding::Pad(button));
}
if keys.get_just_pressed().next().is_some() || mouse.get_just_pressed().next().is_some() {
return Heard::Exit;
}
Heard::Nothing
}
#[allow(clippy::too_many_arguments)]
pub fn bind_by_input(
settings: Res<BindingSettings>,
keys: Res<ButtonInput<KeyCode>>,
mouse: Res<ButtonInput<MouseButton>>,
gamepads: Query<&Gamepad>,
mut focus: ResMut<InputFocus>,
mut shown: ResMut<InputFocusVisible>,
rows: Query<(Entity, &BindingValue, Has<Armed>), With<Capturing>>,
mut out: MessageWriter<UiFeedback>,
mut commands: Commands,
) {
for (row, held, armed) in &rows {
if armed {
commands.entity(row).remove::<Armed>();
continue;
}
let heard = match held.0 {
Binding::Chord(_) => heard_by_chord_row(&settings, &keys, &mouse, &gamepads),
other => match other.device() {
InputDevice::Keyboard => heard_by_keyboard_row(&settings, &keys, &mouse, &gamepads),
InputDevice::Pad => heard_by_pad_row(&settings, &keys, &mouse, &gamepads),
},
};
match heard {
Heard::Nothing => {
if focus.get() != Some(row) {
commands.entity(row).remove::<Capturing>();
}
continue;
}
Heard::Exit => {}
Heard::Bind(binding) => {
if binding != held.0 {
commands.trigger(ValueChange {
source: row,
value: binding,
is_final: true,
});
out.write(UiFeedback {
entity: row,
kind: UiFeedbackKind::Activate,
});
}
}
}
commands.entity(row).remove::<Capturing>();
if mouse.get_just_pressed().next().is_some() && focus.get() != Some(row) {
focus.set(row, FocusCause::Navigated);
}
let by_keys = keys.get_just_pressed().next().is_some()
|| gamepads
.iter()
.any(|pad| pad.get_just_pressed().next().is_some());
if by_keys {
show_focus(&mut shown);
}
}
}
pub fn catch_pointer(
waiting: Query<Entity, With<Capturing>>,
catchers: Query<(Entity, &Catcher)>,
mut commands: Commands,
) {
for (catcher, Catcher(row)) in &catchers {
if !waiting.contains(*row) {
commands.entity(catcher).despawn();
}
}
for row in &waiting {
if !catchers.iter().any(|(_, Catcher(held))| *held == row) {
commands.spawn((
Name::new("Binding catcher"),
Catcher(row),
Node {
position_type: PositionType::Absolute,
width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default()
},
GlobalZIndex(CATCHER_Z),
Pickable::default(),
));
}
}
}
type Written = Or<(Changed<BindingValue>, Added<Capturing>)>;
pub fn update_bindings(
settings: Res<BindingSettings>,
rows: Query<(Entity, &BindingValue, Has<Capturing>)>,
changed: Query<Entity, (With<BindingValue>, Written)>,
mut ended: RemovedComponents<Capturing>,
children: Query<&Children>,
mut parts: Query<(&Role, &mut Text, &mut Visibility)>,
) {
let mut dirty: Vec<Entity> = if settings.is_changed() {
rows.iter().map(|(row, ..)| row).collect()
} else {
changed.iter().collect()
};
dirty.extend(ended.read());
dirty.sort_unstable();
dirty.dedup();
for row in dirty {
let Ok((_, value, waiting)) = rows.get(row) else {
continue;
};
let name = value.0.label(settings.pad_style);
let prompt = settings.prompt(&value.0);
for part in children.iter_descendants(row) {
let Ok((role, mut text, mut visibility)) = parts.get_mut(part) else {
continue;
};
let (words, shown) = match role {
Role::BindingText => (&name, !waiting),
Role::BindingPrompt => (&prompt, waiting),
_ => continue,
};
if text.0 != *words {
text.0.clone_from(words);
}
visibility.set_if_neq(if shown {
Visibility::Inherited
} else {
Visibility::Hidden
});
}
}
}