use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use crate::virtual_input::VirtualButton;
#[derive(Debug, Clone, Default, Reflect)]
pub struct ButtonChord {
#[reflect(ignore)]
buttons: Vec<VirtualButton>,
}
impl ButtonChord {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn from_buttons(buttons: impl IntoIterator<Item = VirtualButton>) -> Self {
Self {
buttons: buttons.into_iter().collect(),
}
}
#[must_use]
pub fn from_keys(keys: &[KeyCode]) -> Self {
Self {
buttons: keys.iter().map(|k| VirtualButton::Key(*k)).collect(),
}
}
#[must_use]
pub fn from_gamepad_buttons(buttons: &[GamepadButton]) -> Self {
Self {
buttons: buttons.iter().map(|b| VirtualButton::Gamepad(*b)).collect(),
}
}
#[must_use]
pub fn with(mut self, button: impl Into<VirtualButton>) -> Self {
self.buttons.push(button.into());
self
}
#[must_use]
pub fn with_multiple(
mut self,
buttons: impl IntoIterator<Item = impl Into<VirtualButton>>,
) -> Self {
self.buttons.extend(buttons.into_iter().map(Into::into));
self
}
#[must_use]
pub fn buttons(&self) -> &[VirtualButton] {
&self.buttons
}
#[must_use]
pub fn len(&self) -> usize {
self.buttons.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.buttons.is_empty()
}
#[must_use]
pub fn is_pressed(&self, keyboard: &ButtonInput<KeyCode>, gamepads: &Query<&Gamepad>) -> bool {
if self.buttons.is_empty() {
return false;
}
self.buttons
.iter()
.all(|b| b.is_pressed(keyboard, gamepads))
}
#[must_use]
pub fn is_pressed_with_mouse(
&self,
keyboard: &ButtonInput<KeyCode>,
mouse: &ButtonInput<MouseButton>,
gamepads: &Query<&Gamepad>,
) -> bool {
if self.buttons.is_empty() {
return false;
}
self.buttons
.iter()
.all(|b| b.is_pressed_with_mouse(keyboard, mouse, gamepads))
}
#[must_use]
pub fn clashes_with(&self, other: &Self) -> bool {
if self.is_empty() || other.is_empty() {
return false;
}
let self_set: HashSet<_> = self.buttons.iter().collect();
let other_set: HashSet<_> = other.buttons.iter().collect();
self_set.is_subset(&other_set) || other_set.is_subset(&self_set)
}
#[must_use]
pub fn decompose(&self) -> Vec<VirtualButton> {
self.buttons.clone()
}
}
impl PartialEq for ButtonChord {
fn eq(&self, other: &Self) -> bool {
if self.buttons.len() != other.buttons.len() {
return false;
}
let self_set: HashSet<_> = self.buttons.iter().collect();
let other_set: HashSet<_> = other.buttons.iter().collect();
self_set == other_set
}
}
impl Eq for ButtonChord {}
impl std::hash::Hash for ButtonChord {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
let mut sorted: Vec<_> = self.buttons.iter().collect();
sorted.sort_by_key(|b| format!("{b:?}"));
for button in sorted {
button.hash(state);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize, Reflect)]
pub enum ClashStrategy {
#[default]
PrioritizeLongest,
UseAll,
PrioritizeFirst,
ExactOnly,
}
#[derive(Debug, Clone)]
pub struct ChordBinding<A> {
pub chord: ButtonChord,
pub action: A,
pub priority: i32,
}
impl<A> ChordBinding<A> {
pub fn new(chord: ButtonChord, action: A) -> Self {
Self {
chord,
action,
priority: 0,
}
}
pub fn with_priority(chord: ButtonChord, action: A, priority: i32) -> Self {
Self {
chord,
action,
priority,
}
}
}
pub fn resolve_clashes<A: Clone>(
pressed_bindings: &[ChordBinding<A>],
strategy: ClashStrategy,
) -> Vec<A> {
if pressed_bindings.is_empty() {
return Vec::new();
}
match strategy {
ClashStrategy::UseAll => pressed_bindings.iter().map(|b| b.action.clone()).collect(),
ClashStrategy::PrioritizeLongest => {
let max_len = pressed_bindings
.iter()
.map(|b| b.chord.len())
.max()
.unwrap_or(0);
pressed_bindings
.iter()
.filter(|b| b.chord.len() == max_len)
.map(|b| b.action.clone())
.collect()
}
ClashStrategy::PrioritizeFirst => {
pressed_bindings
.first()
.map(|b| b.action.clone())
.into_iter()
.collect()
}
ClashStrategy::ExactOnly => {
let mut result = Vec::new();
for binding in pressed_bindings {
let is_subset_of_longer = pressed_bindings.iter().any(|other| {
other.chord.len() > binding.chord.len()
&& binding.chord.clashes_with(&other.chord)
});
if !is_subset_of_longer {
result.push(binding.action.clone());
}
}
result
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect)]
pub enum ModifierKey {
Control,
Shift,
Alt,
Super,
}
impl ModifierKey {
#[must_use]
pub fn is_pressed(self, keyboard: &ButtonInput<KeyCode>) -> bool {
match self {
Self::Control => {
keyboard.pressed(KeyCode::ControlLeft) || keyboard.pressed(KeyCode::ControlRight)
}
Self::Shift => {
keyboard.pressed(KeyCode::ShiftLeft) || keyboard.pressed(KeyCode::ShiftRight)
}
Self::Alt => keyboard.pressed(KeyCode::AltLeft) || keyboard.pressed(KeyCode::AltRight),
Self::Super => {
keyboard.pressed(KeyCode::SuperLeft) || keyboard.pressed(KeyCode::SuperRight)
}
}
}
}
pub(crate) fn register_chord_types(app: &mut App) {
app.register_type::<ButtonChord>()
.register_type::<ClashStrategy>()
.register_type::<ModifierKey>();
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_chord_creation() {
let chord = ButtonChord::from_keys(&[KeyCode::ControlLeft, KeyCode::KeyC]);
assert_eq!(chord.len(), 2);
assert!(!chord.is_empty());
}
#[test]
fn test_chord_with_builder() {
let chord = ButtonChord::new()
.with(KeyCode::ControlLeft)
.with(KeyCode::ShiftLeft)
.with(KeyCode::KeyC);
assert_eq!(chord.len(), 3);
}
#[test]
fn test_chord_equality() {
let chord1 = ButtonChord::from_keys(&[KeyCode::KeyA, KeyCode::KeyB]);
let chord2 = ButtonChord::from_keys(&[KeyCode::KeyB, KeyCode::KeyA]);
assert_eq!(chord1, chord2); }
#[test]
fn test_chord_clash_detection() {
let short = ButtonChord::from_keys(&[KeyCode::KeyA]);
let long = ButtonChord::from_keys(&[KeyCode::ControlLeft, KeyCode::KeyA]);
let unrelated = ButtonChord::from_keys(&[KeyCode::KeyB]);
assert!(short.clashes_with(&long)); assert!(long.clashes_with(&short));
assert!(!short.clashes_with(&unrelated));
}
#[test]
fn test_clash_strategy_prioritize_longest() {
let bindings = vec![
ChordBinding::new(ButtonChord::from_keys(&[KeyCode::KeyA]), "A"),
ChordBinding::new(
ButtonChord::from_keys(&[KeyCode::ControlLeft, KeyCode::KeyA]),
"Ctrl+A",
),
];
let result = resolve_clashes(&bindings, ClashStrategy::PrioritizeLongest);
assert_eq!(result.len(), 1);
assert_eq!(result[0], "Ctrl+A");
}
#[test]
fn test_clash_strategy_use_all() {
let bindings = vec![
ChordBinding::new(ButtonChord::from_keys(&[KeyCode::KeyA]), "A"),
ChordBinding::new(
ButtonChord::from_keys(&[KeyCode::ControlLeft, KeyCode::KeyA]),
"Ctrl+A",
),
];
let result = resolve_clashes(&bindings, ClashStrategy::UseAll);
assert_eq!(result.len(), 2);
}
#[test]
fn test_modifier_key() {
assert_ne!(ModifierKey::Control, ModifierKey::Shift);
assert_ne!(ModifierKey::Alt, ModifierKey::Super);
}
}