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 From<&crate::theme::Theme> for MenuBarStyle {
561    fn from(theme: &crate::theme::Theme) -> Self {
562        let p = &theme.palette;
563        Self {
564            bar_bg: p.surface_raised,
565            bar_fg: p.text,
566            bar_highlight_bg: Color::Rgb(70, 70, 70),
567            bar_highlight_fg: p.text,
568            dropdown_bg: p.surface,
569            dropdown_border: p.separator,
570            item_fg: p.text,
571            item_highlight_bg: p.menu_highlight_bg,
572            item_highlight_fg: p.menu_highlight_fg,
573            shortcut_fg: p.text_muted,
574            disabled_fg: p.text_disabled,
575            separator_fg: p.separator,
576            dropdown_min_width: 15,
577            dropdown_max_height: 15,
578            menu_padding: 2,
579            dropdown_padding: 1,
580            submenu_indicator: "▶",
581            separator_char: '─',
582        }
583    }
584}
585
586impl MenuBarStyle {
587    /// Create a light theme style.
588    pub fn light() -> Self {
589        Self {
590            bar_bg: Color::Rgb(240, 240, 240),
591            bar_fg: Color::Rgb(30, 30, 30),
592            bar_highlight_bg: Color::Rgb(200, 200, 200),
593            bar_highlight_fg: Color::Rgb(30, 30, 30),
594            dropdown_bg: Color::Rgb(250, 250, 250),
595            dropdown_border: Color::Rgb(180, 180, 180),
596            item_fg: Color::Rgb(30, 30, 30),
597            item_highlight_bg: Color::Rgb(0, 120, 215),
598            item_highlight_fg: Color::White,
599            shortcut_fg: Color::Rgb(100, 100, 100),
600            disabled_fg: Color::Rgb(160, 160, 160),
601            separator_fg: Color::Rgb(200, 200, 200),
602            ..Default::default()
603        }
604    }
605
606    /// Create a minimal style.
607    pub fn minimal() -> Self {
608        Self {
609            bar_bg: Color::Reset,
610            bar_fg: Color::White,
611            bar_highlight_bg: Color::Blue,
612            bar_highlight_fg: Color::White,
613            dropdown_bg: Color::Reset,
614            dropdown_border: Color::Gray,
615            item_fg: Color::White,
616            item_highlight_bg: Color::Blue,
617            item_highlight_fg: Color::White,
618            shortcut_fg: Color::Gray,
619            disabled_fg: Color::DarkGray,
620            separator_fg: Color::DarkGray,
621            ..Default::default()
622        }
623    }
624
625    /// Set bar colors.
626    pub fn bar_colors(mut self, fg: Color, bg: Color) -> Self {
627        self.bar_fg = fg;
628        self.bar_bg = bg;
629        self
630    }
631
632    /// Set bar highlight colors.
633    pub fn bar_highlight(mut self, fg: Color, bg: Color) -> Self {
634        self.bar_highlight_fg = fg;
635        self.bar_highlight_bg = bg;
636        self
637    }
638
639    /// Set dropdown colors.
640    pub fn dropdown_colors(mut self, fg: Color, bg: Color, border: Color) -> Self {
641        self.item_fg = fg;
642        self.dropdown_bg = bg;
643        self.dropdown_border = border;
644        self
645    }
646
647    /// Set item highlight colors.
648    pub fn item_highlight(mut self, fg: Color, bg: Color) -> Self {
649        self.item_highlight_fg = fg;
650        self.item_highlight_bg = bg;
651        self
652    }
653
654    /// Set minimum dropdown width.
655    pub fn dropdown_min_width(mut self, width: u16) -> Self {
656        self.dropdown_min_width = width;
657        self
658    }
659
660    /// Set maximum dropdown height.
661    pub fn dropdown_max_height(mut self, height: u16) -> Self {
662        self.dropdown_max_height = height;
663        self
664    }
665
666    /// Set the submenu indicator.
667    pub fn submenu_indicator(mut self, indicator: &'static str) -> Self {
668        self.submenu_indicator = indicator;
669        self
670    }
671}
672
673/// Click region identifier for menu bar.
674#[derive(Debug, Clone, PartialEq, Eq)]
675pub enum MenuBarClickTarget {
676    /// A menu label in the bar.
677    MenuLabel(usize),
678    /// An item in the dropdown.
679    DropdownItem(usize),
680    /// An item in a submenu.
681    SubmenuItem(usize),
682}
683
684/// Menu bar widget.
685///
686/// A horizontal menu bar with dropdown menus.
687pub struct MenuBar<'a> {
688    menus: &'a [Menu],
689    state: &'a MenuBarState,
690    style: MenuBarStyle,
691}
692
693impl<'a> MenuBar<'a> {
694    /// Create a new menu bar.
695    pub fn new(menus: &'a [Menu], state: &'a MenuBarState) -> Self {
696        Self {
697            menus,
698            state,
699            style: MenuBarStyle::default(),
700        }
701    }
702
703    /// Set the style.
704    pub fn style(mut self, style: MenuBarStyle) -> Self {
705        self.style = style;
706        self
707    }
708
709    /// Apply a theme to derive the style.
710    pub fn theme(self, theme: &crate::theme::Theme) -> Self {
711        self.style(MenuBarStyle::from(theme))
712    }
713
714    /// Calculate the required width for a dropdown.
715    fn calculate_dropdown_width(&self, items: &[MenuBarItem]) -> u16 {
716        let mut max_label_width = 0u16;
717        let mut max_shortcut_width = 0u16;
718
719        for item in items {
720            match item {
721                MenuBarItem::Action {
722                    label, shortcut, ..
723                } => {
724                    max_label_width = max_label_width.max(label.chars().count() as u16);
725                    if let Some(s) = shortcut {
726                        max_shortcut_width = max_shortcut_width.max(s.chars().count() as u16);
727                    }
728                }
729                MenuBarItem::Submenu { label, .. } => {
730                    // +2 for submenu indicator
731                    let label_width = label.chars().count() as u16 + 2;
732                    max_label_width = max_label_width.max(label_width);
733                }
734                MenuBarItem::Separator => {}
735            }
736        }
737
738        // Total width: padding + label + gap + shortcut + padding + borders
739        let content_width = self.style.dropdown_padding
740            + max_label_width
741            + if max_shortcut_width > 0 {
742                2 + max_shortcut_width
743            } else {
744                0
745            }
746            + self.style.dropdown_padding;
747
748        (content_width + 2).max(self.style.dropdown_min_width)
749    }
750
751    /// Calculate the height for a dropdown.
752    fn calculate_dropdown_height(&self, item_count: usize) -> u16 {
753        let visible = (item_count as u16).min(self.style.dropdown_max_height);
754        visible + 2 // +2 for borders
755    }
756
757    /// Calculate dropdown area based on menu position and screen bounds.
758    fn calculate_dropdown_area(
759        &self,
760        menu_x: u16,
761        bar_bottom: u16,
762        items: &[MenuBarItem],
763        screen: Rect,
764    ) -> Rect {
765        let width = self.calculate_dropdown_width(items);
766        let height = self.calculate_dropdown_height(items.len());
767
768        // Prefer below the menu bar
769        let y = bar_bottom;
770
771        // Prefer aligned with menu label, adjust if needed
772        let x = if menu_x + width <= screen.x + screen.width {
773            menu_x
774        } else {
775            screen.x + screen.width.saturating_sub(width)
776        };
777
778        // Ensure we stay within screen bounds
779        let final_width = width.min(screen.width.saturating_sub(x.saturating_sub(screen.x)));
780        let final_height = height.min(screen.height.saturating_sub(y.saturating_sub(screen.y)));
781
782        Rect::new(x, y, final_width, final_height)
783    }
784
785    /// Render the menu bar and return click regions.
786    ///
787    /// Returns a tuple of (bar_area, dropdown_area, click_regions).
788    pub fn render_stateful(
789        &self,
790        frame: &mut Frame,
791        area: Rect,
792    ) -> (Rect, Option<Rect>, Vec<ClickRegion<MenuBarClickTarget>>) {
793        let mut regions = Vec::new();
794
795        if area.height == 0 || self.menus.is_empty() {
796            return (Rect::default(), None, regions);
797        }
798
799        // Render the bar (1 row high)
800        let bar_area = Rect::new(area.x, area.y, area.width, 1);
801
802        // Fill bar background
803        let bar_style = Style::default().bg(self.style.bar_bg);
804        let bar_line = " ".repeat(bar_area.width as usize);
805        let bar_para = Paragraph::new(Span::styled(bar_line, bar_style));
806        frame.render_widget(bar_para, bar_area);
807
808        // Render menu labels
809        let mut x = bar_area.x;
810        let mut menu_positions: Vec<(u16, u16)> = Vec::new(); // (x, width) for each menu
811
812        for (idx, menu) in self.menus.iter().enumerate() {
813            let label = format!(" {} ", menu.label);
814            let label_width = label.chars().count() as u16;
815
816            let is_active = self.state.focused && idx == self.state.active_menu;
817            let is_open = self.state.is_open && idx == self.state.active_menu;
818
819            let (fg, bg) = if !menu.enabled {
820                (self.style.disabled_fg, self.style.bar_bg)
821            } else if is_active || is_open {
822                (self.style.bar_highlight_fg, self.style.bar_highlight_bg)
823            } else {
824                (self.style.bar_fg, self.style.bar_bg)
825            };
826
827            let style = Style::default().fg(fg).bg(bg);
828            let label_area = Rect::new(x, bar_area.y, label_width, 1);
829
830            let para = Paragraph::new(Span::styled(label.clone(), style));
831            frame.render_widget(para, label_area);
832
833            menu_positions.push((x, label_width));
834
835            // Register click region for menu label
836            if menu.enabled {
837                regions.push(ClickRegion::new(
838                    label_area,
839                    MenuBarClickTarget::MenuLabel(idx),
840                ));
841            }
842
843            x += label_width + self.style.menu_padding;
844        }
845
846        // Render dropdown if a menu is open
847        let dropdown_area = if self.state.is_open {
848            if let Some(menu) = self.menus.get(self.state.active_menu) {
849                if let Some(&(menu_x, _)) = menu_positions.get(self.state.active_menu) {
850                    let screen = frame.area();
851                    let dropdown_area =
852                        self.calculate_dropdown_area(menu_x, bar_area.y + 1, &menu.items, screen);
853
854                    // Clear background (overlay)
855                    frame.render_widget(Clear, dropdown_area);
856
857                    // Render border
858                    let block = Block::default()
859                        .borders(Borders::ALL)
860                        .border_style(Style::default().fg(self.style.dropdown_border))
861                        .style(Style::default().bg(self.style.dropdown_bg));
862
863                    let inner = block.inner(dropdown_area);
864                    frame.render_widget(block, dropdown_area);
865
866                    // Render items
867                    let visible_count = inner.height as usize;
868                    let scroll = self.state.scroll_offset as usize;
869
870                    for (display_idx, (item_idx, item)) in menu
871                        .items
872                        .iter()
873                        .enumerate()
874                        .skip(scroll)
875                        .take(visible_count)
876                        .enumerate()
877                    {
878                        let y = inner.y + display_idx as u16;
879                        let item_area = Rect::new(inner.x, y, inner.width, 1);
880
881                        let is_highlighted = self.state.highlighted_item == Some(item_idx);
882
883                        self.render_menu_item(
884                            frame,
885                            item,
886                            item_area,
887                            is_highlighted,
888                            &mut regions,
889                            item_idx,
890                            false,
891                        );
892                    }
893
894                    // Render submenu if open
895                    if let Some(submenu_idx) = self.state.active_submenu {
896                        if let Some(MenuBarItem::Submenu { items, .. }) =
897                            menu.items.get(submenu_idx)
898                        {
899                            let submenu_x = dropdown_area.x + dropdown_area.width;
900                            let submenu_y = dropdown_area.y
901                                + 1
902                                + (submenu_idx as u16).saturating_sub(self.state.scroll_offset);
903
904                            let submenu_width = self.calculate_dropdown_width(items);
905                            let submenu_height = self.calculate_dropdown_height(items.len());
906
907                            let screen = frame.area();
908
909                            // Adjust submenu position to stay on screen
910                            let final_x = if submenu_x + submenu_width <= screen.x + screen.width {
911                                submenu_x
912                            } else {
913                                dropdown_area.x.saturating_sub(submenu_width)
914                            };
915
916                            let submenu_area = Rect::new(
917                                final_x,
918                                submenu_y.min(screen.y + screen.height - submenu_height),
919                                submenu_width,
920                                submenu_height,
921                            );
922
923                            // Clear and render submenu
924                            frame.render_widget(Clear, submenu_area);
925
926                            let block = Block::default()
927                                .borders(Borders::ALL)
928                                .border_style(Style::default().fg(self.style.dropdown_border))
929                                .style(Style::default().bg(self.style.dropdown_bg));
930
931                            let sub_inner = block.inner(submenu_area);
932                            frame.render_widget(block, submenu_area);
933
934                            let sub_visible = sub_inner.height as usize;
935                            let sub_scroll = self.state.submenu_scroll_offset as usize;
936
937                            for (display_idx, (item_idx, item)) in items
938                                .iter()
939                                .enumerate()
940                                .skip(sub_scroll)
941                                .take(sub_visible)
942                                .enumerate()
943                            {
944                                let y = sub_inner.y + display_idx as u16;
945                                let item_area = Rect::new(sub_inner.x, y, sub_inner.width, 1);
946
947                                let is_highlighted =
948                                    self.state.submenu_highlighted == Some(item_idx);
949
950                                self.render_menu_item(
951                                    frame,
952                                    item,
953                                    item_area,
954                                    is_highlighted,
955                                    &mut regions,
956                                    item_idx,
957                                    true,
958                                );
959                            }
960                        }
961                    }
962
963                    Some(dropdown_area)
964                } else {
965                    None
966                }
967            } else {
968                None
969            }
970        } else {
971            None
972        };
973
974        (bar_area, dropdown_area, regions)
975    }
976
977    /// Render a single menu item.
978    #[allow(clippy::too_many_arguments)]
979    fn render_menu_item(
980        &self,
981        frame: &mut Frame,
982        item: &MenuBarItem,
983        item_area: Rect,
984        is_highlighted: bool,
985        regions: &mut Vec<ClickRegion<MenuBarClickTarget>>,
986        item_idx: usize,
987        is_submenu: bool,
988    ) {
989        match item {
990            MenuBarItem::Separator => {
991                let sep_line: String =
992                    std::iter::repeat_n(self.style.separator_char, item_area.width as usize)
993                        .collect();
994                let para = Paragraph::new(Span::styled(
995                    sep_line,
996                    Style::default()
997                        .fg(self.style.separator_fg)
998                        .bg(self.style.dropdown_bg),
999                ));
1000                frame.render_widget(para, item_area);
1001            }
1002            MenuBarItem::Action {
1003                label,
1004                shortcut,
1005                enabled,
1006                id,
1007            } => {
1008                let (fg, bg) = if !enabled {
1009                    (self.style.disabled_fg, self.style.dropdown_bg)
1010                } else if is_highlighted {
1011                    (self.style.item_highlight_fg, self.style.item_highlight_bg)
1012                } else {
1013                    (self.style.item_fg, self.style.dropdown_bg)
1014                };
1015
1016                let style = Style::default().fg(fg).bg(bg);
1017                let shortcut_style = Style::default()
1018                    .fg(if *enabled {
1019                        self.style.shortcut_fg
1020                    } else {
1021                        self.style.disabled_fg
1022                    })
1023                    .bg(bg);
1024
1025                let mut spans = Vec::new();
1026
1027                // Padding
1028                spans.push(Span::styled(
1029                    " ".repeat(self.style.dropdown_padding as usize),
1030                    style,
1031                ));
1032
1033                // Label
1034                spans.push(Span::styled(label.clone(), style));
1035
1036                // Fill space before shortcut
1037                let current_len: usize = spans.iter().map(|s| s.content.chars().count()).sum();
1038                let shortcut_len = shortcut.as_ref().map(|s| s.chars().count()).unwrap_or(0);
1039                let fill_len = (item_area.width as usize)
1040                    .saturating_sub(current_len)
1041                    .saturating_sub(shortcut_len)
1042                    .saturating_sub(self.style.dropdown_padding as usize);
1043
1044                if fill_len > 0 {
1045                    spans.push(Span::styled(" ".repeat(fill_len), style));
1046                }
1047
1048                // Shortcut
1049                if let Some(sc) = shortcut {
1050                    spans.push(Span::styled(sc.clone(), shortcut_style));
1051                }
1052
1053                // Right padding
1054                spans.push(Span::styled(
1055                    " ".repeat(self.style.dropdown_padding as usize),
1056                    style,
1057                ));
1058
1059                let para = Paragraph::new(Line::from(spans));
1060                frame.render_widget(para, item_area);
1061
1062                // Register click region
1063                if *enabled {
1064                    let target = if is_submenu {
1065                        MenuBarClickTarget::SubmenuItem(item_idx)
1066                    } else {
1067                        MenuBarClickTarget::DropdownItem(item_idx)
1068                    };
1069                    regions.push(ClickRegion::new(item_area, target));
1070                }
1071
1072                // Silence unused variable warning
1073                let _ = id;
1074            }
1075            MenuBarItem::Submenu { label, enabled, .. } => {
1076                let (fg, bg) = if !enabled {
1077                    (self.style.disabled_fg, self.style.dropdown_bg)
1078                } else if is_highlighted {
1079                    (self.style.item_highlight_fg, self.style.item_highlight_bg)
1080                } else {
1081                    (self.style.item_fg, self.style.dropdown_bg)
1082                };
1083
1084                let style = Style::default().fg(fg).bg(bg);
1085
1086                let mut spans = Vec::new();
1087
1088                // Padding
1089                spans.push(Span::styled(
1090                    " ".repeat(self.style.dropdown_padding as usize),
1091                    style,
1092                ));
1093
1094                // Label
1095                spans.push(Span::styled(label.clone(), style));
1096
1097                // Fill and submenu indicator
1098                let current_len: usize = spans.iter().map(|s| s.content.chars().count()).sum();
1099                let indicator_len = self.style.submenu_indicator.chars().count();
1100                let fill_len = (item_area.width as usize)
1101                    .saturating_sub(current_len)
1102                    .saturating_sub(indicator_len)
1103                    .saturating_sub(self.style.dropdown_padding as usize);
1104
1105                if fill_len > 0 {
1106                    spans.push(Span::styled(" ".repeat(fill_len), style));
1107                }
1108
1109                spans.push(Span::styled(self.style.submenu_indicator, style));
1110
1111                // Right padding
1112                spans.push(Span::styled(
1113                    " ".repeat(self.style.dropdown_padding as usize),
1114                    style,
1115                ));
1116
1117                let para = Paragraph::new(Line::from(spans));
1118                frame.render_widget(para, item_area);
1119
1120                // Register click region (only for parent dropdown, not for nested submenus)
1121                if *enabled && !is_submenu {
1122                    regions.push(ClickRegion::new(
1123                        item_area,
1124                        MenuBarClickTarget::DropdownItem(item_idx),
1125                    ));
1126                }
1127            }
1128        }
1129    }
1130}
1131
1132/// Handle keyboard events for menu bar.
1133///
1134/// Returns `Some(MenuBarAction)` if an action was triggered, `None` otherwise.
1135///
1136/// # Key Bindings
1137///
1138/// - `Left/Right` - Navigate between menus
1139/// - `Up/Down` - Navigate within dropdown (opens menu if closed)
1140/// - `Enter/Space` - Select item or toggle menu
1141/// - `Escape` - Close menu
1142/// - `Home` - Jump to first item
1143/// - `End` - Jump to last item
1144#[allow(clippy::collapsible_match)]
1145pub fn handle_menu_bar_key(
1146    key: &KeyEvent,
1147    state: &mut MenuBarState,
1148    menus: &[Menu],
1149) -> Option<MenuBarAction> {
1150    if menus.is_empty() {
1151        return None;
1152    }
1153
1154    // If submenu is open, handle submenu navigation
1155    if state.has_open_submenu() {
1156        if let Some(menu) = menus.get(state.active_menu) {
1157            if let Some(submenu_idx) = state.active_submenu {
1158                if let Some(MenuBarItem::Submenu { items, .. }) = menu.items.get(submenu_idx) {
1159                    match key.code {
1160                        KeyCode::Esc | KeyCode::Left => {
1161                            state.close_submenu();
1162                            return Some(MenuBarAction::SubmenuClose);
1163                        }
1164                        KeyCode::Up => {
1165                            state.prev_submenu_item(items);
1166                            return Some(MenuBarAction::HighlightChange(
1167                                state.active_menu,
1168                                state.submenu_highlighted,
1169                            ));
1170                        }
1171                        KeyCode::Down => {
1172                            state.next_submenu_item(items);
1173                            return Some(MenuBarAction::HighlightChange(
1174                                state.active_menu,
1175                                state.submenu_highlighted,
1176                            ));
1177                        }
1178                        KeyCode::Enter | KeyCode::Char(' ') => {
1179                            if let Some(idx) = state.submenu_highlighted {
1180                                if let Some(item) = items.get(idx) {
1181                                    if let MenuBarItem::Action { id, enabled, .. } = item {
1182                                        if *enabled {
1183                                            let action_id = id.clone();
1184                                            state.close_menu();
1185                                            return Some(MenuBarAction::ItemSelect(action_id));
1186                                        }
1187                                    }
1188                                }
1189                            }
1190                            return None;
1191                        }
1192                        _ => return None,
1193                    }
1194                }
1195            }
1196        }
1197    }
1198
1199    match key.code {
1200        KeyCode::Left => {
1201            state.prev_menu(menus.len());
1202            Some(MenuBarAction::HighlightChange(state.active_menu, None))
1203        }
1204        KeyCode::Right => {
1205            // If on a submenu item, open it
1206            if state.is_open {
1207                if let Some(menu) = menus.get(state.active_menu) {
1208                    if let Some(idx) = state.highlighted_item {
1209                        if let Some(item) = menu.items.get(idx) {
1210                            if item.has_submenu() && item.is_enabled() {
1211                                state.open_submenu();
1212                                return Some(MenuBarAction::SubmenuOpen(state.active_menu, idx));
1213                            }
1214                        }
1215                    }
1216                }
1217            }
1218            state.next_menu(menus.len());
1219            Some(MenuBarAction::HighlightChange(state.active_menu, None))
1220        }
1221        KeyCode::Down => {
1222            if state.is_open {
1223                if let Some(menu) = menus.get(state.active_menu) {
1224                    state.next_item(&menu.items);
1225                    state.ensure_visible(8);
1226                    Some(MenuBarAction::HighlightChange(
1227                        state.active_menu,
1228                        state.highlighted_item,
1229                    ))
1230                } else {
1231                    None
1232                }
1233            } else {
1234                state.open_menu(state.active_menu);
1235                if let Some(menu) = menus.get(state.active_menu) {
1236                    state.highlight_first(&menu.items);
1237                }
1238                Some(MenuBarAction::MenuOpen(state.active_menu))
1239            }
1240        }
1241        KeyCode::Up => {
1242            if state.is_open {
1243                if let Some(menu) = menus.get(state.active_menu) {
1244                    state.prev_item(&menu.items);
1245                    state.ensure_visible(8);
1246                    Some(MenuBarAction::HighlightChange(
1247                        state.active_menu,
1248                        state.highlighted_item,
1249                    ))
1250                } else {
1251                    None
1252                }
1253            } else {
1254                None
1255            }
1256        }
1257        KeyCode::Enter | KeyCode::Char(' ') => {
1258            if state.is_open {
1259                if let Some(menu) = menus.get(state.active_menu) {
1260                    if let Some(idx) = state.highlighted_item {
1261                        if let Some(item) = menu.items.get(idx) {
1262                            match item {
1263                                MenuBarItem::Action { id, enabled, .. } if *enabled => {
1264                                    let action_id = id.clone();
1265                                    state.close_menu();
1266                                    return Some(MenuBarAction::ItemSelect(action_id));
1267                                }
1268                                MenuBarItem::Submenu { enabled, .. } if *enabled => {
1269                                    state.open_submenu();
1270                                    return Some(MenuBarAction::SubmenuOpen(
1271                                        state.active_menu,
1272                                        idx,
1273                                    ));
1274                                }
1275                                _ => {}
1276                            }
1277                        }
1278                    }
1279                }
1280                None
1281            } else {
1282                state.open_menu(state.active_menu);
1283                if let Some(menu) = menus.get(state.active_menu) {
1284                    state.highlight_first(&menu.items);
1285                }
1286                Some(MenuBarAction::MenuOpen(state.active_menu))
1287            }
1288        }
1289        KeyCode::Esc => {
1290            if state.is_open {
1291                state.close_menu();
1292                Some(MenuBarAction::MenuClose)
1293            } else {
1294                None
1295            }
1296        }
1297        KeyCode::Home => {
1298            if state.is_open {
1299                if let Some(menu) = menus.get(state.active_menu) {
1300                    state.highlight_first(&menu.items);
1301                    Some(MenuBarAction::HighlightChange(
1302                        state.active_menu,
1303                        state.highlighted_item,
1304                    ))
1305                } else {
1306                    None
1307                }
1308            } else {
1309                state.active_menu = 0;
1310                Some(MenuBarAction::HighlightChange(0, None))
1311            }
1312        }
1313        KeyCode::End => {
1314            if state.is_open {
1315                if let Some(menu) = menus.get(state.active_menu) {
1316                    state.highlight_last(&menu.items);
1317                    state.ensure_visible(menu.items.len());
1318                    Some(MenuBarAction::HighlightChange(
1319                        state.active_menu,
1320                        state.highlighted_item,
1321                    ))
1322                } else {
1323                    None
1324                }
1325            } else {
1326                state.active_menu = menus.len().saturating_sub(1);
1327                Some(MenuBarAction::HighlightChange(state.active_menu, None))
1328            }
1329        }
1330        _ => None,
1331    }
1332}
1333
1334/// Handle mouse events for menu bar.
1335///
1336/// Returns `Some(MenuBarAction)` if an action was triggered, `None` otherwise.
1337///
1338/// # Arguments
1339///
1340/// * `mouse` - The mouse event
1341/// * `state` - Mutable reference to menu bar state
1342/// * `bar_area` - The rendered bar area
1343/// * `dropdown_area` - The rendered dropdown area (if any)
1344/// * `click_regions` - Click regions from `render_stateful`
1345/// * `menus` - The menu definitions
1346#[allow(clippy::collapsible_match)]
1347pub fn handle_menu_bar_mouse(
1348    mouse: &MouseEvent,
1349    state: &mut MenuBarState,
1350    bar_area: Rect,
1351    dropdown_area: Option<Rect>,
1352    click_regions: &[ClickRegion<MenuBarClickTarget>],
1353    menus: &[Menu],
1354) -> Option<MenuBarAction> {
1355    let col = mouse.column;
1356    let row = mouse.row;
1357
1358    match mouse.kind {
1359        MouseEventKind::Down(MouseButton::Left) => {
1360            // Check if clicked on a menu label
1361            for region in click_regions {
1362                if region.contains(col, row) {
1363                    match &region.data {
1364                        MenuBarClickTarget::MenuLabel(idx) => {
1365                            state.toggle_menu(*idx);
1366                            if state.is_open {
1367                                if let Some(menu) = menus.get(*idx) {
1368                                    state.highlight_first(&menu.items);
1369                                }
1370                                return Some(MenuBarAction::MenuOpen(*idx));
1371                            } else {
1372                                return Some(MenuBarAction::MenuClose);
1373                            }
1374                        }
1375                        MenuBarClickTarget::DropdownItem(idx) => {
1376                            if let Some(menu) = menus.get(state.active_menu) {
1377                                if let Some(item) = menu.items.get(*idx) {
1378                                    match item {
1379                                        MenuBarItem::Action { id, enabled, .. } if *enabled => {
1380                                            let action_id = id.clone();
1381                                            state.close_menu();
1382                                            return Some(MenuBarAction::ItemSelect(action_id));
1383                                        }
1384                                        MenuBarItem::Submenu { enabled, .. } if *enabled => {
1385                                            state.highlighted_item = Some(*idx);
1386                                            state.open_submenu();
1387                                            return Some(MenuBarAction::SubmenuOpen(
1388                                                state.active_menu,
1389                                                *idx,
1390                                            ));
1391                                        }
1392                                        _ => {}
1393                                    }
1394                                }
1395                            }
1396                        }
1397                        MenuBarClickTarget::SubmenuItem(idx) => {
1398                            if let Some(menu) = menus.get(state.active_menu) {
1399                                if let Some(submenu_idx) = state.active_submenu {
1400                                    if let Some(MenuBarItem::Submenu { items, .. }) =
1401                                        menu.items.get(submenu_idx)
1402                                    {
1403                                        if let Some(item) = items.get(*idx) {
1404                                            if let MenuBarItem::Action { id, enabled, .. } = item {
1405                                                if *enabled {
1406                                                    let action_id = id.clone();
1407                                                    state.close_menu();
1408                                                    return Some(MenuBarAction::ItemSelect(
1409                                                        action_id,
1410                                                    ));
1411                                                }
1412                                            }
1413                                        }
1414                                    }
1415                                }
1416                            }
1417                        }
1418                    }
1419                }
1420            }
1421
1422            // Check if clicked outside menu
1423            let in_bar = bar_area.intersects(Rect::new(col, row, 1, 1));
1424            let in_dropdown = dropdown_area
1425                .map(|d| d.intersects(Rect::new(col, row, 1, 1)))
1426                .unwrap_or(false);
1427
1428            if state.is_open && !in_bar && !in_dropdown {
1429                state.close_menu();
1430                return Some(MenuBarAction::MenuClose);
1431            }
1432
1433            None
1434        }
1435        MouseEventKind::Moved => {
1436            // Update highlight on hover
1437            for region in click_regions {
1438                if region.contains(col, row) {
1439                    match &region.data {
1440                        MenuBarClickTarget::MenuLabel(idx) => {
1441                            // If a menu is open and we hover over a different menu label, switch to it
1442                            if state.is_open && state.active_menu != *idx {
1443                                state.open_menu(*idx);
1444                                if let Some(menu) = menus.get(*idx) {
1445                                    state.highlight_first(&menu.items);
1446                                }
1447                                return Some(MenuBarAction::MenuOpen(*idx));
1448                            }
1449                        }
1450                        MenuBarClickTarget::DropdownItem(idx) => {
1451                            if state.highlighted_item != Some(*idx) {
1452                                state.highlighted_item = Some(*idx);
1453                                // Close submenu when moving to different item
1454                                if state.active_submenu.is_some()
1455                                    && state.active_submenu != Some(*idx)
1456                                {
1457                                    state.close_submenu();
1458                                }
1459                                return Some(MenuBarAction::HighlightChange(
1460                                    state.active_menu,
1461                                    Some(*idx),
1462                                ));
1463                            }
1464                        }
1465                        MenuBarClickTarget::SubmenuItem(idx) => {
1466                            if state.submenu_highlighted != Some(*idx) {
1467                                state.submenu_highlighted = Some(*idx);
1468                                return Some(MenuBarAction::HighlightChange(
1469                                    state.active_menu,
1470                                    Some(*idx),
1471                                ));
1472                            }
1473                        }
1474                    }
1475                    break;
1476                }
1477            }
1478            None
1479        }
1480        _ => None,
1481    }
1482}
1483
1484/// Calculate the height needed for a menu bar (always 1).
1485pub fn calculate_menu_bar_height() -> u16 {
1486    1
1487}
1488
1489/// Calculate the height needed for a dropdown menu.
1490pub fn calculate_dropdown_height(item_count: usize, max_visible: u16) -> u16 {
1491    let visible = (item_count as u16).min(max_visible);
1492    visible + 2 // +2 for borders
1493}
1494
1495#[cfg(test)]
1496mod tests {
1497    use super::*;
1498
1499    #[test]
1500    fn test_menu_bar_item_action() {
1501        let item = MenuBarItem::action("save", "Save").shortcut("Ctrl+S");
1502
1503        assert!(item.is_selectable());
1504        assert!(!item.has_submenu());
1505        assert_eq!(item.id(), Some("save"));
1506        assert_eq!(item.label(), Some("Save"));
1507        assert_eq!(item.get_shortcut(), Some("Ctrl+S"));
1508    }
1509
1510    #[test]
1511    fn test_menu_bar_item_separator() {
1512        let item = MenuBarItem::separator();
1513
1514        assert!(!item.is_selectable());
1515        assert!(!item.has_submenu());
1516        assert_eq!(item.label(), None);
1517    }
1518
1519    #[test]
1520    fn test_menu_bar_item_submenu() {
1521        let items = vec![MenuBarItem::action("sub1", "Sub Item 1")];
1522        let item = MenuBarItem::submenu("More", items);
1523
1524        assert!(item.is_selectable());
1525        assert!(item.has_submenu());
1526        assert_eq!(item.label(), Some("More"));
1527        assert!(item.submenu_items().is_some());
1528    }
1529
1530    #[test]
1531    fn test_menu_bar_item_disabled() {
1532        let item = MenuBarItem::action("delete", "Delete").enabled(false);
1533
1534        assert!(!item.is_selectable());
1535        assert!(!item.is_enabled());
1536    }
1537
1538    #[test]
1539    fn test_menu_creation() {
1540        let menu = Menu::new("File")
1541            .items(vec![
1542                MenuBarItem::action("new", "New"),
1543                MenuBarItem::separator(),
1544                MenuBarItem::action("quit", "Quit"),
1545            ])
1546            .enabled(true);
1547
1548        assert_eq!(menu.label, "File");
1549        assert_eq!(menu.items.len(), 3);
1550        assert!(menu.enabled);
1551    }
1552
1553    #[test]
1554    fn test_menu_bar_state_new() {
1555        let state = MenuBarState::new();
1556
1557        assert!(!state.is_open);
1558        assert_eq!(state.active_menu, 0);
1559        assert_eq!(state.highlighted_item, None);
1560        assert!(!state.focused);
1561    }
1562
1563    #[test]
1564    fn test_menu_bar_state_open_close() {
1565        let mut state = MenuBarState::new();
1566
1567        state.open_menu(1);
1568        assert!(state.is_open);
1569        assert_eq!(state.active_menu, 1);
1570        assert_eq!(state.highlighted_item, None);
1571
1572        state.close_menu();
1573        assert!(!state.is_open);
1574    }
1575
1576    #[test]
1577    fn test_menu_bar_state_toggle() {
1578        let mut state = MenuBarState::new();
1579
1580        state.toggle_menu(0);
1581        assert!(state.is_open);
1582        assert_eq!(state.active_menu, 0);
1583
1584        state.toggle_menu(0);
1585        assert!(!state.is_open);
1586
1587        state.toggle_menu(0);
1588        assert!(state.is_open);
1589
1590        // Toggle different menu while open
1591        state.toggle_menu(1);
1592        assert!(state.is_open);
1593        assert_eq!(state.active_menu, 1);
1594    }
1595
1596    #[test]
1597    fn test_menu_bar_state_navigation() {
1598        let mut state = MenuBarState::new();
1599        state.active_menu = 0;
1600
1601        state.next_menu(3);
1602        assert_eq!(state.active_menu, 1);
1603
1604        state.next_menu(3);
1605        assert_eq!(state.active_menu, 2);
1606
1607        state.next_menu(3);
1608        assert_eq!(state.active_menu, 0); // Wrap around
1609
1610        state.prev_menu(3);
1611        assert_eq!(state.active_menu, 2); // Wrap around
1612
1613        state.prev_menu(3);
1614        assert_eq!(state.active_menu, 1);
1615    }
1616
1617    #[test]
1618    fn test_menu_bar_state_item_navigation() {
1619        let mut state = MenuBarState::new();
1620        state.open_menu(0);
1621
1622        let items = vec![
1623            MenuBarItem::action("a", "A"),
1624            MenuBarItem::separator(),
1625            MenuBarItem::action("b", "B"),
1626            MenuBarItem::action("c", "C"),
1627        ];
1628
1629        // Move down (should skip separator)
1630        state.next_item(&items);
1631        // With no initial highlighted item, wraps from 0
1632        assert!(state.highlighted_item.is_some());
1633
1634        state.highlight_first(&items);
1635        assert_eq!(state.highlighted_item, Some(0));
1636
1637        state.next_item(&items);
1638        assert_eq!(state.highlighted_item, Some(2)); // Skipped separator
1639
1640        state.next_item(&items);
1641        assert_eq!(state.highlighted_item, Some(3));
1642
1643        state.prev_item(&items);
1644        assert_eq!(state.highlighted_item, Some(2));
1645
1646        state.prev_item(&items);
1647        assert_eq!(state.highlighted_item, Some(0));
1648    }
1649
1650    #[test]
1651    fn test_menu_bar_state_submenu() {
1652        let mut state = MenuBarState::new();
1653        state.open_menu(0);
1654        state.highlighted_item = Some(2);
1655
1656        assert!(!state.has_open_submenu());
1657
1658        state.open_submenu();
1659        assert!(state.has_open_submenu());
1660        assert_eq!(state.active_submenu, Some(2));
1661
1662        state.close_submenu();
1663        assert!(!state.has_open_submenu());
1664    }
1665
1666    #[test]
1667    fn test_menu_bar_style_default() {
1668        let style = MenuBarStyle::default();
1669        assert_eq!(style.dropdown_min_width, 15);
1670        assert_eq!(style.dropdown_max_height, 15);
1671        assert_eq!(style.submenu_indicator, "▶");
1672    }
1673
1674    #[test]
1675    fn test_menu_bar_style_builders() {
1676        let style = MenuBarStyle::default()
1677            .dropdown_min_width(20)
1678            .dropdown_max_height(10)
1679            .submenu_indicator("→");
1680
1681        assert_eq!(style.dropdown_min_width, 20);
1682        assert_eq!(style.dropdown_max_height, 10);
1683        assert_eq!(style.submenu_indicator, "→");
1684    }
1685
1686    #[test]
1687    fn test_menu_bar_style_presets() {
1688        let light = MenuBarStyle::light();
1689        assert_eq!(light.bar_bg, Color::Rgb(240, 240, 240));
1690
1691        let minimal = MenuBarStyle::minimal();
1692        assert_eq!(minimal.bar_bg, Color::Reset);
1693    }
1694
1695    #[test]
1696    fn test_handle_key_left_right() {
1697        let mut state = MenuBarState::new();
1698        state.focused = true;
1699
1700        let menus = vec![
1701            Menu::new("File").items(vec![]),
1702            Menu::new("Edit").items(vec![]),
1703            Menu::new("View").items(vec![]),
1704        ];
1705
1706        let key = KeyEvent::from(KeyCode::Right);
1707        let action = handle_menu_bar_key(&key, &mut state, &menus);
1708        assert_eq!(action, Some(MenuBarAction::HighlightChange(1, None)));
1709        assert_eq!(state.active_menu, 1);
1710
1711        let key = KeyEvent::from(KeyCode::Left);
1712        let action = handle_menu_bar_key(&key, &mut state, &menus);
1713        assert_eq!(action, Some(MenuBarAction::HighlightChange(0, None)));
1714        assert_eq!(state.active_menu, 0);
1715    }
1716
1717    #[test]
1718    fn test_handle_key_down_opens_menu() {
1719        let mut state = MenuBarState::new();
1720        state.focused = true;
1721
1722        let menus = vec![Menu::new("File").items(vec![MenuBarItem::action("new", "New")])];
1723
1724        let key = KeyEvent::from(KeyCode::Down);
1725        let action = handle_menu_bar_key(&key, &mut state, &menus);
1726
1727        assert_eq!(action, Some(MenuBarAction::MenuOpen(0)));
1728        assert!(state.is_open);
1729    }
1730
1731    #[test]
1732    fn test_handle_key_escape_closes() {
1733        let mut state = MenuBarState::new();
1734        state.open_menu(0);
1735
1736        let menus = vec![Menu::new("File").items(vec![])];
1737
1738        let key = KeyEvent::from(KeyCode::Esc);
1739        let action = handle_menu_bar_key(&key, &mut state, &menus);
1740
1741        assert_eq!(action, Some(MenuBarAction::MenuClose));
1742        assert!(!state.is_open);
1743    }
1744
1745    #[test]
1746    fn test_handle_key_enter_selects_item() {
1747        let mut state = MenuBarState::new();
1748        state.open_menu(0);
1749        state.highlighted_item = Some(0);
1750
1751        let menus = vec![Menu::new("File").items(vec![MenuBarItem::action("new", "New")])];
1752
1753        let key = KeyEvent::from(KeyCode::Enter);
1754        let action = handle_menu_bar_key(&key, &mut state, &menus);
1755
1756        assert_eq!(action, Some(MenuBarAction::ItemSelect("new".to_string())));
1757        assert!(!state.is_open);
1758    }
1759
1760    #[test]
1761    fn test_handle_key_enter_opens_submenu() {
1762        let mut state = MenuBarState::new();
1763        state.open_menu(0);
1764        state.highlighted_item = Some(0);
1765
1766        let menus = vec![Menu::new("File").items(vec![MenuBarItem::submenu(
1767            "Recent",
1768            vec![MenuBarItem::action("file1", "File 1")],
1769        )])];
1770
1771        let key = KeyEvent::from(KeyCode::Enter);
1772        let action = handle_menu_bar_key(&key, &mut state, &menus);
1773
1774        assert_eq!(action, Some(MenuBarAction::SubmenuOpen(0, 0)));
1775        assert!(state.has_open_submenu());
1776    }
1777
1778    #[test]
1779    fn test_handle_key_empty_menus() {
1780        let mut state = MenuBarState::new();
1781        let menus: Vec<Menu> = vec![];
1782
1783        let key = KeyEvent::from(KeyCode::Down);
1784        let action = handle_menu_bar_key(&key, &mut state, &menus);
1785
1786        assert!(action.is_none());
1787    }
1788
1789    #[test]
1790    fn test_menu_bar_action_equality() {
1791        assert_eq!(MenuBarAction::MenuOpen(0), MenuBarAction::MenuOpen(0));
1792        assert_ne!(MenuBarAction::MenuOpen(0), MenuBarAction::MenuOpen(1));
1793        assert_eq!(MenuBarAction::MenuClose, MenuBarAction::MenuClose);
1794        assert_eq!(
1795            MenuBarAction::ItemSelect("test".to_string()),
1796            MenuBarAction::ItemSelect("test".to_string())
1797        );
1798        assert_eq!(
1799            MenuBarAction::HighlightChange(0, Some(1)),
1800            MenuBarAction::HighlightChange(0, Some(1))
1801        );
1802    }
1803
1804    #[test]
1805    fn test_calculate_heights() {
1806        assert_eq!(calculate_menu_bar_height(), 1);
1807        assert_eq!(calculate_dropdown_height(5, 15), 7); // 5 + 2
1808        assert_eq!(calculate_dropdown_height(20, 15), 17); // 15 + 2 (clamped)
1809    }
1810
1811    #[test]
1812    fn test_menu_bar_widget_new() {
1813        let menus = vec![Menu::new("File").items(vec![])];
1814        let state = MenuBarState::new();
1815        let _menu_bar = MenuBar::new(&menus, &state);
1816    }
1817
1818    #[test]
1819    fn test_menu_bar_widget_style() {
1820        let menus = vec![Menu::new("File").items(vec![])];
1821        let state = MenuBarState::new();
1822        let style = MenuBarStyle::light();
1823        let _menu_bar = MenuBar::new(&menus, &state).style(style);
1824    }
1825
1826    #[test]
1827    fn test_click_target_equality() {
1828        assert_eq!(
1829            MenuBarClickTarget::MenuLabel(0),
1830            MenuBarClickTarget::MenuLabel(0)
1831        );
1832        assert_ne!(
1833            MenuBarClickTarget::MenuLabel(0),
1834            MenuBarClickTarget::MenuLabel(1)
1835        );
1836        assert_eq!(
1837            MenuBarClickTarget::DropdownItem(0),
1838            MenuBarClickTarget::DropdownItem(0)
1839        );
1840        assert_eq!(
1841            MenuBarClickTarget::SubmenuItem(0),
1842            MenuBarClickTarget::SubmenuItem(0)
1843        );
1844    }
1845
1846    #[test]
1847    fn test_menu_bar_state_ensure_visible() {
1848        let mut state = MenuBarState::new();
1849        state.highlighted_item = Some(15);
1850        state.scroll_offset = 0;
1851
1852        state.ensure_visible(10);
1853        assert!(state.scroll_offset >= 6);
1854
1855        state.highlighted_item = Some(3);
1856        state.ensure_visible(10);
1857        assert!(state.scroll_offset <= 3);
1858    }
1859
1860    #[test]
1861    fn test_menu_bar_state_highlight_first_last() {
1862        let mut state = MenuBarState::new();
1863        state.open_menu(0);
1864
1865        let items = vec![
1866            MenuBarItem::separator(),
1867            MenuBarItem::action("a", "A"),
1868            MenuBarItem::action("b", "B"),
1869            MenuBarItem::separator(),
1870            MenuBarItem::action("c", "C"),
1871        ];
1872
1873        state.highlight_first(&items);
1874        assert_eq!(state.highlighted_item, Some(1));
1875
1876        state.highlight_last(&items);
1877        assert_eq!(state.highlighted_item, Some(4));
1878    }
1879
1880    #[test]
1881    fn test_submenu_navigation() {
1882        let mut state = MenuBarState::new();
1883        state.open_menu(0);
1884        state.highlighted_item = Some(0);
1885        state.open_submenu();
1886
1887        let items = vec![
1888            MenuBarItem::action("a", "A"),
1889            MenuBarItem::separator(),
1890            MenuBarItem::action("b", "B"),
1891        ];
1892
1893        state.next_submenu_item(&items);
1894        // Should skip separator
1895        assert!(state.submenu_highlighted.is_some());
1896
1897        state.prev_submenu_item(&items);
1898        assert!(state.submenu_highlighted.is_some());
1899    }
1900}