#![allow(clippy::unwrap_used, clippy::disallowed_methods)]
use crate::event::Key;
use crate::widget::WidgetId;
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct Modifiers {
pub ctrl: bool,
pub alt: bool,
pub shift: bool,
pub meta: bool,
}
impl Modifiers {
pub const NONE: Self = Self {
ctrl: false,
alt: false,
shift: false,
meta: false,
};
pub const CTRL: Self = Self {
ctrl: true,
alt: false,
shift: false,
meta: false,
};
pub const ALT: Self = Self {
ctrl: false,
alt: true,
shift: false,
meta: false,
};
pub const SHIFT: Self = Self {
ctrl: false,
alt: false,
shift: true,
meta: false,
};
pub const META: Self = Self {
ctrl: false,
alt: false,
shift: false,
meta: true,
};
pub const CTRL_SHIFT: Self = Self {
ctrl: true,
alt: false,
shift: true,
meta: false,
};
pub const CTRL_ALT: Self = Self {
ctrl: true,
alt: true,
shift: false,
meta: false,
};
pub const fn new(ctrl: bool, alt: bool, shift: bool, meta: bool) -> Self {
Self {
ctrl,
alt,
shift,
meta,
}
}
pub const fn any(&self) -> bool {
self.ctrl || self.alt || self.shift || self.meta
}
pub const fn none(&self) -> bool {
!self.any()
}
pub fn display(&self) -> String {
let mut parts = Vec::new();
if self.ctrl {
parts.push("Ctrl");
}
if self.alt {
parts.push("Alt");
}
if self.shift {
parts.push("Shift");
}
if self.meta {
parts.push("Meta");
}
parts.join("+")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Shortcut {
pub key: Key,
pub modifiers: Modifiers,
}
impl Shortcut {
pub const fn new(key: Key, modifiers: Modifiers) -> Self {
Self { key, modifiers }
}
pub const fn key(key: Key) -> Self {
Self::new(key, Modifiers::NONE)
}
pub const fn ctrl(key: Key) -> Self {
Self::new(key, Modifiers::CTRL)
}
pub const fn alt(key: Key) -> Self {
Self::new(key, Modifiers::ALT)
}
pub const fn shift(key: Key) -> Self {
Self::new(key, Modifiers::SHIFT)
}
pub const fn ctrl_shift(key: Key) -> Self {
Self::new(key, Modifiers::CTRL_SHIFT)
}
pub fn display(&self) -> String {
let key_name = format!("{:?}", self.key);
if self.modifiers.none() {
key_name
} else {
format!("{}+{}", self.modifiers.display(), key_name)
}
}
pub const COPY: Self = Self::ctrl(Key::C);
pub const CUT: Self = Self::ctrl(Key::X);
pub const PASTE: Self = Self::ctrl(Key::V);
pub const UNDO: Self = Self::ctrl(Key::Z);
pub const REDO: Self = Self::ctrl_shift(Key::Z);
pub const SAVE: Self = Self::ctrl(Key::S);
pub const SELECT_ALL: Self = Self::ctrl(Key::A);
pub const FIND: Self = Self::ctrl(Key::F);
pub const ESCAPE: Self = Self::key(Key::Escape);
pub const ENTER: Self = Self::key(Key::Enter);
pub const TAB: Self = Self::key(Key::Tab);
pub const DELETE: Self = Self::key(Key::Delete);
pub const BACKSPACE: Self = Self::key(Key::Backspace);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ShortcutId(pub u64);
impl ShortcutId {
pub const fn new(id: u64) -> Self {
Self(id)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub enum ShortcutContext {
#[default]
Global,
Widget(WidgetId),
WidgetType(String),
Custom(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub enum ShortcutPriority {
Low = 0,
#[default]
Normal = 1,
High = 2,
}
pub type ShortcutHandler = Box<dyn FnMut() -> bool + Send>;
struct ShortcutBinding {
#[allow(dead_code)]
id: ShortcutId,
shortcut: Shortcut,
context: ShortcutContext,
priority: ShortcutPriority,
description: String,
enabled: bool,
}
pub struct ShortcutManager {
next_id: u64,
bindings: HashMap<ShortcutId, ShortcutBinding>,
handlers: HashMap<ShortcutId, ShortcutHandler>,
by_shortcut: HashMap<Shortcut, Vec<ShortcutId>>,
active_contexts: Vec<ShortcutContext>,
modifiers: Modifiers,
}
impl ShortcutManager {
pub fn new() -> Self {
Self {
next_id: 0,
bindings: HashMap::new(),
handlers: HashMap::new(),
by_shortcut: HashMap::new(),
active_contexts: vec![ShortcutContext::Global],
modifiers: Modifiers::NONE,
}
}
pub fn register(&mut self, shortcut: Shortcut, handler: ShortcutHandler) -> ShortcutId {
self.register_with_options(
shortcut,
handler,
ShortcutContext::Global,
ShortcutPriority::Normal,
"",
)
}
pub fn register_with_options(
&mut self,
shortcut: Shortcut,
handler: ShortcutHandler,
context: ShortcutContext,
priority: ShortcutPriority,
description: &str,
) -> ShortcutId {
let id = ShortcutId::new(self.next_id);
self.next_id += 1;
let binding = ShortcutBinding {
id,
shortcut,
context,
priority,
description: description.to_string(),
enabled: true,
};
self.bindings.insert(id, binding);
self.handlers.insert(id, handler);
self.by_shortcut.entry(shortcut).or_default().push(id);
id
}
pub fn unregister(&mut self, id: ShortcutId) -> bool {
if let Some(binding) = self.bindings.remove(&id) {
self.handlers.remove(&id);
if let Some(ids) = self.by_shortcut.get_mut(&binding.shortcut) {
ids.retain(|&i| i != id);
}
true
} else {
false
}
}
pub fn set_enabled(&mut self, id: ShortcutId, enabled: bool) {
if let Some(binding) = self.bindings.get_mut(&id) {
binding.enabled = enabled;
}
}
pub fn is_enabled(&self, id: ShortcutId) -> bool {
self.bindings.get(&id).is_some_and(|b| b.enabled)
}
pub fn set_modifiers(&mut self, modifiers: Modifiers) {
self.modifiers = modifiers;
}
pub fn modifiers(&self) -> Modifiers {
self.modifiers
}
pub fn push_context(&mut self, context: ShortcutContext) {
self.active_contexts.push(context);
}
pub fn pop_context(&mut self) -> Option<ShortcutContext> {
if self.active_contexts.len() > 1 {
self.active_contexts.pop()
} else {
None
}
}
pub fn set_focused_widget(&mut self, widget_id: Option<WidgetId>) {
self.active_contexts
.retain(|c| !matches!(c, ShortcutContext::Widget(_)));
if let Some(id) = widget_id {
self.active_contexts.push(ShortcutContext::Widget(id));
}
}
pub fn handle_key(&mut self, key: Key) -> bool {
let shortcut = Shortcut::new(key, self.modifiers);
self.trigger(shortcut)
}
pub fn trigger(&mut self, shortcut: Shortcut) -> bool {
let binding_ids = match self.by_shortcut.get(&shortcut) {
Some(ids) => ids.clone(),
None => return false,
};
let mut matches: Vec<(ShortcutId, ShortcutPriority)> = binding_ids
.iter()
.filter_map(|&id| {
let binding = self.bindings.get(&id)?;
if !binding.enabled {
return None;
}
if self.is_context_active(&binding.context) {
Some((id, binding.priority))
} else {
None
}
})
.collect();
matches.sort_by(|a, b| b.1.cmp(&a.1));
for (id, _) in matches {
if let Some(handler) = self.handlers.get_mut(&id) {
if handler() {
return true;
}
}
}
false
}
fn is_context_active(&self, context: &ShortcutContext) -> bool {
match context {
ShortcutContext::Global => true,
other => self.active_contexts.contains(other),
}
}
pub fn shortcuts(&self) -> impl Iterator<Item = (&Shortcut, &str)> {
self.bindings
.values()
.map(|b| (&b.shortcut, b.description.as_str()))
}
pub fn binding_count(&self) -> usize {
self.bindings.len()
}
pub fn find_conflicts(&self) -> Vec<(Shortcut, Vec<ShortcutId>)> {
let mut conflicts = Vec::new();
for (shortcut, ids) in &self.by_shortcut {
if ids.len() < 2 {
continue;
}
let mut by_context: HashMap<&ShortcutContext, Vec<ShortcutId>> = HashMap::new();
for &id in ids {
if let Some(binding) = self.bindings.get(&id) {
by_context.entry(&binding.context).or_default().push(id);
}
}
for (_, context_ids) in by_context {
if context_ids.len() > 1 {
conflicts.push((*shortcut, context_ids));
}
}
}
conflicts
}
pub fn clear(&mut self) {
self.bindings.clear();
self.handlers.clear();
self.by_shortcut.clear();
}
pub fn description(&self, id: ShortcutId) -> Option<&str> {
self.bindings.get(&id).map(|b| b.description.as_str())
}
}
impl Default for ShortcutManager {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Debug for ShortcutManager {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ShortcutManager")
.field("binding_count", &self.bindings.len())
.field("active_contexts", &self.active_contexts)
.field("modifiers", &self.modifiers)
.finish()
}
}
#[derive(Debug, Clone)]
pub struct ShortcutBuilder {
key: Key,
modifiers: Modifiers,
context: ShortcutContext,
priority: ShortcutPriority,
description: String,
}
impl ShortcutBuilder {
pub fn new(key: Key) -> Self {
Self {
key,
modifiers: Modifiers::NONE,
context: ShortcutContext::Global,
priority: ShortcutPriority::Normal,
description: String::new(),
}
}
pub fn ctrl(mut self) -> Self {
self.modifiers.ctrl = true;
self
}
pub fn alt(mut self) -> Self {
self.modifiers.alt = true;
self
}
pub fn shift(mut self) -> Self {
self.modifiers.shift = true;
self
}
pub fn meta(mut self) -> Self {
self.modifiers.meta = true;
self
}
pub fn context(mut self, context: ShortcutContext) -> Self {
self.context = context;
self
}
pub fn for_widget(mut self, widget_id: WidgetId) -> Self {
self.context = ShortcutContext::Widget(widget_id);
self
}
pub fn priority(mut self, priority: ShortcutPriority) -> Self {
self.priority = priority;
self
}
pub fn description(mut self, desc: &str) -> Self {
self.description = desc.to_string();
self
}
pub fn build(self) -> Shortcut {
Shortcut::new(self.key, self.modifiers)
}
pub fn register(self, manager: &mut ShortcutManager, handler: ShortcutHandler) -> ShortcutId {
let shortcut = Shortcut::new(self.key, self.modifiers);
manager.register_with_options(
shortcut,
handler,
self.context,
self.priority,
&self.description,
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
#[test]
fn test_modifiers_constants() {
assert!(!Modifiers::NONE.any());
assert!(Modifiers::NONE.none());
assert!(Modifiers::CTRL.ctrl);
assert!(!Modifiers::CTRL.alt);
assert!(Modifiers::CTRL_SHIFT.ctrl);
assert!(Modifiers::CTRL_SHIFT.shift);
}
#[test]
fn test_modifiers_new() {
let mods = Modifiers::new(true, true, false, false);
assert!(mods.ctrl);
assert!(mods.alt);
assert!(!mods.shift);
assert!(!mods.meta);
}
#[test]
fn test_modifiers_display() {
assert_eq!(Modifiers::NONE.display(), "");
assert_eq!(Modifiers::CTRL.display(), "Ctrl");
assert_eq!(Modifiers::CTRL_SHIFT.display(), "Ctrl+Shift");
}
#[test]
fn test_shortcut_new() {
let shortcut = Shortcut::new(Key::A, Modifiers::CTRL);
assert_eq!(shortcut.key, Key::A);
assert!(shortcut.modifiers.ctrl);
}
#[test]
fn test_shortcut_constructors() {
let key_only = Shortcut::key(Key::Escape);
assert!(key_only.modifiers.none());
let ctrl = Shortcut::ctrl(Key::S);
assert!(ctrl.modifiers.ctrl);
let alt = Shortcut::alt(Key::F4);
assert!(alt.modifiers.alt);
let shift = Shortcut::shift(Key::Tab);
assert!(shift.modifiers.shift);
let ctrl_shift = Shortcut::ctrl_shift(Key::Z);
assert!(ctrl_shift.modifiers.ctrl);
assert!(ctrl_shift.modifiers.shift);
}
#[test]
fn test_shortcut_display() {
assert_eq!(Shortcut::key(Key::A).display(), "A");
assert_eq!(Shortcut::ctrl(Key::S).display(), "Ctrl+S");
assert_eq!(Shortcut::ctrl_shift(Key::Z).display(), "Ctrl+Shift+Z");
}
#[test]
fn test_shortcut_constants() {
assert_eq!(Shortcut::COPY, Shortcut::ctrl(Key::C));
assert_eq!(Shortcut::UNDO, Shortcut::ctrl(Key::Z));
assert_eq!(Shortcut::REDO, Shortcut::ctrl_shift(Key::Z));
}
#[test]
fn test_shortcut_equality() {
let s1 = Shortcut::ctrl(Key::S);
let s2 = Shortcut::ctrl(Key::S);
let s3 = Shortcut::ctrl(Key::A);
assert_eq!(s1, s2);
assert_ne!(s1, s3);
}
#[test]
fn test_shortcut_id() {
let id1 = ShortcutId::new(1);
let id2 = ShortcutId::new(1);
let id3 = ShortcutId::new(2);
assert_eq!(id1, id2);
assert_ne!(id1, id3);
}
#[test]
fn test_shortcut_context_default() {
assert_eq!(ShortcutContext::default(), ShortcutContext::Global);
}
#[test]
fn test_shortcut_priority_ordering() {
assert!(ShortcutPriority::High > ShortcutPriority::Normal);
assert!(ShortcutPriority::Normal > ShortcutPriority::Low);
}
#[test]
fn test_manager_new() {
let manager = ShortcutManager::new();
assert_eq!(manager.binding_count(), 0);
}
#[test]
fn test_manager_register() {
let mut manager = ShortcutManager::new();
let id = manager.register(Shortcut::ctrl(Key::S), Box::new(|| true));
assert_eq!(manager.binding_count(), 1);
assert!(manager.is_enabled(id));
}
#[test]
fn test_manager_unregister() {
let mut manager = ShortcutManager::new();
let id = manager.register(Shortcut::ctrl(Key::S), Box::new(|| true));
assert_eq!(manager.binding_count(), 1);
let removed = manager.unregister(id);
assert!(removed);
assert_eq!(manager.binding_count(), 0);
}
#[test]
fn test_manager_set_enabled() {
let mut manager = ShortcutManager::new();
let id = manager.register(Shortcut::ctrl(Key::S), Box::new(|| true));
assert!(manager.is_enabled(id));
manager.set_enabled(id, false);
assert!(!manager.is_enabled(id));
manager.set_enabled(id, true);
assert!(manager.is_enabled(id));
}
#[test]
fn test_manager_handle_key() {
let triggered = Arc::new(AtomicBool::new(false));
let triggered_clone = triggered.clone();
let mut manager = ShortcutManager::new();
manager.register(
Shortcut::ctrl(Key::S),
Box::new(move || {
triggered_clone.store(true, Ordering::SeqCst);
true
}),
);
manager.set_modifiers(Modifiers::NONE);
let result = manager.handle_key(Key::S);
assert!(!result);
assert!(!triggered.load(Ordering::SeqCst));
manager.set_modifiers(Modifiers::CTRL);
let result = manager.handle_key(Key::S);
assert!(result);
assert!(triggered.load(Ordering::SeqCst));
}
#[test]
fn test_manager_trigger() {
let counter = Arc::new(AtomicUsize::new(0));
let counter_clone = counter.clone();
let mut manager = ShortcutManager::new();
manager.register(
Shortcut::ctrl(Key::C),
Box::new(move || {
counter_clone.fetch_add(1, Ordering::SeqCst);
true
}),
);
manager.trigger(Shortcut::ctrl(Key::C));
assert_eq!(counter.load(Ordering::SeqCst), 1);
manager.trigger(Shortcut::ctrl(Key::C));
assert_eq!(counter.load(Ordering::SeqCst), 2);
}
#[test]
fn test_manager_disabled_shortcut_not_triggered() {
let triggered = Arc::new(AtomicBool::new(false));
let triggered_clone = triggered.clone();
let mut manager = ShortcutManager::new();
let id = manager.register(
Shortcut::ctrl(Key::S),
Box::new(move || {
triggered_clone.store(true, Ordering::SeqCst);
true
}),
);
manager.set_enabled(id, false);
manager.set_modifiers(Modifiers::CTRL);
let result = manager.handle_key(Key::S);
assert!(!result);
assert!(!triggered.load(Ordering::SeqCst));
}
#[test]
fn test_manager_context() {
let triggered = Arc::new(AtomicBool::new(false));
let triggered_clone = triggered.clone();
let mut manager = ShortcutManager::new();
manager.register_with_options(
Shortcut::ctrl(Key::S),
Box::new(move || {
triggered_clone.store(true, Ordering::SeqCst);
true
}),
ShortcutContext::Widget(WidgetId::new(1)),
ShortcutPriority::Normal,
"",
);
manager.set_modifiers(Modifiers::CTRL);
let result = manager.handle_key(Key::S);
assert!(!result);
manager.set_focused_widget(Some(WidgetId::new(1)));
let result = manager.handle_key(Key::S);
assert!(result);
assert!(triggered.load(Ordering::SeqCst));
}
#[test]
fn test_manager_priority() {
let order = Arc::new(std::sync::Mutex::new(Vec::new()));
let order1 = order.clone();
let order2 = order.clone();
let mut manager = ShortcutManager::new();
manager.register_with_options(
Shortcut::ctrl(Key::S),
Box::new(move || {
order1.lock().unwrap().push("low");
false }),
ShortcutContext::Global,
ShortcutPriority::Low,
"",
);
manager.register_with_options(
Shortcut::ctrl(Key::S),
Box::new(move || {
order2.lock().unwrap().push("high");
false
}),
ShortcutContext::Global,
ShortcutPriority::High,
"",
);
manager.trigger(Shortcut::ctrl(Key::S));
let order_vec = order.lock().unwrap();
assert_eq!(*order_vec, vec!["high", "low"]);
}
#[test]
fn test_manager_handler_consumes() {
let counter = Arc::new(AtomicUsize::new(0));
let c1 = counter.clone();
let c2 = counter.clone();
let mut manager = ShortcutManager::new();
manager.register_with_options(
Shortcut::ctrl(Key::S),
Box::new(move || {
c1.fetch_add(1, Ordering::SeqCst);
true }),
ShortcutContext::Global,
ShortcutPriority::High,
"",
);
manager.register_with_options(
Shortcut::ctrl(Key::S),
Box::new(move || {
c2.fetch_add(1, Ordering::SeqCst);
true
}),
ShortcutContext::Global,
ShortcutPriority::Low,
"",
);
manager.trigger(Shortcut::ctrl(Key::S));
assert_eq!(counter.load(Ordering::SeqCst), 1);
}
#[test]
fn test_manager_push_pop_context() {
let mut manager = ShortcutManager::new();
manager.push_context(ShortcutContext::Custom("editor".to_string()));
manager.push_context(ShortcutContext::Custom("modal".to_string()));
let popped = manager.pop_context();
assert_eq!(popped, Some(ShortcutContext::Custom("modal".to_string())));
let popped = manager.pop_context();
assert_eq!(popped, Some(ShortcutContext::Custom("editor".to_string())));
let popped = manager.pop_context();
assert!(popped.is_none());
}
#[test]
fn test_manager_find_conflicts() {
let mut manager = ShortcutManager::new();
let id1 = manager.register(Shortcut::ctrl(Key::S), Box::new(|| true));
let id2 = manager.register(Shortcut::ctrl(Key::S), Box::new(|| true));
manager.register(Shortcut::ctrl(Key::A), Box::new(|| true));
let conflicts = manager.find_conflicts();
assert_eq!(conflicts.len(), 1);
assert!(conflicts[0].1.contains(&id1));
assert!(conflicts[0].1.contains(&id2));
}
#[test]
fn test_manager_shortcuts() {
let mut manager = ShortcutManager::new();
manager.register_with_options(
Shortcut::ctrl(Key::S),
Box::new(|| true),
ShortcutContext::Global,
ShortcutPriority::Normal,
"Save",
);
manager.register_with_options(
Shortcut::ctrl(Key::O),
Box::new(|| true),
ShortcutContext::Global,
ShortcutPriority::Normal,
"Open",
);
let shortcuts: Vec<_> = manager.shortcuts().collect();
assert_eq!(shortcuts.len(), 2);
}
#[test]
fn test_manager_clear() {
let mut manager = ShortcutManager::new();
manager.register(Shortcut::ctrl(Key::S), Box::new(|| true));
manager.register(Shortcut::ctrl(Key::O), Box::new(|| true));
manager.clear();
assert_eq!(manager.binding_count(), 0);
}
#[test]
fn test_manager_description() {
let mut manager = ShortcutManager::new();
let id = manager.register_with_options(
Shortcut::ctrl(Key::S),
Box::new(|| true),
ShortcutContext::Global,
ShortcutPriority::Normal,
"Save document",
);
assert_eq!(manager.description(id), Some("Save document"));
}
#[test]
fn test_builder() {
let shortcut = ShortcutBuilder::new(Key::S).ctrl().shift().build();
assert_eq!(shortcut.key, Key::S);
assert!(shortcut.modifiers.ctrl);
assert!(shortcut.modifiers.shift);
assert!(!shortcut.modifiers.alt);
}
#[test]
fn test_builder_register() {
let mut manager = ShortcutManager::new();
let id = ShortcutBuilder::new(Key::S)
.ctrl()
.description("Save")
.register(&mut manager, Box::new(|| true));
assert!(manager.is_enabled(id));
assert_eq!(manager.description(id), Some("Save"));
}
#[test]
fn test_builder_for_widget() {
let builder = ShortcutBuilder::new(Key::Enter).for_widget(WidgetId::new(42));
assert_eq!(builder.context, ShortcutContext::Widget(WidgetId::new(42)));
}
}