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