tui_menu/
lib.rs

1/*! Drop-down main menu for Ratatui.
2
3Ratatui immediate mode split visual elements in a Widget and a WidgetState.
4The first holds the configuration, and never changes.
5The second holds the parts that can be affected
6by user actions.
7
8In this case the style is found in the Widget [Menu]
9and the menu tree is found in WidgetState [MenuState].
10
11The menu tree is built with one [MenuItem] per possible selection.
12A MenuItem with children is called a group.
13
14When a menu item is selected, this generates an event which
15will be stored in MenuState.events.
16
17To define a menu, see examples in [MenuState].
18*/
19
20use ratatui_core::{
21    buffer::Buffer,
22    layout::{Margin, Rect},
23    style::{Color, Style},
24    text::{Line, Span},
25    widgets::{StatefulWidget, Widget},
26};
27use ratatui_widgets::{block::Block, borders::Borders, clear::Clear};
28use std::{borrow::Cow, marker::PhantomData};
29
30/// Events this widget produce
31/// Now only emit Selected, may add few in future
32#[derive(Debug)]
33pub enum MenuEvent<T> {
34    /// Item selected, with its data attached
35    Selected(T),
36}
37
38/// The state for menu, keep track of runtime info
39pub struct MenuState<T> {
40    /// stores the menu tree
41    root_item: MenuItem<T>,
42    /// stores events generated in one frame
43    events: Vec<MenuEvent<T>>,
44}
45
46impl<T: Clone> MenuState<T> {
47    /// create with items
48    /// # Example
49    ///
50    /// ```
51    /// use tui_menu::{MenuState, MenuItem};
52    ///
53    /// let state = MenuState::<&'static str>::new(vec![
54    ///     MenuItem::item("Foo", "label_foo"),
55    ///     MenuItem::group("Group", vec![
56    ///         MenuItem::item("Bar 1", "label_bar_1"),
57    ///         MenuItem::item("Bar 2", "label_bar_1"),
58    ///     ])
59    /// ]);
60    /// ```
61    pub fn new(items: Vec<MenuItem<T>>) -> Self {
62        let mut root_item = MenuItem::group("root", items);
63        // the root item marked as always highlight
64        // this makes highlight logic more consistent
65        root_item.is_highlight = true;
66
67        Self {
68            root_item,
69            events: Default::default(),
70        }
71    }
72
73    /// active the menu, this will select the first item
74    ///
75    /// # Example
76    ///
77    /// ```
78    /// use tui_menu::{MenuState, MenuItem};
79    ///
80    /// let mut state = MenuState::<&'static str>::new(vec![
81    ///     MenuItem::item("Foo", "label_foo"),
82    ///     MenuItem::group("Group", vec![
83    ///         MenuItem::item("Bar 1", "label_bar_1"),
84    ///         MenuItem::item("Bar 2", "label_bar_1"),
85    ///     ])
86    /// ]);
87    ///
88    /// state.activate();
89    ///
90    /// assert_eq!(state.highlight().unwrap().data.unwrap(), "label_foo");
91    ///
92    /// ```
93    ///
94    pub fn activate(&mut self) {
95        self.root_item.highlight_next();
96    }
97
98    /// Check if menu is active
99    pub fn is_active(&self) -> bool {
100        self.root_item.highlight().is_some()
101    }
102
103    /// trigger up movement
104    /// NOTE: this action tries to do intuitive movement,
105    /// which means logically it is not consistent, e.g:
106    /// case 1:
107    ///    group 1        group 2        group 3
108    ///                 > sub item 1
109    ///                   sub item 2
110    /// up is pop, which closes the group 2
111    ///
112    /// case 2:
113    ///    group 1        group 2        group 3
114    ///                   sub item 1
115    ///                 > sub item 2
116    /// up is move prev
117    ///
118    /// case 3:
119    ///
120    ///    group 1        group 2
121    ///                   sub item 1
122    ///                 > sub item 2  > sub sub item 1
123    ///                                 sub sub item 2
124    ///
125    /// up does nothing
126    pub fn up(&mut self) {
127        match self.active_depth() {
128            0 | 1 => {
129                // do nothing
130            }
131            2 => match self
132                .root_item
133                .highlight_child()
134                .and_then(|child| child.highlight_child_index())
135            {
136                // case 1
137                Some(0) => {
138                    self.pop();
139                }
140                _ => {
141                    self.prev();
142                }
143            },
144            _ => {
145                self.prev();
146            }
147        }
148    }
149
150    /// trigger down movement
151    ///
152    /// NOTE: this action tries to do intuitive movement,
153    /// which means logicially it is not consistent, e.g:
154    /// case 1:
155    ///    group 1      > group 2        group 3
156    ///                   sub item 1
157    ///                   sub item 2
158    /// down is enter, which enter the sub group of group 2
159    ///
160    /// case 2:
161    ///    group 1        group 2        group 3
162    ///                   sub item 1
163    ///                 > sub item 2
164    /// down does nothing
165    ///
166    /// case 3:
167    ///    group 1        group 2
168    ///                 > sub item 1
169    ///                   sub item 2
170    ///
171    /// down highlights "sub item 2"
172    pub fn down(&mut self) {
173        if self.active_depth() == 1 {
174            self.push();
175        } else {
176            self.next();
177        }
178    }
179
180    /// trigger left movement
181    ///
182    /// NOTE: this action tries to do intuitive movement,
183    /// which means logicially it is not consistent, e.g:
184    /// case 1:
185    ///    group 1      > group 2        group 3
186    ///                   sub item 1
187    ///                   sub item 2
188    /// left highlights "group 1"
189    ///
190    /// case 2:
191    ///    group 1        group 2        group 3
192    ///                   sub item 1
193    ///                 > sub item 2
194    /// left first pop "sub item group", then highlights "group 1"
195    ///
196    /// case 3:
197    ///    group 1        group 2
198    ///                 > sub item 1    sub sub item 1
199    ///                   sub item 2  > sub sub item 2
200    ///
201    /// left pop "sub sub group"
202    pub fn left(&mut self) {
203        if self.active_depth() == 0 {
204            // do nothing
205        } else if self.active_depth() == 1 {
206            self.prev();
207        } else if self.active_depth() == 2 {
208            self.pop();
209            self.prev();
210        } else {
211            self.pop();
212        }
213    }
214
215    /// trigger right movement
216    ///
217    /// NOTE: this action tries to do intuitive movement,
218    /// which means logicially it is not consistent, e.g:
219    /// case 1:
220    ///    group 1      > group 2        group 3
221    ///                   sub item 1
222    ///                   sub item 2
223    /// right highlights "group 3"
224    ///
225    /// case 2:
226    ///    group 1        group 2        group 3
227    ///                   sub item 1
228    ///                 > sub item 2
229    /// right pop group "sub item *", then highlights "group 3"
230    ///
231    /// case 3:
232    ///    group 1        group 2        group 3
233    ///                   sub item 1
234    ///                 > sub item 2 +
235    /// right pushes "sub sub item 2". this differs from case 2 that
236    /// current highlighted item can be expanded
237    pub fn right(&mut self) {
238        if self.active_depth() == 0 {
239            // do nothing
240        } else if self.active_depth() == 1 {
241            self.next();
242        } else if self.active_depth() == 2 {
243            if self.push().is_none() {
244                // special handling, make menu navigation
245                // more productive
246                self.pop();
247                self.next();
248            }
249        } else {
250            self.push();
251        }
252    }
253
254    /// highlight the prev item in current group
255    /// if already the first, then do nothing
256    fn prev(&mut self) {
257        if let Some(item) = self.root_item.highlight_last_but_one() {
258            item.highlight_prev();
259        } else {
260            self.root_item.highlight_prev();
261        }
262    }
263
264    /// highlight the next item in current group
265    /// if already the last, then do nothing
266    fn next(&mut self) {
267        if let Some(item) = self.root_item.highlight_last_but_one() {
268            item.highlight_next();
269        } else {
270            self.root_item.highlight_next();
271        }
272    }
273
274    /// active depth, how many levels dropdown/sub menus expanded.
275    /// when no drop down, it is 1
276    /// one drop down, 2
277    fn active_depth(&self) -> usize {
278        let mut item = self.root_item.highlight_child();
279        let mut depth = 0;
280        while let Some(inner_item) = item {
281            depth += 1;
282            item = inner_item.highlight_child();
283        }
284        depth
285    }
286
287    /// How many dropdown to render, including preview
288    /// NOTE: If current group contains sub-group, in order to keep ui consistent,
289    ///   even the sub-group not selected, its space is counted
290    fn dropdown_count(&self) -> u16 {
291        let mut node = &self.root_item;
292        let mut count = 0;
293        loop {
294            match node.highlight_child() {
295                None => {
296                    return count;
297                }
298                Some(highlight_child) => {
299                    if highlight_child.is_group() {
300                        // highlighted child is a group, then it's children is previewed
301                        count += 1;
302                    } else if node.children.iter().any(|c| c.is_group()) {
303                        // if highlighted item is not a group, but if sibling contains group
304                        // in order to keep ui consistency, also count it
305                        count += 1;
306                    }
307
308                    node = highlight_child;
309                }
310            }
311        }
312    }
313
314    /// select current highlight item, if it has children
315    /// then push
316    pub fn select(&mut self) {
317        if let Some(item) = self.root_item.highlight_mut() {
318            if !item.children.is_empty() {
319                self.push();
320            } else if let Some(ref data) = item.data {
321                self.events.push(MenuEvent::Selected(data.clone()));
322            }
323        }
324    }
325
326    /// dive into sub menu if applicable.
327    /// Return: Some if entered deeper level
328    ///         None if nothing happen
329    pub fn push(&mut self) -> Option<()> {
330        self.root_item.highlight_mut()?.highlight_first_child()
331    }
332
333    /// pop the current menu group. move one layer up
334    pub fn pop(&mut self) {
335        if let Some(item) = self.root_item.highlight_mut() {
336            item.clear_highlight();
337        }
338    }
339
340    /// clear all highlighted items. This is useful
341    /// when the menu bar lose focus
342    pub fn reset(&mut self) {
343        self.root_item
344            .children
345            .iter_mut()
346            .for_each(|c| c.clear_highlight());
347    }
348
349    /// client should drain events each frame, otherwise user action
350    /// will feel laggy
351    pub fn drain_events(&mut self) -> impl Iterator<Item = MenuEvent<T>> {
352        std::mem::take(&mut self.events).into_iter()
353    }
354
355    /// return current highlighted item's reference
356    pub fn highlight(&self) -> Option<&MenuItem<T>> {
357        self.root_item.highlight()
358    }
359}
360
361/// MenuItem is the node in menu tree. If children is not
362/// empty, then this item is the group item.
363pub struct MenuItem<T> {
364    name: Cow<'static, str>,
365    pub data: Option<T>,
366    children: Vec<MenuItem<T>>,
367    is_highlight: bool,
368}
369
370impl<T> MenuItem<T> {
371    /// helper function to create a non group item.
372    pub fn item(name: impl Into<Cow<'static, str>>, data: T) -> Self {
373        Self {
374            name: name.into(),
375            data: Some(data),
376            is_highlight: false,
377            children: vec![],
378        }
379    }
380
381    /// helper function to create a group item.
382    ///
383    /// # Example
384    ///
385    /// ```
386    /// use tui_menu::MenuItem;
387    ///
388    /// let item = MenuItem::<&'static str>::group("group", vec![
389    ///     MenuItem::item("foo", "label_foo"),
390    /// ]);
391    ///
392    /// assert!(item.is_group());
393    ///
394    /// ```
395    pub fn group(name: impl Into<Cow<'static, str>>, children: Vec<Self>) -> Self {
396        Self {
397            name: name.into(),
398            data: None,
399            is_highlight: false,
400            children,
401        }
402    }
403
404    #[cfg(test)]
405    fn with_highlight(mut self, highlight: bool) -> Self {
406        self.is_highlight = highlight;
407        self
408    }
409
410    /// whether this item is group
411    pub fn is_group(&self) -> bool {
412        !self.children.is_empty()
413    }
414
415    /// get current item's name
416    fn name(&self) -> &str {
417        &self.name
418    }
419
420    /// highlight first child
421    fn highlight_first_child(&mut self) -> Option<()> {
422        if !self.children.is_empty() {
423            if let Some(it) = self.children.get_mut(0) {
424                it.is_highlight = true;
425            }
426            Some(())
427        } else {
428            None
429        }
430    }
431
432    /// highlight prev item in this node
433    fn highlight_prev(&mut self) {
434        // if no child selected, then
435        let Some(current_index) = self.highlight_child_index() else {
436            self.highlight_first_child();
437            return;
438        };
439
440        let index_to_highlight = if current_index > 0 {
441            current_index - 1
442        } else {
443            0
444        };
445
446        self.children[current_index].clear_highlight();
447        self.children[index_to_highlight].is_highlight = true;
448    }
449
450    /// highlight prev item in this node
451    fn highlight_next(&mut self) {
452        // if no child selected, then
453        let Some(current_index) = self.highlight_child_index() else {
454            self.highlight_first_child();
455            return;
456        };
457
458        let index_to_highlight = (current_index + 1).min(self.children.len() - 1);
459        self.children[current_index].clear_highlight();
460        self.children[index_to_highlight].is_highlight = true;
461    }
462
463    /// return highlighted child index
464    fn highlight_child_index(&self) -> Option<usize> {
465        for (idx, child) in self.children.iter().enumerate() {
466            if child.is_highlight {
467                return Some(idx);
468            }
469        }
470
471        None
472    }
473
474    /// if any child highlighted, then return its reference
475    fn highlight_child(&self) -> Option<&Self> {
476        self.children.iter().filter(|i| i.is_highlight).nth(0)
477    }
478
479    /// if any child highlighted, then return its reference
480    fn highlight_child_mut(&mut self) -> Option<&mut Self> {
481        self.children.iter_mut().filter(|i| i.is_highlight).nth(0)
482    }
483
484    /// clear is_highlight flag recursively.
485    fn clear_highlight(&mut self) {
486        self.is_highlight = false;
487        for child in self.children.iter_mut() {
488            child.clear_highlight();
489        }
490    }
491
492    /// return deepest highlight item's reference
493    pub fn highlight(&self) -> Option<&Self> {
494        if !self.is_highlight {
495            return None;
496        }
497
498        let mut highlight_item = self;
499        while highlight_item.highlight_child().is_some() {
500            highlight_item = highlight_item.highlight_child().unwrap();
501        }
502
503        Some(highlight_item)
504    }
505
506    /// mut version of highlight
507    fn highlight_mut(&mut self) -> Option<&mut Self> {
508        if !self.is_highlight {
509            return None;
510        }
511
512        let mut highlight_item = self;
513        while highlight_item.highlight_child_mut().is_some() {
514            highlight_item = highlight_item.highlight_child_mut().unwrap();
515        }
516
517        Some(highlight_item)
518    }
519
520    /// last but one layer in highlight
521    fn highlight_last_but_one(&mut self) -> Option<&mut Self> {
522        // if self is not highlighted or there is no highlighted child, return None
523        if !self.is_highlight || self.highlight_child_mut().is_none() {
524            return None;
525        }
526
527        let mut last_but_one = self;
528        while last_but_one
529            .highlight_child_mut()
530            .and_then(|x| x.highlight_child_mut())
531            .is_some()
532        {
533            last_but_one = last_but_one.highlight_child_mut().unwrap();
534        }
535        Some(last_but_one)
536    }
537}
538
539/// Widget focus on display/render
540pub struct Menu<T> {
541    /// style for default item style
542    default_item_style: Style,
543    /// style for highlighted item
544    highlight_item_style: Style,
545    /// width for drop down panel
546    drop_down_width: u16,
547    /// style for drop down panel
548    drop_down_style: Style,
549    _priv: PhantomData<T>,
550}
551
552impl<T> Menu<T> {
553    pub fn new() -> Self {
554        Self {
555            highlight_item_style: Style::default().fg(Color::White).bg(Color::LightBlue),
556            default_item_style: Style::default().fg(Color::White),
557            drop_down_width: 20,
558            drop_down_style: Style::default().bg(Color::DarkGray),
559            _priv: Default::default(),
560        }
561    }
562
563    /// update with highlight style
564    pub fn default_style(mut self, style: Style) -> Self {
565        self.default_item_style = style;
566        self
567    }
568
569    /// update with highlight style
570    pub fn highlight(mut self, style: Style) -> Self {
571        self.highlight_item_style = style;
572        self
573    }
574
575    /// update drop_down_width
576    pub fn dropdown_width(mut self, width: u16) -> Self {
577        self.drop_down_width = width;
578        self
579    }
580
581    /// update drop_down fill style
582    pub fn dropdown_style(mut self, style: Style) -> Self {
583        self.drop_down_style = style;
584        self
585    }
586
587    /// render an item group in drop down
588    /* Each menu item is rendered like this
589    .|.NameString.|.
590      ^^^^^^^^^^^^ ------ this area will be highlighted
591    */
592    fn render_dropdown(
593        &self,
594        x: u16,
595        y: u16,
596        group: &[MenuItem<T>],
597        buf: &mut Buffer,
598        dropdown_count_to_go: u16, // including current, it is not drawn yet
599    ) {
600        // Compute width of all menu items
601        let child_max_width = group
602            .iter()
603            .map(|menu_item| Span::from(menu_item.name.clone()).width())
604            .max()
605            .unwrap_or(0) as u16;
606
607        // Compute minimum size needed after border is added
608        // Border is 3 chars wide and 1 char high, on both sides.
609        let min_drop_down_width: u16 = child_max_width + 3 + 3;
610        let min_drop_down_height: u16 = (group.len() as u16) + 1 + 1;
611
612        // prevent calculation issue if canvas is narrow
613        let drop_down_width = self.drop_down_width.min(buf.area.width);
614
615        // calculate the maximum x, leaving enough space for deeper items
616        // drawing area:
617        // |  a |  b   |            c                |        d       |
618        // | .. |  me  |  child_1  |  child_of_child |  nothing here  |
619        // x_max is the x when d is 0
620        let b_plus_c = dropdown_count_to_go * drop_down_width;
621        let x_max = buf.area().right().saturating_sub(b_plus_c);
622
623        let x = x.min(x_max);
624
625        let area = Rect::new(x, y, min_drop_down_width, min_drop_down_height);
626
627        // clamp to ensure we draw in areas
628        let area = area.clamp(*buf.area());
629
630        Clear.render(area, buf);
631
632        buf.set_style(area, self.default_item_style);
633
634        // Render menu border
635        let border = Block::default()
636            .borders(Borders::ALL)
637            .style(self.default_item_style);
638        border.render(
639            area.inner(Margin {
640                vertical: 0,
641                horizontal: 1,
642            }),
643            buf,
644        );
645
646        // Render menu items
647        let mut active_group: Option<_> = None;
648        for (idx, item) in group.iter().enumerate() {
649            let item_x = x + 2;
650            let item_y = y + 1 + idx as u16;
651            let is_active = item.is_highlight;
652
653            let item_name = item.name();
654
655            // make style apply to whole line by make name whole line
656            let mut item_name =
657                format!(" {: <width$} ", item_name, width = child_max_width as usize);
658
659            if !item.children.is_empty() {
660                item_name.pop();
661                item_name.push('>');
662            }
663
664            buf.set_span(
665                item_x,
666                item_y,
667                &Span::styled(
668                    item_name,
669                    if is_active {
670                        self.highlight_item_style
671                    } else {
672                        self.default_item_style
673                    },
674                ),
675                child_max_width + 2,
676            );
677
678            if is_active && !item.children.is_empty() {
679                active_group = Some((item_x + child_max_width, item_y, item));
680            }
681        }
682
683        // draw at the end to ensure its content above all items in current level
684        if let Some((x, y, item)) = active_group {
685            self.render_dropdown(x, y, &item.children, buf, dropdown_count_to_go - 1);
686        }
687    }
688}
689
690impl<T> Default for Menu<T> {
691    fn default() -> Self {
692        Self::new()
693    }
694}
695
696impl<T: Clone> StatefulWidget for Menu<T> {
697    type State = MenuState<T>;
698
699    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
700        let area = area.clamp(*buf.area());
701
702        let mut spans = vec![];
703        let mut x_pos = area.x;
704        let y_pos = area.y;
705
706        let dropdown_count = state.dropdown_count();
707
708        // Skip top left char
709        spans.push(Span::raw(" ").style(self.default_item_style));
710
711        for item in state.root_item.children.iter() {
712            let is_highlight = item.is_highlight;
713            let item_style = if is_highlight {
714                self.highlight_item_style
715            } else {
716                self.default_item_style
717            };
718            let has_children = !item.children.is_empty();
719
720            let group_x_pos = x_pos;
721            let span = Span::styled(format!(" {} ", item.name()), item_style);
722            x_pos += span.width() as u16;
723            spans.push(span);
724
725            if has_children && is_highlight {
726                self.render_dropdown(group_x_pos, y_pos + 1, &item.children, buf, dropdown_count);
727            }
728        }
729        buf.set_line(area.x, area.y, &Line::from(spans), area.width);
730    }
731}
732
733#[cfg(test)]
734mod tests {
735    use crate::MenuState;
736
737    type MenuItem = super::MenuItem<i32>;
738
739    #[test]
740    fn test_active_depth() {
741        {
742            let menu_state = MenuState::new(vec![MenuItem::item("item1", 0)]);
743            assert_eq!(menu_state.active_depth(), 0);
744        }
745
746        {
747            let menu_state = MenuState::new(vec![MenuItem::item("item1", 0).with_highlight(true)]);
748            assert_eq!(menu_state.active_depth(), 1);
749        }
750
751        {
752            let menu_state = MenuState::new(vec![MenuItem::group("layer1", vec![])]);
753            assert_eq!(menu_state.active_depth(), 0);
754        }
755
756        {
757            let menu_state =
758                MenuState::new(vec![MenuItem::group("layer1", vec![]).with_highlight(true)]);
759            assert_eq!(menu_state.active_depth(), 1);
760        }
761
762        {
763            let menu_state = MenuState::new(vec![MenuItem::group(
764                "layer_1",
765                vec![MenuItem::item("item_layer_2", 0)],
766            )
767            .with_highlight(true)]);
768            assert_eq!(menu_state.active_depth(), 1);
769        }
770
771        {
772            let menu_state = MenuState::new(vec![MenuItem::group(
773                "layer_1",
774                vec![MenuItem::item("item_layer_2", 0).with_highlight(true)],
775            )
776            .with_highlight(true)]);
777            assert_eq!(menu_state.active_depth(), 2);
778        }
779    }
780
781    #[test]
782    fn test_dropdown_count() {
783        {
784            // only item in menu bar
785            let menu_state = MenuState::new(vec![MenuItem::item("item1", 0)]);
786            assert_eq!(menu_state.dropdown_count(), 0);
787        }
788
789        {
790            // group in menu bar,
791            let menu_state = MenuState::new(vec![MenuItem::group(
792                "menu bar",
793                vec![MenuItem::item("item layer 1", 0)],
794            )
795            .with_highlight(true)]);
796            assert_eq!(menu_state.dropdown_count(), 1);
797        }
798
799        {
800            // group in menu bar,
801            let menu_state = MenuState::new(vec![MenuItem::group(
802                "menu bar 1",
803                vec![
804                    MenuItem::group("dropdown 1", vec![MenuItem::item("item layer 2", 0)])
805                        .with_highlight(true),
806                    MenuItem::item("item layer 1", 0),
807                ],
808            )
809            .with_highlight(true)]);
810            assert_eq!(menu_state.dropdown_count(), 2);
811        }
812
813        {
814            // *menu bar 1
815            // *dropdown 1   >  item layer 2
816            // item layer 1    group layer 2 >
817            let menu_state = MenuState::new(vec![MenuItem::group(
818                "menu bar 1",
819                vec![
820                    MenuItem::group(
821                        "dropdown 1",
822                        vec![
823                            MenuItem::item("item layer 2", 0),
824                            MenuItem::group(
825                                "group layer 2",
826                                vec![MenuItem::item("item layer 3", 0)],
827                            ),
828                        ],
829                    )
830                    .with_highlight(true),
831                    MenuItem::item("item layer 1", 0),
832                ],
833            )
834            .with_highlight(true)]);
835            assert_eq!(menu_state.dropdown_count(), 2);
836        }
837
838        {
839            // *menu bar 1
840            // *dropdown 1   >  *item layer 2
841            // item layer 1    group layer 2 > item layer 3
842            let menu_state = MenuState::new(vec![MenuItem::group(
843                "menu bar 1",
844                vec![
845                    MenuItem::group(
846                        "dropdown 1",
847                        vec![
848                            MenuItem::item("item layer 2", 0).with_highlight(true),
849                            MenuItem::group(
850                                "group layer 2",
851                                vec![MenuItem::item("item layer 3", 0)],
852                            ),
853                        ],
854                    )
855                    .with_highlight(true),
856                    MenuItem::item("item layer 1", 0),
857                ],
858            )
859            .with_highlight(true)]);
860            assert_eq!(menu_state.dropdown_count(), 3);
861        }
862    }
863}