tui-vision 0.1.1

A turbo vision inspired library for ratatui.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
use ratatui_core::{
    buffer::Buffer,
    layout::Rect,
    style::{Modifier, Style},
    widgets::{StatefulWidget, Widget},
};
use ratatui_widgets::{block::Block, clear::Clear};

use super::{Menu, MenuBar, MenuItem};

/// Rendering implementation for the MenuBar widget.
///
/// This implementation renders the menu bar as a horizontal strip across the top
/// of the terminal, with menus displayed as clickable titles. When a menu is open,
/// it displays a dropdown with the menu items.
impl Widget for MenuBar {
    fn render(self, area: Rect, buf: &mut Buffer) {
        (&self).render(area, buf);
    }
}

impl Widget for &MenuBar {
    fn render(self, area: Rect, buf: &mut Buffer) {
        // First render the dropdown if a menu is open (before the menu bar)
        if let Some(menu_index) = self.opened_menu {
            if let Some(menu) = self.menus.get(menu_index) {
                let dropdown_area = self.calculate_dropdown_area(area, menu_index);
                self.render_dropdown(menu, dropdown_area, buf);
            }
        }

        // Then render the menu bar on top
        self.render_menu_bar(area, buf);
    }
}

impl MenuBar {
    /// Clears an area in the buffer with the specified style.
    fn clear_area(&self, area: Rect, buf: &mut Buffer, style: Style) {
        for y in area.y..area.y + area.height {
            for x in area.x..area.x + area.width {
                buf.set_string(x, y, " ", style);
            }
        }
    }

    /// Renders the horizontal menu bar.
    ///
    /// Only affects the first line of the given area, leaving the rest of the buffer unchanged.
    fn render_menu_bar(&self, area: Rect, buf: &mut Buffer) {
        let menu_bar_style = self.theme.menu_bar;

        // Only clear and render the first line of the area
        let menu_bar_area = Rect {
            x: area.x,
            y: area.y,
            width: area.width,
            height: 1,
        };

        // Clear only the menu bar line
        self.clear_area(menu_bar_area, buf, menu_bar_style);

        let mut x_offset = area.x + 1; // Start with padding from left edge

        for (index, menu) in self.menus.iter().enumerate() {
            let is_open = self.opened_menu == Some(index);
            let menu_style = if is_open {
                self.theme.menu_bar_focused
            } else {
                self.theme.menu_bar
            };

            // Render menu title with padding
            let menu_text = format!(" {menu_title} ", menu_title = &menu.title);
            if x_offset + menu_text.len() as u16 <= area.x + area.width {
                buf.set_string(x_offset, area.y, &menu_text, menu_style);
                x_offset += menu_text.len() as u16;
            }

            // Add space between menus
            if index < self.menus.len() - 1 {
                buf.set_string(x_offset, area.y, " ", menu_bar_style);
                x_offset += 1;
            }
        }
    }

    /// Calculates the area for the dropdown menu.
    fn calculate_dropdown_area(&self, area: Rect, menu_index: usize) -> Rect {
        let mut x_offset = area.x + 1;

        // Calculate x position based on menu titles
        for (index, menu) in self.menus.iter().enumerate() {
            if index == menu_index {
                break;
            }
            x_offset += format!(" {menu_title} ", menu_title = &menu.title).len() as u16 + 1;
        }

        let menu = &self.menus[menu_index];

        // Calculate dropdown dimensions with proper width accounting for shortcuts and arrows
        let max_item_width = menu
            .items
            .iter()
            .map(|item| match item {
                MenuItem::Action(action) => {
                    let label_len = action.label.len();
                    let shortcut_len = action.shortcut.as_ref().map_or(0, |s| s.len() + 1);
                    label_len + shortcut_len
                }
                MenuItem::SubMenu(submenu) => {
                    submenu.label.len() + 2 // +2 for arrow and space
                }
                MenuItem::Separator(_) => 0, // Separators use full width
            })
            .max()
            .unwrap_or(10) as u16;

        let dropdown_width = (max_item_width + 4).min(40); // Padding + max width
        let dropdown_height = menu.items.len() as u16 + 2; // Items + borders

        Rect {
            x: x_offset,
            y: area.y + 1,
            width: dropdown_width,
            height: dropdown_height.min(area.height - 1),
        }
    }

