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 { 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::(); let per = a.iter().map(|(k, t)| string + 64 + k.len() + t.iter().map(|t| string + 16 + t.len()).sum::()).sum::(); 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::>(); 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::::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, ©, &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"); }