rg3d_ui/
menu.rs

1use crate::{
2    border::BorderBuilder,
3    brush::Brush,
4    core::{algebra::Vector2, color::Color, pool::Handle},
5    decorator::DecoratorBuilder,
6    define_constructor,
7    grid::{Column, GridBuilder, Row},
8    message::{ButtonState, MessageDirection, OsEvent, UiMessage},
9    popup::{Placement, Popup, PopupBuilder, PopupMessage},
10    stack_panel::StackPanelBuilder,
11    text::TextBuilder,
12    widget::{Widget, WidgetBuilder, WidgetMessage},
13    BuildContext, Control, HorizontalAlignment, NodeHandleMapping, Orientation, RestrictionEntry,
14    Thickness, UiNode, UserInterface, VerticalAlignment, BRUSH_BRIGHT_BLUE, BRUSH_PRIMARY,
15};
16use std::sync::mpsc::Sender;
17use std::{
18    any::{Any, TypeId},
19    ops::{Deref, DerefMut},
20    rc::Rc,
21};
22
23#[derive(Debug, Clone, PartialEq)]
24pub enum MenuMessage {
25    Activate,
26    Deactivate,
27}
28
29impl MenuMessage {
30    define_constructor!(MenuMessage:Activate => fn activate(), layout: false);
31    define_constructor!(MenuMessage:Deactivate => fn deactivate(), layout: false);
32}
33
34#[derive(Debug, Clone, PartialEq)]
35pub enum MenuItemMessage {
36    Open,
37    Close,
38    Click,
39}
40
41impl MenuItemMessage {
42    define_constructor!(MenuItemMessage:Open => fn open(), layout: false);
43    define_constructor!(MenuItemMessage:Close => fn close(), layout: false);
44    define_constructor!(MenuItemMessage:Click => fn click(), layout: false);
45}
46
47#[derive(Clone)]
48pub struct Menu {
49    widget: Widget,
50    active: bool,
51}
52
53crate::define_widget_deref!(Menu);
54
55impl Control for Menu {
56    fn query_component(&self, type_id: TypeId) -> Option<&dyn Any> {
57        if type_id == TypeId::of::<Self>() {
58            Some(self)
59        } else {
60            None
61        }
62    }
63
64    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
65        self.widget.handle_routed_message(ui, message);
66
67        if let Some(msg) = message.data::<MenuMessage>() {
68            match msg {
69                MenuMessage::Activate => {
70                    if !self.active {
71                        ui.push_picking_restriction(RestrictionEntry {
72                            handle: self.handle(),
73                            stop: false,
74                        });
75                        self.active = true;
76                    }
77                }
78                MenuMessage::Deactivate => {
79                    if self.active {
80                        self.active = false;
81                        ui.remove_picking_restriction(self.handle());
82
83                        // Close descendant menu items.
84                        let mut stack = self.children().to_vec();
85                        while let Some(handle) = stack.pop() {
86                            let node = ui.node(handle);
87                            if let Some(item) = node.cast::<MenuItem>() {
88                                ui.send_message(MenuItemMessage::close(
89                                    handle,
90                                    MessageDirection::ToWidget,
91                                ));
92                                // We have to search in popup content too because menu shows its content
93                                // in popup and content could be another menu item.
94                                stack.push(item.popup);
95                            }
96                            // Continue depth search.
97                            stack.extend_from_slice(node.children());
98                        }
99                    }
100                }
101            }
102        }
103    }
104
105    fn handle_os_event(
106        &mut self,
107        _self_handle: Handle<UiNode>,
108        ui: &mut UserInterface,
109        event: &OsEvent,
110    ) {
111        // Handle menu items close by clicking outside of menu item. We using
112        // raw event here because we need to know the fact that mouse was clicked
113        // and we do not care which element was clicked so we'll get here in any
114        // case.
115        if let OsEvent::MouseInput { state, .. } = event {
116            if *state == ButtonState::Pressed && self.active {
117                // TODO: Make picking more accurate - right now it works only with rects.
118                let pos = ui.cursor_position();
119                if !self.widget.screen_bounds().contains(pos) {
120                    // Also check if we clicked inside some descendant menu item - in this
121                    // case we don't need to close menu.
122                    let mut any_picked = false;
123                    let mut stack = self.children().to_vec();
124                    'depth_search: while let Some(handle) = stack.pop() {
125                        let node = ui.node(handle);
126                        if let Some(item) = node.cast::<MenuItem>() {
127                            if ui.node(item.popup).screen_bounds().contains(pos) {
128                                // Once we found that we clicked inside some descendant menu item
129                                // we can immediately stop search - we don't want to close menu
130                                // items popups in this case and can safely skip all stuff below.
131                                any_picked = true;
132                                break 'depth_search;
133                            }
134                            // We have to search in popup content too because menu shows its content
135                            // in popup and content could be another menu item.
136                            stack.push(item.popup);
137                        }
138                        // Continue depth search.
139                        stack.extend_from_slice(node.children());
140                    }
141
142                    if !any_picked {
143                        ui.send_message(MenuMessage::deactivate(
144                            self.handle(),
145                            MessageDirection::ToWidget,
146                        ));
147                    }
148                }
149            }
150        }
151    }
152}
153
154#[derive(Copy, Clone, PartialOrd, PartialEq, Hash)]
155enum MenuItemPlacement {
156    Bottom,
157    Right,
158}
159
160#[derive(Clone)]
161pub struct MenuItem {
162    widget: Widget,
163    items: Vec<Handle<UiNode>>,
164    popup: Handle<UiNode>,
165    placement: MenuItemPlacement,
166}
167
168crate::define_widget_deref!(MenuItem);
169
170// MenuItem uses popup to show its content, popup can be top-most only if it is
171// direct child of root canvas of UI. This fact adds some complications to search
172// of parent menu - we can't just traverse the tree because popup is not a child
173// of menu item, instead we trying to fetch handle to parent menu item from popup's
174// user data and continue up-search until we find menu.
175fn find_menu(from: Handle<UiNode>, ui: &UserInterface) -> Handle<UiNode> {
176    let mut handle = from;
177    while handle.is_some() {
178        if let Some((_, popup)) = ui.try_borrow_by_type_up::<Popup>(handle) {
179            // Continue search from parent menu item of popup.
180            handle = popup
181                .user_data_ref::<Handle<UiNode>>()
182                .cloned()
183                .unwrap_or_default();
184        } else {
185            // Maybe we have Menu as parent for MenuItem.
186            return ui.find_by_criteria_up(handle, |n| n.cast::<Menu>().is_some());
187        }
188    }
189    Default::default()
190}
191
192fn close_menu_chain(from: Handle<UiNode>, ui: &UserInterface) {
193    let mut handle = from;
194    while handle.is_some() {
195        if let Some((popup_handle, popup)) = ui.try_borrow_by_type_up::<Popup>(handle) {
196            ui.send_message(PopupMessage::close(
197                popup_handle,
198                MessageDirection::ToWidget,
199            ));
200
201            // Continue search from parent menu item of popup.
202            handle = popup
203                .user_data_ref::<Handle<UiNode>>()
204                .cloned()
205                .unwrap_or_default();
206        }
207    }
208}
209
210impl Control for MenuItem {
211    fn query_component(&self, type_id: TypeId) -> Option<&dyn Any> {
212        if type_id == TypeId::of::<Self>() {
213            Some(self)
214        } else {
215            None
216        }
217    }
218
219    fn on_remove(&self, sender: &Sender<UiMessage>) {
220        // Popup won't be deleted with the menu item, because it is not the child of the item.
221        // So we have to remove it manually.
222        sender
223            .send(WidgetMessage::remove(
224                self.popup,
225                MessageDirection::ToWidget,
226            ))
227            .unwrap();
228    }
229
230    fn resolve(&mut self, node_map: &NodeHandleMapping) {
231        node_map.resolve_slice(&mut self.items);
232        node_map.resolve(&mut self.popup);
233    }
234
235    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
236        self.widget.handle_routed_message(ui, message);
237
238        if let Some(msg) = message.data::<WidgetMessage>() {
239            match msg {
240                WidgetMessage::MouseDown { .. } => {
241                    let menu = find_menu(self.parent(), ui);
242                    if menu.is_some() {
243                        // Activate menu so it user will be able to open submenus by
244                        // mouse hover.
245                        ui.send_message(MenuMessage::activate(menu, MessageDirection::ToWidget));
246
247                        ui.send_message(MenuItemMessage::open(
248                            self.handle(),
249                            MessageDirection::ToWidget,
250                        ));
251                    }
252                }
253                WidgetMessage::MouseUp { .. } => {
254                    if !message.handled() {
255                        ui.send_message(MenuItemMessage::click(
256                            self.handle(),
257                            MessageDirection::ToWidget,
258                        ));
259                        if self.items.is_empty() {
260                            let menu = find_menu(self.parent(), ui);
261                            if menu.is_some() {
262                                // Deactivate menu if we have one.
263                                ui.send_message(MenuMessage::deactivate(
264                                    menu,
265                                    MessageDirection::ToWidget,
266                                ));
267                            } else {
268                                // Or close menu chain if menu item is in "orphaned" state.
269                                close_menu_chain(self.parent(), ui);
270                            }
271                        }
272                        message.set_handled(true);
273                    }
274                }
275                WidgetMessage::MouseEnter => {
276                    // While parent menu active it is possible to open submenus
277                    // by simple mouse hover.
278                    let menu = find_menu(self.parent(), ui);
279                    let open = if menu.is_some() {
280                        if let Some(menu) = ui.node(menu).cast::<Menu>() {
281                            menu.active
282                        } else {
283                            false
284                        }
285                    } else {
286                        true
287                    };
288                    if open {
289                        ui.send_message(MenuItemMessage::open(
290                            self.handle(),
291                            MessageDirection::ToWidget,
292                        ));
293                    }
294                }
295                _ => {}
296            }
297        } else if let Some(msg) = message.data::<MenuItemMessage>() {
298            match msg {
299                MenuItemMessage::Open => {
300                    if !self.items.is_empty() {
301                        let placement = match self.placement {
302                            MenuItemPlacement::Bottom => Placement::LeftBottom(self.handle),
303                            MenuItemPlacement::Right => Placement::RightTop(self.handle),
304                        };
305
306                        // Open popup.
307                        ui.send_message(PopupMessage::placement(
308                            self.popup,
309                            MessageDirection::ToWidget,
310                            placement,
311                        ));
312                        ui.send_message(PopupMessage::open(self.popup, MessageDirection::ToWidget));
313                    }
314                }
315                MenuItemMessage::Close => {
316                    ui.send_message(PopupMessage::close(self.popup, MessageDirection::ToWidget));
317                }
318                MenuItemMessage::Click => {}
319            }
320        }
321    }
322
323    fn preview_message(&self, ui: &UserInterface, message: &mut UiMessage) {
324        // We need to check if some new menu item opened and then close other not in
325        // direct chain of menu items until to menu.
326        if message.destination() != self.handle() {
327            if let Some(MenuItemMessage::Open) = message.data::<MenuItemMessage>() {
328                let mut found = false;
329                let mut handle = message.destination();
330                while handle.is_some() {
331                    if handle == self.handle() {
332                        found = true;
333                        break;
334                    } else {
335                        let node = ui.node(handle);
336                        if let Some(popup) = node.cast::<Popup>() {
337                            // Once we found popup in chain, we must extract handle
338                            // of parent menu item to continue search.
339                            handle = popup
340                                .user_data_ref::<Handle<UiNode>>()
341                                .cloned()
342                                .unwrap_or_default();
343                        } else {
344                            handle = node.parent();
345                        }
346                    }
347                }
348
349                if !found {
350                    ui.send_message(MenuItemMessage::close(
351                        self.handle(),
352                        MessageDirection::ToWidget,
353                    ));
354                }
355            }
356        }
357    }
358}
359
360pub struct MenuBuilder {
361    widget_builder: WidgetBuilder,
362    items: Vec<Handle<UiNode>>,
363}
364
365impl MenuBuilder {
366    pub fn new(widget_builder: WidgetBuilder) -> Self {
367        Self {
368            widget_builder,
369            items: Default::default(),
370        }
371    }
372
373    pub fn with_items(mut self, items: Vec<Handle<UiNode>>) -> Self {
374        self.items = items;
375        self
376    }
377
378    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
379        for &item in self.items.iter() {
380            if let Some(item) = ctx[item].cast_mut::<MenuItem>() {
381                item.placement = MenuItemPlacement::Bottom;
382            }
383        }
384
385        let back = BorderBuilder::new(
386            WidgetBuilder::new()
387                .with_background(BRUSH_PRIMARY)
388                .with_child(
389                    StackPanelBuilder::new(
390                        WidgetBuilder::new().with_children(self.items.iter().cloned()),
391                    )
392                    .with_orientation(Orientation::Horizontal)
393                    .build(ctx),
394                ),
395        )
396        .build(ctx);
397
398        let menu = Menu {
399            widget: self
400                .widget_builder
401                .with_handle_os_events(true)
402                .with_child(back)
403                .build(),
404            active: false,
405        };
406
407        ctx.add_node(UiNode::new(menu))
408    }
409}
410
411pub enum MenuItemContent<'a, 'b> {
412    /// Quick-n-dirty way of building elements. It can cover most of use
413    /// cases - it builds classic menu item:
414    ///   _____________________
415    ///  |    |      |        |
416    ///  |icon| text |shortcut|
417    ///  |____|______|________|
418    Text {
419        text: &'a str,
420        shortcut: &'b str,
421        icon: Handle<UiNode>,
422    },
423    /// Allows to put any node into menu item. It allows to customize menu
424    /// item how needed - i.e. put image in it, or other user control.
425    Node(Handle<UiNode>),
426}
427
428impl<'a, 'b> MenuItemContent<'a, 'b> {
429    pub fn text_with_shortcut(text: &'a str, shortcut: &'b str) -> Self {
430        MenuItemContent::Text {
431            text,
432            shortcut,
433            icon: Default::default(),
434        }
435    }
436
437    pub fn text(text: &'a str) -> Self {
438        MenuItemContent::Text {
439            text,
440            shortcut: "",
441            icon: Default::default(),
442        }
443    }
444}
445
446pub struct MenuItemBuilder<'a, 'b> {
447    widget_builder: WidgetBuilder,
448    items: Vec<Handle<UiNode>>,
449    content: Option<MenuItemContent<'a, 'b>>,
450    back: Option<Handle<UiNode>>,
451}
452
453impl<'a, 'b> MenuItemBuilder<'a, 'b> {
454    pub fn new(widget_builder: WidgetBuilder) -> Self {
455        Self {
456            widget_builder,
457            items: Default::default(),
458            content: None,
459            back: None,
460        }
461    }
462
463    pub fn with_content(mut self, content: MenuItemContent<'a, 'b>) -> Self {
464        self.content = Some(content);
465        self
466    }
467
468    pub fn with_items(mut self, items: Vec<Handle<UiNode>>) -> Self {
469        self.items = items;
470        self
471    }
472
473    /// Allows you to specify the background content. Background node is only for decoration purpose,
474    /// it can be any kind of node, by default it is Decorator.
475    pub fn with_back(mut self, handle: Handle<UiNode>) -> Self {
476        self.back = Some(handle);
477        self
478    }
479
480    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
481        let content = match self.content {
482            None => Handle::NONE,
483            Some(MenuItemContent::Text {
484                text,
485                shortcut,
486                icon,
487            }) => GridBuilder::new(
488                WidgetBuilder::new()
489                    .with_child(icon)
490                    .with_child(
491                        TextBuilder::new(
492                            WidgetBuilder::new()
493                                .with_vertical_alignment(VerticalAlignment::Center)
494                                .with_margin(Thickness::uniform(1.0))
495                                .on_column(1),
496                        )
497                        .with_text(text)
498                        .build(ctx),
499                    )
500                    .with_child(
501                        TextBuilder::new(
502                            WidgetBuilder::new()
503                                .with_vertical_alignment(VerticalAlignment::Center)
504                                .with_horizontal_alignment(HorizontalAlignment::Right)
505                                .with_margin(Thickness::uniform(1.0))
506                                .on_column(2),
507                        )
508                        .with_text(shortcut)
509                        .build(ctx),
510                    ),
511            )
512            .add_row(Row::auto())
513            .add_column(Column::auto())
514            .add_column(Column::stretch())
515            .add_column(Column::auto())
516            .build(ctx),
517            Some(MenuItemContent::Node(node)) => node,
518        };
519
520        let back = self.back.unwrap_or_else(|| {
521            DecoratorBuilder::new(
522                BorderBuilder::new(WidgetBuilder::new())
523                    .with_stroke_thickness(Thickness::uniform(0.0)),
524            )
525            .with_hover_brush(BRUSH_BRIGHT_BLUE)
526            .with_normal_brush(BRUSH_PRIMARY)
527            .with_pressed_brush(Brush::Solid(Color::TRANSPARENT))
528            .with_pressable(false)
529            .build(ctx)
530        });
531
532        ctx.link(content, back);
533
534        let popup = PopupBuilder::new(WidgetBuilder::new().with_min_size(Vector2::new(10.0, 10.0)))
535            .with_content(
536                StackPanelBuilder::new(
537                    WidgetBuilder::new().with_children(self.items.iter().cloned()),
538                )
539                .build(ctx),
540            )
541            // We'll manually control if popup is either open or closed.
542            .stays_open(true)
543            .build(ctx);
544
545        let menu = MenuItem {
546            widget: self
547                .widget_builder
548                .with_preview_messages(true)
549                .with_child(back)
550                .build(),
551            popup,
552            items: self.items,
553            placement: MenuItemPlacement::Right,
554        };
555
556        let handle = ctx.add_node(UiNode::new(menu));
557
558        // "Link" popup with its parent menu item.
559        if let Some(popup) = ctx[popup].cast_mut::<Popup>() {
560            popup.user_data = Some(Rc::new(handle));
561        }
562
563        handle
564    }
565}