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