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