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(
624 touch::Event::FingerLifted { id, position } | touch::Event::FingerLost { id, position },
625 ) = event
626 && press.lift(*id)
627 {
628 let lost = IcedEvent::Touch(touch::Event::FingerLost {
629 id: *id,
630 position: *position,
631 });
632 self.content.as_widget_mut().update(
633 &mut tree.children[0],
634 &lost,
635 layout,
636 cursor,
637 renderer,
638 clipboard,
639 shell,
640 viewport,
641 );
642 shell.capture_event();
643 return;
644 }
645 self.long_press(press, event, layout.bounds(), cursor, shell);
646
647 self.content.as_widget_mut().update(
648 &mut tree.children[0],
649 event,
650 layout,
651 cursor,
652 renderer,
653 clipboard,
654 shell,
655 viewport,
656 );
657 if shell.is_event_captured() {
658 return;
659 }
660 let IcedEvent::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) = event else {
661 return;
662 };
663 let (Some(on_open), Some(position)) = (&self.on_open, cursor.position_in(layout.bounds()))
664 else {
665 return;
666 };
667 shell.publish(on_open(position));
668 shell.capture_event();
669 }
670
671 fn mouse_interaction(
672 &self,
673 tree: &Tree,
674 layout: Layout<'_>,
675 cursor: mouse::Cursor,
676 viewport: &Rectangle,
677 renderer: &Renderer,
678 ) -> mouse::Interaction {
679 self.content.as_widget().mouse_interaction(
680 &tree.children[0],
681 layout,
682 cursor,
683 viewport,
684 renderer,
685 )
686 }
687
688 fn overlay<'b>(
689 &'b mut self,
690 tree: &'b mut Tree,
691 layout: Layout<'b>,
692 renderer: &Renderer,
693 viewport: &Rectangle,
694 translation: Vector,
695 ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
696 self.content.as_widget_mut().overlay(
697 &mut tree.children[0],
698 layout,
699 renderer,
700 viewport,
701 translation,
702 )
703 }
704}
705
706#[cfg(test)]
707mod tests {
708 use super::*;
709 use crate::overlay::dropdown_menu::{checkbox_item, item, separator, submenu};
710 use iced::keyboard::{Key, Modifiers};
711 use iced::widget::text;
712
713 fn state() -> State<u8> {
714 State::new([
715 item(1, "Back"),
716 item(2, "Forward").disabled(true),
717 item(3, "Reload"),
718 submenu(4, "More tools", [item(5, "Save page")]),
719 separator(),
720 checkbox_item(6, "Show bookmarks", true),
721 ])
722 }
723
724 fn rows() -> State<u8, &'static str> {
725 State::new([item(1, "Open"), item(3, "Rename"), item(6, "Delete")])
726 }
727
728 #[test]
729 fn starts_closed_with_no_target() {
730 let state = state();
731 assert!(!state.is_open());
732 assert_eq!(state.position(), None);
733 assert_eq!(state.target(), None);
734 assert!(!state.is_open_on(&()));
735 assert_eq!(state.menu().entries().len(), 6);
736 }
737
738 #[test]
739 fn right_click_opens_at_the_point_without_a_highlight() {
740 let mut state = state();
741 assert_eq!(state.update(Event::Open((), Point::new(40.0, 30.0))), None);
742 assert!(state.is_open());
743 assert!(state.is_open_on(&()));
744 assert_eq!(state.position(), Some(Point::new(40.0, 30.0)));
745 assert_eq!(state.menu().highlighted(), None);
746 }
747
748 #[test]
749 fn a_second_right_click_moves_the_menu_and_resets_it() {
750 let mut state = state();
751 let _ = state.update(Event::Open((), Point::new(40.0, 30.0)));
752 let _ = state.update(Event::Menu(dropdown_menu::Event::Highlight(4)));
753 assert_eq!(state.menu().depth(), 2);
754 let _ = state.update(Event::Open((), Point::new(5.0, 6.0)));
755 assert_eq!(state.position(), Some(Point::new(5.0, 6.0)));
756 assert_eq!(state.menu().depth(), 1);
757 assert_eq!(state.menu().highlighted(), None);
758 }
759
760 #[test]
761 fn keyboard_opens_below_the_area_on_the_first_item() {
762 let mut state = state();
763 let _ = state.update(Event::Open((), Point::new(40.0, 30.0)));
764 let _ = state.update(Event::OpenFromKeyboard(()));
765 assert_eq!(state.position(), None);
766 assert_eq!(state.menu().highlighted(), Some(1));
767 }
768
769 #[test]
770 fn keyboard_placement_sits_below_the_area_and_pointer_placement_at_the_point() {
771 let below = placement(None);
772 assert_eq!(
773 below,
774 Placement::new(Side::Bottom, Align::Start).gap(KEYBOARD_GAP)
775 );
776 let at_pointer = placement(Some(Point::new(3.0, 4.0)));
777 assert_eq!(
778 at_pointer,
779 Placement::new(Side::Bottom, Align::Start).gap(0.0)
780 );
781 }
782
783 #[test]
784 fn one_state_serves_many_rows() {
785 let mut state = rows();
786 let _ = state.update(Event::Open("a.txt", Point::new(10.0, 8.0)));
787 assert!(state.is_open_on(&"a.txt"));
788 assert!(!state.is_open_on(&"b.txt"));
789
790 let _ = state.update(Event::OpenFromKeyboard("b.txt"));
791 assert!(state.is_open_on(&"b.txt"));
792 assert!(!state.is_open_on(&"a.txt"));
793 assert_eq!(state.target(), Some(&"b.txt"));
794 }
795
796 #[test]
797 fn the_target_outlives_the_menu_so_a_choice_knows_its_row() {
798 let mut state = rows();
799 let _ = state.update(Event::Open("b.txt", Point::ORIGIN));
800 assert_eq!(
801 state.update(Event::Menu(dropdown_menu::Event::Activate(6))),
802 Some(Output::Activated(6))
803 );
804 assert!(!state.is_open());
805 assert!(!state.is_open_on(&"b.txt"));
806 assert_eq!(state.target(), Some(&"b.txt"));
807 }
808
809 #[test]
810 fn the_opening_chord_opens_on_the_row_it_is_given() {
811 let state = rows();
812 let open = keys::Event {
813 key: Key::Named(Named::F10),
814 modifiers: Modifiers::SHIFT,
815 };
816 assert_eq!(
817 state.key_event(&default_keymap(), &open, "c.txt"),
818 Some(Event::OpenFromKeyboard("c.txt"))
819 );
820 }
821
822 #[test]
823 fn menu_events_are_ignored_while_closed() {
824 let mut state = state();
825 assert_eq!(state.update(Event::Menu(dropdown_menu::Event::Next)), None);
826 assert!(!state.is_open());
827 assert_eq!(
828 state.update(Event::Menu(dropdown_menu::Event::Activate(1))),
829 None
830 );
831 }
832
833 #[test]
834 fn choosing_items_returns_outputs_and_closes() {
835 let mut state = state();
836 let _ = state.update(Event::Open((), Point::ORIGIN));
837 assert_eq!(
838 state.update(Event::Menu(dropdown_menu::Event::Activate(6))),
839 Some(Output::Toggled(6, false))
840 );
841 assert!(!state.is_open());
842 assert!(!state.is_checked(6));
843 state.set_checked(6, true);
844 assert!(state.is_checked(6));
845 state.set_disabled(3, true);
846 assert!(state.menu().item(3).is_some_and(|item| item.disabled));
847 }
848
849 fn press(keymap: &Keymap<Action>, chord: &str) -> Option<Action> {
850 let chord: Chord = chord.parse().unwrap();
851 keymap.resolve(chord.key(), chord.modifiers())
852 }
853
854 #[test]
855 fn default_keymap_adds_open_to_the_menu_keys() {
856 let keymap = default_keymap();
857 assert_eq!(press(&keymap, "Shift+F10"), Some(Action::Open));
858 assert_eq!(press(&keymap, "Menu"), Some(Action::Open));
859 assert_eq!(press(&keymap, "F10"), None);
860 assert_eq!(press(&keymap, "ArrowDown"), Some(Action::Next));
861 assert_eq!(press(&keymap, "ArrowUp"), Some(Action::Previous));
862 assert_eq!(press(&keymap, "Home"), Some(Action::First));
863 assert_eq!(press(&keymap, "End"), Some(Action::Last));
864 assert_eq!(press(&keymap, "Enter"), Some(Action::Activate));
865 assert_eq!(press(&keymap, "Space"), Some(Action::Activate));
866 assert_eq!(press(&keymap, "Escape"), Some(Action::Close));
867 assert_eq!(press(&keymap, "ArrowRight"), Some(Action::OpenSubmenu));
868 assert_eq!(press(&keymap, "ArrowLeft"), Some(Action::CloseSubmenu));
869 for action in <Action as keys::Action>::ALL {
870 assert!(!keymap.chords(action).is_empty(), "{action:?}");
871 }
872 assert_eq!(Keymap::<Action>::defaults(), keymap);
873 }
874
875 #[test]
876 fn keymap_overrides_and_unbinding() {
877 let keymap = default_keymap()
878 .unbind(&"Menu".parse().unwrap())
879 .bind("Ctrl+M".parse().unwrap(), Action::Open);
880 assert_eq!(press(&keymap, "Menu"), None);
881 assert_eq!(press(&keymap, "Ctrl+M"), Some(Action::Open));
882 assert_eq!(press(&keymap, "Shift+F10"), Some(Action::Open));
883 }
884
885 #[test]
886 fn open_works_only_while_closed_and_the_rest_only_while_open() {
887 let mut state = state();
888 assert_eq!(
889 Action::Open.event(&state, ()),
890 Some(Event::OpenFromKeyboard(()))
891 );
892 assert_eq!(Action::Next.event(&state, ()), None);
893 assert_eq!(Action::Activate.event(&state, ()), None);
894
895 let _ = state.update(Event::OpenFromKeyboard(()));
896 assert_eq!(Action::Open.event(&state, ()), None);
897 assert_eq!(
898 Action::Next.event(&state, ()),
899 Some(Event::Menu(dropdown_menu::Event::Next))
900 );
901 assert_eq!(
902 Action::Close.event(&state, ()),
903 Some(Event::Menu(dropdown_menu::Event::Close))
904 );
905 }
906
907 #[test]
908 fn every_action_round_trips_through_the_menu_action() {
909 for &action in <Action as keys::Action>::ALL {
910 match action.menu_action() {
911 Some(menu) => assert_eq!(Action::from(menu), action),
912 None => assert_eq!(action, Action::Open),
913 }
914 assert!(keys::Action::description(action).ends_with('.'));
915 assert!(!keys::Action::name(action).is_empty());
916 }
917 }
918
919 #[test]
920 fn key_event_uses_the_keymap_then_typeahead() {
921 let keymap = default_keymap();
922 let mut state = state();
923 let letter = keys::Event {
924 key: Key::Character("r".into()),
925 modifiers: Modifiers::empty(),
926 };
927 assert_eq!(state.key_event(&keymap, &letter, ()), None);
928 let _ = state.update(Event::OpenFromKeyboard(()));
929 assert_eq!(
930 state.key_event(&keymap, &letter, ()),
931 Some(Event::Menu(dropdown_menu::Event::Typeahead('r')))
932 );
933 let menu_key = keys::Event {
934 key: Key::Named(Named::ContextMenu),
935 modifiers: Modifiers::empty(),
936 };
937 assert_eq!(state.key_event(&keymap, &menu_key, ()), None);
938 }
939
940 const FINGER: Finger = Finger(7);
941
942 #[test]
943 fn a_long_press_fires_once_after_the_delay_at_its_origin() {
944 let start = Instant::now();
945 let mut press = LongPress::default();
946 assert_eq!(press.redraw(), None);
947 assert_eq!(press.tick(start), None);
948
949 press.press(FINGER, Point::new(12.0, 8.0));
950 assert_eq!(press.redraw(), Some(window::RedrawRequest::NextFrame));
951 assert_eq!(press.tick(start), None);
952 assert_eq!(
953 press.redraw(),
954 Some(window::RedrawRequest::At(start + LONG_PRESS))
955 );
956 assert_eq!(press.tick(start + Duration::from_millis(499)), None);
957 assert_eq!(press.tick(start + LONG_PRESS), Some(Point::new(12.0, 8.0)));
958 assert_eq!(press.tick(start + LONG_PRESS * 2), None);
959 assert_eq!(press.redraw(), None);
960 assert!(press.lift(FINGER), "the lift after firing is swallowed");
961 assert!(!press.lift(FINGER));
962 }
963
964 #[test]
965 fn lifting_early_is_a_tap() {
966 let start = Instant::now();
967 let mut press = LongPress::default();
968 press.press(FINGER, Point::ORIGIN);
969 let _ = press.tick(start);
970 assert!(!press.lift(FINGER));
971 assert_eq!(press.tick(start + LONG_PRESS), None);
972 }
973
974 #[test]
975 fn drifting_beyond_the_slop_cancels_but_a_wobble_does_not() {
976 let start = Instant::now();
977 let mut press = LongPress::default();
978 press.press(FINGER, Point::new(50.0, 50.0));
979 let _ = press.tick(start);
980 press.moved(FINGER, Point::new(56.0, 54.0));
981 press.moved(Finger(8), Point::new(200.0, 200.0));
982 assert!(press.tick(start + LONG_PRESS).is_some());
983
984 press.press(FINGER, Point::new(50.0, 50.0));
985 let _ = press.tick(start);
986 press.moved(FINGER, Point::new(50.0, 50.0 + LONG_PRESS_SLOP + 1.0));
987 assert_eq!(press.tick(start + LONG_PRESS), None);
988 assert_eq!(press.redraw(), None);
989 }
990
991 #[test]
992 fn another_finger_lifting_leaves_the_press_alone() {
993 let start = Instant::now();
994 let mut press = LongPress::default();
995 press.press(FINGER, Point::ORIGIN);
996 let _ = press.tick(start);
997 assert!(!press.lift(Finger(8)));
998 assert!(press.tick(start + LONG_PRESS).is_some());
999 }
1000
1001 #[test]
1002 fn builder_defaults() {
1003 let state = state();
1004 let menu: ContextMenu<'_, u8, ()> = context_menu(&state, text("Area"));
1005 assert_eq!(menu.width, WIDTH);
1006 assert_eq!(menu.keymap, default_keymap());
1007 assert!(menu.on_event.is_none());
1008 let menu: ContextMenu<'_, u8, ()> = context_menu(&state, text("Area"))
1009 .width(180.0)
1010 .keymap(Keymap::new())
1011 .on_event(|_| ());
1012 assert_eq!(menu.width, 180.0);
1013 assert!(menu.keymap.is_empty());
1014 assert!(menu.on_event.is_some());
1015
1016 let rows = rows();
1017 let row: ContextMenu<'_, u8, (), &str> = keyed(&rows, "a.txt", text("a.txt"));
1018 assert_eq!(row.key, "a.txt");
1019 }
1020}