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: Option<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: None,
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 = Some(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
381impl Widget for Menu {
382 fn type_id(&self) -> TypeId {
383 TypeId::of::<Self>()
384 }
385
386 fn measure(&self, constraints: Constraints) -> Size {
387 if let Some(ref trigger) = self.trigger_widget {
389 trigger.measure(constraints)
390 } else {
391 Size::new(self.width.min(constraints.max_width), 32.0)
392 }
393 }
394
395 fn layout(&mut self, bounds: Rect) -> LayoutResult {
396 self.bounds = bounds;
397
398 if let Some(ref mut trigger) = self.trigger_widget {
400 trigger.layout(bounds);
401 }
402
403 if self.open {
405 let menu_height = self.calculate_menu_height();
406 self.panel_bounds =
407 Rect::new(bounds.x, bounds.y + bounds.height, self.width, menu_height);
408 }
409
410 LayoutResult {
411 size: bounds.size(),
412 }
413 }
414
415 #[allow(clippy::too_many_lines)]
416 fn paint(&self, canvas: &mut dyn Canvas) {
417 if let Some(ref trigger) = self.trigger_widget {
419 trigger.paint(canvas);
420 }
421
422 if !self.open {
423 return;
424 }
425
426 let shadow_bounds = Rect::new(
428 self.panel_bounds.x + 2.0,
429 self.panel_bounds.y + 2.0,
430 self.panel_bounds.width,
431 self.panel_bounds.height,
432 );
433 canvas.fill_rect(shadow_bounds, Color::rgba(0.0, 0.0, 0.0, 0.1));
434 canvas.fill_rect(self.panel_bounds, self.background_color);
435
436 let mut y = self.panel_bounds.y + 8.0; let text_style = TextStyle {
439 size: 14.0,
440 color: self.text_color,
441 ..Default::default()
442 };
443 let disabled_style = TextStyle {
444 size: 14.0,
445 color: self.disabled_color,
446 ..Default::default()
447 };
448
449 for (i, item) in self.items.iter().enumerate() {
450 let height = item.height();
451
452 match item {
453 MenuItem::Action {
454 label,
455 disabled,
456 shortcut,
457 ..
458 } => {
459 if self.highlighted_index == Some(i) && !disabled {
461 let hover_rect =
462 Rect::new(self.panel_bounds.x, y, self.panel_bounds.width, height);
463 canvas.fill_rect(hover_rect, self.hover_color);
464 }
465
466 let style = if *disabled {
468 &disabled_style
469 } else {
470 &text_style
471 };
472 canvas.draw_text(
473 label,
474 Point::new(self.panel_bounds.x + 12.0, y + 20.0),
475 style,
476 );
477
478 if let Some(ref shortcut) = shortcut {
480 let shortcut_style = TextStyle {
481 size: 12.0,
482 color: self.disabled_color,
483 ..Default::default()
484 };
485 canvas.draw_text(
486 shortcut,
487 Point::new(
488 self.panel_bounds.x + self.panel_bounds.width - 60.0,
489 y + 20.0,
490 ),
491 &shortcut_style,
492 );
493 }
494 }
495 MenuItem::Checkbox {
496 label,
497 checked,
498 disabled,
499 ..
500 } => {
501 if self.highlighted_index == Some(i) && !disabled {
503 let hover_rect =
504 Rect::new(self.panel_bounds.x, y, self.panel_bounds.width, height);
505 canvas.fill_rect(hover_rect, self.hover_color);
506 }
507
508 let check_text = if *checked { "✓" } else { " " };
510 let style = if *disabled {
511 &disabled_style
512 } else {
513 &text_style
514 };
515 canvas.draw_text(
516 check_text,
517 Point::new(self.panel_bounds.x + 12.0, y + 20.0),
518 style,
519 );
520
521 canvas.draw_text(
523 label,
524 Point::new(self.panel_bounds.x + 32.0, y + 20.0),
525 style,
526 );
527 }
528 MenuItem::Separator => {
529 let line_y = y + 4.0;
530 canvas.draw_line(
531 Point::new(self.panel_bounds.x + 8.0, line_y),
532 Point::new(self.panel_bounds.x + self.panel_bounds.width - 8.0, line_y),
533 Color::rgb(0.9, 0.9, 0.9),
534 1.0,
535 );
536 }
537 MenuItem::Submenu {
538 label, disabled, ..
539 } => {
540 if self.highlighted_index == Some(i) && !disabled {
542 let hover_rect =
543 Rect::new(self.panel_bounds.x, y, self.panel_bounds.width, height);
544 canvas.fill_rect(hover_rect, self.hover_color);
545 }
546
547 let style = if *disabled {
549 &disabled_style
550 } else {
551 &text_style
552 };
553 canvas.draw_text(
554 label,
555 Point::new(self.panel_bounds.x + 12.0, y + 20.0),
556 style,
557 );
558
559 canvas.draw_text(
561 "›",
562 Point::new(
563 self.panel_bounds.x + self.panel_bounds.width - 20.0,
564 y + 20.0,
565 ),
566 style,
567 );
568 }
569 }
570
571 y += height;
572 }
573 }
574
575 #[allow(clippy::too_many_lines)]
576 fn event(&mut self, event: &Event) -> Option<Box<dyn Any + Send>> {
577 match event {
578 Event::MouseDown { position, .. } => {
579 let on_trigger = position.x >= self.bounds.x
581 && position.x <= self.bounds.x + self.bounds.width
582 && position.y >= self.bounds.y
583 && position.y <= self.bounds.y + self.bounds.height;
584
585 if on_trigger && self.trigger == MenuTrigger::Click {
586 self.toggle();
587 return Some(Box::new(MenuToggled { open: self.open }));
588 }
589
590 if self.open {
592 let on_menu = position.x >= self.panel_bounds.x
593 && position.x <= self.panel_bounds.x + self.panel_bounds.width
594 && position.y >= self.panel_bounds.y
595 && position.y <= self.panel_bounds.y + self.panel_bounds.height;
596
597 if on_menu {
598 if let Some(idx) = self.item_at_position(position.y) {
599 if let Some(item) = self.items.get_mut(idx) {
600 match item {
601 MenuItem::Action {
602 action, disabled, ..
603 } if !*disabled => {
604 let action_id = action.clone();
605 self.hide();
606 return Some(Box::new(MenuItemSelected {
607 action: action_id,
608 }));
609 }
610 MenuItem::Checkbox {
611 action,
612 checked,
613 disabled,
614 ..
615 } if !*disabled => {
616 *checked = !*checked;
617 let action_id = action.clone();
618 let is_checked = *checked;
619 return Some(Box::new(MenuCheckboxToggled {
620 action: action_id,
621 checked: is_checked,
622 }));
623 }
624 MenuItem::Submenu { disabled, .. } if !*disabled => {
625 self.open_submenu = Some(idx);
626 }
627 _ => {}
628 }
629 }
630 }
631 } else {
632 self.hide();
634 return Some(Box::new(MenuClosed));
635 }
636 }
637 }
638 Event::MouseMove { position } => {
639 if self.open {
640 let on_menu = position.x >= self.panel_bounds.x
641 && position.x <= self.panel_bounds.x + self.panel_bounds.width
642 && position.y >= self.panel_bounds.y
643 && position.y <= self.panel_bounds.y + self.panel_bounds.height;
644
645 if on_menu {
646 self.highlighted_index = self.item_at_position(position.y);
647 } else {
648 self.highlighted_index = None;
649 }
650 }
651 }
652 Event::KeyDown { key, .. } if self.open => match key {
653 Key::Escape => {
654 self.hide();
655 return Some(Box::new(MenuClosed));
656 }
657 Key::Up => {
658 self.highlighted_index = self.next_selectable(self.highlighted_index, false);
659 }
660 Key::Down => {
661 self.highlighted_index = self.next_selectable(self.highlighted_index, true);
662 }
663 Key::Enter | Key::Space => {
664 if let Some(idx) = self.highlighted_index {
665 if let Some(item) = self.items.get_mut(idx) {
666 match item {
667 MenuItem::Action {
668 action, disabled, ..
669 } if !*disabled => {
670 let action_id = action.clone();
671 self.hide();
672 return Some(Box::new(MenuItemSelected { action: action_id }));
673 }
674 MenuItem::Checkbox {
675 action,
676 checked,
677 disabled,
678 ..
679 } if !*disabled => {
680 *checked = !*checked;
681 let action_id = action.clone();
682 let is_checked = *checked;
683 return Some(Box::new(MenuCheckboxToggled {
684 action: action_id,
685 checked: is_checked,
686 }));
687 }
688 _ => {}
689 }
690 }
691 }
692 }
693 _ => {}
694 },
695 _ => {}
696 }
697
698 None
699 }
700
701 fn children(&self) -> &[Box<dyn Widget>] {
702 &[]
703 }
704
705 fn children_mut(&mut self) -> &mut [Box<dyn Widget>] {
706 &mut []
707 }
708
709 fn is_focusable(&self) -> bool {
710 true
711 }
712
713 fn test_id(&self) -> Option<&str> {
714 self.test_id_value.as_deref()
715 }
716
717 fn bounds(&self) -> Rect {
718 self.bounds
719 }
720}
721
722impl Brick for Menu {
724 fn brick_name(&self) -> &'static str {
725 "Menu"
726 }
727
728 fn assertions(&self) -> &[BrickAssertion] {
729 &[BrickAssertion::MaxLatencyMs(16)]
730 }
731
732 fn budget(&self) -> BrickBudget {
733 BrickBudget::uniform(16)
734 }
735
736 fn verify(&self) -> BrickVerification {
737 BrickVerification {
738 passed: self.assertions().to_vec(),
739 failed: vec![],
740 verification_time: Duration::from_micros(10),
741 }
742 }
743
744 fn to_html(&self) -> String {
745 r#"<div class="brick-menu"></div>"#.to_string()
746 }
747
748 fn to_css(&self) -> String {
749 ".brick-menu { display: block; position: relative; }".to_string()
750 }
751
752 fn test_id(&self) -> Option<&str> {
753 self.test_id_value.as_deref()
754 }
755}
756
757#[derive(Debug, Clone)]
759pub struct MenuToggled {
760 pub open: bool,
762}
763
764#[derive(Debug, Clone)]
766pub struct MenuItemSelected {
767 pub action: String,
769}
770
771#[derive(Debug, Clone)]
773pub struct MenuCheckboxToggled {
774 pub action: String,
776 pub checked: bool,
778}
779
780#[derive(Debug, Clone)]
782pub struct MenuClosed;
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787
788 #[test]
793 fn test_menu_item_action() {
794 let item = MenuItem::action("Cut", "edit.cut");
795 match item {
796 MenuItem::Action {
797 label,
798 action,
799 disabled,
800 shortcut,
801 } => {
802 assert_eq!(label, "Cut");
803 assert_eq!(action, "edit.cut");
804 assert!(!disabled);
805 assert!(shortcut.is_none());
806 }
807 _ => panic!("Expected Action"),
808 }
809 }
810
811 #[test]
812 fn test_menu_item_action_with_shortcut() {
813 let item = MenuItem::action("Cut", "edit.cut").shortcut("Ctrl+X");
814 match item {
815 MenuItem::Action { shortcut, .. } => {
816 assert_eq!(shortcut, Some("Ctrl+X".to_string()));
817 }
818 _ => panic!("Expected Action"),
819 }
820 }
821
822 #[test]
823 fn test_menu_item_checkbox() {
824 let item = MenuItem::checkbox("Show Grid", "view.grid", true);
825 match item {
826 MenuItem::Checkbox {
827 label,
828 checked,
829 disabled,
830 ..
831 } => {
832 assert_eq!(label, "Show Grid");
833 assert!(checked);
834 assert!(!disabled);
835 }
836 _ => panic!("Expected Checkbox"),
837 }
838 }
839
840 #[test]
841 fn test_menu_item_separator() {
842 let item = MenuItem::separator();
843 assert!(matches!(item, MenuItem::Separator));
844 }
845
846 #[test]
847 fn test_menu_item_submenu() {
848 let items = vec![MenuItem::action("Sub 1", "sub.1")];
849 let item = MenuItem::submenu("More", items);
850 match item {
851 MenuItem::Submenu {
852 label,
853 items,
854 disabled,
855 } => {
856 assert_eq!(label, "More");
857 assert_eq!(items.len(), 1);
858 assert!(!disabled);
859 }
860 _ => panic!("Expected Submenu"),
861 }
862 }
863
864 #[test]
865 fn test_menu_item_disabled() {
866 let item = MenuItem::action("Cut", "edit.cut").disabled(true);
867 match item {
868 MenuItem::Action { disabled, .. } => assert!(disabled),
869 _ => panic!("Expected Action"),
870 }
871 }
872
873 #[test]
874 fn test_menu_item_is_selectable() {
875 assert!(MenuItem::action("Cut", "edit.cut").is_selectable());
876 assert!(!MenuItem::action("Cut", "edit.cut")
877 .disabled(true)
878 .is_selectable());
879 assert!(!MenuItem::separator().is_selectable());
880 assert!(MenuItem::checkbox("Show", "show", false).is_selectable());
881 }
882
883 #[test]
884 fn test_menu_item_height() {
885 assert_eq!(MenuItem::action("Cut", "edit.cut").height(), 32.0);
886 assert_eq!(MenuItem::separator().height(), 9.0);
887 }
888
889 #[test]
894 fn test_menu_new() {
895 let menu = Menu::new();
896 assert!(menu.items.is_empty());
897 assert!(!menu.open);
898 assert_eq!(menu.trigger, MenuTrigger::Click);
899 }
900
901 #[test]
902 fn test_menu_builder() {
903 let menu = Menu::new()
904 .items(vec![
905 MenuItem::action("Cut", "cut"),
906 MenuItem::separator(),
907 MenuItem::action("Paste", "paste"),
908 ])
909 .trigger(MenuTrigger::Hover)
910 .width(250.0);
911
912 assert_eq!(menu.items.len(), 3);
913 assert_eq!(menu.trigger, MenuTrigger::Hover);
914 assert_eq!(menu.width, 250.0);
915 }
916
917 #[test]
918 fn test_menu_add_item() {
919 let menu = Menu::new()
920 .item(MenuItem::action("Cut", "cut"))
921 .item(MenuItem::action("Copy", "copy"));
922 assert_eq!(menu.items.len(), 2);
923 }
924
925 #[test]
926 fn test_menu_show_hide() {
927 let mut menu = Menu::new();
928 assert!(!menu.is_open());
929
930 menu.show();
931 assert!(menu.is_open());
932
933 menu.hide();
934 assert!(!menu.is_open());
935 }
936
937 #[test]
938 fn test_menu_toggle() {
939 let mut menu = Menu::new();
940
941 menu.toggle();
942 assert!(menu.is_open());
943
944 menu.toggle();
945 assert!(!menu.is_open());
946 }
947
948 #[test]
949 fn test_menu_calculate_height() {
950 let menu = Menu::new().items(vec![
951 MenuItem::action("Cut", "cut"),
952 MenuItem::separator(),
953 MenuItem::action("Paste", "paste"),
954 ]);
955 assert_eq!(menu.calculate_menu_height(), 89.0);
957 }
958
959 #[test]
960 fn test_menu_measure() {
961 let menu = Menu::new().width(200.0);
962 let size = menu.measure(Constraints::loose(Size::new(300.0, 400.0)));
963 assert_eq!(size.width, 200.0);
964 }
965
966 #[test]
967 fn test_menu_layout() {
968 let mut menu = Menu::new().width(200.0);
969 menu.open = true;
970 menu.items = vec![MenuItem::action("Cut", "cut")];
971
972 let result = menu.layout(Rect::new(10.0, 20.0, 100.0, 32.0));
973 assert_eq!(result.size, Size::new(100.0, 32.0));
974 assert_eq!(menu.panel_bounds.x, 10.0);
975 assert_eq!(menu.panel_bounds.y, 52.0); }
977
978 #[test]
979 fn test_menu_type_id() {
980 let menu = Menu::new();
981 assert_eq!(Widget::type_id(&menu), TypeId::of::<Menu>());
982 }
983
984 #[test]
985 fn test_menu_is_focusable() {
986 let menu = Menu::new();
987 assert!(menu.is_focusable());
988 }
989
990 #[test]
991 fn test_menu_test_id() {
992 let menu = Menu::new().with_test_id("my-menu");
993 assert_eq!(Widget::test_id(&menu), Some("my-menu"));
994 }
995
996 #[test]
997 fn test_menu_highlighted_index() {
998 let mut menu = Menu::new();
999 assert!(menu.highlighted_index().is_none());
1000
1001 menu.highlighted_index = Some(2);
1002 assert_eq!(menu.highlighted_index(), Some(2));
1003 }
1004
1005 #[test]
1006 fn test_menu_next_selectable() {
1007 let menu = Menu::new().items(vec![
1008 MenuItem::action("Cut", "cut"),
1009 MenuItem::separator(),
1010 MenuItem::action("Paste", "paste"),
1011 ]);
1012
1013 assert_eq!(menu.next_selectable(None, true), Some(0));
1015
1016 assert_eq!(menu.next_selectable(Some(0), true), Some(2)); assert_eq!(menu.next_selectable(Some(2), false), Some(0)); }
1022
1023 #[test]
1024 fn test_menu_escape_closes() {
1025 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1026 menu.show();
1027
1028 let result = menu.event(&Event::key_down(Key::Escape));
1029 assert!(result.is_some());
1030 assert!(!menu.is_open());
1031 }
1032
1033 #[test]
1034 fn test_menu_arrow_navigation() {
1035 let mut menu = Menu::new().items(vec![
1036 MenuItem::action("Cut", "cut"),
1037 MenuItem::action("Copy", "copy"),
1038 ]);
1039 menu.show();
1040
1041 menu.event(&Event::key_down(Key::Down));
1042 assert_eq!(menu.highlighted_index, Some(0));
1043
1044 menu.event(&Event::key_down(Key::Down));
1045 assert_eq!(menu.highlighted_index, Some(1));
1046 }
1047
1048 #[test]
1053 fn test_menu_toggled_message() {
1054 let msg = MenuToggled { open: true };
1055 assert!(msg.open);
1056 }
1057
1058 #[test]
1059 fn test_menu_item_selected_message() {
1060 let msg = MenuItemSelected {
1061 action: "edit.cut".to_string(),
1062 };
1063 assert_eq!(msg.action, "edit.cut");
1064 }
1065
1066 #[test]
1067 fn test_menu_checkbox_toggled_message() {
1068 let msg = MenuCheckboxToggled {
1069 action: "view.grid".to_string(),
1070 checked: true,
1071 };
1072 assert_eq!(msg.action, "view.grid");
1073 assert!(msg.checked);
1074 }
1075
1076 #[test]
1077 fn test_menu_closed_message() {
1078 let msg = MenuClosed;
1079 assert_eq!(format!("{msg:?}"), "MenuClosed");
1080 }
1081
1082 #[test]
1087 fn test_menu_shortcut_on_non_action() {
1088 let item = MenuItem::checkbox("Show", "show", false).shortcut("Ctrl+S");
1090 match item {
1091 MenuItem::Checkbox { .. } => {} _ => panic!("Expected Checkbox"),
1093 }
1094 }
1095
1096 #[test]
1097 fn test_menu_disabled_checkbox() {
1098 let item = MenuItem::checkbox("Show", "show", true).disabled(true);
1099 match item {
1100 MenuItem::Checkbox { disabled, .. } => assert!(disabled),
1101 _ => panic!("Expected Checkbox"),
1102 }
1103 }
1104
1105 #[test]
1106 fn test_menu_disabled_submenu() {
1107 let item = MenuItem::submenu("More", vec![]).disabled(true);
1108 match item {
1109 MenuItem::Submenu { disabled, .. } => assert!(disabled),
1110 _ => panic!("Expected Submenu"),
1111 }
1112 }
1113
1114 #[test]
1115 fn test_menu_disabled_separator_no_op() {
1116 let item = MenuItem::separator().disabled(true);
1118 assert!(matches!(item, MenuItem::Separator));
1119 }
1120
1121 #[test]
1122 fn test_menu_submenu_not_selectable_when_disabled() {
1123 let item = MenuItem::submenu("More", vec![]).disabled(true);
1124 assert!(!item.is_selectable());
1125 }
1126
1127 #[test]
1128 fn test_menu_context_menu_trigger() {
1129 let menu = Menu::new().trigger(MenuTrigger::ContextMenu);
1130 assert_eq!(menu.trigger, MenuTrigger::ContextMenu);
1131 }
1132
1133 #[test]
1134 fn test_menu_hover_trigger() {
1135 let menu = Menu::new().trigger(MenuTrigger::Hover);
1136 assert_eq!(menu.trigger, MenuTrigger::Hover);
1137 }
1138
1139 #[test]
1140 fn test_menu_background_color() {
1141 let menu = Menu::new().background_color(Color::RED);
1142 assert_eq!(menu.background_color, Color::RED);
1143 }
1144
1145 #[test]
1146 fn test_menu_hover_color() {
1147 let menu = Menu::new().hover_color(Color::BLUE);
1148 assert_eq!(menu.hover_color, Color::BLUE);
1149 }
1150
1151 #[test]
1152 fn test_menu_text_color() {
1153 let menu = Menu::new().text_color(Color::GREEN);
1154 assert_eq!(menu.text_color, Color::GREEN);
1155 }
1156
1157 #[test]
1158 fn test_menu_next_selectable_empty() {
1159 let menu = Menu::new();
1160 assert!(menu.next_selectable(None, true).is_none());
1161 assert!(menu.next_selectable(None, false).is_none());
1162 }
1163
1164 #[test]
1165 fn test_menu_next_selectable_all_disabled() {
1166 let menu = Menu::new().items(vec![
1167 MenuItem::separator(),
1168 MenuItem::action("Cut", "cut").disabled(true),
1169 MenuItem::separator(),
1170 ]);
1171 assert!(menu.next_selectable(None, true).is_none());
1172 }
1173
1174 #[test]
1175 fn test_menu_next_selectable_wrap_forward() {
1176 let menu = Menu::new().items(vec![MenuItem::action("A", "a"), MenuItem::action("B", "b")]);
1177 assert_eq!(menu.next_selectable(Some(1), true), Some(0));
1179 }
1180
1181 #[test]
1182 fn test_menu_next_selectable_wrap_backward() {
1183 let menu = Menu::new().items(vec![MenuItem::action("A", "a"), MenuItem::action("B", "b")]);
1184 assert_eq!(menu.next_selectable(Some(0), false), Some(1));
1186 }
1187
1188 #[test]
1189 fn test_menu_children_empty() {
1190 let menu = Menu::new();
1191 assert!(menu.children().is_empty());
1192 }
1193
1194 #[test]
1195 fn test_menu_children_mut_empty() {
1196 let mut menu = Menu::new();
1197 assert!(menu.children_mut().is_empty());
1198 }
1199
1200 #[test]
1201 fn test_menu_bounds() {
1202 let mut menu = Menu::new();
1203 menu.layout(Rect::new(10.0, 20.0, 200.0, 32.0));
1204 assert_eq!(menu.bounds(), Rect::new(10.0, 20.0, 200.0, 32.0));
1205 }
1206
1207 #[test]
1208 fn test_menu_trigger_default() {
1209 assert_eq!(MenuTrigger::default(), MenuTrigger::Click);
1210 }
1211
1212 #[test]
1213 fn test_menu_event_closed_returns_none() {
1214 let mut menu = Menu::new();
1215 let result = menu.event(&Event::key_down(Key::Down));
1217 assert!(result.is_none());
1218 }
1219
1220 #[test]
1221 fn test_menu_enter_selects_item() {
1222 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1223 menu.show();
1224 menu.highlighted_index = Some(0);
1225 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1226
1227 let result = menu.event(&Event::key_down(Key::Enter));
1228 assert!(result.is_some());
1229 assert!(!menu.is_open());
1230 }
1231
1232 #[test]
1233 fn test_menu_space_selects_item() {
1234 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1235 menu.show();
1236 menu.highlighted_index = Some(0);
1237 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1238
1239 let result = menu.event(&Event::key_down(Key::Space));
1240 assert!(result.is_some());
1241 }
1242
1243 #[test]
1244 fn test_menu_enter_on_checkbox_toggles() {
1245 let mut menu = Menu::new().items(vec![MenuItem::checkbox("Show", "show", false)]);
1246 menu.show();
1247 menu.highlighted_index = Some(0);
1248 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1249
1250 let result = menu.event(&Event::key_down(Key::Enter));
1251 assert!(result.is_some());
1252 assert!(menu.is_open());
1254 }
1255
1256 #[test]
1257 fn test_menu_enter_on_disabled_does_nothing() {
1258 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut").disabled(true)]);
1259 menu.show();
1260 menu.highlighted_index = Some(0);
1261 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1262
1263 let result = menu.event(&Event::key_down(Key::Enter));
1264 assert!(result.is_none());
1265 assert!(menu.is_open()); }
1267
1268 #[test]
1269 fn test_menu_up_arrow_navigation() {
1270 let mut menu =
1271 Menu::new().items(vec![MenuItem::action("A", "a"), MenuItem::action("B", "b")]);
1272 menu.show();
1273 menu.highlighted_index = Some(1);
1274
1275 menu.event(&Event::key_down(Key::Up));
1276 assert_eq!(menu.highlighted_index, Some(0));
1277 }
1278
1279 #[test]
1284 fn test_menu_brick_name() {
1285 let menu = Menu::new();
1286 assert_eq!(menu.brick_name(), "Menu");
1287 }
1288
1289 #[test]
1290 fn test_menu_brick_assertions() {
1291 let menu = Menu::new();
1292 let assertions = menu.assertions();
1293 assert!(!assertions.is_empty());
1294 assert!(matches!(assertions[0], BrickAssertion::MaxLatencyMs(16)));
1295 }
1296
1297 #[test]
1298 fn test_menu_brick_budget() {
1299 let menu = Menu::new();
1300 let budget = menu.budget();
1301 assert!(budget.layout_ms > 0);
1303 assert!(budget.paint_ms > 0);
1304 }
1305
1306 #[test]
1307 fn test_menu_brick_verify() {
1308 let menu = Menu::new();
1309 let verification = menu.verify();
1310 assert!(!verification.passed.is_empty());
1311 assert!(verification.failed.is_empty());
1312 }
1313
1314 #[test]
1315 fn test_menu_brick_to_html() {
1316 let menu = Menu::new();
1317 let html = menu.to_html();
1318 assert!(html.contains("brick-menu"));
1319 }
1320
1321 #[test]
1322 fn test_menu_brick_to_css() {
1323 let menu = Menu::new();
1324 let css = menu.to_css();
1325 assert!(css.contains(".brick-menu"));
1326 assert!(css.contains("display: block"));
1327 assert!(css.contains("position: relative"));
1328 }
1329
1330 #[test]
1331 fn test_menu_brick_test_id() {
1332 let menu = Menu::new().with_test_id("my-menu");
1333 assert_eq!(Brick::test_id(&menu), Some("my-menu"));
1334 }
1335
1336 #[test]
1337 fn test_menu_brick_test_id_none() {
1338 let menu = Menu::new();
1339 assert!(Brick::test_id(&menu).is_none());
1340 }
1341
1342 #[test]
1347 fn test_menu_item_at_position_valid() {
1348 let mut menu = Menu::new().items(vec![
1349 MenuItem::action("Cut", "cut"),
1350 MenuItem::action("Copy", "copy"),
1351 MenuItem::action("Paste", "paste"),
1352 ]);
1353 menu.open = true;
1354 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1355
1356 let item = menu.item_at_position(menu.panel_bounds.y + 8.0 + 10.0);
1359 assert_eq!(item, Some(0));
1360
1361 let item = menu.item_at_position(menu.panel_bounds.y + 8.0 + 40.0);
1362 assert_eq!(item, Some(1));
1363
1364 let item = menu.item_at_position(menu.panel_bounds.y + 8.0 + 72.0);
1365 assert_eq!(item, Some(2));
1366 }
1367
1368 #[test]
1369 fn test_menu_item_at_position_above_menu() {
1370 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1371 menu.open = true;
1372 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1373
1374 let item = menu.item_at_position(menu.panel_bounds.y - 10.0);
1376 assert!(item.is_none());
1377 }
1378
1379 #[test]
1380 fn test_menu_item_at_position_below_items() {
1381 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1382 menu.open = true;
1383 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1384
1385 let item = menu.item_at_position(menu.panel_bounds.y + 500.0);
1387 assert!(item.is_none());
1388 }
1389
1390 #[test]
1395 fn test_menu_click_on_trigger_opens() {
1396 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1397 menu.layout(Rect::new(10.0, 10.0, 200.0, 32.0));
1398
1399 let result = menu.event(&Event::MouseDown {
1401 position: Point::new(50.0, 20.0),
1402 button: presentar_core::MouseButton::Left,
1403 });
1404
1405 assert!(result.is_some());
1406 assert!(menu.is_open());
1407 }
1408
1409 #[test]
1410 fn test_menu_click_outside_closes() {
1411 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1412 menu.show();
1413 menu.layout(Rect::new(10.0, 10.0, 200.0, 32.0));
1414
1415 let result = menu.event(&Event::MouseDown {
1417 position: Point::new(500.0, 500.0),
1418 button: presentar_core::MouseButton::Left,
1419 });
1420
1421 assert!(result.is_some());
1422 assert!(!menu.is_open());
1423 }
1424
1425 #[test]
1426 fn test_menu_click_on_action_item() {
1427 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
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; 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_some());
1439 assert!(!menu.is_open()); }
1441
1442 #[test]
1443 fn test_menu_click_on_checkbox_item() {
1444 let mut menu = Menu::new().items(vec![MenuItem::checkbox("Show Grid", "show", false)]);
1445 menu.show();
1446 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1447
1448 let click_y = menu.panel_bounds.y + 8.0 + 16.0;
1450 let result = menu.event(&Event::MouseDown {
1451 position: Point::new(menu.panel_bounds.x + 50.0, click_y),
1452 button: presentar_core::MouseButton::Left,
1453 });
1454
1455 assert!(result.is_some());
1456 if let MenuItem::Checkbox { checked, .. } = &menu.items[0] {
1458 assert!(*checked);
1459 } else {
1460 panic!("Expected Checkbox item");
1461 }
1462 }
1463
1464 #[test]
1465 fn test_menu_click_on_disabled_action() {
1466 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut").disabled(true)]);
1467 menu.show();
1468 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1469
1470 let click_y = menu.panel_bounds.y + 8.0 + 16.0;
1472 let result = menu.event(&Event::MouseDown {
1473 position: Point::new(menu.panel_bounds.x + 50.0, click_y),
1474 button: presentar_core::MouseButton::Left,
1475 });
1476
1477 assert!(result.is_none()); assert!(menu.is_open()); }
1480
1481 #[test]
1482 fn test_menu_click_on_submenu_opens_it() {
1483 let mut menu = Menu::new().items(vec![MenuItem::submenu(
1484 "More",
1485 vec![MenuItem::action("Sub", "sub")],
1486 )]);
1487 menu.show();
1488 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1489
1490 let click_y = menu.panel_bounds.y + 8.0 + 16.0;
1492 let result = menu.event(&Event::MouseDown {
1493 position: Point::new(menu.panel_bounds.x + 50.0, click_y),
1494 button: presentar_core::MouseButton::Left,
1495 });
1496
1497 assert!(result.is_none()); assert_eq!(menu.open_submenu, Some(0));
1499 }
1500
1501 #[test]
1502 fn test_menu_mouse_move_updates_highlight() {
1503 let mut menu = Menu::new().items(vec![
1504 MenuItem::action("Cut", "cut"),
1505 MenuItem::action("Copy", "copy"),
1506 ]);
1507 menu.show();
1508 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1509
1510 let y1 = menu.panel_bounds.y + 8.0 + 16.0;
1512 menu.event(&Event::MouseMove {
1513 position: Point::new(menu.panel_bounds.x + 50.0, y1),
1514 });
1515 assert_eq!(menu.highlighted_index, Some(0));
1516
1517 let y2 = menu.panel_bounds.y + 8.0 + 48.0;
1519 menu.event(&Event::MouseMove {
1520 position: Point::new(menu.panel_bounds.x + 50.0, y2),
1521 });
1522 assert_eq!(menu.highlighted_index, Some(1));
1523 }
1524
1525 #[test]
1526 fn test_menu_mouse_move_outside_clears_highlight() {
1527 let mut menu = Menu::new().items(vec![MenuItem::action("Cut", "cut")]);
1528 menu.show();
1529 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1530 menu.highlighted_index = Some(0);
1531
1532 menu.event(&Event::MouseMove {
1534 position: Point::new(500.0, 500.0),
1535 });
1536 assert!(menu.highlighted_index.is_none());
1537 }
1538
1539 #[test]
1544 fn test_menu_up_from_none_selects_last() {
1545 let mut menu =
1546 Menu::new().items(vec![MenuItem::action("A", "a"), MenuItem::action("B", "b")]);
1547 menu.show();
1548
1549 menu.event(&Event::key_down(Key::Up));
1550 assert_eq!(menu.highlighted_index, Some(1)); }
1552
1553 #[test]
1554 fn test_menu_down_from_last_wraps_to_first() {
1555 let mut menu =
1556 Menu::new().items(vec![MenuItem::action("A", "a"), MenuItem::action("B", "b")]);
1557 menu.show();
1558 menu.highlighted_index = Some(1);
1559
1560 menu.event(&Event::key_down(Key::Down));
1561 assert_eq!(menu.highlighted_index, Some(0)); }
1563
1564 #[test]
1565 fn test_menu_up_skips_separator() {
1566 let mut menu = Menu::new().items(vec![
1567 MenuItem::action("A", "a"),
1568 MenuItem::separator(),
1569 MenuItem::action("B", "b"),
1570 ]);
1571 menu.show();
1572 menu.highlighted_index = Some(2);
1573
1574 menu.event(&Event::key_down(Key::Up));
1575 assert_eq!(menu.highlighted_index, Some(0)); }
1577
1578 #[test]
1579 fn test_menu_down_skips_disabled() {
1580 let mut menu = Menu::new().items(vec![
1581 MenuItem::action("A", "a"),
1582 MenuItem::action("B", "b").disabled(true),
1583 MenuItem::action("C", "c"),
1584 ]);
1585 menu.show();
1586 menu.highlighted_index = Some(0);
1587
1588 menu.event(&Event::key_down(Key::Down));
1589 assert_eq!(menu.highlighted_index, Some(2)); }
1591
1592 #[test]
1593 fn test_menu_other_key_does_nothing() {
1594 let mut menu = Menu::new().items(vec![MenuItem::action("A", "a")]);
1595 menu.show();
1596 menu.highlighted_index = Some(0);
1597
1598 let result = menu.event(&Event::key_down(Key::Tab));
1599 assert!(result.is_none());
1600 assert_eq!(menu.highlighted_index, Some(0));
1601 }
1602
1603 #[test]
1604 fn test_menu_enter_on_separator_does_nothing() {
1605 let mut menu = Menu::new().items(vec![MenuItem::separator(), MenuItem::action("A", "a")]);
1606 menu.show();
1607 menu.highlighted_index = Some(0); let result = menu.event(&Event::key_down(Key::Enter));
1610 assert!(result.is_none());
1611 assert!(menu.is_open());
1612 }
1613
1614 #[test]
1615 fn test_menu_enter_on_submenu_does_nothing() {
1616 let mut menu = Menu::new().items(vec![MenuItem::submenu(
1617 "More",
1618 vec![MenuItem::action("Sub", "sub")],
1619 )]);
1620 menu.show();
1621 menu.highlighted_index = Some(0);
1622
1623 let result = menu.event(&Event::key_down(Key::Enter));
1624 assert!(result.is_none());
1625 assert!(menu.is_open());
1626 }
1627
1628 #[test]
1629 fn test_menu_space_on_checkbox_toggles() {
1630 let mut menu = Menu::new().items(vec![MenuItem::checkbox("Show", "show", false)]);
1631 menu.show();
1632 menu.highlighted_index = Some(0);
1633 menu.layout(Rect::new(0.0, 0.0, 200.0, 32.0));
1634
1635 let result = menu.event(&Event::key_down(Key::Space));
1636 assert!(result.is_some());
1637 if let MenuItem::Checkbox { checked, .. } = &menu.items[0] {
1639 assert!(*checked);
1640 }
1641 }
1642
1643 #[test]
1648 fn test_menu_item_height_action() {
1649 let item = MenuItem::action("Test", "test");
1650 assert_eq!(item.height(), 32.0);
1651 }
1652
1653 #[test]
1654 fn test_menu_item_height_checkbox() {
1655 let item = MenuItem::checkbox("Test", "test", false);
1656 assert_eq!(item.height(), 32.0);
1657 }
1658
1659 #[test]
1660 fn test_menu_item_height_submenu() {
1661 let item = MenuItem::submenu("More", vec![]);
1662 assert_eq!(item.height(), 32.0);
1663 }
1664
1665 #[test]
1666 fn test_menu_item_is_selectable_submenu() {
1667 let item = MenuItem::submenu("More", vec![]);
1668 assert!(item.is_selectable());
1669 }
1670
1671 #[test]
1672 fn test_menu_item_is_selectable_disabled_checkbox() {
1673 let item = MenuItem::checkbox("Test", "test", false).disabled(true);
1674 assert!(!item.is_selectable());
1675 }
1676
1677 #[test]
1682 fn test_menu_trigger_hover_no_click_open() {
1683 let mut menu = Menu::new()
1684 .trigger(MenuTrigger::Hover)
1685 .items(vec![MenuItem::action("Cut", "cut")]);
1686 menu.layout(Rect::new(10.0, 10.0, 200.0, 32.0));
1687
1688 let result = menu.event(&Event::MouseDown {
1690 position: Point::new(50.0, 20.0),
1691 button: presentar_core::MouseButton::Left,
1692 });
1693
1694 assert!(result.is_none());
1695 assert!(!menu.is_open());
1696 }
1697
1698 #[test]
1699 fn test_menu_trigger_context_menu_no_click_open() {
1700 let mut menu = Menu::new()
1701 .trigger(MenuTrigger::ContextMenu)
1702 .items(vec![MenuItem::action("Cut", "cut")]);
1703 menu.layout(Rect::new(10.0, 10.0, 200.0, 32.0));
1704
1705 let result = menu.event(&Event::MouseDown {
1707 position: Point::new(50.0, 20.0),
1708 button: presentar_core::MouseButton::Left,
1709 });
1710
1711 assert!(result.is_none());
1712 assert!(!menu.is_open());
1713 }
1714
1715 #[test]
1720 fn test_menu_toggled_clone() {
1721 let msg = MenuToggled { open: true };
1722 let cloned = msg.clone();
1723 assert_eq!(cloned.open, msg.open);
1724 }
1725
1726 #[test]
1727 fn test_menu_item_selected_clone() {
1728 let msg = MenuItemSelected {
1729 action: "test".to_string(),
1730 };
1731 let cloned = msg.clone();
1732 assert_eq!(cloned.action, msg.action);
1733 }
1734
1735 #[test]
1736 fn test_menu_checkbox_toggled_clone() {
1737 let msg = MenuCheckboxToggled {
1738 action: "test".to_string(),
1739 checked: true,
1740 };
1741 let cloned = msg.clone();
1742 assert_eq!(cloned.action, msg.action);
1743 assert_eq!(cloned.checked, msg.checked);
1744 }
1745
1746 #[test]
1747 fn test_menu_closed_clone() {
1748 let msg = MenuClosed;
1749 let cloned = msg;
1750 assert_eq!(format!("{cloned:?}"), "MenuClosed");
1751 }
1752
1753 #[test]
1758 fn test_menu_default() {
1759 let menu = Menu::default();
1760 assert!(menu.items.is_empty());
1761 assert!(!menu.open);
1762 assert_eq!(menu.trigger, MenuTrigger::Click);
1763 assert_eq!(menu.width, 200.0);
1764 }
1765
1766 #[test]
1767 fn test_menu_trigger_eq() {
1768 assert_eq!(MenuTrigger::Click, MenuTrigger::Click);
1769 assert_ne!(MenuTrigger::Click, MenuTrigger::Hover);
1770 assert_ne!(MenuTrigger::Hover, MenuTrigger::ContextMenu);
1771 }
1772
1773 #[test]
1774 fn test_menu_hide_clears_submenu() {
1775 let mut menu = Menu::new().items(vec![MenuItem::submenu(
1776 "More",
1777 vec![MenuItem::action("Sub", "sub")],
1778 )]);
1779 menu.show();
1780 menu.open_submenu = Some(0);
1781
1782 menu.hide();
1783 assert!(!menu.is_open());
1784 assert!(menu.open_submenu.is_none());
1785 assert!(menu.highlighted_index.is_none());
1786 }
1787
1788 #[test]
1789 fn test_menu_debug() {
1790 let item = MenuItem::action("Test", "test");
1791 let debug_str = format!("{item:?}");
1792 assert!(debug_str.contains("Test"));
1793 }
1794
1795 #[test]
1796 fn test_menu_toggled_debug() {
1797 let msg = MenuToggled { open: true };
1798 let debug_str = format!("{msg:?}");
1799 assert!(debug_str.contains("true"));
1800 }
1801}