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 { Placement::RightStart } else { Placement::BottomStart }
126                        }))
127                        .arrow_size(Pixels(0.0))
128                        .checked(Submenu::is_open)
129                        .on_hover(|cx| {
130                            cx.emit_custom(
131                                Event::new(MenuEvent::Close)
132                                    .target(cx.current)
133                                    .propagate(Propagation::Subtree),
134                            )
135                        });
136                    }
137                });
138                // .on_press_down(|cx| cx.emit(MenuEvent::CloseAll));
139                // .on_blur(|cx| cx.emit(MenuEvent::CloseAll));
140            })
141            .navigable(true)
142            .checked(Submenu::is_open)
143            .layout_type(LayoutType::Row)
144            .on_press(|cx| cx.emit(MenuEvent::ToggleOpen));
145
146        if handle.data::<MenuBar>().is_some() {
147            handle.bind(MenuBar::is_open, |handle, is_open| {
148                let is_open = is_open.get(&handle);
149                handle.modify(|menu_button| menu_button.open_on_hover = is_open);
150            })
151        } else {
152            handle
153        }
154    }
155}
156
157impl View for Submenu {
158    fn element(&self) -> Option<&'static str> {
159        Some("submenu")
160    }
161
162    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
163        event.map(|window_event, meta| match window_event {
164            WindowEvent::MouseEnter => {
165                if meta.target == cx.current {
166                    // if self.open_on_hover {
167                    //     cx.focus();
168                    // }
169                    if self.open_on_hover {
170                        // Close any open submenus of the parent
171                        let parent = cx.tree.get_parent(cx.current).unwrap();
172                        cx.emit_custom(
173                            Event::new(MenuEvent::Close)
174                                .target(parent)
175                                .propagate(Propagation::Subtree),
176                        );
177                        // Open this submenu
178                        cx.emit(MenuEvent::Open);
179                    }
180                }
181            }
182
183            WindowEvent::KeyDown(code, _) => match code {
184                Code::ArrowLeft => {
185                    // if cx.is_focused() {
186                    if self.is_open {
187                        self.is_open = false;
188                        cx.focus();
189                        meta.consume();
190                    }
191                    // }
192                }
193
194                Code::ArrowRight => {
195                    if !self.is_open {
196                        self.is_open = true;
197                    }
198                }
199
200                _ => {}
201            },
202
203            _ => {}
204        });
205
206        event.map(|menu_event, meta| match menu_event {
207            MenuEvent::Open => {
208                self.is_open = true;
209                meta.consume();
210            }
211
212            MenuEvent::Close => {
213                self.is_open = false;
214                // meta.consume();
215            }
216
217            MenuEvent::ToggleOpen => {
218                self.is_open ^= true;
219                if self.is_open {
220                    cx.emit(MenuEvent::MenuIsOpen);
221                } else {
222                    // If the parent is a MenuBar then this will reset the is_open state
223                    let parent = cx.tree.get_parent(cx.current).unwrap();
224                    cx.emit_custom(
225                        Event::new(MenuEvent::CloseAll)
226                            .target(parent)
227                            .propagate(Propagation::Direct),
228                    );
229                }
230                meta.consume();
231            }
232
233            _ => {}
234        });
235    }
236}
237
238/// A view which represents a pressable item within a menu.
239#[derive(Lens)]
240pub struct MenuButton {}
241
242impl MenuButton {
243    /// Creates a new [MenuButton] view.
244    pub fn new<V: View>(
245        cx: &mut Context,
246        action: impl Fn(&mut EventContext) + Send + Sync + 'static,
247        content: impl Fn(&mut Context) -> Handle<V> + 'static,
248    ) -> Handle<Self> {
249        Self {}
250            .build(cx, |cx| {
251                (content)(cx).hoverable(false);
252            })
253            .on_press(move |cx| {
254                (action)(cx);
255                cx.emit(MenuEvent::CloseAll);
256                cx.emit(ModalEvent::HideMenu);
257                cx.emit(MenuEvent::Close);
258            })
259            .role(Role::MenuItem)
260            .navigable(true)
261    }
262}
263
264impl View for MenuButton {
265    fn element(&self) -> Option<&'static str> {
266        Some("menubutton")
267    }
268
269    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
270        event.map(|window_event, meta| match window_event {
271            WindowEvent::MouseEnter => {
272                if meta.target == cx.current {
273                    let parent = cx.tree.get_parent(cx.current).unwrap();
274                    cx.emit_custom(
275                        Event::new(MenuEvent::Close).target(parent).propagate(Propagation::Subtree),
276                    );
277                }
278            }
279
280            _ => {}
281        });
282    }
283}