1use std::rc::Rc;
21
22use iced::advanced::layout::{self, Layout};
23use iced::advanced::overlay;
24use iced::advanced::renderer;
25use iced::advanced::widget::{Operation, Tree, Widget, tree};
26use iced::advanced::{Clipboard, Shell};
27use iced::keyboard::key::Named;
28use iced::time::{Duration, Instant};
29use iced::touch::{self, Finger};
30use iced::{
31 Element, Event as IcedEvent, Length, Point, Rectangle, Renderer, Size, Theme, Vector, mouse,
32 window,
33};
34
35use crate::keys::{self, Chord, Keymap};
36use crate::overlay::anchored::{Align, Placement, Side, anchored};
37use crate::overlay::dropdown_menu::{self, Entry, Output, WIDTH};
38
39#[derive(Debug, Clone, Copy, PartialEq)]
42pub enum Event<Id, Key = ()> {
43 Open(Key, Point),
47 OpenFromKeyboard(Key),
49 Menu(dropdown_menu::Event<Id>),
51}
52
53#[derive(Debug, Clone, PartialEq)]
55pub struct State<Id, Key = ()> {
56 menu: dropdown_menu::State<Id>,
57 position: Option<Point>,
58 target: Option<Key>,
59}
60
61impl<Id: Copy + PartialEq, Key: Clone + PartialEq> State<Id, Key> {
62 pub fn new(entries: impl IntoIterator<Item = Entry<Id>>) -> Self {
64 Self {
65 menu: dropdown_menu::State::new(entries),
66 position: None,
67 target: None,
68 }
69 }
70
71 pub fn menu(&self) -> &dropdown_menu::State<Id> {
73 &self.menu
74 }
75
76 pub fn is_open(&self) -> bool {
77 self.menu.is_open()
78 }
79
80 pub fn is_open_on(&self, key: &Key) -> bool {
82 self.is_open() && self.target.as_ref() == Some(key)
83 }
84
85 pub fn target(&self) -> Option<&Key> {
88 self.target.as_ref()
89 }
90
91 pub fn position(&self) -> Option<Point> {
94 self.position
95 }
96
97 pub fn is_checked(&self, id: Id) -> bool {
98 self.menu.is_checked(id)
99 }
100
101 pub fn set_checked(&mut self, id: Id, checked: bool) {
103 self.menu.set_checked(id, checked);
104 }
105
106 pub fn set_disabled(&mut self, id: Id, disabled: bool) {
107 self.menu.set_disabled(id, disabled);
108 }
109
110 pub fn update(&mut self, event: Event<Id, Key>) -> Option<Output<Id>> {
113 match event {
114 Event::Open(key, position) => {
115 self.target = Some(key);
116 self.position = Some(position);
117 let _ = self.menu.update(dropdown_menu::Event::Close);
118 self.menu.update(dropdown_menu::Event::Open)
119 }
120 Event::OpenFromKeyboard(key) => {
121 self.target = Some(key);
122 self.position = None;
123 let _ = self.menu.update(dropdown_menu::Event::Close);
124 self.menu.update(dropdown_menu::Event::First)
125 }
126 Event::Menu(_) if !self.is_open() => None,
127 Event::Menu(event) => self.menu.update(event),
128 }
129 }
130
131 pub fn key_event(
136 &self,
137 keymap: &Keymap<Action>,
138 key: &keys::Event,
139 target: Key,
140 ) -> Option<Event<Id, Key>> {
141 if let Some(action) = keymap.resolve_event(key) {
142 return action.event(self, target);
143 }
144 if !self.is_open() {
145 return None;
146 }
147 dropdown_menu::typeahead(key)
148 .map(|character| Event::Menu(dropdown_menu::Event::Typeahead(character)))
149 }
150}
151
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
154pub enum Action {
155 Open,
156 Next,
157 Previous,
158 First,
159 Last,
160 Activate,
161 Close,
162 OpenSubmenu,
163 CloseSubmenu,
164}
165
166impl Action {
167 pub fn menu_action(self) -> Option<dropdown_menu::Action> {
169 use dropdown_menu::Action as Menu;
170 match self {
171 Action::Open => None,
172 Action::Next => Some(Menu::Next),
173 Action::Previous => Some(Menu::Previous),
174 Action::First => Some(Menu::First),
175 Action::Last => Some(Menu::Last),
176 Action::Activate => Some(Menu::Activate),
177 Action::Close => Some(Menu::Close),
178 Action::OpenSubmenu => Some(Menu::OpenSubmenu),
179 Action::CloseSubmenu => Some(Menu::CloseSubmenu),
180 }
181 }
182
183 pub fn event<Id: Copy + PartialEq, Key: Clone + PartialEq>(
187 self,
188 state: &State<Id, Key>,
189 target: Key,
190 ) -> Option<Event<Id, Key>> {
191 let open = state.is_open();
192 match self.menu_action() {
193 None => (!open).then_some(Event::OpenFromKeyboard(target)),
194 Some(_) if !open => None,
195 Some(action) => action.event(state.menu()).map(Event::Menu),
196 }
197 }
198}
199
200impl From<dropdown_menu::Action> for Action {
201 fn from(action: dropdown_menu::Action) -> Self {
202 use dropdown_menu::Action as Menu;
203 match action {
204 Menu::Open => Action::Open,
205 Menu::Next => Action::Next,
206 Menu::Previous => Action::Previous,
207 Menu::First => Action::First,
208 Menu::Last => Action::Last,
209 Menu::Activate => Action::Activate,
210 Menu::Close => Action::Close,
211 Menu::OpenSubmenu => Action::OpenSubmenu,
212 Menu::CloseSubmenu => Action::CloseSubmenu,
213 }
214 }
215}
216
217impl keys::Action for Action {
218 const ALL: &'static [Self] = &[
219 Action::Open,
220 Action::Next,
221 Action::Previous,
222 Action::First,
223 Action::Last,
224 Action::Activate,
225 Action::Close,
226 Action::OpenSubmenu,
227 Action::CloseSubmenu,
228 ];
229
230 fn defaults() -> Keymap<Self> {
231 default_keymap()
232 }
233
234 fn name(self) -> &'static str {
235 match self.menu_action() {
236 None => "Open",
237 Some(action) => keys::Action::name(action),
238 }
239 }
240
241 fn description(self) -> &'static str {
242 match self {
243 Action::Open => "Opens the menu below the area, on its first item.",
244 Action::Next => "Highlights the next enabled item, wrapping.",
245 Action::Previous => "Highlights the previous enabled item, wrapping.",
246 Action::Activate => "Chooses the highlighted item or opens its submenu.",
247 action => action.menu_action().map_or("", keys::Action::description),
248 }
249 }
250}
251
252pub fn default_keymap() -> Keymap<Action> {
263 let open = Keymap::new()
264 .bind(Chord::named(Named::F10).shift(), Action::Open)
265 .bind(Chord::named(Named::ContextMenu), Action::Open);
266 dropdown_menu::default_keymap()
267 .bindings()
268 .fold(open, |keymap, (chord, action)| {
269 keymap.bind(chord.clone(), Action::from(*action))
270 })
271}
272
273pub const KEYBOARD_GAP: f32 = 4.0;
275
276pub const LONG_PRESS: Duration = Duration::from_millis(500);
278
279pub const LONG_PRESS_SLOP: f32 = 10.0;
282
283#[derive(Debug, Clone, Copy, Default, PartialEq)]
286struct LongPress {
287 finger: Option<Finger>,
288 origin: Point,
289 since: Option<Instant>,
290 fired: bool,
291}
292
293impl LongPress {
294 fn press(&mut self, finger: Finger, position: Point) {
296 *self = Self {
297 finger: Some(finger),
298 origin: position,
299 since: None,
300 fired: false,
301 };
302 }
303
304 fn moved(&mut self, finger: Finger, position: Point) {
306 if self.finger != Some(finger) || self.fired {
307 return;
308 }
309 if self.origin.distance(position) > LONG_PRESS_SLOP {
310 *self = Self::default();
311 }
312 }
313
314 fn lift(&mut self, finger: Finger) -> bool {
317 if self.finger != Some(finger) {
318 return false;
319 }
320 let fired = self.fired;
321 *self = Self::default();
322 fired
323 }
324
325 fn tick(&mut self, now: Instant) -> Option<Point> {
328 if self.finger.is_none() || self.fired {
329 return None;
330 }
331 let since = *self.since.get_or_insert(now);
332 if now.saturating_duration_since(since) < LONG_PRESS {
333 return None;
334 }
335 self.fired = true;
336 Some(self.origin)
337 }
338
339 fn redraw(&self) -> Option<window::RedrawRequest> {
341 if self.finger.is_none() || self.fired {
342 return None;
343 }
344 Some(match self.since {
345 None => window::RedrawRequest::NextFrame,
346 Some(since) => window::RedrawRequest::At(since + LONG_PRESS),
347 })
348 }
349}
350
351type OnEvent<'a, Id, Key, Message> = dyn Fn(Event<Id, Key>) -> Message + 'a;
352
353pub struct ContextMenu<'a, Id, Message, Key = ()> {
355 state: &'a State<Id, Key>,
356 key: Key,
357 content: Element<'a, Message>,
358 width: f32,
359 keymap: Keymap<Action>,
360 on_event: Option<Box<OnEvent<'a, Id, Key, Message>>>,
361}
362
363impl<Id: std::fmt::Debug, Message, Key: std::fmt::Debug> std::fmt::Debug
364 for ContextMenu<'_, Id, Message, Key>
365{
366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367 f.debug_struct("ContextMenu")
368 .field("state", self.state)
369 .field("key", &self.key)
370 .field("width", &self.width)
371 .field("keymap", &self.keymap)
372 .finish_non_exhaustive()
373 }
374}
375
376pub fn context_menu<'a, Id, Message>(
379 state: &'a State<Id>,
380 content: impl Into<Element<'a, Message>>,
381) -> ContextMenu<'a, Id, Message> {
382 keyed(state, (), content)
383}
384
385pub fn keyed<'a, Id, Key, Message>(
389 state: &'a State<Id, Key>,
390 key: Key,
391 content: impl Into<Element<'a, Message>>,
392) -> ContextMenu<'a, Id, Message, Key> {
393 ContextMenu {
394 state,
395 key,
396 content: content.into(),
397 width: WIDTH,
398 keymap: default_keymap(),
399 on_event: None,
400 }
401}
402
403impl<'a, Id, Message, Key> ContextMenu<'a, Id, Message, Key> {
404 pub fn width(mut self, width: f32) -> Self {
406 self.width = width;
407 self
408 }
409
410 pub fn keymap(mut self, keymap: Keymap<Action>) -> Self {
412 self.keymap = keymap;
413 self
414 }
415
416 pub fn on_event(mut self, on_event: impl Fn(Event<Id, Key>) -> Message + 'a) -> Self {
417 self.on_event = Some(Box::new(on_event));
418 self
419 }
420}
421
422pub fn placement(position: Option<Point>) -> Placement {
425 let gap = if position.is_some() {
426 0.0
427 } else {
428 KEYBOARD_GAP
429 };
430 Placement::new(Side::Bottom, Align::Start).gap(gap)
431}
432
433impl<'a, Id, Message, Key> From<ContextMenu<'a, Id, Message, Key>> for Element<'a, Message>
434where
435 Id: Copy + PartialEq + 'a,
436 Key: Clone + PartialEq + 'a,
437 Message: Clone + 'a,
438{
439 fn from(menu: ContextMenu<'a, Id, Message, Key>) -> Self {
440 let ContextMenu {
441 state,
442 key,
443 content,
444 width,
445 keymap,
446 on_event,
447 } = menu;
448
449 let open = state.is_open_on(&key);
450 let on_event: Option<Rc<OnEvent<'a, Id, Key, Message>>> = on_event.map(Rc::from);
451
452 let panel = match &on_event {
453 _ if !open => None,
454 Some(on_event) => {
455 let on_menu = |event: dropdown_menu::Event<Id>| on_event(Event::Menu(event));
456 Some(dropdown_menu::panel(state.menu(), Some(&on_menu), width))
457 }
458 None => Some(dropdown_menu::panel(state.menu(), None, width)),
459 };
460
461 let area = Area {
462 content,
463 on_open: on_event.clone().map(|on_event| {
464 let key = key.clone();
465 Box::new(move |point| on_event(Event::Open(key.clone(), point)))
466 as Box<dyn Fn(Point) -> Message + 'a>
467 }),
468 };
469 let mut anchored = anchored(Element::new(area))
470 .content(panel)
471 .placement(placement(state.position()))
472 .dismiss_on_anchor_press(true)
473 .dismiss_keys([]);
474 if let Some(position) = state.position() {
475 anchored = anchored.at(position);
476 }
477 let Some(on_event) = on_event.filter(|_| open) else {
478 return anchored.into();
479 };
480 anchored
481 .on_dismiss(on_event(Event::Menu(dropdown_menu::Event::Close)))
482 .on_key(move |press| state.key_event(&keymap, press, key.clone()).map(&*on_event))
483 .into()
484 }
485}
486
487struct Area<'a, Message> {
490 content: Element<'a, Message>,
491 on_open: Option<Box<dyn Fn(Point) -> Message + 'a>>,
492}
493
494fn relative(cursor: mouse::Cursor, bounds: Rectangle) -> Option<Point> {
496 let position = cursor.position()?;
497 Some(Point::new(position.x - bounds.x, position.y - bounds.y))
498}
499
500impl<Message> Area<'_, Message> {
501 fn long_press(
505 &self,
506 press: &mut LongPress,
507 event: &IcedEvent,
508 bounds: Rectangle,
509 cursor: mouse::Cursor,
510 shell: &mut Shell<'_, Message>,
511 ) {
512 let Some(on_open) = &self.on_open else {
513 return;
514 };
515 match event {
516 IcedEvent::Touch(touch::Event::FingerPressed { id, .. }) => {
517 if let Some(position) = cursor.position_in(bounds) {
518 press.press(*id, position);
519 }
520 }
521 IcedEvent::Touch(touch::Event::FingerMoved { id, .. }) => {
522 if let Some(position) = relative(cursor, bounds) {
523 press.moved(*id, position);
524 }
525 }
526 IcedEvent::Window(window::Event::RedrawRequested(now)) => {
527 if let Some(position) = press.tick(*now) {
528 shell.publish(on_open(position));
529 }
530 }
531 _ => {}
532 }
533 if let Some(request) = press.redraw() {
534 shell.request_redraw_at(request);
535 }
536 }
537}
538
539impl<Message> Widget<Message, Theme, Renderer> for Area<'_, Message> {
540 fn size(&self) -> Size<Length> {
541 self.content.as_widget().size()
542 }
543
544 fn size_hint(&self) -> Size<Length> {
545 self.content.as_widget().size_hint()
546 }
547
548 fn tag(&self) -> tree::Tag {
549 tree::Tag::of::<LongPress>()
550 }
551
552 fn state(&self) -> tree::State {
553 tree::State::new(LongPress::default())
554 }
555
556 fn children(&self) -> Vec<Tree> {
557 vec![Tree::new(&self.content)]
558 }
559
560 fn diff(&self, tree: &mut Tree) {
561 tree.diff_children(std::slice::from_ref(&self.content));
562 }
563
564 fn layout(
565 &mut self,
566 tree: &mut Tree,
567 renderer: &Renderer,
568 limits: &layout::Limits,
569 ) -> layout::Node {
570 self.content
571 .as_widget_mut()
572 .layout(&mut tree.children[0], renderer, limits)
573 }
574
575 fn draw(
576 &self,
577 tree: &Tree,
578 renderer: &mut Renderer,
579 theme: &Theme,
580 style: &renderer::Style,
581 layout: Layout<'_>,
582 cursor: mouse::Cursor,
583 viewport: &Rectangle,
584 ) {
585 self.content.as_widget().draw(
586 &tree.children[0],
587 renderer,
588 theme,
589 style,
590 layout,
591 cursor,
592 viewport,
593 );
594 }
595
596 fn operate(
597 &mut self,
598 tree: &mut Tree,
599 layout: Layout<'_>,
600 renderer: &Renderer,
601 operation: &mut dyn Operation,
602 ) {
603 self.content
604 .as_widget_mut()
605 .operate(&mut tree.children[0], layout, renderer, operation);
606 }
607
608 fn update(
609 &mut self,
610 tree: &mut Tree,
611 event: &IcedEvent,
612 layout: Layout<'_>,
613 cursor: mouse::Cursor,
614 renderer: &Renderer,
615 clipboard: &mut dyn Clipboard,
616 shell: &mut Shell<'_, Message>,
617 viewport: &Rectangle,
618 ) {
619 let press = tree.state.downcast_mut::<LongPress>();
620 if let IcedEvent::Touch(
622 touch::Event::FingerLifted { id, .. } | touch::Event::FingerLost { id, .. },
623 ) = event
624 && press.lift(*id)
625 {
626 shell.capture_event();
627 return;
628 }
629 self.long_press(press, event, layout.bounds(), cursor, shell);
630
631 self.content.as_widget_mut().update(
632 &mut tree.children[0],
633 event,
634 layout,
635 cursor,
636 renderer,
637 clipboard,
638 shell,
639 viewport,
640 );
641 if shell.is_event_captured() {
642 return;
643 }
644 let IcedEvent::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) = event else {
645 return;
646 };
647 let (Some(on_open), Some(position)) = (&self.on_open, cursor.position_in(layout.bounds()))
648 else {
649 return;
650 };
651 shell.publish(on_open(position));
652 shell.capture_event();
653 }
654
655 fn mouse_interaction(
656 &self,
657 tree: &Tree,
658 layout: Layout<'_>,
659 cursor: mouse::Cursor,
660 viewport: &Rectangle,
661 renderer: &Renderer,
662 ) -> mouse::Interaction {
663 self.content.as_widget().mouse_interaction(
664 &tree.children[0],
665 layout,
666 cursor,
667 viewport,
668 renderer,
669 )
670 }
671
672 fn overlay<'b>(
673 &'b mut self,
674 tree: &'b mut Tree,
675 layout: Layout<'b>,
676 renderer: &Renderer,
677 viewport: &Rectangle,
678 translation: Vector,
679 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
680 self.content.as_widget_mut().overlay(
681 &mut tree.children[0],
682 layout,
683 renderer,
684 viewport,
685 translation,
686 )
687 }
688}
689
690#[cfg(test)]
691mod tests {
692 use super::*;
693 use crate::overlay::dropdown_menu::{checkbox_item, item, separator, submenu};
694 use iced::keyboard::{Key, Modifiers};
695 use iced::widget::text;
696
697 fn state() -> State<u8> {
698 State::new([
699 item(1, "Back"),
700 item(2, "Forward").disabled(true),
701 item(3, "Reload"),
702 submenu(4, "More tools", [item(5, "Save page")]),
703 separator(),
704 checkbox_item(6, "Show bookmarks", true),
705 ])
706 }
707
708 fn rows() -> State<u8, &'static str> {
709 State::new([item(1, "Open"), item(3, "Rename"), item(6, "Delete")])
710 }
711
712 #[test]
713 fn starts_closed_with_no_target() {
714 let state = state();
715 assert!(!state.is_open());
716 assert_eq!(state.position(), None);
717 assert_eq!(state.target(), None);
718 assert!(!state.is_open_on(&()));
719 assert_eq!(state.menu().entries().len(), 6);
720 }
721
722 #[test]
723 fn right_click_opens_at_the_point_without_a_highlight() {
724 let mut state = state();
725 assert_eq!(state.update(Event::Open((), Point::new(40.0, 30.0))), None);
726 assert!(state.is_open());
727 assert!(state.is_open_on(&()));
728 assert_eq!(state.position(), Some(Point::new(40.0, 30.0)));
729 assert_eq!(state.menu().highlighted(), None);
730 }
731
732 #[test]
733 fn a_second_right_click_moves_the_menu_and_resets_it() {
734 let mut state = state();
735 let _ = state.update(Event::Open((), Point::new(40.0, 30.0)));
736 let _ = state.update(Event::Menu(dropdown_menu::Event::Highlight(4)));
737 assert_eq!(state.menu().depth(), 2);
738 let _ = state.update(Event::Open((), Point::new(5.0, 6.0)));
739 assert_eq!(state.position(), Some(Point::new(5.0, 6.0)));
740 assert_eq!(state.menu().depth(), 1);
741 assert_eq!(state.menu().highlighted(), None);
742 }
743
744 #[test]
745 fn keyboard_opens_below_the_area_on_the_first_item() {
746 let mut state = state();
747 let _ = state.update(Event::Open((), Point::new(40.0, 30.0)));
748 let _ = state.update(Event::OpenFromKeyboard(()));
749 assert_eq!(state.position(), None);
750 assert_eq!(state.menu().highlighted(), Some(1));
751 }
752
753 #[test]
754 fn keyboard_placement_sits_below_the_area_and_pointer_placement_at_the_point() {
755 let below = placement(None);
756 assert_eq!(
757 below,
758 Placement::new(Side::Bottom, Align::Start).gap(KEYBOARD_GAP)
759 );
760 let at_pointer = placement(Some(Point::new(3.0, 4.0)));
761 assert_eq!(
762 at_pointer,
763 Placement::new(Side::Bottom, Align::Start).gap(0.0)
764 );
765 }
766
767 #[test]
768 fn one_state_serves_many_rows() {
769 let mut state = rows();
770 let _ = state.update(Event::Open("a.txt", Point::new(10.0, 8.0)));
771 assert!(state.is_open_on(&"a.txt"));
772 assert!(!state.is_open_on(&"b.txt"));
773
774 let _ = state.update(Event::OpenFromKeyboard("b.txt"));
775 assert!(state.is_open_on(&"b.txt"));
776 assert!(!state.is_open_on(&"a.txt"));
777 assert_eq!(state.target(), Some(&"b.txt"));
778 }
779
780 #[test]
781 fn the_target_outlives_the_menu_so_a_choice_knows_its_row() {
782 let mut state = rows();
783 let _ = state.update(Event::Open("b.txt", Point::ORIGIN));
784 assert_eq!(
785 state.update(Event::Menu(dropdown_menu::Event::Activate(6))),
786 Some(Output::Activated(6))
787 );
788 assert!(!state.is_open());
789 assert!(!state.is_open_on(&"b.txt"));
790 assert_eq!(state.target(), Some(&"b.txt"));
791 }
792
793 #[test]
794 fn the_opening_chord_opens_on_the_row_it_is_given() {
795 let state = rows();
796 let open = keys::Event {
797 key: Key::Named(Named::F10),
798 modifiers: Modifiers::SHIFT,
799 };
800 assert_eq!(
801 state.key_event(&default_keymap(), &open, "c.txt"),
802 Some(Event::OpenFromKeyboard("c.txt"))
803 );
804 }
805
806 #[test]
807 fn menu_events_are_ignored_while_closed() {
808 let mut state = state();
809 assert_eq!(state.update(Event::Menu(dropdown_menu::Event::Next)), None);
810 assert!(!state.is_open());
811 assert_eq!(
812 state.update(Event::Menu(dropdown_menu::Event::Activate(1))),
813 None
814 );
815 }
816
817 #[test]
818 fn choosing_items_returns_outputs_and_closes() {
819 let mut state = state();
820 let _ = state.update(Event::Open((), Point::ORIGIN));
821 assert_eq!(
822 state.update(Event::Menu(dropdown_menu::Event::Activate(6))),
823 Some(Output::Toggled(6, false))
824 );
825 assert!(!state.is_open());
826 assert!(!state.is_checked(6));
827 state.set_checked(6, true);
828 assert!(state.is_checked(6));
829 state.set_disabled(3, true);
830 assert!(state.menu().item(3).is_some_and(|item| item.disabled));
831 }
832
833 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
834 let chord: Chord = chord.parse().unwrap();
835 keymap.resolve(chord.key(), chord.modifiers())
836 }
837
838 #[test]
839 fn default_keymap_adds_open_to_the_menu_keys() {
840 let keymap = default_keymap();
841 assert_eq!(press(&keymap, "Shift+F10"), Some(Action::Open));
842 assert_eq!(press(&keymap, "Menu"), Some(Action::Open));
843 assert_eq!(press(&keymap, "F10"), None);
844 assert_eq!(press(&keymap, "ArrowDown"), Some(Action::Next));
845 assert_eq!(press(&keymap, "ArrowUp"), Some(Action::Previous));
846 assert_eq!(press(&keymap, "Home"), Some(Action::First));
847 assert_eq!(press(&keymap, "End"), Some(Action::Last));
848 assert_eq!(press(&keymap, "Enter"), Some(Action::Activate));
849 assert_eq!(press(&keymap, "Space"), Some(Action::Activate));
850 assert_eq!(press(&keymap, "Escape"), Some(Action::Close));
851 assert_eq!(press(&keymap, "ArrowRight"), Some(Action::OpenSubmenu));
852 assert_eq!(press(&keymap, "ArrowLeft"), Some(Action::CloseSubmenu));
853 for action in <Action as keys::Action>::ALL {
854 assert!(!keymap.chords(action).is_empty(), "{action:?}");
855 }
856 assert_eq!(Keymap::<Action>::defaults(), keymap);
857 }
858
859 #[test]
860 fn keymap_overrides_and_unbinding() {
861 let keymap = default_keymap()
862 .unbind(&"Menu".parse().unwrap())
863 .bind("Ctrl+M".parse().unwrap(), Action::Open);
864 assert_eq!(press(&keymap, "Menu"), None);
865 assert_eq!(press(&keymap, "Ctrl+M"), Some(Action::Open));
866 assert_eq!(press(&keymap, "Shift+F10"), Some(Action::Open));
867 }
868
869 #[test]
870 fn open_works_only_while_closed_and_the_rest_only_while_open() {
871 let mut state = state();
872 assert_eq!(
873 Action::Open.event(&state, ()),
874 Some(Event::OpenFromKeyboard(()))
875 );
876 assert_eq!(Action::Next.event(&state, ()), None);
877 assert_eq!(Action::Activate.event(&state, ()), None);
878
879 let _ = state.update(Event::OpenFromKeyboard(()));
880 assert_eq!(Action::Open.event(&state, ()), None);
881 assert_eq!(
882 Action::Next.event(&state, ()),
883 Some(Event::Menu(dropdown_menu::Event::Next))
884 );
885 assert_eq!(
886 Action::Close.event(&state, ()),
887 Some(Event::Menu(dropdown_menu::Event::Close))
888 );
889 }
890
891 #[test]
892 fn every_action_round_trips_through_the_menu_action() {
893 for &action in <Action as keys::Action>::ALL {
894 match action.menu_action() {
895 Some(menu) => assert_eq!(Action::from(menu), action),
896 None => assert_eq!(action, Action::Open),
897 }
898 assert!(keys::Action::description(action).ends_with('.'));
899 assert!(!keys::Action::name(action).is_empty());
900 }
901 }
902
903 #[test]
904 fn key_event_uses_the_keymap_then_typeahead() {
905 let keymap = default_keymap();
906 let mut state = state();
907 let letter = keys::Event {
908 key: Key::Character("r".into()),
909 modifiers: Modifiers::empty(),
910 };
911 assert_eq!(state.key_event(&keymap, &letter, ()), None);
912 let _ = state.update(Event::OpenFromKeyboard(()));
913 assert_eq!(
914 state.key_event(&keymap, &letter, ()),
915 Some(Event::Menu(dropdown_menu::Event::Typeahead('r')))
916 );
917 let menu_key = keys::Event {
918 key: Key::Named(Named::ContextMenu),
919 modifiers: Modifiers::empty(),
920 };
921 assert_eq!(state.key_event(&keymap, &menu_key, ()), None);
922 }
923
924 const FINGER: Finger = Finger(7);
925
926 #[test]
927 fn a_long_press_fires_once_after_the_delay_at_its_origin() {
928 let start = Instant::now();
929 let mut press = LongPress::default();
930 assert_eq!(press.redraw(), None);
931 assert_eq!(press.tick(start), None);
932
933 press.press(FINGER, Point::new(12.0, 8.0));
934 assert_eq!(press.redraw(), Some(window::RedrawRequest::NextFrame));
935 assert_eq!(press.tick(start), None);
936 assert_eq!(
937 press.redraw(),
938 Some(window::RedrawRequest::At(start + LONG_PRESS))
939 );
940 assert_eq!(press.tick(start + Duration::from_millis(499)), None);
941 assert_eq!(press.tick(start + LONG_PRESS), Some(Point::new(12.0, 8.0)));
942 assert_eq!(press.tick(start + LONG_PRESS * 2), None);
943 assert_eq!(press.redraw(), None);
944 assert!(press.lift(FINGER), "the lift after firing is swallowed");
945 assert!(!press.lift(FINGER));
946 }
947
948 #[test]
949 fn lifting_early_is_a_tap() {
950 let start = Instant::now();
951 let mut press = LongPress::default();
952 press.press(FINGER, Point::ORIGIN);
953 let _ = press.tick(start);
954 assert!(!press.lift(FINGER));
955 assert_eq!(press.tick(start + LONG_PRESS), None);
956 }
957
958 #[test]
959 fn drifting_beyond_the_slop_cancels_but_a_wobble_does_not() {
960 let start = Instant::now();
961 let mut press = LongPress::default();
962 press.press(FINGER, Point::new(50.0, 50.0));
963 let _ = press.tick(start);
964 press.moved(FINGER, Point::new(56.0, 54.0));
965 press.moved(Finger(8), Point::new(200.0, 200.0));
966 assert!(press.tick(start + LONG_PRESS).is_some());
967
968 press.press(FINGER, Point::new(50.0, 50.0));
969 let _ = press.tick(start);
970 press.moved(FINGER, Point::new(50.0, 50.0 + LONG_PRESS_SLOP + 1.0));
971 assert_eq!(press.tick(start + LONG_PRESS), None);
972 assert_eq!(press.redraw(), None);
973 }
974
975 #[test]
976 fn another_finger_lifting_leaves_the_press_alone() {
977 let start = Instant::now();
978 let mut press = LongPress::default();
979 press.press(FINGER, Point::ORIGIN);
980 let _ = press.tick(start);
981 assert!(!press.lift(Finger(8)));
982 assert!(press.tick(start + LONG_PRESS).is_some());
983 }
984
985 #[test]
986 fn builder_defaults() {
987 let state = state();
988 let menu: ContextMenu<'_, u8, ()> = context_menu(&state, text("Area"));
989 assert_eq!(menu.width, WIDTH);
990 assert_eq!(menu.keymap, default_keymap());
991 assert!(menu.on_event.is_none());
992 let menu: ContextMenu<'_, u8, ()> = context_menu(&state, text("Area"))
993 .width(180.0)
994 .keymap(Keymap::new())
995 .on_event(|_| ());
996 assert_eq!(menu.width, 180.0);
997 assert!(menu.keymap.is_empty());
998 assert!(menu.on_event.is_some());
999
1000 let rows = rows();
1001 let row: ContextMenu<'_, u8, (), &str> = keyed(&rows, "a.txt", text("a.txt"));
1002 assert_eq!(row.key, "a.txt");
1003 }
1004}