Files
craft-libs/crates/keymap/tests/editor.rs
T

201 lines
8.3 KiB
Rust

use craft_keymap::editor::{MAX_UNDO_BYTES, holders, search};
use craft_keymap::{
AssignError, Bindable, Binding, Chord, CommandRef, DOUBLE_PRESS, EditSession, Holder, Keymap, MAX_BINDING_TEXT, MAX_BINDINGS, MAX_COMMANDS,
MAX_UNDO, Overrides, Platform, Policy, Recorded, Recorder, Scope, Scopes, Trigger,
};
fn chord(text: &str) -> Chord {
Chord::parse(text).unwrap_or_else(|e| panic!("{text}: {e}"))
}
fn b(text: &str) -> Binding {
Binding::parse(text).unwrap_or_else(|e| panic!("{text}: {e}"))
}
fn keymap(platform: Platform) -> Keymap {
let catalog = vec![
Bindable::new("file.save", "Save", Scope::app()).with_default(b("Cmd+S")).with_path(vec!["File".into(), "Save".into()]),
Bindable::new("edit.copy", "Copy", Scope::app()).with_default(b("Cmd+C")).with_alias("Kopieren"),
Bindable::new("view.zoomIn", "Zoom In", Scope::app()).with_default(b("Cmd+=")),
Bindable::new("tool.brush", "Brush", Scope::app()).with_default(b("B")).with_path(vec!["Tools".into(), "Paint".into()]),
];
Keymap::new(catalog, Scopes::new(), platform)
}
fn keys(km: &Keymap, id: &str) -> Vec<String> {
km.bindings(&CommandRef::new(id)).iter().map(|b| b.to_string()).collect()
}
#[test]
fn edits_undo_redo_and_cancel() {
let mut km = keymap(Platform::Windows);
let mut session = EditSession::begin(&km);
let save = CommandRef::new("file.save");
let assigned = session.edit(&mut km, |km| km.assign(&save, b("Cmd+Shift+S"), Policy::Steal));
assert!(assigned.is_ok());
session.edit(&mut km, |km| km.clear(&CommandRef::new("edit.copy")));
assert_eq!(keys(&km, "file.save"), ["Cmd+S", "Cmd+Shift+S"]);
assert!(keys(&km, "edit.copy").is_empty());
assert!(session.is_modified(&km));
// An edit that changes nothing is no undo step.
session.edit(&mut km, |km| km.reset(&CommandRef::new("tool.brush")));
assert!(session.undo(&mut km));
assert_eq!(keys(&km, "edit.copy"), ["Cmd+C"]);
assert!(session.undo(&mut km));
assert_eq!(keys(&km, "file.save"), ["Cmd+S"]);
assert!(!session.undo(&mut km));
assert!(!session.is_modified(&km));
assert!(session.redo(&mut km));
assert_eq!(keys(&km, "file.save"), ["Cmd+S", "Cmd+Shift+S"]);
// A new edit drops what could be redone.
session.edit(&mut km, |km| km.clear(&CommandRef::new("tool.brush")));
assert!(!session.redo(&mut km));
session.cancel(&mut km);
assert!(!session.is_modified(&km));
assert_eq!(keys(&km, "file.save"), ["Cmd+S"]);
assert_eq!(keys(&km, "tool.brush"), ["B"]);
assert!(!session.can_undo());
}
#[test]
fn undo_keeps_the_newest_steps() {
let mut km = keymap(Platform::Windows);
let mut session = EditSession::begin(&km);
let brush = CommandRef::new("tool.brush");
// Each edit changes the key: N, B, N, … (B is the default).
let edits = MAX_UNDO + 11;
for i in 0..edits {
let key = if i % 2 == 0 { "N" } else { "B" };
assert!(session.edit(&mut km, |km| km.set_bindings(&brush, vec![b(key)], Policy::Keep)).is_ok());
}
let mut steps = 0;
while session.undo(&mut km) {
steps += 1;
}
assert_eq!(steps, MAX_UNDO);
// The 11 oldest steps are gone: undo stops after the 11th edit, which set N.
assert_eq!(keys(&km, "tool.brush"), ["N"]);
assert!(session.is_modified(&km));
}
#[test]
fn undo_history_is_capped_by_size() {
let mut km = keymap(Platform::Windows);
let mut session = EditSession::begin(&km);
// Full-size snapshots of entries for commands the catalog does not have.
let big = |tag: char| {
let mut o = Overrides::new();
for n in 0..MAX_COMMANDS {
assert!(o.insert(format!("junk.{n}"), vec![tag.to_string().repeat(MAX_BINDING_TEXT); MAX_BINDINGS]));
}
o
};
let (a, b) = (big('a'), big('b'));
let string = size_of::<String>();
let per = a.iter().map(|(k, t)| string + 64 + k.len() + t.iter().map(|t| string + 16 + t.len()).sum::<usize>()).sum::<usize>();
let edits = 2 * MAX_UNDO_BYTES / per + 4;
for n in 0..edits {
let next = if n % 2 == 0 { a.clone() } else { b.clone() };
session.edit(&mut km, |km| km.set_overrides(next));
}
let mut steps = 0;
while session.undo(&mut km) {
steps += 1;
}
assert!(steps >= 2 && steps <= MAX_UNDO_BYTES / per, "{steps} steps of {per} bytes");
}
#[test]
fn recorder_turns_presses_into_triggers() {
let mut r = Recorder::new();
let a = chord("A");
assert_eq!(r.press(a, 1.0), Recorded::Single(Trigger::single(a)));
assert_eq!(r.press(a, 1.0 + DOUBLE_PRESS / 2.0), Recorded::Double(Trigger::double(a)));
// The press after a double starts over.
assert_eq!(r.press(a, 1.4), Recorded::Single(Trigger::single(a)));
// Too late for a double.
assert_eq!(r.press(a, 1.4 + DOUBLE_PRESS + 0.1), Recorded::Single(Trigger::single(a)));
// Another chord in between.
assert_eq!(r.press(chord("B"), 3.0), Recorded::Single(Trigger::single(chord("B"))));
assert_eq!(r.press(a, 3.1), Recorded::Single(Trigger::single(a)));
assert_eq!(r.press(chord("Escape"), 3.2), Recorded::Cancel);
let shift_escape = chord("Shift+Escape");
assert_eq!(r.press(shift_escape, 3.3), Recorded::Single(Trigger::single(shift_escape)));
// Two presses in one egui frame share its time.
assert_eq!(r.press(a, 4.0), Recorded::Single(Trigger::single(a)));
assert_eq!(r.press(a, 4.0), Recorded::Double(Trigger::double(a)));
}
#[test]
fn search_reads_labels_aliases_paths_ids_and_keys() {
let ids = |km: &Keymap, q: &str, p: Platform| search(km, q, p).iter().map(|b| b.command.id.clone()).collect::<Vec<_>>();
let win = keymap(Platform::Windows);
let mac = keymap(Platform::Mac);
assert_eq!(ids(&win, "zoom", Platform::Windows), ["view.zoomIn"]);
assert_eq!(ids(&win, "KOPIEREN", Platform::Windows), ["edit.copy"]);
assert_eq!(ids(&win, "paint", Platform::Windows), ["tool.brush"]);
assert_eq!(ids(&win, "file.sa", Platform::Windows), ["file.save"]);
assert_eq!(ids(&win, "ctrl+s", Platform::Windows), ["file.save"]);
assert_eq!(ids(&win, "cmd+c", Platform::Windows), ["edit.copy"]);
assert_eq!(ids(&mac, "⌘s", Platform::Mac), ["file.save"]);
assert_eq!(ids(&mac, "⌘s", Platform::Windows), Vec::<String>::new());
assert_eq!(ids(&mac, " ", Platform::Mac).len(), 4);
}
#[test]
fn a_dialog_asks_before_stealing_and_undoes_each_answer() {
let mut km = keymap(Platform::Mac);
let mut session = EditSession::begin(&km);
let save = CommandRef::new("file.save");
let copy = CommandRef::new("edit.copy");
let held = vec![Holder { command: copy.clone(), binding: b("Cmd+C") }];
// While recording: who else has the key.
assert_eq!(holders(&km, &save, &b("Cmd+C")), held);
assert!(holders(&km, &copy, &b("Cmd+C")).is_empty(), "its own key");
assert_eq!(session.assign(&mut km, &save, b("Cmd+C"), Policy::Refuse), Err(AssignError::Taken(held.clone())));
assert!(!session.can_undo(), "a refused assignment changes nothing");
// Keep: both hold it.
let kept = session.assign(&mut km, &save, b("Cmd+C"), Policy::Keep).unwrap();
assert_eq!(kept.clashes, held);
assert_eq!(keys(&km, "edit.copy"), ["Cmd+C"]);
assert!(session.undo(&mut km));
// Steal: the other command loses it.
let stolen = session.assign(&mut km, &save, b("Cmd+C"), Policy::Steal).unwrap();
assert_eq!(stolen.taken_from, held);
assert!(keys(&km, "edit.copy").is_empty());
assert_eq!(keys(&km, "file.save"), ["Cmd+S", "Cmd+C"]);
// Recording over one key of the list replaces it in place.
session.replace(&mut km, &save, &b("Cmd+S"), b("Cmd+Shift+S"), Policy::Refuse).unwrap();
assert_eq!(keys(&km, "file.save"), ["Cmd+Shift+S", "Cmd+C"]);
assert!(session.remove(&mut km, &save, &b("Cmd+C")));
assert!(session.clear(&mut km, &CommandRef::new("tool.brush")));
assert!(session.reset(&mut km, &save));
assert_eq!(keys(&km, "file.save"), ["Cmd+S"]);
session.reset_all(&mut km);
assert!(km.overrides().is_empty());
assert!(!session.is_modified(&km));
// One undo per action, back to the steal.
for _ in 0..5 {
assert!(session.undo(&mut km));
}
assert_eq!(keys(&km, "file.save"), ["Cmd+S", "Cmd+C"]);
assert!(session.undo(&mut km));
assert!(!session.can_undo());
session.redo(&mut km);
session.cancel(&mut km);
assert!(km.overrides().is_empty(), "cancel restores the state at open");
}