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 || focus.a11y_id());
149
150        rect()
151            .a11y_id(focus.a11y_id())
152            .a11y_member_of(focus.a11y_id())
153            .a11y_focusable(true)
154            .a11y_role(AccessibilityRole::Menu)
155            .position(Position::new_absolute())
156            .shadow((0.0, 4.0, 10.0, 0., theme.shadow))
157            .background(theme.background)
158            .corner_radius(theme.corner_radius)
159            .padding(theme.padding)
160            .border(Border::new().width(1.).fill(theme.border_fill))
161            .content(Content::fit())
162            .children(self.children)
163    }
164
165    fn render_key(&self) -> DiffKey {
166        self.key.clone().or(self.default_key())
167    }
168}
169
170/// A clickable menu item with hover and focus states.
171///
172/// This is the base component used by MenuButton and SubMenu.
173///
174/// # Example
175///
176/// ```rust
177/// # use freya::prelude::*;
178/// fn app() -> impl IntoElement {
179///     MenuItem::new()
180///         .on_press(|_| println!("Clicked!"))
181///         .child("Open File")
182/// }
183/// ```
184#[derive(Default, Clone, PartialEq)]
185pub struct MenuItem {
186    pub(crate) theme: Option<MenuItemThemePartial>,
187    children: Vec<Element>,
188    on_press: Option<EventHandler<Event<PressEventData>>>,
189    on_pointer_enter: Option<EventHandler<Event<PointerEventData>>>,
190    key: DiffKey,
191}
192
193impl KeyExt for MenuItem {
194    fn write_key(&mut self) -> &mut DiffKey {
195        &mut self.key
196    }
197}
198
199impl MenuItem {
200    pub fn new() -> Self {
201        Self::default()
202    }
203
204    pub fn on_press<F>(mut self, f: F) -> Self
205    where
206        F: Into<EventHandler<Event<PressEventData>>>,
207    {
208        self.on_press = Some(f.into());
209        self
210    }
211
212    pub fn on_pointer_enter<F>(mut self, f: F) -> Self
213    where
214        F: Into<EventHandler<Event<PointerEventData>>>,
215    {
216        self.on_pointer_enter = Some(f.into());
217        self
218    }
219}
220
221impl ChildrenExt for MenuItem {
222    fn get_children(&mut self) -> &mut Vec<Element> {
223        &mut self.children
224    }
225}
226
227impl RenderOwned for MenuItem {
228    fn render(self) -> impl IntoElement {
229        let theme = get_theme!(self.theme, menu_item);
230        let mut hovering = use_state(|| false);
231        let focus = use_focus();
232        let focus_status = use_focus_status(focus);
233        let menu_group = use_consume::<AccessibilityId>();
234
235        let background = if focus_status() == FocusStatus::Keyboard || *hovering.read() {
236            theme.hover_background
237        } else {
238            Color::TRANSPARENT
239        };
240
241        let on_pointer_enter = move |e| {
242            hovering.set(true);
243            if let Some(on_pointer_enter) = &self.on_pointer_enter {
244                on_pointer_enter.call(e);
245            }
246        };
247
248        let on_pointer_leave = move |_| {
249            hovering.set(false);
250        };
251
252        let on_press = move |e: Event<PressEventData>| {
253            e.stop_propagation();
254            e.prevent_default();
255            focus.request_focus();
256            if let Some(on_press) = &self.on_press {
257                on_press.call(e);
258            }
259        };
260
261        rect()
262            .a11y_role(AccessibilityRole::MenuItem)
263            .a11y_id(focus.a11y_id())
264            .a11y_focusable(true)
265            .a11y_member_of(menu_group)
266            .min_width(Size::px(105.))
267            .width(Size::fill_minimum())
268            .padding((4.0, 10.0))
269            .corner_radius(theme.corner_radius)
270            .background(background)
271            .color(theme.color)
272            .text_align(TextAlign::Start)
273            .main_align(Alignment::Center)
274            .on_pointer_enter(on_pointer_enter)
275            .on_pointer_leave(on_pointer_leave)
276            .on_press(on_press)
277            .children(self.children)
278    }
279
280    fn render_key(&self) -> DiffKey {
281        self.key.clone().or(self.default_key())
282    }
283}
284
285/// Like a button, but for Menus.
286///
287/// # Example
288///
289/// ```rust
290/// # use freya::prelude::*;
291/// fn app() -> impl IntoElement {
292///     MenuButton::new()
293///         .on_press(|_| println!("Clicked!"))
294///         .child("Item")
295/// }
296/// ```
297#[derive(Default, Clone, PartialEq)]
298pub struct MenuButton {
299    children: Vec<Element>,
300    on_press: Option<EventHandler<()>>,
301    key: DiffKey,
302}
303
304impl ChildrenExt for MenuButton {
305    fn get_children(&mut self) -> &mut Vec<Element> {
306        &mut self.children
307    }
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 on_press(mut self, on_press: impl Into<EventHandler<()>>) -> Self {
322        self.on_press = Some(on_press.into());
323        self
324    }
325}
326
327impl RenderOwned for MenuButton {
328    fn render(self) -> impl IntoElement {
329        let mut menus = use_consume::<State<Vec<MenuId>>>();
330        let parent_menu_id = use_consume::<MenuId>();
331
332        MenuItem::new()
333            .on_pointer_enter(move |_| close_menus_until(&mut menus, parent_menu_id))
334            .on_press(move |_| {
335                if let Some(on_press) = &self.on_press {
336                    on_press.call(());
337                }
338            })
339            .children(self.children)
340    }
341
342    fn render_key(&self) -> DiffKey {
343        self.key.clone().or(self.default_key())
344    }
345}
346
347/// Create sub menus inside a Menu.
348///
349/// # Example
350///
351/// ```rust
352/// # use freya::prelude::*;
353/// fn app() -> impl IntoElement {
354///     SubMenu::new()
355///         .label("Export")
356///         .child(MenuButton::new().child("PDF"))
357/// }
358/// ```
359#[derive(Default, Clone, PartialEq)]
360pub struct SubMenu {
361    label: Option<Element>,
362    items: Vec<Element>,
363    key: DiffKey,
364}
365
366impl KeyExt for SubMenu {
367    fn write_key(&mut self) -> &mut DiffKey {
368        &mut self.key
369    }
370}
371
372impl SubMenu {
373    pub fn new() -> Self {
374        Self::default()
375    }
376
377    pub fn label(mut self, label: impl IntoElement) -> Self {
378        self.label = Some(label.into_element());
379        self
380    }
381}
382
383impl ChildrenExt for SubMenu {
384    fn get_children(&mut self) -> &mut Vec<Element> {
385        &mut self.items
386    }
387}
388
389impl RenderOwned for SubMenu {
390    fn render(self) -> impl IntoElement {
391        let parent_menu_id = use_consume::<MenuId>();
392        let mut menus = use_consume::<State<Vec<MenuId>>>();
393        let mut menus_ids_generator = use_consume::<State<usize>>();
394
395        let submenu_id = use_hook(|| {
396            *menus_ids_generator.write() += 1;
397            let menu_id = MenuId(*menus_ids_generator.peek());
398            provide_context(menu_id);
399            menu_id
400        });
401
402        let show_submenu = menus.read().contains(&submenu_id);
403
404        let onmouseenter = move |_| {
405            close_menus_until(&mut menus, parent_menu_id);
406            push_menu(&mut menus, submenu_id);
407        };
408
409        let onpress = move |_| {
410            close_menus_until(&mut menus, parent_menu_id);
411            push_menu(&mut menus, submenu_id);
412        };
413
414        MenuItem::new()
415            .on_pointer_enter(onmouseenter)
416            .on_press(onpress)
417            .child(rect().horizontal().maybe_child(self.label.clone()))
418            .maybe_child(show_submenu.then(|| {
419                rect()
420                    .position(Position::new_absolute().top(-8.).right(-10.))
421                    .width(Size::px(0.))
422                    .height(Size::px(0.))
423                    .child(
424                        rect()
425                            .width(Size::window_percent(100.))
426                            .child(MenuContainer::new().children(self.items)),
427                    )
428            }))
429    }
430
431    fn render_key(&self) -> DiffKey {
432        self.key.clone().or(self.default_key())
433    }
434}
435
436static ROOT_MENU: MenuId = MenuId(0);
437
438#[derive(Clone, Copy, PartialEq, Eq)]
439struct MenuId(usize);
440
441fn close_menus_until(menus: &mut State<Vec<MenuId>>, until: MenuId) {
442    menus.write().retain(|&id| id.0 <= until.0);
443}
444
445fn push_menu(menus: &mut State<Vec<MenuId>>, id: MenuId) {
446    if !menus.read().contains(&id) {
447        menus.write().push(id);
448    }
449}