use egui::{Context, Key, KeyboardShortcut, Modifiers};
use std::collections::HashMap;
use std::hash::Hash;
pub trait InputBinding {
fn matches(&self, ctx: &Context) -> bool;
fn consume(&self, ctx: &Context) -> bool;
fn display(&self) -> String;
fn as_keyboard_shortcut(&self) -> Option<KeyboardShortcut>;
}
impl InputBinding for KeyboardShortcut {
fn matches(&self, ctx: &Context) -> bool {
ctx.input(|i| i.modifiers == self.modifiers && i.key_pressed(self.logical_key))
}
fn consume(&self, ctx: &Context) -> bool {
ctx.input_mut(|i| i.consume_shortcut(self))
}
fn display(&self) -> String {
self.format(&modifier_names(), self.logical_key == Key::Plus)
}
fn as_keyboard_shortcut(&self) -> Option<KeyboardShortcut> {
Some(*self)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct DynamicShortcut {
pub modifiers: Modifiers,
pub key: Key,
}
impl DynamicShortcut {
pub const fn new(modifiers: Modifiers, key: Key) -> Self {
Self { modifiers, key }
}
pub const fn key_only(key: Key) -> Self {
Self::new(Modifiers::NONE, key)
}
pub const fn to_keyboard_shortcut(&self) -> KeyboardShortcut {
KeyboardShortcut::new(self.modifiers, self.key)
}
}
impl From<KeyboardShortcut> for DynamicShortcut {
fn from(shortcut: KeyboardShortcut) -> Self {
Self {
modifiers: shortcut.modifiers,
key: shortcut.logical_key,
}
}
}
impl From<DynamicShortcut> for KeyboardShortcut {
fn from(shortcut: DynamicShortcut) -> Self {
KeyboardShortcut::new(shortcut.modifiers, shortcut.key)
}
}
impl InputBinding for DynamicShortcut {
fn matches(&self, ctx: &Context) -> bool {
self.to_keyboard_shortcut().matches(ctx)
}
fn consume(&self, ctx: &Context) -> bool {
self.to_keyboard_shortcut().consume(ctx)
}
fn display(&self) -> String {
self.to_keyboard_shortcut().display()
}
fn as_keyboard_shortcut(&self) -> Option<KeyboardShortcut> {
Some(self.to_keyboard_shortcut())
}
}
#[derive(Clone, Debug)]
pub struct ActionBindings<A> {
bindings: HashMap<A, DynamicShortcut>,
defaults: HashMap<A, DynamicShortcut>,
}
impl<A> Default for ActionBindings<A> {
fn default() -> Self {
Self::new()
}
}
impl<A> ActionBindings<A> {
pub fn new() -> Self {
Self {
bindings: HashMap::new(),
defaults: HashMap::new(),
}
}
}
impl<A: Eq + Hash + Clone> ActionBindings<A> {
pub fn with_default(mut self, action: A, shortcut: impl Into<DynamicShortcut>) -> Self {
self.register_default(action, shortcut);
self
}
pub fn register_default(&mut self, action: A, shortcut: impl Into<DynamicShortcut>) {
let shortcut = shortcut.into();
self.defaults.insert(action.clone(), shortcut.clone());
self.bindings.insert(action, shortcut);
}
pub fn register_defaults<I, S>(&mut self, iter: I)
where
I: IntoIterator<Item = (A, S)>,
S: Into<DynamicShortcut>,
{
for (action, shortcut) in iter {
self.register_default(action, shortcut);
}
}
pub fn rebind(&mut self, action: &A, shortcut: DynamicShortcut) -> Option<DynamicShortcut> {
self.bindings.insert(action.clone(), shortcut)
}
pub fn reset(&mut self, action: &A) -> bool {
if let Some(default) = self.defaults.get(action) {
self.bindings.insert(action.clone(), default.clone());
true
} else {
false
}
}
pub fn reset_all(&mut self) {
self.bindings = self.defaults.clone();
}
pub fn get(&self, action: &A) -> Option<&DynamicShortcut> {
self.bindings.get(action)
}
pub fn get_default(&self, action: &A) -> Option<&DynamicShortcut> {
self.defaults.get(action)
}
pub fn is_modified(&self, action: &A) -> bool {
match (self.bindings.get(action), self.defaults.get(action)) {
(Some(current), Some(default)) => current != default,
_ => false,
}
}
pub fn find_action(&self, shortcut: &DynamicShortcut) -> Option<&A> {
self.bindings
.iter()
.find(|(_, s)| *s == shortcut)
.map(|(a, _)| a)
}
pub fn find_conflicts(&self) -> Vec<(&A, &A)> {
let mut conflicts = Vec::new();
let actions: Vec<_> = self.bindings.keys().collect();
for i in 0..actions.len() {
for j in (i + 1)..actions.len() {
if self.bindings.get(actions[i]) == self.bindings.get(actions[j]) {
conflicts.push((actions[i], actions[j]));
}
}
}
conflicts
}
pub fn iter(&self) -> impl Iterator<Item = (&A, &DynamicShortcut)> {
self.bindings.iter()
}
pub fn len(&self) -> usize {
self.bindings.len()
}
pub fn is_empty(&self) -> bool {
self.bindings.is_empty()
}
pub fn remove(&mut self, action: &A) -> Option<DynamicShortcut> {
self.defaults.remove(action);
self.bindings.remove(action)
}
pub fn check_triggered(&self, ctx: &Context) -> Option<&A> {
for (action, shortcut) in &self.bindings {
if shortcut.consume(ctx) {
return Some(action);
}
}
None
}
}
fn modifier_names() -> egui::ModifierNames<'static> {
egui::ModifierNames::NAMES
}
#[derive(Clone, Debug, Default)]
pub struct ShortcutGroup {
shortcuts: Vec<DynamicShortcut>,
}
impl ShortcutGroup {
pub fn new() -> Self {
Self::default()
}
pub fn with(mut self, shortcut: impl Into<DynamicShortcut>) -> Self {
self.shortcuts.push(shortcut.into());
self
}
pub fn add(&mut self, shortcut: impl Into<DynamicShortcut>) {
self.shortcuts.push(shortcut.into());
}
pub fn matches(&self, ctx: &Context) -> bool {
self.shortcuts.iter().any(|s| s.matches(ctx))
}
pub fn consume(&self, ctx: &Context) -> bool {
for shortcut in &self.shortcuts {
if shortcut.consume(ctx) {
return true;
}
}
false
}
}
impl InputBinding for ShortcutGroup {
fn matches(&self, ctx: &Context) -> bool {
ShortcutGroup::matches(self, ctx)
}
fn consume(&self, ctx: &Context) -> bool {
ShortcutGroup::consume(self, ctx)
}
fn display(&self) -> String {
self.shortcuts
.iter()
.map(|s| s.display())
.collect::<Vec<_>>()
.join(" / ")
}
fn as_keyboard_shortcut(&self) -> Option<KeyboardShortcut> {
self.shortcuts
.first()
.and_then(|s| s.as_keyboard_shortcut())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[allow(dead_code)]
enum TestAction {
Save,
Undo,
Redo,
Copy,
}
#[test]
fn test_dynamic_shortcut_creation() {
let shortcut = DynamicShortcut::new(Modifiers::COMMAND, Key::S);
assert_eq!(shortcut.modifiers, Modifiers::COMMAND);
assert_eq!(shortcut.key, Key::S);
}
#[test]
fn test_dynamic_shortcut_from_keyboard_shortcut() {
let ks = KeyboardShortcut::new(Modifiers::CTRL, Key::Z);
let ds = DynamicShortcut::from(ks);
assert_eq!(ds.modifiers, Modifiers::CTRL);
assert_eq!(ds.key, Key::Z);
}
#[test]
fn test_action_bindings_defaults() {
let bindings = ActionBindings::new()
.with_default(
TestAction::Save,
DynamicShortcut::new(Modifiers::COMMAND, Key::S),
)
.with_default(
TestAction::Undo,
DynamicShortcut::new(Modifiers::COMMAND, Key::Z),
);
assert_eq!(bindings.len(), 2);
assert_eq!(
bindings.get(&TestAction::Save),
Some(&DynamicShortcut::new(Modifiers::COMMAND, Key::S))
);
}
#[test]
fn test_action_bindings_rebind() {
let mut bindings = ActionBindings::new().with_default(
TestAction::Save,
DynamicShortcut::new(Modifiers::COMMAND, Key::S),
);
let old = bindings.rebind(
&TestAction::Save,
DynamicShortcut::new(Modifiers::CTRL.plus(Modifiers::SHIFT), Key::S),
);
assert_eq!(old, Some(DynamicShortcut::new(Modifiers::COMMAND, Key::S)));
assert_eq!(
bindings.get(&TestAction::Save),
Some(&DynamicShortcut::new(
Modifiers::CTRL.plus(Modifiers::SHIFT),
Key::S
))
);
assert!(bindings.is_modified(&TestAction::Save));
}
#[test]
fn test_action_bindings_reset() {
let mut bindings = ActionBindings::new().with_default(
TestAction::Save,
DynamicShortcut::new(Modifiers::COMMAND, Key::S),
);
bindings.rebind(
&TestAction::Save,
DynamicShortcut::new(Modifiers::CTRL, Key::S),
);
assert!(bindings.is_modified(&TestAction::Save));
bindings.reset(&TestAction::Save);
assert!(!bindings.is_modified(&TestAction::Save));
assert_eq!(
bindings.get(&TestAction::Save),
Some(&DynamicShortcut::new(Modifiers::COMMAND, Key::S))
);
}
#[test]
fn test_find_action() {
let bindings = ActionBindings::new()
.with_default(
TestAction::Save,
DynamicShortcut::new(Modifiers::COMMAND, Key::S),
)
.with_default(
TestAction::Undo,
DynamicShortcut::new(Modifiers::COMMAND, Key::Z),
);
let found = bindings.find_action(&DynamicShortcut::new(Modifiers::COMMAND, Key::S));
assert_eq!(found, Some(&TestAction::Save));
let not_found = bindings.find_action(&DynamicShortcut::new(Modifiers::COMMAND, Key::X));
assert_eq!(not_found, None);
}
#[test]
fn test_find_conflicts() {
let mut bindings = ActionBindings::new()
.with_default(
TestAction::Save,
DynamicShortcut::new(Modifiers::COMMAND, Key::S),
)
.with_default(
TestAction::Undo,
DynamicShortcut::new(Modifiers::COMMAND, Key::Z),
);
assert!(bindings.find_conflicts().is_empty());
bindings.rebind(
&TestAction::Undo,
DynamicShortcut::new(Modifiers::COMMAND, Key::S),
);
let conflicts = bindings.find_conflicts();
assert_eq!(conflicts.len(), 1);
}
#[test]
fn test_shortcut_group() {
let group = ShortcutGroup::new()
.with(DynamicShortcut::new(Modifiers::COMMAND, Key::Z))
.with(DynamicShortcut::new(Modifiers::CTRL, Key::Z));
let display = group.display();
assert!(
display.contains(" / "),
"Expected separator in: {}",
display
);
assert!(display.contains("Z"), "Expected key Z in: {}", display);
}
}