1use freya_core::prelude::*;
2use torin::{
3 content::Content,
4 gaps::Gaps,
5 prelude::{
6 Alignment,
7 Area,
8 Position,
9 },
10 size::Size,
11};
12
13use crate::{
14 define_theme,
15 get_theme,
16};
17
18define_theme! {
19 for = MenuContainer; theme_field = theme;
20 for = Menu; theme_field = theme;
21 for = SubMenu; theme_field = theme;
22
23 %[component]
24 pub MenuContainer {
25 %[fields]
26 background: Color,
27 padding: Gaps,
28 shadow: Color,
29 border_fill: Color,
30 corner_radius: CornerRadius,
31 }
32}
33
34define_theme! {
35 for = MenuItem; theme_field = theme;
36 for = MenuButton; theme_field = theme;
37
38 %[component]
39 pub MenuItem {
40 %[fields]
41 background: Color,
42 hover_background: Color,
43 select_background: Color,
44 border_fill: Color,
45 select_border_fill: Color,
46 corner_radius: CornerRadius,
47 color: Color,
48 }
49}
50
51#[cfg_attr(feature = "docs",
101 doc = embed_doc_image::embed_image!("menu", "images/gallery_menu.png"),
102)]
103#[derive(Default, Clone, PartialEq)]
104pub struct Menu {
105 pub(crate) theme: Option<MenuContainerThemePartial>,
106 children: Vec<Element>,
107 on_close: Option<EventHandler<()>>,
108 key: DiffKey,
109}
110
111impl ChildrenExt for Menu {
112 fn get_children(&mut self) -> &mut Vec<Element> {
113 &mut self.children
114 }
115}
116
117impl KeyExt for Menu {
118 fn write_key(&mut self) -> &mut DiffKey {
119 &mut self.key
120 }
121}
122
123impl Menu {
124 pub fn new() -> Self {
125 Self::default()
126 }
127
128 pub fn on_close<F>(mut self, f: F) -> Self
129 where
130 F: Into<EventHandler<()>>,
131 {
132 self.on_close = Some(f.into());
133 self
134 }
135
136 pub fn theme(mut self, theme: MenuContainerThemePartial) -> Self {
137 self.theme = Some(theme);
138 self
139 }
140}
141
142impl ComponentOwned for Menu {
143 fn render(self) -> impl IntoElement {
144 use_provide_context(|| State::create(ROOT_MENU.0));
146 let mut menus =
148 use_provide_context::<State<Vec<MenuId>>>(|| State::create(vec![ROOT_MENU]));
149 use_provide_context(|| ROOT_MENU);
151
152 let on_close = self.on_close.clone();
153 let on_global_key_down = move |e: Event<KeyboardEventData>| {
154 if e.key == Key::Named(NamedKey::Escape) {
155 if menus.read().len() > 1 {
156 menus.write().pop();
157 } else if let Some(on_close) = &on_close {
158 on_close.call(());
159 }
160 }
161 };
162
163 rect()
164 .layer(Layer::Overlay)
165 .corner_radius(8.0)
166 .on_press(move |ev: Event<PressEventData>| {
167 ev.stop_propagation();
168 })
169 .on_global_pointer_press(move |_: Event<PointerEventData>| {
170 if let Some(on_close) = &self.on_close {
171 on_close.call(());
172 }
173 })
174 .on_global_key_down(on_global_key_down)
175 .child(
176 MenuContainer::new()
177 .map(self.theme, |el, theme| el.theme(theme))
178 .children(self.children),
179 )
180 }
181 fn render_key(&self) -> DiffKey {
182 self.key.clone().or(self.default_key())
183 }
184}
185
186#[derive(Default, Clone, PartialEq)]
199pub struct MenuContainer {
200 pub(crate) theme: Option<MenuContainerThemePartial>,
201 children: Vec<Element>,
202 key: DiffKey,
203}
204
205impl KeyExt for MenuContainer {
206 fn write_key(&mut self) -> &mut DiffKey {
207 &mut self.key
208 }
209}
210
211impl ChildrenExt for MenuContainer {
212 fn get_children(&mut self) -> &mut Vec<Element> {
213 &mut self.children
214 }
215}
216
217impl MenuContainer {
218 pub fn new() -> Self {
219 Self::default()
220 }
221
222 pub fn theme(mut self, theme: MenuContainerThemePartial) -> Self {
223 self.theme = Some(theme);
224 self
225 }
226}
227
228impl ComponentOwned for MenuContainer {
229 fn render(self) -> impl IntoElement {
230 let a11y_id = use_a11y();
231 let theme = get_theme!(self.theme, MenuContainerThemePreference, "menu_container");
232 let mut measured = use_state(|| None::<(Area, f32, f32)>);
233
234 use_provide_context(move || MenuGroup { group_id: a11y_id });
235
236 let (offset_x, offset_y, opacity) = match *measured.read() {
237 None => (0.0, 0.0, 0.0),
238 Some((area, win_w, win_h)) => (
239 overflow_offset(area.origin.x, area.size.width, win_w),
240 overflow_offset(area.origin.y, area.size.height, win_h),
241 1.0,
242 ),
243 };
244
245 rect()
246 .layer(Layer::Overlay)
247 .content(Content::fit())
248 .opacity(opacity)
249 .offset_x(offset_x)
250 .offset_y(offset_y)
251 .on_sized(move |e: Event<SizedEventData>| {
252 if measured.peek().is_none() {
253 let window = Platform::get().root_size.peek();
254 measured.set(Some((e.area, window.width, window.height)));
255 }
256 })
257 .child(
258 rect()
259 .a11y_id(a11y_id)
260 .a11y_member_of(a11y_id)
261 .a11y_focusable(true)
262 .a11y_role(AccessibilityRole::Menu)
263 .shadow((0.0, 4.0, 10.0, 0., theme.shadow))
264 .background(theme.background)
265 .corner_radius(theme.corner_radius)
266 .padding(theme.padding)
267 .border(Border::new().width(1.).fill(theme.border_fill))
268 .content(Content::fit())
269 .children(self.children),
270 )
271 }
272
273 fn render_key(&self) -> DiffKey {
274 self.key.clone().or(self.default_key())
275 }
276}
277
278#[derive(Clone)]
279pub struct MenuGroup {
280 pub group_id: AccessibilityId,
281}
282
283#[derive(Clone, PartialEq)]
298pub struct MenuItem {
299 pub(crate) theme: Option<MenuItemThemePartial>,
300 children: Vec<Element>,
301 on_press: Option<EventHandler<Event<PressEventData>>>,
302 on_pointer_enter: Option<EventHandler<Event<PointerEventData>>>,
303 selected: bool,
304 padding: Gaps,
305 key: DiffKey,
306}
307
308impl Default for MenuItem {
309 fn default() -> Self {
310 Self {
311 theme: None,
312 children: Vec::new(),
313 on_press: None,
314 on_pointer_enter: None,
315 selected: false,
316 padding: (6.0, 12.0).into(),
317 key: DiffKey::None,
318 }
319 }
320}
321
322impl KeyExt for MenuItem {
323 fn write_key(&mut self) -> &mut DiffKey {
324 &mut self.key
325 }
326}
327
328impl MenuItem {
329 pub fn new() -> Self {
330 Self::default()
331 }
332
333 pub fn on_press<F>(mut self, f: F) -> Self
334 where
335 F: Into<EventHandler<Event<PressEventData>>>,
336 {
337 self.on_press = Some(f.into());
338 self
339 }
340
341 pub fn on_pointer_enter<F>(mut self, f: F) -> Self
342 where
343 F: Into<EventHandler<Event<PointerEventData>>>,
344 {
345 self.on_pointer_enter = Some(f.into());
346 self
347 }
348
349 pub fn selected(mut self, selected: bool) -> Self {
350 self.selected = selected;
351 self
352 }
353
354 pub fn padding(mut self, padding: impl Into<Gaps>) -> Self {
356 self.padding = padding.into();
357 self
358 }
359
360 pub fn get_padding(&self) -> Gaps {
362 self.padding
363 }
364
365 pub fn get_theme(&self) -> Option<&MenuItemThemePartial> {
367 self.theme.as_ref()
368 }
369
370 pub fn theme(mut self, theme: MenuItemThemePartial) -> Self {
372 self.theme = Some(theme);
373 self
374 }
375}
376
377impl ChildrenExt for MenuItem {
378 fn get_children(&mut self) -> &mut Vec<Element> {
379 &mut self.children
380 }
381}
382
383impl ComponentOwned for MenuItem {
384 fn render(self) -> impl IntoElement {
385 let theme = get_theme!(self.theme, MenuItemThemePreference, "menu_item");
386 let mut hovering = use_state(|| false);
387 let a11y_id = use_a11y();
388 let focus = use_focus(a11y_id);
389 let MenuGroup { group_id } = use_consume::<MenuGroup>();
390
391 let background = if self.selected {
392 theme.select_background
393 } else if hovering() {
394 theme.hover_background
395 } else {
396 theme.background
397 };
398
399 let border = if focus() == Focus::Keyboard {
400 Border::new()
401 .fill(theme.select_border_fill)
402 .width(2.)
403 .alignment(BorderAlignment::Inner)
404 } else {
405 Border::new()
406 .fill(theme.border_fill)
407 .width(1.)
408 .alignment(BorderAlignment::Inner)
409 };
410
411 let on_pointer_enter = move |e: Event<PointerEventData>| {
412 hovering.set(true);
413 if let Some(on_pointer_enter) = &self.on_pointer_enter {
414 on_pointer_enter.call(e);
415 }
416 };
417
418 let on_pointer_leave = move |_| {
419 hovering.set(false);
420 };
421
422 let on_press = move |e: Event<PressEventData>| {
423 let prevent_default = e.get_prevent_default();
424 if let Some(on_press) = &self.on_press {
425 on_press.call(e);
426 }
427 if *prevent_default.borrow() {
428 a11y_id.request_focus();
429 }
430 };
431
432 rect()
433 .a11y_role(AccessibilityRole::MenuItem)
434 .a11y_id(a11y_id)
435 .a11y_focusable(true)
436 .a11y_member_of(group_id)
437 .min_width(Size::px(105.))
438 .width(Size::fill_minimum())
439 .content(Content::fit())
440 .padding(self.padding)
441 .corner_radius(theme.corner_radius)
442 .background(background)
443 .border(border)
444 .color(theme.color)
445 .text_align(TextAlign::Start)
446 .main_align(Alignment::Center)
447 .overflow(Overflow::Clip)
448 .on_pointer_enter(on_pointer_enter)
449 .on_pointer_leave(on_pointer_leave)
450 .on_press(on_press)
451 .children(self.children)
452 }
453
454 fn render_key(&self) -> DiffKey {
455 self.key.clone().or(self.default_key())
456 }
457}
458
459#[derive(Default, Clone, PartialEq)]
472pub struct MenuButton {
473 pub(crate) theme: Option<MenuItemThemePartial>,
474 children: Vec<Element>,
475 on_press: Option<EventHandler<Event<PressEventData>>>,
476 key: DiffKey,
477}
478
479impl ChildrenExt for MenuButton {
480 fn get_children(&mut self) -> &mut Vec<Element> {
481 &mut self.children
482 }
483}
484
485impl KeyExt for MenuButton {
486 fn write_key(&mut self) -> &mut DiffKey {
487 &mut self.key
488 }
489}
490
491impl MenuButton {
492 pub fn new() -> Self {
493 Self::default()
494 }
495
496 pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
497 self.on_press = Some(on_press.into());
498 self
499 }
500
501 pub fn theme(mut self, theme: MenuItemThemePartial) -> Self {
503 self.theme = Some(theme);
504 self
505 }
506}
507
508impl ComponentOwned for MenuButton {
509 fn render(self) -> impl IntoElement {
510 let mut menus = use_consume::<State<Vec<MenuId>>>();
511 let parent_menu_id = use_consume::<MenuId>();
512
513 MenuItem::new()
514 .map(self.theme, |el, theme| el.theme(theme))
515 .on_pointer_enter(move |_| close_menus_until(&mut menus, parent_menu_id))
516 .map(self.on_press, |el, on_press| el.on_press(on_press))
517 .children(self.children)
518 }
519
520 fn render_key(&self) -> DiffKey {
521 self.key.clone().or(self.default_key())
522 }
523}
524
525#[derive(Default, Clone, PartialEq)]
538pub struct SubMenu {
539 pub(crate) theme: Option<MenuContainerThemePartial>,
540 label: Option<Element>,
541 items: Vec<Element>,
542 key: DiffKey,
543}
544
545impl KeyExt for SubMenu {
546 fn write_key(&mut self) -> &mut DiffKey {
547 &mut self.key
548 }
549}
550
551impl SubMenu {
552 pub fn new() -> Self {
553 Self::default()
554 }
555
556 pub fn label(mut self, label: impl IntoElement) -> Self {
557 self.label = Some(label.into_element());
558 self
559 }
560
561 pub fn theme(mut self, theme: MenuContainerThemePartial) -> Self {
563 self.theme = Some(theme);
564 self
565 }
566}
567
568impl ChildrenExt for SubMenu {
569 fn get_children(&mut self) -> &mut Vec<Element> {
570 &mut self.items
571 }
572}
573
574impl ComponentOwned for SubMenu {
575 fn render(self) -> impl IntoElement {
576 let parent_menu_id = use_consume::<MenuId>();
577 let mut menus = use_consume::<State<Vec<MenuId>>>();
578 let mut menus_ids_generator = use_consume::<State<usize>>();
579
580 let submenu_id = use_hook(|| {
581 *menus_ids_generator.write() += 1;
582 let menu_id = MenuId(*menus_ids_generator.peek());
583 provide_context(menu_id);
584 menu_id
585 });
586
587 let show_submenu = menus.read().contains(&submenu_id);
588
589 let on_pointer_enter = move |_| {
590 close_menus_until(&mut menus, parent_menu_id);
591 push_menu(&mut menus, submenu_id);
592 };
593
594 let on_press = move |_| {
595 close_menus_until(&mut menus, parent_menu_id);
596 push_menu(&mut menus, submenu_id);
597 };
598
599 MenuItem::new()
600 .on_pointer_enter(on_pointer_enter)
601 .on_press(on_press)
602 .child(rect().horizontal().maybe_child(self.label.clone()))
603 .maybe_child(show_submenu.then(|| {
604 rect()
605 .position(Position::new_absolute().top(-8.).right(-10.))
606 .width(Size::px(0.))
607 .height(Size::px(0.))
608 .child(
609 rect().width(Size::window_percent(100.)).child(
610 MenuContainer::new()
611 .map(self.theme, |el, theme| el.theme(theme))
612 .children(self.items),
613 ),
614 )
615 }))
616 }
617
618 fn render_key(&self) -> DiffKey {
619 self.key.clone().or(self.default_key())
620 }
621}
622
623fn overflow_offset(origin: f32, size: f32, window: f32) -> f32 {
626 let overflow = origin + size - window;
627 if overflow > 0.0 {
628 -overflow.min(origin)
629 } else {
630 0.0
631 }
632}
633
634static ROOT_MENU: MenuId = MenuId(0);
635
636#[derive(Clone, Copy, PartialEq, Eq)]
637struct MenuId(usize);
638
639fn close_menus_until(menus: &mut State<Vec<MenuId>>, until: MenuId) {
640 menus.write().retain(|&id| id.0 <= until.0);
641}
642
643fn push_menu(menus: &mut State<Vec<MenuId>>, id: MenuId) {
644 if !menus.read().contains(&id) {
645 menus.write().push(id);
646 }
647}