    /// Calculates the area for a submenu dropdown.
    fn calculate_submenu_area(
        &self,
        parent_area: Rect,
        _parent_x: u16,
        parent_y: u16,
        submenu: &super::SubMenuItem,
    ) -> Rect {
        // Calculate submenu dimensions
        let max_item_width = submenu
            .items
            .iter()
            .map(|item| match item {
                MenuItem::Action(action) => {
                    let label_len = action.label.len();
                    let shortcut_len = action.shortcut.as_ref().map_or(0, |s| s.len() + 1);
                    label_len + shortcut_len
                }
                MenuItem::SubMenu(sub) => {
                    sub.label.len() + 2 // +2 for arrow and space
                }
                MenuItem::Separator(_) => 0,
            })
            .max()
            .unwrap_or(10) as u16;

        let submenu_width = (max_item_width + 4).min(30); // Padding + max width (smaller than main menu)
        let submenu_height = submenu.items.len() as u16 + 2; // Items + borders

        // Position submenu to the right of the parent menu item
        // Don't constrain by parent area height - let submenu extend as needed
        Rect {
            x: parent_area.x + parent_area.width,
            y: parent_y,
            width: submenu_width,
            height: submenu_height,
        }
    }

    /// Renders the dropdown menu.
    ///
    /// The dropdown renders on top of existing buffer content, clearing only its own area.
    /// This allows menus to overlay other content in the terminal.
    fn render_dropdown(&self, menu: &Menu, area: Rect, buf: &mut Buffer) {
        let dropdown_style = self.theme.dropdown;
        let border_style = self.theme.dropdown_border;

        // Use Clear widget to clear the dropdown area
        Clear.render(area, buf);

        // Use Block widget for the border
        let block = Block::bordered()
            .border_style(border_style)
            .style(dropdown_style);

        // Calculate content area before rendering the block
        let content_area = block.inner(area);
        block.render(area, buf);

        // Render menu items
        for (index, item) in menu.items.iter().enumerate() {
            if index as u16 >= content_area.height {
                break; // Don't render beyond dropdown height
            }

            let y = content_area.y + index as u16;
            let is_focused = menu.focused_item == Some(index);

            // Special handling for separators - they span the full dropdown width
            if matches!(item, MenuItem::Separator(_)) {
                let separator_style = self.theme.separator;

                // Render separator line across the full dropdown width
                buf.set_string(area.x, y, "", separator_style);
                for x in area.x + 1..area.x + area.width - 1 {
                    buf.set_string(x, y, "", separator_style);
                }
                buf.set_string(area.x + area.width - 1, y, "", separator_style);
            } else {
                self.render_menu_item(item, content_area.x, y, content_area.width, is_focused, buf);

                // If this is an open submenu, render its dropdown
                if let MenuItem::SubMenu(submenu) = item {
                    if submenu.is_open {
                        let submenu_area =
                            self.calculate_submenu_area(area, content_area.x, y, submenu);
                        self.render_submenu_dropdown(submenu, submenu_area, buf);
                    }
                }
            }
        }
    }

    /// Renders a submenu dropdown.
    fn render_submenu_dropdown(&self, submenu: &super::SubMenuItem, area: Rect, buf: &mut Buffer) {
        let dropdown_style = self.theme.dropdown;
        let border_style = self.theme.dropdown_border;

        // Use Clear widget to clear the submenu area
        Clear.render(area, buf);

        // Use Block widget for the border
        let block = Block::bordered()
            .border_style(border_style)
            .style(dropdown_style);

        // Calculate content area before rendering the block
        let content_area = block.inner(area);
        block.render(area, buf);

        // Render submenu items
        for (index, item) in submenu.items.iter().enumerate() {
            if index as u16 >= content_area.height {
                break; // Don't render beyond submenu height
            }

            let y = content_area.y + index as u16;
            let is_focused = submenu.focused_item == Some(index);

            // Special handling for separators - they span the full submenu width
            if matches!(item, MenuItem::Separator(_)) {
                let separator_style = self.theme.separator;

                // Render separator line across the full submenu width
                buf.set_string(area.x, y, "", separator_style);
                for x in area.x + 1..area.x + area.width - 1 {
                    buf.set_string(x, y, "", separator_style);
                }
                buf.set_string(area.x + area.width - 1, y, "", separator_style);
            } else {
                self.render_menu_item(item, content_area.x, y, content_area.width, is_focused, buf);
            }
        }
    }

