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 respective widget for more info and usage examples.
23
24#![warn(missing_docs)]
25
26use crate::{
27    border::BorderBuilder,
28    brush::Brush,
29    core::{
30        algebra::Vector2, color::Color, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
31        uuid_provider, variable::InheritableVariable, visitor::prelude::*,
32    },
33    decorator::{DecoratorBuilder, DecoratorMessage},
34    define_constructor,
35    draw::DrawingContext,
36    grid::{Column, GridBuilder, Row},
37    message::{ButtonState, KeyCode, MessageDirection, OsEvent, UiMessage},
38    popup::{Placement, Popup, PopupBuilder, PopupMessage},
39    stack_panel::StackPanelBuilder,
40    style::{resource::StyleResourceExt, Style},
41    text::TextBuilder,
42    utils::{make_arrow_primitives, ArrowDirection},
43    vector_image::VectorImageBuilder,
44    widget::{self, Widget, WidgetBuilder, WidgetMessage},
45    BuildContext, Control, HorizontalAlignment, Orientation, RestrictionEntry, Thickness, UiNode,
46    UserInterface, VerticalAlignment,
47};
48use fyrox_graph::{
49    constructor::{ConstructorProvider, GraphNodeConstructor},
50    BaseSceneGraph, SceneGraph, SceneGraphNode,
51};
52use std::{
53    any::TypeId,
54    cmp::Ordering,
55    fmt::{Debug, Formatter},
56    ops::{Deref, DerefMut},
57    sync::{mpsc::Sender, Arc},
58};
59
60/// A set of messages that can be used to manipulate a [`Menu`] widget at runtime.
61#[derive(Debug, Clone, PartialEq, Eq)]
62pub enum MenuMessage {
63    /// Activates the menu so it captures mouse input by itself and allows you to open menu item by a simple mouse
64    /// hover.
65    Activate,
66    /// Deactivates the menu.
67    Deactivate,
68}
69
70impl MenuMessage {
71    define_constructor!(
72        /// Creates [`MenuMessage::Activate`] message.
73        MenuMessage:Activate => fn activate(), layout: false
74    );
75    define_constructor!(
76        /// Creates [`MenuMessage::Deactivate`] message.
77        MenuMessage:Deactivate => fn deactivate(), layout: false
78    );
79}
80
81/// A predicate that is used to sort menu items.
82#[derive(Clone)]
83pub struct SortingPredicate(
84    pub Arc<dyn Fn(&MenuItemContent, &MenuItemContent, &UserInterface) -> Ordering + Send + Sync>,
85);
86
87impl SortingPredicate {
88    /// Creates new sorting predicate.
89    pub fn new<F>(func: F) -> Self
90    where
91        F: Fn(&MenuItemContent, &MenuItemContent, &UserInterface) -> Ordering
92            + Send
93            + Sync
94            + 'static,
95    {
96        Self(Arc::new(func))
97    }
98
99    /// Creates new sorting predicate that sorts menu items by their textual content. This predicate
100    /// won't work with custom menu item content!
101    pub fn sort_by_text() -> Self {
102        Self::new(|a, b, _| {
103            if let MenuItemContent::Text { text: a_text, .. } = a {
104                if let MenuItemContent::Text { text: b_text, .. } = b {
105                    return a_text.cmp(b_text);
106                }
107            }
108
109            if let MenuItemContent::TextCentered(a_text) = a {
110                if let MenuItemContent::TextCentered(b_text) = b {
111                    return a_text.cmp(b_text);
112                }
113            }
114
115            Ordering::Equal
116        })
117    }
118}
119
120impl Debug for SortingPredicate {
121    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
122        write!(f, "SortingPredicate")
123    }
124}
125
126impl PartialEq for SortingPredicate {
127    fn eq(&self, other: &Self) -> bool {
128        std::ptr::eq(self.0.as_ref(), other.0.as_ref())
129    }
130}
131
132/// A set of messages that can be used to manipulate a [`MenuItem`] widget at runtime.
133#[derive(Debug, Clone, PartialEq)]
134pub enum MenuItemMessage {
135    /// Opens the menu item's popup with inner items.
136    Open,
137    /// Closes the menu item's popup with inner items.
138    Close {
139        /// Defines, whether the item should be deselected when closed or not.
140        deselect: bool,
141    },
142    /// The message is generated by a menu item when it is clicked.
143    Click,
144    /// Adds a new item to the menu item.
145    AddItem(Handle<UiNode>),
146    /// Removes an item from the menu item.
147    RemoveItem(Handle<UiNode>),
148    /// Sets the new items of the menu item.
149    Items(Vec<Handle<UiNode>>),
150    /// Selects/deselects the item.
151    Select(bool),
152    /// Sorts menu items by the given predicate.
153    Sort(SortingPredicate),
154}
155
156impl MenuItemMessage {
157    define_constructor!(
158        /// Creates [`MenuItemMessage::Open`] message.
159        MenuItemMessage:Open => fn open(), layout: false
160    );
161    define_constructor!(
162          /// Creates [`MenuItemMessage::Close`] message.
163        MenuItemMessage:Close => fn close(deselect: bool), layout: false
164    );
165    define_constructor!(
166          /// Creates [`MenuItemMessage::Click`] message.
167        MenuItemMessage:Click => fn click(), layout: false
168    );
169    define_constructor!(
170          /// Creates [`MenuItemMessage::AddItem`] message.
171        MenuItemMessage:AddItem => fn add_item(Handle<UiNode>), layout: false
172    );
173    define_constructor!(
174          /// Creates [`MenuItemMessage::RemoveItem`] message.
175        MenuItemMessage:RemoveItem => fn remove_item(Handle<UiNode>), layout: false
176    );
177    define_constructor!(
178          /// Creates [`MenuItemMessage::Items`] message.
179        MenuItemMessage:Items => fn items(Vec<Handle<UiNode>>), layout: false
180    );
181    define_constructor!(
182          /// Creates [`MenuItemMessage::Select`] message.
183        MenuItemMessage:Select => fn select(bool), layout: false
184    );
185    define_constructor!(
186          /// Creates [`MenuItemMessage::Sort`] message.
187        MenuItemMessage:Sort => fn sort(SortingPredicate), layout: false
188    );
189}
190
191/// Menu widget is a root widget of an arbitrary menu hierarchy. An example could be "standard" menu strip with `File`, `Edit`, `View`, etc.
192/// items. Menu widget can contain any number of children item (`File`, `Edit` in the previous example). These items should be [`MenuItem`]
193/// widgets, however you can use any widget type (for example - to create some sort of a separator).
194///
195/// ## Examples
196///
197/// The next example creates a menu with the following structure:
198///
199/// ```text
200/// |  File |  Edit |
201/// |--Save |--Undo
202/// |--Load |--Redo
203/// ```
204///
205/// ```rust
206/// # use fyrox_ui::{
207/// #     core::pool::Handle,
208/// #     menu::{MenuBuilder, MenuItemBuilder, MenuItemContent},
209/// #     widget::WidgetBuilder,
210/// #     BuildContext, UiNode,
211/// # };
212/// #
213/// fn create_menu(ctx: &mut BuildContext) -> Handle<UiNode> {
214///     MenuBuilder::new(WidgetBuilder::new())
215///         .with_items(vec![
216///             MenuItemBuilder::new(WidgetBuilder::new())
217///                 .with_content(MenuItemContent::text_no_arrow("File"))
218///                 .with_items(vec![
219///                     MenuItemBuilder::new(WidgetBuilder::new())
220///                         .with_content(MenuItemContent::text_no_arrow("Save"))
221///                         .build(ctx),
222///                     MenuItemBuilder::new(WidgetBuilder::new())
223///                         .with_content(MenuItemContent::text_no_arrow("Load"))
224///                         .build(ctx),
225///                 ])
226///                 .build(ctx),
227///             MenuItemBuilder::new(WidgetBuilder::new())
228///                 .with_content(MenuItemContent::text_no_arrow("Edit"))
229///                 .with_items(vec![
230///                     MenuItemBuilder::new(WidgetBuilder::new())
231///                         .with_content(MenuItemContent::text_no_arrow("Undo"))
232///                         .build(ctx),
233///                     MenuItemBuilder::new(WidgetBuilder::new())
234///                         .with_content(MenuItemContent::text_no_arrow("Redo"))
235///                         .build(ctx),
236///                 ])
237///                 .build(ctx),
238///         ])
239///         .build(ctx)
240/// }
241/// ```
242#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
243#[reflect(derived_type = "UiNode")]
244pub struct Menu {
245    widget: Widget,
246    active: bool,
247    #[component(include)]
248    items: ItemsContainer,
249    /// A flag, that defines whether the menu should restrict all the mouse input or not.
250    pub restrict_picking: InheritableVariable<bool>,
251}
252
253impl ConstructorProvider<UiNode, UserInterface> for Menu {
254    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
255        GraphNodeConstructor::new::<Self>()
256            .with_variant("Menu", |ui| {
257                MenuBuilder::new(WidgetBuilder::new().with_name("Menu"))
258                    .build(&mut ui.build_ctx())
259                    .into()
260            })
261            .with_group("Input")
262    }
263}
264
265crate::define_widget_deref!(Menu);
266
267uuid_provider!(Menu = "582a04f3-a7fd-4e70-bbd1-eb95e2275b75");
268
269impl Control for Menu {
270    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
271        self.widget.handle_routed_message(ui, message);
272
273        if let Some(msg) = message.data::<MenuMessage>() {
274            match msg {
275                MenuMessage::Activate => {
276                    if !self.active {
277                        if *self.restrict_picking {
278                            ui.push_picking_restriction(RestrictionEntry {
279                                handle: self.handle(),
280                                stop: false,
281                            });
282                        }
283                        self.active = true;
284                    }
285                }
286                MenuMessage::Deactivate => {
287                    if self.active {
288                        self.active = false;
289
290                        if *self.restrict_picking {
291                            ui.remove_picking_restriction(self.handle());
292                        }
293
294                        // Close descendant menu items.
295                        let mut stack = self.children().to_vec();
296                        while let Some(handle) = stack.pop() {
297                            let node = ui.node(handle);
298                            if let Some(item) = node.cast::<MenuItem>() {
299                                ui.send_message(MenuItemMessage::close(
300                                    handle,
301                                    MessageDirection::ToWidget,
302                                    true,
303                                ));
304                                // We have to search in popup content too because menu shows its content
305                                // in popup and content could be another menu item.
306                                stack.push(*item.items_panel);
307                            }
308                            // Continue depth search.
309                            stack.extend_from_slice(node.children());
310                        }
311                    }
312                }
313            }
314        } else if let Some(WidgetMessage::KeyDown(key_code)) = message.data() {
315            if !message.handled() {
316                if keyboard_navigation(ui, *key_code, self, self.handle) {
317                    message.set_handled(true);
318                } else if *key_code == KeyCode::Escape {
319                    ui.send_message(MenuMessage::deactivate(
320                        self.handle,
321                        MessageDirection::ToWidget,
322                    ));
323                    message.set_handled(true);
324                }
325            }
326        }
327    }
328
329    fn handle_os_event(
330        &mut self,
331        _self_handle: Handle<UiNode>,
332        ui: &mut UserInterface,
333        event: &OsEvent,
334    ) {
335        // Handle menu items close by clicking outside of menu item. We using
336        // raw event here because we need to know the fact that mouse was clicked
337        // and we do not care which element was clicked so we'll get here in any
338        // case.
339        if let OsEvent::MouseInput { state, .. } = event {
340            if *state == ButtonState::Pressed && self.active {
341                // TODO: Make picking more accurate - right now it works only with rects.
342                let pos = ui.cursor_position();
343                if !self.widget.screen_bounds().contains(pos) {
344                    // Also check if we clicked inside some descendant menu item - in this
345                    // case we don't need to close menu.
346                    let mut any_picked = false;
347                    let mut stack = self.children().to_vec();
348                    'depth_search: while let Some(handle) = stack.pop() {
349                        let node = ui.node(handle);
350                        if let Some(item) = node.cast::<MenuItem>() {
351                            let popup = ui.node(*item.items_panel);
352                            if popup.screen_bounds().contains(pos) && popup.is_globally_visible() {
353                                // Once we found that we clicked inside some descendant menu item
354                                // we can immediately stop search - we don't want to close menu
355                                // items popups in this case and can safely skip all stuff below.
356                                any_picked = true;
357                                break 'depth_search;
358                            }
359                            // We have to search in popup content too because menu shows its content
360                            // in popup and content could be another menu item.
361                            stack.push(*item.items_panel);
362                        }
363                        // Continue depth search.
364                        stack.extend_from_slice(node.children());
365                    }
366
367                    if !any_picked {
368                        ui.send_message(MenuMessage::deactivate(
369                            self.handle(),
370                            MessageDirection::ToWidget,
371                        ));
372                    }
373                }
374            }
375        }
376    }
377}
378
379/// A set of possible placements of a popup with items of a menu item.
380#[derive(Copy, Clone, PartialOrd, PartialEq, Eq, Hash, Visit, Reflect, Default, Debug)]
381pub enum MenuItemPlacement {
382    /// Bottom placement.
383    Bottom,
384    /// Right placement.
385    #[default]
386    Right,
387}
388
389#[derive(Copy, Clone, PartialOrd, PartialEq, Eq, Hash, Visit, Reflect, Default, Debug)]
390enum NavigationDirection {
391    #[default]
392    Horizontal,
393    Vertical,
394}
395
396#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
397#[doc(hidden)]
398pub struct ItemsContainer {
399    #[doc(hidden)]
400    pub items: InheritableVariable<Vec<Handle<UiNode>>>,
401    navigation_direction: NavigationDirection,
402}
403
404impl Deref for ItemsContainer {
405    type Target = Vec<Handle<UiNode>>;
406
407    fn deref(&self) -> &Self::Target {
408        self.items.deref()
409    }
410}
411
412impl DerefMut for ItemsContainer {
413    fn deref_mut(&mut self) -> &mut Self::Target {
414        self.items.deref_mut()
415    }
416}
417
418impl ItemsContainer {
419    fn selected_item_index(&self, ui: &UserInterface) -> Option<usize> {
420        for (index, item) in self.items.iter().enumerate() {
421            if let Some(item_ref) = ui.try_get_of_type::<MenuItem>(*item) {
422                if *item_ref.is_selected {
423                    return Some(index);
424                }
425            }
426        }
427
428        None
429    }
430
431    fn next_item_to_select_in_dir(&self, ui: &UserInterface, dir: isize) -> Option<Handle<UiNode>> {
432        self.selected_item_index(ui)
433            .map(|i| i as isize)
434            .and_then(|mut index| {
435                // Do a full circle search.
436                let count = self.items.len() as isize;
437                for _ in 0..count {
438                    index += dir;
439                    if index < 0 {
440                        index += count;
441                    }
442                    index %= count;
443                    let handle = self.items.get(index as usize).cloned();
444                    if let Some(item) = handle.and_then(|h| ui.try_get_of_type::<MenuItem>(h)) {
445                        if item.enabled() {
446                            return handle;
447                        }
448                    }
449                }
450
451                None
452            })
453    }
454}
455
456/// 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
457/// arbitrary hierarchies. See [`Menu`] docs for examples.
458#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
459#[reflect(derived_type = "UiNode")]
460pub struct MenuItem {
461    /// Base widget of the menu item.
462    pub widget: Widget,
463    /// Current items of the menu item
464    #[component(include)]
465    pub items_container: ItemsContainer,
466    /// A handle of a popup that holds the items of the menu item.
467    pub items_panel: InheritableVariable<Handle<UiNode>>,
468    /// A handle of a panel widget that arranges items of the menu item.
469    pub panel: InheritableVariable<Handle<UiNode>>,
470    /// Current placement of the menu item.
471    pub placement: InheritableVariable<MenuItemPlacement>,
472    /// A flag, that defines whether the menu item is clickable when it has sub-items or not.
473    pub clickable_when_not_empty: InheritableVariable<bool>,
474    /// A handle to the decorator of the item.
475    pub decorator: InheritableVariable<Handle<UiNode>>,
476    /// Is this item selected or not.
477    pub is_selected: InheritableVariable<bool>,
478    /// An arrow primitive that is used to indicate that there's sub-items in the menu item.
479    pub arrow: InheritableVariable<Handle<UiNode>>,
480    /// Content of the menu item with which it was created.
481    pub content: InheritableVariable<Option<MenuItemContent>>,
482}
483
484impl ConstructorProvider<UiNode, UserInterface> for MenuItem {
485    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
486        GraphNodeConstructor::new::<Self>()
487            .with_variant("Menu Item", |ui| {
488                MenuItemBuilder::new(WidgetBuilder::new().with_name("Menu Item"))
489                    .build(&mut ui.build_ctx())
490                    .into()
491            })
492            .with_group("Input")
493    }
494}
495
496crate::define_widget_deref!(MenuItem);
497
498impl MenuItem {
499    fn is_opened(&self, ui: &UserInterface) -> bool {
500        ui.try_get_of_type::<ContextMenu>(*self.items_panel)
501            .is_some_and(|items_panel| *items_panel.popup.is_open)
502    }
503
504    fn sync_arrow_visibility(&self, ui: &UserInterface) {
505        ui.send_message(WidgetMessage::visibility(
506            *self.arrow,
507            MessageDirection::ToWidget,
508            !self.items_container.is_empty(),
509        ));
510    }
511}
512
513// MenuItem uses popup to show its content, popup can be top-most only if it is
514// direct child of root canvas of UI. This fact adds some complications to search
515// of parent menu - we can't just traverse the tree because popup is not a child
516// of menu item, instead we trying to fetch handle to parent menu item from popup's
517// user data and continue up-search until we find menu.
518fn find_menu(from: Handle<UiNode>, ui: &UserInterface) -> Handle<UiNode> {
519    let mut handle = from;
520    while handle.is_some() {
521        if let Some((_, panel)) = ui.find_component_up::<ContextMenu>(handle) {
522            // Continue search from parent menu item of popup.
523            handle = panel.parent_menu_item;
524        } else {
525            // Maybe we have Menu as parent for MenuItem.
526            return ui.find_handle_up(handle, &mut |n| n.cast::<Menu>().is_some());
527        }
528    }
529    Default::default()
530}
531
532fn is_any_menu_item_contains_point(ui: &UserInterface, pt: Vector2<f32>) -> bool {
533    for (handle, menu) in ui
534        .nodes()
535        .pair_iter()
536        .filter_map(|(h, n)| n.query_component::<MenuItem>().map(|menu| (h, menu)))
537    {
538        if ui.find_component_up::<Menu>(handle).is_none()
539            && menu.is_globally_visible()
540            && menu.screen_bounds().contains(pt)
541        {
542            return true;
543        }
544    }
545    false
546}
547
548fn close_menu_chain(from: Handle<UiNode>, ui: &UserInterface) {
549    let mut handle = from;
550    while handle.is_some() {
551        let popup_handle = ui.find_handle_up(handle, &mut |n| n.has_component::<ContextMenu>());
552
553        if let Some(panel) = ui.try_get_of_type::<ContextMenu>(popup_handle) {
554            if *panel.popup.is_open {
555                ui.send_message(PopupMessage::close(
556                    popup_handle,
557                    MessageDirection::ToWidget,
558                ));
559            }
560
561            // Continue search from parent menu item of popup.
562            handle = panel.parent_menu_item;
563        } else {
564            // Prevent infinite loops.
565            break;
566        }
567    }
568}
569
570uuid_provider!(MenuItem = "72e002c6-6060-4583-b5b7-0c5500244fef");
571
572impl Control for MenuItem {
573    fn on_remove(&self, sender: &Sender<UiMessage>) {
574        // Popup won't be deleted with the menu item, because it is not the child of the item.
575        // So we have to remove it manually.
576        sender
577            .send(WidgetMessage::remove(
578                *self.items_panel,
579                MessageDirection::ToWidget,
580            ))
581            .unwrap();
582    }
583
584    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
585        self.widget.handle_routed_message(ui, message);
586
587        if let Some(msg) = message.data::<WidgetMessage>() {
588            match msg {
589                WidgetMessage::MouseDown { .. } => {
590                    let menu = find_menu(self.parent(), ui);
591                    if menu.is_some() {
592                        if self.is_opened(ui) {
593                            ui.send_message(MenuItemMessage::close(
594                                self.handle(),
595                                MessageDirection::ToWidget,
596                                true,
597                            ));
598                            ui.send_message(MenuMessage::deactivate(
599                                menu,
600                                MessageDirection::ToWidget,
601                            ));
602                        } else {
603                            // Activate menu so it user will be able to open submenus by
604                            // mouse hover.
605                            ui.send_message(MenuMessage::activate(
606                                menu,
607                                MessageDirection::ToWidget,
608                            ));
609
610                            ui.send_message(MenuItemMessage::open(
611                                self.handle(),
612                                MessageDirection::ToWidget,
613                            ));
614                        }
615                    }
616                }
617                WidgetMessage::MouseUp { .. } => {
618                    if !message.handled() {
619                        if self.items_container.is_empty() || *self.clickable_when_not_empty {
620                            ui.send_message(MenuItemMessage::click(
621                                self.handle(),
622                                MessageDirection::ToWidget,
623                            ));
624                        }
625                        if self.items_container.is_empty() {
626                            let menu = find_menu(self.parent(), ui);
627                            if menu.is_some() {
628                                // Deactivate menu if we have one.
629                                ui.send_message(MenuMessage::deactivate(
630                                    menu,
631                                    MessageDirection::ToWidget,
632                                ));
633                            } else {
634                                // Or close menu chain if menu item is in "orphaned" state.
635                                close_menu_chain(self.parent(), ui);
636                            }
637                        }
638                        message.set_handled(true);
639                    }
640                }
641                WidgetMessage::MouseEnter => {
642                    // While parent menu active it is possible to open submenus
643                    // by simple mouse hover.
644                    let menu = find_menu(self.parent(), ui);
645                    let open = if menu.is_some() {
646                        if let Some(menu) = ui.node(menu).cast::<Menu>() {
647                            menu.active
648                        } else {
649                            false
650                        }
651                    } else {
652                        true
653                    };
654                    if open {
655                        ui.send_message(MenuItemMessage::open(
656                            self.handle(),
657                            MessageDirection::ToWidget,
658                        ));
659                    }
660                }
661                WidgetMessage::MouseLeave => {
662                    if !self.is_opened(ui) {
663                        ui.send_message(MenuItemMessage::select(
664                            self.handle,
665                            MessageDirection::ToWidget,
666                            false,
667                        ));
668                    }
669                }
670                WidgetMessage::KeyDown(key_code) => {
671                    if !message.handled() && *self.is_selected && *key_code == KeyCode::Enter {
672                        ui.send_message(MenuItemMessage::click(
673                            self.handle,
674                            MessageDirection::FromWidget,
675                        ));
676                        let menu = find_menu(self.parent(), ui);
677                        ui.send_message(MenuMessage::deactivate(menu, MessageDirection::ToWidget));
678                        message.set_handled(true);
679                    }
680                }
681                _ => {}
682            }
683        } else if let Some(msg) = message.data::<MenuItemMessage>() {
684            if message.destination() == self.handle
685                && message.direction() == MessageDirection::ToWidget
686            {
687                match msg {
688                    MenuItemMessage::Select(selected) => {
689                        if *self.is_selected != *selected {
690                            self.is_selected.set_value_and_mark_modified(*selected);
691
692                            ui.send_message(DecoratorMessage::select(
693                                *self.decorator,
694                                MessageDirection::ToWidget,
695                                *selected,
696                            ));
697
698                            if *selected {
699                                ui.send_message(WidgetMessage::focus(
700                                    self.handle,
701                                    MessageDirection::ToWidget,
702                                ));
703                            }
704                        }
705                    }
706                    MenuItemMessage::Open => {
707                        if !self.items_container.is_empty() && !self.is_opened(ui) {
708                            let placement = match *self.placement {
709                                MenuItemPlacement::Bottom => Placement::LeftBottom(self.handle),
710                                MenuItemPlacement::Right => Placement::RightTop(self.handle),
711                            };
712
713                            if !*self.is_selected {
714                                ui.send_message(MenuItemMessage::select(
715                                    self.handle,
716                                    MessageDirection::ToWidget,
717                                    true,
718                                ));
719                            }
720
721                            // Open popup.
722                            ui.send_message(PopupMessage::placement(
723                                *self.items_panel,
724                                MessageDirection::ToWidget,
725                                placement,
726                            ));
727                            ui.send_message(PopupMessage::open(
728                                *self.items_panel,
729                                MessageDirection::ToWidget,
730                            ));
731                        }
732                    }
733                    MenuItemMessage::Close { deselect } => {
734                        if let Some(panel) =
735                            ui.node(*self.items_panel).query_component::<ContextMenu>()
736                        {
737                            if *panel.popup.is_open {
738                                ui.send_message(PopupMessage::close(
739                                    *self.items_panel,
740                                    MessageDirection::ToWidget,
741                                ));
742
743                                if *deselect && *self.is_selected {
744                                    ui.send_message(MenuItemMessage::select(
745                                        self.handle,
746                                        MessageDirection::ToWidget,
747                                        false,
748                                    ));
749                                }
750
751                                // Recursively deselect everything in the sub-items container.
752                                for &item in &*self.items_container.items {
753                                    ui.send_message(MenuItemMessage::close(
754                                        item,
755                                        MessageDirection::ToWidget,
756                                        true,
757                                    ));
758                                }
759                            }
760                        }
761                    }
762                    MenuItemMessage::Click => {}
763                    MenuItemMessage::AddItem(item) => {
764                        ui.send_message(WidgetMessage::link(
765                            *item,
766                            MessageDirection::ToWidget,
767                            *self.panel,
768                        ));
769                        self.items_container.push(*item);
770                        if self.items_container.len() == 1 {
771                            self.sync_arrow_visibility(ui);
772                        }
773                    }
774                    MenuItemMessage::RemoveItem(item) => {
775                        if let Some(position) =
776                            self.items_container.iter().position(|i| *i == *item)
777                        {
778                            self.items_container.remove(position);
779
780                            ui.send_message(WidgetMessage::remove(
781                                *item,
782                                MessageDirection::ToWidget,
783                            ));
784
785                            if self.items_container.is_empty() {
786                                self.sync_arrow_visibility(ui);
787                            }
788                        }
789                    }
790                    MenuItemMessage::Items(items) => {
791                        for &current_item in self.items_container.iter() {
792                            ui.send_message(WidgetMessage::remove(
793                                current_item,
794                                MessageDirection::ToWidget,
795                            ));
796                        }
797
798                        for &item in items {
799                            ui.send_message(WidgetMessage::link(
800                                item,
801                                MessageDirection::ToWidget,
802                                *self.panel,
803                            ));
804                        }
805
806                        self.items_container
807                            .items
808                            .set_value_and_mark_modified(items.clone());
809
810                        self.sync_arrow_visibility(ui);
811                    }
812                    MenuItemMessage::Sort(predicate) => {
813                        let predicate = predicate.clone();
814                        ui.send_message(WidgetMessage::sort_children(
815                            *self.panel,
816                            MessageDirection::ToWidget,
817                            widget::SortingPredicate::new(move |a, b, ui| {
818                                let item_a = ui.try_get_of_type::<MenuItem>(a).unwrap();
819                                let item_b = ui.try_get_of_type::<MenuItem>(b).unwrap();
820
821                                if let (Some(a_content), Some(b_content)) =
822                                    (item_a.content.as_ref(), item_b.content.as_ref())
823                                {
824                                    predicate.0(a_content, b_content, ui)
825                                } else {
826                                    Ordering::Equal
827                                }
828                            }),
829                        ));
830                    }
831                }
832            }
833        }
834    }
835
836    fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
837        // We need to check if some new menu item opened and then close other not in
838        // direct chain of menu items until to menu.
839        if message.destination() != self.handle() {
840            if let Some(MenuItemMessage::Open) = message.data::<MenuItemMessage>() {
841                let mut found = false;
842                let mut handle = message.destination();
843                while handle.is_some() {
844                    if handle == self.handle() {
845                        found = true;
846                        break;
847                    } else {
848                        let node = ui.node(handle);
849                        if let Some(panel) = node.component_ref::<ContextMenu>() {
850                            // Once we found popup in chain, we must extract handle
851                            // of parent menu item to continue search.
852                            handle = panel.parent_menu_item;
853                        } else {
854                            handle = node.parent();
855                        }
856                    }
857                }
858
859                if !found {
860                    if let Some(panel) = ui.node(*self.items_panel).query_component::<ContextMenu>()
861                    {
862                        if *panel.popup.is_open {
863                            ui.send_message(MenuItemMessage::close(
864                                self.handle(),
865                                MessageDirection::ToWidget,
866                                true,
867                            ));
868                        }
869                    }
870                }
871            }
872        }
873    }
874
875    fn handle_os_event(
876        &mut self,
877        _self_handle: Handle<UiNode>,
878        ui: &mut UserInterface,
879        event: &OsEvent,
880    ) {
881        // Allow closing "orphaned" menus by clicking outside of them.
882        if let OsEvent::MouseInput { state, .. } = event {
883            if *state == ButtonState::Pressed {
884                if let Some(panel) = ui.node(*self.items_panel).query_component::<ContextMenu>() {
885                    if *panel.popup.is_open {
886                        // Ensure that cursor is outside of any menus.
887                        if !is_any_menu_item_contains_point(ui, ui.cursor_position())
888                            && find_menu(self.parent(), ui).is_none()
889                        {
890                            if *panel.popup.is_open {
891                                ui.send_message(PopupMessage::close(
892                                    *self.items_panel,
893                                    MessageDirection::ToWidget,
894                                ));
895                            }
896
897                            // Close all other popups.
898                            close_menu_chain(self.parent(), ui);
899                        }
900                    }
901                }
902            }
903        }
904    }
905}
906
907/// Menu builder creates [`Menu`] widgets and adds them to the user interface.
908pub struct MenuBuilder {
909    widget_builder: WidgetBuilder,
910    items: Vec<Handle<UiNode>>,
911    restrict_picking: bool,
912}
913
914impl MenuBuilder {
915    /// Creates new builder instance.
916    pub fn new(widget_builder: WidgetBuilder) -> Self {
917        Self {
918            widget_builder,
919            items: Default::default(),
920            restrict_picking: false,
921        }
922    }
923
924    /// Sets the desired items of the menu.
925    pub fn with_items(mut self, items: Vec<Handle<UiNode>>) -> Self {
926        self.items = items;
927        self
928    }
929
930    /// Sets a flag, that defines whether the popup should restrict all the mouse input or not.
931    pub fn with_restrict_picking(mut self, restrict_picking: bool) -> Self {
932        self.restrict_picking = restrict_picking;
933        self
934    }
935
936    /// Finishes menu building and adds them to the user interface.
937    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
938        for &item in self.items.iter() {
939            if let Some(item) = ctx[item].cast_mut::<MenuItem>() {
940                item.placement
941                    .set_value_and_mark_modified(MenuItemPlacement::Bottom);
942            }
943        }
944
945        let back = BorderBuilder::new(
946            WidgetBuilder::new()
947                .with_background(ctx.style.property(Style::BRUSH_PRIMARY))
948                .with_child(
949                    StackPanelBuilder::new(
950                        WidgetBuilder::new().with_children(self.items.iter().cloned()),
951                    )
952                    .with_orientation(Orientation::Horizontal)
953                    .build(ctx),
954                ),
955        )
956        .build(ctx);
957
958        let menu = Menu {
959            widget: self
960                .widget_builder
961                .with_handle_os_events(true)
962                .with_child(back)
963                .build(ctx),
964            active: false,
965            items: ItemsContainer {
966                items: self.items.into(),
967                navigation_direction: NavigationDirection::Horizontal,
968            },
969            restrict_picking: self.restrict_picking.into(),
970        };
971
972        ctx.add_node(UiNode::new(menu))
973    }
974}
975
976/// Allows you to set a content of a menu item either from a pre-built "layout" with icon/text/shortcut/arrow or a custom
977/// widget.
978#[derive(Clone, Debug, Visit, Reflect, PartialEq)]
979pub enum MenuItemContent {
980    /// Quick-n-dirty way of building elements. It can cover most of use cases - it builds classic menu item:
981    ///
982    /// ```text
983    ///   _________________________
984    ///  |    |      |        |   |
985    ///  |icon| text |shortcut| > |
986    ///  |____|______|________|___|
987    /// ```
988    Text {
989        /// Text of the menu item.
990        text: String,
991        /// Shortcut of the menu item.
992        shortcut: String,
993        /// Icon of the menu item. Usually it is a [`crate::image::Image`] or [`crate::vector_image::VectorImage`] widget instance.
994        icon: Handle<UiNode>,
995        /// Create an arrow or not.
996        arrow: bool,
997    },
998    /// Horizontally and Vertically centered text
999    ///
1000    /// ```text
1001    ///   _________________________
1002    ///  |                        |
1003    ///  |          text          |
1004    ///  |________________________|
1005    /// ```
1006    TextCentered(String),
1007    /// 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
1008    /// control.
1009    Node(Handle<UiNode>),
1010}
1011
1012impl Default for MenuItemContent {
1013    fn default() -> Self {
1014        Self::TextCentered(Default::default())
1015    }
1016}
1017
1018impl MenuItemContent {
1019    /// Creates a menu item content with a text, a shortcut and an arrow (with no icon).
1020    pub fn text_with_shortcut(text: impl AsRef<str>, shortcut: impl AsRef<str>) -> Self {
1021        MenuItemContent::Text {
1022            text: text.as_ref().to_owned(),
1023            shortcut: shortcut.as_ref().to_owned(),
1024            icon: Default::default(),
1025            arrow: true,
1026        }
1027    }
1028
1029    /// Creates a menu item content with a text and an arrow (with no icon or shortcut).
1030    pub fn text(text: impl AsRef<str>) -> Self {
1031        MenuItemContent::Text {
1032            text: text.as_ref().to_owned(),
1033            shortcut: Default::default(),
1034            icon: Default::default(),
1035            arrow: true,
1036        }
1037    }
1038
1039    /// Creates a menu item content with a text only (with no icon, shortcut, arrow).
1040    pub fn text_no_arrow(text: impl AsRef<str>) -> Self {
1041        MenuItemContent::Text {
1042            text: text.as_ref().to_owned(),
1043            shortcut: Default::default(),
1044            icon: Default::default(),
1045            arrow: false,
1046        }
1047    }
1048
1049    /// Creates a menu item content with only horizontally and vertically centered text.
1050    pub fn text_centered(text: impl AsRef<str>) -> Self {
1051        MenuItemContent::TextCentered(text.as_ref().to_owned())
1052    }
1053}
1054
1055/// Menu builder creates [`MenuItem`] widgets and adds them to the user interface.
1056pub struct MenuItemBuilder {
1057    widget_builder: WidgetBuilder,
1058    items: Vec<Handle<UiNode>>,
1059    content: Option<MenuItemContent>,
1060    back: Option<Handle<UiNode>>,
1061    clickable_when_not_empty: bool,
1062    restrict_picking: bool,
1063}
1064
1065impl MenuItemBuilder {
1066    /// Creates new menu item builder instance.
1067    pub fn new(widget_builder: WidgetBuilder) -> Self {
1068        Self {
1069            widget_builder,
1070            items: Default::default(),
1071            content: None,
1072            back: None,
1073            clickable_when_not_empty: false,
1074            restrict_picking: false,
1075        }
1076    }
1077
1078    /// Sets the desired content of the menu item. In most cases [`MenuItemContent::text_no_arrow`] is enough here.
1079    pub fn with_content(mut self, content: MenuItemContent) -> Self {
1080        self.content = Some(content);
1081        self
1082    }
1083
1084    /// Sets the desired items of the menu.
1085    pub fn with_items(mut self, items: Vec<Handle<UiNode>>) -> Self {
1086        self.items = items;
1087        self
1088    }
1089
1090    /// Allows you to specify the background content. Background node is only for decoration purpose, it can be any kind of node,
1091    /// by default it is Decorator.
1092    pub fn with_back(mut self, handle: Handle<UiNode>) -> Self {
1093        self.back = Some(handle);
1094        self
1095    }
1096
1097    /// Sets whether the menu item is clickable when it has sub-items or not.
1098    pub fn with_clickable_when_not_empty(mut self, value: bool) -> Self {
1099        self.clickable_when_not_empty = value;
1100        self
1101    }
1102
1103    /// Sets a flag, that defines whether the popup should restrict all the mouse input or not.
1104    pub fn with_restrict_picking(mut self, restrict_picking: bool) -> Self {
1105        self.restrict_picking = restrict_picking;
1106        self
1107    }
1108
1109    /// Finishes menu item building and adds it to the user interface.
1110    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
1111        let mut arrow_widget = Handle::NONE;
1112        let content = match self.content.as_ref() {
1113            None => Handle::NONE,
1114            Some(MenuItemContent::Text {
1115                text,
1116                shortcut,
1117                icon,
1118                arrow,
1119            }) => GridBuilder::new(
1120                WidgetBuilder::new()
1121                    .with_vertical_alignment(VerticalAlignment::Center)
1122                    .with_child(*icon)
1123                    .with_child(
1124                        TextBuilder::new(
1125                            WidgetBuilder::new()
1126                                .with_margin(Thickness::left(2.0))
1127                                .on_column(1)
1128                                .with_vertical_alignment(VerticalAlignment::Center),
1129                        )
1130                        .with_text(text)
1131                        .build(ctx),
1132                    )
1133                    .with_child(
1134                        TextBuilder::new(
1135                            WidgetBuilder::new()
1136                                .with_horizontal_alignment(HorizontalAlignment::Right)
1137                                .with_margin(Thickness::uniform(1.0))
1138                                .on_column(2),
1139                        )
1140                        .with_text(shortcut)
1141                        .build(ctx),
1142                    )
1143                    .with_child({
1144                        arrow_widget = if *arrow {
1145                            VectorImageBuilder::new(
1146                                WidgetBuilder::new()
1147                                    .with_visibility(!self.items.is_empty())
1148                                    .on_column(3)
1149                                    .with_width(8.0)
1150                                    .with_height(8.0)
1151                                    .with_foreground(ctx.style.property(Style::BRUSH_BRIGHT))
1152                                    .with_horizontal_alignment(HorizontalAlignment::Center)
1153                                    .with_vertical_alignment(VerticalAlignment::Center),
1154                            )
1155                            .with_primitives(make_arrow_primitives(ArrowDirection::Right, 8.0))
1156                            .build(ctx)
1157                        } else {
1158                            Handle::NONE
1159                        };
1160                        arrow_widget
1161                    }),
1162            )
1163            .add_row(Row::auto())
1164            .add_column(Column::auto())
1165            .add_column(Column::stretch())
1166            .add_column(Column::auto())
1167            .add_column(Column::strict(10.0))
1168            .add_column(Column::strict(5.0))
1169            .build(ctx),
1170            Some(MenuItemContent::TextCentered(text)) => {
1171                TextBuilder::new(WidgetBuilder::new().with_margin(Thickness::left_right(5.0)))
1172                    .with_text(text)
1173                    .with_horizontal_text_alignment(HorizontalAlignment::Center)
1174                    .with_vertical_text_alignment(VerticalAlignment::Center)
1175                    .build(ctx)
1176            }
1177            Some(MenuItemContent::Node(node)) => *node,
1178        };
1179
1180        let decorator = self.back.unwrap_or_else(|| {
1181            DecoratorBuilder::new(
1182                BorderBuilder::new(WidgetBuilder::new())
1183                    .with_stroke_thickness(Thickness::uniform(0.0).into()),
1184            )
1185            .with_hover_brush(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
1186            .with_selected_brush(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
1187            .with_normal_brush(ctx.style.property(Style::BRUSH_PRIMARY))
1188            .with_pressed_brush(Brush::Solid(Color::TRANSPARENT).into())
1189            .with_pressable(false)
1190            .build(ctx)
1191        });
1192
1193        if content.is_some() {
1194            ctx.link(content, decorator);
1195        }
1196
1197        let panel;
1198        let items_panel = ContextMenuBuilder::new(
1199            PopupBuilder::new(WidgetBuilder::new().with_min_size(Vector2::new(10.0, 10.0)))
1200                .with_content({
1201                    panel = StackPanelBuilder::new(
1202                        WidgetBuilder::new().with_children(self.items.iter().cloned()),
1203                    )
1204                    .build(ctx);
1205                    panel
1206                })
1207                .with_restrict_picking(self.restrict_picking)
1208                // We'll manually control if popup is either open or closed.
1209                .stays_open(true),
1210        )
1211        .build(ctx);
1212
1213        let menu = MenuItem {
1214            widget: self
1215                .widget_builder
1216                .with_handle_os_events(true)
1217                .with_preview_messages(true)
1218                .with_child(decorator)
1219                .build(ctx),
1220            items_panel: items_panel.into(),
1221            items_container: ItemsContainer {
1222                items: self.items.into(),
1223                navigation_direction: NavigationDirection::Vertical,
1224            },
1225            placement: MenuItemPlacement::Right.into(),
1226            panel: panel.into(),
1227            clickable_when_not_empty: false.into(),
1228            decorator: decorator.into(),
1229            is_selected: Default::default(),
1230            arrow: arrow_widget.into(),
1231            content: self.content.into(),
1232        };
1233
1234        let handle = ctx.add_node(UiNode::new(menu));
1235
1236        // "Link" popup with its parent menu item.
1237        if let Some(popup) = ctx[items_panel].cast_mut::<ContextMenu>() {
1238            popup.parent_menu_item = handle;
1239        }
1240
1241        handle
1242    }
1243}
1244
1245/// A simple wrapper over [`Popup`] widget, that holds the sub-items of a menu item and provides
1246/// an ability for keyboard navigation.
1247#[derive(Default, Clone, Debug, Visit, Reflect, TypeUuidProvider, ComponentProvider)]
1248#[type_uuid(id = "ad8e9e76-c213-4232-9bab-80ebcabd69fa")]
1249#[reflect(derived_type = "UiNode")]
1250pub struct ContextMenu {
1251    /// Inner popup widget of the context menu.
1252    #[component(include)]
1253    pub popup: Popup,
1254    /// Parent menu item of the context menu. Allows you to build chained context menus.
1255    pub parent_menu_item: Handle<UiNode>,
1256}
1257
1258impl ConstructorProvider<UiNode, UserInterface> for ContextMenu {
1259    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
1260        GraphNodeConstructor::new::<Self>()
1261            .with_variant("Context Menu", |ui| {
1262                ContextMenuBuilder::new(
1263                    PopupBuilder::new(WidgetBuilder::new().with_name("Context Menu"))
1264                        .with_restrict_picking(false),
1265                )
1266                .build(&mut ui.build_ctx())
1267                .into()
1268            })
1269            .with_group("Input")
1270    }
1271}
1272
1273impl Deref for ContextMenu {
1274    type Target = Widget;
1275
1276    fn deref(&self) -> &Self::Target {
1277        &self.popup.widget
1278    }
1279}
1280
1281impl DerefMut for ContextMenu {
1282    fn deref_mut(&mut self) -> &mut Self::Target {
1283        &mut self.popup.widget
1284    }
1285}
1286
1287impl Control for ContextMenu {
1288    fn on_remove(&self, sender: &Sender<UiMessage>) {
1289        self.popup.on_remove(sender)
1290    }
1291
1292    fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
1293        self.popup.measure_override(ui, available_size)
1294    }
1295
1296    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
1297        self.popup.arrange_override(ui, final_size)
1298    }
1299
1300    fn draw(&self, drawing_context: &mut DrawingContext) {
1301        self.popup.draw(drawing_context)
1302    }
1303
1304    fn post_draw(&self, drawing_context: &mut DrawingContext) {
1305        self.popup.post_draw(drawing_context)
1306    }
1307
1308    fn update(&mut self, dt: f32, ui: &mut UserInterface) {
1309        self.popup.update(dt, ui);
1310    }
1311
1312    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
1313        self.popup.handle_routed_message(ui, message);
1314
1315        if let Some(WidgetMessage::KeyDown(key_code)) = message.data() {
1316            if !message.handled() {
1317                if let Some(parent_menu_item) = ui.try_get_node(self.parent_menu_item) {
1318                    if keyboard_navigation(
1319                        ui,
1320                        *key_code,
1321                        parent_menu_item.deref(),
1322                        self.parent_menu_item,
1323                    ) {
1324                        message.set_handled(true);
1325                    }
1326                }
1327            }
1328        }
1329    }
1330
1331    fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
1332        self.popup.preview_message(ui, message)
1333    }
1334
1335    fn handle_os_event(
1336        &mut self,
1337        self_handle: Handle<UiNode>,
1338        ui: &mut UserInterface,
1339        event: &OsEvent,
1340    ) {
1341        self.popup.handle_os_event(self_handle, ui, event)
1342    }
1343}
1344
1345/// Creates [`ContextMenu`] widgets.
1346pub struct ContextMenuBuilder {
1347    popup_builder: PopupBuilder,
1348    parent_menu_item: Handle<UiNode>,
1349}
1350
1351impl ContextMenuBuilder {
1352    /// Creates new builder instance using an instance of the [`PopupBuilder`].
1353    pub fn new(popup_builder: PopupBuilder) -> Self {
1354        Self {
1355            popup_builder,
1356            parent_menu_item: Default::default(),
1357        }
1358    }
1359
1360    /// Sets the desired parent menu item.
1361    pub fn with_parent_menu_item(mut self, parent_menu_item: Handle<UiNode>) -> Self {
1362        self.parent_menu_item = parent_menu_item;
1363        self
1364    }
1365
1366    /// Finishes context menu building.
1367    pub fn build_context_menu(self, ctx: &mut BuildContext) -> ContextMenu {
1368        ContextMenu {
1369            popup: self.popup_builder.build_popup(ctx),
1370            parent_menu_item: self.parent_menu_item,
1371        }
1372    }
1373
1374    /// Finishes context menu building and adds it to the user interface.
1375    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
1376        let context_menu = self.build_context_menu(ctx);
1377        ctx.add_node(UiNode::new(context_menu))
1378    }
1379}
1380
1381fn keyboard_navigation(
1382    ui: &UserInterface,
1383    key_code: KeyCode,
1384    parent_menu_item: &dyn Control,
1385    parent_menu_item_handle: Handle<UiNode>,
1386) -> bool {
1387    let Some(items_container) = parent_menu_item
1388        .query_component_ref(TypeId::of::<ItemsContainer>())
1389        .and_then(|c| c.downcast_ref::<ItemsContainer>())
1390    else {
1391        return false;
1392    };
1393
1394    let (close_key, enter_key, next_key, prev_key) = match items_container.navigation_direction {
1395        NavigationDirection::Horizontal => (
1396            KeyCode::ArrowUp,
1397            KeyCode::ArrowDown,
1398            KeyCode::ArrowRight,
1399            KeyCode::ArrowLeft,
1400        ),
1401        NavigationDirection::Vertical => (
1402            KeyCode::ArrowLeft,
1403            KeyCode::ArrowRight,
1404            KeyCode::ArrowDown,
1405            KeyCode::ArrowUp,
1406        ),
1407    };
1408
1409    if key_code == close_key {
1410        ui.send_message(MenuItemMessage::close(
1411            parent_menu_item_handle,
1412            MessageDirection::ToWidget,
1413            false,
1414        ));
1415        return true;
1416    } else if key_code == enter_key {
1417        if let Some(selected_item_index) = items_container.selected_item_index(ui) {
1418            let selected_item = items_container.items[selected_item_index];
1419
1420            ui.send_message(MenuItemMessage::open(
1421                selected_item,
1422                MessageDirection::ToWidget,
1423            ));
1424
1425            if let Some(selected_item_ref) = ui.try_get_of_type::<MenuItem>(selected_item) {
1426                if let Some(first_item) = selected_item_ref.items_container.first() {
1427                    ui.send_message(MenuItemMessage::select(
1428                        *first_item,
1429                        MessageDirection::ToWidget,
1430                        true,
1431                    ));
1432                }
1433            }
1434        }
1435        return true;
1436    } else if key_code == next_key || key_code == prev_key {
1437        if let Some(selected_item_index) = items_container.selected_item_index(ui) {
1438            let dir = if key_code == next_key {
1439                1
1440            } else if key_code == prev_key {
1441                -1
1442            } else {
1443                unreachable!()
1444            };
1445
1446            if let Some(new_selection) = items_container.next_item_to_select_in_dir(ui, dir) {
1447                ui.send_message(MenuItemMessage::select(
1448                    items_container.items[selected_item_index],
1449                    MessageDirection::ToWidget,
1450                    false,
1451                ));
1452                ui.send_message(MenuItemMessage::select(
1453                    new_selection,
1454                    MessageDirection::ToWidget,
1455                    true,
1456                ));
1457
1458                return true;
1459            }
1460        } else if let Some(first_item) = items_container.items.first() {
1461            ui.send_message(MenuItemMessage::select(
1462                *first_item,
1463                MessageDirection::ToWidget,
1464                true,
1465            ));
1466
1467            return true;
1468        }
1469    }
1470
1471    false
1472}
1473
1474/// Creates a menu items splitter. Such splitter could be used to divide a menu into groups with
1475/// common semantics (for example: new document, new image; save, save all; close).
1476pub fn make_menu_splitter(ctx: &mut BuildContext) -> Handle<UiNode> {
1477    BorderBuilder::new(
1478        WidgetBuilder::new()
1479            .with_height(1.0)
1480            .with_margin(Thickness::top_bottom(1.0))
1481            .with_foreground(ctx.style.property(Style::BRUSH_LIGHTEST)),
1482    )
1483    .build(ctx)
1484}
1485
1486#[cfg(test)]
1487mod test {
1488    use crate::menu::{MenuBuilder, MenuItemBuilder};
1489    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
1490
1491    #[test]
1492    fn test_deletion() {
1493        test_widget_deletion(|ctx| MenuBuilder::new(WidgetBuilder::new()).build(ctx));
1494        test_widget_deletion(|ctx| MenuItemBuilder::new(WidgetBuilder::new()).build(ctx));
1495    }
1496}