Skip to main content

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