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