Skip to main content

ratatui_interact/components/
menu_bar.rs

1//! MenuBar component - Horizontal menu bar with dropdown menus
2//!
3//! A traditional desktop-style menu bar (File, Edit, View, Help) with support
4//! for dropdown menus, keyboard navigation, and mouse interaction.
5//!
6//! # Example
7//!
8//! ```rust
9//! use ratatui_interact::components::{
10//!     MenuBar, MenuBarState, MenuBarStyle, MenuBarItem, Menu,
11//!     handle_menu_bar_key, handle_menu_bar_mouse,
12//! };
13//! use ratatui::layout::Rect;
14//!
15//! // Create menus
16//! let menus = vec![
17//!     Menu::new("File")
18//!         .items(vec![
19//!             MenuBarItem::action("new", "New").shortcut("Ctrl+N"),
20//!             MenuBarItem::action("open", "Open").shortcut("Ctrl+O"),
21//!             MenuBarItem::separator(),
22//!             MenuBarItem::action("save", "Save").shortcut("Ctrl+S"),
23//!             MenuBarItem::action("quit", "Quit").shortcut("Ctrl+Q"),
24//!         ]),
25//!     Menu::new("Edit")
26//!         .items(vec![
27//!             MenuBarItem::action("undo", "Undo").shortcut("Ctrl+Z"),
28//!             MenuBarItem::action("redo", "Redo").shortcut("Ctrl+Y"),
29//!         ]),
30//! ];
31//!
32//! // Create state
33//! let mut state = MenuBarState::new();
34//!
35//! // Create menu bar widget
36//! let menu_bar = MenuBar::new(&menus, &state);
37//!
38//! // Render and handle events (see handle_menu_bar_key, handle_menu_bar_mouse)
39//! ```
40
41use crossterm::event::{KeyCode, KeyEvent, MouseButton, MouseEvent, MouseEventKind};
42use ratatui::{
43    Frame,
44    layout::Rect,
45    style::{Color, Style},
46    text::{Line, Span},
47    widgets::{Block, Borders, Clear, Paragraph},
48};
49
50use crate::traits::ClickRegion;
51
52/// Actions a menu bar can emit.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum MenuBarAction {
55    /// A menu was opened (menu index).
56    MenuOpen(usize),
57    /// The active menu was closed.
58    MenuClose,
59    /// An action item was selected (item ID).
60    ItemSelect(String),
61    /// Highlight changed (menu index, optional item index within dropdown).
62    HighlightChange(usize, Option<usize>),
63    /// A submenu was opened (parent menu index, parent item index).
64    SubmenuOpen(usize, usize),
65    /// A submenu was closed.
66    SubmenuClose,
67}
68
69/// A single item in a menu dropdown.
70#[derive(Debug, Clone)]
71pub enum MenuBarItem {
72    /// A clickable action item.
73    Action {
74        /// Unique identifier for this action.
75        id: String,
76        /// Display label.
77        label: String,
78        /// Optional keyboard shortcut display.
79        shortcut: Option<String>,
80        /// Whether the item is enabled.
81        enabled: bool,
82    },
83    /// A visual separator line.
84    Separator,
85    /// A submenu that opens additional items.
86    Submenu {
87        /// Display label.
88        label: String,
89        /// Child menu items.
90        items: Vec<MenuBarItem>,
91        /// Whether the submenu is enabled.
92        enabled: bool,
93    },
94}
95
96impl MenuBarItem {
97    /// Create a new action item.
98    pub fn action(id: impl Into<String>, label: impl Into<String>) -> Self {
99        Self::Action {
100            id: id.into(),
101            label: label.into(),
102            shortcut: None,
103            enabled: true,
104        }
105    }
106
107    /// Create a separator.
108    pub fn separator() -> Self {
109        Self::Separator
110    }
111
112    /// Create a submenu.
113    pub fn submenu(label: impl Into<String>, items: Vec<MenuBarItem>) -> Self {
114        Self::Submenu {
115            label: label.into(),
116            items,
117            enabled: true,
118        }
119    }
120
121    /// Add a shortcut display to this item.
122    pub fn shortcut(mut self, shortcut: impl Into<String>) -> Self {
123        if let Self::Action { shortcut: s, .. } = &mut self {
124            *s = Some(shortcut.into());
125        }
126        self
127    }
128
129    /// Set whether this item is enabled.
130    pub fn enabled(mut self, enabled: bool) -> Self {
131        match &mut self {
132            Self::Action { enabled: e, .. } => *e = enabled,
133            Self::Submenu { enabled: e, .. } => *e = enabled,
134            Self::Separator => {}
135        }
136        self
137    }
138
139    /// Check if this item is selectable (not a separator and enabled).
140    pub fn is_selectable(&self) -> bool {
141        match self {
142            Self::Action { enabled, .. } => *enabled,
143            Self::Separator => false,
144            Self::Submenu { enabled, .. } => *enabled,
145        }
146    }
147
148    /// Check if this item has a submenu.
149    pub fn has_submenu(&self) -> bool {
150        matches!(self, Self::Submenu { .. })
151    }
152
153    /// Get the ID if this is an action item.
154    pub fn id(&self) -> Option<&str> {
155        if let Self::Action { id, .. } = self {
156            Some(id)
157        } else {
158            None
159        }
160    }
161
162    /// Get the label for this item.
163    pub fn label(&self) -> Option<&str> {
164        match self {
165            Self::Action { label, .. } => Some(label),
166            Self::Submenu { label, .. } => Some(label),
167            Self::Separator => None,
168        }
169    }
170
171    /// Get the shortcut for this item.
172    pub fn get_shortcut(&self) -> Option<&str> {
173        if let Self::Action { shortcut, .. } = self {
174            shortcut.as_deref()
175        } else {
176            None
177        }
178    }
179
180    /// Check if this item is enabled.
181    pub fn is_enabled(&self) -> bool {
182        match self {
183            Self::Action { enabled, .. } => *enabled,
184            Self::Separator => false,
185            Self::Submenu { enabled, .. } => *enabled,
186        }
187    }
188
189    /// Get submenu items if this is a submenu.
190    pub fn submenu_items(&self) -> Option<&[MenuBarItem]> {
191        if let Self::Submenu { items, .. } = self {
192            Some(items)
193        } else {
194            None
195        }
196    }
197}
198
199/// A top-level menu in the menu bar.
200#[derive(Debug, Clone)]
201pub struct Menu {
202    /// Display label for the menu.
203    pub label: String,
204    /// Items in this menu's dropdown.
205    pub items: Vec<MenuBarItem>,
206    /// Whether this menu is enabled.
207    pub enabled: bool,
208}
209
210impl Menu {
211    /// Create a new menu with a label.
212    pub fn new(label: impl Into<String>) -> Self {
213        Self {
214            label: label.into(),
215            items: Vec::new(),
216            enabled: true,
217        }
218    }
219
220    /// Set the items for this menu.
221    pub fn items(mut self, items: Vec<MenuBarItem>) -> Self {
222        self.items = items;
223        self
224    }
225
226    /// Set whether this menu is enabled.
227    pub fn enabled(mut self, enabled: bool) -> Self {
228        self.enabled = enabled;
229        self
230    }
231}
232
233/// State for a menu bar.
234#[derive(Debug, Clone)]
235pub struct MenuBarState {
236    /// Whether any menu is currently open.
237    pub is_open: bool,
238    /// Index of the currently active/highlighted menu (always set when focused).
239    pub active_menu: usize,
240    /// Currently highlighted item index within the dropdown (if open).
241    pub highlighted_item: Option<usize>,
242    /// Scroll offset for long dropdown menus.
243    pub scroll_offset: u16,
244    /// Whether the menu bar has focus.
245    pub focused: bool,
246    /// Index of active submenu item (if any).
247    pub active_submenu: Option<usize>,
248    /// State for active submenu.
249    pub submenu_highlighted: Option<usize>,
250    /// Submenu scroll offset.
251    pub submenu_scroll_offset: u16,
252}
253
254impl Default for MenuBarState {
255    fn default() -> Self {
256        Self::new()
257    }
258}
259
260impl MenuBarState {
261    /// Create a new menu bar state.
262    pub fn new() -> Self {
263        Self {
264            is_open: false,
265            active_menu: 0,
266            highlighted_item: None,
267            scroll_offset: 0,
268            focused: false,
269            active_submenu: None,
270            submenu_highlighted: None,
271            submenu_scroll_offset: 0,
272        }
273    }
274
275    /// Open the menu at the given index.
276    pub fn open_menu(&mut self, index: usize) {
277        self.is_open = true;
278        self.active_menu = index;
279        self.highlighted_item = None;
280        self.scroll_offset = 0;
281        self.close_submenu();
282    }
283
284    /// Close any open menu.
285    pub fn close_menu(&mut self) {
286        self.is_open = false;
287        self.highlighted_item = None;
288        self.scroll_offset = 0;
289        self.close_submenu();
290    }
291
292    /// Toggle the menu at the given index.
293    pub fn toggle_menu(&mut self, index: usize) {
294        if self.is_open && self.active_menu == index {
295            self.close_menu();
296        } else {
297            self.open_menu(index);
298        }
299    }
300
301    /// Move to the next menu in the bar.
302    pub fn next_menu(&mut self, menu_count: usize) {
303        if menu_count == 0 {
304            return;
305        }
306        self.active_menu = (self.active_menu + 1) % menu_count;
307        if self.is_open {
308            self.highlighted_item = None;
309            self.scroll_offset = 0;
310            self.close_submenu();
311        }
312    }
313
314    /// Move to the previous menu in the bar.
315    pub fn prev_menu(&mut self, menu_count: usize) {
316        if menu_count == 0 {
317            return;
318        }
319        if self.active_menu == 0 {
320            self.active_menu = menu_count - 1;
321        } else {
322            self.active_menu -= 1;
323        }
324        if self.is_open {
325            self.highlighted_item = None;
326            self.scroll_offset = 0;
327            self.close_submenu();
328        }
329    }
330
331    /// Move highlight to the next item in the dropdown.
332    pub fn next_item(&mut self, items: &[MenuBarItem]) {
333        if items.is_empty() {
334            return;
335        }
336
337        let current = self.highlighted_item.unwrap_or(0);
338        let mut new_index = current;
339
340        loop {
341            new_index += 1;
342            if new_index >= items.len() {
343                // Wrap around to start
344                new_index = 0;
345            }
346            if new_index == current {
347                // We've gone full circle
348                break;
349            }
350            if items.get(new_index).is_some_and(|i| i.is_selectable()) {
351                self.highlighted_item = Some(new_index);
352                break;
353            }
354        }
355    }
356
357    /// Move highlight to the previous item in the dropdown.
358    pub fn prev_item(&mut self, items: &[MenuBarItem]) {
359        if items.is_empty() {
360            return;
361        }
362
363        let current = self.highlighted_item.unwrap_or(0);
364        let mut new_index = current;
365
366        loop {
367            if new_index == 0 {
368                new_index = items.len() - 1;
369            } else {
370                new_index -= 1;
371            }
372            if new_index == current {
373                // We've gone full circle
374                break;
375            }
376            if items.get(new_index).is_some_and(|i| i.is_selectable()) {
377                self.highlighted_item = Some(new_index);
378                break;
379            }
380        }
381    }
382
383    /// Move to first selectable item.
384    pub fn highlight_first(&mut self, items: &[MenuBarItem]) {
385        for (i, item) in items.iter().enumerate() {
386            if item.is_selectable() {
387                self.highlighted_item = Some(i);
388                self.scroll_offset = 0;
389                break;
390            }
391        }
392    }
393
394    /// Move to last selectable item.
395    pub fn highlight_last(&mut self, items: &[MenuBarItem]) {
396        for (i, item) in items.iter().enumerate().rev() {
397            if item.is_selectable() {
398                self.highlighted_item = Some(i);
399                break;
400            }
401        }
402    }
403
404    /// Select an item by index.
405    pub fn select_item(&mut self, index: usize) {
406        self.highlighted_item = Some(index);
407    }
408
409    /// Open submenu at the highlighted index.
410    pub fn open_submenu(&mut self) {
411        if let Some(idx) = self.highlighted_item {
412            self.active_submenu = Some(idx);
413            self.submenu_highlighted = None;
414            self.submenu_scroll_offset = 0;
415        }
416    }
417
418    /// Close any open submenu.
419    pub fn close_submenu(&mut self) {
420        self.active_submenu = None;
421        self.submenu_highlighted = None;
422        self.submenu_scroll_offset = 0;
423    }
424
425    /// Check if a submenu is open.
426    pub fn has_open_submenu(&self) -> bool {
427        self.active_submenu.is_some()
428    }
429
430    /// Move to next item in submenu.
431    pub fn next_submenu_item(&mut self, items: &[MenuBarItem]) {
432        if items.is_empty() {
433            return;
434        }
435
436        let current = self.submenu_highlighted.unwrap_or(0);
437        let mut new_index = current;
438
439        loop {
440            new_index += 1;
441            if new_index >= items.len() {
442                new_index = 0;
443            }
444            if new_index == current {
445                break;
446            }
447            if items.get(new_index).is_some_and(|i| i.is_selectable()) {
448                self.submenu_highlighted = Some(new_index);
449                break;
450            }
451        }
452    }
453
454    /// Move to previous item in submenu.
455    pub fn prev_submenu_item(&mut self, items: &[MenuBarItem]) {
456        if items.is_empty() {
457            return;
458        }
459
460        let current = self.submenu_highlighted.unwrap_or(0);
461        let mut new_index = current;
462
463        loop {
464            if new_index == 0 {
465                new_index = items.len() - 1;
466            } else {
467                new_index -= 1;
468            }
469            if new_index == current {
470                break;
471            }
472            if items.get(new_index).is_some_and(|i| i.is_selectable()) {
473                self.submenu_highlighted = Some(new_index);
474                break;
475            }
476        }
477    }
478
479    /// Ensure highlighted item is visible in viewport.
480    pub fn ensure_visible(&mut self, viewport_height: usize) {
481        if viewport_height == 0 {
482            return;
483        }
484        if let Some(idx) = self.highlighted_item {
485            if idx < self.scroll_offset as usize {
486                self.scroll_offset = idx as u16;
487            } else if idx >= self.scroll_offset as usize + viewport_height {
488                self.scroll_offset = (idx - viewport_height + 1) as u16;
489            }
490        }
491    }
492}
493
494/// Style configuration for menu bar.
495#[derive(Debug, Clone)]
496pub struct MenuBarStyle {
497    /// Background color for the menu bar.
498    pub bar_bg: Color,
499    /// Foreground color for menu labels.
500    pub bar_fg: Color,
501    /// Background color for highlighted menu label.
502    pub bar_highlight_bg: Color,
503    /// Foreground color for highlighted menu label.
504    pub bar_highlight_fg: Color,
505    /// Background color for dropdown.
506    pub dropdown_bg: Color,
507    /// Border color for dropdown.
508    pub dropdown_border: Color,
509    /// Normal item foreground color.
510    pub item_fg: Color,
511    /// Highlighted item background.
512    pub item_highlight_bg: Color,
513    /// Highlighted item foreground.
514    pub item_highlight_fg: Color,
515    /// Shortcut text color.
516    pub shortcut_fg: Color,
517    /// Disabled item/menu foreground.
518    pub disabled_fg: Color,
519    /// Separator color.
520    pub separator_fg: Color,
521    /// Minimum dropdown width.
522    pub dropdown_min_width: u16,
523    /// Maximum dropdown height (items visible).
524    pub dropdown_max_height: u16,
525    /// Padding between menu labels.
526    pub menu_padding: u16,
527    /// Horizontal padding inside dropdown.
528    pub dropdown_padding: u16,
529    /// Submenu indicator.
530    pub submenu_indicator: &'static str,
531    /// Separator character.
532    pub separator_char: char,
533}
534
535impl Default for MenuBarStyle {
536    fn default() -> Self {
537        Self {
538            bar_bg: Color::Rgb(50, 50, 50),
539            bar_fg: Color::White,
540            bar_highlight_bg: Color::Rgb(70, 70, 70),
541            bar_highlight_fg: Color::White,
542            dropdown_bg: Color::Rgb(40, 40, 40),
543            dropdown_border: Color::Rgb(80, 80, 80),
544            item_fg: Color::White,
545            item_highlight_bg: Color::Rgb(60, 100, 180),
546            item_highlight_fg: Color::White,
547            shortcut_fg: Color::Rgb(140, 140, 140),
548            disabled_fg: Color::DarkGray,
549            separator_fg: Color::Rgb(80, 80, 80),
550            dropdown_min_width: 15,
551            dropdown_max_height: 15,
552            menu_padding: 2,
553            dropdown_padding: 1,
554            submenu_indicator: "▶",
555            separator_char: '─',
556        }
557    }
558}
559
560impl MenuBarStyle {
561    /// Create a light theme style.
562    pub fn light() -> Self {
563        Self {
564            bar_bg: Color::Rgb(240, 240, 240),
565            bar_fg: Color::Rgb(30, 30, 30),
566            bar_highlight_bg: Color::Rgb(200, 200, 200),
567            bar_highlight_fg: Color::Rgb(30, 30, 30),
568            dropdown_bg: Color::Rgb(250, 250, 250),
569            dropdown_border: Color::Rgb(180, 180, 180),
570            item_fg: Color::Rgb(30, 30, 30),
571            item_highlight_bg: Color::Rgb(0, 120, 215),
572            item_highlight_fg: Color::White,
573            shortcut_fg: Color::Rgb(100, 100, 100),
574            disabled_fg: Color::Rgb(160, 160, 160),
575            separator_fg: Color::Rgb(200, 200, 200),
576            ..Default::default()
577        }
578    }
579
580    /// Create a minimal style.
581    pub fn minimal() -> Self {
582        Self {
583            bar_bg: Color::Reset,
584            bar_fg: Color::White,
585            bar_highlight_bg: Color::Blue,
586            bar_highlight_fg: Color::White,
587            dropdown_bg: Color::Reset,
588            dropdown_border: Color::Gray,
589            item_fg: Color::White,
590            item_highlight_bg: Color::Blue,
591            item_highlight_fg: Color::White,
592            shortcut_fg: Color::Gray,
593            disabled_fg: Color::DarkGray,
594            separator_fg: Color::DarkGray,
595            ..Default::default()
596        }
597    }
598
599    /// Set bar colors.
600    pub fn bar_colors(mut self, fg: Color, bg: Color) -> Self {
601        self.bar_fg = fg;
602        self.bar_bg = bg;
603        self
604    }
605
606    /// Set bar highlight colors.
607    pub fn bar_highlight(mut self, fg: Color, bg: Color) -> Self {
608        self.bar_highlight_fg = fg;
609        self.bar_highlight_bg = bg;
610        self
611    }
612
613    /// Set dropdown colors.
614    pub fn dropdown_colors(mut self, fg: Color, bg: Color, border: Color) -> Self {
615        self.item_fg = fg;
616        self.dropdown_bg = bg;
617        self.dropdown_border = border;
618        self
619    }
620
621    /// Set item highlight colors.
622    pub fn item_highlight(mut self, fg: Color, bg: Color) -> Self {
623        self.item_highlight_fg = fg;
624        self.item_highlight_bg = bg;
625        self
626    }
627
628    /// Set minimum dropdown width.
629    pub fn dropdown_min_width(mut self, width: u16) -> Self {
630        self.dropdown_min_width = width;
631        self
632    }
633
634    /// Set maximum dropdown height.
635    pub fn dropdown_max_height(mut self, height: u16) -> Self {
636        self.dropdown_max_height = height;
637        self
638    }
639
640    /// Set the submenu indicator.
641    pub fn submenu_indicator(mut self, indicator: &'static str) -> Self {
642        self.submenu_indicator = indicator;
643        self
644    }
645}
646
647/// Click region identifier for menu bar.
648#[derive(Debug, Clone, PartialEq, Eq)]
649pub enum MenuBarClickTarget {
650    /// A menu label in the bar.
651    MenuLabel(usize),
652    /// An item in the dropdown.
653    DropdownItem(usize),
654    /// An item in a submenu.
655    SubmenuItem(usize),
656}
657
658/// Menu bar widget.
659///
660/// A horizontal menu bar with dropdown menus.
661pub struct MenuBar<'a> {
662    menus: &'a [Menu],
663    state: &'a MenuBarState,
664    style: MenuBarStyle,
665}
666
667impl<'a> MenuBar<'a> {
668    /// Create a new menu bar.
669    pub fn new(menus: &'a [Menu], state: &'a MenuBarState) -> Self {
670        Self {
671            menus,
672            state,
673            style: MenuBarStyle::default(),
674        }
675    }
676
677    /// Set the style.
678    pub fn style(mut self, style: MenuBarStyle) -> Self {
679        self.style = style;
680        self
681    }
682
683    /// Calculate the required width for a dropdown.
684    fn calculate_dropdown_width(&self, items: &[MenuBarItem]) -> u16 {
685        let mut max_label_width = 0u16;
686        let mut max_shortcut_width = 0u16;
687
688        for item in items {
689            match item {
690                MenuBarItem::Action {
691                    label, shortcut, ..
692                } => {
693                    max_label_width = max_label_width.max(label.chars().count() as u16);
694                    if let Some(s) = shortcut {
695                        max_shortcut_width = max_shortcut_width.max(s.chars().count() as u16);
696                    }
697                }
698                MenuBarItem::Submenu { label, .. } => {
699                    // +2 for submenu indicator
700                    let label_width = label.chars().count() as u16 + 2;
701                    max_label_width = max_label_width.max(label_width);
702                }
703                MenuBarItem::Separator => {}
704            }
705        }
706
707        // Total width: padding + label + gap + shortcut + padding + borders
708        let content_width = self.style.dropdown_padding
709            + max_label_width
710            + if max_shortcut_width > 0 {
711                2 + max_shortcut_width
712            } else {
713                0
714            }
715            + self.style.dropdown_padding;
716
717        (content_width + 2).max(self.style.dropdown_min_width)
718    }
719
720    /// Calculate the height for a dropdown.
721    fn calculate_dropdown_height(&self, item_count: usize) -> u16 {
722        let visible = (item_count as u16).min(self.style.dropdown_max_height);
723        visible + 2 // +2 for borders
724    }
725
726    /// Calculate dropdown area based on menu position and screen bounds.
727    fn calculate_dropdown_area(&self, menu_x: u16, bar_bottom: u16, items: &[MenuBarItem], screen: Rect) -> Rect {
728        let width = self.calculate_dropdown_width(items);
729        let height = self.calculate_dropdown_height(items.len());
730
731        // Prefer below the menu bar
732        let y = bar_bottom;
733
734        // Prefer aligned with menu label, adjust if needed
735        let x = if menu_x + width <= screen.x + screen.width {
736            menu_x
737        } else {
738            screen.x + screen.width.saturating_sub(width)
739        };
740
741        // Ensure we stay within screen bounds
742        let final_width = width.min(screen.width.saturating_sub(x.saturating_sub(screen.x)));
743        let final_height = height.min(screen.height.saturating_sub(y.saturating_sub(screen.y)));
744
745        Rect::new(x, y, final_width, final_height)
746    }
747
748    /// Render the menu bar and return click regions.
749    ///
750    /// Returns a tuple of (bar_area, dropdown_area, click_regions).
751    pub fn render_stateful(
752        &self,
753        frame: &mut Frame,
754        area: Rect,
755    ) -> (Rect, Option<Rect>, Vec<ClickRegion<MenuBarClickTarget>>) {
756        let mut regions = Vec::new();
757
758        if area.height == 0 || self.menus.is_empty() {
759            return (Rect::default(), None, regions);
760        }
761
762        // Render the bar (1 row high)
763        let bar_area = Rect::new(area.x, area.y, area.width, 1);
764
765        // Fill bar background
766        let bar_style = Style::default().bg(self.style.bar_bg);
767        let bar_line = " ".repeat(bar_area.width as usize);
768        let bar_para = Paragraph::new(Span::styled(bar_line, bar_style));
769        frame.render_widget(bar_para, bar_area);
770
771        // Render menu labels
772        let mut x = bar_area.x;
773        let mut menu_positions: Vec<(u16, u16)> = Vec::new(); // (x, width) for each menu
774
775        for (idx, menu) in self.menus.iter().enumerate() {
776            let label = format!(" {} ", menu.label);
777            let label_width = label.chars().count() as u16;
778
779            let is_active = self.state.focused && idx == self.state.active_menu;
780            let is_open = self.state.is_open && idx == self.state.active_menu;
781
782            let (fg, bg) = if !menu.enabled {
783                (self.style.disabled_fg, self.style.bar_bg)
784            } else if is_active || is_open {
785                (self.style.bar_highlight_fg, self.style.bar_highlight_bg)
786            } else {
787                (self.style.bar_fg, self.style.bar_bg)
788            };
789
790            let style = Style::default().fg(fg).bg(bg);
791            let label_area = Rect::new(x, bar_area.y, label_width, 1);
792
793            let para = Paragraph::new(Span::styled(label.clone(), style));
794            frame.render_widget(para, label_area);
795
796            menu_positions.push((x, label_width));
797
798            // Register click region for menu label
799            if menu.enabled {
800                regions.push(ClickRegion::new(label_area, MenuBarClickTarget::MenuLabel(idx)));
801            }
802
803            x += label_width + self.style.menu_padding;
804        }
805
806        // Render dropdown if a menu is open
807        let dropdown_area = if self.state.is_open {
808            if let Some(menu) = self.menus.get(self.state.active_menu) {
809                if let Some(&(menu_x, _)) = menu_positions.get(self.state.active_menu) {
810                    let screen = frame.area();
811                    let dropdown_area = self.calculate_dropdown_area(
812                        menu_x,
813                        bar_area.y + 1,
814                        &menu.items,
815                        screen,
816                    );
817
818                    // Clear background (overlay)
819                    frame.render_widget(Clear, dropdown_area);
820
821                    // Render border
822                    let block = Block::default()
823                        .borders(Borders::ALL)
824                        .border_style(Style::default().fg(self.style.dropdown_border))
825                        .style(Style::default().bg(self.style.dropdown_bg));
826
827                    let inner = block.inner(dropdown_area);
828                    frame.render_widget(block, dropdown_area);
829
830                    // Render items
831                    let visible_count = inner.height as usize;
832                    let scroll = self.state.scroll_offset as usize;
833
834                    for (display_idx, (item_idx, item)) in menu
835                        .items
836                        .iter()
837                        .enumerate()
838                        .skip(scroll)
839                        .take(visible_count)
840                        .enumerate()
841                    {
842                        let y = inner.y + display_idx as u16;
843                        let item_area = Rect::new(inner.x, y, inner.width, 1);
844
845                        let is_highlighted = self.state.highlighted_item == Some(item_idx);
846
847                        self.render_menu_item(
848                            frame,
849                            item,
850                            item_area,
851                            is_highlighted,
852                            &mut regions,
853                            item_idx,
854                            false,
855                        );
856                    }
857
858                    // Render submenu if open
859                    if let Some(submenu_idx) = self.state.active_submenu {
860                        if let Some(MenuBarItem::Submenu { items, .. }) = menu.items.get(submenu_idx) {
861                            let submenu_x = dropdown_area.x + dropdown_area.width;
862                            let submenu_y = dropdown_area.y + 1 + (submenu_idx as u16).saturating_sub(self.state.scroll_offset);
863
864                            let submenu_width = self.calculate_dropdown_width(items);
865                            let submenu_height = self.calculate_dropdown_height(items.len());
866
867                            let screen = frame.area();
868
869                            // Adjust submenu position to stay on screen
870                            let final_x = if submenu_x + submenu_width <= screen.x + screen.width {
871                                submenu_x
872                            } else {
873                                dropdown_area.x.saturating_sub(submenu_width)
874                            };
875
876                            let submenu_area = Rect::new(
877                                final_x,
878                                submenu_y.min(screen.y + screen.height - submenu_height),
879                                submenu_width,
880                                submenu_height,
881                            );
882
883                            // Clear and render submenu
884                            frame.render_widget(Clear, submenu_area);
885
886                            let block = Block::default()
887                                .borders(Borders::ALL)
888                                .border_style(Style::default().fg(self.style.dropdown_border))
889                                .style(Style::default().bg(self.style.dropdown_bg));
890
891                            let sub_inner = block.inner(submenu_area);
892                            frame.render_widget(block, submenu_area);
893
894                            let sub_visible = sub_inner.height as usize;
895                            let sub_scroll = self.state.submenu_scroll_offset as usize;
896
897                            for (display_idx, (item_idx, item)) in items
898                                .iter()
899                                .enumerate()
900                                .skip(sub_scroll)
901                                .take(sub_visible)
902                                .enumerate()
903                            {
904                                let y = sub_inner.y + display_idx as u16;
905                                let item_area = Rect::new(sub_inner.x, y, sub_inner.width, 1);
906
907                                let is_highlighted = self.state.submenu_highlighted == Some(item_idx);
908
909                                self.render_menu_item(
910                                    frame,
911                                    item,
912                                    item_area,
913                                    is_highlighted,
914                                    &mut regions,
915                                    item_idx,
916                                    true,
917                                );
918                            }
919                        }
920                    }
921
922                    Some(dropdown_area)
923                } else {
924                    None
925                }
926            } else {
927                None
928            }
929        } else {
930            None
931        };
932
933        (bar_area, dropdown_area, regions)
934    }
935
936    /// Render a single menu item.
937    #[allow(clippy::too_many_arguments)]
938    fn render_menu_item(
939        &self,
940        frame: &mut Frame,
941        item: &MenuBarItem,
942        item_area: Rect,
943        is_highlighted: bool,
944        regions: &mut Vec<ClickRegion<MenuBarClickTarget>>,
945        item_idx: usize,
946        is_submenu: bool,
947    ) {
948        match item {
949            MenuBarItem::Separator => {
950                let sep_line: String =
951                    std::iter::repeat_n(self.style.separator_char, item_area.width as usize).collect();
952                let para = Paragraph::new(Span::styled(
953                    sep_line,
954                    Style::default().fg(self.style.separator_fg).bg(self.style.dropdown_bg),
955                ));
956                frame.render_widget(para, item_area);
957            }
958            MenuBarItem::Action {
959                label,
960                shortcut,
961                enabled,
962                id,
963            } => {
964                let (fg, bg) = if !enabled {
965                    (self.style.disabled_fg, self.style.dropdown_bg)
966                } else if is_highlighted {
967                    (self.style.item_highlight_fg, self.style.item_highlight_bg)
968                } else {
969                    (self.style.item_fg, self.style.dropdown_bg)
970                };
971
972                let style = Style::default().fg(fg).bg(bg);
973                let shortcut_style = Style::default()
974                    .fg(if *enabled {
975                        self.style.shortcut_fg
976                    } else {
977                        self.style.disabled_fg
978                    })
979                    .bg(bg);
980
981                let mut spans = Vec::new();
982
983                // Padding
984                spans.push(Span::styled(
985                    " ".repeat(self.style.dropdown_padding as usize),
986                    style,
987                ));
988
989                // Label
990                spans.push(Span::styled(label.clone(), style));
991
992                // Fill space before shortcut
993                let current_len: usize = spans.iter().map(|s| s.content.chars().count()).sum();
994                let shortcut_len = shortcut.as_ref().map(|s| s.chars().count()).unwrap_or(0);
995                let fill_len = (item_area.width as usize)
996                    .saturating_sub(current_len)
997                    .saturating_sub(shortcut_len)
998                    .saturating_sub(self.style.dropdown_padding as usize);
999
1000                if fill_len > 0 {
1001                    spans.push(Span::styled(" ".repeat(fill_len), style));
1002                }
1003
1004                // Shortcut
1005                if let Some(sc) = shortcut {
1006                    spans.push(Span::styled(sc.clone(), shortcut_style));
1007                }
1008
1009                // Right padding
1010                spans.push(Span::styled(
1011                    " ".repeat(self.style.dropdown_padding as usize),
1012                    style,
1013                ));
1014
1015                let para = Paragraph::new(Line::from(spans));
1016                frame.render_widget(para, item_area);
1017
1018                // Register click region
1019                if *enabled {
1020                    let target = if is_submenu {
1021                        MenuBarClickTarget::SubmenuItem(item_idx)
1022                    } else {
1023                        MenuBarClickTarget::DropdownItem(item_idx)
1024                    };
1025                    regions.push(ClickRegion::new(item_area, target));
1026                }
1027
1028                // Silence unused variable warning
1029                let _ = id;
1030            }
1031            MenuBarItem::Submenu { label, enabled, .. } => {
1032                let (fg, bg) = if !enabled {
1033                    (self.style.disabled_fg, self.style.dropdown_bg)
1034                } else if is_highlighted {
1035                    (self.style.item_highlight_fg, self.style.item_highlight_bg)
1036                } else {
1037                    (self.style.item_fg, self.style.dropdown_bg)
1038                };
1039
1040                let style = Style::default().fg(fg).bg(bg);
1041
1042                let mut spans = Vec::new();
1043
1044                // Padding
1045                spans.push(Span::styled(
1046                    " ".repeat(self.style.dropdown_padding as usize),
1047                    style,
1048                ));
1049
1050                // Label
1051                spans.push(Span::styled(label.clone(), style));
1052
1053                // Fill and submenu indicator
1054                let current_len: usize = spans.iter().map(|s| s.content.chars().count()).sum();
1055                let indicator_len = self.style.submenu_indicator.chars().count();
1056                let fill_len = (item_area.width as usize)
1057                    .saturating_sub(current_len)
1058                    .saturating_sub(indicator_len)
1059                    .saturating_sub(self.style.dropdown_padding as usize);
1060
1061                if fill_len > 0 {
1062                    spans.push(Span::styled(" ".repeat(fill_len), style));
1063                }
1064
1065                spans.push(Span::styled(self.style.submenu_indicator, style));
1066
1067                // Right padding
1068                spans.push(Span::styled(
1069                    " ".repeat(self.style.dropdown_padding as usize),
1070                    style,
1071                ));
1072
1073                let para = Paragraph::new(Line::from(spans));
1074                frame.render_widget(para, item_area);
1075
1076                // Register click region (only for parent dropdown, not for nested submenus)
1077                if *enabled && !is_submenu {
1078                    regions.push(ClickRegion::new(item_area, MenuBarClickTarget::DropdownItem(item_idx)));
1079                }
1080            }
1081        }
1082    }
1083}
1084
1085/// Handle keyboard events for menu bar.
1086///
1087/// Returns `Some(MenuBarAction)` if an action was triggered, `None` otherwise.
1088///
1089/// # Key Bindings
1090///
1091/// - `Left/Right` - Navigate between menus
1092/// - `Up/Down` - Navigate within dropdown (opens menu if closed)
1093/// - `Enter/Space` - Select item or toggle menu
1094/// - `Escape` - Close menu
1095/// - `Home` - Jump to first item
1096/// - `End` - Jump to last item
1097#[allow(clippy::collapsible_match)]
1098pub fn handle_menu_bar_key(
1099    key: &KeyEvent,
1100    state: &mut MenuBarState,
1101    menus: &[Menu],
1102) -> Option<MenuBarAction> {
1103    if menus.is_empty() {
1104        return None;
1105    }
1106
1107    // If submenu is open, handle submenu navigation
1108    if state.has_open_submenu() {
1109        if let Some(menu) = menus.get(state.active_menu) {
1110            if let Some(submenu_idx) = state.active_submenu {
1111                if let Some(MenuBarItem::Submenu { items, .. }) = menu.items.get(submenu_idx) {
1112                    match key.code {
1113                        KeyCode::Esc | KeyCode::Left => {
1114                            state.close_submenu();
1115                            return Some(MenuBarAction::SubmenuClose);
1116                        }
1117                        KeyCode::Up => {
1118                            state.prev_submenu_item(items);
1119                            return Some(MenuBarAction::HighlightChange(
1120                                state.active_menu,
1121                                state.submenu_highlighted,
1122                            ));
1123                        }
1124                        KeyCode::Down => {
1125                            state.next_submenu_item(items);
1126                            return Some(MenuBarAction::HighlightChange(
1127                                state.active_menu,
1128                                state.submenu_highlighted,
1129                            ));
1130                        }
1131                        KeyCode::Enter | KeyCode::Char(' ') => {
1132                            if let Some(idx) = state.submenu_highlighted {
1133                                if let Some(item) = items.get(idx) {
1134                                    if let MenuBarItem::Action { id, enabled, .. } = item {
1135                                        if *enabled {
1136                                            let action_id = id.clone();
1137                                            state.close_menu();
1138                                            return Some(MenuBarAction::ItemSelect(action_id));
1139                                        }
1140                                    }
1141                                }
1142                            }
1143                            return None;
1144                        }
1145                        _ => return None,
1146                    }
1147                }
1148            }
1149        }
1150    }
1151
1152    match key.code {
1153        KeyCode::Left => {
1154            state.prev_menu(menus.len());
1155            Some(MenuBarAction::HighlightChange(state.active_menu, None))
1156        }
1157        KeyCode::Right => {
1158            // If on a submenu item, open it
1159            if state.is_open {
1160                if let Some(menu) = menus.get(state.active_menu) {
1161                    if let Some(idx) = state.highlighted_item {
1162                        if let Some(item) = menu.items.get(idx) {
1163                            if item.has_submenu() && item.is_enabled() {
1164                                state.open_submenu();
1165                                return Some(MenuBarAction::SubmenuOpen(state.active_menu, idx));
1166                            }
1167                        }
1168                    }
1169                }
1170            }
1171            state.next_menu(menus.len());
1172            Some(MenuBarAction::HighlightChange(state.active_menu, None))
1173        }
1174        KeyCode::Down => {
1175            if state.is_open {
1176                if let Some(menu) = menus.get(state.active_menu) {
1177                    state.next_item(&menu.items);
1178                    state.ensure_visible(8);
1179                    Some(MenuBarAction::HighlightChange(
1180                        state.active_menu,
1181                        state.highlighted_item,
1182                    ))
1183                } else {
1184                    None
1185                }
1186            } else {
1187                state.open_menu(state.active_menu);
1188                if let Some(menu) = menus.get(state.active_menu) {
1189                    state.highlight_first(&menu.items);
1190                }
1191                Some(MenuBarAction::MenuOpen(state.active_menu))
1192            }
1193        }
1194        KeyCode::Up => {
1195            if state.is_open {
1196                if let Some(menu) = menus.get(state.active_menu) {
1197                    state.prev_item(&menu.items);
1198                    state.ensure_visible(8);
1199                    Some(MenuBarAction::HighlightChange(
1200                        state.active_menu,
1201                        state.highlighted_item,
1202                    ))
1203                } else {
1204                    None
1205                }
1206            } else {
1207                None
1208            }
1209        }
1210        KeyCode::Enter | KeyCode::Char(' ') => {
1211            if state.is_open {
1212                if let Some(menu) = menus.get(state.active_menu) {
1213                    if let Some(idx) = state.highlighted_item {
1214                        if let Some(item) = menu.items.get(idx) {
1215                            match item {
1216                                MenuBarItem::Action { id, enabled, .. } if *enabled => {
1217                                    let action_id = id.clone();
1218                                    state.close_menu();
1219                                    return Some(MenuBarAction::ItemSelect(action_id));
1220                                }
1221                                MenuBarItem::Submenu { enabled, .. } if *enabled => {
1222                                    state.open_submenu();
1223                                    return Some(MenuBarAction::SubmenuOpen(state.active_menu, idx));
1224                                }
1225                                _ => {}
1226                            }
1227                        }
1228                    }
1229                }
1230                None
1231            } else {
1232                state.open_menu(state.active_menu);
1233                if let Some(menu) = menus.get(state.active_menu) {
1234                    state.highlight_first(&menu.items);
1235                }
1236                Some(MenuBarAction::MenuOpen(state.active_menu))
1237            }
1238        }
1239        KeyCode::Esc => {
1240            if state.is_open {
1241                state.close_menu();
1242                Some(MenuBarAction::MenuClose)
1243            } else {
1244                None
1245            }
1246        }
1247        KeyCode::Home => {
1248            if state.is_open {
1249                if let Some(menu) = menus.get(state.active_menu) {
1250                    state.highlight_first(&menu.items);
1251                    Some(MenuBarAction::HighlightChange(
1252                        state.active_menu,
1253                        state.highlighted_item,
1254                    ))
1255                } else {
1256                    None
1257                }
1258            } else {
1259                state.active_menu = 0;
1260                Some(MenuBarAction::HighlightChange(0, None))
1261            }
1262        }
1263        KeyCode::End => {
1264            if state.is_open {
1265                if let Some(menu) = menus.get(state.active_menu) {
1266                    state.highlight_last(&menu.items);
1267                    state.ensure_visible(menu.items.len());
1268                    Some(MenuBarAction::HighlightChange(
1269                        state.active_menu,
1270                        state.highlighted_item,
1271                    ))
1272                } else {
1273                    None
1274                }
1275            } else {
1276                state.active_menu = menus.len().saturating_sub(1);
1277                Some(MenuBarAction::HighlightChange(state.active_menu, None))
1278            }
1279        }
1280        _ => None,
1281    }
1282}
1283
1284/// Handle mouse events for menu bar.
1285///
1286/// Returns `Some(MenuBarAction)` if an action was triggered, `None` otherwise.
1287///
1288/// # Arguments
1289///
1290/// * `mouse` - The mouse event
1291/// * `state` - Mutable reference to menu bar state
1292/// * `bar_area` - The rendered bar area
1293/// * `dropdown_area` - The rendered dropdown area (if any)
1294/// * `click_regions` - Click regions from `render_stateful`
1295/// * `menus` - The menu definitions
1296#[allow(clippy::collapsible_match)]
1297pub fn handle_menu_bar_mouse(
1298    mouse: &MouseEvent,
1299    state: &mut MenuBarState,
1300    bar_area: Rect,
1301    dropdown_area: Option<Rect>,
1302    click_regions: &[ClickRegion<MenuBarClickTarget>],
1303    menus: &[Menu],
1304) -> Option<MenuBarAction> {
1305    let col = mouse.column;
1306    let row = mouse.row;
1307
1308    match mouse.kind {
1309        MouseEventKind::Down(MouseButton::Left) => {
1310            // Check if clicked on a menu label
1311            for region in click_regions {
1312                if region.contains(col, row) {
1313                    match &region.data {
1314                        MenuBarClickTarget::MenuLabel(idx) => {
1315                            state.toggle_menu(*idx);
1316                            if state.is_open {
1317                                if let Some(menu) = menus.get(*idx) {
1318                                    state.highlight_first(&menu.items);
1319                                }
1320                                return Some(MenuBarAction::MenuOpen(*idx));
1321                            } else {
1322                                return Some(MenuBarAction::MenuClose);
1323                            }
1324                        }
1325                        MenuBarClickTarget::DropdownItem(idx) => {
1326                            if let Some(menu) = menus.get(state.active_menu) {
1327                                if let Some(item) = menu.items.get(*idx) {
1328                                    match item {
1329                                        MenuBarItem::Action { id, enabled, .. } if *enabled => {
1330                                            let action_id = id.clone();
1331                                            state.close_menu();
1332                                            return Some(MenuBarAction::ItemSelect(action_id));
1333                                        }
1334                                        MenuBarItem::Submenu { enabled, .. } if *enabled => {
1335                                            state.highlighted_item = Some(*idx);
1336                                            state.open_submenu();
1337                                            return Some(MenuBarAction::SubmenuOpen(state.active_menu, *idx));
1338                                        }
1339                                        _ => {}
1340                                    }
1341                                }
1342                            }
1343                        }
1344                        MenuBarClickTarget::SubmenuItem(idx) => {
1345                            if let Some(menu) = menus.get(state.active_menu) {
1346                                if let Some(submenu_idx) = state.active_submenu {
1347                                    if let Some(MenuBarItem::Submenu { items, .. }) = menu.items.get(submenu_idx) {
1348                                        if let Some(item) = items.get(*idx) {
1349                                            if let MenuBarItem::Action { id, enabled, .. } = item {
1350                                                if *enabled {
1351                                                    let action_id = id.clone();
1352                                                    state.close_menu();
1353                                                    return Some(MenuBarAction::ItemSelect(action_id));
1354                                                }
1355                                            }
1356                                        }
1357                                    }
1358                                }
1359                            }
1360                        }
1361                    }
1362                }
1363            }
1364
1365            // Check if clicked outside menu
1366            let in_bar = bar_area.intersects(Rect::new(col, row, 1, 1));
1367            let in_dropdown = dropdown_area
1368                .map(|d| d.intersects(Rect::new(col, row, 1, 1)))
1369                .unwrap_or(false);
1370
1371            if state.is_open && !in_bar && !in_dropdown {
1372                state.close_menu();
1373                return Some(MenuBarAction::MenuClose);
1374            }
1375
1376            None
1377        }
1378        MouseEventKind::Moved => {
1379            // Update highlight on hover
1380            for region in click_regions {
1381                if region.contains(col, row) {
1382                    match &region.data {
1383                        MenuBarClickTarget::MenuLabel(idx) => {
1384                            // If a menu is open and we hover over a different menu label, switch to it
1385                            if state.is_open && state.active_menu != *idx {
1386                                state.open_menu(*idx);
1387                                if let Some(menu) = menus.get(*idx) {
1388                                    state.highlight_first(&menu.items);
1389                                }
1390                                return Some(MenuBarAction::MenuOpen(*idx));
1391                            }
1392                        }
1393                        MenuBarClickTarget::DropdownItem(idx) => {
1394                            if state.highlighted_item != Some(*idx) {
1395                                state.highlighted_item = Some(*idx);
1396                                // Close submenu when moving to different item
1397                                if state.active_submenu.is_some() && state.active_submenu != Some(*idx) {
1398                                    state.close_submenu();
1399                                }
1400                                return Some(MenuBarAction::HighlightChange(
1401                                    state.active_menu,
1402                                    Some(*idx),
1403                                ));
1404                            }
1405                        }
1406                        MenuBarClickTarget::SubmenuItem(idx) => {
1407                            if state.submenu_highlighted != Some(*idx) {
1408                                state.submenu_highlighted = Some(*idx);
1409                                return Some(MenuBarAction::HighlightChange(
1410                                    state.active_menu,
1411                                    Some(*idx),
1412                                ));
1413                            }
1414                        }
1415                    }
1416                    break;
1417                }
1418            }
1419            None
1420        }
1421        _ => None,
1422    }
1423}
1424
1425/// Calculate the height needed for a menu bar (always 1).
1426pub fn calculate_menu_bar_height() -> u16 {
1427    1
1428}
1429
1430/// Calculate the height needed for a dropdown menu.
1431pub fn calculate_dropdown_height(item_count: usize, max_visible: u16) -> u16 {
1432    let visible = (item_count as u16).min(max_visible);
1433    visible + 2 // +2 for borders
1434}
1435
1436#[cfg(test)]
1437mod tests {
1438    use super::*;
1439
1440    #[test]
1441    fn test_menu_bar_item_action() {
1442        let item = MenuBarItem::action("save", "Save").shortcut("Ctrl+S");
1443
1444        assert!(item.is_selectable());
1445        assert!(!item.has_submenu());
1446        assert_eq!(item.id(), Some("save"));
1447        assert_eq!(item.label(), Some("Save"));
1448        assert_eq!(item.get_shortcut(), Some("Ctrl+S"));
1449    }
1450
1451    #[test]
1452    fn test_menu_bar_item_separator() {
1453        let item = MenuBarItem::separator();
1454
1455        assert!(!item.is_selectable());
1456        assert!(!item.has_submenu());
1457        assert_eq!(item.label(), None);
1458    }
1459
1460    #[test]
1461    fn test_menu_bar_item_submenu() {
1462        let items = vec![MenuBarItem::action("sub1", "Sub Item 1")];
1463        let item = MenuBarItem::submenu("More", items);
1464
1465        assert!(item.is_selectable());
1466        assert!(item.has_submenu());
1467        assert_eq!(item.label(), Some("More"));
1468        assert!(item.submenu_items().is_some());
1469    }
1470
1471    #[test]
1472    fn test_menu_bar_item_disabled() {
1473        let item = MenuBarItem::action("delete", "Delete").enabled(false);
1474
1475        assert!(!item.is_selectable());
1476        assert!(!item.is_enabled());
1477    }
1478
1479    #[test]
1480    fn test_menu_creation() {
1481        let menu = Menu::new("File")
1482            .items(vec![
1483                MenuBarItem::action("new", "New"),
1484                MenuBarItem::separator(),
1485                MenuBarItem::action("quit", "Quit"),
1486            ])
1487            .enabled(true);
1488
1489        assert_eq!(menu.label, "File");
1490        assert_eq!(menu.items.len(), 3);
1491        assert!(menu.enabled);
1492    }
1493
1494    #[test]
1495    fn test_menu_bar_state_new() {
1496        let state = MenuBarState::new();
1497
1498        assert!(!state.is_open);
1499        assert_eq!(state.active_menu, 0);
1500        assert_eq!(state.highlighted_item, None);
1501        assert!(!state.focused);
1502    }
1503
1504    #[test]
1505    fn test_menu_bar_state_open_close() {
1506        let mut state = MenuBarState::new();
1507
1508        state.open_menu(1);
1509        assert!(state.is_open);
1510        assert_eq!(state.active_menu, 1);
1511        assert_eq!(state.highlighted_item, None);
1512
1513        state.close_menu();
1514        assert!(!state.is_open);
1515    }
1516
1517    #[test]
1518    fn test_menu_bar_state_toggle() {
1519        let mut state = MenuBarState::new();
1520
1521        state.toggle_menu(0);
1522        assert!(state.is_open);
1523        assert_eq!(state.active_menu, 0);
1524
1525        state.toggle_menu(0);
1526        assert!(!state.is_open);
1527
1528        state.toggle_menu(0);
1529        assert!(state.is_open);
1530
1531        // Toggle different menu while open
1532        state.toggle_menu(1);
1533        assert!(state.is_open);
1534        assert_eq!(state.active_menu, 1);
1535    }
1536
1537    #[test]
1538    fn test_menu_bar_state_navigation() {
1539        let mut state = MenuBarState::new();
1540        state.active_menu = 0;
1541
1542        state.next_menu(3);
1543        assert_eq!(state.active_menu, 1);
1544
1545        state.next_menu(3);
1546        assert_eq!(state.active_menu, 2);
1547
1548        state.next_menu(3);
1549        assert_eq!(state.active_menu, 0); // Wrap around
1550
1551        state.prev_menu(3);
1552        assert_eq!(state.active_menu, 2); // Wrap around
1553
1554        state.prev_menu(3);
1555        assert_eq!(state.active_menu, 1);
1556    }
1557
1558    #[test]
1559    fn test_menu_bar_state_item_navigation() {
1560        let mut state = MenuBarState::new();
1561        state.open_menu(0);
1562
1563        let items = vec![
1564            MenuBarItem::action("a", "A"),
1565            MenuBarItem::separator(),
1566            MenuBarItem::action("b", "B"),
1567            MenuBarItem::action("c", "C"),
1568        ];
1569
1570        // Move down (should skip separator)
1571        state.next_item(&items);
1572        // With no initial highlighted item, wraps from 0
1573        assert!(state.highlighted_item.is_some());
1574
1575        state.highlight_first(&items);
1576        assert_eq!(state.highlighted_item, Some(0));
1577
1578        state.next_item(&items);
1579        assert_eq!(state.highlighted_item, Some(2)); // Skipped separator
1580
1581        state.next_item(&items);
1582        assert_eq!(state.highlighted_item, Some(3));
1583
1584        state.prev_item(&items);
1585        assert_eq!(state.highlighted_item, Some(2));
1586
1587        state.prev_item(&items);
1588        assert_eq!(state.highlighted_item, Some(0));
1589    }
1590
1591    #[test]
1592    fn test_menu_bar_state_submenu() {
1593        let mut state = MenuBarState::new();
1594        state.open_menu(0);
1595        state.highlighted_item = Some(2);
1596
1597        assert!(!state.has_open_submenu());
1598
1599        state.open_submenu();
1600        assert!(state.has_open_submenu());
1601        assert_eq!(state.active_submenu, Some(2));
1602
1603        state.close_submenu();
1604        assert!(!state.has_open_submenu());
1605    }
1606
1607    #[test]
1608    fn test_menu_bar_style_default() {
1609        let style = MenuBarStyle::default();
1610        assert_eq!(style.dropdown_min_width, 15);
1611        assert_eq!(style.dropdown_max_height, 15);
1612        assert_eq!(style.submenu_indicator, "▶");
1613    }
1614
1615    #[test]
1616    fn test_menu_bar_style_builders() {
1617        let style = MenuBarStyle::default()
1618            .dropdown_min_width(20)
1619            .dropdown_max_height(10)
1620            .submenu_indicator("→");
1621
1622        assert_eq!(style.dropdown_min_width, 20);
1623        assert_eq!(style.dropdown_max_height, 10);
1624        assert_eq!(style.submenu_indicator, "→");
1625    }
1626
1627    #[test]
1628    fn test_menu_bar_style_presets() {
1629        let light = MenuBarStyle::light();
1630        assert_eq!(light.bar_bg, Color::Rgb(240, 240, 240));
1631
1632        let minimal = MenuBarStyle::minimal();
1633        assert_eq!(minimal.bar_bg, Color::Reset);
1634    }
1635
1636    #[test]
1637    fn test_handle_key_left_right() {
1638        let mut state = MenuBarState::new();
1639        state.focused = true;
1640
1641        let menus = vec![
1642            Menu::new("File").items(vec![]),
1643            Menu::new("Edit").items(vec![]),
1644            Menu::new("View").items(vec![]),
1645        ];
1646
1647        let key = KeyEvent::from(KeyCode::Right);
1648        let action = handle_menu_bar_key(&key, &mut state, &menus);
1649        assert_eq!(action, Some(MenuBarAction::HighlightChange(1, None)));
1650        assert_eq!(state.active_menu, 1);
1651
1652        let key = KeyEvent::from(KeyCode::Left);
1653        let action = handle_menu_bar_key(&key, &mut state, &menus);
1654        assert_eq!(action, Some(MenuBarAction::HighlightChange(0, None)));
1655        assert_eq!(state.active_menu, 0);
1656    }
1657
1658    #[test]
1659    fn test_handle_key_down_opens_menu() {
1660        let mut state = MenuBarState::new();
1661        state.focused = true;
1662
1663        let menus = vec![Menu::new("File").items(vec![
1664            MenuBarItem::action("new", "New"),
1665        ])];
1666
1667        let key = KeyEvent::from(KeyCode::Down);
1668        let action = handle_menu_bar_key(&key, &mut state, &menus);
1669
1670        assert_eq!(action, Some(MenuBarAction::MenuOpen(0)));
1671        assert!(state.is_open);
1672    }
1673
1674    #[test]
1675    fn test_handle_key_escape_closes() {
1676        let mut state = MenuBarState::new();
1677        state.open_menu(0);
1678
1679        let menus = vec![Menu::new("File").items(vec![])];
1680
1681        let key = KeyEvent::from(KeyCode::Esc);
1682        let action = handle_menu_bar_key(&key, &mut state, &menus);
1683
1684        assert_eq!(action, Some(MenuBarAction::MenuClose));
1685        assert!(!state.is_open);
1686    }
1687
1688    #[test]
1689    fn test_handle_key_enter_selects_item() {
1690        let mut state = MenuBarState::new();
1691        state.open_menu(0);
1692        state.highlighted_item = Some(0);
1693
1694        let menus = vec![Menu::new("File").items(vec![
1695            MenuBarItem::action("new", "New"),
1696        ])];
1697
1698        let key = KeyEvent::from(KeyCode::Enter);
1699        let action = handle_menu_bar_key(&key, &mut state, &menus);
1700
1701        assert_eq!(action, Some(MenuBarAction::ItemSelect("new".to_string())));
1702        assert!(!state.is_open);
1703    }
1704
1705    #[test]
1706    fn test_handle_key_enter_opens_submenu() {
1707        let mut state = MenuBarState::new();
1708        state.open_menu(0);
1709        state.highlighted_item = Some(0);
1710
1711        let menus = vec![Menu::new("File").items(vec![
1712            MenuBarItem::submenu("Recent", vec![
1713                MenuBarItem::action("file1", "File 1"),
1714            ]),
1715        ])];
1716
1717        let key = KeyEvent::from(KeyCode::Enter);
1718        let action = handle_menu_bar_key(&key, &mut state, &menus);
1719
1720        assert_eq!(action, Some(MenuBarAction::SubmenuOpen(0, 0)));
1721        assert!(state.has_open_submenu());
1722    }
1723
1724    #[test]
1725    fn test_handle_key_empty_menus() {
1726        let mut state = MenuBarState::new();
1727        let menus: Vec<Menu> = vec![];
1728
1729        let key = KeyEvent::from(KeyCode::Down);
1730        let action = handle_menu_bar_key(&key, &mut state, &menus);
1731
1732        assert!(action.is_none());
1733    }
1734
1735    #[test]
1736    fn test_menu_bar_action_equality() {
1737        assert_eq!(MenuBarAction::MenuOpen(0), MenuBarAction::MenuOpen(0));
1738        assert_ne!(MenuBarAction::MenuOpen(0), MenuBarAction::MenuOpen(1));
1739        assert_eq!(MenuBarAction::MenuClose, MenuBarAction::MenuClose);
1740        assert_eq!(
1741            MenuBarAction::ItemSelect("test".to_string()),
1742            MenuBarAction::ItemSelect("test".to_string())
1743        );
1744        assert_eq!(
1745            MenuBarAction::HighlightChange(0, Some(1)),
1746            MenuBarAction::HighlightChange(0, Some(1))
1747        );
1748    }
1749
1750    #[test]
1751    fn test_calculate_heights() {
1752        assert_eq!(calculate_menu_bar_height(), 1);
1753        assert_eq!(calculate_dropdown_height(5, 15), 7); // 5 + 2
1754        assert_eq!(calculate_dropdown_height(20, 15), 17); // 15 + 2 (clamped)
1755    }
1756
1757    #[test]
1758    fn test_menu_bar_widget_new() {
1759        let menus = vec![Menu::new("File").items(vec![])];
1760        let state = MenuBarState::new();
1761        let _menu_bar = MenuBar::new(&menus, &state);
1762    }
1763
1764    #[test]
1765    fn test_menu_bar_widget_style() {
1766        let menus = vec![Menu::new("File").items(vec![])];
1767        let state = MenuBarState::new();
1768        let style = MenuBarStyle::light();
1769        let _menu_bar = MenuBar::new(&menus, &state).style(style);
1770    }
1771
1772    #[test]
1773    fn test_click_target_equality() {
1774        assert_eq!(
1775            MenuBarClickTarget::MenuLabel(0),
1776            MenuBarClickTarget::MenuLabel(0)
1777        );
1778        assert_ne!(
1779            MenuBarClickTarget::MenuLabel(0),
1780            MenuBarClickTarget::MenuLabel(1)
1781        );
1782        assert_eq!(
1783            MenuBarClickTarget::DropdownItem(0),
1784            MenuBarClickTarget::DropdownItem(0)
1785        );
1786        assert_eq!(
1787            MenuBarClickTarget::SubmenuItem(0),
1788            MenuBarClickTarget::SubmenuItem(0)
1789        );
1790    }
1791
1792    #[test]
1793    fn test_menu_bar_state_ensure_visible() {
1794        let mut state = MenuBarState::new();
1795        state.highlighted_item = Some(15);
1796        state.scroll_offset = 0;
1797
1798        state.ensure_visible(10);
1799        assert!(state.scroll_offset >= 6);
1800
1801        state.highlighted_item = Some(3);
1802        state.ensure_visible(10);
1803        assert!(state.scroll_offset <= 3);
1804    }
1805
1806    #[test]
1807    fn test_menu_bar_state_highlight_first_last() {
1808        let mut state = MenuBarState::new();
1809        state.open_menu(0);
1810
1811        let items = vec![
1812            MenuBarItem::separator(),
1813            MenuBarItem::action("a", "A"),
1814            MenuBarItem::action("b", "B"),
1815            MenuBarItem::separator(),
1816            MenuBarItem::action("c", "C"),
1817        ];
1818
1819        state.highlight_first(&items);
1820        assert_eq!(state.highlighted_item, Some(1));
1821
1822        state.highlight_last(&items);
1823        assert_eq!(state.highlighted_item, Some(4));
1824    }
1825
1826    #[test]
1827    fn test_submenu_navigation() {
1828        let mut state = MenuBarState::new();
1829        state.open_menu(0);
1830        state.highlighted_item = Some(0);
1831        state.open_submenu();
1832
1833        let items = vec![
1834            MenuBarItem::action("a", "A"),
1835            MenuBarItem::separator(),
1836            MenuBarItem::action("b", "B"),
1837        ];
1838
1839        state.next_submenu_item(&items);
1840        // Should skip separator
1841        assert!(state.submenu_highlighted.is_some());
1842
1843        state.prev_submenu_item(&items);
1844        assert!(state.submenu_highlighted.is_some());
1845    }
1846}