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 focus = use_focus();
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 {
235            group_id: focus.a11y_id(),
236        });
237
238        let (offset_x, offset_y, opacity) = match *measured.read() {
239            None => (0.0, 0.0, 0.0),
240            Some((area, win_w, win_h)) => (
241                overflow_offset(area.origin.x, area.size.width, win_w),
242                overflow_offset(area.origin.y, area.size.height, win_h),
243                1.0,
244            ),
245        };
246
247        rect()
248            .layer(Layer::Overlay)
249            .content(Content::fit())
250            .opacity(opacity)
251            .offset_x(offset_x)
252            .offset_y(offset_y)
253            .on_sized(move |e: Event<SizedEventData>| {
254                if measured.peek().is_none() {
255                    let window = Platform::get().root_size.peek();
256                    measured.set(Some((e.area, window.width, window.height)));
257                }
258            })
259            .child(
260                rect()
261                    .a11y_id(focus.a11y_id())
262                    .a11y_member_of(focus.a11y_id())
263                    .a11y_focusable(true)
264                    .a11y_role(AccessibilityRole::Menu)
265                    .shadow((0.0, 4.0, 10.0, 0., theme.shadow))
266                    .background(theme.background)
267                    .corner_radius(theme.corner_radius)
268                    .padding(theme.padding)
269                    .border(Border::new().width(1.).fill(theme.border_fill))
270                    .content(Content::fit())
271                    .children(self.children),
272            )
273    }
274
275    fn render_key(&self) -> DiffKey {
276        self.key.clone().or(self.default_key())
277    }
278}
279
280#[derive(Clone)]
281pub struct MenuGroup {
282    pub group_id: AccessibilityId,
283}
284
285/// A clickable menu item with hover and focus states.
286///
287/// This is the base component used by MenuButton and SubMenu.
288///
289/// # Example
290///
291/// ```rust
292/// # use freya::prelude::*;
293/// fn app() -> impl IntoElement {
294///     MenuItem::new()
295///         .on_press(|_| println!("Clicked!"))
296///         .child("Open File")
297/// }
298/// ```
299#[derive(Clone, PartialEq)]
300pub struct MenuItem {
301    pub(crate) theme: Option<MenuItemThemePartial>,
302    children: Vec<Element>,
303    on_press: Option<EventHandler<Event<PressEventData>>>,
304    on_pointer_enter: Option<EventHandler<Event<PointerEventData>>>,
305    selected: bool,
306    padding: Gaps,
307    key: DiffKey,
308}
309
310impl Default for MenuItem {
311    fn default() -> Self {
312        Self {
313            theme: None,
314            children: Vec::new(),
315            on_press: None,
316            on_pointer_enter: None,
317            selected: false,
318            padding: (6.0, 12.0).into(),
319            key: DiffKey::None,
320        }
321    }
322}
323
324impl KeyExt for MenuItem {
325    fn write_key(&mut self) -> &mut DiffKey {
326        &mut self.key
327    }
328}
329
330impl MenuItem {
331    pub fn new() -> Self {
332        Self::default()
333    }
334
335    pub fn on_press<F>(mut self, f: F) -> Self
336    where
337        F: Into<EventHandler<Event<PressEventData>>>,
338    {
339        self.on_press = Some(f.into());
340        self
341    }
342
343    pub fn on_pointer_enter<F>(mut self, f: F) -> Self
344    where
345        F: Into<EventHandler<Event<PointerEventData>>>,
346    {
347        self.on_pointer_enter = Some(f.into());
348        self
349    }
350
351    pub fn selected(mut self, selected: bool) -> Self {
352        self.selected = selected;
353        self
354    }
355
356    /// Set the padding for this menu item.
357    pub fn padding(mut self, padding: impl Into<Gaps>) -> Self {
358        self.padding = padding.into();
359        self
360    }
361
362    /// Get the current padding.
363    pub fn get_padding(&self) -> Gaps {
364        self.padding
365    }
366
367    /// Get the theme override for this component.
368    pub fn get_theme(&self) -> Option<&MenuItemThemePartial> {
369        self.theme.as_ref()
370    }
371
372    /// Set a theme override for this component.
373    pub fn theme(mut self, theme: MenuItemThemePartial) -> Self {
374        self.theme = Some(theme);
375        self
376    }
377}
378
379impl ChildrenExt for MenuItem {
380    fn get_children(&mut self) -> &mut Vec<Element> {
381        &mut self.children
382    }
383}
384
385impl ComponentOwned for MenuItem {
386    fn render(self) -> impl IntoElement {
387        let theme = get_theme!(self.theme, MenuItemThemePreference, "menu_item");
388        let mut hovering = use_state(|| false);
389        let focus = use_focus();
390        let focus_status = use_focus_status(focus);
391        let MenuGroup { group_id } = use_consume::<MenuGroup>();
392
393        let background = if self.selected {
394            theme.select_background
395        } else if hovering() {
396            theme.hover_background
397        } else {
398            theme.background
399        };
400
401        let border = if focus_status() == FocusStatus::Keyboard {
402            Border::new()
403                .fill(theme.select_border_fill)
404                .width(2.)
405                .alignment(BorderAlignment::Inner)
406        } else {
407            Border::new()
408                .fill(theme.border_fill)
409                .width(1.)
410                .alignment(BorderAlignment::Inner)
411        };
412
413        let on_pointer_enter = move |e: Event<PointerEventData>| {
414            hovering.set(true);
415            if let Some(on_pointer_enter) = &self.on_pointer_enter {
416                on_pointer_enter.call(e);
417            }
418        };
419
420        let on_pointer_leave = move |_| {
421            hovering.set(false);
422        };
423
424        let on_press = move |e: Event<PressEventData>| {
425            let prevent_default = e.get_prevent_default();
426            if let Some(on_press) = &self.on_press {
427                on_press.call(e);
428            }
429            if *prevent_default.borrow() {
430                focus.request_focus();
431            }
432        };
433
434        rect()
435            .a11y_role(AccessibilityRole::MenuItem)
436            .a11y_id(focus.a11y_id())
437            .a11y_focusable(true)
438            .a11y_member_of(group_id)
439            .min_width(Size::px(105.))
440            .width(Size::fill_minimum())
441            .content(Content::fit())
442            .padding(self.padding)
443            .corner_radius(theme.corner_radius)
444            .background(background)
445            .border(border)
446            .color(theme.color)
447            .text_align(TextAlign::Start)
448            .main_align(Alignment::Center)
449            .overflow(Overflow::Clip)
450            .on_pointer_enter(on_pointer_enter)
451            .on_pointer_leave(on_pointer_leave)
452            .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/// Like a button, but for Menus.
462///
463/// # Example
464///
465/// ```rust
466/// # use freya::prelude::*;
467/// fn app() -> impl IntoElement {
468///     MenuButton::new()
469///         .on_press(|_| println!("Clicked!"))
470///         .child("Item")
471/// }
472/// ```
473#[derive(Default, Clone, PartialEq)]
474pub struct MenuButton {
475    pub(crate) theme: Option<MenuItemThemePartial>,
476    children: Vec<Element>,
477    on_press: Option<EventHandler<Event<PressEventData>>>,
478    key: DiffKey,
479}
480
481impl ChildrenExt for MenuButton {
482    fn get_children(&mut self) -> &mut Vec<Element> {
483        &mut self.children
484    }
485}
486
487impl KeyExt for MenuButton {
488    fn write_key(&mut self) -> &mut DiffKey {
489        &mut self.key
490    }
491}
492
493impl MenuButton {
494    pub fn new() -> Self {
495        Self::default()
496    }
497
498    pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
499        self.on_press = Some(on_press.into());
500        self
501    }
502
503    /// Set a theme override for the inner [`MenuItem`].
504    pub fn theme(mut self, theme: MenuItemThemePartial) -> Self {
505        self.theme = Some(theme);
506        self
507    }
508}
509
510impl ComponentOwned for MenuButton {
511    fn render(self) -> impl IntoElement {
512        let mut menus = use_consume::<State<Vec<MenuId>>>();
513        let parent_menu_id = use_consume::<MenuId>();
514
515        MenuItem::new()
516            .map(self.theme, |el, theme| el.theme(theme))
517            .on_pointer_enter(move |_| close_menus_until(&mut menus, parent_menu_id))
518            .map(self.on_press, |el, on_press| el.on_press(on_press))
519            .children(self.children)
520    }
521
522    fn render_key(&self) -> DiffKey {
523        self.key.clone().or(self.default_key())
524    }
525}
526
527/// Create sub menus inside a Menu.
528///
529/// # Example
530///
531/// ```rust
532/// # use freya::prelude::*;
533/// fn app() -> impl IntoElement {
534///     SubMenu::new()
535///         .label("Export")
536///         .child(MenuButton::new().child("PDF"))
537/// }
538/// ```
539#[derive(Default, Clone, PartialEq)]
540pub struct SubMenu {
541    pub(crate) theme: Option<MenuContainerThemePartial>,
542    label: Option<Element>,
543    items: Vec<Element>,
544    key: DiffKey,
545}
546
547impl KeyExt for SubMenu {
548    fn write_key(&mut self) -> &mut DiffKey {
549        &mut self.key
550    }
551}
552
553impl SubMenu {
554    pub fn new() -> Self {
555        Self::default()
556    }
557
558    pub fn label(mut self, label: impl IntoElement) -> Self {
559        self.label = Some(label.into_element());
560        self
561    }
562
563    /// Set a theme override for the inner [`MenuContainer`].
564    pub fn theme(mut self, theme: MenuContainerThemePartial) -> Self {
565        self.theme = Some(theme);
566        self
567    }
568}
569
570impl ChildrenExt for SubMenu {
571    fn get_children(&mut self) -> &mut Vec<Element> {
572        &mut self.items
573    }
574}
575
576impl ComponentOwned for SubMenu {
577    fn render(self) -> impl IntoElement {
578        let parent_menu_id = use_consume::<MenuId>();
579        let mut menus = use_consume::<State<Vec<MenuId>>>();
580        let mut menus_ids_generator = use_consume::<State<usize>>();
581
582        let submenu_id = use_hook(|| {
583            *menus_ids_generator.write() += 1;
584            let menu_id = MenuId(*menus_ids_generator.peek());
585            provide_context(menu_id);
586            menu_id
587        });
588
589        let show_submenu = menus.read().contains(&submenu_id);
590
591        let on_pointer_enter = move |_| {
592            close_menus_until(&mut menus, parent_menu_id);
593            push_menu(&mut menus, submenu_id);
594        };
595
596        let on_press = move |_| {
597            close_menus_until(&mut menus, parent_menu_id);
598            push_menu(&mut menus, submenu_id);
599        };
600
601        MenuItem::new()
602            .on_pointer_enter(on_pointer_enter)
603            .on_press(on_press)
604            .child(rect().horizontal().maybe_child(self.label.clone()))
605            .maybe_child(show_submenu.then(|| {
606                rect()
607                    .position(Position::new_absolute().top(-8.).right(-10.))
608                    .width(Size::px(0.))
609                    .height(Size::px(0.))
610                    .child(
611                        rect().width(Size::window_percent(100.)).child(
612                            MenuContainer::new()
613                                .map(self.theme, |el, theme| el.theme(theme))
614                                .children(self.items),
615                        ),
616                    )
617            }))
618    }
619
620    fn render_key(&self) -> DiffKey {
621        self.key.clone().or(self.default_key())
622    }
623}
624
625/// Returns a negative offset to shift an element back within the window boundary,
626/// or `0.0` if it already fits.
627fn overflow_offset(origin: f32, size: f32, window: f32) -> f32 {
628    let overflow = origin + size - window;
629    if overflow > 0.0 {
630        -overflow.min(origin)
631    } else {
632        0.0
633    }
634}
635
636static ROOT_MENU: MenuId = MenuId(0);
637
638#[derive(Clone, Copy, PartialEq, Eq)]
639struct MenuId(usize);
640
641fn close_menus_until(menus: &mut State<Vec<MenuId>>, until: MenuId) {
642    menus.write().retain(|&id| id.0 <= until.0);
643}
644
645fn push_menu(menus: &mut State<Vec<MenuId>>, id: MenuId) {
646    if !menus.read().contains(&id) {
647        menus.write().push(id);
648    }
649}