Skip to main content

freya_components/
menu.rs

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