freya_components/
menu.rs

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