1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub struct CommandId(u64);
38
39impl CommandId {
40 pub fn new<T: std::hash::Hash>(value: T) -> Self {
46 use {
47 rustc_hash::FxHasher,
48 std::hash::{BuildHasher, BuildHasherDefault},
49 };
50 Self(BuildHasherDefault::<FxHasher>::default().hash_one(value))
51 }
52
53 pub fn raw(self) -> u64 { self.0 }
60
61 pub fn from_raw(v: u64) -> Self { Self(v) }
63}
64
65#[derive(Debug, Clone)]
71pub struct CommandSpec {
72 pub id: CommandId,
74 pub label: String,
76 pub description: Option<String>,
78 pub shortcut_hint: Option<String>,
81}
82
83impl CommandSpec {
84 pub fn new(id: CommandId, label: impl Into<String>) -> Self {
86 Self {
87 id,
88 label: label.into(),
89 description: None,
90 shortcut_hint: None,
91 }
92 }
93
94 pub fn with_description(mut self, desc: impl Into<String>) -> Self {
96 self.description = Some(desc.into());
97 self
98 }
99
100 pub fn with_shortcut_hint(mut self, hint: impl Into<String>) -> Self {
102 self.shortcut_hint = Some(hint.into());
103 self
104 }
105}
106
107#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
112pub enum CommandState {
113 #[default]
115 Enabled,
116 Disabled,
118 Hidden,
120}
121
122impl CommandState {
123 pub fn is_enabled(self) -> bool { self == CommandState::Enabled }
125
126 pub fn is_visible(self) -> bool { self != CommandState::Hidden }
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Eq)]
132pub enum CommandSource {
133 Keyboard,
135 Menu,
137 Button,
139 Programmatic,
141}
142
143#[derive(Debug, Clone)]
148pub struct CommandTriggered {
149 pub id: CommandId,
151 pub source: CommandSource,
153}
154
155impl CommandTriggered {
156 pub fn new(id: CommandId, source: CommandSource) -> Self { Self { id, source } }
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163
164 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
165 enum AppCmd {
166 ShowHelp,
167 Save,
168 Quit,
169 }
170
171 #[test]
172 fn command_id_same_value_is_equal() {
173 let a = CommandId::new(AppCmd::ShowHelp);
174 let b = CommandId::new(AppCmd::ShowHelp);
175 assert_eq!(a, b);
176 }
177
178 #[test]
179 fn command_id_different_variants_are_not_equal() {
180 let a = CommandId::new(AppCmd::Save);
181 let b = CommandId::new(AppCmd::Quit);
182 assert_ne!(a, b);
183 }
184
185 #[test]
186 fn command_id_raw_roundtrip() {
187 let id = CommandId::new(AppCmd::Save);
188 assert_eq!(CommandId::from_raw(id.raw()), id);
189 }
190
191 #[test]
192 fn command_id_hashable_in_map() {
193 let mut map = std::collections::HashMap::new();
194 map.insert(CommandId::new(AppCmd::ShowHelp), "help");
195 map.insert(CommandId::new(AppCmd::Save), "save");
196 assert_eq!(map[&CommandId::new(AppCmd::ShowHelp)], "help");
197 assert_eq!(map[&CommandId::new(AppCmd::Save)], "save");
198 }
199
200 #[test]
201 fn command_spec_builder_chain() {
202 let id = CommandId::new(AppCmd::Save);
203 let spec = CommandSpec::new(id, "Save")
204 .with_description("Save the current file")
205 .with_shortcut_hint("Ctrl+S");
206 assert_eq!(spec.id, id);
207 assert_eq!(spec.label, "Save");
208 assert_eq!(spec.description.as_deref(), Some("Save the current file"));
209 assert_eq!(spec.shortcut_hint.as_deref(), Some("Ctrl+S"));
210 }
211
212 #[test]
213 fn command_spec_minimal_has_no_optional_fields() {
214 let spec = CommandSpec::new(CommandId::new(AppCmd::Quit), "Quit");
215 assert_eq!(spec.label, "Quit");
216 assert!(spec.description.is_none());
217 assert!(spec.shortcut_hint.is_none());
218 }
219
220 #[test]
221 fn command_state_is_enabled() {
222 assert!(CommandState::Enabled.is_enabled());
223 assert!(!CommandState::Disabled.is_enabled());
224 assert!(!CommandState::Hidden.is_enabled());
225 }
226
227 #[test]
228 fn command_state_is_visible() {
229 assert!(CommandState::Enabled.is_visible());
230 assert!(CommandState::Disabled.is_visible());
231 assert!(!CommandState::Hidden.is_visible());
232 }
233
234 #[test]
235 fn command_state_default_is_enabled() {
236 assert_eq!(CommandState::default(), CommandState::Enabled);
237 }
238
239 #[test]
240 fn command_triggered_stores_id_and_source() {
241 let id = CommandId::new(AppCmd::Save);
242 let triggered = CommandTriggered::new(id, CommandSource::Keyboard);
243 assert_eq!(triggered.id, id);
244 assert_eq!(triggered.source, CommandSource::Keyboard);
245 }
246
247 #[test]
248 fn command_source_variants_are_distinct() {
249 assert_ne!(CommandSource::Keyboard, CommandSource::Menu);
250 assert_ne!(CommandSource::Button, CommandSource::Programmatic);
251 }
252}