freya_components/
menu.rs

1use freya_core::prelude::*;
2use torin::{
3    content::Content,
4    prelude::{
5        Alignment,
6        Position,
7    },
8    size::Size,
9};
10
11use crate::{
12    get_theme,
13    theming::component_themes::{
14        MenuContainerThemePartial,
15        MenuItemThemePartial,
16    },
17};
18
19/// Floating menu container.
20///
21/// # Example
22///
23/// ```rust
24/// # use freya::prelude::*;
25/// fn app() -> impl IntoElement {
26///     let mut show_menu = use_state(|| false);
27///
28///     rect()
29///         .child(
30///             Button::new()
31///                 .on_press(move |_| show_menu.toggle())
32///                 .child("Open Menu"),
33///         )
34///         .maybe_child(show_menu().then(|| {
35///             Menu::new()
36///                 .on_close(move |_| show_menu.set(false))
37///                 .child(MenuButton::new().child("Open"))
38///                 .child(MenuButton::new().child("Save"))
39///                 .child(
40///                     SubMenu::new()
41///                         .label("Export")
42///                         .child(MenuButton::new().child("PDF")),
43///                 )
44///         }))
45/// }
46/// # use freya_testing::prelude::*;
47/// # launch_doc(|| {
48/// #   let mut show_menu = use_state(|| true);
49/// #   rect().center().expanded().child(
50/// #       rect()
51/// #           .child(
52/// #               Button::new()
53/// #                   .on_press(move |_| show_menu.toggle())
54/// #                   .child("Open Menu"),
55/// #           )
56/// #           .maybe_child(show_menu().then(|| {
57/// #               Menu::new()
58/// #                   .on_close(move |_| show_menu.set(false))
59/// #                   .child(MenuButton::new().child("Open"))
60/// #                   .child(MenuButton::new().child("Save"))
61/// #           }))
62/// #   )
63/// # }, "./images/gallery_menu.png").render();
64/// ```
65///
66/// # Preview
67/// ![Menu Preview][menu]
68#[cfg_attr(feature = "docs",
69    doc = embed_doc_image::embed_image!("menu", "images/gallery_menu.png"),
70)]
71#[derive(Default, Clone, PartialEq)]
72pub struct Menu {
73    children: Vec<Element>,
74    on_close: Option<EventHandler<()>>,
75    key: DiffKey,
76}
77
78impl ChildrenExt for Menu {
79    fn get_children(&mut self) -> &mut Vec<Element> {
80        &mut self.children
81    }
82}
83
84impl KeyExt for Menu {
85    fn write_key(&mut self) -> &mut DiffKey {
86        &mut self.key
87    }
88}
89
90impl Menu {
91    pub fn new() -> Self {
92        Self::default()
93    }
94
95    pub fn on_close<F>(mut self, f: F) -> Self
96    where
97        F: Into<EventHandler<()>>,
98    {
99        self.on_close = Some(f.into());
100        self
101    }
102}
103
104impl ComponentOwned for Menu {
105    fn render(self) -> impl IntoElement {
106        // Provide the menus ID generator
107        use_provide_context(|| State::create(ROOT_MENU.0));
108        // Provide the menus stack
109        use_provide_context::<State<Vec<MenuId>>>(|| State::create(vec![ROOT_MENU]));
110        // Provide this the ROOT Menu ID
111        use_provide_context(|| ROOT_MENU);
112
113        rect()
114            .layer(Layer::Overlay)
115            .corner_radius(8.0)
116            .on_press(move |ev: Event<PressEventData>| {
117                ev.stop_propagation();
118            })
119            .on_global_mouse_up(move |_| {
120                if let Some(on_close) = &self.on_close {
121                    on_close.call(());
122                }
123            })
124            .child(MenuContainer::new().children(self.children))
125    }
126    fn render_key(&self) -> DiffKey {
127        self.key.clone().or(self.default_key())
128    }
129}
130
131/// Container for menu items with proper spacing and layout.
132///
133/// # Example
134///
135/// ```rust
136/// # use freya::prelude::*;
137/// fn app() -> impl IntoElement {
138///     MenuContainer::new()
139///         .child(MenuItem::new().child("Item 1"))
140///         .child(MenuItem::new().child("Item 2"))
141/// }
142/// ```
143#[derive(Default, Clone, PartialEq)]
144pub struct MenuContainer {
145    pub(crate) theme: Option<MenuContainerThemePartial>,
146    children: Vec<Element>,
147    key: DiffKey,
148}
149
150impl KeyExt for MenuContainer {
151    fn write_key(&mut self) -> &mut DiffKey {
152        &mut self.key
153    }
154}
155
156impl ChildrenExt for MenuContainer {
157    fn get_children(&mut self) -> &mut Vec<Element> {
158        &mut self.children
159    }
160}
161
162impl MenuContainer {
163    pub fn new() -> Self {
164        Self::default()
165    }
166}
167
168impl ComponentOwned for MenuContainer {
169    fn render(self) -> impl IntoElement {
170        let focus = use_focus();
171        let theme = get_theme!(self.theme, menu_container);
172
173        use_provide_context(move || MenuGroup {
174            group_id: focus.a11y_id(),
175        });
176
177        rect()
178            .a11y_id(focus.a11y_id())
179            .a11y_member_of(focus.a11y_id())
180            .a11y_focusable(true)
181            .a11y_role(AccessibilityRole::Menu)
182            .position(Position::new_absolute())
183            .shadow((0.0, 4.0, 10.0, 0., theme.shadow))
184            .background(theme.background)
185            .corner_radius(theme.corner_radius)
186            .padding(theme.padding)
187            .border(Border::new().width(1.).fill(theme.border_fill))
188            .content(Content::fit())
189            .children(self.children)
190    }
191
192    fn render_key(&self) -> DiffKey {
193        self.key.clone().or(self.default_key())
194    }
195}
196
197#[derive(Clone)]
198pub struct MenuGroup {
199    pub group_id: AccessibilityId,
200}
201
202/// A clickable menu item with hover and focus states.
203///
204/// This is the base component used by MenuButton and SubMenu.
205///
206/// # Example
207///
208/// ```rust
209/// # use freya::prelude::*;
210/// fn app() -> impl IntoElement {
211///     MenuItem::new()
212///         .on_press(|_| println!("Clicked!"))
213///         .child("Open File")
214/// }
215/// ```
216#[derive(Default, Clone, PartialEq)]
217pub struct MenuItem {
218    pub(crate) theme: Option<MenuItemThemePartial>,
219    children: Vec<Element>,
220    on_press: Option<EventHandler<Event<PressEventData>>>,
221    on_pointer_enter: Option<EventHandler<Event<PointerEventData>>>,
222    selected: bool,
223    key: DiffKey,
224}
225
226impl KeyExt for MenuItem {
227    fn write_key(&mut self) -> &mut DiffKey {
228        &mut self.key
229    }
230}
231
232impl MenuItem {
233    pub fn new() -> Self {
234        Self::default()
235    }
236
237    pub fn on_press<F>(mut self, f: F) -> Self
238    where
239        F: Into<EventHandler<Event<PressEventData>>>,
240    {
241        self.on_press = Some(f.into());
242        self
243    }
244
245    pub fn on_pointer_enter<F>(mut self, f: F) -> Self
246    where
247        F: Into<EventHandler<Event<PointerEventData>>>,
248    {
249        self.on_pointer_enter = Some(f.into());
250        self
251    }
252
253    pub fn selected(mut self, selected: bool) -> Self {
254        self.selected = selected;
255        self
256    }
257}
258
259impl ChildrenExt for MenuItem {
260    fn get_children(&mut self) -> &mut Vec<Element> {
261        &mut self.children
262    }
263}
264
265impl ComponentOwned for MenuItem {
266    fn render(self) -> impl IntoElement {
267        let theme = get_theme!(self.theme, menu_item);
268        let mut hovering = use_state(|| false);
269        let focus = use_focus();
270        let focus_status = use_focus_status(focus);
271        let MenuGroup { group_id } = use_consume::<MenuGroup>();
272
273        let background = if self.selected {
274            theme.select_background
275        } else if hovering() {
276            theme.hover_background
277        } else {
278            theme.background
279        };
280
281        let border = if focus_status() == FocusStatus::Keyboard {
282            Border::new()
283                .fill(theme.select_border_fill)
284                .width(2.)
285                .alignment(BorderAlignment::Inner)
286        } else {
287            Border::new()
288                .fill(theme.border_fill)
289                .width(1.)
290                .alignment(BorderAlignment::Inner)
291        };
292
293        let on_pointer_enter = move |e| {
294            hovering.set(true);
295            if let Some(on_pointer_enter) = &self.on_pointer_enter {
296                on_pointer_enter.call(e);
297            }
298        };
299
300        let on_pointer_leave = move |_| {
301            hovering.set(false);
302        };
303
304        let on_press = move |e: Event<PressEventData>| {
305            focus.request_focus();
306            if let Some(on_press) = &self.on_press {
307                on_press.call(e);
308            }
309        };
310
311        rect()
312            .a11y_role(AccessibilityRole::MenuItem)
313            .a11y_id(focus.a11y_id())
314            .a11y_focusable(true)
315            .a11y_member_of(group_id)
316            .min_width(Size::px(105.))
317            .width(Size::fill_minimum())
318            .padding((4.0, 10.0))
319            .corner_radius(theme.corner_radius)
320            .background(background)
321            .border(border)
322            .color(theme.color)
323            .text_align(TextAlign::Start)
324            .main_align(Alignment::Center)
325            .on_pointer_enter(on_pointer_enter)
326            .on_pointer_leave(on_pointer_leave)
327            .on_press(on_press)
328            .children(self.children)
329    }
330
331    fn render_key(&self) -> DiffKey {
332        self.key.clone().or(self.default_key())
333    }
334}
335
336/// Like a button, but for Menus.
337///
338/// # Example
339///
340/// ```rust
341/// # use freya::prelude::*;
342/// fn app() -> impl IntoElement {
343///     MenuButton::new()
344///         .on_press(|_| println!("Clicked!"))
345///         .child("Item")
346/// }
347/// ```
348#[derive(Default, Clone, PartialEq)]
349pub struct MenuButton {
350    children: Vec<Element>,
351    on_press: Option<EventHandler<Event<PressEventData>>>,
352    key: DiffKey,
353}
354
355impl ChildrenExt for MenuButton {
356    fn get_children(&mut self) -> &mut Vec<Element> {
357        &mut self.children
358    }
359}
360
361impl KeyExt for MenuButton {
362    fn write_key(&mut self) -> &mut DiffKey {
363        &mut self.key
364    }
365}
366
367impl MenuButton {
368    pub fn new() -> Self {
369        Self::default()
370    }
371
372    pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
373        self.on_press = Some(on_press.into());
374        self
375    }
376}
377
378impl ComponentOwned for MenuButton {
379    fn render(self) -> impl IntoElement {
380        let mut menus = use_consume::<State<Vec<MenuId>>>();
381        let parent_menu_id = use_consume::<MenuId>();
382
383        MenuItem::new()
384            .on_pointer_enter(move |_| close_menus_until(&mut menus, parent_menu_id))
385            .map(self.on_press.clone(), |el, on_press| el.on_press(on_press))
386            .children(self.children)
387    }
388
389    fn render_key(&self) -> DiffKey {
390        self.key.clone().or(self.default_key())
391    }
392}
393
394/// Create sub menus inside a Menu.
395///
396/// # Example
397///
398/// ```rust
399/// # use freya::prelude::*;
400/// fn app() -> impl IntoElement {
401///     SubMenu::new()
402///         .label("Export")
403///         .child(MenuButton::new().child("PDF"))
404/// }
405/// ```
406#[derive(Default, Clone, PartialEq)]
407pub struct SubMenu {
408    label: Option<Element>,
409    items: Vec<Element>,
410    key: DiffKey,
411}
412
413impl KeyExt for SubMenu {
414    fn write_key(&mut self) -> &mut DiffKey {
415        &mut self.key
416    }
417}
418
419impl SubMenu {
420    pub fn new() -> Self {
421        Self::default()
422    }
423
424    pub fn label(mut self, label: impl IntoElement) -> Self {
425        self.label = Some(label.into_element());
426        self
427    }
428}
429
430impl ChildrenExt for SubMenu {
431    fn get_children(&mut self) -> &mut Vec<Element> {
432        &mut self.items
433    }
434}
435
436impl ComponentOwned for SubMenu {
437    fn render(self) -> impl IntoElement {
438        let parent_menu_id = use_consume::<MenuId>();
439        let mut menus = use_consume::<State<Vec<MenuId>>>();
440        let mut menus_ids_generator = use_consume::<State<usize>>();
441
442        let submenu_id = use_hook(|| {
443            *menus_ids_generator.write() += 1;
444            let menu_id = MenuId(*menus_ids_generator.peek());
445            provide_context(menu_id);
446            menu_id
447        });
448
449        let show_submenu = menus.read().contains(&submenu_id);
450
451        let on_pointer_enter = move |_| {
452            close_menus_until(&mut menus, parent_menu_id);
453            push_menu(&mut menus, submenu_id);
454        };
455
456        let on_press = move |_| {
457            close_menus_until(&mut menus, parent_menu_id);
458            push_menu(&mut menus, submenu_id);
459        };
460
461        MenuItem::new()
462            .on_pointer_enter(on_pointer_enter)
463            .on_press(on_press)
464            .child(rect().horizontal().maybe_child(self.label.clone()))
465            .maybe_child(show_submenu.then(|| {
466                rect()
467                    .position(Position::new_absolute().top(-8.).right(-10.))
468                    .width(Size::px(0.))
469                    .height(Size::px(0.))
470                    .child(
471                        rect()
472                            .width(Size::window_percent(100.))
473                            .child(MenuContainer::new().children(self.items)),
474                    )
475            }))
476    }
477
478    fn render_key(&self) -> DiffKey {
479        self.key.clone().or(self.default_key())
480    }
481}
482
483static ROOT_MENU: MenuId = MenuId(0);
484
485#[derive(Clone, Copy, PartialEq, Eq)]
486struct MenuId(usize);
487
488fn close_menus_until(menus: &mut State<Vec<MenuId>>, until: MenuId) {
489    menus.write().retain(|&id| id.0 <= until.0);
490}
491
492fn push_menu(menus: &mut State<Vec<MenuId>>, id: MenuId) {
493    if !menus.read().contains(&id) {
494        menus.write().push(id);
495    }
496}