rat_menu/
popup_menu.rs

1//! Renders a popup menu.
2//!
3//! The popup menu ignores the area you give to render() completely.
4//! Instead, you need to call [constraint](PopupMenu::constraint)
5//! to give some constraints where the popup-menu should be rendered.
6//! You can also set an outer [boundary](PopupMenu::boundary) to limit
7//! the area. The size of the popup menu is fully determined by the
8//! menu-items.
9//!
10//! ```
11//! use ratatui::buffer::Buffer;
12//! use ratatui::layout::{Alignment, Rect};
13//! use ratatui::widgets::{Block, StatefulWidget};
14//! use rat_menu::menuitem::Separator;
15//! use rat_menu::popup_menu::{PopupMenu, PopupMenuState};
16//! use rat_popup::PopupConstraint;
17//!
18//! # struct State { popup: PopupMenuState }
19//! # let mut state = State { popup: Default::default() };
20//! # let mut buf = Buffer::default();
21//! # let buf = &mut buf;
22//! # let widget_area = Rect::default();
23//!
24//! PopupMenu::new()
25//!     .item_parsed("Item _1")
26//!     .separator(Separator::Plain)
27//!     .item_parsed("Item _2")
28//!     .item_parsed("Item _3")
29//!     .block(Block::bordered())
30//!     .constraint(
31//!         PopupConstraint::Above(Alignment::Left, widget_area)
32//!     )
33//!     .render(Rect::default(), buf, &mut state.popup);
34//! ```
35//!
36use crate::_private::NonExhaustive;
37use crate::event::MenuOutcome;
38use crate::util::{get_block_padding, get_block_size, revert_style};
39use crate::{MenuBuilder, MenuItem, MenuStyle, Separator};
40use rat_event::util::{MouseFlags, mouse_trap};
41use rat_event::{ConsumedEvent, HandleEvent, MouseOnly, Popup, ct_event};
42pub use rat_popup::PopupConstraint;
43use rat_popup::event::PopupOutcome;
44use rat_popup::{PopupCore, PopupCoreState};
45use ratatui::buffer::Buffer;
46use ratatui::layout::{Rect, Size};
47use ratatui::prelude::BlockExt;
48use ratatui::style::{Style, Stylize};
49use ratatui::text::{Line, Span};
50use ratatui::widgets::StatefulWidget;
51use ratatui::widgets::{Block, Padding, Widget};
52use std::cmp::max;
53use unicode_segmentation::UnicodeSegmentation;
54
55/// Popup menu.
56#[derive(Debug, Default, Clone)]
57pub struct PopupMenu<'a> {
58    pub(crate) menu: MenuBuilder<'a>,
59
60    width: Option<u16>,
61    popup: PopupCore,
62    block: Option<Block<'a>>,
63    style: Style,
64    highlight_style: Option<Style>,
65    disabled_style: Option<Style>,
66    right_style: Option<Style>,
67    focus_style: Option<Style>,
68}
69
70/// State & event handling.
71#[derive(Debug, Clone)]
72pub struct PopupMenuState {
73    /// Popup data.
74    pub popup: PopupCoreState,
75    /// Areas for each item.
76    /// __readonly__. renewed for each render.
77    pub item_areas: Vec<Rect>,
78    /// Area for the separator after each item.
79    /// The area has height 0 if there is no separator.
80    /// __readonly__. renewed for each render.
81    pub sep_areas: Vec<Rect>,
82    /// Letter navigation
83    /// __readonly__. renewed for each render.
84    pub navchar: Vec<Option<char>>,
85    /// Disabled menu-items.
86    pub disabled: Vec<bool>,
87    /// Selected item.
88    /// __read+write__
89    pub selected: Option<usize>,
90
91    /// Mouse flags
92    /// __used for mouse interaction__
93    pub mouse: MouseFlags,
94
95    pub non_exhaustive: NonExhaustive,
96}
97
98impl Default for PopupMenuState {
99    fn default() -> Self {
100        Self {
101            popup: Default::default(),
102            item_areas: Default::default(),
103            sep_areas: Default::default(),
104            navchar: Default::default(),
105            disabled: Default::default(),
106            selected: Default::default(),
107            mouse: Default::default(),
108            non_exhaustive: NonExhaustive,
109        }
110    }
111}
112
113impl PopupMenu<'_> {
114    fn size(&self) -> Size {
115        let width = if let Some(width) = self.width {
116            width
117        } else {
118            let text_width = self
119                .menu
120                .items
121                .iter()
122                .map(|v| (v.item_width() * 3) / 2 + v.right_width())
123                .max();
124            text_width.unwrap_or(10)
125        };
126        let height = self.menu.items.iter().map(MenuItem::height).sum::<u16>();
127
128        let block = get_block_size(&self.block);
129
130        #[allow(clippy::if_same_then_else)]
131        let vertical_padding = if block.height == 0 { 2 } else { 0 };
132        let horizontal_padding = 2;
133
134        Size::new(
135            width + horizontal_padding + block.width,
136            height + vertical_padding + block.height,
137        )
138    }
139
140    fn layout(&self, area: Rect, state: &mut PopupMenuState) {
141        let block = get_block_size(&self.block);
142        let inner = self.block.inner_if_some(area);
143
144        // add text padding.
145        #[allow(clippy::if_same_then_else)]
146        let vert_offset = if block.height == 0 { 1 } else { 0 };
147        let horiz_offset = 1;
148        let horiz_offset_sep = 0;
149
150        state.item_areas.clear();
151        state.sep_areas.clear();
152
153        let mut row = 0;
154
155        for item in &self.menu.items {
156            state.item_areas.push(Rect::new(
157                inner.x + horiz_offset,
158                inner.y + row + vert_offset,
159                inner.width.saturating_sub(2 * horiz_offset),
160                1,
161            ));
162            state.sep_areas.push(Rect::new(
163                inner.x + horiz_offset_sep,
164                inner.y + row + 1 + vert_offset,
165                inner.width.saturating_sub(2 * horiz_offset_sep),
166                if item.separator.is_some() { 1 } else { 0 },
167            ));
168
169            row += item.height();
170        }
171    }
172}
173
174impl<'a> PopupMenu<'a> {
175    /// New, empty.
176    pub fn new() -> Self {
177        Default::default()
178    }
179
180    /// Add an item.
181    pub fn item(mut self, item: MenuItem<'a>) -> Self {
182        self.menu.item(item);
183        self
184    }
185
186    /// Parse the text.
187    ///
188    /// __See__
189    ///
190    /// [MenuItem::new_parsed]
191    pub fn item_parsed(mut self, text: &'a str) -> Self {
192        self.menu.item_parsed(text);
193        self
194    }
195
196    /// Add a text-item.
197    pub fn item_str(mut self, txt: &'a str) -> Self {
198        self.menu.item_str(txt);
199        self
200    }
201
202    /// Add an owned text as item.
203    pub fn item_string(mut self, txt: String) -> Self {
204        self.menu.item_string(txt);
205        self
206    }
207
208    /// Sets the separator for the last item added.
209    /// If there is none adds this as an empty menu-item.
210    pub fn separator(mut self, separator: Separator) -> Self {
211        self.menu.separator(separator);
212        self
213    }
214
215    /// Fixed width for the menu.
216    /// If not set it uses 1.5 times the length of the longest item.
217    pub fn width(mut self, width: u16) -> Self {
218        self.width = Some(width);
219        self
220    }
221
222    /// Fixed width for the menu.
223    /// If not set it uses 1.5 times the length of the longest item.
224    pub fn width_opt(mut self, width: Option<u16>) -> Self {
225        self.width = width;
226        self
227    }
228
229    /// Set relative placement.
230    pub fn constraint(mut self, placement: PopupConstraint) -> Self {
231        self.popup = self.popup.constraint(placement);
232        self
233    }
234
235    /// Adds an extra offset.
236    pub fn offset(mut self, offset: (i16, i16)) -> Self {
237        self.popup = self.popup.offset(offset);
238        self
239    }
240
241    /// Adds an extra x offset.
242    pub fn x_offset(mut self, offset: i16) -> Self {
243        self.popup = self.popup.x_offset(offset);
244        self
245    }
246
247    /// Adds an extra y offset.
248    pub fn y_offset(mut self, offset: i16) -> Self {
249        self.popup = self.popup.y_offset(offset);
250        self
251    }
252
253    /// Set outer bounds for the popup-menu.
254    /// If not set, the [Buffer::area] is used as outer bounds.
255    pub fn boundary(mut self, boundary: Rect) -> Self {
256        self.popup = self.popup.boundary(boundary);
257        self
258    }
259
260    /// Set a style-set.
261    pub fn styles(mut self, styles: MenuStyle) -> Self {
262        self.popup = self.popup.styles(styles.popup.clone());
263
264        if let Some(popup_style) = styles.popup_style {
265            self.style = popup_style;
266        } else {
267            self.style = styles.style;
268        }
269
270        self.block = self.block.map(|v| v.style(self.style));
271        if let Some(border_style) = styles.popup_border {
272            self.block = self.block.map(|v| v.border_style(border_style));
273        }
274        if styles.block.is_some() {
275            self.block = styles.block;
276        }
277
278        if styles.highlight.is_some() {
279            self.highlight_style = styles.highlight;
280        }
281        if styles.disabled.is_some() {
282            self.disabled_style = styles.disabled;
283        }
284        if styles.right.is_some() {
285            self.right_style = styles.right;
286        }
287        if styles.focus.is_some() {
288            self.focus_style = styles.focus;
289        }
290        self
291    }
292
293    /// Base style.
294    pub fn style(mut self, style: Style) -> Self {
295        self.style = style;
296        self.block = self.block.map(|v| v.style(self.style));
297        self
298    }
299
300    /// Highlight style.
301    pub fn highlight_style(mut self, style: Style) -> Self {
302        self.highlight_style = Some(style);
303        self
304    }
305
306    /// Highlight style.
307    pub fn highlight_style_opt(mut self, style: Option<Style>) -> Self {
308        self.highlight_style = style;
309        self
310    }
311
312    /// Disabled item style.
313    #[inline]
314    pub fn disabled_style(mut self, style: Style) -> Self {
315        self.disabled_style = Some(style);
316        self
317    }
318
319    /// Disabled item style.
320    #[inline]
321    pub fn disabled_style_opt(mut self, style: Option<Style>) -> Self {
322        self.disabled_style = style;
323        self
324    }
325
326    /// Style for the hotkey.
327    #[inline]
328    pub fn right_style(mut self, style: Style) -> Self {
329        self.right_style = Some(style);
330        self
331    }
332
333    /// Style for the hotkey.
334    #[inline]
335    pub fn right_style_opt(mut self, style: Option<Style>) -> Self {
336        self.right_style = style;
337        self
338    }
339
340    /// Focus/Selection style.
341    pub fn focus_style(mut self, style: Style) -> Self {
342        self.focus_style = Some(style);
343        self
344    }
345
346    /// Focus/Selection style.
347    pub fn focus_style_opt(mut self, style: Option<Style>) -> Self {
348        self.focus_style = style;
349        self
350    }
351
352    /// Block for borders.
353    pub fn block(mut self, block: Block<'a>) -> Self {
354        self.block = Some(block);
355        self.block = self.block.map(|v| v.style(self.style));
356        self
357    }
358
359    /// Block for borders.
360    pub fn block_opt(mut self, block: Option<Block<'a>>) -> Self {
361        self.block = block;
362        self.block = self.block.map(|v| v.style(self.style));
363        self
364    }
365
366    /// Get the padding the block imposes as a Size.
367    pub fn get_block_size(&self) -> Size {
368        get_block_size(&self.block)
369    }
370
371    /// Get the padding the block imposes as Padding.
372    pub fn get_block_padding(&self) -> Padding {
373        get_block_padding(&self.block)
374    }
375}
376
377impl<'a> StatefulWidget for &PopupMenu<'a> {
378    type State = PopupMenuState;
379
380    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
381        render_popup_menu(self, area, buf, state);
382    }
383}
384
385impl StatefulWidget for PopupMenu<'_> {
386    type State = PopupMenuState;
387
388    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
389        render_popup_menu(&self, area, buf, state);
390    }
391}
392
393fn render_popup_menu(
394    widget: &PopupMenu<'_>,
395    _area: Rect,
396    buf: &mut Buffer,
397    state: &mut PopupMenuState,
398) {
399    if widget.menu.items.is_empty() {
400        state.selected = None;
401    } else if state.selected.is_none() {
402        state.selected = Some(0);
403    }
404
405    state.navchar = widget.menu.items.iter().map(|v| v.navchar).collect();
406    state.disabled = widget.menu.items.iter().map(|v| v.disabled).collect();
407
408    if !state.is_active() {
409        state.clear_areas();
410        return;
411    }
412
413    let size = widget.size();
414    let area = Rect::new(0, 0, size.width, size.height);
415
416    (&widget.popup).render(area, buf, &mut state.popup);
417    if widget.block.is_some() {
418        widget.block.render(state.popup.area, buf);
419    } else {
420        buf.set_style(state.popup.area, widget.style);
421    }
422    widget.layout(state.popup.area, state);
423    render_items(widget, buf, state);
424}
425
426fn render_items(widget: &PopupMenu<'_>, buf: &mut Buffer, state: &mut PopupMenuState) {
427    let focus_style = widget.focus_style.unwrap_or(revert_style(widget.style));
428
429    let style = widget.style;
430    let right_style = style.patch(widget.right_style.unwrap_or_default());
431    let highlight_style = style.patch(widget.highlight_style.unwrap_or(Style::new().underlined()));
432    let disabled_style = style.patch(widget.disabled_style.unwrap_or_default());
433
434    let sel_style = focus_style;
435    let sel_right_style = focus_style.patch(widget.right_style.unwrap_or_default());
436    let sel_highlight_style = focus_style;
437    let sel_disabled_style = focus_style.patch(widget.disabled_style.unwrap_or_default());
438
439    for (n, item) in widget.menu.items.iter().enumerate() {
440        let mut item_area = state.item_areas[n];
441
442        #[allow(clippy::collapsible_else_if)]
443        let (style, right_style, highlight_style) = if state.selected == Some(n) {
444            if item.disabled {
445                (sel_disabled_style, sel_right_style, sel_highlight_style)
446            } else {
447                (sel_style, sel_right_style, sel_highlight_style)
448            }
449        } else {
450            if item.disabled {
451                (disabled_style, right_style, highlight_style)
452            } else {
453                (style, right_style, highlight_style)
454            }
455        };
456
457        let item_line = if let Some(highlight) = item.highlight.clone() {
458            Line::from_iter([
459                Span::from(&item.item[..highlight.start - 1]), // account for _
460                Span::from(&item.item[highlight.start..highlight.end]).style(highlight_style),
461                Span::from(&item.item[highlight.end..]),
462            ])
463        } else {
464            Line::from(item.item.as_ref())
465        };
466        item_line.style(style).render(item_area, buf);
467
468        if !item.right.is_empty() {
469            let right_width = item.right.graphemes(true).count() as u16;
470            if right_width < item_area.width {
471                let delta = item_area.width.saturating_sub(right_width);
472                item_area.x += delta;
473                item_area.width -= delta;
474            }
475            Span::from(item.right.as_ref())
476                .style(right_style)
477                .render(item_area, buf);
478        }
479
480        if let Some(separator) = item.separator {
481            let sep_area = state.sep_areas[n];
482            let sym = match separator {
483                Separator::Empty => " ",
484                Separator::Plain => "\u{2500}",
485                Separator::Thick => "\u{2501}",
486                Separator::Double => "\u{2550}",
487                Separator::Dashed => "\u{2212}",
488                Separator::Dotted => "\u{2508}",
489            };
490            for x in 0..sep_area.width {
491                if let Some(cell) = buf.cell_mut((sep_area.x + x, sep_area.y)) {
492                    cell.set_symbol(sym);
493                }
494            }
495        }
496    }
497}
498
499impl PopupMenuState {
500    /// New
501    #[inline]
502    pub fn new() -> Self {
503        Default::default()
504    }
505
506    /// Set the z-index for the popup-menu.
507    pub fn set_popup_z(&mut self, z: u16) {
508        self.popup.area_z = z;
509    }
510
511    /// The z-index for the popup-menu.
512    pub fn popup_z(&self) -> u16 {
513        self.popup.area_z
514    }
515
516    /// Show the popup.
517    pub fn flip_active(&mut self) {
518        self.popup.flip_active();
519    }
520
521    /// Show the popup.
522    pub fn is_active(&self) -> bool {
523        self.popup.is_active()
524    }
525
526    /// Show the popup.
527    pub fn set_active(&mut self, active: bool) {
528        self.popup.set_active(active);
529        if !active {
530            self.clear_areas();
531        }
532    }
533
534    /// Clear the areas.
535    pub fn clear_areas(&mut self) {
536        self.popup.clear_areas();
537        self.sep_areas.clear();
538        self.navchar.clear();
539        self.item_areas.clear();
540        self.disabled.clear();
541    }
542
543    /// Number of items.
544    #[inline]
545    pub fn len(&self) -> usize {
546        self.item_areas.len()
547    }
548
549    /// Any items.
550    #[inline]
551    pub fn is_empty(&self) -> bool {
552        self.item_areas.is_empty()
553    }
554
555    /// Selected item.
556    #[inline]
557    pub fn select(&mut self, select: Option<usize>) -> bool {
558        let old = self.selected;
559        self.selected = select;
560        old != self.selected
561    }
562
563    /// Selected item.
564    #[inline]
565    pub fn selected(&self) -> Option<usize> {
566        self.selected
567    }
568
569    /// Select the previous item.
570    #[inline]
571    pub fn prev_item(&mut self) -> bool {
572        let old = self.selected;
573
574        // before first render or no items:
575        if self.disabled.is_empty() {
576            return false;
577        }
578
579        self.selected = if let Some(start) = old {
580            let mut idx = start;
581            loop {
582                if idx == 0 {
583                    idx = start;
584                    break;
585                }
586                idx -= 1;
587
588                if self.disabled.get(idx) == Some(&false) {
589                    break;
590                }
591            }
592            Some(idx)
593        } else if !self.is_empty() {
594            Some(self.len() - 1)
595        } else {
596            None
597        };
598
599        old != self.selected
600    }
601
602    /// Select the next item.
603    #[inline]
604    pub fn next_item(&mut self) -> bool {
605        let old = self.selected;
606
607        // before first render or no items:
608        if self.disabled.is_empty() {
609            return false;
610        }
611
612        self.selected = if let Some(start) = old {
613            let mut idx = start;
614            loop {
615                if idx + 1 == self.len() {
616                    idx = start;
617                    break;
618                }
619                idx += 1;
620
621                if self.disabled.get(idx) == Some(&false) {
622                    break;
623                }
624            }
625            Some(idx)
626        } else if !self.is_empty() {
627            Some(0)
628        } else {
629            None
630        };
631
632        old != self.selected
633    }
634
635    /// Select by navigation key.
636    #[inline]
637    pub fn navigate(&mut self, c: char) -> MenuOutcome {
638        // before first render or no items:
639        if self.disabled.is_empty() {
640            return MenuOutcome::Continue;
641        }
642
643        let c = c.to_ascii_lowercase();
644        for (i, cc) in self.navchar.iter().enumerate() {
645            #[allow(clippy::collapsible_if)]
646            if *cc == Some(c) {
647                if self.disabled.get(i) == Some(&false) {
648                    if self.selected == Some(i) {
649                        return MenuOutcome::Activated(i);
650                    } else {
651                        self.selected = Some(i);
652                        return MenuOutcome::Selected(i);
653                    }
654                }
655            }
656        }
657
658        MenuOutcome::Continue
659    }
660
661    /// Select item at position.
662    #[inline]
663    #[allow(clippy::collapsible_if)]
664    pub fn select_at(&mut self, pos: (u16, u16)) -> bool {
665        let old_selected = self.selected;
666
667        // before first render or no items:
668        if self.disabled.is_empty() {
669            return false;
670        }
671
672        if let Some(idx) = self.mouse.item_at(&self.item_areas, pos.0, pos.1) {
673            if !self.disabled[idx] {
674                self.selected = Some(idx);
675            }
676        }
677
678        self.selected != old_selected
679    }
680
681    /// Item at position.
682    #[inline]
683    pub fn item_at(&self, pos: (u16, u16)) -> Option<usize> {
684        self.mouse.item_at(&self.item_areas, pos.0, pos.1)
685    }
686}
687
688impl HandleEvent<crossterm::event::Event, Popup, MenuOutcome> for PopupMenuState {
689    /// Handle all events.
690    ///
691    /// Key-events are only handled when the popup menu
692    /// [is_active](PopupMenuState::is_active).
693    /// You need to set this state according to your logic.
694    ///
695    /// The popup menu will return MenuOutcome::Hide if it
696    /// thinks it should be hidden.
697    ///
698    fn handle(&mut self, event: &crossterm::event::Event, _qualifier: Popup) -> MenuOutcome {
699        let r0 = match self.popup.handle(event, Popup) {
700            PopupOutcome::Hide => MenuOutcome::Hide,
701            r => r.into(),
702        };
703
704        let r1 = if self.is_active() {
705            match event {
706                ct_event!(key press ANY-c) => {
707                    let r = self.navigate(*c);
708                    if matches!(r, MenuOutcome::Activated(_)) {
709                        self.set_active(false);
710                    }
711                    r
712                }
713                ct_event!(keycode press Up) => {
714                    if self.prev_item() {
715                        if let Some(selected) = self.selected {
716                            MenuOutcome::Selected(selected)
717                        } else {
718                            MenuOutcome::Changed
719                        }
720                    } else {
721                        MenuOutcome::Continue
722                    }
723                }
724                ct_event!(keycode press Down) => {
725                    if self.next_item() {
726                        if let Some(selected) = self.selected {
727                            MenuOutcome::Selected(selected)
728                        } else {
729                            MenuOutcome::Changed
730                        }
731                    } else {
732                        MenuOutcome::Continue
733                    }
734                }
735                ct_event!(keycode press Home) => {
736                    if self.select(Some(0)) {
737                        if let Some(selected) = self.selected {
738                            MenuOutcome::Selected(selected)
739                        } else {
740                            MenuOutcome::Changed
741                        }
742                    } else {
743                        MenuOutcome::Continue
744                    }
745                }
746                ct_event!(keycode press End) => {
747                    if self.select(Some(self.len().saturating_sub(1))) {
748                        if let Some(selected) = self.selected {
749                            MenuOutcome::Selected(selected)
750                        } else {
751                            MenuOutcome::Changed
752                        }
753                    } else {
754                        MenuOutcome::Continue
755                    }
756                }
757                ct_event!(keycode press Esc) => {
758                    self.set_active(false);
759                    MenuOutcome::Changed
760                }
761                ct_event!(keycode press Enter) => {
762                    if let Some(select) = self.selected {
763                        self.set_active(false);
764                        MenuOutcome::Activated(select)
765                    } else {
766                        MenuOutcome::Continue
767                    }
768                }
769
770                _ => MenuOutcome::Continue,
771            }
772        } else {
773            MenuOutcome::Continue
774        };
775
776        let r = max(r0, r1);
777
778        if !r.is_consumed() {
779            self.handle(event, MouseOnly)
780        } else {
781            r
782        }
783    }
784}
785
786impl HandleEvent<crossterm::event::Event, MouseOnly, MenuOutcome> for PopupMenuState {
787    fn handle(&mut self, event: &crossterm::event::Event, _: MouseOnly) -> MenuOutcome {
788        if self.is_active() {
789            let r = match event {
790                ct_event!(mouse moved for col, row)
791                    if self.popup.area.contains((*col, *row).into()) =>
792                {
793                    if self.select_at((*col, *row)) {
794                        MenuOutcome::Selected(self.selected().expect("selection"))
795                    } else {
796                        MenuOutcome::Unchanged
797                    }
798                }
799                ct_event!(mouse down Left for col, row)
800                    if self.popup.area.contains((*col, *row).into()) =>
801                {
802                    if self.item_at((*col, *row)).is_some() {
803                        self.set_active(false);
804                        MenuOutcome::Activated(self.selected().expect("selection"))
805                    } else {
806                        MenuOutcome::Unchanged
807                    }
808                }
809                _ => MenuOutcome::Continue,
810            };
811
812            r.or_else(|| mouse_trap(event, self.popup.area).into())
813        } else {
814            MenuOutcome::Continue
815        }
816    }
817}
818
819/// Handle all events.
820///
821/// Key-events are only handled when the popup menu
822/// [is_active](PopupMenuState::is_active).
823/// You need to set this state according to your logic.
824///
825/// The popup menu will return MenuOutcome::Hide if it
826/// thinks it should be hidden.
827///
828pub fn handle_popup_events(
829    state: &mut PopupMenuState,
830    event: &crossterm::event::Event,
831) -> MenuOutcome {
832    state.handle(event, Popup)
833}
834
835/// Handle only mouse-events.
836pub fn handle_mouse_events(
837    state: &mut PopupMenuState,
838    event: &crossterm::event::Event,
839) -> MenuOutcome {
840    state.handle(event, MouseOnly)
841}