    /// Renders a single menu item.
    fn render_menu_item(
        &self,
        item: &MenuItem,
        x: u16,
        y: u16,
        width: u16,
        is_focused: bool,
        buf: &mut Buffer,
    ) {
        match item {
            MenuItem::Action(action) => {
                let base_style = if is_focused {
                    self.theme.item_focused
                } else if action.enabled {
                    self.theme.item
                } else {
                    self.theme.item_disabled
                };

                self.render_action_item(action, x, y, width, base_style, buf);
            }
            MenuItem::Separator(_) => {
                // Separators are handled separately in the main render loop
                // to allow them to span the full dropdown width
            }
            MenuItem::SubMenu(submenu) => {
                let base_style = if is_focused {
                    self.theme.item_focused
                } else if submenu.enabled {
                    self.theme.item
                } else {
                    self.theme.item_disabled
                };

                self.render_submenu_item(submenu, x, y, width, base_style, buf);
            }
        }
    }

    /// Renders an action menu item with proper hotkey underlining.
    fn render_action_item(
        &self,
        action: &super::ActionItem,
        x: u16,
        y: u16,
        width: u16,
        style: Style,
        buf: &mut Buffer,
    ) {
        let label = &action.label;
        let shortcut = &action.shortcut;

        // First, fill the entire line with the background color
        for i in 0..width {
            buf.set_string(x + i, y, " ", style);
        }

        // Render label with hotkey underlining
        let mut current_x = x;
        if let Some(hotkey) = action.hotkey {
            if let Some(pos) = label
                .to_lowercase()
                .find(&hotkey.to_lowercase().to_string())
            {
                // Render text before hotkey
                let before = &label[..pos];
                buf.set_string(current_x, y, before, style);
                current_x += before.len() as u16;

                // Render hotkey with underline
                let hotkey_char = &label[pos..pos + 1];
                let hotkey_style = style.add_modifier(Modifier::UNDERLINED);
                buf.set_string(current_x, y, hotkey_char, hotkey_style);
                current_x += 1;

                // Render text after hotkey
                let after = &label[pos + 1..];
                buf.set_string(current_x, y, after, style);
                current_x += after.len() as u16;
            } else {
                // Hotkey not found in label, render normally
                buf.set_string(current_x, y, label, style);
                current_x += label.len() as u16;
            }
        } else {
            // No hotkey, render normally
            buf.set_string(current_x, y, label, style);
            current_x += label.len() as u16;
        }

        // Render shortcut if available, positioned from the right
        if let Some(shortcut) = shortcut {
            let shortcut_x = x + width.saturating_sub(shortcut.len() as u16);
            if shortcut_x > current_x {
                buf.set_string(shortcut_x, y, shortcut, style);
            }
        }
    }

