use iced::keyboard::key::{Named, Physical};
use iced::keyboard::{Key, Modifiers};
use iced::mouse;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyAction {
SelectAll,
ClearSelection,
CloneSelection,
DeleteSelection,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ComboKey {
Char(char),
Named(Named),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyCombo {
pub key: ComboKey,
pub modifiers: Modifiers,
}
impl KeyCombo {
pub fn new(key: ComboKey, modifiers: Modifiers) -> Self {
Self { key, modifiers }
}
pub fn command(c: char) -> Self {
Self::new(ComboKey::Char(c), Modifiers::COMMAND)
}
pub fn alt(c: char) -> Self {
Self::new(ComboKey::Char(c), Modifiers::ALT)
}
pub fn bare(key: ComboKey) -> Self {
Self::new(key, Modifiers::empty())
}
pub fn matches(&self, key: &Key, physical: Physical, modifiers: Modifiers) -> bool {
if self.modifiers != modifiers {
return false;
}
match self.key {
ComboKey::Char(target) => match key.to_latin(physical) {
Some(latin) => latin.eq_ignore_ascii_case(&target),
None => match key {
Key::Character(s) => character_matches(s, target),
_ => false,
},
},
ComboKey::Named(named) => matches!(key, Key::Named(n) if *n == named),
}
}
}
fn character_matches(s: &str, target: char) -> bool {
let mut chars = s.chars();
match (chars.next(), chars.next()) {
(Some(c), None) => c.eq_ignore_ascii_case(&target),
_ => false,
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct Keymap {
pub select_all: Option<KeyCombo>,
pub clear_selection: Option<KeyCombo>,
pub clone_selection: Option<KeyCombo>,
pub delete_selection: Vec<KeyCombo>,
pub pan_button: mouse::Button,
pub edge_cut_modifiers: Modifiers,
pub multi_select_modifiers: Modifiers,
}
impl Default for Keymap {
fn default() -> Self {
#[cfg(not(target_arch = "wasm32"))]
let clone_selection = Some(KeyCombo::command('d'));
#[cfg(target_arch = "wasm32")]
let clone_selection = Some(KeyCombo::alt('d'));
#[cfg(not(target_arch = "wasm32"))]
let delete_selection = vec![
KeyCombo::bare(ComboKey::Named(Named::Delete)),
KeyCombo::bare(ComboKey::Named(Named::Backspace)),
];
#[cfg(target_arch = "wasm32")]
let delete_selection = vec![KeyCombo::bare(ComboKey::Named(Named::Delete))];
Self {
select_all: Some(KeyCombo::command('a')),
clear_selection: Some(KeyCombo::bare(ComboKey::Named(Named::Escape))),
clone_selection,
delete_selection,
pan_button: mouse::Button::Right,
edge_cut_modifiers: Modifiers::COMMAND,
multi_select_modifiers: Modifiers::SHIFT,
}
}
}
impl Keymap {
pub fn none() -> Self {
Self {
select_all: None,
clear_selection: None,
clone_selection: None,
delete_selection: Vec::new(),
..Self::default()
}
}
pub fn key_action(
&self,
key: &Key,
physical: Physical,
modifiers: Modifiers,
) -> Option<KeyAction> {
let hit =
|combo: Option<KeyCombo>| combo.is_some_and(|c| c.matches(key, physical, modifiers));
if hit(self.select_all) {
return Some(KeyAction::SelectAll);
}
if hit(self.clear_selection) {
return Some(KeyAction::ClearSelection);
}
if hit(self.clone_selection) {
return Some(KeyAction::CloneSelection);
}
if self
.delete_selection
.iter()
.any(|combo| combo.matches(key, physical, modifiers))
{
return Some(KeyAction::DeleteSelection);
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use iced::keyboard::key::Code;
#[test]
fn select_all_resolves_from_default_combo() {
let keymap = Keymap::default();
let key = Key::Character("a".into());
let physical = Physical::Code(Code::KeyA);
assert_eq!(
keymap.key_action(&key, physical, Modifiers::COMMAND),
Some(KeyAction::SelectAll)
);
}
#[test]
fn clone_selection_resolves_from_default_combo() {
let keymap = Keymap::default();
let key = Key::Character("d".into());
let physical = Physical::Code(Code::KeyD);
assert_eq!(
keymap.key_action(&key, physical, Modifiers::COMMAND),
Some(KeyAction::CloneSelection)
);
}
#[test]
fn exact_modifier_match_rejects_extra_modifiers() {
let keymap = Keymap::default();
let key = Key::Character("a".into());
let physical = Physical::Code(Code::KeyA);
assert_eq!(
keymap.key_action(&key, physical, Modifiers::COMMAND | Modifiers::SHIFT),
None
);
}
#[test]
fn char_combo_is_layout_independent_via_to_latin() {
let keymap = Keymap::default();
let key = Key::Character("ะด".into());
let physical = Physical::Code(Code::KeyD);
assert_eq!(
keymap.key_action(&key, physical, Modifiers::COMMAND),
Some(KeyAction::CloneSelection)
);
}
#[test]
fn escape_resolves_clear_selection() {
let keymap = Keymap::default();
let key = Key::Named(Named::Escape);
let physical = Physical::Code(Code::Escape);
assert_eq!(
keymap.key_action(&key, physical, Modifiers::empty()),
Some(KeyAction::ClearSelection)
);
}
#[test]
fn delete_and_backspace_both_resolve_delete_selection() {
let keymap = Keymap::default();
let delete = Key::Named(Named::Delete);
assert_eq!(
keymap.key_action(&delete, Physical::Code(Code::Delete), Modifiers::empty()),
Some(KeyAction::DeleteSelection)
);
let backspace = Key::Named(Named::Backspace);
assert_eq!(
keymap.key_action(
&backspace,
Physical::Code(Code::Backspace),
Modifiers::empty()
),
Some(KeyAction::DeleteSelection)
);
}
#[test]
fn none_resolves_nothing() {
let keymap = Keymap::none();
let a = Key::Character("a".into());
assert_eq!(
keymap.key_action(&a, Physical::Code(Code::KeyA), Modifiers::COMMAND),
None
);
let escape = Key::Named(Named::Escape);
assert_eq!(
keymap.key_action(&escape, Physical::Code(Code::Escape), Modifiers::empty()),
None
);
let delete = Key::Named(Named::Delete);
assert_eq!(
keymap.key_action(&delete, Physical::Code(Code::Delete), Modifiers::empty()),
None
);
}
#[test]
fn disabling_one_binding_leaves_the_others_working() {
let keymap = Keymap {
select_all: None,
..Keymap::default()
};
let a = Key::Character("a".into());
assert_eq!(
keymap.key_action(&a, Physical::Code(Code::KeyA), Modifiers::COMMAND),
None
);
let escape = Key::Named(Named::Escape);
assert_eq!(
keymap.key_action(&escape, Physical::Code(Code::Escape), Modifiers::empty()),
Some(KeyAction::ClearSelection)
);
}
}