use iced::keyboard;
use std::sync::Arc;
pub type CommandId = &'static str;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Category {
pub id: &'static str,
pub name: &'static str,
pub order: u32,
}
impl Category {
pub const fn new(id: &'static str, name: &'static str, order: u32) -> Self {
Self { id, name, order }
}
pub const FILE: Category = Category::new("file", "File", 100);
pub const EDIT: Category = Category::new("edit", "Edit", 200);
pub const VIEW: Category = Category::new("view", "View", 300);
pub const GOTO: Category = Category::new("goto", "Go to", 400);
pub const HELP: Category = Category::new("help", "Help", 900);
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Shortcut {
pub key: keyboard::Key,
pub modifiers: keyboard::Modifiers,
}
impl Shortcut {
pub fn new(key: keyboard::Key, modifiers: keyboard::Modifiers) -> Self {
Self { key, modifiers }
}
pub fn cmd(c: char) -> Self {
Self {
key: keyboard::Key::Character(c.to_string().into()),
modifiers: keyboard::Modifiers::COMMAND,
}
}
pub fn cmd_shift(c: char) -> Self {
Self {
key: keyboard::Key::Character(c.to_string().into()),
modifiers: keyboard::Modifiers::COMMAND.union(keyboard::Modifiers::SHIFT),
}
}
pub fn ctrl(c: char) -> Self {
Self {
key: keyboard::Key::Character(c.to_string().into()),
modifiers: keyboard::Modifiers::CTRL,
}
}
pub fn alt(c: char) -> Self {
Self {
key: keyboard::Key::Character(c.to_string().into()),
modifiers: keyboard::Modifiers::ALT,
}
}
pub fn matches(&self, key: &keyboard::Key, modifiers: keyboard::Modifiers) -> bool {
let key_matches = match (&self.key, key) {
(
keyboard::Key::Character(a),
keyboard::Key::Character(b),
) => a.to_lowercase() == b.to_lowercase(),
(a, b) => a == b,
};
key_matches && self.modifiers == modifiers
}
pub fn display(&self) -> String {
let mut parts = Vec::new();
#[cfg(target_os = "macos")]
{
if self.modifiers.control() {
parts.push("Ctrl");
}
if self.modifiers.alt() {
parts.push("Opt");
}
if self.modifiers.shift() {
parts.push("Shift");
}
if self.modifiers.command() {
parts.push("Cmd");
}
}
#[cfg(not(target_os = "macos"))]
{
if self.modifiers.control() || self.modifiers.command() {
parts.push("Ctrl");
}
if self.modifiers.alt() {
parts.push("Alt");
}
if self.modifiers.shift() {
parts.push("Shift");
}
}
let key_str = match &self.key {
keyboard::Key::Character(c) => c.to_uppercase(),
keyboard::Key::Named(named) => format!("{:?}", named),
_ => "?".to_string(),
};
parts.push(&key_str);
parts.join("+")
}
}
#[derive(Clone)]
pub struct Command<Message> {
pub id: CommandId,
pub name: String,
pub description: Option<String>,
pub category: Option<&'static str>,
pub shortcut: Option<Shortcut>,
pub keywords: Vec<String>,
pub enabled: bool,
pub action: CommandAction<Message>,
}
#[derive(Clone)]
pub enum CommandAction<Message> {
Message(Message),
Callback(Arc<dyn Fn() -> Message + Send + Sync>),
Submenu(Vec<Command<Message>>),
}
impl<Message> Command<Message> {
pub fn new(id: CommandId, name: impl Into<String>, action: CommandAction<Message>) -> Self {
Self {
id,
name: name.into(),
description: None,
category: None,
shortcut: None,
keywords: Vec::new(),
enabled: true,
action,
}
}
}
pub struct CommandBuilder<Message> {
id: CommandId,
name: String,
description: Option<String>,
category: Option<&'static str>,
shortcut: Option<Shortcut>,
keywords: Vec<String>,
enabled: bool,
_phantom: std::marker::PhantomData<Message>,
}
impl<Message> CommandBuilder<Message> {
pub fn new(id: CommandId, name: impl Into<String>) -> Self {
Self {
id,
name: name.into(),
description: None,
category: None,
shortcut: None,
keywords: Vec::new(),
enabled: true,
_phantom: std::marker::PhantomData,
}
}
pub fn description(mut self, desc: impl Into<String>) -> Self {
self.description = Some(desc.into());
self
}
pub fn category(mut self, category: &'static str) -> Self {
self.category = Some(category);
self
}
pub fn shortcut(mut self, shortcut: Shortcut) -> Self {
self.shortcut = Some(shortcut);
self
}
pub fn keyword(mut self, keyword: impl Into<String>) -> Self {
self.keywords.push(keyword.into());
self
}
pub fn keywords(mut self, keywords: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.keywords.extend(keywords.into_iter().map(Into::into));
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn action(self, message: Message) -> Command<Message>
where
Message: Clone,
{
Command {
id: self.id,
name: self.name,
description: self.description,
category: self.category,
shortcut: self.shortcut,
keywords: self.keywords,
enabled: self.enabled,
action: CommandAction::Message(message),
}
}
pub fn submenu(self, commands: Vec<Command<Message>>) -> Command<Message> {
Command {
id: self.id,
name: self.name,
description: self.description,
category: self.category,
shortcut: self.shortcut,
keywords: self.keywords,
enabled: self.enabled,
action: CommandAction::Submenu(commands),
}
}
}
pub fn command<Message>(id: CommandId, name: impl Into<String>) -> CommandBuilder<Message> {
CommandBuilder::new(id, name)
}
pub fn find_by_shortcut<'a, Message>(
commands: &'a [Command<Message>],
key: &keyboard::Key,
modifiers: keyboard::Modifiers,
) -> Option<(usize, &'a Command<Message>)> {
commands.iter().enumerate().find(|(_, cmd)| {
cmd.shortcut
.as_ref()
.map(|s| s.matches(key, modifiers))
.unwrap_or(false)
})
}