    /// Renders a submenu item with proper hotkey underlining.
    fn render_submenu_item(
        &self,
        submenu: &super::SubMenuItem,
        x: u16,
        y: u16,
        width: u16,
        style: Style,
        buf: &mut Buffer,
    ) {
        let label = &submenu.label;

        // First, fill the entire line with the background color
        for i in 0..width {
            buf.set_string(x + i, y, " ", style);
        }

        // Reserve space for arrow (positioned 1 cell from the right edge)
        let arrow = "";
        let arrow_x = x + width.saturating_sub(2); // 2 positions from right edge

        // Render label with hotkey underlining
        let mut current_x = x;
        if let Some(hotkey) = submenu.hotkey {
            if let Some(pos) = label
                .to_lowercase()
                .find(&hotkey.to_lowercase().to_string())
            {
                // Render text before hotkey
                let before = &label[..pos];
                buf.set_string(current_x, y, before, style);
                current_x += before.len() as u16;

                // Render hotkey with underline
                let hotkey_char = &label[pos..pos + 1];
                let hotkey_style = style.add_modifier(Modifier::UNDERLINED);
                buf.set_string(current_x, y, hotkey_char, hotkey_style);
                current_x += 1;

                // Render text after hotkey
                let after = &label[pos + 1..];
                buf.set_string(current_x, y, after, style);
                current_x += after.len() as u16;
            } else {
                // Hotkey not found in label, render normally
                buf.set_string(current_x, y, label, style);
                current_x += label.len() as u16;
            }
        } else {
            // No hotkey, render normally
            buf.set_string(current_x, y, label, style);
            current_x += label.len() as u16;
        }

        // Render arrow indicator (positioned 1 cell from the right edge)
        if arrow_x > current_x {
            buf.set_string(arrow_x, y, arrow, style);
        }
    }
}

/// Stateful widget implementation for cases where you need to pass state.
impl StatefulWidget for MenuBar {
    type State = ();

