1#![warn(missing_docs)]
25
26use crate::{
27 border::BorderBuilder,
28 brush::Brush,
29 core::{
30 algebra::Vector2, color::Color, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
31 uuid_provider, variable::InheritableVariable, visitor::prelude::*,
32 },
33 decorator::{DecoratorBuilder, DecoratorMessage},
34 define_constructor,
35 draw::DrawingContext,
36 grid::{Column, GridBuilder, Row},
37 message::{ButtonState, KeyCode, MessageDirection, OsEvent, UiMessage},
38 popup::{Placement, Popup, PopupBuilder, PopupMessage},
39 stack_panel::StackPanelBuilder,
40 style::{resource::StyleResourceExt, Style},
41 text::TextBuilder,
42 utils::{make_arrow_primitives, ArrowDirection},
43 vector_image::VectorImageBuilder,
44 widget::{self, Widget, WidgetBuilder, WidgetMessage},
45 BuildContext, Control, HorizontalAlignment, Orientation, RestrictionEntry, Thickness, UiNode,
46 UserInterface, VerticalAlignment,
47};
48use fyrox_graph::{
49 constructor::{ConstructorProvider, GraphNodeConstructor},
50 BaseSceneGraph, SceneGraph, SceneGraphNode,
51};
52use std::{
53 any::TypeId,
54 cmp::Ordering,
55 fmt::{Debug, Formatter},
56 ops::{Deref, DerefMut},
57 sync::{mpsc::Sender, Arc},
58};
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum MenuMessage {
63 Activate,
66 Deactivate,
68}
69
70impl MenuMessage {
71 define_constructor!(
72 MenuMessage:Activate => fn activate(), layout: false
74 );
75 define_constructor!(
76 MenuMessage:Deactivate => fn deactivate(), layout: false
78 );
79}
80
81#[derive(Clone)]
83pub struct SortingPredicate(
84 pub Arc<dyn Fn(&MenuItemContent, &MenuItemContent, &UserInterface) -> Ordering + Send + Sync>,
85);
86
87impl SortingPredicate {
88 pub fn new<F>(func: F) -> Self
90 where
91 F: Fn(&MenuItemContent, &MenuItemContent, &UserInterface) -> Ordering
92 + Send
93 + Sync
94 + 'static,
95 {
96 Self(Arc::new(func))
97 }
98
99 pub fn sort_by_text() -> Self {
102 Self::new(|a, b, _| {
103 if let MenuItemContent::Text { text: a_text, .. } = a {
104 if let MenuItemContent::Text { text: b_text, .. } = b {
105 return a_text.cmp(b_text);
106 }
107 }
108
109 if let MenuItemContent::TextCentered(a_text) = a {
110 if let MenuItemContent::TextCentered(b_text) = b {
111 return a_text.cmp(b_text);
112 }
113 }
114
115 Ordering::Equal
116 })
117 }
118}
119
120impl Debug for SortingPredicate {
121 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
122 write!(f, "SortingPredicate")
123 }
124}
125
126impl PartialEq for SortingPredicate {
127 fn eq(&self, other: &Self) -> bool {
128 std::ptr::eq(self.0.as_ref(), other.0.as_ref())
129 }
130}
131
132#[derive(Debug, Clone, PartialEq)]
134pub enum MenuItemMessage {
135 Open,
137 Close {
139 deselect: bool,
141 },
142 Click,
144 AddItem(Handle<UiNode>),
146 RemoveItem(Handle<UiNode>),
148 Items(Vec<Handle<UiNode>>),
150 Select(bool),
152 Sort(SortingPredicate),
154}
155
156impl MenuItemMessage {
157 define_constructor!(
158 MenuItemMessage:Open => fn open(), layout: false
160 );
161 define_constructor!(
162 MenuItemMessage:Close => fn close(deselect: bool), layout: false
164 );
165 define_constructor!(
166 MenuItemMessage:Click => fn click(), layout: false
168 );
169 define_constructor!(
170 MenuItemMessage:AddItem => fn add_item(Handle<UiNode>), layout: false
172 );
173 define_constructor!(
174 MenuItemMessage:RemoveItem => fn remove_item(Handle<UiNode>), layout: false
176 );
177 define_constructor!(
178 MenuItemMessage:Items => fn items(Vec<Handle<UiNode>>), layout: false
180 );
181 define_constructor!(
182 MenuItemMessage:Select => fn select(bool), layout: false
184 );
185 define_constructor!(
186 MenuItemMessage:Sort => fn sort(SortingPredicate), layout: false
188 );
189}
190
191#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
243#[reflect(derived_type = "UiNode")]
244pub struct Menu {
245 widget: Widget,
246 active: bool,
247 #[component(include)]
248 items: ItemsContainer,
249 pub restrict_picking: InheritableVariable<bool>,
251}
252
253impl ConstructorProvider<UiNode, UserInterface> for Menu {
254 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
255 GraphNodeConstructor::new::<Self>()
256 .with_variant("Menu", |ui| {
257 MenuBuilder::new(WidgetBuilder::new().with_name("Menu"))
258 .build(&mut ui.build_ctx())
259 .into()
260 })
261 .with_group("Input")
262 }
263}
264
265crate::define_widget_deref!(Menu);
266
267uuid_provider!(Menu = "582a04f3-a7fd-4e70-bbd1-eb95e2275b75");
268
269impl Control for Menu {
270 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
271 self.widget.handle_routed_message(ui, message);
272
273 if let Some(msg) = message.data::<MenuMessage>() {
274 match msg {
275 MenuMessage::Activate => {
276 if !self.active {
277 if *self.restrict_picking {
278 ui.push_picking_restriction(RestrictionEntry {
279 handle: self.handle(),
280 stop: false,
281 });
282 }
283 self.active = true;
284 }
285 }
286 MenuMessage::Deactivate => {
287 if self.active {
288 self.active = false;
289
290 if *self.restrict_picking {
291 ui.remove_picking_restriction(self.handle());
292 }
293
294 let mut stack = self.children().to_vec();
296 while let Some(handle) = stack.pop() {
297 let node = ui.node(handle);
298 if let Some(item) = node.cast::<MenuItem>() {
299 ui.send_message(MenuItemMessage::close(
300 handle,
301 MessageDirection::ToWidget,
302 true,
303 ));
304 stack.push(*item.items_panel);
307 }
308 stack.extend_from_slice(node.children());
310 }
311 }
312 }
313 }
314 } else if let Some(WidgetMessage::KeyDown(key_code)) = message.data() {
315 if !message.handled() {
316 if keyboard_navigation(ui, *key_code, self, self.handle) {
317 message.set_handled(true);
318 } else if *key_code == KeyCode::Escape {
319 ui.send_message(MenuMessage::deactivate(
320 self.handle,
321 MessageDirection::ToWidget,
322 ));
323 message.set_handled(true);
324 }
325 }
326 }
327 }
328
329 fn handle_os_event(
330 &mut self,
331 _self_handle: Handle<UiNode>,
332 ui: &mut UserInterface,
333 event: &OsEvent,
334 ) {
335 if let OsEvent::MouseInput { state, .. } = event {
340 if *state == ButtonState::Pressed && self.active {
341 let pos = ui.cursor_position();
343 if !self.widget.screen_bounds().contains(pos) {
344 let mut any_picked = false;
347 let mut stack = self.children().to_vec();
348 'depth_search: while let Some(handle) = stack.pop() {
349 let node = ui.node(handle);
350 if let Some(item) = node.cast::<MenuItem>() {
351 let popup = ui.node(*item.items_panel);
352 if popup.screen_bounds().contains(pos) && popup.is_globally_visible() {
353 any_picked = true;
357 break 'depth_search;
358 }
359 stack.push(*item.items_panel);
362 }
363 stack.extend_from_slice(node.children());
365 }
366
367 if !any_picked {
368 ui.send_message(MenuMessage::deactivate(
369 self.handle(),
370 MessageDirection::ToWidget,
371 ));
372 }
373 }
374 }
375 }
376 }
377}
378
379#[derive(Copy, Clone, PartialOrd, PartialEq, Eq, Hash, Visit, Reflect, Default, Debug)]
381pub enum MenuItemPlacement {
382 Bottom,
384 #[default]
386 Right,
387}
388
389#[derive(Copy, Clone, PartialOrd, PartialEq, Eq, Hash, Visit, Reflect, Default, Debug)]
390enum NavigationDirection {
391 #[default]
392 Horizontal,
393 Vertical,
394}
395
396#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
397#[doc(hidden)]
398pub struct ItemsContainer {
399 #[doc(hidden)]
400 pub items: InheritableVariable<Vec<Handle<UiNode>>>,
401 navigation_direction: NavigationDirection,
402}
403
404impl Deref for ItemsContainer {
405 type Target = Vec<Handle<UiNode>>;
406
407 fn deref(&self) -> &Self::Target {
408 self.items.deref()
409 }
410}
411
412impl DerefMut for ItemsContainer {
413 fn deref_mut(&mut self) -> &mut Self::Target {
414 self.items.deref_mut()
415 }
416}
417
418impl ItemsContainer {
419 fn selected_item_index(&self, ui: &UserInterface) -> Option<usize> {
420 for (index, item) in self.items.iter().enumerate() {
421 if let Some(item_ref) = ui.try_get_of_type::<MenuItem>(*item) {
422 if *item_ref.is_selected {
423 return Some(index);
424 }
425 }
426 }
427
428 None
429 }
430
431 fn next_item_to_select_in_dir(&self, ui: &UserInterface, dir: isize) -> Option<Handle<UiNode>> {
432 self.selected_item_index(ui)
433 .map(|i| i as isize)
434 .and_then(|mut index| {
435 let count = self.items.len() as isize;
437 for _ in 0..count {
438 index += dir;
439 if index < 0 {
440 index += count;
441 }
442 index %= count;
443 let handle = self.items.get(index as usize).cloned();
444 if let Some(item) = handle.and_then(|h| ui.try_get_of_type::<MenuItem>(h)) {
445 if item.enabled() {
446 return handle;
447 }
448 }
449 }
450
451 None
452 })
453 }
454}
455
456#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
459#[reflect(derived_type = "UiNode")]
460pub struct MenuItem {
461 pub widget: Widget,
463 #[component(include)]
465 pub items_container: ItemsContainer,
466 pub items_panel: InheritableVariable<Handle<UiNode>>,
468 pub panel: InheritableVariable<Handle<UiNode>>,
470 pub placement: InheritableVariable<MenuItemPlacement>,
472 pub clickable_when_not_empty: InheritableVariable<bool>,
474 pub decorator: InheritableVariable<Handle<UiNode>>,
476 pub is_selected: InheritableVariable<bool>,
478 pub arrow: InheritableVariable<Handle<UiNode>>,
480 pub content: InheritableVariable<Option<MenuItemContent>>,
482}
483
484impl ConstructorProvider<UiNode, UserInterface> for MenuItem {
485 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
486 GraphNodeConstructor::new::<Self>()
487 .with_variant("Menu Item", |ui| {
488 MenuItemBuilder::new(WidgetBuilder::new().with_name("Menu Item"))
489 .build(&mut ui.build_ctx())
490 .into()
491 })
492 .with_group("Input")
493 }
494}
495
496crate::define_widget_deref!(MenuItem);
497
498impl MenuItem {
499 fn is_opened(&self, ui: &UserInterface) -> bool {
500 ui.try_get_of_type::<ContextMenu>(*self.items_panel)
501 .is_some_and(|items_panel| *items_panel.popup.is_open)
502 }
503
504 fn sync_arrow_visibility(&self, ui: &UserInterface) {
505 ui.send_message(WidgetMessage::visibility(
506 *self.arrow,
507 MessageDirection::ToWidget,
508 !self.items_container.is_empty(),
509 ));
510 }
511}
512
513fn find_menu(from: Handle<UiNode>, ui: &UserInterface) -> Handle<UiNode> {
519 let mut handle = from;
520 while handle.is_some() {
521 if let Some((_, panel)) = ui.find_component_up::<ContextMenu>(handle) {
522 handle = panel.parent_menu_item;
524 } else {
525 return ui.find_handle_up(handle, &mut |n| n.cast::<Menu>().is_some());
527 }
528 }
529 Default::default()
530}
531
532fn is_any_menu_item_contains_point(ui: &UserInterface, pt: Vector2<f32>) -> bool {
533 for (handle, menu) in ui
534 .nodes()
535 .pair_iter()
536 .filter_map(|(h, n)| n.query_component::<MenuItem>().map(|menu| (h, menu)))
537 {
538 if ui.find_component_up::<Menu>(handle).is_none()
539 && menu.is_globally_visible()
540 && menu.screen_bounds().contains(pt)
541 {
542 return true;
543 }
544 }
545 false
546}
547
548fn close_menu_chain(from: Handle<UiNode>, ui: &UserInterface) {
549 let mut handle = from;
550 while handle.is_some() {
551 let popup_handle = ui.find_handle_up(handle, &mut |n| n.has_component::<ContextMenu>());
552
553 if let Some(panel) = ui.try_get_of_type::<ContextMenu>(popup_handle) {
554 if *panel.popup.is_open {
555 ui.send_message(PopupMessage::close(
556 popup_handle,
557 MessageDirection::ToWidget,
558 ));
559 }
560
561 handle = panel.parent_menu_item;
563 } else {
564 break;
566 }
567 }
568}
569
570uuid_provider!(MenuItem = "72e002c6-6060-4583-b5b7-0c5500244fef");
571
572impl Control for MenuItem {
573 fn on_remove(&self, sender: &Sender<UiMessage>) {
574 sender
577 .send(WidgetMessage::remove(
578 *self.items_panel,
579 MessageDirection::ToWidget,
580 ))
581 .unwrap();
582 }
583
584 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
585 self.widget.handle_routed_message(ui, message);
586
587 if let Some(msg) = message.data::<WidgetMessage>() {
588 match msg {
589 WidgetMessage::MouseDown { .. } => {
590 let menu = find_menu(self.parent(), ui);
591 if menu.is_some() {
592 if self.is_opened(ui) {
593 ui.send_message(MenuItemMessage::close(
594 self.handle(),
595 MessageDirection::ToWidget,
596 true,
597 ));
598 ui.send_message(MenuMessage::deactivate(
599 menu,
600 MessageDirection::ToWidget,
601 ));
602 } else {
603 ui.send_message(MenuMessage::activate(
606 menu,
607 MessageDirection::ToWidget,
608 ));
609
610 ui.send_message(MenuItemMessage::open(
611 self.handle(),
612 MessageDirection::ToWidget,
613 ));
614 }
615 }
616 }
617 WidgetMessage::MouseUp { .. } => {
618 if !message.handled() {
619 if self.items_container.is_empty() || *self.clickable_when_not_empty {
620 ui.send_message(MenuItemMessage::click(
621 self.handle(),
622 MessageDirection::ToWidget,
623 ));
624 }
625 if self.items_container.is_empty() {
626 let menu = find_menu(self.parent(), ui);
627 if menu.is_some() {
628 ui.send_message(MenuMessage::deactivate(
630 menu,
631 MessageDirection::ToWidget,
632 ));
633 } else {
634 close_menu_chain(self.parent(), ui);
636 }
637 }
638 message.set_handled(true);
639 }
640 }
641 WidgetMessage::MouseEnter => {
642 let menu = find_menu(self.parent(), ui);
645 let open = if menu.is_some() {
646 if let Some(menu) = ui.node(menu).cast::<Menu>() {
647 menu.active
648 } else {
649 false
650 }
651 } else {
652 true
653 };
654 if open {
655 ui.send_message(MenuItemMessage::open(
656 self.handle(),
657 MessageDirection::ToWidget,
658 ));
659 }
660 }
661 WidgetMessage::MouseLeave => {
662 if !self.is_opened(ui) {
663 ui.send_message(MenuItemMessage::select(
664 self.handle,
665 MessageDirection::ToWidget,
666 false,
667 ));
668 }
669 }
670 WidgetMessage::KeyDown(key_code) => {
671 if !message.handled() && *self.is_selected && *key_code == KeyCode::Enter {
672 ui.send_message(MenuItemMessage::click(
673 self.handle,
674 MessageDirection::FromWidget,
675 ));
676 let menu = find_menu(self.parent(), ui);
677 ui.send_message(MenuMessage::deactivate(menu, MessageDirection::ToWidget));
678 message.set_handled(true);
679 }
680 }
681 _ => {}
682 }
683 } else if let Some(msg) = message.data::<MenuItemMessage>() {
684 if message.destination() == self.handle
685 && message.direction() == MessageDirection::ToWidget
686 {
687 match msg {
688 MenuItemMessage::Select(selected) => {
689 if *self.is_selected != *selected {
690 self.is_selected.set_value_and_mark_modified(*selected);
691
692 ui.send_message(DecoratorMessage::select(
693 *self.decorator,
694 MessageDirection::ToWidget,
695 *selected,
696 ));
697
698 if *selected {
699 ui.send_message(WidgetMessage::focus(
700 self.handle,
701 MessageDirection::ToWidget,
702 ));
703 }
704 }
705 }
706 MenuItemMessage::Open => {
707 if !self.items_container.is_empty() && !self.is_opened(ui) {
708 let placement = match *self.placement {
709 MenuItemPlacement::Bottom => Placement::LeftBottom(self.handle),
710 MenuItemPlacement::Right => Placement::RightTop(self.handle),
711 };
712
713 if !*self.is_selected {
714 ui.send_message(MenuItemMessage::select(
715 self.handle,
716 MessageDirection::ToWidget,
717 true,
718 ));
719 }
720
721 ui.send_message(PopupMessage::placement(
723 *self.items_panel,
724 MessageDirection::ToWidget,
725 placement,
726 ));
727 ui.send_message(PopupMessage::open(
728 *self.items_panel,
729 MessageDirection::ToWidget,
730 ));
731 }
732 }
733 MenuItemMessage::Close { deselect } => {
734 if let Some(panel) =
735 ui.node(*self.items_panel).query_component::<ContextMenu>()
736 {
737 if *panel.popup.is_open {
738 ui.send_message(PopupMessage::close(
739 *self.items_panel,
740 MessageDirection::ToWidget,
741 ));
742
743 if *deselect && *self.is_selected {
744 ui.send_message(MenuItemMessage::select(
745 self.handle,
746 MessageDirection::ToWidget,
747 false,
748 ));
749 }
750
751 for &item in &*self.items_container.items {
753 ui.send_message(MenuItemMessage::close(
754 item,
755 MessageDirection::ToWidget,
756 true,
757 ));
758 }
759 }
760 }
761 }
762 MenuItemMessage::Click => {}
763 MenuItemMessage::AddItem(item) => {
764 ui.send_message(WidgetMessage::link(
765 *item,
766 MessageDirection::ToWidget,
767 *self.panel,
768 ));
769 self.items_container.push(*item);
770 if self.items_container.len() == 1 {
771 self.sync_arrow_visibility(ui);
772 }
773 }
774 MenuItemMessage::RemoveItem(item) => {
775 if let Some(position) =
776 self.items_container.iter().position(|i| *i == *item)
777 {
778 self.items_container.remove(position);
779
780 ui.send_message(WidgetMessage::remove(
781 *item,
782 MessageDirection::ToWidget,
783 ));
784
785 if self.items_container.is_empty() {
786 self.sync_arrow_visibility(ui);
787 }
788 }
789 }
790 MenuItemMessage::Items(items) => {
791 for ¤t_item in self.items_container.iter() {
792 ui.send_message(WidgetMessage::remove(
793 current_item,
794 MessageDirection::ToWidget,
795 ));
796 }
797
798 for &item in items {
799 ui.send_message(WidgetMessage::link(
800 item,
801 MessageDirection::ToWidget,
802 *self.panel,
803 ));
804 }
805
806 self.items_container
807 .items
808 .set_value_and_mark_modified(items.clone());
809
810 self.sync_arrow_visibility(ui);
811 }
812 MenuItemMessage::Sort(predicate) => {
813 let predicate = predicate.clone();
814 ui.send_message(WidgetMessage::sort_children(
815 *self.panel,
816 MessageDirection::ToWidget,
817 widget::SortingPredicate::new(move |a, b, ui| {
818 let item_a = ui.try_get_of_type::<MenuItem>(a).unwrap();
819 let item_b = ui.try_get_of_type::<MenuItem>(b).unwrap();
820
821 if let (Some(a_content), Some(b_content)) =
822 (item_a.content.as_ref(), item_b.content.as_ref())
823 {
824 predicate.0(a_content, b_content, ui)
825 } else {
826 Ordering::Equal
827 }
828 }),
829 ));
830 }
831 }
832 }
833 }
834 }
835
836 fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
837 if message.destination() != self.handle() {
840 if let Some(MenuItemMessage::Open) = message.data::<MenuItemMessage>() {
841 let mut found = false;
842 let mut handle = message.destination();
843 while handle.is_some() {
844 if handle == self.handle() {
845 found = true;
846 break;
847 } else {
848 let node = ui.node(handle);
849 if let Some(panel) = node.component_ref::<ContextMenu>() {
850 handle = panel.parent_menu_item;
853 } else {
854 handle = node.parent();
855 }
856 }
857 }
858
859 if !found {
860 if let Some(panel) = ui.node(*self.items_panel).query_component::<ContextMenu>()
861 {
862 if *panel.popup.is_open {
863 ui.send_message(MenuItemMessage::close(
864 self.handle(),
865 MessageDirection::ToWidget,
866 true,
867 ));
868 }
869 }
870 }
871 }
872 }
873 }
874
875 fn handle_os_event(
876 &mut self,
877 _self_handle: Handle<UiNode>,
878 ui: &mut UserInterface,
879 event: &OsEvent,
880 ) {
881 if let OsEvent::MouseInput { state, .. } = event {
883 if *state == ButtonState::Pressed {
884 if let Some(panel) = ui.node(*self.items_panel).query_component::<ContextMenu>() {
885 if *panel.popup.is_open {
886 if !is_any_menu_item_contains_point(ui, ui.cursor_position())
888 && find_menu(self.parent(), ui).is_none()
889 {
890 if *panel.popup.is_open {
891 ui.send_message(PopupMessage::close(
892 *self.items_panel,
893 MessageDirection::ToWidget,
894 ));
895 }
896
897 close_menu_chain(self.parent(), ui);
899 }
900 }
901 }
902 }
903 }
904 }
905}
906
907pub struct MenuBuilder {
909 widget_builder: WidgetBuilder,
910 items: Vec<Handle<UiNode>>,
911 restrict_picking: bool,
912}
913
914impl MenuBuilder {
915 pub fn new(widget_builder: WidgetBuilder) -> Self {
917 Self {
918 widget_builder,
919 items: Default::default(),
920 restrict_picking: false,
921 }
922 }
923
924 pub fn with_items(mut self, items: Vec<Handle<UiNode>>) -> Self {
926 self.items = items;
927 self
928 }
929
930 pub fn with_restrict_picking(mut self, restrict_picking: bool) -> Self {
932 self.restrict_picking = restrict_picking;
933 self
934 }
935
936 pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
938 for &item in self.items.iter() {
939 if let Some(item) = ctx[item].cast_mut::<MenuItem>() {
940 item.placement
941 .set_value_and_mark_modified(MenuItemPlacement::Bottom);
942 }
943 }
944
945 let back = BorderBuilder::new(
946 WidgetBuilder::new()
947 .with_background(ctx.style.property(Style::BRUSH_PRIMARY))
948 .with_child(
949 StackPanelBuilder::new(
950 WidgetBuilder::new().with_children(self.items.iter().cloned()),
951 )
952 .with_orientation(Orientation::Horizontal)
953 .build(ctx),
954 ),
955 )
956 .build(ctx);
957
958 let menu = Menu {
959 widget: self
960 .widget_builder
961 .with_handle_os_events(true)
962 .with_child(back)
963 .build(ctx),
964 active: false,
965 items: ItemsContainer {
966 items: self.items.into(),
967 navigation_direction: NavigationDirection::Horizontal,
968 },
969 restrict_picking: self.restrict_picking.into(),
970 };
971
972 ctx.add_node(UiNode::new(menu))
973 }
974}
975
976#[derive(Clone, Debug, Visit, Reflect, PartialEq)]
979pub enum MenuItemContent {
980 Text {
989 text: String,
991 shortcut: String,
993 icon: Handle<UiNode>,
995 arrow: bool,
997 },
998 TextCentered(String),
1007 Node(Handle<UiNode>),
1010}
1011
1012impl Default for MenuItemContent {
1013 fn default() -> Self {
1014 Self::TextCentered(Default::default())
1015 }
1016}
1017
1018impl MenuItemContent {
1019 pub fn text_with_shortcut(text: impl AsRef<str>, shortcut: impl AsRef<str>) -> Self {
1021 MenuItemContent::Text {
1022 text: text.as_ref().to_owned(),
1023 shortcut: shortcut.as_ref().to_owned(),
1024 icon: Default::default(),
1025 arrow: true,
1026 }
1027 }
1028
1029 pub fn text(text: impl AsRef<str>) -> Self {
1031 MenuItemContent::Text {
1032 text: text.as_ref().to_owned(),
1033 shortcut: Default::default(),
1034 icon: Default::default(),
1035 arrow: true,
1036 }
1037 }
1038
1039 pub fn text_no_arrow(text: impl AsRef<str>) -> Self {
1041 MenuItemContent::Text {
1042 text: text.as_ref().to_owned(),
1043 shortcut: Default::default(),
1044 icon: Default::default(),
1045 arrow: false,
1046 }
1047 }
1048
1049 pub fn text_centered(text: impl AsRef<str>) -> Self {
1051 MenuItemContent::TextCentered(text.as_ref().to_owned())
1052 }
1053}
1054
1055pub struct MenuItemBuilder {
1057 widget_builder: WidgetBuilder,
1058 items: Vec<Handle<UiNode>>,
1059 content: Option<MenuItemContent>,
1060 back: Option<Handle<UiNode>>,
1061 clickable_when_not_empty: bool,
1062 restrict_picking: bool,
1063}
1064
1065impl MenuItemBuilder {
1066 pub fn new(widget_builder: WidgetBuilder) -> Self {
1068 Self {
1069 widget_builder,
1070 items: Default::default(),
1071 content: None,
1072 back: None,
1073 clickable_when_not_empty: false,
1074 restrict_picking: false,
1075 }
1076 }
1077
1078 pub fn with_content(mut self, content: MenuItemContent) -> Self {
1080 self.content = Some(content);
1081 self
1082 }
1083
1084 pub fn with_items(mut self, items: Vec<Handle<UiNode>>) -> Self {
1086 self.items = items;
1087 self
1088 }
1089
1090 pub fn with_back(mut self, handle: Handle<UiNode>) -> Self {
1093 self.back = Some(handle);
1094 self
1095 }
1096
1097 pub fn with_clickable_when_not_empty(mut self, value: bool) -> Self {
1099 self.clickable_when_not_empty = value;
1100 self
1101 }
1102
1103 pub fn with_restrict_picking(mut self, restrict_picking: bool) -> Self {
1105 self.restrict_picking = restrict_picking;
1106 self
1107 }
1108
1109 pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
1111 let mut arrow_widget = Handle::NONE;
1112 let content = match self.content.as_ref() {
1113 None => Handle::NONE,
1114 Some(MenuItemContent::Text {
1115 text,
1116 shortcut,
1117 icon,
1118 arrow,
1119 }) => GridBuilder::new(
1120 WidgetBuilder::new()
1121 .with_vertical_alignment(VerticalAlignment::Center)
1122 .with_child(*icon)
1123 .with_child(
1124 TextBuilder::new(
1125 WidgetBuilder::new()
1126 .with_margin(Thickness::left(2.0))
1127 .on_column(1)
1128 .with_vertical_alignment(VerticalAlignment::Center),
1129 )
1130 .with_text(text)
1131 .build(ctx),
1132 )
1133 .with_child(
1134 TextBuilder::new(
1135 WidgetBuilder::new()
1136 .with_horizontal_alignment(HorizontalAlignment::Right)
1137 .with_margin(Thickness::uniform(1.0))
1138 .on_column(2),
1139 )
1140 .with_text(shortcut)
1141 .build(ctx),
1142 )
1143 .with_child({
1144 arrow_widget = if *arrow {
1145 VectorImageBuilder::new(
1146 WidgetBuilder::new()
1147 .with_visibility(!self.items.is_empty())
1148 .on_column(3)
1149 .with_width(8.0)
1150 .with_height(8.0)
1151 .with_foreground(ctx.style.property(Style::BRUSH_BRIGHT))
1152 .with_horizontal_alignment(HorizontalAlignment::Center)
1153 .with_vertical_alignment(VerticalAlignment::Center),
1154 )
1155 .with_primitives(make_arrow_primitives(ArrowDirection::Right, 8.0))
1156 .build(ctx)
1157 } else {
1158 Handle::NONE
1159 };
1160 arrow_widget
1161 }),
1162 )
1163 .add_row(Row::auto())
1164 .add_column(Column::auto())
1165 .add_column(Column::stretch())
1166 .add_column(Column::auto())
1167 .add_column(Column::strict(10.0))
1168 .add_column(Column::strict(5.0))
1169 .build(ctx),
1170 Some(MenuItemContent::TextCentered(text)) => {
1171 TextBuilder::new(WidgetBuilder::new().with_margin(Thickness::left_right(5.0)))
1172 .with_text(text)
1173 .with_horizontal_text_alignment(HorizontalAlignment::Center)
1174 .with_vertical_text_alignment(VerticalAlignment::Center)
1175 .build(ctx)
1176 }
1177 Some(MenuItemContent::Node(node)) => *node,
1178 };
1179
1180 let decorator = self.back.unwrap_or_else(|| {
1181 DecoratorBuilder::new(
1182 BorderBuilder::new(WidgetBuilder::new())
1183 .with_stroke_thickness(Thickness::uniform(0.0).into()),
1184 )
1185 .with_hover_brush(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
1186 .with_selected_brush(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
1187 .with_normal_brush(ctx.style.property(Style::BRUSH_PRIMARY))
1188 .with_pressed_brush(Brush::Solid(Color::TRANSPARENT).into())
1189 .with_pressable(false)
1190 .build(ctx)
1191 });
1192
1193 if content.is_some() {
1194 ctx.link(content, decorator);
1195 }
1196
1197 let panel;
1198 let items_panel = ContextMenuBuilder::new(
1199 PopupBuilder::new(WidgetBuilder::new().with_min_size(Vector2::new(10.0, 10.0)))
1200 .with_content({
1201 panel = StackPanelBuilder::new(
1202 WidgetBuilder::new().with_children(self.items.iter().cloned()),
1203 )
1204 .build(ctx);
1205 panel
1206 })
1207 .with_restrict_picking(self.restrict_picking)
1208 .stays_open(true),
1210 )
1211 .build(ctx);
1212
1213 let menu = MenuItem {
1214 widget: self
1215 .widget_builder
1216 .with_handle_os_events(true)
1217 .with_preview_messages(true)
1218 .with_child(decorator)
1219 .build(ctx),
1220 items_panel: items_panel.into(),
1221 items_container: ItemsContainer {
1222 items: self.items.into(),
1223 navigation_direction: NavigationDirection::Vertical,
1224 },
1225 placement: MenuItemPlacement::Right.into(),
1226 panel: panel.into(),
1227 clickable_when_not_empty: false.into(),
1228 decorator: decorator.into(),
1229 is_selected: Default::default(),
1230 arrow: arrow_widget.into(),
1231 content: self.content.into(),
1232 };
1233
1234 let handle = ctx.add_node(UiNode::new(menu));
1235
1236 if let Some(popup) = ctx[items_panel].cast_mut::<ContextMenu>() {
1238 popup.parent_menu_item = handle;
1239 }
1240
1241 handle
1242 }
1243}
1244
1245#[derive(Default, Clone, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
1248#[type_uuid(id = "ad8e9e76-c213-4232-9bab-80ebcabd69fa")]
1249#[reflect(derived_type = "UiNode")]
1250pub struct ContextMenu {
1251 #[component(include)]
1253 pub popup: Popup,
1254 pub parent_menu_item: Handle<UiNode>,
1256}
1257
1258impl ConstructorProvider<UiNode, UserInterface> for ContextMenu {
1259 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
1260 GraphNodeConstructor::new::<Self>()
1261 .with_variant("Context Menu", |ui| {
1262 ContextMenuBuilder::new(
1263 PopupBuilder::new(WidgetBuilder::new().with_name("Context Menu"))
1264 .with_restrict_picking(false),
1265 )
1266 .build(&mut ui.build_ctx())
1267 .into()
1268 })
1269 .with_group("Input")
1270 }
1271}
1272
1273impl Deref for ContextMenu {
1274 type Target = Widget;
1275
1276 fn deref(&self) -> &Self::Target {
1277 &self.popup.widget
1278 }
1279}
1280
1281impl DerefMut for ContextMenu {
1282 fn deref_mut(&mut self) -> &mut Self::Target {
1283 &mut self.popup.widget
1284 }
1285}
1286
1287impl Control for ContextMenu {
1288 fn on_remove(&self, sender: &Sender<UiMessage>) {
1289 self.popup.on_remove(sender)
1290 }
1291
1292 fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
1293 self.popup.measure_override(ui, available_size)
1294 }
1295
1296 fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
1297 self.popup.arrange_override(ui, final_size)
1298 }
1299
1300 fn draw(&self, drawing_context: &mut DrawingContext) {
1301 self.popup.draw(drawing_context)
1302 }
1303
1304 fn post_draw(&self, drawing_context: &mut DrawingContext) {
1305 self.popup.post_draw(drawing_context)
1306 }
1307
1308 fn update(&mut self, dt: f32, ui: &mut UserInterface) {
1309 self.popup.update(dt, ui);
1310 }
1311
1312 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
1313 self.popup.handle_routed_message(ui, message);
1314
1315 if let Some(WidgetMessage::KeyDown(key_code)) = message.data() {
1316 if !message.handled() {
1317 if let Some(parent_menu_item) = ui.try_get_node(self.parent_menu_item) {
1318 if keyboard_navigation(
1319 ui,
1320 *key_code,
1321 parent_menu_item.deref(),
1322 self.parent_menu_item,
1323 ) {
1324 message.set_handled(true);
1325 }
1326 }
1327 }
1328 }
1329 }
1330
1331 fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
1332 self.popup.preview_message(ui, message)
1333 }
1334
1335 fn handle_os_event(
1336 &mut self,
1337 self_handle: Handle<UiNode>,
1338 ui: &mut UserInterface,
1339 event: &OsEvent,
1340 ) {
1341 self.popup.handle_os_event(self_handle, ui, event)
1342 }
1343}
1344
1345pub struct ContextMenuBuilder {
1347 popup_builder: PopupBuilder,
1348 parent_menu_item: Handle<UiNode>,
1349}
1350
1351impl ContextMenuBuilder {
1352 pub fn new(popup_builder: PopupBuilder) -> Self {
1354 Self {
1355 popup_builder,
1356 parent_menu_item: Default::default(),
1357 }
1358 }
1359
1360 pub fn with_parent_menu_item(mut self, parent_menu_item: Handle<UiNode>) -> Self {
1362 self.parent_menu_item = parent_menu_item;
1363 self
1364 }
1365
1366 pub fn build_context_menu(self, ctx: &mut BuildContext) -> ContextMenu {
1368 ContextMenu {
1369 popup: self.popup_builder.build_popup(ctx),
1370 parent_menu_item: self.parent_menu_item,
1371 }
1372 }
1373
1374 pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
1376 let context_menu = self.build_context_menu(ctx);
1377 ctx.add_node(UiNode::new(context_menu))
1378 }
1379}
1380
1381fn keyboard_navigation(
1382 ui: &UserInterface,
1383 key_code: KeyCode,
1384 parent_menu_item: &dyn Control,
1385 parent_menu_item_handle: Handle<UiNode>,
1386) -> bool {
1387 let Some(items_container) = parent_menu_item
1388 .query_component_ref(TypeId::of::<ItemsContainer>())
1389 .and_then(|c| c.downcast_ref::<ItemsContainer>())
1390 else {
1391 return false;
1392 };
1393
1394 let (close_key, enter_key, next_key, prev_key) = match items_container.navigation_direction {
1395 NavigationDirection::Horizontal => (
1396 KeyCode::ArrowUp,
1397 KeyCode::ArrowDown,
1398 KeyCode::ArrowRight,
1399 KeyCode::ArrowLeft,
1400 ),
1401 NavigationDirection::Vertical => (
1402 KeyCode::ArrowLeft,
1403 KeyCode::ArrowRight,
1404 KeyCode::ArrowDown,
1405 KeyCode::ArrowUp,
1406 ),
1407 };
1408
1409 if key_code == close_key {
1410 ui.send_message(MenuItemMessage::close(
1411 parent_menu_item_handle,
1412 MessageDirection::ToWidget,
1413 false,
1414 ));
1415 return true;
1416 } else if key_code == enter_key {
1417 if let Some(selected_item_index) = items_container.selected_item_index(ui) {
1418 let selected_item = items_container.items[selected_item_index];
1419
1420 ui.send_message(MenuItemMessage::open(
1421 selected_item,
1422 MessageDirection::ToWidget,
1423 ));
1424
1425 if let Some(selected_item_ref) = ui.try_get_of_type::<MenuItem>(selected_item) {
1426 if let Some(first_item) = selected_item_ref.items_container.first() {
1427 ui.send_message(MenuItemMessage::select(
1428 *first_item,
1429 MessageDirection::ToWidget,
1430 true,
1431 ));
1432 }
1433 }
1434 }
1435 return true;
1436 } else if key_code == next_key || key_code == prev_key {
1437 if let Some(selected_item_index) = items_container.selected_item_index(ui) {
1438 let dir = if key_code == next_key {
1439 1
1440 } else if key_code == prev_key {
1441 -1
1442 } else {
1443 unreachable!()
1444 };
1445
1446 if let Some(new_selection) = items_container.next_item_to_select_in_dir(ui, dir) {
1447 ui.send_message(MenuItemMessage::select(
1448 items_container.items[selected_item_index],
1449 MessageDirection::ToWidget,
1450 false,
1451 ));
1452 ui.send_message(MenuItemMessage::select(
1453 new_selection,
1454 MessageDirection::ToWidget,
1455 true,
1456 ));
1457
1458 return true;
1459 }
1460 } else if let Some(first_item) = items_container.items.first() {
1461 ui.send_message(MenuItemMessage::select(
1462 *first_item,
1463 MessageDirection::ToWidget,
1464 true,
1465 ));
1466
1467 return true;
1468 }
1469 }
1470
1471 false
1472}
1473
1474pub fn make_menu_splitter(ctx: &mut BuildContext) -> Handle<UiNode> {
1477 BorderBuilder::new(
1478 WidgetBuilder::new()
1479 .with_height(1.0)
1480 .with_margin(Thickness::top_bottom(1.0))
1481 .with_foreground(ctx.style.property(Style::BRUSH_LIGHTEST)),
1482 )
1483 .build(ctx)
1484}
1485
1486#[cfg(test)]
1487mod test {
1488 use crate::menu::{MenuBuilder, MenuItemBuilder};
1489 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
1490
1491 #[test]
1492 fn test_deletion() {
1493 test_widget_deletion(|ctx| MenuBuilder::new(WidgetBuilder::new()).build(ctx));
1494 test_widget_deletion(|ctx| MenuItemBuilder::new(WidgetBuilder::new()).build(ctx));
1495 }
1496}