1use std::rc::Rc;
72
73use iced::keyboard::key::Named;
74use iced::widget;
75use iced::{Element, Subscription};
76
77use crate::keys::{self, Chord, Keymap};
78use crate::navigation::command::{self, Group, Item, command, group};
79use crate::overlay::anchored::anchored;
80use crate::overlay::dialog::{self, dialog};
81use crate::primitives::button::{self, Variant, button};
82
83pub const SEARCH_ID: widget::Id = widget::Id::new("iced-cube-command-palette-search");
85pub const DIALOG_ID: widget::Id = widget::Id::new("iced-cube-command-palette");
87pub const RECENT_LIMIT: usize = 5;
89pub const RECENT_LABEL: &str = "Recent";
91pub const VISIBLE_ROWS: usize = 8;
93pub const DEFAULT_TITLE: &str = "Command palette";
95
96#[derive(Debug, Clone, PartialEq)]
98pub struct Page<Id> {
99 pub id: Id,
100 pub title: String,
102 pub placeholder: Option<String>,
104 pub groups: Vec<Group<Id>>,
105}
106
107pub fn page<Id>(
109 id: Id,
110 title: impl Into<String>,
111 groups: impl IntoIterator<Item = Group<Id>>,
112) -> Page<Id> {
113 Page {
114 id,
115 title: title.into(),
116 placeholder: None,
117 groups: groups.into_iter().collect(),
118 }
119}
120
121impl<Id> Page<Id> {
122 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
123 self.placeholder = Some(placeholder.into());
124 self
125 }
126}
127
128#[derive(Debug, Clone)]
130pub enum Event<Id> {
131 Open,
133 Close,
134 Toggle,
135 Back,
137 Command(command::Event<Id>),
139}
140
141#[derive(Debug, Clone)]
143pub enum Output<Id> {
144 Ready(command::Sender<Id>),
146 Search(String),
149 Activated(Id),
151}
152
153#[derive(Debug, Clone, PartialEq)]
156pub struct State<Id> {
157 open: bool,
158 root: Vec<Group<Id>>,
159 pages: Vec<Page<Id>>,
160 stack: Vec<Id>,
161 recent: Vec<Id>,
162 recent_limit: usize,
163 rows: usize,
164 command: command::State<Id>,
165}
166
167impl<Id: Clone + PartialEq> State<Id> {
168 pub fn new(groups: impl IntoIterator<Item = Group<Id>>) -> Self {
170 let mut state = Self {
171 open: false,
172 root: groups.into_iter().collect(),
173 pages: Vec::new(),
174 stack: Vec::new(),
175 recent: Vec::new(),
176 recent_limit: RECENT_LIMIT,
177 rows: VISIBLE_ROWS,
178 command: command::State::new([]),
179 };
180 state.rebuild(String::new());
181 state
182 }
183
184 pub fn with_page(mut self, page: Page<Id>) -> Self {
186 self.pages.push(page);
187 self
188 }
189
190 pub fn with_recent(mut self, ids: impl IntoIterator<Item = Id>) -> Self {
192 self.recent = ids.into_iter().take(self.recent_limit).collect();
193 self.rebuild(String::new());
194 self
195 }
196
197 pub fn with_recent_limit(mut self, limit: usize) -> Self {
199 self.recent_limit = limit;
200 self.recent.truncate(limit);
201 self.rebuild(String::new());
202 self
203 }
204
205 pub fn with_visible_rows(mut self, rows: usize) -> Self {
207 self.rows = rows.max(1);
208 self.rebuild(String::new());
209 self
210 }
211
212 pub fn is_open(&self) -> bool {
213 self.open
214 }
215
216 pub fn page(&self) -> Option<&Page<Id>> {
218 let id = self.stack.last()?;
219 self.pages.iter().find(|page| page.id == *id)
220 }
221
222 pub fn page_id(&self) -> Option<&Id> {
224 self.stack.last()
225 }
226
227 pub fn depth(&self) -> usize {
229 self.stack.len()
230 }
231
232 pub fn recent(&self) -> &[Id] {
234 &self.recent
235 }
236
237 pub fn command(&self) -> &command::State<Id> {
239 &self.command
240 }
241
242 pub fn query(&self) -> &str {
243 self.command.query()
244 }
245
246 pub fn set_groups(&mut self, groups: impl IntoIterator<Item = Group<Id>>) {
248 self.root = groups.into_iter().collect();
249 if self.stack.is_empty() {
250 self.rebuild(self.command.query().to_owned());
251 }
252 }
253
254 pub fn update(&mut self, event: Event<Id>) -> Option<Output<Id>> {
256 match event {
257 Event::Open => self.open(),
258 Event::Close => {
259 self.open = false;
260 None
261 }
262 Event::Toggle if self.open => {
263 self.open = false;
264 None
265 }
266 Event::Toggle => self.open(),
267 Event::Back => self.back(),
268 Event::Command(command::Event::Input(query)) => self.input(query),
269 Event::Command(event) => match self.command.update(event)? {
270 command::Output::Ready(sender) => Some(Output::Ready(sender)),
271 command::Output::Search(query) => Some(Output::Search(query)),
272 command::Output::Activated(id) => self.activate(id),
273 command::Output::Closed if self.stack.is_empty() => {
274 self.open = false;
275 None
276 }
277 command::Output::Closed => self.back(),
278 },
279 }
280 }
281
282 pub fn key_event(&self, keymap: &Keymap<Action>, key: &keys::Event) -> Option<Event<Id>> {
286 keymap
287 .resolve_event(key)
288 .and_then(|action| action.event(self))
289 }
290
291 fn open(&mut self) -> Option<Output<Id>> {
292 let had_query = !self.command.query().is_empty();
293 self.open = true;
294 self.stack.clear();
295 self.rebuild(String::new());
296 had_query.then(|| Output::Search(String::new()))
297 }
298
299 fn back(&mut self) -> Option<Output<Id>> {
300 self.stack.pop()?;
301 let had_query = !self.command.query().is_empty();
302 self.rebuild(String::new());
303 had_query.then(|| Output::Search(String::new()))
304 }
305
306 fn input(&mut self, query: String) -> Option<Output<Id>> {
307 let shows_recent = self.stack.is_empty() && !self.recent.is_empty();
308 let was_empty = self.command.query().is_empty();
309 if shows_recent && was_empty != query.is_empty() {
310 self.rebuild(query.clone());
311 return Some(Output::Search(query));
312 }
313 match self.command.update(command::Event::Input(query))? {
314 command::Output::Search(query) => Some(Output::Search(query)),
315 _ => None,
316 }
317 }
318
319 fn activate(&mut self, id: Id) -> Option<Output<Id>> {
320 if self.pages.iter().any(|page| page.id == id) {
321 self.stack.push(id);
322 self.rebuild(String::new());
323 return None;
324 }
325 self.remember(id.clone());
326 self.open = false;
327 Some(Output::Activated(id))
328 }
329
330 fn remember(&mut self, id: Id) {
332 self.recent.retain(|recent| *recent != id);
333 self.recent.insert(0, id);
334 self.recent.truncate(self.recent_limit);
335 }
336
337 fn groups(&self, query: &str) -> Vec<Group<Id>> {
340 let Some(id) = self.stack.last() else {
341 let mut groups = Vec::with_capacity(self.root.len() + 1);
342 let recent: Vec<Item<Id>> = self
343 .recent
344 .iter()
345 .filter_map(|id| self.find(id).cloned())
346 .collect();
347 if query.is_empty() && !recent.is_empty() {
348 groups.push(group(RECENT_LABEL, recent));
349 }
350 groups.extend(self.root.iter().cloned());
351 return groups;
352 };
353 self.pages
354 .iter()
355 .find(|page| page.id == *id)
356 .map(|page| page.groups.clone())
357 .unwrap_or_default()
358 }
359
360 fn find(&self, id: &Id) -> Option<&Item<Id>> {
362 self.root
363 .iter()
364 .chain(self.pages.iter().flat_map(|page| &page.groups))
365 .flat_map(|group| &group.items)
366 .find(|item| item.id == *id)
367 }
368
369 fn rebuild(&mut self, query: String) {
371 let focused = self.command.is_focused();
372 self.command = command::State::new(self.groups(&query)).with_visible_rows(self.rows);
373 if !query.is_empty() {
374 let _ = self.command.update(command::Event::Input(query));
375 }
376 if focused {
377 let _ = self.command.update(command::Event::Focus(true));
378 }
379 }
380}
381
382pub fn subscription<Id: Send + 'static>() -> Subscription<Event<Id>> {
385 command::subscription().map(Event::Command)
386}
387
388#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
390pub enum Action {
391 Toggle,
393 Back,
395}
396
397impl Action {
398 pub fn event<Id: Clone + PartialEq>(self, state: &State<Id>) -> Option<Event<Id>> {
401 match self {
402 Action::Toggle => Some(Event::Toggle),
403 Action::Back => {
404 let back = state.open && !state.stack.is_empty() && state.query().is_empty();
405 back.then_some(Event::Back)
406 }
407 }
408 }
409}
410
411impl keys::Action for Action {
412 const ALL: &'static [Self] = &[Action::Toggle, Action::Back];
413
414 fn defaults() -> Keymap<Self> {
415 default_keymap()
416 }
417
418 fn name(self) -> &'static str {
419 match self {
420 Action::Toggle => "Toggle",
421 Action::Back => "Back",
422 }
423 }
424
425 fn description(self) -> &'static str {
426 match self {
427 Action::Toggle => "Opens the palette from anywhere, or closes it.",
428 Action::Back => "Returns to the page before, while the query is empty.",
429 }
430 }
431}
432
433pub fn default_keymap() -> Keymap<Action> {
446 Keymap::new()
447 .bind(Chord::character('k').command(), Action::Toggle)
448 .bind(Chord::character('p').command().shift(), Action::Toggle)
449 .bind(Chord::named(Named::Backspace), Action::Back)
450}
451
452type OnEvent<'a, Id, Message> = Rc<dyn Fn(Event<Id>) -> Message + 'a>;
453
454pub struct CommandPalette<'a, Id, Message> {
456 state: &'a State<Id>,
457 base: Element<'a, Message>,
458 title: String,
459 placeholder: String,
460 empty: Option<String>,
461 loading: bool,
462 max_height: f32,
463 size: dialog::Size,
464 keymap: Keymap<Action>,
465 command_keymap: Keymap<command::Action>,
466 dialog_keymap: Keymap<dialog::Action>,
467 on_event: Option<Box<dyn Fn(Event<Id>) -> Message + 'a>>,
468}
469
470impl<Id: std::fmt::Debug, Message> std::fmt::Debug for CommandPalette<'_, Id, Message> {
471 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
472 f.debug_struct("CommandPalette")
473 .field("state", self.state)
474 .field("title", &self.title)
475 .field("placeholder", &self.placeholder)
476 .field("loading", &self.loading)
477 .field("max_height", &self.max_height)
478 .field("size", &self.size)
479 .finish_non_exhaustive()
480 }
481}
482
483pub fn command_palette<'a, Id, Message>(
486 state: &'a State<Id>,
487 base: impl Into<Element<'a, Message>>,
488) -> CommandPalette<'a, Id, Message> {
489 CommandPalette {
490 state,
491 base: base.into(),
492 title: DEFAULT_TITLE.to_owned(),
493 placeholder: "Type a command or search...".to_owned(),
494 empty: None,
495 loading: false,
496 max_height: 400.0,
497 size: dialog::Size::Md,
498 keymap: default_keymap(),
499 command_keymap: command::default_keymap(),
500 dialog_keymap: dialog::default_keymap(),
501 on_event: None,
502 }
503}
504
505impl<'a, Id, Message> CommandPalette<'a, Id, Message> {
506 pub fn title(mut self, title: impl Into<String>) -> Self {
508 self.title = title.into();
509 self
510 }
511
512 pub fn placeholder(mut self, placeholder: impl Into<String>) -> Self {
514 self.placeholder = placeholder.into();
515 self
516 }
517
518 pub fn empty(mut self, empty: impl Into<String>) -> Self {
520 self.empty = Some(empty.into());
521 self
522 }
523
524 pub fn loading(mut self, loading: bool) -> Self {
526 self.loading = loading;
527 self
528 }
529
530 pub fn max_height(mut self, max_height: f32) -> Self {
532 self.max_height = max_height;
533 self
534 }
535
536 pub fn size(mut self, size: dialog::Size) -> Self {
538 self.size = size;
539 self
540 }
541
542 pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
544 self.keymap = keymap;
545 self
546 }
547
548 pub fn command_keymap(mut self, keymap: Keymap<command::Action>) -> Self {
550 self.command_keymap = keymap;
551 self
552 }
553
554 pub fn dialog_keymap(mut self, keymap: Keymap<dialog::Action>) -> Self {
556 self.dialog_keymap = keymap;
557 self
558 }
559
560 pub fn on_event(mut self, on_event: impl Fn(Event<Id>) -> Message + 'a) -> Self {
561 self.on_event = Some(Box::new(on_event));
562 self
563 }
564}
565
566impl<'a, Id, Message> From<CommandPalette<'a, Id, Message>> for Element<'a, Message>
567where
568 Id: Clone + PartialEq + 'a,
569 Message: Clone + 'a,
570{
571 fn from(palette: CommandPalette<'a, Id, Message>) -> Self {
572 let CommandPalette {
573 state,
574 base,
575 title,
576 placeholder,
577 empty,
578 loading,
579 max_height,
580 size,
581 keymap,
582 command_keymap,
583 dialog_keymap,
584 on_event,
585 } = palette;
586 let on_event: Option<OnEvent<'a, Id, Message>> = on_event.map(Rc::from);
587 let page = state.page();
588 let title = page.map_or(title, |page| page.title.clone());
589 let placeholder = page
590 .and_then(|page| page.placeholder.clone())
591 .unwrap_or(placeholder);
592 let toggles: Vec<Chord> = keymap
593 .chords(&Action::Toggle)
594 .into_iter()
595 .cloned()
596 .collect();
597
598 let mut list = command(&state.command)
599 .placeholder(placeholder)
600 .loading(loading)
601 .max_height(max_height)
602 .id(SEARCH_ID)
603 .keymap(command_keymap);
604 if let Some(empty) = empty {
605 list = list.empty(empty);
606 }
607
608 let mut palette = dialog(base)
609 .open(state.open)
610 .title(title)
611 .size(size)
612 .close_button(false)
613 .id(DIALOG_ID)
614 .keymap(dialog_keymap)
615 .pass_through(toggles);
616 let Some(on_event) = on_event else {
617 return palette.body(list).into();
618 };
619
620 if !state.stack.is_empty() {
621 palette = palette.action(
622 button("Back")
623 .icon(crate::lucide!(ChevronLeft))
624 .variant(Variant::Ghost)
625 .size(button::Size::Sm)
626 .on_press(on_event(Event::Back)),
627 );
628 }
629 let on_list = on_event.clone();
630 let on_key = on_event.clone();
631 let body = anchored(list.on_event(move |event| on_list(Event::Command(event))))
632 .dismiss_keys([])
633 .on_key(move |key| state.key_event(&keymap, key).map(&*on_key));
634 palette.body(body).on_dismiss(on_event(Event::Close)).into()
635 }
636}
637
638#[cfg(test)]
639mod tests {
640 use super::*;
641 use crate::navigation::command::item;
642 use iced::keyboard::{Key, Modifiers};
643
644 fn state() -> State<&'static str> {
645 State::new([
646 group(
647 "Files",
648 [item("new", "New file"), item("open", "Open file")],
649 ),
650 group(
651 "Preferences",
652 [
653 item("theme", "Change theme..."),
654 item("settings", "Settings"),
655 ],
656 ),
657 ])
658 .with_page(
659 page(
660 "theme",
661 "Theme",
662 [group("", [item("light", "Light"), item("dark", "Dark")])],
663 )
664 .placeholder("Pick a theme..."),
665 )
666 }
667
668 fn opened() -> State<&'static str> {
669 let mut state = state();
670 let _ = state.update(Event::Open);
671 state
672 }
673
674 fn labels(state: &State<&'static str>) -> Vec<(String, String)> {
675 state
676 .command()
677 .results()
678 .map(|(group, item)| (group.to_owned(), item.label.clone()))
679 .collect()
680 }
681
682 fn run(state: &mut State<&'static str>, id: &'static str) -> Option<Output<&'static str>> {
683 let position = state
684 .command()
685 .results()
686 .position(|(_, item)| item.id == id)
687 .expect("the item is listed");
688 state.update(Event::Command(command::Event::Activate(position)))
689 }
690
691 #[test]
692 fn starts_closed_on_the_first_page_with_nothing_recent() {
693 let state = state();
694 assert!(!state.is_open());
695 assert_eq!(state.depth(), 0);
696 assert!(state.page().is_none() && state.page_id().is_none());
697 assert!(state.recent().is_empty());
698 assert_eq!(state.command().len(), 4);
699 assert_eq!(state.query(), "");
700 }
701
702 #[test]
703 fn open_close_and_toggle() {
704 let mut state = state();
705 assert!(state.update(Event::Open).is_none());
706 assert!(state.is_open());
707 let _ = state.update(Event::Close);
708 assert!(!state.is_open());
709 let _ = state.update(Event::Toggle);
710 assert!(state.is_open());
711 let _ = state.update(Event::Toggle);
712 assert!(!state.is_open());
713 }
714
715 #[test]
716 fn running_a_command_closes_and_remembers_it() {
717 let mut state = opened();
718 assert!(matches!(
719 run(&mut state, "settings"),
720 Some(Output::Activated("settings"))
721 ));
722 assert!(!state.is_open());
723 assert_eq!(state.recent(), ["settings"]);
724
725 let _ = state.update(Event::Open);
726 let first = labels(&state).into_iter().next();
727 assert_eq!(first, Some(("Recent".into(), "Settings".into())));
728 }
729
730 #[test]
731 fn recent_commands_are_newest_first_unique_and_capped() {
732 let mut state = state().with_recent_limit(2);
733 for id in ["new", "open", "new", "settings"] {
734 let _ = state.update(Event::Open);
735 let _ = run(&mut state, id);
736 }
737 assert_eq!(state.recent(), ["settings", "new"]);
738
739 let off = self::state().with_recent_limit(0).with_recent(["new"]);
740 assert!(off.recent().is_empty());
741 }
742
743 #[test]
744 fn recent_items_from_nested_pages_are_found() {
745 let state = state().with_recent(["dark", "missing"]);
746 let recent: Vec<_> = labels(&state)
747 .into_iter()
748 .filter(|(group, _)| group == RECENT_LABEL)
749 .map(|(_, label)| label)
750 .collect();
751 assert_eq!(recent, ["Dark"], "unknown ids are skipped");
752 }
753
754 #[test]
755 fn typing_hides_the_recent_group_and_clearing_brings_it_back() {
756 let mut state = state().with_recent(["open"]);
757 let _ = state.update(Event::Open);
758 let output = state.update(Event::Command(command::Event::Input("fi".into())));
759 assert!(matches!(output, Some(Output::Search(query)) if query == "fi"));
760 assert!(
761 labels(&state)
762 .iter()
763 .all(|(group, _)| group != RECENT_LABEL)
764 );
765 assert_eq!(state.query(), "fi");
766
767 let output = state.update(Event::Command(command::Event::Input("fil".into())));
768 assert!(matches!(output, Some(Output::Search(query)) if query == "fil"));
769
770 let output = state.update(Event::Command(command::Event::Input(String::new())));
771 assert!(matches!(output, Some(Output::Search(query)) if query.is_empty()));
772 assert_eq!(labels(&state)[0].0, RECENT_LABEL);
773 }
774
775 #[test]
776 fn a_page_item_opens_its_page_instead_of_running() {
777 let mut state = opened();
778 assert!(run(&mut state, "theme").is_none());
779 assert!(state.is_open());
780 assert_eq!(state.depth(), 1);
781 assert_eq!(state.page_id(), Some(&"theme"));
782 assert_eq!(state.page().map(|page| page.title.as_str()), Some("Theme"));
783 let items: Vec<_> = labels(&state).into_iter().map(|(_, label)| label).collect();
784 assert_eq!(items, ["Light", "Dark"]);
785
786 assert!(matches!(
787 run(&mut state, "dark"),
788 Some(Output::Activated("dark"))
789 ));
790 assert!(!state.is_open());
791 assert_eq!(state.recent(), ["dark"], "the page itself is not recent");
792 }
793
794 #[test]
795 fn back_returns_to_the_page_before() {
796 let mut state = opened();
797 let _ = run(&mut state, "theme");
798 assert!(state.update(Event::Back).is_none());
799 assert_eq!(state.depth(), 0);
800 assert_eq!(state.command().len(), 4);
801 assert!(state.update(Event::Back).is_none(), "nothing to go back to");
802 assert!(state.is_open());
803 }
804
805 #[test]
806 fn going_back_with_a_query_clears_it() {
807 let mut state = opened();
808 let _ = run(&mut state, "theme");
809 let _ = state.update(Event::Command(command::Event::Input("da".into())));
810 let output = state.update(Event::Back);
811 assert!(matches!(output, Some(Output::Search(query)) if query.is_empty()));
812 assert_eq!(state.query(), "");
813 }
814
815 #[test]
816 fn escape_on_an_empty_query_goes_back_then_closes() {
817 let mut state = opened();
818 let _ = run(&mut state, "theme");
819 let _ = state.update(Event::Command(command::Event::Close));
820 assert_eq!(state.depth(), 0);
821 assert!(state.is_open());
822 let _ = state.update(Event::Command(command::Event::Close));
823 assert!(!state.is_open());
824 }
825
826 #[test]
827 fn opening_again_starts_on_the_first_page() {
828 let mut state = opened();
829 let _ = run(&mut state, "theme");
830 let _ = state.update(Event::Command(command::Event::Input("li".into())));
831 let _ = state.update(Event::Close);
832 let output = state.update(Event::Open);
833 assert!(matches!(output, Some(Output::Search(query)) if query.is_empty()));
834 assert_eq!(state.depth(), 0);
835 assert_eq!(state.query(), "");
836 }
837
838 #[test]
839 fn focus_survives_a_page_change() {
840 let mut state = opened();
841 let _ = state.update(Event::Command(command::Event::Focus(true)));
842 let _ = run(&mut state, "theme");
843 assert!(state.command().is_focused());
844 assert!(state.command().shows_highlight());
845 }
846
847 #[test]
848 fn set_groups_replaces_the_first_page() {
849 let mut state = opened();
850 state.set_groups([group("Only", [item("one", "One")])]);
851 assert_eq!(labels(&state), [("Only".into(), "One".into())]);
852
853 let _ = state.update(Event::Open);
854 state.set_groups([group("Only", [item("theme", "Change theme...")])]);
855 let _ = run(&mut state, "theme");
856 state.set_groups([group("Other", [item("two", "Two")])]);
857 assert_eq!(state.depth(), 1, "a nested page stays on show");
858 }
859
860 #[test]
861 fn channel_events_pass_through() {
862 let mut state = opened();
863 let (sender, _receiver) = iced::futures::channel::mpsc::channel(1);
864 let output = state.update(Event::Command(command::Event::Ready(sender)));
865 assert!(matches!(output, Some(Output::Ready(_))));
866
867 let _ = state.update(Event::Command(command::Event::Input("xyz".into())));
868 let _ = state.update(Event::Command(command::Event::Received(vec![
869 command::results("xyz", "Files", [item("remote", "xyz.txt")]),
870 ])));
871 assert!(labels(&state).contains(&("Files".into(), "xyz.txt".into())));
872 }
873
874 fn key(key: Key, modifiers: Modifiers) -> keys::Event {
875 keys::Event { key, modifiers }
876 }
877
878 #[test]
879 fn toggle_chords_work_anywhere_and_back_only_on_a_nested_empty_page() {
880 let keymap = default_keymap();
881 let mut state = state();
882 let ctrl_k = key(Key::Character("k".into()), Modifiers::COMMAND);
883 let ctrl_shift_p = key(
884 Key::Character("P".into()),
885 Modifiers::COMMAND | Modifiers::SHIFT,
886 );
887 let backspace = key(Key::Named(Named::Backspace), Modifiers::empty());
888 assert!(matches!(
889 state.key_event(&keymap, &ctrl_k),
890 Some(Event::Toggle)
891 ));
892 assert!(matches!(
893 state.key_event(&keymap, &ctrl_shift_p),
894 Some(Event::Toggle)
895 ));
896 assert!(state.key_event(&keymap, &backspace).is_none());
897
898 let _ = state.update(Event::Open);
899 assert!(state.key_event(&keymap, &backspace).is_none(), "first page");
900 let _ = run(&mut state, "theme");
901 assert!(matches!(
902 state.key_event(&keymap, &backspace),
903 Some(Event::Back)
904 ));
905 let _ = state.update(Event::Command(command::Event::Input("d".into())));
906 assert!(
907 state.key_event(&keymap, &backspace).is_none(),
908 "Backspace deletes text"
909 );
910 }
911
912 #[test]
913 fn default_keymap_covers_every_action() {
914 let keymap = default_keymap();
915 assert_eq!(keymap.chords(&Action::Toggle).len(), 2);
916 assert_eq!(
917 keymap.chords(&Action::Back),
918 [&Chord::named(Named::Backspace)]
919 );
920 assert_eq!(Keymap::<Action>::defaults(), keymap);
921 for &action in <Action as keys::Action>::ALL {
922 assert!(!keys::Action::name(action).is_empty());
923 assert!(keys::Action::description(action).ends_with('.'));
924 }
925 }
926
927 #[test]
928 fn keymap_overrides_and_unbinding() {
929 let keymap = default_keymap()
930 .unbind_action(&Action::Back)
931 .bind(Chord::named(Named::F1), Action::Toggle);
932 let mut state = opened();
933 let _ = run(&mut state, "theme");
934 let backspace = key(Key::Named(Named::Backspace), Modifiers::empty());
935 let f1 = key(Key::Named(Named::F1), Modifiers::empty());
936 assert!(state.key_event(&keymap, &backspace).is_none());
937 assert!(matches!(state.key_event(&keymap, &f1), Some(Event::Toggle)));
938 }
939
940 #[test]
941 fn builder_defaults_and_overrides() {
942 let state = state();
943 let palette: CommandPalette<'_, &str, ()> = command_palette(&state, widget::text("App"));
944 assert_eq!(palette.title, DEFAULT_TITLE);
945 assert!(palette.empty.is_none());
946 assert!(!palette.loading);
947 assert_eq!(palette.size, dialog::Size::Md);
948 assert_eq!(palette.keymap, default_keymap());
949 assert_eq!(palette.command_keymap, command::default_keymap());
950 assert_eq!(palette.dialog_keymap, dialog::default_keymap());
951 assert!(palette.on_event.is_none());
952
953 let palette: CommandPalette<'_, &str, ()> = command_palette(&state, widget::text("App"))
954 .title("Go to")
955 .placeholder("Search...")
956 .empty("Nothing here.")
957 .loading(true)
958 .max_height(200.0)
959 .size(dialog::Size::Lg)
960 .keymap(Keymap::new())
961 .command_keymap(Keymap::new())
962 .dialog_keymap(Keymap::new())
963 .on_event(|_| ());
964 assert_eq!(palette.title, "Go to");
965 assert_eq!(palette.placeholder, "Search...");
966 assert_eq!(palette.empty.as_deref(), Some("Nothing here."));
967 assert!(palette.loading);
968 assert_eq!(palette.max_height, 200.0);
969 assert_eq!(palette.size, dialog::Size::Lg);
970 assert!(palette.keymap.is_empty() && palette.command_keymap.is_empty());
971 assert!(palette.dialog_keymap.is_empty() && palette.on_event.is_some());
972 }
973}