    fn render(self, area: Rect, buf: &mut Buffer, _state: &mut Self::State) {
        Widget::render(self, area, buf);
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{item, menu, menu_bar};
    use ratatui::style::{Color, Style};
    use ratatui_core::buffer::Buffer;
    use ratatui_core::layout::Rect;

    #[test]
    fn empty_menu_bar_rendering() {
        let menu_bar = MenuBar::new();
        let area = Rect::new(0, 0, 20, 1);
        let mut buffer = Buffer::empty(area);

        let menu_bar_style = menu_bar.theme.menu_bar;
        Widget::render(menu_bar, area, &mut buffer);

        // An empty menu bar should render as all spaces with menu bar style
        let mut expected = Buffer::with_lines(["                    "]);
        for x in 0..20 {
            expected[(x, 0)].set_style(menu_bar_style);
        }
        assert_eq!(buffer, expected);
    }

    #[test]
    fn menu_bar_with_menus_rendering() {
        let menu_bar = menu_bar![menu!["File", 'F',], menu!["Edit", 'E',],];
        let area = Rect::new(0, 0, 15, 1);
        let mut buffer = Buffer::empty(area);

        let menu_bar_style = menu_bar.theme.menu_bar;
        Widget::render(menu_bar, area, &mut buffer);

        // Menu bar should render "  File   Edit  " (with proper spacing)
        let mut expected = Buffer::with_lines(["  File   Edit  "]);
        // Apply the menu bar style to the entire line
        for x in 0..15 {
            expected[(x, 0)].set_style(menu_bar_style);
        }
        assert_eq!(buffer, expected);
    }

    #[test]
    fn menu_bar_with_opened_dropdown() {
        let mut menu_bar = menu_bar![
            menu![
                "File",
                'F',
                item![action: "New", command: "file.new"],
                item![action: "Open", command: "file.open"],
                item![separator],
                item![action: "Save", command: "file.save"],
                item![action: "Exit", command: "file.exit"],
            ],
            menu!["Edit", 'E',],
        ];
        menu_bar.open_menu(0);

        // Use larger area to accommodate full dropdown
        let area = Rect::new(0, 0, 20, 8);
        let mut buffer = Buffer::empty(area);

        Widget::render(menu_bar, area, &mut buffer);

        // Create expected buffer with the actual rendered content
        let mut expected = Buffer::with_lines([
            "  File   Edit       ", // Menu bar (with padding)
            " ┌──────┐           ", // Dropdown top border
            " │New   │           ", // First menu item
            " │Open  │           ", // Second menu item
            " ├──────┤           ", // Separator spanning full width
            " │Save  │           ", // Third menu item
            " │Exit  │           ", // Fourth menu item
            " └──────┘           ", // Dropdown bottom border
        ]);

        // Reset styles on both buffers to focus on content only
        use ratatui_core::style::Style;
        buffer.set_style(area, Style::reset());
        expected.set_style(area, Style::reset());

        assert_eq!(buffer, expected);
    }

    #[test]
    fn menu_item_display() {
        let action_item = item![action: "New", command: "file.new"];
        let separator_item = item![separator];

        // Test action item display
        assert_eq!(action_item.label(), Some("New"));
        assert!(action_item.is_selectable());

        // Test separator item display
        assert_eq!(separator_item.label(), None);
        assert!(!separator_item.is_selectable());
    }

    #[test]
    fn dropdown_area_calculation() {
        let menu_bar = menu_bar![menu!["File", 'F',], menu!["Edit", 'E',],];

        let area = Rect::new(0, 0, 80, 25);
        let dropdown_area = menu_bar.calculate_dropdown_area(area, 1);

        // Edit menu should be positioned after "File" menu
        assert!(dropdown_area.x > 1);
        assert_eq!(dropdown_area.y, 1);
    }

    #[test]
    fn menu_width_calculation() {
        let short_menu = Menu::new("Hi");
        let long_menu = Menu::new("File Operations");

        assert!(short_menu.title.len() < long_menu.title.len());

        // Test that menu width affects dropdown positioning
        let menu_bar = menu_bar![Menu::new("Short"), Menu::new("VeryLongMenuTitle"),];

        let area = Rect::new(0, 0, 80, 25);
        let first_dropdown = menu_bar.calculate_dropdown_area(area, 0);
        let second_dropdown = menu_bar.calculate_dropdown_area(area, 1);

        // Second dropdown should be positioned after the first menu
        assert!(second_dropdown.x > first_dropdown.x);
    }

    #[test]
    fn clear_area_functionality() {
        let menu_bar = MenuBar::new();
        let area = Rect::new(0, 0, 5, 3);
        let mut buffer = Buffer::empty(area);
        let style = Style::default().bg(Color::Red).fg(Color::White);

        // Initially buffer should be empty
        let initial_content = buffer[(0, 0)].symbol();
        assert_eq!(initial_content, " ");

        // Clear the area with a specific style
        menu_bar.clear_area(area, &mut buffer, style);

        // Verify the area is cleared with spaces and proper style
        for y in 0..3 {
            for x in 0..5 {
                let cell = &buffer[(x, y)];
                assert_eq!(cell.symbol(), " ");
                assert_eq!(cell.bg, Color::Red);
                assert_eq!(cell.fg, Color::White);
            }
        }
    }

    #[test]
    fn menu_bar_only_affects_first_line() {
        use ratatui_core::style::{Color, Style};

        let menu_bar = menu_bar![menu!["File", 'F',], menu!["Edit", 'E',],];

        // Create a larger area with some existing content
        let area = Rect::new(0, 0, 20, 5);
        let mut buffer = Buffer::empty(area);

        // Fill the buffer with some existing content
        let existing_style = Style::default().bg(Color::Blue).fg(Color::Yellow);
        for y in 0..5 {
            for x in 0..20 {
                buffer.set_string(x, y, "X", existing_style);
            }
        }

        // Render the menu bar
        Widget::render(menu_bar, area, &mut buffer);

        // First line should be styled as menu bar
        for x in 0..20 {
            let cell = &buffer[(x, 0)];
            // Content should be menu bar content or spaces, not the original "X"
            assert_ne!(cell.symbol(), "X");
        }

        // Lines below should retain original content and style
        for y in 1..5 {
            for x in 0..20 {
                let cell = &buffer[(x, y)];
                assert_eq!(cell.symbol(), "X");
                assert_eq!(cell.bg, Color::Blue);
                assert_eq!(cell.fg, Color::Yellow);
            }
        }
    }

    #[test]
    fn dropdown_renders_on_top_of_content() {
        use ratatui_core::style::{Color, Style};

        let mut menu_bar = menu_bar![menu![
            "File",
            'F',
            item![action: "New", command: "file.new"],
            item![action: "Open", command: "file.open"],
        ],];
        menu_bar.open_menu(0);

        // Create area with existing content
        let area = Rect::new(0, 0, 15, 6);
        let mut buffer = Buffer::empty(area);

        // Fill with background content
        let bg_style = Style::default().bg(Color::Red).fg(Color::White);
        for y in 0..6 {
            for x in 0..15 {
                buffer.set_string(x, y, "Z", bg_style);
            }
        }

        // Render the menu bar
        Widget::render(menu_bar, area, &mut buffer);

        // First line should be menu bar (overwriting background)
        for x in 0..15 {
            let cell = &buffer[(x, 0)];
            assert_ne!(cell.symbol(), "Z");
        }

        // Dropdown area should be cleared and contain menu content
        // (approximately x=1-7, y=1-4 based on dropdown size)
        for y in 1..4 {
            for x in 1..8 {
                let cell = &buffer[(x, y)];
                // Should not be the background "Z" in dropdown area
                assert_ne!(cell.symbol(), "Z");
            }
        }

        // Areas outside dropdown should retain background content
        for x in 10..15 {
            for y in 1..6 {
                let cell = &buffer[(x, y)];
                assert_eq!(cell.symbol(), "Z");
                assert_eq!(cell.bg, Color::Red);
            }
        }
    }

    #[test]
    fn separator_styling_test() {
        use ratatui_core::buffer::Buffer;
        use ratatui_core::layout::Rect;
        use ratatui_core::style::Color;
        use ratatui_core::widgets::Widget;

        let mut menu_bar = menu_bar![menu![
            "Edit",
            'E',
            item![action: "Cut", command: "edit.cut"],
            item![separator],
            item![action: "Paste", command: "edit.paste"],
        ],];
        menu_bar.open_menu(0);

        let area = Rect::new(0, 0, 20, 8);
        let mut buffer = Buffer::empty(area);
        Widget::render(menu_bar, area, &mut buffer);

        // Check that the separator line exists and has proper styling
        // The separator should be on line 3 (0=menu bar, 1=border, 2=Cut, 3=separator)
        let separator_y = 3;

        // The separator now spans the full dropdown width, starting at dropdown's x position
        // Find the dropdown area first
        let mut dropdown_x = 0;
        let mut dropdown_width = 0;

        // Look for the dropdown border to determine its position and width
        for x in 0..20 {
            if buffer[(x, 1)].symbol() == "" {
                // Top-left corner of dropdown
                dropdown_x = x;
                break;
            }
        }

        // Find the width by looking for the top-right corner
        for x in dropdown_x + 1..20 {
            if buffer[(x, 1)].symbol() == "" {
                // Top-right corner
                dropdown_width = x - dropdown_x + 1;
                break;
            }
        }

        // Check the left connector of separator
        let left_cell = &buffer[(dropdown_x, separator_y)];
        assert_eq!(left_cell.symbol(), "");
        assert_eq!(left_cell.bg, Color::Blue);
        assert_eq!(left_cell.fg, Color::White);

        // Check the horizontal line
        let middle_cell = &buffer[(dropdown_x + 1, separator_y)];
        assert_eq!(middle_cell.symbol(), "");
        assert_eq!(middle_cell.bg, Color::Blue);
        assert_eq!(middle_cell.fg, Color::White);

        // Check the right connector
        let right_cell = &buffer[(dropdown_x + dropdown_width - 1, separator_y)];
        assert_eq!(right_cell.symbol(), "");
        assert_eq!(right_cell.bg, Color::Blue);
        assert_eq!(right_cell.fg, Color::White);
    }

    #[test]
    fn hotkey_underlining_test() {
        use ratatui_core::buffer::Buffer;
        use ratatui_core::layout::Rect;
        use ratatui_core::style::Modifier;
        use ratatui_core::widgets::Widget;

        let mut menu_bar = menu_bar![menu![
            "File",
            'F',
            item![action: "New File", command: "file.new", hotkey: 'N'],
            item![action: "Open", command: "file.open", hotkey: 'O'],
        ],];
        menu_bar.open_menu(0);

        let area = Rect::new(0, 0, 20, 6);
        let mut buffer = Buffer::empty(area);
        Widget::render(menu_bar, area, &mut buffer);

        // Check that hotkeys are underlined
        // "New File" should have 'N' underlined
        let mut found_underlined_n = false;
        for y in 0..6 {
            for x in 0..20 {
                let cell = &buffer[(x, y)];
                if cell.symbol() == "N" && cell.modifier.contains(Modifier::UNDERLINED) {
                    found_underlined_n = true;
                    break;
                }
            }
        }
        assert!(
            found_underlined_n,
            "Expected to find underlined 'N' in 'New File'"
        );

        // "Open" should have 'O' underlined
        let mut found_underlined_o = false;
        for y in 0..6 {
            for x in 0..20 {
                let cell = &buffer[(x, y)];
                if cell.symbol() == "O" && cell.modifier.contains(Modifier::UNDERLINED) {
                    found_underlined_o = true;
                    break;
                }
            }
        }
        assert!(
            found_underlined_o,
            "Expected to find underlined 'O' in 'Open'"
        );
    }

    #[test]
    fn selection_visibility_test() {
        use ratatui_core::buffer::Buffer;
        use ratatui_core::layout::Rect;
        use ratatui_core::style::Color;
        use ratatui_core::widgets::Widget;

        let mut menu_bar = menu_bar![menu![
            "File",
            'F',
            item![action: "New", command: "file.new"],
            item![action: "Open", command: "file.open"],
        ],];
        menu_bar.open_menu(0);

        // Focus the first item manually by setting focused_item
        if let Some(menu) = menu_bar.menus.get_mut(0) {
            menu.focused_item = Some(0);
        }

        let area = Rect::new(0, 0, 15, 6);
        let mut buffer = Buffer::empty(area);
        Widget::render(menu_bar, area, &mut buffer);

        // Check that the focused item has white background (selection highlighting)
        let mut found_white_bg = false;
        for y in 0..6 {
            for x in 0..15 {
                let cell = &buffer[(x, y)];
                if cell.bg == Color::White && cell.symbol() != " " {
                    found_white_bg = true;
                    break;
                }
            }
        }
        assert!(
            found_white_bg,
            "Expected to find white background for focused item"
        );

        // Check that non-focused areas have blue background
        let mut found_blue_bg = false;
        for y in 0..6 {
            for x in 0..15 {
                let cell = &buffer[(x, y)];
                if cell.bg == Color::Blue {
                    found_blue_bg = true;
                    break;
                }
            }
        }
        assert!(
            found_blue_bg,
            "Expected to find blue background for non-focused areas"
        );
    }

    #[test]
    fn submenu_rendering_test() {
        let mut menu_bar = menu_bar![menu![
            "View",
            'V',
            item![submenu: "Theme", items: [
                    item![action: "Light", command: "light", hotkey: 'L'],
                    item![action: "Dark", command: "dark", hotkey: 'D']
                ], hotkey: 'T']
        ]];

        // Open the View menu and the Theme submenu
        menu_bar.open_menu(0);
        if let Some(menu) = menu_bar.opened_menu_mut() {
            menu.focused_item = Some(0); // Focus the Theme submenu
            if let Some(MenuItem::SubMenu(submenu)) = menu.items.get_mut(0) {
                submenu.is_open = true;
                submenu.focused_item = Some(0); // Focus Light theme
            }
        }

        let mut buffer = Buffer::empty(Rect::new(0, 0, 40, 10));
        Widget::render(&menu_bar, Rect::new(0, 0, 40, 10), &mut buffer);

        // Check that the submenu items are rendered
        let content = buffer.content();
        let rendered = content.iter().map(|cell| cell.symbol()).collect::<String>();

        assert!(
            rendered.contains("Light"),
            "Should contain 'Light' theme option"
        );
        assert!(
            rendered.contains("Dark"),
            "Should contain 'Dark' theme option"
        );
    }
}