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