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