#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CommandId(u64);
impl CommandId {
pub fn new<T: std::hash::Hash>(value: T) -> Self {
use {
rustc_hash::FxHasher,
std::hash::{BuildHasher, BuildHasherDefault},
};
Self(BuildHasherDefault::<FxHasher>::default().hash_one(value))
}
pub fn raw(self) -> u64 { self.0 }
pub fn from_raw(v: u64) -> Self { Self(v) }
}
#[derive(Debug, Clone)]
pub struct CommandSpec {
pub id: CommandId,
pub label: String,
pub description: Option<String>,
pub shortcut_hint: Option<String>,
}
impl CommandSpec {
pub fn new(id: CommandId, label: impl Into<String>) -> Self {
Self {
id,
label: label.into(),
description: None,
shortcut_hint: None,
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn with_shortcut_hint(mut self, hint: impl Into<String>) -> Self {
self.shortcut_hint = Some(hint.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum CommandState {
#[default]
Enabled,
Disabled,
Hidden,
}
impl CommandState {
pub fn is_enabled(self) -> bool { self == CommandState::Enabled }
pub fn is_visible(self) -> bool { self != CommandState::Hidden }
}
#[derive(Debug, Default)]
pub struct CommandRegistry<C: Copy + std::hash::Hash + Eq + Into<CommandId>> {
specs: std::collections::HashMap<CommandId, CommandSpec>,
states: std::collections::HashMap<CommandId, CommandState>,
_phantom: std::marker::PhantomData<C>,
}
impl<C: Copy + std::hash::Hash + Eq + Into<CommandId>> CommandRegistry<C> {
pub fn new() -> Self {
Self {
specs: std::collections::HashMap::new(),
states: std::collections::HashMap::new(),
_phantom: std::marker::PhantomData,
}
}
pub fn register(&mut self, cmd: C, spec: CommandSpec) -> &mut Self {
let id: CommandId = cmd.into();
assert_eq!(
spec.id, id,
"CommandSpec::id does not match the registered command; \
build the spec with CommandId::new(cmd) or CommandSpec::new(id, label)"
);
self.states.entry(id).or_insert(CommandState::Enabled);
self.specs.insert(id, spec);
self
}
pub fn with(mut self, cmd: C, spec: CommandSpec) -> Self {
self.register(cmd, spec);
self
}
pub fn spec(&self, cmd: C) -> Option<&CommandSpec> { self.specs.get(&cmd.into()) }
pub fn spec_by_id(&self, id: CommandId) -> Option<&CommandSpec> { self.specs.get(&id) }
pub fn state(&self, cmd: C) -> Option<CommandState> { self.states.get(&cmd.into()).copied() }
pub fn state_by_id(&self, id: CommandId) -> Option<CommandState> {
self.states.get(&id).copied()
}
pub fn set_state(&mut self, cmd: C, state: CommandState) {
let id: CommandId = cmd.into();
if self.specs.contains_key(&id) {
self.states.insert(id, state);
}
}
pub fn set_state_by_id(&mut self, id: CommandId, state: CommandState) {
if self.specs.contains_key(&id) {
self.states.insert(id, state);
}
}
pub fn iter_specs(&self) -> impl Iterator<Item = (CommandId, &CommandSpec)> {
self.specs.iter().map(|(&id, spec)| (id, spec))
}
pub fn spec_by_id_mut(&mut self, id: CommandId) -> Option<&mut CommandSpec> {
self.specs.get_mut(&id)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandSource {
Keyboard,
Menu,
Button,
Programmatic,
}
#[derive(Debug, Clone)]
pub struct CommandTriggered {
pub id: CommandId,
pub source: CommandSource,
}
impl CommandTriggered {
pub fn new(id: CommandId, source: CommandSource) -> Self { Self { id, source } }
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum AppCmd {
ShowHelp,
Save,
Quit,
}
#[test]
fn command_id_same_value_is_equal() {
let a = CommandId::new(AppCmd::ShowHelp);
let b = CommandId::new(AppCmd::ShowHelp);
assert_eq!(a, b);
}
#[test]
fn command_id_different_variants_are_not_equal() {
let a = CommandId::new(AppCmd::Save);
let b = CommandId::new(AppCmd::Quit);
assert_ne!(a, b);
}
#[test]
fn command_id_raw_roundtrip() {
let id = CommandId::new(AppCmd::Save);
assert_eq!(CommandId::from_raw(id.raw()), id);
}
#[test]
fn command_id_hashable_in_map() {
let mut map = std::collections::HashMap::new();
map.insert(CommandId::new(AppCmd::ShowHelp), "help");
map.insert(CommandId::new(AppCmd::Save), "save");
assert_eq!(map[&CommandId::new(AppCmd::ShowHelp)], "help");
assert_eq!(map[&CommandId::new(AppCmd::Save)], "save");
}
#[test]
fn command_spec_builder_chain() {
let id = CommandId::new(AppCmd::Save);
let spec = CommandSpec::new(id, "Save")
.with_description("Save the current file")
.with_shortcut_hint("Ctrl+S");
assert_eq!(spec.id, id);
assert_eq!(spec.label, "Save");
assert_eq!(spec.description.as_deref(), Some("Save the current file"));
assert_eq!(spec.shortcut_hint.as_deref(), Some("Ctrl+S"));
}
#[test]
fn command_spec_minimal_has_no_optional_fields() {
let spec = CommandSpec::new(CommandId::new(AppCmd::Quit), "Quit");
assert_eq!(spec.label, "Quit");
assert!(spec.description.is_none());
assert!(spec.shortcut_hint.is_none());
}
#[test]
fn command_state_is_enabled() {
assert!(CommandState::Enabled.is_enabled());
assert!(!CommandState::Disabled.is_enabled());
assert!(!CommandState::Hidden.is_enabled());
}
#[test]
fn command_state_is_visible() {
assert!(CommandState::Enabled.is_visible());
assert!(CommandState::Disabled.is_visible());
assert!(!CommandState::Hidden.is_visible());
}
#[test]
fn command_state_default_is_enabled() {
assert_eq!(CommandState::default(), CommandState::Enabled);
}
#[test]
fn command_triggered_stores_id_and_source() {
let id = CommandId::new(AppCmd::Save);
let triggered = CommandTriggered::new(id, CommandSource::Keyboard);
assert_eq!(triggered.id, id);
assert_eq!(triggered.source, CommandSource::Keyboard);
}
#[test]
fn command_source_variants_are_distinct() {
assert_ne!(CommandSource::Keyboard, CommandSource::Menu);
assert_ne!(CommandSource::Button, CommandSource::Programmatic);
}
impl From<AppCmd> for CommandId {
fn from(c: AppCmd) -> Self { CommandId::new(c) }
}
fn make_spec(cmd: AppCmd, label: &str) -> CommandSpec {
CommandSpec::new(CommandId::new(cmd), label)
}
#[test]
fn registry_register_and_query_spec() {
let mut reg = CommandRegistry::new();
reg.register(AppCmd::Save, make_spec(AppCmd::Save, "Save"));
assert!(reg.spec(AppCmd::Save).is_some());
assert_eq!(reg.spec(AppCmd::Save).unwrap().label, "Save");
}
#[test]
fn registry_unregistered_returns_none() {
let reg: CommandRegistry<AppCmd> = CommandRegistry::new();
assert!(reg.spec(AppCmd::Quit).is_none());
assert!(reg.state(AppCmd::Quit).is_none());
}
#[test]
fn registry_default_state_is_enabled() {
let mut reg = CommandRegistry::new();
reg.register(AppCmd::Save, make_spec(AppCmd::Save, "Save"));
assert_eq!(reg.state(AppCmd::Save), Some(CommandState::Enabled));
}
#[test]
fn registry_set_state_updates_value() {
let mut reg = CommandRegistry::new();
reg.register(AppCmd::Save, make_spec(AppCmd::Save, "Save"));
reg.set_state(AppCmd::Save, CommandState::Disabled);
assert_eq!(reg.state(AppCmd::Save), Some(CommandState::Disabled));
}
#[test]
fn registry_set_state_unregistered_is_noop() {
let mut reg: CommandRegistry<AppCmd> = CommandRegistry::new();
reg.set_state(AppCmd::Quit, CommandState::Hidden);
assert!(reg.state(AppCmd::Quit).is_none());
}
#[test]
fn registry_builder_chain() {
let reg = CommandRegistry::new()
.with(AppCmd::ShowHelp, make_spec(AppCmd::ShowHelp, "Help"))
.with(AppCmd::Save, make_spec(AppCmd::Save, "Save"))
.with(AppCmd::Quit, make_spec(AppCmd::Quit, "Quit"));
assert!(reg.spec(AppCmd::ShowHelp).is_some());
assert!(reg.spec(AppCmd::Save).is_some());
assert!(reg.spec(AppCmd::Quit).is_some());
}
#[test]
fn registry_register_id_mismatch_panics() {
let mut reg = CommandRegistry::new();
let wrong_id = CommandId::new(AppCmd::Quit);
let spec = CommandSpec::new(wrong_id, "Save");
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
reg.register(AppCmd::Save, spec);
}));
assert!(result.is_err(), "expected panic on id mismatch");
}
#[test]
fn registry_iter_specs_covers_all_registered() {
let reg = CommandRegistry::new()
.with(AppCmd::Save, make_spec(AppCmd::Save, "Save"))
.with(AppCmd::Quit, make_spec(AppCmd::Quit, "Quit"));
let ids: Vec<CommandId> = reg.iter_specs().map(|(id, _)| id).collect();
assert_eq!(ids.len(), 2);
assert!(ids.contains(&CommandId::new(AppCmd::Save)));
assert!(ids.contains(&CommandId::new(AppCmd::Quit)));
}
#[test]
fn registry_spec_by_id() {
let mut reg = CommandRegistry::new();
reg.register(AppCmd::Save, make_spec(AppCmd::Save, "Save"));
let id = CommandId::new(AppCmd::Save);
assert!(reg.spec_by_id(id).is_some());
assert_eq!(reg.spec_by_id(id).unwrap().label, "Save");
}
}