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