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