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