Skip to main content

iced_cube/overlay/
context_menu.rs

1//! A menu that opens at the pointer on a right-click inside an area, or at
2//! the finger on a long press.
3//!
4//! It uses the dropdown menu's entries, rows and navigation: build the
5//! entries with [`dropdown_menu::item`]
6//! and friends. [`State`] adds where the menu opened. On a touch screen, a
7//! finger held still on the area for [`LONG_PRESS`] opens it. From the
8//! keyboard, Shift+F10 or the Menu key opens it just below the area, aligned
9//! with its start, as desktop apps do.
10//!
11//! One [`State`] can serve many areas, such as every row of a list: give
12//! each area a key with [`keyed`], and the open events carry the key of the
13//! area they came from. [`State::target`] tells the app which row a chosen
14//! item belongs to.
15//!
16//! Keyboard shortcuts resolve through a [`Keymap`] of [`Action`]s; see
17//! [`default_keymap`]. The open menu handles its own keys; the app routes
18//! the opening chord through [`State::key_event`].
19
20use 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/// Changes to a context menu. `Key` names the area an open event came from;
40/// it is `()` for a menu with a single area.
41#[derive(Debug, Clone, Copy, PartialEq)]
42pub enum Event<Id, Key = ()> {
43    /// A right-click or a long press on the area `Key` at this point,
44    /// relative to the area's top left corner. Opens the menu there with
45    /// nothing highlighted.
46    Open(Key, Point),
47    /// Opens the menu below the area `Key` on its first item.
48    OpenFromKeyboard(Key),
49    /// Navigation and choosing, as in a dropdown menu. Ignored while closed.
50    Menu(dropdown_menu::Event<Id>),
51}
52
53/// The menu, which area it opened on and where.
54#[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    /// A closed menu. Item ids must be unique across submenus.
63    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    /// The menu entries, highlight and open submenus.
72    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    /// Whether the menu is open on the area `key`.
81    pub fn is_open_on(&self, key: &Key) -> bool {
82        self.is_open() && self.target.as_ref() == Some(key)
83    }
84
85    /// The area the menu last opened on. It stays set after the menu closes,
86    /// so the app can tell which row a chosen item belongs to.
87    pub fn target(&self) -> Option<&Key> {
88        self.target.as_ref()
89    }
90
91    /// Where a right-click opened the menu, relative to the area's top left
92    /// corner, or `None` when it opened from the keyboard below the area.
93    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    /// Checks or unchecks an item. Checking a radio item unchecks its group.
102    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    /// Applies an event and returns what was chosen, if anything. Opening
111    /// on another area moves the menu there and resets it.
112    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    /// Turns a key press into an event: first through `keymap`, then, while
132    /// the menu is open, as typeahead for a letter or digit. `target` is the
133    /// area the opening chord opens the menu on, such as the focused row;
134    /// pass `()` for a menu with a single area.
135    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/// What a context menu keyboard shortcut does.
153#[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    /// The dropdown menu action this one drives, if it is not [`Action::Open`].
168    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    /// The [`Event`] this action sends to `state`, or `None` when it does
184    /// nothing: only [`Action::Open`] works while the menu is closed, and
185    /// only the others while it is open. [`Action::Open`] opens on `target`.
186    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
252/// The default context menu shortcuts: the dropdown menu's, plus
253///
254/// | Keys | Action |
255/// | --- | --- |
256/// | `Shift+F10`, `Menu` | [`Action::Open`] |
257///
258/// The open menu resolves its keys itself; pass a changed keymap with
259/// [`ContextMenu::keymap`]. While it is closed, only [`Action::Open`] does
260/// anything, and the app routes it from [`keys::subscription`] through
261/// [`State::key_event`].
262pub 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
273/// Gap between an area and a menu opened from the keyboard below it.
274pub const KEYBOARD_GAP: f32 = 4.0;
275
276/// How long a finger rests on an area before the menu opens.
277pub const LONG_PRESS: Duration = Duration::from_millis(500);
278
279/// How far, in logical pixels, a finger may drift during a long press. Any
280/// further and it counts as a scroll or a drag.
281pub const LONG_PRESS_SLOP: f32 = 10.0;
282
283/// A finger held on an area. Time only comes in through [`LongPress::tick`],
284/// which the area calls on every redraw, so the timing is testable.
285#[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    /// Starts timing a finger pressed at `position`, replacing any other.
295    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    /// Cancels the press once the finger drifts beyond [`LONG_PRESS_SLOP`].
305    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    /// Ends the press. Returns whether it had opened the menu, in which case
315    /// the lift must not reach the content as a tap.
316    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    /// Starts the clock on the first redraw after the press, and returns
326    /// where the press began once, when it has been held for [`LONG_PRESS`].
327    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    /// When the area next needs a redraw to keep timing, if it does.
340    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
353/// A context menu builder. Convert it into an [`Element`] to render.
354pub 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
376/// Makes `content` an area that opens the menu of `state` on a right-click.
377/// Without [`on_event`](ContextMenu::on_event) right-clicks do nothing.
378pub 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
385/// Makes `content` one of many areas sharing the menu of `state`, such as
386/// a row in a list. The menu shows on this area only while it is open on
387/// `key`, and the open events it sends carry `key`.
388pub 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    /// Width of the menu and its submenus.
405    pub fn width(mut self, width: f32) -> Self {
406        self.width = width;
407        self
408    }
409
410    /// Replaces the [`default_keymap`] the open menu resolves its keys with.
411    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
422/// Where the panel goes: at the right-click point, or just below the area,
423/// aligned with its start, when the menu opened from the keyboard.
424pub 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
487/// Wraps content and reports right-clicks and long presses on it with their
488/// position.
489struct Area<'a, Message> {
490    content: Element<'a, Message>,
491    on_open: Option<Box<dyn Fn(Point) -> Message + 'a>>,
492}
493
494/// Where the cursor or finger is, relative to the area's top left corner.
495fn 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    /// Times a finger held on the area and opens the menu once it has been
502    /// held long enough. The cursor follows the finger, and unlike the
503    /// event's position it is already translated inside scroll areas.
504    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        // The finger that opened the menu lifts over it, not as a tap.
621        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}