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