Skip to main content

fyrox_ui/
menu.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! [`Menu`] and [`MenuItem`] widgets are used to create menu chains like standard `File`, `Edit`, etc. menus. See doc
22//!  of the respective widget for more info and usage examples.
23
24#![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/// A set of messages that can be used to manipulate a [`Menu`] widget at runtime.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum MenuMessage {
66    /// Activates the menu so it captures mouse input by itself and allows you to open menu item by a simple mouse
67    /// hover.
68    Activate,
69    /// Deactivates the menu.
70    Deactivate,
71}
72impl MessageData for MenuMessage {}
73
74/// A predicate that is used to sort menu items.
75#[derive(Clone)]
76pub struct SortingPredicate(
77    pub Arc<dyn Fn(&MenuItemContent, &MenuItemContent, &UserInterface) -> Ordering + Send + Sync>,
78);
79
80impl SortingPredicate {
81    /// Creates new sorting predicate.
82    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    /// Creates new sorting predicate that sorts menu items by their textual content. This predicate
93    /// won't work with custom menu item content!
94    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/// A set of messages that can be used to manipulate a [`MenuItem`] widget at runtime.
126#[derive(Debug, Clone, PartialEq)]
127pub enum MenuItemMessage {
128    /// Opens the menu item's popup with inner items.
129    Open,
130    /// Closes the menu item's popup with inner items.
131    Close {
132        /// Defines, whether the item should be deselected when closed or not.
133        deselect: bool,
134    },
135    /// The message is generated by a menu item when it is clicked.
136    Click,
137    /// Adds a new item to the menu item.
138    AddItem(Handle<MenuItem>),
139    /// Removes an item from the menu item.
140    RemoveItem(Handle<MenuItem>),
141    /// Sets the new items of the menu item.
142    Items(Vec<Handle<MenuItem>>),
143    /// Selects/deselects the item.
144    Select(bool),
145    /// Sorts menu items by the given predicate.
146    Sort(SortingPredicate),
147}
148impl MessageData for MenuItemMessage {}
149
150/// Menu widget is a root widget of an arbitrary menu hierarchy. An example could be "standard" menu strip with `File`, `Edit`, `View`, etc.
151/// items. Menu widget can contain any number of children items (`File`, `Edit` in the previous example). These items should be [`MenuItem`]
152/// widgets, however, you can use any widget type (for example, to create some sort of separator).
153///
154/// ## Examples
155///
156/// The next example creates a menu with the following structure:
157///
158/// ```text
159/// |  File |  Edit |
160/// |--Save |--Undo
161/// |--Load |--Redo
162/// ```
163///
164/// ```rust
165/// # use fyrox_ui::{
166/// #     core::pool::Handle,
167/// #     menu::{MenuBuilder, Menu, MenuItemBuilder, MenuItemContent},
168/// #     widget::WidgetBuilder,
169/// #     BuildContext, UiNode,
170/// # };
171/// #
172/// fn create_menu(ctx: &mut BuildContext) -> Handle<Menu> {
173///     MenuBuilder::new(WidgetBuilder::new())
174///         .with_items(vec![
175///             MenuItemBuilder::new(WidgetBuilder::new())
176///                 .with_content(MenuItemContent::text_no_arrow("File"))
177///                 .with_items(vec![
178///                     MenuItemBuilder::new(WidgetBuilder::new())
179///                         .with_content(MenuItemContent::text_no_arrow("Save"))
180///                         .build(ctx),
181///                     MenuItemBuilder::new(WidgetBuilder::new())
182///                         .with_content(MenuItemContent::text_no_arrow("Load"))
183///                         .build(ctx),
184///                 ])
185///                 .build(ctx),
186///             MenuItemBuilder::new(WidgetBuilder::new())
187///                 .with_content(MenuItemContent::text_no_arrow("Edit"))
188///                 .with_items(vec![
189///                     MenuItemBuilder::new(WidgetBuilder::new())
190///                         .with_content(MenuItemContent::text_no_arrow("Undo"))
191///                         .build(ctx),
192///                     MenuItemBuilder::new(WidgetBuilder::new())
193///                         .with_content(MenuItemContent::text_no_arrow("Redo"))
194///                         .build(ctx),
195///                 ])
196///                 .build(ctx),
197///         ])
198///         .build(ctx)
199/// }
200/// ```
201#[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    /// A flag, that defines whether the menu should restrict all the mouse input or not.
209    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                        // Close descendant menu items.
255                        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                                // We have to search in popup content too because the menu shows its content
261                                // in popup and content could be another menu item.
262                                stack.push(*item.items_panel);
263                            }
264                            // Continue depth search.
265                            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        // Handle menu items close by clicking outside of menu item. We're using
289        // raw event here because we need to know the fact that mouse was clicked,
290        // and we do not care which element was clicked, so we'll get here in any
291        // case.
292        if let OsEvent::MouseInput { state, .. } = event {
293            if *state == ButtonState::Pressed && self.active {
294                // TODO: Make picking more accurate - right now it works only with rects.
295                let pos = ui.cursor_position();
296                if !self.widget.screen_bounds().contains(pos) {
297                    // Also check if we clicked inside some descendant menu item - in this
298                    //  case, we don't need to close the menu.
299                    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                                // Once we found that we clicked inside some descendant menu item,
307                                // we can immediately stop search - we don't want to close menu
308                                // items popups in this case and can safely skip all the stuff below.
309                                any_picked = true;
310                                break 'depth_search;
311                            }
312                            // We have to search in popup content too because the menu shows its content
313                            // in popup and content could be another menu item.
314                            stack.push(*item.items_panel);
315                        }
316                        // Continue depth search.
317                        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/// A set of possible placements of a popup with items of a menu item.
330#[derive(Copy, Clone, PartialOrd, PartialEq, Eq, Hash, Visit, Reflect, Default, Debug)]
331pub enum MenuItemPlacement {
332    /// Bottom placement.
333    Bottom,
334    /// Right placement.
335    #[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                // Do a full circle search.
390                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/// Menu item is a widget with arbitrary content, that has a "floating" panel (popup) for sub-items if the menu item. This was menu items can form
411/// arbitrary hierarchies. See [`Menu`] docs for examples.
412#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
413#[reflect(derived_type = "UiNode")]
414pub struct MenuItem {
415    /// Base widget of the menu item.
416    pub widget: Widget,
417    /// Current items of the menu item
418    #[component(include)]
419    pub items_container: ItemsContainer,
420    /// A handle of a popup that holds the items of the menu item.
421    pub items_panel: InheritableVariable<Handle<UiNode>>,
422    /// A handle of a panel widget that arranges items of the menu item.
423    pub panel: InheritableVariable<Handle<StackPanel>>,
424    /// Current placement of the menu item.
425    pub placement: InheritableVariable<MenuItemPlacement>,
426    /// A flag, that defines whether the menu item is clickable when it has sub-items or not.
427    pub clickable_when_not_empty: InheritableVariable<bool>,
428    /// A handle to the decorator of the item.
429    pub decorator: InheritableVariable<Handle<UiNode>>,
430    /// Is this item selected or not.
431    pub is_selected: InheritableVariable<bool>,
432    /// An arrow primitive that is used to indicate that there's sub-items in the menu item.
433    pub arrow: InheritableVariable<Handle<VectorImage>>,
434    /// Content of the menu item with which it was created.
435    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
468// MenuItem uses popup to show its content, popup can be top-most only if it is
469// direct child of root canvas of UI. This fact adds some complications to search
470// of parent menu - we can't just traverse the tree because popup is not a child
471// of menu item, instead we're trying to fetch handle to parent menu item from popup's
472// user data and continue up-search until we find menu.
473fn 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            // Continue search from parent menu item of popup.
478            handle = panel.parent_menu_item;
479        } else {
480            // Maybe we have Menu as parent for MenuItem.
481            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            // Continue search from parent menu item of popup.
514            handle = panel.parent_menu_item;
515        } else {
516            // Prevent infinite loops.
517            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        // Popup won't be deleted with the menu item, because it is not the child of the item.
527        // So we have to remove it manually.
528        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                            // Activate menu so its user will be able to open submenus by
549                            // mouse hover.
550                            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                                // Deactivate the menu if we have one.
564                                ui.send(menu, MenuMessage::Deactivate);
565                            } else {
566                                // Or close the menu chain if menu item is in "orphaned" state.
567                                close_menu_chain(self.parent(), ui);
568                            }
569                        }
570                        message.set_handled(true);
571                    }
572                }
573                WidgetMessage::MouseEnter => {
574                    // While parent menu active it is possible to open submenus
575                    // by simple mouse hover.
576                    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                        // Open popup.
630                        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                            // Recursively deselect everything in the sub-items container.
645                            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 &current_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        // We need to check if some new menu item opened and then close other not in
711        // direct chain of menu items until to menu.
712        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                            // Once we found popup in chain, we must extract handle
724                            // of parent menu item to continue search.
725                            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        // Allow closing "orphaned" menus by clicking outside of them.
751        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                        // Ensure that the cursor is outside any menus.
756                        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 all other popups.
764                            close_menu_chain(self.parent(), ui);
765                        }
766                    }
767                }
768            }
769        }
770    }
771}
772
773/// Menu builder creates [`Menu`] widgets and adds them to the user interface.
774pub struct MenuBuilder {
775    widget_builder: WidgetBuilder,
776    items: Vec<Handle<MenuItem>>,
777    restrict_picking: bool,
778}
779
780impl MenuBuilder {
781    /// Creates new builder instance.
782    pub fn new(widget_builder: WidgetBuilder) -> Self {
783        Self {
784            widget_builder,
785            items: Default::default(),
786            restrict_picking: false,
787        }
788    }
789
790    /// Sets the desired items of the menu.
791    pub fn with_items(mut self, items: Vec<Handle<MenuItem>>) -> Self {
792        self.items = items;
793        self
794    }
795
796    /// Sets a flag, that defines whether the popup should restrict all the mouse input or not.
797    pub fn with_restrict_picking(mut self, restrict_picking: bool) -> Self {
798        self.restrict_picking = restrict_picking;
799        self
800    }
801
802    /// Finishes menu building and adds them to the user interface.
803    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/// Allows you to set a content of a menu item either from a pre-built "layout" with icon/text/shortcut/arrow or a custom
843/// widget.
844#[derive(Clone, Debug, Visit, Reflect, PartialEq)]
845pub enum MenuItemContent {
846    /// Quick-n-dirty way of building elements. It can cover most use cases - it builds classic menu item:
847    ///
848    /// ```text
849    ///   _________________________
850    ///  |    |      |        |   |
851    ///  |icon| text |shortcut| > |
852    ///  |____|______|________|___|
853    /// ```
854    Text {
855        /// Text of the menu item.
856        text: String,
857        /// Shortcut of the menu item.
858        shortcut: String,
859        /// Icon of the menu item. Usually it is a [`crate::image::Image`] or [`VectorImage`] widget instance.
860        icon: Handle<UiNode>,
861        /// Create an arrow or not.
862        arrow: bool,
863    },
864    /// Horizontally and Vertically centered text
865    ///
866    /// ```text
867    ///   _________________________
868    ///  |                        |
869    ///  |          text          |
870    ///  |________________________|
871    /// ```
872    TextCentered(String),
873    /// Allows to put any node into menu item. It allows to customize menu item how needed - i.e. put image in it, or other user
874    /// control.
875    Node(Handle<UiNode>),
876}
877
878impl Default for MenuItemContent {
879    fn default() -> Self {
880        Self::TextCentered(Default::default())
881    }
882}
883
884impl MenuItemContent {
885    /// Creates a menu item content with a text, a shortcut and an arrow (with no icon).
886    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    /// Creates a menu item content with a text, a shortcut and an arrow and an icon.
896    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    /// Creates a menu item content with a text and an arrow (with no icon or shortcut).
910    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    /// Creates a menu item content with a text only (with no icon, shortcut, arrow).
920    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    /// Creates a menu item content with only horizontally and vertically centered text.
930    pub fn text_centered(text: impl AsRef<str>) -> Self {
931        MenuItemContent::TextCentered(text.as_ref().to_owned())
932    }
933}
934
935/// Menu builder creates [`MenuItem`] widgets and adds them to the user interface.
936pub 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    /// Creates new menu item builder instance.
947    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    /// Sets the desired content of the menu item. In most cases [`MenuItemContent::text_no_arrow`] is enough here.
959    pub fn with_content(mut self, content: MenuItemContent) -> Self {
960        self.content = Some(content);
961        self
962    }
963
964    /// Sets the desired items of the menu.
965    pub fn with_items(mut self, items: Vec<Handle<MenuItem>>) -> Self {
966        self.items = items;
967        self
968    }
969
970    /// Allows you to specify the background content. Background node is only for decoration purpose, it can be any kind of node,
971    /// by default it is a Decorator.
972    pub fn with_back(mut self, handle: Handle<UiNode>) -> Self {
973        self.back = Some(handle);
974        self
975    }
976
977    /// Sets whether the menu item is clickable when it has sub-items or not.
978    pub fn with_clickable_when_not_empty(mut self, value: bool) -> Self {
979        self.clickable_when_not_empty = value;
980        self
981    }
982
983    /// Sets a flag, that defines whether the popup should restrict all the mouse input or not.
984    pub fn with_restrict_picking(mut self, restrict_picking: bool) -> Self {
985        self.restrict_picking = restrict_picking;
986        self
987    }
988
989    /// Finishes menu item building and adds it to the user interface.
990    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                // We'll manually control if the popup is either open or closed.
1093                .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        // "Link" popup with its parent menu item.
1122        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/// A simple wrapper over [`Popup`] widget, that holds the sub-items of a menu item and provides
1131/// an ability for keyboard navigation.
1132#[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    /// Inner popup widget of the context menu.
1137    #[component(include)]
1138    pub popup: Popup,
1139    /// Parent menu item of the context menu. Allows you to build chained context menus.
1140    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
1196/// Creates [`ContextMenu`] widgets.
1197pub struct ContextMenuBuilder {
1198    popup_builder: PopupBuilder,
1199    parent_menu_item: Handle<UiNode>,
1200}
1201
1202impl ContextMenuBuilder {
1203    /// Creates new builder instance using an instance of the [`PopupBuilder`].
1204    pub fn new(popup_builder: PopupBuilder) -> Self {
1205        Self {
1206            popup_builder,
1207            parent_menu_item: Default::default(),
1208        }
1209    }
1210
1211    /// Sets the desired parent menu item.
1212    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    /// Finishes context menu building.
1218    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    /// Finishes context menu building and adds it to the user interface.
1226    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
1308/// Creates a menu items splitter. Such splitter could be used to divide a menu into groups with
1309/// common semantics (for example: new document, new image; save, save all; close).
1310pub 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        // ====
1371        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}