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