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