1use 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#[derive(Debug, Clone, PartialEq, Eq)]
54pub enum MenuBarAction {
55 MenuOpen(usize),
57 MenuClose,
59 ItemSelect(String),
61 HighlightChange(usize, Option<usize>),
63 SubmenuOpen(usize, usize),
65 SubmenuClose,
67}
68
69#[derive(Debug, Clone)]
71pub enum MenuBarItem {
72 Action {
74 id: String,
76 label: String,
78 shortcut: Option<String>,
80 enabled: bool,
82 },
83 Separator,
85 Submenu {
87 label: String,
89 items: Vec<MenuBarItem>,
91 enabled: bool,
93 },
94}
95
96impl MenuBarItem {
97 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 pub fn separator() -> Self {
109 Self::Separator
110 }
111
112 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 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 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 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 pub fn has_submenu(&self) -> bool {
150 matches!(self, Self::Submenu { .. })
151 }
152
153 pub fn id(&self) -> Option<&str> {
155 if let Self::Action { id, .. } = self {
156 Some(id)
157 } else {
158 None
159 }
160 }
161
162 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 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 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 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#[derive(Debug, Clone)]
201pub struct Menu {
202 pub label: String,
204 pub items: Vec<MenuBarItem>,
206 pub enabled: bool,
208}
209
210impl Menu {
211 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 pub fn items(mut self, items: Vec<MenuBarItem>) -> Self {
222 self.items = items;
223 self
224 }
225
226 pub fn enabled(mut self, enabled: bool) -> Self {
228 self.enabled = enabled;
229 self
230 }
231}
232
233#[derive(Debug, Clone)]
235pub struct MenuBarState {
236 pub is_open: bool,
238 pub active_menu: usize,
240 pub highlighted_item: Option<usize>,
242 pub scroll_offset: u16,
244 pub focused: bool,
246 pub active_submenu: Option<usize>,
248 pub submenu_highlighted: Option<usize>,
250 pub submenu_scroll_offset: u16,
252}
253
254impl Default for MenuBarState {
255 fn default() -> Self {
256 Self::new()
257 }
258}
259
260impl MenuBarState {
261 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 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 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 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 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 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 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 new_index = 0;
345 }
346 if new_index == current {
347 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 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 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 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 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 pub fn select_item(&mut self, index: usize) {
406 self.highlighted_item = Some(index);
407 }
408
409 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 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 pub fn has_open_submenu(&self) -> bool {
427 self.active_submenu.is_some()
428 }
429
430 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 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 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#[derive(Debug, Clone)]
496pub struct MenuBarStyle {
497 pub bar_bg: Color,
499 pub bar_fg: Color,
501 pub bar_highlight_bg: Color,
503 pub bar_highlight_fg: Color,
505 pub dropdown_bg: Color,
507 pub dropdown_border: Color,
509 pub item_fg: Color,
511 pub item_highlight_bg: Color,
513 pub item_highlight_fg: Color,
515 pub shortcut_fg: Color,
517 pub disabled_fg: Color,
519 pub separator_fg: Color,
521 pub dropdown_min_width: u16,
523 pub dropdown_max_height: u16,
525 pub menu_padding: u16,
527 pub dropdown_padding: u16,
529 pub submenu_indicator: &'static str,
531 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 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 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 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 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 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 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 pub fn dropdown_min_width(mut self, width: u16) -> Self {
656 self.dropdown_min_width = width;
657 self
658 }
659
660 pub fn dropdown_max_height(mut self, height: u16) -> Self {
662 self.dropdown_max_height = height;
663 self
664 }
665
666 pub fn submenu_indicator(mut self, indicator: &'static str) -> Self {
668 self.submenu_indicator = indicator;
669 self
670 }
671}
672
673#[derive(Debug, Clone, PartialEq, Eq)]
675pub enum MenuBarClickTarget {
676 MenuLabel(usize),
678 DropdownItem(usize),
680 SubmenuItem(usize),
682}
683
684pub struct MenuBar<'a> {
688 menus: &'a [Menu],
689 state: &'a MenuBarState,
690 style: MenuBarStyle,
691}
692
693impl<'a> MenuBar<'a> {
694 pub fn new(menus: &'a [Menu], state: &'a MenuBarState) -> Self {
696 Self {
697 menus,
698 state,
699 style: MenuBarStyle::default(),
700 }
701 }
702
703 pub fn style(mut self, style: MenuBarStyle) -> Self {
705 self.style = style;
706 self
707 }
708
709 pub fn theme(self, theme: &crate::theme::Theme) -> Self {
711 self.style(MenuBarStyle::from(theme))
712 }
713
714 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 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 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 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 }
756
757 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 let y = bar_bottom;
770
771 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 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 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 let bar_area = Rect::new(area.x, area.y, area.width, 1);
801
802 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 let mut x = bar_area.x;
810 let mut menu_positions: Vec<(u16, u16)> = Vec::new(); 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 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 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 frame.render_widget(Clear, dropdown_area);
856
857 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 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 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 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 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 #[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 spans.push(Span::styled(
1029 " ".repeat(self.style.dropdown_padding as usize),
1030 style,
1031 ));
1032
1033 spans.push(Span::styled(label.clone(), style));
1035
1036 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 if let Some(sc) = shortcut {
1050 spans.push(Span::styled(sc.clone(), shortcut_style));
1051 }
1052
1053 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 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 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 spans.push(Span::styled(
1090 " ".repeat(self.style.dropdown_padding as usize),
1091 style,
1092 ));
1093
1094 spans.push(Span::styled(label.clone(), style));
1096
1097 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 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 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#[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 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 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#[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 for region in click_regions {
1362 if region.contains(col, row) {
1363 match ®ion.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 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 for region in click_regions {
1438 if region.contains(col, row) {
1439 match ®ion.data {
1440 MenuBarClickTarget::MenuLabel(idx) => {
1441 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 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
1484pub fn calculate_menu_bar_height() -> u16 {
1486 1
1487}
1488
1489pub fn calculate_dropdown_height(item_count: usize, max_visible: u16) -> u16 {
1491 let visible = (item_count as u16).min(max_visible);
1492 visible + 2 }
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 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); state.prev_menu(3);
1611 assert_eq!(state.active_menu, 2); 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 state.next_item(&items);
1631 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)); 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); assert_eq!(calculate_dropdown_height(20, 15), 17); }
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 assert!(state.submenu_highlighted.is_some());
1896
1897 state.prev_submenu_item(&items);
1898 assert!(state.submenu_highlighted.is_some());
1899 }
1900}