1use presentar_core::{
7 widget::{LayoutResult, TextStyle},
8 Brick, BrickAssertion, BrickBudget, BrickVerification, Canvas, Color, Constraints, Event, Key,
9 Point, Rect, Size, TypeId, Widget,
10};
11use serde::{Deserialize, Serialize};
12use std::any::Any;
13use std::time::Duration;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub enum MenuItem {
18 Action {
20 label: String,
22 action: String,
24 disabled: bool,
26 shortcut: Option<String>,
28 },
29 Checkbox {
31 label: String,
33 action: String,
35 checked: bool,
37 disabled: bool,
39 },
40 Separator,
42 Submenu {
44 label: String,
46 items: Vec<Self>,
48 disabled: bool,
50 },
51}
52
53impl MenuItem {
54 #[must_use]
56 pub fn action(label: impl Into<String>, action: impl Into<String>) -> Self {
57 Self::Action {
58 label: label.into(),
59 action: action.into(),
60 disabled: false,
61 shortcut: None,
62 }
63 }
64
65 #[must_use]
67 pub fn checkbox(label: impl Into<String>, action: impl Into<String>, checked: bool) -> Self {
68 Self::Checkbox {
69 label: label.into(),
70 action: action.into(),
71 checked,
72 disabled: false,
73 }
74 }
75
76 #[must_use]
78 pub const fn separator() -> Self {
79 Self::Separator
80 }
81
82 #[must_use]
84 pub fn submenu(label: impl Into<String>, items: Vec<Self>) -> Self {
85 Self::Submenu {
86 label: label.into(),
87 items,
88 disabled: false,
89 }
90 }
91
92 #[must_use]
94 pub fn disabled(mut self, disabled: bool) -> Self {
95 match &mut self {
96 Self::Action { disabled: d, .. }
97 | Self::Checkbox { disabled: d, .. }
98 | Self::Submenu { disabled: d, .. } => *d = disabled,
99 Self::Separator => {}
100 }
101 self
102 }
103
104 #[must_use]
106 pub fn shortcut(mut self, shortcut: impl Into<String>) -> Self {
107 if let Self::Action { shortcut: s, .. } = &mut self {
108 *s = Some(shortcut.into());
109 }
110 self
111 }
112
113 #[must_use]
115 pub fn is_selectable(&self) -> bool {
116 match self {
117 Self::Action { disabled, .. }
118 | Self::Checkbox { disabled, .. }
119 | Self::Submenu { disabled, .. } => !disabled,
120 Self::Separator => false,
121 }
122 }
123
124 #[must_use]
126 pub const fn height(&self) -> f32 {
127 match self {
128 Self::Separator => 9.0, _ => 32.0,
130 }
131 }
132}
133
134#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
136pub enum MenuTrigger {
137 #[default]
139 Click,
140 Hover,
142 ContextMenu,
144}
145
146#[derive(Serialize, Deserialize)]
148pub struct Menu {
149 pub items: Vec<MenuItem>,
151 pub open: bool,
153 pub trigger: MenuTrigger,
155 pub width: f32,
157 pub background_color: Color,
159 pub hover_color: Color,
161 pub text_color: Color,
163 pub disabled_color: Color,
165 test_id_value: Option<String>,
167 #[serde(skip)]
169 bounds: Rect,
170 #[serde(skip)]
172 panel_bounds: Rect,
173 #[serde(skip)]
175 highlighted_index: Option<usize>,
176 #[serde(skip)]
178 open_submenu: Option<usize>,
179 #[serde(skip)]
181 trigger_widget: Vec<Box<dyn Widget>>,
182}
183
184impl Default for Menu {
185 fn default() -> Self {
186 Self {
187 items: Vec::new(),
188 open: false,
189 trigger: MenuTrigger::Click,
190 width: 200.0,
191 background_color: Color::WHITE,
192 hover_color: Color::rgba(0.0, 0.0, 0.0, 0.1),
193 text_color: Color::BLACK,
194 disabled_color: Color::rgb(0.6, 0.6, 0.6),
195 test_id_value: None,
196 bounds: Rect::default(),
197 panel_bounds: Rect::default(),
198 highlighted_index: None,
199 open_submenu: None,
200 trigger_widget: Vec::new(),
201 }
202 }
203}
204
205impl Menu {
206 #[must_use]
208 pub fn new() -> Self {
209 Self::default()
210 }
211
212 #[must_use]
214 pub fn items(mut self, items: Vec<MenuItem>) -> Self {
215 self.items = items;
216 self
217 }
218
219 #[must_use]
221 pub fn item(mut self, item: MenuItem) -> Self {
222 self.items.push(item);
223 self
224 }
225
226 #[must_use]
228 pub const fn trigger(mut self, trigger: MenuTrigger) -> Self {
229 self.trigger = trigger;
230 self
231 }
232
233 #[must_use]
235 pub const fn width(mut self, width: f32) -> Self {
236 self.width = width;
237 self
238 }
239
240 #[must_use]
242 pub const fn background_color(mut self, color: Color) -> Self {
243 self.background_color = color;
244 self
245 }
246
247 #[must_use]
249 pub const fn hover_color(mut self, color: Color) -> Self {
250 self.hover_color = color;
251 self
252 }
253
254 #[must_use]
256 pub const fn text_color(mut self, color: Color) -> Self {
257 self.text_color = color;
258 self
259 }
260
261 pub fn trigger_widget(mut self, widget: impl Widget + 'static) -> Self {
263 self.trigger_widget = vec![Box::new(widget)];
264 self
265 }
266
267 #[must_use]
269 pub fn with_test_id(mut self, id: impl Into<String>) -> Self {
270 self.test_id_value = Some(id.into());
271 self
272 }
273
274 pub fn show(&mut self) {
276 self.open = true;
277 self.highlighted_index = None;
278 }
279
280 pub fn hide(&mut self) {
282 self.open = false;
283 self.highlighted_index = None;
284 self.open_submenu = None;
285 }
286
287 pub fn toggle(&mut self) {
289 if self.open {
290 self.hide();
291 } else {
292 self.show();
293 }
294 }
295
296 #[must_use]
298 pub const fn is_open(&self) -> bool {
299 self.open
300 }
301
302 #[must_use]
304 pub const fn highlighted_index(&self) -> Option<usize> {
305 self.highlighted_index
306 }
307
308 fn calculate_menu_height(&self) -> f32 {
310 let padding = 8.0; let items_height: f32 = self.items.iter().map(MenuItem::height).sum();
312 items_height + padding * 2.0
313 }
314
315 fn next_selectable(&self, from: Option<usize>, forward: bool) -> Option<usize> {
317 if self.items.is_empty() {
318 return None;
319 }
320
321 let start = from.map_or_else(
322 || if forward { 0 } else { self.items.len() - 1 },
323 |i| {
324 if forward {
325 if i + 1 >= self.items.len() {
326 0
327 } else {
328 i + 1
329 }
330 } else if i == 0 {
331 self.items.len() - 1
332 } else {
333 i - 1
334 }
335 },
336 );
337
338 let mut idx = start;
339 for _ in 0..self.items.len() {
340 if self.items[idx].is_selectable() {
341 return Some(idx);
342 }
343 if forward {
344 idx = if idx + 1 >= self.items.len() {
345 0
346 } else {
347 idx + 1
348 };
349 } else {
350 idx = if idx == 0 {
351 self.items.len() - 1
352 } else {
353 idx - 1
354 };
355 }
356 }
357
358 None
359 }
360
361 fn item_at_position(&self, y: f32) -> Option<usize> {
363 let relative_y = y - self.panel_bounds.y - 8.0; if relative_y < 0.0 {
365 return None;
366 }
367
368 let mut current_y = 0.0;
369 for (i, item) in self.items.iter().enumerate() {
370 let height = item.height();
371 if relative_y >= current_y && relative_y < current_y + height {
372 return Some(i);
373 }
374 current_y += height;
375 }
376
377 None
378 }
379
380 fn rect_contains(r: &Rect, p: &Point) -> bool {
382 p.x >= r.x && p.x <= r.x + r.width && p.y >= r.y && p.y <= r.y + r.height
383 }
384
385 fn activate_item(&mut self, idx: usize, allow_submenu: bool) -> Option<Box<dyn Any + Send>> {
389 let item = self.items.get_mut(idx)?;
390 match item {
391 MenuItem::Action {
392 action, disabled, ..
393 } if !*disabled => {
394 let action_id = action.clone();
395 self.hide();
396 Some(Box::new(MenuItemSelected { action: action_id }))
397 }
398 MenuItem::Checkbox {
399 action,
400 checked,
401 disabled,
402 ..
403 } if !*disabled => {
404 *checked = !*checked;
405 let action_id = action.clone();
406 let is_checked = *checked;
407 Some(Box::new(MenuCheckboxToggled {
408 action: action_id,
409 checked: is_checked,
410 }))
411 }
412 MenuItem::Submenu { disabled, .. } if allow_submenu && !*disabled => {
413 self.open_submenu = Some(idx);
414 None
415 }
416 _ => None,
417 }
418 }
419
420 fn on_mouse_down(&mut self, position: &Point) -> Option<Box<dyn Any + Send>> {
422 if Self::rect_contains(&self.bounds, position) && self.trigger == MenuTrigger::Click {
423 self.toggle();
424 return Some(Box::new(MenuToggled { open: self.open }));
425 }
426 if !self.open {
427 return None;
428 }
429 if !Self::rect_contains(&self.panel_bounds, position) {
430 self.hide();
432 return Some(Box::new(MenuClosed));
433 }
434 let idx = self.item_at_position(position.y)?;
435 self.activate_item(idx, true)
436 }
437
438 fn on_mouse_move(&mut self, position: &Point) {
440 if !self.open {
441 return;
442 }
443 self.highlighted_index = if Self::rect_contains(&self.panel_bounds, position) {
444 self.item_at_position(position.y)
445 } else {
446 None
447 };
448 }
449
450 fn on_key_down(&mut self, key: &Key) -> Option<Box<dyn Any + Send>> {
452 match key {
453 Key::Escape => {
454 self.hide();
455 Some(Box::new(MenuClosed))
456 }
457 Key::Up => {
458 self.highlighted_index = self.next_selectable(self.highlighted_index, false);
459 None
460 }
461 Key::Down => {
462 self.highlighted_index = self.next_selectable(self.highlighted_index, true);
463 None
464 }
465 Key::Enter | Key::Space => {
466 let idx = self.highlighted_index?;
467 self.activate_item(idx, false)
468 }
469 _ => None,
470 }
471 }
472
473 fn paint_hover(&self, canvas: &mut dyn Canvas, i: usize, y: f32, height: f32, disabled: bool) {
475 if self.highlighted_index == Some(i) && !disabled {
476 let hover_rect = Rect::new(self.panel_bounds.x, y, self.panel_bounds.width, height);
477 canvas.fill_rect(hover_rect, self.hover_color);
478 }
479 }
480
481 fn paint_item(&self, canvas: &mut dyn Canvas, i: usize, item: &MenuItem, y: f32) {
483 let height = item.height();
484 let text_style = TextStyle {
485 size: 14.0,
486 color: self.text_color,
487 ..Default::default()
488 };
489 let disabled_style = TextStyle {
490 size: 14.0,
491 color: self.disabled_color,
492 ..Default::default()
493 };
494 match item {
495 MenuItem::Action {
496 label,
497 disabled,
498 shortcut,
499 ..
500 } => {
501 self.paint_hover(canvas, i, y, height, *disabled);
502 let style = if *disabled {
503 &disabled_style
504 } else {
505 &text_style
506 };
507 canvas.draw_text(
508 label,
509 Point::new(self.panel_bounds.x + 12.0, y + 20.0),
510 style,
511 );
512 if let Some(shortcut) = shortcut {
513 let shortcut_style = TextStyle {
514 size: 12.0,
515 color: self.disabled_color,
516 ..Default::default()
517 };
518 canvas.draw_text(
519 shortcut,
520 Point::new(
521 self.panel_bounds.x + self.panel_bounds.width - 60.0,
522 y + 20.0,
523 ),
524 &shortcut_style,
525 );
526 }
527 }
528 MenuItem::Checkbox {
529 label,
530 checked,
531 disabled,
532 ..
533 } => {
534 self.paint_hover(canvas, i, y, height, *disabled);
535 let check_text = if *checked { "✓" } else { " " };
536 let style = if *disabled {
537 &disabled_style
538 } else {
539 &text_style
540 };
541 canvas.draw_text(
542 check_text,
543 Point::new(self.panel_bounds.x + 12.0, y + 20.0),
544 style,
545 );
546 canvas.draw_text(
547 label,
548 Point::new(self.panel_bounds.x + 32.0, y + 20.0),
549 style,
550 );
551 }
552 MenuItem::Separator => {
553 let line_y = y + 4.0;
554 canvas.draw_line(
555 Point::new(self.panel_bounds.x + 8.0, line_y),
556 Point::new(self.panel_bounds.x + self.panel_bounds.width - 8.0, line_y),
557 Color::rgb(0.9, 0.9, 0.9),
558 1.0,
559 );
560 }
561 MenuItem::Submenu {
562 label, disabled, ..
563 } => {
564 self.paint_hover(canvas, i, y, height, *disabled);
565 let style = if *disabled {
566 &disabled_style
567 } else {
568 &text_style
569 };
570 canvas.draw_text(
571 label,
572 Point::new(self.panel_bounds.x + 12.0, y + 20.0),
573 style,
574 );
575 canvas.draw_text(
576 "›",
577 Point::new(
578 self.panel_bounds.x + self.panel_bounds.width - 20.0,
579 y + 20.0,
580 ),
581 style,
582 );
583 }
584 }
585 }
586}
587
588impl Widget for Menu {
589 fn type_id(&self) -> TypeId {
590 TypeId::of::<Self>()
591 }
592
593 fn measure(&self, constraints: Constraints) -> Size {
594 if let Some(trigger) = self.trigger_widget.first() {
596 trigger.measure(constraints)
597 } else {
598 Size::new(self.width.min(constraints.max_width), 32.0)
599 }
600 }
601
602 fn layout(&mut self, bounds: Rect) -> LayoutResult {
603 self.bounds = bounds;
604
605 if let Some(trigger) = self.trigger_widget.first_mut() {
607 trigger.layout(bounds);
608 }
609
610 if self.open {
612 let menu_height = self.calculate_menu_height();
613 self.panel_bounds =
614 Rect::new(bounds.x, bounds.y + bounds.height, self.width, menu_height);
615 }
616
617 LayoutResult {
618 size: bounds.size(),
619 }
620 }
621
622 fn paint(&self, canvas: &mut dyn Canvas) {
623 if let Some(trigger) = self.trigger_widget.first() {
625 trigger.paint(canvas);
626 }
627
628 if !self.open {
629 return;
630 }
631
632 let shadow_bounds = Rect::new(
634 self.panel_bounds.x + 2.0,
635 self.panel_bounds.y + 2.0,
636 self.panel_bounds.width,
637 self.panel_bounds.height,
638 );
639 canvas.fill_rect(shadow_bounds, Color::rgba(0.0, 0.0, 0.0, 0.1));
640 canvas.fill_rect(self.panel_bounds, self.background_color);
641
642 let mut y = self.panel_bounds.y + 8.0; for (i, item) in self.items.iter().enumerate() {
645 self.paint_item(canvas, i, item, y);
646 y += item.height();
647 }
648 }
649
650 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
651 match event {
652 Event::MouseDown { position, .. } => self.on_mouse_down(position),
653 Event::MouseMove { position } => {
654 self.on_mouse_move(position);
655 None
656 }
657 Event::KeyDown { key, .. } if self.open => self.on_key_down(key),
658 _ => None,
659 }
660 }
661
662 fn children(&self) -> &[Box<dyn Widget>] {
663 &self.trigger_widget
664 }
665
666 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
667 &mut self.trigger_widget
668 }
669
670 fn is_focusable(&self) -> bool {
671 true
672 }
673
674 fn test_id(&self) -> Option<&str> {
675 self.test_id_value.as_deref()
676 }
677
678 fn bounds(&self) -> Rect {
679 self.bounds
680 }
681}
682
683impl Brick for Menu {
685 fn brick_name(&self) -> &'static str {
686 "Menu"
687 }
688
689 fn assertions(&self) -> &[BrickAssertion] {
690 &[BrickAssertion::MaxLatencyMs(16)]
691 }
692
693 fn budget(&self) -> BrickBudget {
694 BrickBudget::uniform(16)
695 }
696
697 fn verify(&self) -> BrickVerification {
698 BrickVerification {
699 passed: self.assertions().to_vec(),
700 failed: vec![],
701 verification_time: Duration::from_micros(10),
702 }
703 }
704
705 fn to_html(&self) -> String {
706 r#"<div class="brick-menu"></div>"#.to_string()
707 }
708
709 fn to_css(&self) -> String {
710 ".brick-menu { display: block; position: relative; }".to_string()
711 }
712
713 fn test_id(&self) -> Option<&str> {
714 self.test_id_value.as_deref()
715 }
716}
717
718#[derive(Debug, Clone)]
720pub struct MenuToggled {
721 pub open: bool,
723}
724
725#[derive(Debug, Clone)]
727pub struct MenuItemSelected {
728 pub action: String,
730}
731
732#[derive(Debug, Clone)]
734pub struct MenuCheckboxToggled {
735 pub action: String,
737 pub checked: bool,
739}
740
741#[derive(Debug, Clone)]
743pub struct MenuClosed;
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748
749 #[test]
754 fn test_menu_item_action() {
755 let item = MenuItem::action("Cut", "edit.cut");
756 match item {
757 MenuItem::Action {
758 label,
759 action,
760 disabled,
761 shortcut,
762 } => {
763 assert_eq!(label, "Cut");
764 assert_eq!(action, "edit.cut");
765 assert!(!disabled);
766 assert!(shortcut.is_none());
767 }
768 _ => panic!("Expected Action"),
769 }
770 }
771
772 #[test]
773 fn test_menu_item_action_with_shortcut() {
774 let item = MenuItem::action("Cut", "edit.cut").shortcut("Ctrl+X");
775 match item {
776 MenuItem::Action { shortcut, .. } => {
777 assert_eq!(shortcut, Some("Ctrl+X".to_string()));
778 }
779 _ => panic!("Expected Action"),
780 }
781 }
782
783 #[test]
784 fn test_menu_item_checkbox() {
785 let item = MenuItem::checkbox("Show Grid", "view.grid", true);
786 match item {
787 MenuItem::Checkbox {
788 label,
789 checked,
790 disabled,
791 ..
792 } => {
793 assert_eq!(label, "Show Grid");
794 assert!(checked);
795 assert!(!disabled);
796 }
797 _ => panic!("Expected Checkbox"),
798 }
799 }
800
801 #[test]
802 fn test_menu_item_separator() {
803 let item = MenuItem::separator();
804 assert!(matches!(item, MenuItem::Separator));
805 }
806
807 #[test]
808 fn test_menu_item_submenu() {
809 let items = vec![MenuItem::action("Sub 1", "sub.1")];
810 let item = MenuItem::submenu("More", items);
811 match item {
812 MenuItem::Submenu {
813 label,
814 items,
815 disabled,
816 } => {
817 assert_eq!(label, "More");
818 assert_eq!(items.len(), 1);
819 assert!(!disabled);
820 }
821 _ => panic!("Expected Submenu"),
822 }
823 }
824
825 #[test]
826 fn test_menu_item_disabled() {
827 let item = MenuItem::action("Cut", "edit.cut").disabled(true);
828 match item {
829 MenuItem::Action { disabled, .. } => assert!(disabled),
830 _ => panic!("Expected Action"),
831 }
832 }
833
834 #[test]
835 fn test_menu_item_is_selectable() {
836 assert!(MenuItem::action("Cut", "edit.cut").is_selectable());
837 assert!(!MenuItem::action("Cut", "edit.cut")
838 .disabled(true)
839 .is_selectable());
840 assert!(!MenuItem::separator().is_selectable());
841 assert!(MenuItem::checkbox("Show", "show", false).is_selectable());
842 }
843
844 #[test]
845 fn test_menu_item_height() {
846 assert_eq!(MenuItem::action("Cut", "edit.cut").height(), 32.0);
847 assert_eq!(MenuItem::separator().height(), 9.0);
848 }
849
850 #[test]
855 fn test_menu_new() {
856 let menu = Menu::new();
857 assert!(menu.items.is_empty());
858 assert!(!menu.open);
859 assert_eq!(menu.trigger, MenuTrigger::Click);
860 }
861
862 #[test]
863 fn test_menu_builder() {
864 let menu = Menu::new()
865 .items(vec![
866 MenuItem::action("Cut", "cut"),
867 MenuItem::separator(),
868 MenuItem::action("Paste", "paste"),
869 ])
870 .trigger(MenuTrigger::Hover)
871 .width(250.0);
872
873 assert_eq!(menu.items.len(), 3);
874 assert_eq!(menu.trigger, MenuTrigger::Hover);
875 assert_eq!(menu.width, 250.0);
876 }
877
878 #[test]
879 fn test_menu_add_item() {
880 let menu = Menu::new()
881 .item(MenuItem::action("Cut", "cut"))
882 .item(MenuItem::action("Copy", "copy"));
883 assert_eq!(menu.items.len(), 2);
884 }
885
886 #[test]
887 fn test_menu_show_hide() {
888 let mut menu = Menu::new();
889 assert!(!menu.is_open());
890
891 menu.show();
892 assert!(menu.is_open());
893
894 menu.hide();
895 assert!(!menu.is_open());
896 }
897
898 #[test]
899 fn test_menu_toggle() {
900 let mut menu = Menu::new();
901
902 menu.toggle();
903 assert!(menu.is_open());
904
905 menu.toggle();
906 assert!(!menu.is_open());
907 }
908
909 #[test]
910 fn test_menu_calculate_height() {
911 let menu = Menu::new().items(vec![
912 MenuItem::action("Cut", "cut"),
913 MenuItem::separator(),
914 MenuItem::action("Paste", "paste"),
915 ]);
916 assert_eq!(menu.calculate_menu_height(), 89.0);
918 }
919
920 #[test]
921 fn test_menu_measure() {
922 let menu = Menu::new().width(200.0);
923 let size = menu.measure(Constraints::loose(Size::new(300.0, 400.0)));
924 assert_eq!(size.width, 200.0);
925 }
926
927 #[test]
928 fn test_menu_layout() {
929 let mut menu = Menu::new().width(200.0);
930 menu.open = true;
931 menu.items = vec![MenuItem::action("Cut", "cut")];
932
933 let result = menu.layout(Rect::new(10.0, 20.0, 100.0, 32.0));
934 assert_eq!(result.size, Size::new(100.0, 32.0));
935 assert_eq!(menu.panel_bounds.x, 10.0);
936 assert_eq!(menu.panel_bounds.y, 52.0); }
938
939 #[test]
940 fn test_menu_type_id() {
941 let menu = Menu::new();
942 assert_eq!(Widget::type_id(&menu), TypeId::of::<Menu>());
943 }
944
945 #[test]
946 fn test_menu_is_focusable() {
947 let menu = Menu::new();
948 assert!(menu.is_focusable());
949 }
950
951 #[test]
952 fn test_menu_test_id() {
953 let menu = Menu::new().with_test_id("my-menu");
954 assert_eq!(Widget::test_id(&menu), Some("my-menu"));
955 }
956
957 #[test]
958 fn test_menu_highlighted_index() {
959 let mut menu = Menu::new();
960 assert!(menu.highlighted_index().is_none());
961
962 menu.highlighted_index = Some(2);
963 assert_eq!(menu.highlighted_index(), Some(2));
964 }
965
966 #[test]
967 fn test_menu_next_selectable() {
968 let menu = Menu::new().items(vec![
969 MenuItem::action("Cut", "cut"),
970 MenuItem::separator(),
971 MenuItem::action("Paste", "paste"),
972 ]);
973
974 assert_eq!(menu.next_selectable(None, true), Some(0));
976
977 assert_eq!(menu.next_selectable(Some(0), true), Some(2)); assert_eq!(menu.next_selectable(Some(2), false), Some(0)); }
983
984 #[test]
985 fn test_menu_escape_closes() {
986 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
987 menu.show();
988
989 let result = menu.event(&Event::key_down(Key::Escape));
990 assert!(result.is_some());
991 assert!(!menu.is_open());
992 }
993
994 #[test]
995 fn test_menu_arrow_navigation() {
996 let mut menu = Menu::new().items(vec![
997 MenuItem::action("Cut", "cut"),
998 MenuItem::action("Copy", "copy"),
999 ]);
1000 menu.show();
1001
1002 menu.event(&Event::key_down(Key::Down));
1003 assert_eq!(menu.highlighted_index, Some(0));
1004
1005 menu.event(&Event::key_down(Key::Down));
1006 assert_eq!(menu.highlighted_index, Some(1));
1007 }
1008
1009 #[test]
1014 fn test_menu_toggled_message() {
1015 let msg = MenuToggled { open: true };
1016 assert!(msg.open);
1017 }
1018
1019 #[test]
1020 fn test_menu_item_selected_message() {
1021 let msg = MenuItemSelected {
1022 action: "edit.cut".to_string(),
1023 };
1024 assert_eq!(msg.action, "edit.cut");
1025 }
1026
1027 #[test]
1028 fn test_menu_checkbox_toggled_message() {
1029 let msg = MenuCheckboxToggled {
1030 action: "view.grid".to_string(),
1031 checked: true,
1032 };
1033 assert_eq!(msg.action, "view.grid");
1034 assert!(msg.checked);
1035 }
1036
1037 #[test]
1038 fn test_menu_closed_message() {
1039 let msg = MenuClosed;
1040 assert_eq!(format!("{msg:?}"), "MenuClosed");
1041 }
1042
1043 #[test]
1048 fn test_menu_shortcut_on_non_action() {
1049 let item = MenuItem::checkbox("Show", "show", false).shortcut("Ctrl+S");
1051 match item {
1052 MenuItem::Checkbox { .. } => {} _ => panic!("Expected Checkbox"),
1054 }
1055 }
1056
1057 #[test]
1058 fn test_menu_disabled_checkbox() {
1059 let item = MenuItem::checkbox("Show", "show", true).disabled(true);
1060 match item {
1061 MenuItem::Checkbox { disabled, .. } => assert!(disabled),
1062 _ => panic!("Expected Checkbox"),
1063 }
1064 }
1065
1066 #[test]
1067 fn test_menu_disabled_submenu() {
1068 let item = MenuItem::submenu("More", vec![]).disabled(true);
1069 match item {
1070 MenuItem::Submenu { disabled, .. } => assert!(disabled),
1071 _ => panic!("Expected Submenu"),
1072 }
1073 }
1074
1075 #[test]
1076 fn test_menu_disabled_separator_no_op() {
1077 let item = MenuItem::separator().disabled(true);
1079 assert!(matches!(item, MenuItem::Separator));
1080 }
1081
1082 #[test]
1083 fn test_menu_submenu_not_selectable_when_disabled() {
1084 let item = MenuItem::submenu("More", vec![]).disabled(true);
1085 assert!(!item.is_selectable());
1086 }
1087
1088 #[test]
1089 fn test_menu_context_menu_trigger() {
1090 let menu = Menu::new().trigger(MenuTrigger::ContextMenu);
1091 assert_eq!(menu.trigger, MenuTrigger::ContextMenu);
1092 }
1093
1094 #[test]
1095 fn test_menu_hover_trigger() {
1096 let menu = Menu::new().trigger(MenuTrigger::Hover);
1097 assert_eq!(menu.trigger, MenuTrigger::Hover);
1098 }
1099
1100 #[test]
1101 fn test_menu_background_color() {
1102 let menu = Menu::new().background_color(Color::RED);
1103 assert_eq!(menu.background_color, Color::RED);
1104 }
1105
1106 #[test]
1107 fn test_menu_hover_color() {
1108 let menu = Menu::new().hover_color(Color::BLUE);
1109 assert_eq!(menu.hover_color, Color::BLUE);
1110 }
1111
1112 #[test]
1113 fn test_menu_text_color() {
1114 let menu = Menu::new().text_color(Color::GREEN);
1115 assert_eq!(menu.text_color, Color::GREEN);
1116 }
1117
1118 #[test]
1119 fn test_menu_next_selectable_empty() {
1120 let menu = Menu::new();
1121 assert!(menu.next_selectable(None, true).is_none());
1122 assert!(menu.next_selectable(None, false).is_none());
1123 }
1124
1125 #[test]
1126 fn test_menu_next_selectable_all_disabled() {
1127 let menu = Menu::new().items(vec![
1128 MenuItem::separator(),
1129 MenuItem::action("Cut", "cut").disabled(true),
1130 MenuItem::separator(),
1131 ]);
1132 assert!(menu.next_selectable(None, true).is_none());
1133 }
1134
1135 #[test]
1136 fn test_menu_next_selectable_wrap_forward() {
1137 let menu = Menu::new().items(vec![MenuItem::action("A", "a"), MenuItem::action("B", "b")]);
1138 assert_eq!(menu.next_selectable(Some(1), true), Some(0));
1140 }
1141
1142 #[test]
1143 fn test_menu_next_selectable_wrap_backward() {
1144 let menu = Menu::new().items(vec![MenuItem::action("A", "a"), MenuItem::action("B", "b")]);
1145 assert_eq!(menu.next_selectable(Some(0), false), Some(1));
1147 }
1148
1149 #[test]
1150 fn test_menu_children_empty() {
1151 let menu = Menu::new();
1152 assert!(menu.children().is_empty());
1153 }
1154
1155 #[test]
1156 fn test_menu_children_mut_empty() {
1157 let mut menu = Menu::new();
1158 assert!(menu.children_mut().is_empty());
1159 }
1160
1161 #[test]
1162 fn test_menu_bounds() {
1163 let mut menu = Menu::new();
1164 menu.layout(Rect::new(10.0, 20.0, 200.0, 32.0));
1165 assert_eq!(menu.bounds(), Rect::new(10.0, 20.0, 200.0, 32.0));
1166 }
1167
1168 #[test]
1169 fn test_menu_trigger_default() {
1170 assert_eq!(MenuTrigger::default(), MenuTrigger::Click);
1171 }
1172
1173 #[test]
1174 fn test_menu_event_closed_returns_none() {
1175 let mut menu = Menu::new();
1176 let result = menu.event(&Event::key_down(Key::Down));
1178 assert!(result.is_none());
1179 }
1180
1181 #[test]
1182 fn test_menu_enter_selects_item() {
1183 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1184 menu.show();
1185 menu.highlighted_index = Some(0);
1186 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1187
1188 let result = menu.event(&Event::key_down(Key::Enter));
1189 assert!(result.is_some());
1190 assert!(!menu.is_open());
1191 }
1192
1193 #[test]
1194 fn test_menu_space_selects_item() {
1195 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1196 menu.show();
1197 menu.highlighted_index = Some(0);
1198 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1199
1200 let result = menu.event(&Event::key_down(Key::Space));
1201 assert!(result.is_some());
1202 }
1203
1204 #[test]
1205 fn test_menu_enter_on_checkbox_toggles() {
1206 let mut menu = Menu::new().items(vec![MenuItem::checkbox("Show", "show", false)]);
1207 menu.show();
1208 menu.highlighted_index = Some(0);
1209 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1210
1211 let result = menu.event(&Event::key_down(Key::Enter));
1212 assert!(result.is_some());
1213 assert!(menu.is_open());
1215 }
1216
1217 #[test]
1218 fn test_menu_enter_on_disabled_does_nothing() {
1219 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut").disabled(true)]);
1220 menu.show();
1221 menu.highlighted_index = Some(0);
1222 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1223
1224 let result = menu.event(&Event::key_down(Key::Enter));
1225 assert!(result.is_none());
1226 assert!(menu.is_open()); }
1228
1229 #[test]
1230 fn test_menu_up_arrow_navigation() {
1231 let mut menu =
1232 Menu::new().items(vec![MenuItem::action("A", "a"), MenuItem::action("B", "b")]);
1233 menu.show();
1234 menu.highlighted_index = Some(1);
1235
1236 menu.event(&Event::key_down(Key::Up));
1237 assert_eq!(menu.highlighted_index, Some(0));
1238 }
1239
1240 #[test]
1245 fn test_menu_brick_name() {
1246 let menu = Menu::new();
1247 assert_eq!(menu.brick_name(), "Menu");
1248 }
1249
1250 #[test]
1251 fn test_menu_brick_assertions() {
1252 let menu = Menu::new();
1253 let assertions = menu.assertions();
1254 assert!(!assertions.is_empty());
1255 assert!(matches!(assertions[0], BrickAssertion::MaxLatencyMs(16)));
1256 }
1257
1258 #[test]
1259 fn test_menu_brick_budget() {
1260 let menu = Menu::new();
1261 let budget = menu.budget();
1262 assert!(budget.layout_ms > 0);
1264 assert!(budget.paint_ms > 0);
1265 }
1266
1267 #[test]
1268 fn test_menu_brick_verify() {
1269 let menu = Menu::new();
1270 let verification = menu.verify();
1271 assert!(!verification.passed.is_empty());
1272 assert!(verification.failed.is_empty());
1273 }
1274
1275 #[test]
1276 fn test_menu_brick_to_html() {
1277 let menu = Menu::new();
1278 let html = menu.to_html();
1279 assert!(html.contains("brick-menu"));
1280 }
1281
1282 #[test]
1283 fn test_menu_brick_to_css() {
1284 let menu = Menu::new();
1285 let css = menu.to_css();
1286 assert!(css.contains(".brick-menu"));
1287 assert!(css.contains("display: block"));
1288 assert!(css.contains("position: relative"));
1289 }
1290
1291 #[test]
1292 fn test_menu_brick_test_id() {
1293 let menu = Menu::new().with_test_id("my-menu");
1294 assert_eq!(Brick::test_id(&menu), Some("my-menu"));
1295 }
1296
1297 #[test]
1298 fn test_menu_brick_test_id_none() {
1299 let menu = Menu::new();
1300 assert!(Brick::test_id(&menu).is_none());
1301 }
1302
1303 #[test]
1308 fn test_menu_item_at_position_valid() {
1309 let mut menu = Menu::new().items(vec![
1310 MenuItem::action("Cut", "cut"),
1311 MenuItem::action("Copy", "copy"),
1312 MenuItem::action("Paste", "paste"),
1313 ]);
1314 menu.open = true;
1315 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1316
1317 let item = menu.item_at_position(menu.panel_bounds.y + 8.0 + 10.0);
1320 assert_eq!(item, Some(0));
1321
1322 let item = menu.item_at_position(menu.panel_bounds.y + 8.0 + 40.0);
1323 assert_eq!(item, Some(1));
1324
1325 let item = menu.item_at_position(menu.panel_bounds.y + 8.0 + 72.0);
1326 assert_eq!(item, Some(2));
1327 }
1328
1329 #[test]
1330 fn test_menu_item_at_position_above_menu() {
1331 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1332 menu.open = true;
1333 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1334
1335 let item = menu.item_at_position(menu.panel_bounds.y - 10.0);
1337 assert!(item.is_none());
1338 }
1339
1340 #[test]
1341 fn test_menu_item_at_position_below_items() {
1342 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1343 menu.open = true;
1344 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1345
1346 let item = menu.item_at_position(menu.panel_bounds.y + 500.0);
1348 assert!(item.is_none());
1349 }
1350
1351 #[test]
1356 fn test_menu_click_on_trigger_opens() {
1357 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1358 menu.layout(Rect::new(10.0, 10.0, 200.0, 32.0));
1359
1360 let result = menu.event(&Event::MouseDown {
1362 position: Point::new(50.0, 20.0),
1363 button: presentar_core::MouseButton::Left,
1364 });
1365
1366 assert!(result.is_some());
1367 assert!(menu.is_open());
1368 }
1369
1370 #[test]
1371 fn test_menu_click_outside_closes() {
1372 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1373 menu.show();
1374 menu.layout(Rect::new(10.0, 10.0, 200.0, 32.0));
1375
1376 let result = menu.event(&Event::MouseDown {
1378 position: Point::new(500.0, 500.0),
1379 button: presentar_core::MouseButton::Left,
1380 });
1381
1382 assert!(result.is_some());
1383 assert!(!menu.is_open());
1384 }
1385
1386 #[test]
1387 fn test_menu_click_on_action_item() {
1388 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1389 menu.show();
1390 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1391
1392 let click_y = menu.panel_bounds.y + 8.0 + 16.0; let result = menu.event(&Event::MouseDown {
1395 position: Point::new(menu.panel_bounds.x + 50.0, click_y),
1396 button: presentar_core::MouseButton::Left,
1397 });
1398
1399 assert!(result.is_some());
1400 assert!(!menu.is_open()); }
1402
1403 #[test]
1404 fn test_menu_click_on_checkbox_item() {
1405 let mut menu = Menu::new().items(vec![MenuItem::checkbox("Show Grid", "show", false)]);
1406 menu.show();
1407 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1408
1409 let click_y = menu.panel_bounds.y + 8.0 + 16.0;
1411 let result = menu.event(&Event::MouseDown {
1412 position: Point::new(menu.panel_bounds.x + 50.0, click_y),
1413 button: presentar_core::MouseButton::Left,
1414 });
1415
1416 assert!(result.is_some());
1417 if let MenuItem::Checkbox { checked, .. } = &menu.items[0] {
1419 assert!(*checked);
1420 } else {
1421 panic!("Expected Checkbox item");
1422 }
1423 }
1424
1425 #[test]
1426 fn test_menu_click_on_disabled_action() {
1427 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut").disabled(true)]);
1428 menu.show();
1429 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1430
1431 let click_y = menu.panel_bounds.y + 8.0 + 16.0;
1433 let result = menu.event(&Event::MouseDown {
1434 position: Point::new(menu.panel_bounds.x + 50.0, click_y),
1435 button: presentar_core::MouseButton::Left,
1436 });
1437
1438 assert!(result.is_none()); assert!(menu.is_open()); }
1441
1442 #[test]
1443 fn test_menu_click_on_submenu_opens_it() {
1444 let mut menu = Menu::new().items(vec![MenuItem::submenu(
1445 "More",
1446 vec![MenuItem::action("Sub", "sub")],
1447 )]);
1448 menu.show();
1449 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1450
1451 let click_y = menu.panel_bounds.y + 8.0 + 16.0;
1453 let result = menu.event(&Event::MouseDown {
1454 position: Point::new(menu.panel_bounds.x + 50.0, click_y),
1455 button: presentar_core::MouseButton::Left,
1456 });
1457
1458 assert!(result.is_none()); assert_eq!(menu.open_submenu, Some(0));
1460 }
1461
1462 #[test]
1463 fn test_menu_mouse_move_updates_highlight() {
1464 let mut menu = Menu::new().items(vec![
1465 MenuItem::action("Cut", "cut"),
1466 MenuItem::action("Copy", "copy"),
1467 ]);
1468 menu.show();
1469 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1470
1471 let y1 = menu.panel_bounds.y + 8.0 + 16.0;
1473 menu.event(&Event::MouseMove {
1474 position: Point::new(menu.panel_bounds.x + 50.0, y1),
1475 });
1476 assert_eq!(menu.highlighted_index, Some(0));
1477
1478 let y2 = menu.panel_bounds.y + 8.0 + 48.0;
1480 menu.event(&Event::MouseMove {
1481 position: Point::new(menu.panel_bounds.x + 50.0, y2),
1482 });
1483 assert_eq!(menu.highlighted_index, Some(1));
1484 }
1485
1486 #[test]
1487 fn test_menu_mouse_move_outside_clears_highlight() {
1488 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1489 menu.show();
1490 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1491 menu.highlighted_index = Some(0);
1492
1493 menu.event(&Event::MouseMove {
1495 position: Point::new(500.0, 500.0),
1496 });
1497 assert!(menu.highlighted_index.is_none());
1498 }
1499
1500 #[test]
1505 fn test_menu_up_from_none_selects_last() {
1506 let mut menu =
1507 Menu::new().items(vec![MenuItem::action("A", "a"), MenuItem::action("B", "b")]);
1508 menu.show();
1509
1510 menu.event(&Event::key_down(Key::Up));
1511 assert_eq!(menu.highlighted_index, Some(1)); }
1513
1514 #[test]
1515 fn test_menu_down_from_last_wraps_to_first() {
1516 let mut menu =
1517 Menu::new().items(vec![MenuItem::action("A", "a"), MenuItem::action("B", "b")]);
1518 menu.show();
1519 menu.highlighted_index = Some(1);
1520
1521 menu.event(&Event::key_down(Key::Down));
1522 assert_eq!(menu.highlighted_index, Some(0)); }
1524
1525 #[test]
1526 fn test_menu_up_skips_separator() {
1527 let mut menu = Menu::new().items(vec![
1528 MenuItem::action("A", "a"),
1529 MenuItem::separator(),
1530 MenuItem::action("B", "b"),
1531 ]);
1532 menu.show();
1533 menu.highlighted_index = Some(2);
1534
1535 menu.event(&Event::key_down(Key::Up));
1536 assert_eq!(menu.highlighted_index, Some(0)); }
1538
1539 #[test]
1540 fn test_menu_down_skips_disabled() {
1541 let mut menu = Menu::new().items(vec![
1542 MenuItem::action("A", "a"),
1543 MenuItem::action("B", "b").disabled(true),
1544 MenuItem::action("C", "c"),
1545 ]);
1546 menu.show();
1547 menu.highlighted_index = Some(0);
1548
1549 menu.event(&Event::key_down(Key::Down));
1550 assert_eq!(menu.highlighted_index, Some(2)); }
1552
1553 #[test]
1554 fn test_menu_other_key_does_nothing() {
1555 let mut menu = Menu::new().items(vec![MenuItem::action("A", "a")]);
1556 menu.show();
1557 menu.highlighted_index = Some(0);
1558
1559 let result = menu.event(&Event::key_down(Key::Tab));
1560 assert!(result.is_none());
1561 assert_eq!(menu.highlighted_index, Some(0));
1562 }
1563
1564 #[test]
1565 fn test_menu_enter_on_separator_does_nothing() {
1566 let mut menu = Menu::new().items(vec![MenuItem::separator(), MenuItem::action("A", "a")]);
1567 menu.show();
1568 menu.highlighted_index = Some(0); let result = menu.event(&Event::key_down(Key::Enter));
1571 assert!(result.is_none());
1572 assert!(menu.is_open());
1573 }
1574
1575 #[test]
1576 fn test_menu_enter_on_submenu_does_nothing() {
1577 let mut menu = Menu::new().items(vec![MenuItem::submenu(
1578 "More",
1579 vec![MenuItem::action("Sub", "sub")],
1580 )]);
1581 menu.show();
1582 menu.highlighted_index = Some(0);
1583
1584 let result = menu.event(&Event::key_down(Key::Enter));
1585 assert!(result.is_none());
1586 assert!(menu.is_open());
1587 }
1588
1589 #[test]
1590 fn test_menu_space_on_checkbox_toggles() {
1591 let mut menu = Menu::new().items(vec![MenuItem::checkbox("Show", "show", false)]);
1592 menu.show();
1593 menu.highlighted_index = Some(0);
1594 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1595
1596 let result = menu.event(&Event::key_down(Key::Space));
1597 assert!(result.is_some());
1598 if let MenuItem::Checkbox { checked, .. } = &menu.items[0] {
1600 assert!(*checked);
1601 }
1602 }
1603
1604 #[test]
1609 fn test_menu_item_height_action() {
1610 let item = MenuItem::action("Test", "test");
1611 assert_eq!(item.height(), 32.0);
1612 }
1613
1614 #[test]
1615 fn test_menu_item_height_checkbox() {
1616 let item = MenuItem::checkbox("Test", "test", false);
1617 assert_eq!(item.height(), 32.0);
1618 }
1619
1620 #[test]
1621 fn test_menu_item_height_submenu() {
1622 let item = MenuItem::submenu("More", vec![]);
1623 assert_eq!(item.height(), 32.0);
1624 }
1625
1626 #[test]
1627 fn test_menu_item_is_selectable_submenu() {
1628 let item = MenuItem::submenu("More", vec![]);
1629 assert!(item.is_selectable());
1630 }
1631
1632 #[test]
1633 fn test_menu_item_is_selectable_disabled_checkbox() {
1634 let item = MenuItem::checkbox("Test", "test", false).disabled(true);
1635 assert!(!item.is_selectable());
1636 }
1637
1638 #[test]
1643 fn test_menu_trigger_hover_no_click_open() {
1644 let mut menu = Menu::new()
1645 .trigger(MenuTrigger::Hover)
1646 .items(vec![MenuItem::action("Cut", "cut")]);
1647 menu.layout(Rect::new(10.0, 10.0, 200.0, 32.0));
1648
1649 let result = menu.event(&Event::MouseDown {
1651 position: Point::new(50.0, 20.0),
1652 button: presentar_core::MouseButton::Left,
1653 });
1654
1655 assert!(result.is_none());
1656 assert!(!menu.is_open());
1657 }
1658
1659 #[test]
1660 fn test_menu_trigger_context_menu_no_click_open() {
1661 let mut menu = Menu::new()
1662 .trigger(MenuTrigger::ContextMenu)
1663 .items(vec![MenuItem::action("Cut", "cut")]);
1664 menu.layout(Rect::new(10.0, 10.0, 200.0, 32.0));
1665
1666 let result = menu.event(&Event::MouseDown {
1668 position: Point::new(50.0, 20.0),
1669 button: presentar_core::MouseButton::Left,
1670 });
1671
1672 assert!(result.is_none());
1673 assert!(!menu.is_open());
1674 }
1675
1676 #[test]
1681 fn test_menu_toggled_clone() {
1682 let msg = MenuToggled { open: true };
1683 let cloned = msg.clone();
1684 assert_eq!(cloned.open, msg.open);
1685 }
1686
1687 #[test]
1688 fn test_menu_item_selected_clone() {
1689 let msg = MenuItemSelected {
1690 action: "test".to_string(),
1691 };
1692 let cloned = msg.clone();
1693 assert_eq!(cloned.action, msg.action);
1694 }
1695
1696 #[test]
1697 fn test_menu_checkbox_toggled_clone() {
1698 let msg = MenuCheckboxToggled {
1699 action: "test".to_string(),
1700 checked: true,
1701 };
1702 let cloned = msg.clone();
1703 assert_eq!(cloned.action, msg.action);
1704 assert_eq!(cloned.checked, msg.checked);
1705 }
1706
1707 #[test]
1708 fn test_menu_closed_clone() {
1709 let msg = MenuClosed;
1710 let cloned = msg;
1711 assert_eq!(format!("{cloned:?}"), "MenuClosed");
1712 }
1713
1714 #[test]
1719 fn test_menu_default() {
1720 let menu = Menu::default();
1721 assert!(menu.items.is_empty());
1722 assert!(!menu.open);
1723 assert_eq!(menu.trigger, MenuTrigger::Click);
1724 assert_eq!(menu.width, 200.0);
1725 }
1726
1727 #[test]
1728 fn test_menu_trigger_eq() {
1729 assert_eq!(MenuTrigger::Click, MenuTrigger::Click);
1730 assert_ne!(MenuTrigger::Click, MenuTrigger::Hover);
1731 assert_ne!(MenuTrigger::Hover, MenuTrigger::ContextMenu);
1732 }
1733
1734 #[test]
1735 fn test_menu_hide_clears_submenu() {
1736 let mut menu = Menu::new().items(vec![MenuItem::submenu(
1737 "More",
1738 vec![MenuItem::action("Sub", "sub")],
1739 )]);
1740 menu.show();
1741 menu.open_submenu = Some(0);
1742
1743 menu.hide();
1744 assert!(!menu.is_open());
1745 assert!(menu.open_submenu.is_none());
1746 assert!(menu.highlighted_index.is_none());
1747 }
1748
1749 #[test]
1750 fn test_menu_debug() {
1751 let item = MenuItem::action("Test", "test");
1752 let debug_str = format!("{item:?}");
1753 assert!(debug_str.contains("Test"));
1754 }
1755
1756 #[test]
1757 fn test_menu_toggled_debug() {
1758 let msg = MenuToggled { open: true };
1759 let debug_str = format!("{msg:?}");
1760 assert!(debug_str.contains("true"));
1761 }
1762}