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