Skip to main content

freya_components/
menu.rs

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