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 .fit_height(true)
473 .dismiss_on_anchor_press(true)
474 .dismiss_keys([]);
475 if let Some(position) = state.position() {
476 anchored = anchored.at(position);
477 }
478 let Some(on_event) = on_event.filter(|_| open) else {
479 return anchored.into();
480 };
481 anchored
482 .on_dismiss(on_event(Event::Menu(dropdown_menu::Event::Close)))
483 .on_key(move |press| state.key_event(&keymap, press, key.clone()).map(&*on_event))
484 .into()
485 }
486}
487
488struct Area<'a, Message> {
491 content: Element<'a, Message>,
492 on_open: Option<Box<dyn Fn(Point) -> Message + 'a>>,
493}
494
495fn relative(cursor: mouse::Cursor, bounds: Rectangle) -> Option<Point> {
497 let position = cursor.position()?;
498 Some(Point::new(position.x - bounds.x, position.y - bounds.y))
499}
500
501impl<Message> Area<'_, Message> {
502 fn long_press(
506 &self,
507 press: &mut LongPress,
508 event: &IcedEvent,
509 bounds: Rectangle,
510 cursor: mouse::Cursor,
511 shell: &mut Shell<'_, Message>,
512 ) {
513 let Some(on_open) = &self.on_open else {
514 return;
515 };
516 match event {
517 IcedEvent::Touch(touch::Event::FingerPressed { id, .. }) => {
518 if let Some(position) = cursor.position_in(bounds) {
519 press.press(*id, position);
520 }
521 }
522 IcedEvent::Touch(touch::Event::FingerMoved { id, .. }) => {
523 if let Some(position) = relative(cursor, bounds) {
524 press.moved(*id, position);
525 }
526 }
527 IcedEvent::Window(window::Event::RedrawRequested(now)) => {
528 if let Some(position) = press.tick(*now) {
529 shell.publish(on_open(position));
530 }
531 }
532 _ => {}
533 }
534 if let Some(request) = press.redraw() {
535 shell.request_redraw_at(request);
536 }
537 }
538}
539
540impl<Message> Widget<Message, Theme, Renderer> for Area<'_, Message> {
541 fn size(&self) -> Size<Length> {
542 self.content.as_widget().size()
543 }
544
545 fn size_hint(&self) -> Size<Length> {
546 self.content.as_widget().size_hint()
547 }
548
549 fn tag(&self) -> tree::Tag {
550 tree::Tag::of::<LongPress>()
551 }
552
553 fn state(&self) -> tree::State {
554 tree::State::new(LongPress::default())
555 }
556
557 fn children(&self) -> Vec<Tree> {
558 vec![Tree::new(&self.content)]
559 }
560
561 fn diff(&self, tree: &mut Tree) {
562 tree.diff_children(std::slice::from_ref(&self.content));
563 }
564
565 fn layout(
566 &mut self,
567 tree: &mut Tree,
568 renderer: &Renderer,
569 limits: &layout::Limits,
570 ) -> layout::Node {
571 self.content
572 .as_widget_mut()
573 .layout(&mut tree.children[0], renderer, limits)
574 }
575
576 fn draw(
577 &self,
578 tree: &Tree,
579 renderer: &mut Renderer,
580 theme: &Theme,
581 style: &renderer::Style,
582 layout: Layout<'_>,
583 cursor: mouse::Cursor,
584 viewport: &Rectangle,
585 ) {
586 self.content.as_widget().draw(
587 &tree.children[0],
588 renderer,
589 theme,
590 style,
591 layout,
592 cursor,
593 viewport,
594 );
595 }
596
597 fn operate(
598 &mut self,
599 tree: &mut Tree,
600 layout: Layout<'_>,
601 renderer: &Renderer,
602 operation: &mut dyn Operation,
603 ) {
604 self.content
605 .as_widget_mut()
606 .operate(&mut tree.children[0], layout, renderer, operation);
607 }
608
609 fn update(
610 &mut self,
611 tree: &mut Tree,
612 event: &IcedEvent,
613 layout: Layout<'_>,
614 cursor: mouse::Cursor,
615 renderer: &Renderer,
616 clipboard: &mut dyn Clipboard,
617 shell: &mut Shell<'_, Message>,
618 viewport: &Rectangle,
619 ) {
620 let press = tree.state.downcast_mut::<LongPress>();
621 if let IcedEvent::Touch(
623 touch::Event::FingerLifted { id, .. } | touch::Event::FingerLost { id, .. },
624 ) = event
625 && press.lift(*id)
626 {
627 shell.capture_event();
628 return;
629 }
630 self.long_press(press, event, layout.bounds(), cursor, shell);
631
632 self.content.as_widget_mut().update(
633 &mut tree.children[0],
634 event,
635 layout,
636 cursor,
637 renderer,
638 clipboard,
639 shell,
640 viewport,
641 );
642 if shell.is_event_captured() {
643 return;
644 }
645 let IcedEvent::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) = event else {
646 return;
647 };
648 let (Some(on_open), Some(position)) = (&self.on_open, cursor.position_in(layout.bounds()))
649 else {
650 return;
651 };
652 shell.publish(on_open(position));
653 shell.capture_event();
654 }
655
656 fn mouse_interaction(
657 &self,
658 tree: &Tree,
659 layout: Layout<'_>,
660 cursor: mouse::Cursor,
661 viewport: &Rectangle,
662 renderer: &Renderer,
663 ) -> mouse::Interaction {
664 self.content.as_widget().mouse_interaction(
665 &tree.children[0],
666 layout,
667 cursor,
668 viewport,
669 renderer,
670 )
671 }
672
673 fn overlay<'b>(
674 &'b mut self,
675 tree: &'b mut Tree,
676 layout: Layout<'b>,
677 renderer: &Renderer,
678 viewport: &Rectangle,
679 translation: Vector,
680 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
681 self.content.as_widget_mut().overlay(
682 &mut tree.children[0],
683 layout,
684 renderer,
685 viewport,
686 translation,
687 )
688 }
689}
690
691#[cfg(test)]
692mod tests {
693 use super::*;
694 use crate::overlay::dropdown_menu::{checkbox_item, item, separator, submenu};
695 use iced::keyboard::{Key, Modifiers};
696 use iced::widget::text;
697
698 fn state() -> State<u8> {
699 State::new([
700 item(1, "Back"),
701 item(2, "Forward").disabled(true),
702 item(3, "Reload"),
703 submenu(4, "More tools", [item(5, "Save page")]),
704 separator(),
705 checkbox_item(6, "Show bookmarks", true),
706 ])
707 }
708
709 fn rows() -> State<u8, &'static str> {
710 State::new([item(1, "Open"), item(3, "Rename"), item(6, "Delete")])
711 }
712
713 #[test]
714 fn starts_closed_with_no_target() {
715 let state = state();
716 assert!(!state.is_open());
717 assert_eq!(state.position(), None);
718 assert_eq!(state.target(), None);
719 assert!(!state.is_open_on(&()));
720 assert_eq!(state.menu().entries().len(), 6);
721 }
722
723 #[test]
724 fn right_click_opens_at_the_point_without_a_highlight() {
725 let mut state = state();
726 assert_eq!(state.update(Event::Open((), Point::new(40.0, 30.0))), None);
727 assert!(state.is_open());
728 assert!(state.is_open_on(&()));
729 assert_eq!(state.position(), Some(Point::new(40.0, 30.0)));
730 assert_eq!(state.menu().highlighted(), None);
731 }
732
733 #[test]
734 fn a_second_right_click_moves_the_menu_and_resets_it() {
735 let mut state = state();
736 let _ = state.update(Event::Open((), Point::new(40.0, 30.0)));
737 let _ = state.update(Event::Menu(dropdown_menu::Event::Highlight(4)));
738 assert_eq!(state.menu().depth(), 2);
739 let _ = state.update(Event::Open((), Point::new(5.0, 6.0)));
740 assert_eq!(state.position(), Some(Point::new(5.0, 6.0)));
741 assert_eq!(state.menu().depth(), 1);
742 assert_eq!(state.menu().highlighted(), None);
743 }
744
745 #[test]
746 fn keyboard_opens_below_the_area_on_the_first_item() {
747 let mut state = state();
748 let _ = state.update(Event::Open((), Point::new(40.0, 30.0)));
749 let _ = state.update(Event::OpenFromKeyboard(()));
750 assert_eq!(state.position(), None);
751 assert_eq!(state.menu().highlighted(), Some(1));
752 }
753
754 #[test]
755 fn keyboard_placement_sits_below_the_area_and_pointer_placement_at_the_point() {
756 let below = placement(None);
757 assert_eq!(
758 below,
759 Placement::new(Side::Bottom, Align::Start).gap(KEYBOARD_GAP)
760 );
761 let at_pointer = placement(Some(Point::new(3.0, 4.0)));
762 assert_eq!(
763 at_pointer,
764 Placement::new(Side::Bottom, Align::Start).gap(0.0)
765 );
766 }
767
768 #[test]
769 fn one_state_serves_many_rows() {
770 let mut state = rows();
771 let _ = state.update(Event::Open("a.txt", Point::new(10.0, 8.0)));
772 assert!(state.is_open_on(&"a.txt"));
773 assert!(!state.is_open_on(&"b.txt"));
774
775 let _ = state.update(Event::OpenFromKeyboard("b.txt"));
776 assert!(state.is_open_on(&"b.txt"));
777 assert!(!state.is_open_on(&"a.txt"));
778 assert_eq!(state.target(), Some(&"b.txt"));
779 }
780
781 #[test]
782 fn the_target_outlives_the_menu_so_a_choice_knows_its_row() {
783 let mut state = rows();
784 let _ = state.update(Event::Open("b.txt", Point::ORIGIN));
785 assert_eq!(
786 state.update(Event::Menu(dropdown_menu::Event::Activate(6))),
787 Some(Output::Activated(6))
788 );
789 assert!(!state.is_open());
790 assert!(!state.is_open_on(&"b.txt"));
791 assert_eq!(state.target(), Some(&"b.txt"));
792 }
793
794 #[test]
795 fn the_opening_chord_opens_on_the_row_it_is_given() {
796 let state = rows();
797 let open = keys::Event {
798 key: Key::Named(Named::F10),
799 modifiers: Modifiers::SHIFT,
800 };
801 assert_eq!(
802 state.key_event(&default_keymap(), &open, "c.txt"),
803 Some(Event::OpenFromKeyboard("c.txt"))
804 );
805 }
806
807 #[test]
808 fn menu_events_are_ignored_while_closed() {
809 let mut state = state();
810 assert_eq!(state.update(Event::Menu(dropdown_menu::Event::Next)), None);
811 assert!(!state.is_open());
812 assert_eq!(
813 state.update(Event::Menu(dropdown_menu::Event::Activate(1))),
814 None
815 );
816 }
817
818 #[test]
819 fn choosing_items_returns_outputs_and_closes() {
820 let mut state = state();
821 let _ = state.update(Event::Open((), Point::ORIGIN));
822 assert_eq!(
823 state.update(Event::Menu(dropdown_menu::Event::Activate(6))),
824 Some(Output::Toggled(6, false))
825 );
826 assert!(!state.is_open());
827 assert!(!state.is_checked(6));
828 state.set_checked(6, true);
829 assert!(state.is_checked(6));
830 state.set_disabled(3, true);
831 assert!(state.menu().item(3).is_some_and(|item| item.disabled));
832 }
833
834 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
835 let chord: Chord = chord.parse().unwrap();
836 keymap.resolve(chord.key(), chord.modifiers())
837 }
838
839 #[test]
840 fn default_keymap_adds_open_to_the_menu_keys() {
841 let keymap = default_keymap();
842 assert_eq!(press(&keymap, "Shift+F10"), Some(Action::Open));
843 assert_eq!(press(&keymap, "Menu"), Some(Action::Open));
844 assert_eq!(press(&keymap, "F10"), None);
845 assert_eq!(press(&keymap, "ArrowDown"), Some(Action::Next));
846 assert_eq!(press(&keymap, "ArrowUp"), Some(Action::Previous));
847 assert_eq!(press(&keymap, "Home"), Some(Action::First));
848 assert_eq!(press(&keymap, "End"), Some(Action::Last));
849 assert_eq!(press(&keymap, "Enter"), Some(Action::Activate));
850 assert_eq!(press(&keymap, "Space"), Some(Action::Activate));
851 assert_eq!(press(&keymap, "Escape"), Some(Action::Close));
852 assert_eq!(press(&keymap, "ArrowRight"), Some(Action::OpenSubmenu));
853 assert_eq!(press(&keymap, "ArrowLeft"), Some(Action::CloseSubmenu));
854 for action in <Action as keys::Action>::ALL {
855 assert!(!keymap.chords(action).is_empty(), "{action:?}");
856 }
857 assert_eq!(Keymap::<Action>::defaults(), keymap);
858 }
859
860 #[test]
861 fn keymap_overrides_and_unbinding() {
862 let keymap = default_keymap()
863 .unbind(&"Menu".parse().unwrap())
864 .bind("Ctrl+M".parse().unwrap(), Action::Open);
865 assert_eq!(press(&keymap, "Menu"), None);
866 assert_eq!(press(&keymap, "Ctrl+M"), Some(Action::Open));
867 assert_eq!(press(&keymap, "Shift+F10"), Some(Action::Open));
868 }
869
870 #[test]
871 fn open_works_only_while_closed_and_the_rest_only_while_open() {
872 let mut state = state();
873 assert_eq!(
874 Action::Open.event(&state, ()),
875 Some(Event::OpenFromKeyboard(()))
876 );
877 assert_eq!(Action::Next.event(&state, ()), None);
878 assert_eq!(Action::Activate.event(&state, ()), None);
879
880 let _ = state.update(Event::OpenFromKeyboard(()));
881 assert_eq!(Action::Open.event(&state, ()), None);
882 assert_eq!(
883 Action::Next.event(&state, ()),
884 Some(Event::Menu(dropdown_menu::Event::Next))
885 );
886 assert_eq!(
887 Action::Close.event(&state, ()),
888 Some(Event::Menu(dropdown_menu::Event::Close))
889 );
890 }
891
892 #[test]
893 fn every_action_round_trips_through_the_menu_action() {
894 for &action in <Action as keys::Action>::ALL {
895 match action.menu_action() {
896 Some(menu) => assert_eq!(Action::from(menu), action),
897 None => assert_eq!(action, Action::Open),
898 }
899 assert!(keys::Action::description(action).ends_with('.'));
900 assert!(!keys::Action::name(action).is_empty());
901 }
902 }
903
904 #[test]
905 fn key_event_uses_the_keymap_then_typeahead() {
906 let keymap = default_keymap();
907 let mut state = state();
908 let letter = keys::Event {
909 key: Key::Character("r".into()),
910 modifiers: Modifiers::empty(),
911 };
912 assert_eq!(state.key_event(&keymap, &letter, ()), None);
913 let _ = state.update(Event::OpenFromKeyboard(()));
914 assert_eq!(
915 state.key_event(&keymap, &letter, ()),
916 Some(Event::Menu(dropdown_menu::Event::Typeahead('r')))
917 );
918 let menu_key = keys::Event {
919 key: Key::Named(Named::ContextMenu),
920 modifiers: Modifiers::empty(),
921 };
922 assert_eq!(state.key_event(&keymap, &menu_key, ()), None);
923 }
924
925 const FINGER: Finger = Finger(7);
926
927 #[test]
928 fn a_long_press_fires_once_after_the_delay_at_its_origin() {
929 let start = Instant::now();
930 let mut press = LongPress::default();
931 assert_eq!(press.redraw(), None);
932 assert_eq!(press.tick(start), None);
933
934 press.press(FINGER, Point::new(12.0, 8.0));
935 assert_eq!(press.redraw(), Some(window::RedrawRequest::NextFrame));
936 assert_eq!(press.tick(start), None);
937 assert_eq!(
938 press.redraw(),
939 Some(window::RedrawRequest::At(start + LONG_PRESS))
940 );
941 assert_eq!(press.tick(start + Duration::from_millis(499)), None);
942 assert_eq!(press.tick(start + LONG_PRESS), Some(Point::new(12.0, 8.0)));
943 assert_eq!(press.tick(start + LONG_PRESS * 2), None);
944 assert_eq!(press.redraw(), None);
945 assert!(press.lift(FINGER), "the lift after firing is swallowed");
946 assert!(!press.lift(FINGER));
947 }
948
949 #[test]
950 fn lifting_early_is_a_tap() {
951 let start = Instant::now();
952 let mut press = LongPress::default();
953 press.press(FINGER, Point::ORIGIN);
954 let _ = press.tick(start);
955 assert!(!press.lift(FINGER));
956 assert_eq!(press.tick(start + LONG_PRESS), None);
957 }
958
959 #[test]
960 fn drifting_beyond_the_slop_cancels_but_a_wobble_does_not() {
961 let start = Instant::now();
962 let mut press = LongPress::default();
963 press.press(FINGER, Point::new(50.0, 50.0));
964 let _ = press.tick(start);
965 press.moved(FINGER, Point::new(56.0, 54.0));
966 press.moved(Finger(8), Point::new(200.0, 200.0));
967 assert!(press.tick(start + LONG_PRESS).is_some());
968
969 press.press(FINGER, Point::new(50.0, 50.0));
970 let _ = press.tick(start);
971 press.moved(FINGER, Point::new(50.0, 50.0 + LONG_PRESS_SLOP + 1.0));
972 assert_eq!(press.tick(start + LONG_PRESS), None);
973 assert_eq!(press.redraw(), None);
974 }
975
976 #[test]
977 fn another_finger_lifting_leaves_the_press_alone() {
978 let start = Instant::now();
979 let mut press = LongPress::default();
980 press.press(FINGER, Point::ORIGIN);
981 let _ = press.tick(start);
982 assert!(!press.lift(Finger(8)));
983 assert!(press.tick(start + LONG_PRESS).is_some());
984 }
985
986 #[test]
987 fn builder_defaults() {
988 let state = state();
989 let menu: ContextMenu<'_, u8, ()> = context_menu(&state, text("Area"));
990 assert_eq!(menu.width, WIDTH);
991 assert_eq!(menu.keymap, default_keymap());
992 assert!(menu.on_event.is_none());
993 let menu: ContextMenu<'_, u8, ()> = context_menu(&state, text("Area"))
994 .width(180.0)
995 .keymap(Keymap::new())
996 .on_event(|_| ());
997 assert_eq!(menu.width, 180.0);
998 assert!(menu.keymap.is_empty());
999 assert!(menu.on_event.is_some());
1000
1001 let rows = rows();
1002 let row: ContextMenu<'_, u8, (), &str> = keyed(&rows, "a.txt", text("a.txt"));
1003 assert_eq!(row.key, "a.txt");
1004 }
1005}