vizia_core/views/
menu.rs

1use crate::modifiers::ModalEvent;
2use crate::{icons::ICON_CHEVRON_RIGHT, prelude::*};
3
4/// A view which represents a horizontal group of menus.
5#[derive(Lens)]
6pub struct MenuBar {
7    is_open: bool,
8}
9
10impl MenuBar {
11    /// Creates a new [MenuBar] view.
12    pub fn new(cx: &mut Context, content: impl Fn(&mut Context)) -> Handle<Self> {
13        Self { is_open: false }
14            .build(cx, |cx| {
15                cx.add_listener(move |menu_bar: &mut Self, cx, event| {
16                    let flag: bool = menu_bar.is_open;
17                    event.map(
18                        |window_event, meta: &mut crate::events::EventMeta| match window_event {
19                            WindowEvent::MouseDown(_) => {
20                                if flag && meta.origin != cx.current() {
21                                    // Check if the mouse was pressed outside of any descendants
22                                    if !cx.hovered.is_descendant_of(cx.tree, cx.current) {
23                                        cx.emit(MenuEvent::CloseAll);
24                                    }
25                                }
26                            }
27
28                            _ => {}
29                        },
30                    );
31                });
32
33                (content)(cx);
34            })
35            .layout_type(LayoutType::Row)
36    }
37}
38
39impl View for MenuBar {
40    fn element(&self) -> Option<&'static str> {
41        Some("menubar")
42    }
43
44    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
45        event.map(|menu_event, _| match menu_event {
46            MenuEvent::MenuIsOpen => {
47                self.is_open = true;
48            }
49
50            MenuEvent::CloseAll => {
51                self.is_open = false;
52                cx.emit_custom(
53                    Event::new(MenuEvent::Close).target(cx.current).propagate(Propagation::Subtree),
54                );
55            }
56
57            _ => {}
58        });
59    }
60}
61
62/// Events used by menus.
63pub enum MenuEvent {
64    /// Toggle the open state of the menu.
65    ToggleOpen,
66    /// Sets the menu to an open state.
67    Open,
68    /// Sets the menu to a closed state.
69    Close,
70    /// Closes the menu and any submenus.
71    CloseAll,
72    /// Event emitted when a menu or submenu is opened.
73    MenuIsOpen,
74}
75
76/// A view which represents a submenu within a menu.
77#[derive(Lens)]
78pub struct Submenu {
79    is_open: bool,
80    open_on_hover: bool,
81    is_submenu: bool,
82}
83
84impl Submenu {
85    /// Creates a new [Submenu] view.
86    pub fn new<V: View>(
87        cx: &mut Context,
88        content: impl Fn(&mut Context) -> Handle<V> + 'static,
89        menu: impl Fn(&mut Context) + 'static,
90    ) -> Handle<Self> {
91        let is_submenu = cx.data::<Submenu>().is_some();
92
93        let handle = Self { is_open: false, open_on_hover: is_submenu, is_submenu }
94            .build(cx, |cx| {
95                cx.add_listener(move |menu_button: &mut Self, cx, event| {
96                    let flag: bool = menu_button.is_open;
97                    event.map(
98                        |window_event, meta: &mut crate::events::EventMeta| match window_event {
99                            WindowEvent::MouseDown(_) => {
100                                if flag && meta.origin != cx.current() {
101                                    // Check if the mouse was pressed outside of any descendants
102                                    if !cx.hovered.is_descendant_of(cx.tree, cx.current) {
103                                        cx.emit(MenuEvent::CloseAll);
104                                        cx.emit(MenuEvent::Close);
105                                        // TODO: This might be needed
106                                        // meta.consume();
107                                    }
108                                }
109                            }
110
111                            _ => {}
112                        },
113                    );
114                });
115                // HStack::new(cx, |cx| {
116                (content)(cx).hoverable(false);
117                Svg::new(cx, ICON_CHEVRON_RIGHT).class("arrow").hoverable(false);
118                // });
119                Binding::new(cx, Submenu::is_open, move |cx, is_open| {
120                    if is_open.get(cx) {
121                        Popup::new(cx, |cx| {
122                            (menu)(cx);
123                        })
124                        .placement(Submenu::is_submenu.map(|is_submenu| {
125                            if *is_submenu {
126                                Placement::RightStart
127                            } else {
128                                Placement::BottomStart
129                            }
130                        }))
131                        .arrow_size(Pixels(0.0))
132                        .checked(Submenu::is_open)
133                        .on_hover(|cx| {
134                            cx.emit_custom(
135                                Event::new(MenuEvent::Close)
136                                    .target(cx.current)
137                                    .propagate(Propagation::Subtree),
138                            )
139                        });
140                    }
141                });
142                // .on_press_down(|cx| cx.emit(MenuEvent::CloseAll));
143                // .on_blur(|cx| cx.emit(MenuEvent::CloseAll));
144            })
145            .navigable(true)
146            .checked(Submenu::is_open)
147            .layout_type(LayoutType::Row)
148            .on_press(|cx| cx.emit(MenuEvent::ToggleOpen));
149
150        if handle.data::<MenuBar>().is_some() {
151            handle.bind(MenuBar::is_open, |handle, is_open| {
152                let is_open = is_open.get(&handle);
153                handle.modify(|menu_button| menu_button.open_on_hover = is_open);
154            })
155        } else {
156            handle
157        }
158    }
159}
160
161impl View for Submenu {
162    fn element(&self) -> Option<&'static str> {
163        Some("submenu")
164    }
165
166    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
167        event.map(|window_event, meta| match window_event {
168            WindowEvent::MouseEnter => {
169                if meta.target == cx.current {
170                    // if self.open_on_hover {
171                    //     cx.focus();
172                    // }
173                    if self.open_on_hover {
174                        // Close any open submenus of the parent
175                        let parent = cx.tree.get_parent(cx.current).unwrap();
176                        cx.emit_custom(
177                            Event::new(MenuEvent::Close)
178                                .target(parent)
179                                .propagate(Propagation::Subtree),
180                        );
181                        // Open this submenu
182                        cx.emit(MenuEvent::Open);
183                    }
184                }
185            }
186
187            WindowEvent::KeyDown(code, _) => match code {
188                Code::ArrowLeft => {
189                    // if cx.is_focused() {
190                    if self.is_open {
191                        self.is_open = false;
192                        cx.focus();
193                        meta.consume();
194                    }
195                    // }
196                }
197
198                Code::ArrowRight => {
199                    if !self.is_open {
200                        self.is_open = true;
201                    }
202                }
203
204                _ => {}
205            },
206
207            _ => {}
208        });
209
210        event.map(|menu_event, meta| match menu_event {
211            MenuEvent::Open => {
212                self.is_open = true;
213                meta.consume();
214            }
215
216            MenuEvent::Close => {
217                self.is_open = false;
218                // meta.consume();
219            }
220
221            MenuEvent::ToggleOpen => {
222                self.is_open ^= true;
223                if self.is_open {
224                    cx.emit(MenuEvent::MenuIsOpen);
225                } else {
226                    // If the parent is a MenuBar then this will reset the is_open state
227                    let parent = cx.tree.get_parent(cx.current).unwrap();
228                    cx.emit_custom(
229                        Event::new(MenuEvent::CloseAll)
230                            .target(parent)
231                            .propagate(Propagation::Direct),
232                    );
233                }
234                meta.consume();
235            }
236
237            _ => {}
238        });
239    }
240}
241
242/// A view which represents a pressable item within a menu.
243#[derive(Lens)]
244pub struct MenuButton {}
245
246impl MenuButton {
247    /// Creates a new [MenuButton] view.
248    pub fn new<V: View>(
249        cx: &mut Context,
250        action: impl Fn(&mut EventContext) + Send + Sync + 'static,
251        content: impl Fn(&mut Context) -> Handle<V> + 'static,
252    ) -> Handle<Self> {
253        Self {}
254            .build(cx, |cx| {
255                (content)(cx).hoverable(false);
256            })
257            .on_press(move |cx| {
258                (action)(cx);
259                cx.emit(MenuEvent::CloseAll);
260                cx.emit(ModalEvent::HideMenu);
261                cx.emit(MenuEvent::Close);
262            })
263            .role(Role::MenuItem)
264            .navigable(true)
265    }
266}
267
268impl View for MenuButton {
269    fn element(&self) -> Option<&'static str> {
270        Some("menubutton")
271    }
272
273    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
274        event.map(|window_event, meta| match window_event {
275            WindowEvent::MouseEnter => {
276                if meta.target == cx.current {
277                    let parent = cx.tree.get_parent(cx.current).unwrap();
278                    cx.emit_custom(
279                        Event::new(MenuEvent::Close).target(parent).propagate(Propagation::Subtree),
280                    );
281                }
282            }
283
284            _ => {}
285        });
286    }
287}