1#![warn(missing_docs)]
25
26use crate::message::MessageData;
27use crate::stack_panel::StackPanel;
28use crate::vector_image::VectorImage;
29use crate::{
30 border::BorderBuilder,
31 brush::Brush,
32 control_trait_proxy_impls,
33 core::{
34 algebra::Vector2, color::Color, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
35 uuid_provider, variable::InheritableVariable, visitor::prelude::*,
36 },
37 decorator::{DecoratorBuilder, DecoratorMessage},
38 grid::{Column, GridBuilder, Row},
39 message::{ButtonState, KeyCode, OsEvent, UiMessage},
40 popup::{Placement, Popup, PopupBuilder, PopupMessage},
41 stack_panel::StackPanelBuilder,
42 style::{resource::StyleResourceExt, Style},
43 text::TextBuilder,
44 utils::{make_arrow_primitives, ArrowDirection},
45 vector_image::VectorImageBuilder,
46 widget::{self, Widget, WidgetBuilder, WidgetMessage},
47 BuildContext, Control, HorizontalAlignment, Orientation, RestrictionEntry, Thickness, UiNode,
48 UserInterface, VerticalAlignment,
49};
50use fyrox_core::pool::{HandlesVecExtension, ObjectOrVariant};
51use fyrox_graph::{
52 constructor::{ConstructorProvider, GraphNodeConstructor},
53 SceneGraph, SceneGraphNode,
54};
55use std::{
56 any::TypeId,
57 cmp::Ordering,
58 fmt::{Debug, Formatter},
59 ops::{Deref, DerefMut},
60 sync::{mpsc::Sender, Arc},
61};
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum MenuMessage {
66 Activate,
69 Deactivate,
71}
72impl MessageData for MenuMessage {}
73
74#[derive(Clone)]
76pub struct SortingPredicate(
77 pub Arc<dyn Fn(&MenuItemContent, &MenuItemContent, &UserInterface) -> Ordering + Send + Sync>,
78);
79
80impl SortingPredicate {
81 pub fn new<F>(func: F) -> Self
83 where
84 F: Fn(&MenuItemContent, &MenuItemContent, &UserInterface) -> Ordering
85 + Send
86 + Sync
87 + 'static,
88 {
89 Self(Arc::new(func))
90 }
91
92 pub fn sort_by_text() -> Self {
95 Self::new(|a, b, _| {
96 if let MenuItemContent::Text { text: a_text, .. } = a {
97 if let MenuItemContent::Text { text: b_text, .. } = b {
98 return a_text.cmp(b_text);
99 }
100 }
101
102 if let MenuItemContent::TextCentered(a_text) = a {
103 if let MenuItemContent::TextCentered(b_text) = b {
104 return a_text.cmp(b_text);
105 }
106 }
107
108 Ordering::Equal
109 })
110 }
111}
112
113impl Debug for SortingPredicate {
114 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
115 write!(f, "SortingPredicate")
116 }
117}
118
119impl PartialEq for SortingPredicate {
120 fn eq(&self, other: &Self) -> bool {
121 std::ptr::eq(self.0.as_ref(), other.0.as_ref())
122 }
123}
124
125#[derive(Debug, Clone, PartialEq)]
127pub enum MenuItemMessage {
128 Open,
130 Close {
132 deselect: bool,
134 },
135 Click,
137 AddItem(Handle<MenuItem>),
139 RemoveItem(Handle<MenuItem>),
141 Items(Vec<Handle<MenuItem>>),
143 Select(bool),
145 Sort(SortingPredicate),
147}
148impl MessageData for MenuItemMessage {}
149
150#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
202#[reflect(derived_type = "UiNode")]
203pub struct Menu {
204 widget: Widget,
205 active: bool,
206 #[component(include)]
207 items: ItemsContainer,
208 pub restrict_picking: InheritableVariable<bool>,
210}
211
212impl ConstructorProvider<UiNode, UserInterface> for Menu {
213 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
214 GraphNodeConstructor::new::<Self>()
215 .with_variant("Menu", |ui| {
216 MenuBuilder::new(WidgetBuilder::new().with_name("Menu"))
217 .build(&mut ui.build_ctx())
218 .to_base()
219 .into()
220 })
221 .with_group("Input")
222 }
223}
224
225crate::define_widget_deref!(Menu);
226
227uuid_provider!(Menu = "582a04f3-a7fd-4e70-bbd1-eb95e2275b75");
228
229impl Control for Menu {
230 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
231 self.widget.handle_routed_message(ui, message);
232
233 if let Some(msg) = message.data::<MenuMessage>() {
234 match msg {
235 MenuMessage::Activate => {
236 if !self.active {
237 if *self.restrict_picking {
238 ui.push_picking_restriction(RestrictionEntry {
239 handle: self.handle(),
240 stop: false,
241 });
242 }
243 self.active = true;
244 }
245 }
246 MenuMessage::Deactivate => {
247 if self.active {
248 self.active = false;
249
250 if *self.restrict_picking {
251 ui.remove_picking_restriction(self.handle());
252 }
253
254 let mut stack = self.children().to_vec();
256 while let Some(handle) = stack.pop() {
257 let node = ui.node(handle);
258 if let Some(item) = node.cast::<MenuItem>() {
259 ui.send(handle, MenuItemMessage::Close { deselect: true });
260 stack.push(*item.items_panel);
263 }
264 stack.extend_from_slice(node.children());
266 }
267 }
268 }
269 }
270 } else if let Some(WidgetMessage::KeyDown(key_code)) = message.data() {
271 if !message.handled() {
272 if keyboard_navigation(ui, *key_code, self, self.handle) {
273 message.set_handled(true);
274 } else if *key_code == KeyCode::Escape {
275 ui.send(self.handle, MenuMessage::Deactivate);
276 message.set_handled(true);
277 }
278 }
279 }
280 }
281
282 fn handle_os_event(
283 &mut self,
284 _self_handle: Handle<UiNode>,
285 ui: &mut UserInterface,
286 event: &OsEvent,
287 ) {
288 if let OsEvent::MouseInput { state, .. } = event {
293 if *state == ButtonState::Pressed && self.active {
294 let pos = ui.cursor_position();
296 if !self.widget.screen_bounds().contains(pos) {
297 let mut any_picked = false;
300 let mut stack = self.children().to_vec();
301 'depth_search: while let Some(handle) = stack.pop() {
302 let node = ui.node(handle);
303 if let Some(item) = node.cast::<MenuItem>() {
304 let popup = ui.node(*item.items_panel);
305 if popup.screen_bounds().contains(pos) && popup.is_globally_visible() {
306 any_picked = true;
310 break 'depth_search;
311 }
312 stack.push(*item.items_panel);
315 }
316 stack.extend_from_slice(node.children());
318 }
319
320 if !any_picked {
321 ui.send(self.handle(), MenuMessage::Deactivate);
322 }
323 }
324 }
325 }
326 }
327}
328
329#[derive(Copy, Clone, PartialOrd, PartialEq, Eq, Hash, Visit, Reflect, Default, Debug)]
331pub enum MenuItemPlacement {
332 Bottom,
334 #[default]
336 Right,
337}
338
339#[derive(Copy, Clone, PartialOrd, PartialEq, Eq, Hash, Visit, Reflect, Default, Debug)]
340enum NavigationDirection {
341 #[default]
342 Horizontal,
343 Vertical,
344}
345
346#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
347#[doc(hidden)]
348pub struct ItemsContainer {
349 #[doc(hidden)]
350 pub items: InheritableVariable<Vec<Handle<MenuItem>>>,
351 navigation_direction: NavigationDirection,
352}
353
354impl Deref for ItemsContainer {
355 type Target = Vec<Handle<MenuItem>>;
356
357 fn deref(&self) -> &Self::Target {
358 self.items.deref()
359 }
360}
361
362impl DerefMut for ItemsContainer {
363 fn deref_mut(&mut self) -> &mut Self::Target {
364 self.items.deref_mut()
365 }
366}
367
368impl ItemsContainer {
369 fn selected_item_index(&self, ui: &UserInterface) -> Option<usize> {
370 for (index, item) in self.items.iter().enumerate() {
371 if let Ok(item_ref) = ui.try_get(*item) {
372 if *item_ref.is_selected {
373 return Some(index);
374 }
375 }
376 }
377
378 None
379 }
380
381 fn next_item_to_select_in_dir(
382 &self,
383 ui: &UserInterface,
384 dir: isize,
385 ) -> Option<Handle<MenuItem>> {
386 self.selected_item_index(ui)
387 .map(|i| i as isize)
388 .and_then(|mut index| {
389 let count = self.items.len() as isize;
391 for _ in 0..count {
392 index += dir;
393 if index < 0 {
394 index += count;
395 }
396 index %= count;
397 let handle = self.items.get(index as usize).cloned();
398 if let Some(item) = handle.and_then(|h| ui.try_get(h).ok()) {
399 if item.enabled() {
400 return handle;
401 }
402 }
403 }
404
405 None
406 })
407 }
408}
409
410#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
413#[reflect(derived_type = "UiNode")]
414pub struct MenuItem {
415 pub widget: Widget,
417 #[component(include)]
419 pub items_container: ItemsContainer,
420 pub items_panel: InheritableVariable<Handle<UiNode>>,
422 pub panel: InheritableVariable<Handle<StackPanel>>,
424 pub placement: InheritableVariable<MenuItemPlacement>,
426 pub clickable_when_not_empty: InheritableVariable<bool>,
428 pub decorator: InheritableVariable<Handle<UiNode>>,
430 pub is_selected: InheritableVariable<bool>,
432 pub arrow: InheritableVariable<Handle<VectorImage>>,
434 pub content: InheritableVariable<Option<MenuItemContent>>,
436}
437
438impl ConstructorProvider<UiNode, UserInterface> for MenuItem {
439 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
440 GraphNodeConstructor::new::<Self>()
441 .with_variant("Menu Item", |ui| {
442 MenuItemBuilder::new(WidgetBuilder::new().with_name("Menu Item"))
443 .build(&mut ui.build_ctx())
444 .to_base()
445 .into()
446 })
447 .with_group("Input")
448 }
449}
450
451crate::define_widget_deref!(MenuItem);
452
453impl MenuItem {
454 fn is_opened(&self, ui: &UserInterface) -> bool {
455 ui.try_get_of_type::<ContextMenu>(*self.items_panel)
456 .ok()
457 .is_some_and(|items_panel| *items_panel.popup.is_open)
458 }
459
460 fn sync_arrow_visibility(&self, ui: &UserInterface) {
461 ui.send(
462 *self.arrow,
463 WidgetMessage::Visibility(!self.items_container.is_empty()),
464 );
465 }
466}
467
468fn find_menu(from: Handle<UiNode>, ui: &UserInterface) -> Handle<UiNode> {
474 let mut handle = from;
475 while handle.is_some() {
476 if let Some((_, panel)) = ui.find_component_up::<ContextMenu>(handle) {
477 handle = panel.parent_menu_item;
479 } else {
480 return ui.find_handle_up(handle, &mut |n| n.cast::<Menu>().is_some());
482 }
483 }
484 Default::default()
485}
486
487fn is_any_menu_item_contains_point(ui: &UserInterface, pt: Vector2<f32>) -> bool {
488 for (handle, menu) in ui
489 .nodes()
490 .pair_iter()
491 .filter_map(|(h, n)| n.query_component::<MenuItem>().map(|menu| (h, menu)))
492 {
493 if ui.find_component_up::<Menu>(handle).is_none()
494 && menu.is_globally_visible()
495 && menu.screen_bounds().contains(pt)
496 {
497 return true;
498 }
499 }
500 false
501}
502
503fn close_menu_chain(from: Handle<UiNode>, ui: &UserInterface) {
504 let mut handle = from;
505 while handle.is_some() {
506 let popup_handle = ui.find_handle_up(handle, &mut |n| n.has_component::<ContextMenu>());
507
508 if let Ok(panel) = ui.try_get_of_type::<ContextMenu>(popup_handle) {
509 if *panel.popup.is_open {
510 ui.send(popup_handle, PopupMessage::Close);
511 }
512
513 handle = panel.parent_menu_item;
515 } else {
516 break;
518 }
519 }
520}
521
522uuid_provider!(MenuItem = "72e002c6-6060-4583-b5b7-0c5500244fef");
523
524impl Control for MenuItem {
525 fn on_remove(&self, sender: &Sender<UiMessage>) {
526 sender
529 .send(UiMessage::for_widget(
530 *self.items_panel,
531 WidgetMessage::Remove,
532 ))
533 .unwrap();
534 }
535
536 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
537 self.widget.handle_routed_message(ui, message);
538
539 if let Some(msg) = message.data::<WidgetMessage>() {
540 match msg {
541 WidgetMessage::MouseDown { .. } => {
542 let menu = find_menu(self.parent(), ui);
543 if menu.is_some() {
544 if self.is_opened(ui) {
545 ui.send(self.handle(), MenuItemMessage::Close { deselect: true });
546 ui.send(menu, MenuMessage::Deactivate);
547 } else {
548 ui.send(menu, MenuMessage::Activate);
551 ui.send(self.handle(), MenuItemMessage::Open);
552 }
553 }
554 }
555 WidgetMessage::MouseUp { .. } => {
556 if !message.handled() {
557 if self.items_container.is_empty() || *self.clickable_when_not_empty {
558 ui.post(self.handle(), MenuItemMessage::Click);
559 }
560 if self.items_container.is_empty() {
561 let menu = find_menu(self.parent(), ui);
562 if menu.is_some() {
563 ui.send(menu, MenuMessage::Deactivate);
565 } else {
566 close_menu_chain(self.parent(), ui);
568 }
569 }
570 message.set_handled(true);
571 }
572 }
573 WidgetMessage::MouseEnter => {
574 let menu = find_menu(self.parent(), ui);
577 let open = if menu.is_some() {
578 if let Some(menu) = ui.node(menu).cast::<Menu>() {
579 menu.active
580 } else {
581 false
582 }
583 } else {
584 true
585 };
586 if open {
587 ui.send(self.handle(), MenuItemMessage::Open);
588 }
589 }
590 WidgetMessage::MouseLeave => {
591 if !self.is_opened(ui) {
592 ui.send(self.handle, MenuItemMessage::Select(false));
593 }
594 }
595 WidgetMessage::KeyDown(key_code) => {
596 if !message.handled() && *self.is_selected && *key_code == KeyCode::Enter {
597 ui.post(self.handle, MenuItemMessage::Click);
598 let menu = find_menu(self.parent(), ui);
599 ui.send(menu, MenuMessage::Deactivate);
600 message.set_handled(true);
601 }
602 }
603 _ => {}
604 }
605 } else if let Some(msg) = message.data_for::<MenuItemMessage>(self.handle) {
606 match msg {
607 MenuItemMessage::Select(selected) => {
608 if *self.is_selected != *selected {
609 self.is_selected.set_value_and_mark_modified(*selected);
610
611 ui.send(*self.decorator, DecoratorMessage::Select(*selected));
612
613 if *selected {
614 ui.send(self.handle, WidgetMessage::Focus);
615 }
616 }
617 }
618 MenuItemMessage::Open => {
619 if !self.items_container.is_empty() && !self.is_opened(ui) {
620 let placement = match *self.placement {
621 MenuItemPlacement::Bottom => Placement::LeftBottom(self.handle),
622 MenuItemPlacement::Right => Placement::RightTop(self.handle),
623 };
624
625 if !*self.is_selected {
626 ui.send(self.handle, MenuItemMessage::Select(true));
627 }
628
629 ui.send(*self.items_panel, PopupMessage::Placement(placement));
631 ui.send(*self.items_panel, PopupMessage::Open);
632 }
633 }
634 MenuItemMessage::Close { deselect } => {
635 if let Some(panel) = ui.node(*self.items_panel).query_component::<ContextMenu>()
636 {
637 if *panel.popup.is_open {
638 ui.send(*self.items_panel, PopupMessage::Close);
639
640 if *deselect && *self.is_selected {
641 ui.send(self.handle, MenuItemMessage::Select(false));
642 }
643
644 for &item in &*self.items_container.items {
646 ui.send(item, MenuItemMessage::Close { deselect: true });
647 }
648 }
649 }
650 }
651 MenuItemMessage::Click => {}
652 MenuItemMessage::AddItem(item) => {
653 ui.send(*item, WidgetMessage::link_with(*self.panel));
654 self.items_container.push(*item);
655 if self.items_container.len() == 1 {
656 self.sync_arrow_visibility(ui);
657 }
658 }
659 MenuItemMessage::RemoveItem(item) => {
660 if let Some(position) = self.items_container.iter().position(|i| *i == *item) {
661 self.items_container.remove(position);
662
663 ui.send(*item, WidgetMessage::Remove);
664
665 if self.items_container.is_empty() {
666 self.sync_arrow_visibility(ui);
667 }
668 }
669 }
670 MenuItemMessage::Items(items) => {
671 for ¤t_item in self.items_container.iter() {
672 ui.send(current_item, WidgetMessage::Remove);
673 }
674
675 for &item in items {
676 ui.send(item, WidgetMessage::link_with(*self.panel));
677 }
678
679 self.items_container
680 .items
681 .set_value_and_mark_modified(items.clone());
682
683 self.sync_arrow_visibility(ui);
684 }
685 MenuItemMessage::Sort(predicate) => {
686 let predicate = predicate.clone();
687 ui.send(
688 *self.panel,
689 WidgetMessage::SortChildren(widget::SortingPredicate::new(
690 move |a, b, ui| {
691 let item_a = ui.try_get_of_type::<MenuItem>(a).unwrap();
692 let item_b = ui.try_get_of_type::<MenuItem>(b).unwrap();
693
694 if let (Some(a_content), Some(b_content)) =
695 (item_a.content.as_ref(), item_b.content.as_ref())
696 {
697 predicate.0(a_content, b_content, ui)
698 } else {
699 Ordering::Equal
700 }
701 },
702 )),
703 );
704 }
705 }
706 }
707 }
708
709 fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
710 if message.destination() != self.handle() {
713 if let Some(MenuItemMessage::Open) = message.data::<MenuItemMessage>() {
714 let mut found = false;
715 let mut handle = message.destination();
716 while handle.is_some() {
717 if handle == self.handle() {
718 found = true;
719 break;
720 } else {
721 let node = ui.node(handle);
722 if let Some(panel) = node.component_ref::<ContextMenu>() {
723 handle = panel.parent_menu_item;
726 } else {
727 handle = node.parent();
728 }
729 }
730 }
731
732 if !found {
733 if let Some(panel) = ui.node(*self.items_panel).query_component::<ContextMenu>()
734 {
735 if *panel.popup.is_open {
736 ui.send(self.handle(), MenuItemMessage::Close { deselect: true });
737 }
738 }
739 }
740 }
741 }
742 }
743
744 fn handle_os_event(
745 &mut self,
746 _self_handle: Handle<UiNode>,
747 ui: &mut UserInterface,
748 event: &OsEvent,
749 ) {
750 if let OsEvent::MouseInput { state, .. } = event {
752 if *state == ButtonState::Pressed {
753 if let Some(panel) = ui.node(*self.items_panel).query_component::<ContextMenu>() {
754 if *panel.popup.is_open {
755 if !is_any_menu_item_contains_point(ui, ui.cursor_position())
757 && find_menu(self.parent(), ui).is_none()
758 {
759 if *panel.popup.is_open {
760 ui.send(*self.items_panel, PopupMessage::Close);
761 }
762
763 close_menu_chain(self.parent(), ui);
765 }
766 }
767 }
768 }
769 }
770 }
771}
772
773pub struct MenuBuilder {
775 widget_builder: WidgetBuilder,
776 items: Vec<Handle<MenuItem>>,
777 restrict_picking: bool,
778}
779
780impl MenuBuilder {
781 pub fn new(widget_builder: WidgetBuilder) -> Self {
783 Self {
784 widget_builder,
785 items: Default::default(),
786 restrict_picking: false,
787 }
788 }
789
790 pub fn with_items(mut self, items: Vec<Handle<MenuItem>>) -> Self {
792 self.items = items;
793 self
794 }
795
796 pub fn with_restrict_picking(mut self, restrict_picking: bool) -> Self {
798 self.restrict_picking = restrict_picking;
799 self
800 }
801
802 pub fn build(self, ctx: &mut BuildContext) -> Handle<Menu> {
804 for &item in self.items.iter() {
805 if let Ok(item) = ctx.inner_mut().try_get_mut(item) {
806 item.placement
807 .set_value_and_mark_modified(MenuItemPlacement::Bottom);
808 }
809 }
810
811 let back = BorderBuilder::new(
812 WidgetBuilder::new()
813 .with_background(ctx.style.property(Style::BRUSH_PRIMARY))
814 .with_child(
815 StackPanelBuilder::new(
816 WidgetBuilder::new().with_children(self.items.clone().to_base()),
817 )
818 .with_orientation(Orientation::Horizontal)
819 .build(ctx),
820 ),
821 )
822 .build(ctx);
823
824 let menu = Menu {
825 widget: self
826 .widget_builder
827 .with_handle_os_events(true)
828 .with_child(back)
829 .build(ctx),
830 active: false,
831 items: ItemsContainer {
832 items: self.items.into(),
833 navigation_direction: NavigationDirection::Horizontal,
834 },
835 restrict_picking: self.restrict_picking.into(),
836 };
837
838 ctx.add(menu)
839 }
840}
841
842#[derive(Clone, Debug, Visit, Reflect, PartialEq)]
845pub enum MenuItemContent {
846 Text {
855 text: String,
857 shortcut: String,
859 icon: Handle<UiNode>,
861 arrow: bool,
863 },
864 TextCentered(String),
873 Node(Handle<UiNode>),
876}
877
878impl Default for MenuItemContent {
879 fn default() -> Self {
880 Self::TextCentered(Default::default())
881 }
882}
883
884impl MenuItemContent {
885 pub fn text_with_shortcut(text: impl AsRef<str>, shortcut: impl AsRef<str>) -> Self {
887 MenuItemContent::Text {
888 text: text.as_ref().to_owned(),
889 shortcut: shortcut.as_ref().to_owned(),
890 icon: Default::default(),
891 arrow: true,
892 }
893 }
894
895 pub fn text_with_shortcut_and_icon(
897 text: impl AsRef<str>,
898 shortcut: impl AsRef<str>,
899 icon: Handle<impl ObjectOrVariant<UiNode>>,
900 ) -> Self {
901 MenuItemContent::Text {
902 text: text.as_ref().to_owned(),
903 shortcut: shortcut.as_ref().to_owned(),
904 icon: icon.transmute(),
905 arrow: true,
906 }
907 }
908
909 pub fn text(text: impl AsRef<str>) -> Self {
911 MenuItemContent::Text {
912 text: text.as_ref().to_owned(),
913 shortcut: Default::default(),
914 icon: Default::default(),
915 arrow: true,
916 }
917 }
918
919 pub fn text_no_arrow(text: impl AsRef<str>) -> Self {
921 MenuItemContent::Text {
922 text: text.as_ref().to_owned(),
923 shortcut: Default::default(),
924 icon: Default::default(),
925 arrow: false,
926 }
927 }
928
929 pub fn text_centered(text: impl AsRef<str>) -> Self {
931 MenuItemContent::TextCentered(text.as_ref().to_owned())
932 }
933}
934
935pub struct MenuItemBuilder {
937 widget_builder: WidgetBuilder,
938 items: Vec<Handle<MenuItem>>,
939 content: Option<MenuItemContent>,
940 back: Option<Handle<UiNode>>,
941 clickable_when_not_empty: bool,
942 restrict_picking: bool,
943}
944
945impl MenuItemBuilder {
946 pub fn new(widget_builder: WidgetBuilder) -> Self {
948 Self {
949 widget_builder,
950 items: Default::default(),
951 content: None,
952 back: None,
953 clickable_when_not_empty: false,
954 restrict_picking: false,
955 }
956 }
957
958 pub fn with_content(mut self, content: MenuItemContent) -> Self {
960 self.content = Some(content);
961 self
962 }
963
964 pub fn with_items(mut self, items: Vec<Handle<MenuItem>>) -> Self {
966 self.items = items;
967 self
968 }
969
970 pub fn with_back(mut self, handle: Handle<UiNode>) -> Self {
973 self.back = Some(handle);
974 self
975 }
976
977 pub fn with_clickable_when_not_empty(mut self, value: bool) -> Self {
979 self.clickable_when_not_empty = value;
980 self
981 }
982
983 pub fn with_restrict_picking(mut self, restrict_picking: bool) -> Self {
985 self.restrict_picking = restrict_picking;
986 self
987 }
988
989 pub fn build(self, ctx: &mut BuildContext) -> Handle<MenuItem> {
991 let mut arrow_widget = Handle::NONE;
992 let content = match self.content.as_ref() {
993 None => Handle::NONE,
994 Some(MenuItemContent::Text {
995 text,
996 shortcut,
997 icon,
998 arrow,
999 }) => GridBuilder::new(
1000 WidgetBuilder::new()
1001 .with_vertical_alignment(VerticalAlignment::Center)
1002 .with_child(*icon)
1003 .with_child(
1004 TextBuilder::new(
1005 WidgetBuilder::new()
1006 .with_margin(Thickness::left(2.0))
1007 .on_column(1)
1008 .with_vertical_alignment(VerticalAlignment::Center),
1009 )
1010 .with_text(text)
1011 .build(ctx),
1012 )
1013 .with_child(
1014 TextBuilder::new(
1015 WidgetBuilder::new()
1016 .with_horizontal_alignment(HorizontalAlignment::Right)
1017 .with_vertical_alignment(VerticalAlignment::Center)
1018 .with_margin(Thickness::uniform(1.0))
1019 .on_column(2),
1020 )
1021 .with_text(shortcut)
1022 .build(ctx),
1023 )
1024 .with_child({
1025 arrow_widget = if *arrow {
1026 VectorImageBuilder::new(
1027 WidgetBuilder::new()
1028 .with_visibility(!self.items.is_empty())
1029 .on_column(3)
1030 .with_width(8.0)
1031 .with_height(8.0)
1032 .with_foreground(ctx.style.property(Style::BRUSH_BRIGHT))
1033 .with_horizontal_alignment(HorizontalAlignment::Center)
1034 .with_vertical_alignment(VerticalAlignment::Center),
1035 )
1036 .with_primitives(make_arrow_primitives(ArrowDirection::Right, 8.0))
1037 .build(ctx)
1038 } else {
1039 Handle::NONE
1040 };
1041 arrow_widget
1042 }),
1043 )
1044 .add_row(Row::auto())
1045 .add_column(Column::strict(25.0))
1046 .add_column(Column::stretch())
1047 .add_column(Column::auto())
1048 .add_column(Column::strict(10.0))
1049 .add_column(Column::strict(5.0))
1050 .build(ctx)
1051 .to_base(),
1052 Some(MenuItemContent::TextCentered(text)) => {
1053 TextBuilder::new(WidgetBuilder::new().with_margin(Thickness::left_right(5.0)))
1054 .with_text(text)
1055 .with_horizontal_text_alignment(HorizontalAlignment::Center)
1056 .with_vertical_text_alignment(VerticalAlignment::Center)
1057 .build(ctx)
1058 .to_base()
1059 }
1060 Some(MenuItemContent::Node(node)) => *node,
1061 };
1062
1063 let decorator = self.back.unwrap_or_else(|| {
1064 DecoratorBuilder::new(
1065 BorderBuilder::new(WidgetBuilder::new())
1066 .with_stroke_thickness(Thickness::uniform(0.0).into()),
1067 )
1068 .with_hover_brush(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
1069 .with_selected_brush(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
1070 .with_normal_brush(ctx.style.property(Style::BRUSH_PRIMARY))
1071 .with_pressed_brush(Brush::Solid(Color::TRANSPARENT).into())
1072 .with_pressable(false)
1073 .build(ctx)
1074 .to_base()
1075 });
1076
1077 if content.is_some() {
1078 ctx.link(content, decorator);
1079 }
1080
1081 let panel;
1082 let items_panel = ContextMenuBuilder::new(
1083 PopupBuilder::new(WidgetBuilder::new().with_min_size(Vector2::new(10.0, 10.0)))
1084 .with_content({
1085 panel = StackPanelBuilder::new(
1086 WidgetBuilder::new().with_children(self.items.clone().to_base()),
1087 )
1088 .build(ctx);
1089 panel
1090 })
1091 .with_restrict_picking(self.restrict_picking)
1092 .stays_open(true),
1094 )
1095 .build(ctx)
1096 .to_base();
1097
1098 let menu = MenuItem {
1099 widget: self
1100 .widget_builder
1101 .with_handle_os_events(true)
1102 .with_preview_messages(true)
1103 .with_child(decorator)
1104 .build(ctx),
1105 items_panel: items_panel.into(),
1106 items_container: ItemsContainer {
1107 items: self.items.into(),
1108 navigation_direction: NavigationDirection::Vertical,
1109 },
1110 placement: MenuItemPlacement::Right.into(),
1111 panel: panel.into(),
1112 clickable_when_not_empty: self.clickable_when_not_empty.into(),
1113 decorator: decorator.into(),
1114 is_selected: Default::default(),
1115 arrow: arrow_widget.into(),
1116 content: self.content.into(),
1117 };
1118
1119 let handle = ctx.add(menu);
1120
1121 if let Some(popup) = ctx[items_panel].cast_mut::<ContextMenu>() {
1123 popup.parent_menu_item = handle.to_base();
1124 }
1125
1126 handle
1127 }
1128}
1129
1130#[derive(Default, Clone, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
1133#[type_uuid(id = "ad8e9e76-c213-4232-9bab-80ebcabd69fa")]
1134#[reflect(derived_type = "UiNode")]
1135pub struct ContextMenu {
1136 #[component(include)]
1138 pub popup: Popup,
1139 pub parent_menu_item: Handle<UiNode>,
1141}
1142
1143impl ConstructorProvider<UiNode, UserInterface> for ContextMenu {
1144 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
1145 GraphNodeConstructor::new::<Self>()
1146 .with_variant("Context Menu", |ui| {
1147 ContextMenuBuilder::new(
1148 PopupBuilder::new(WidgetBuilder::new().with_name("Context Menu"))
1149 .with_restrict_picking(false),
1150 )
1151 .build(&mut ui.build_ctx())
1152 .to_base()
1153 .into()
1154 })
1155 .with_group("Input")
1156 }
1157}
1158
1159impl Deref for ContextMenu {
1160 type Target = Widget;
1161
1162 fn deref(&self) -> &Self::Target {
1163 &self.popup.widget
1164 }
1165}
1166
1167impl DerefMut for ContextMenu {
1168 fn deref_mut(&mut self) -> &mut Self::Target {
1169 &mut self.popup.widget
1170 }
1171}
1172
1173impl Control for ContextMenu {
1174 control_trait_proxy_impls!(popup);
1175
1176 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
1177 self.popup.handle_routed_message(ui, message);
1178
1179 if let Some(WidgetMessage::KeyDown(key_code)) = message.data() {
1180 if !message.handled() {
1181 if let Ok(parent_menu_item) = ui.try_get_node(self.parent_menu_item) {
1182 if keyboard_navigation(
1183 ui,
1184 *key_code,
1185 parent_menu_item.deref(),
1186 self.parent_menu_item,
1187 ) {
1188 message.set_handled(true);
1189 }
1190 }
1191 }
1192 }
1193 }
1194}
1195
1196pub struct ContextMenuBuilder {
1198 popup_builder: PopupBuilder,
1199 parent_menu_item: Handle<UiNode>,
1200}
1201
1202impl ContextMenuBuilder {
1203 pub fn new(popup_builder: PopupBuilder) -> Self {
1205 Self {
1206 popup_builder,
1207 parent_menu_item: Default::default(),
1208 }
1209 }
1210
1211 pub fn with_parent_menu_item(mut self, parent_menu_item: Handle<UiNode>) -> Self {
1213 self.parent_menu_item = parent_menu_item;
1214 self
1215 }
1216
1217 pub fn build_context_menu(self, ctx: &mut BuildContext) -> ContextMenu {
1219 ContextMenu {
1220 popup: self.popup_builder.build_popup(ctx),
1221 parent_menu_item: self.parent_menu_item,
1222 }
1223 }
1224
1225 pub fn build(self, ctx: &mut BuildContext) -> Handle<ContextMenu> {
1227 let context_menu = self.build_context_menu(ctx);
1228 ctx.add(context_menu)
1229 }
1230}
1231
1232fn keyboard_navigation(
1233 ui: &UserInterface,
1234 key_code: KeyCode,
1235 parent_menu_item: &dyn Control,
1236 parent_menu_item_handle: Handle<UiNode>,
1237) -> bool {
1238 let Some(items_container) = parent_menu_item
1239 .query_component_ref(TypeId::of::<ItemsContainer>())
1240 .and_then(|c| c.downcast_ref::<ItemsContainer>())
1241 else {
1242 return false;
1243 };
1244
1245 let (close_key, enter_key, next_key, prev_key) = match items_container.navigation_direction {
1246 NavigationDirection::Horizontal => (
1247 KeyCode::ArrowUp,
1248 KeyCode::ArrowDown,
1249 KeyCode::ArrowRight,
1250 KeyCode::ArrowLeft,
1251 ),
1252 NavigationDirection::Vertical => (
1253 KeyCode::ArrowLeft,
1254 KeyCode::ArrowRight,
1255 KeyCode::ArrowDown,
1256 KeyCode::ArrowUp,
1257 ),
1258 };
1259
1260 if key_code == close_key {
1261 ui.send(
1262 parent_menu_item_handle,
1263 MenuItemMessage::Close { deselect: false },
1264 );
1265 return true;
1266 } else if key_code == enter_key {
1267 if let Some(selected_item_index) = items_container.selected_item_index(ui) {
1268 let selected_item = items_container.items[selected_item_index];
1269
1270 ui.send(selected_item, MenuItemMessage::Open);
1271
1272 if let Ok(selected_item_ref) = ui.try_get(selected_item) {
1273 if let Some(first_item) = selected_item_ref.items_container.first() {
1274 ui.send(*first_item, MenuItemMessage::Select(true));
1275 }
1276 }
1277 }
1278 return true;
1279 } else if key_code == next_key || key_code == prev_key {
1280 if let Some(selected_item_index) = items_container.selected_item_index(ui) {
1281 let dir = if key_code == next_key {
1282 1
1283 } else if key_code == prev_key {
1284 -1
1285 } else {
1286 unreachable!()
1287 };
1288
1289 if let Some(new_selection) = items_container.next_item_to_select_in_dir(ui, dir) {
1290 ui.send(
1291 items_container.items[selected_item_index],
1292 MenuItemMessage::Select(false),
1293 );
1294 ui.send(new_selection, MenuItemMessage::Select(true));
1295
1296 return true;
1297 }
1298 } else if let Some(first_item) = items_container.items.first() {
1299 ui.send(*first_item, MenuItemMessage::Select(true));
1300
1301 return true;
1302 }
1303 }
1304
1305 false
1306}
1307
1308pub fn make_menu_splitter(ctx: &mut BuildContext) -> Handle<UiNode> {
1311 BorderBuilder::new(
1312 WidgetBuilder::new()
1313 .with_height(1.0)
1314 .with_margin(Thickness::top_bottom(1.0))
1315 .with_foreground(ctx.style.property(Style::BRUSH_LIGHTEST)),
1316 )
1317 .build(ctx)
1318 .to_base()
1319}
1320
1321#[cfg(test)]
1322mod test {
1323 use crate::{
1324 menu::{MenuBuilder, MenuItemBuilder, MenuItemContent, MenuItemMessage},
1325 test::{test_widget_deletion, UserInterfaceTestingExtension},
1326 widget::WidgetBuilder,
1327 UserInterface,
1328 };
1329 use fyrox_core::algebra::Vector2;
1330 use uuid::uuid;
1331
1332 #[test]
1333 fn test_deletion() {
1334 test_widget_deletion(|ctx| MenuBuilder::new(WidgetBuilder::new()).build(ctx));
1335 test_widget_deletion(|ctx| MenuItemBuilder::new(WidgetBuilder::new()).build(ctx));
1336 }
1337
1338 #[test]
1339 fn test_menu_interaction() {
1340 let screen_size = Vector2::repeat(1000.0);
1341 let mut ui = UserInterface::new(screen_size);
1342 let ctx = &mut ui.build_ctx();
1343 let exit_id = uuid!("2ce5379e-ffa3-410b-9fa9-d92d4d242766");
1344 let exit = MenuItemBuilder::new(WidgetBuilder::new().with_id(exit_id))
1345 .with_content(MenuItemContent::text("Exit"))
1346 .build(ctx);
1347 let save_id = uuid!("11fba834-a96b-445e-a822-f51e0441ccb5");
1348 let save = MenuItemBuilder::new(WidgetBuilder::new().with_id(save_id))
1349 .with_content(MenuItemContent::text("Save"))
1350 .build(ctx);
1351 let file_id = uuid!("93d2d166-45cc-4c01-bc4e-36dab66f99ab");
1352 let file = MenuItemBuilder::new(WidgetBuilder::new().with_id(file_id))
1353 .with_content(MenuItemContent::text("File"))
1354 .with_items(vec![exit, save])
1355 .build(ctx);
1356 let menu = MenuBuilder::new(WidgetBuilder::new())
1357 .with_items(vec![file])
1358 .build(ctx);
1359 ui.poll_all_messages();
1360 assert_eq!(
1361 ui.click_at_count_response(file_id, MenuItemMessage::Click),
1362 0
1363 );
1364 assert!(ui[menu].active);
1365 assert_eq!(
1366 ui.click_at_count_response(exit_id, MenuItemMessage::Click),
1367 1
1368 );
1369 assert!(!ui[menu].active);
1370 assert_eq!(
1372 ui.click_at_count_response(file_id, MenuItemMessage::Click),
1373 0
1374 );
1375 assert!(ui[menu].active);
1376 assert_eq!(
1377 ui.click_at_count_response(save_id, MenuItemMessage::Click),
1378 1
1379 );
1380 assert!(!ui[menu].active);
1381 }
1382}