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