use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use crate::ui::KeyChord;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ActionInfo {
pub name: String,
pub chord: Option<KeyChord>,
}
struct Entry {
name: String,
chord: Option<KeyChord>,
run: Box<dyn FnMut()>,
}
#[derive(Default)]
struct Registry {
entries: Vec<Entry>,
by_chord: HashMap<KeyChord, usize>,
}
#[derive(Clone, Default)]
pub struct Actions {
inner: Rc<RefCell<Registry>>,
}
impl Actions {
pub fn new() -> Actions {
Actions::default()
}
pub fn register(
&self,
name: impl Into<String>,
chord: Option<KeyChord>,
run: impl FnMut() + 'static,
) -> bool {
let name = name.into();
let mut reg = self.inner.borrow_mut();
if reg.entries.iter().any(|e| e.name == name) {
return false;
}
if let Some(c) = chord {
if reg.by_chord.contains_key(&c) {
return false;
}
let idx = reg.entries.len();
reg.by_chord.insert(c, idx);
}
reg.entries.push(Entry {
name,
chord,
run: Box::new(run),
});
true
}
pub fn unregister(&self, name: &str) -> bool {
let mut reg = self.inner.borrow_mut();
let Some(i) = reg.entries.iter().position(|e| e.name == name) else {
return false;
};
reg.entries.remove(i);
reg.by_chord.clear();
let chords: Vec<(KeyChord, usize)> = reg
.entries
.iter()
.enumerate()
.filter_map(|(i, e)| e.chord.map(|c| (c, i)))
.collect();
reg.by_chord.extend(chords);
true
}
pub fn rebind(&self, name: &str, chord: Option<KeyChord>) -> bool {
let mut reg = self.inner.borrow_mut();
let Some(i) = reg.entries.iter().position(|e| e.name == name) else {
return false;
};
if let Some(c) = chord {
if reg.by_chord.get(&c).is_some_and(|&j| j != i) {
return false;
}
}
if let Some(old) = reg.entries[i].chord {
reg.by_chord.remove(&old);
}
reg.entries[i].chord = chord;
if let Some(c) = chord {
reg.by_chord.insert(c, i);
}
true
}
pub fn run(&self, name: &str) -> bool {
let taken = {
let mut reg = self.inner.borrow_mut();
reg.entries.iter().position(|e| e.name == name).map(|i| {
(
i,
std::mem::replace(&mut reg.entries[i].run, Box::new(|| {})),
)
})
};
let Some((i, mut run)) = taken else {
return false;
};
run();
let mut reg = self.inner.borrow_mut();
if let Some(e) = reg.entries.get_mut(i) {
e.run = run;
}
true
}
pub fn dispatch_chord(&self, chord: KeyChord) -> bool {
let name = {
let reg = self.inner.borrow();
reg.by_chord
.get(&chord)
.map(|&i| reg.entries[i].name.clone())
};
match name {
Some(n) => self.run(&n),
None => false,
}
}
pub fn list(&self) -> Vec<ActionInfo> {
self.inner
.borrow()
.entries
.iter()
.map(|e| ActionInfo {
name: e.name.clone(),
chord: e.chord,
})
.collect()
}
pub fn is_empty(&self) -> bool {
self.inner.borrow().entries.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ui::{Key, Mods};
use std::cell::Cell;
fn chord(key: Key, mods: Mods) -> KeyChord {
KeyChord { key, mods }
}
#[test]
fn register_dispatch_and_collision_rules() {
let actions = Actions::new();
let fired: Rc<Cell<u32>> = Rc::new(Cell::new(0));
let f = fired.clone();
let save = chord(Key::Char('s'), Mods::CTRL);
assert!(actions.register("file.save", Some(save), move || f.set(f.get() + 1)));
assert!(
!actions.register("file.save", None, || {}),
"duplicate name refused"
);
assert!(
!actions.register("other", Some(save), || {}),
"duplicate chord refused"
);
assert!(actions.dispatch_chord(save));
assert_eq!(fired.get(), 1);
assert!(!actions.dispatch_chord(chord(Key::Char('x'), Mods::NONE)));
assert!(actions.run("file.save"));
assert_eq!(fired.get(), 2);
assert_eq!(
actions.list(),
vec![ActionInfo {
name: "file.save".into(),
chord: Some(save)
}]
);
}
#[test]
fn rebind_and_unregister_keep_the_chord_map_honest() {
let actions = Actions::new();
let a = chord(Key::Char('a'), Mods::CTRL);
let b = chord(Key::Char('b'), Mods::CTRL);
assert!(actions.register("one", Some(a), || {}));
assert!(actions.register("two", None, || {}));
assert!(actions.rebind("one", Some(b)));
assert!(!actions.dispatch_chord(a), "old chord unbound");
assert!(actions.dispatch_chord(b));
assert!(actions.rebind("two", Some(a)), "freed chord reusable");
assert!(!actions.rebind("two", Some(b)), "occupied chord refused");
assert!(actions.unregister("one"));
assert!(!actions.dispatch_chord(b), "unregistered chord gone");
assert!(
actions.dispatch_chord(a),
"survivor still routed after index shift"
);
}
#[test]
fn action_may_reenter_the_registry_while_running() {
let actions = Actions::new();
let inner = actions.clone();
let observed: Rc<Cell<usize>> = Rc::new(Cell::new(0));
let o = observed.clone();
assert!(actions.register("palette.open", None, move || {
o.set(inner.list().len());
}));
assert!(actions.run("palette.open"));
assert_eq!(observed.get(), 1);
}
}