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#[derive(Default, Clone, PartialEq)]
84pub struct Menu {
85 pub(crate) theme: Option<MenuContainerThemePartial>,
86 children: Vec<Element>,
87 on_close: Option<EventHandler<()>>,
88 on_escape: Option<EventHandler<()>>,
89 key: DiffKey,
90}
91
92impl ChildrenExt for Menu {
93 fn children_mut(&mut self) -> &mut Vec<Element> {
94 &mut self.children
95 }
96}
97
98impl KeyExt for Menu {
99 fn key_mut(&mut self) -> &mut DiffKey {
100 &mut self.key
101 }
102}
103
104impl Menu {
105 pub fn new() -> Self {
106 Self::default()
107 }
108
109 pub fn on_escape(mut self, f: impl Into<EventHandler<()>>) -> Self {
111 self.on_escape = Some(f.into());
112 self
113 }
114
115 pub fn on_close<F>(mut self, f: F) -> Self
116 where
117 F: Into<EventHandler<()>>,
118 {
119 self.on_close = Some(f.into());
120 self
121 }
122
123 pub fn theme(mut self, theme: MenuContainerThemePartial) -> Self {
124 self.theme = Some(theme);
125 self
126 }
127}
128
129impl ComponentOwned for Menu {
130 fn render(self) -> impl IntoElement {
131 use_provide_context(|| State::create(ROOT_MENU.0));
133 let mut menus =
135 use_provide_context::<State<Vec<MenuId>>>(|| State::create(vec![ROOT_MENU]));
136 use_provide_context(|| ROOT_MENU);
138
139 let on_escape = self.on_escape.clone().or(self.on_close.clone());
140 let on_global_key_down = move |e: Event<KeyboardEventData>| {
141 if e.key == Key::Named(NamedKey::Escape) {
142 if menus.read().len() > 1 {
143 menus.write().pop();
144 } else if let Some(on_escape) = &on_escape {
145 on_escape.call(());
146 }
147 }
148 };
149
150 rect()
151 .layer(Layer::Overlay)
152 .corner_radius(8.0)
153 .on_press(move |ev: Event<PressEventData>| {
154 ev.stop_propagation();
155 })
156 .on_global_pointer_up(move |_: Event<PointerEventData>| {
157 if let Some(on_close) = &self.on_close {
158 on_close.call(());
159 }
160 })
161 .on_global_key_down(on_global_key_down)
162 .child(
163 MenuContainer::new()
164 .map(self.theme, |el, theme| el.theme(theme))
165 .children(self.children),
166 )
167 }
168 fn render_key(&self) -> DiffKey {
169 self.key.clone().or(self.default_key())
170 }
171}
172
173#[derive(Default, Clone, PartialEq)]
186pub struct MenuContainer {
187 pub(crate) theme: Option<MenuContainerThemePartial>,
188 children: Vec<Element>,
189 key: DiffKey,
190}
191
192impl KeyExt for MenuContainer {
193 fn key_mut(&mut self) -> &mut DiffKey {
194 &mut self.key
195 }
196}
197
198impl ChildrenExt for MenuContainer {
199 fn children_mut(&mut self) -> &mut Vec<Element> {
200 &mut self.children
201 }
202}
203
204impl MenuContainer {
205 pub fn new() -> Self {
206 Self::default()
207 }
208
209 pub fn theme(mut self, theme: MenuContainerThemePartial) -> Self {
210 self.theme = Some(theme);
211 self
212 }
213}
214
215impl ComponentOwned for MenuContainer {
216 fn render(self) -> impl IntoElement {
217 let a11y_id = use_a11y();
218 let theme = get_theme!(self.theme, MenuContainerThemePreference, "menu_container");
219 let mut measured = use_state(|| None::<(Area, Size2D)>);
220
221 use_provide_context(move || MenuGroup { group_id: a11y_id });
222
223 let (offset_x, offset_y, opacity) = match measured() {
224 None => (0.0, 0.0, 0.0),
225 Some((area, root_size)) => (
226 overflow_offset(area.origin.x, area.size.width, root_size.width),
227 overflow_offset(area.origin.y, area.size.height, root_size.height),
228 1.0,
229 ),
230 };
231
232 rect()
233 .layer(Layer::Overlay)
234 .content(Content::fit())
235 .opacity(opacity)
236 .offset_x(offset_x)
237 .offset_y(offset_y)
238 .on_sized(move |e: Event<SizedEventData>| {
239 if measured.peek().is_none() {
240 let root_size = *Platform::get().current_window().root_size.peek();
241 measured.set(Some((e.area, root_size)));
242 }
243 })
244 .child(
245 rect()
246 .a11y_id(a11y_id)
247 .a11y_member_of(a11y_id)
248 .a11y_focusable(true)
249 .a11y_role(AccessibilityRole::Menu)
250 .shadow((0.0, 4.0, 10.0, 0., theme.shadow))
251 .background(theme.background)
252 .corner_radius(theme.corner_radius)
253 .padding(theme.padding)
254 .border(Border::new().width(1.).fill(theme.border_fill))
255 .content(Content::fit())
256 .children(self.children),
257 )
258 }
259
260 fn render_key(&self) -> DiffKey {
261 self.key.clone().or(self.default_key())
262 }
263}
264
265#[derive(Clone)]
266pub struct MenuGroup {
267 pub group_id: AccessibilityId,
268}
269
270#[derive(Clone, PartialEq)]
285pub struct MenuItem {
286 pub(crate) theme: Option<MenuItemThemePartial>,
287 children: Vec<Element>,
288 on_press: Option<EventHandler<Event<PressEventData>>>,
289 on_pointer_enter: Option<EventHandler<Event<PointerEventData>>>,
290 selected: bool,
291 padding: Gaps,
292 key: DiffKey,
293}
294
295impl Default for MenuItem {
296 fn default() -> Self {
297 Self {
298 theme: None,
299 children: Vec::new(),
300 on_press: None,
301 on_pointer_enter: None,
302 selected: false,
303 padding: (8.0, 14.0).into(),
304 key: DiffKey::None,
305 }
306 }
307}
308
309impl KeyExt for MenuItem {
310 fn key_mut(&mut self) -> &mut DiffKey {
311 &mut self.key
312 }
313}
314
315impl MenuItem {
316 pub fn new() -> Self {
317 Self::default()
318 }
319
320 pub fn on_press<F>(mut self, f: F) -> Self
321 where
322 F: Into<EventHandler<Event<PressEventData>>>,
323 {
324 self.on_press = Some(f.into());
325 self
326 }
327
328 pub fn on_pointer_enter<F>(mut self, f: F) -> Self
329 where
330 F: Into<EventHandler<Event<PointerEventData>>>,
331 {
332 self.on_pointer_enter = Some(f.into());
333 self
334 }
335
336 pub fn selected(mut self, selected: bool) -> Self {
337 self.selected = selected;
338 self
339 }
340
341 pub fn padding(mut self, padding: impl Into<Gaps>) -> Self {
343 self.padding = padding.into();
344 self
345 }
346
347 pub fn get_padding(&self) -> Gaps {
349 self.padding
350 }
351
352 pub fn get_theme(&self) -> Option<&MenuItemThemePartial> {
354 self.theme.as_ref()
355 }
356
357 pub fn theme(mut self, theme: MenuItemThemePartial) -> Self {
359 self.theme = Some(theme);
360 self
361 }
362}
363
364impl ChildrenExt for MenuItem {
365 fn children_mut(&mut self) -> &mut Vec<Element> {
366 &mut self.children
367 }
368}
369
370impl ComponentOwned for MenuItem {
371 fn render(self) -> impl IntoElement {
372 let theme = get_theme!(self.theme, MenuItemThemePreference, "menu_item");
373 let mut hovering = use_state(|| false);
374 let a11y_id = use_a11y();
375 let focus = use_focus(a11y_id);
376 let MenuGroup { group_id } = use_consume::<MenuGroup>();
377
378 let background = if self.selected {
379 theme.select_background
380 } else if hovering() {
381 theme.hover_background
382 } else {
383 theme.background
384 };
385
386 let color = if self.selected {
387 theme.select_color
388 } else {
389 theme.color
390 };
391
392 let border = if focus() == Focus::Keyboard {
393 Border::new()
394 .fill(theme.focus_border_fill)
395 .width(2.)
396 .alignment(BorderAlignment::Inner)
397 } else {
398 Border::new()
399 .fill(theme.border_fill)
400 .width(1.)
401 .alignment(BorderAlignment::Inner)
402 };
403
404 let on_pointer_enter = move |e: Event<PointerEventData>| {
405 hovering.set(true);
406 if let Some(on_pointer_enter) = &self.on_pointer_enter {
407 on_pointer_enter.call(e);
408 }
409 };
410
411 let on_pointer_leave = move |_| {
412 hovering.set(false);
413 };
414
415 let on_press = move |e: Event<PressEventData>| {
416 let prevent_default = e.get_prevent_default();
417 if let Some(on_press) = &self.on_press {
418 on_press.call(e);
419 }
420 if *prevent_default.borrow() {
421 a11y_id.request_focus();
422 }
423 };
424
425 rect()
426 .a11y_role(AccessibilityRole::MenuItem)
427 .a11y_id(a11y_id)
428 .a11y_focusable(true)
429 .a11y_member_of(group_id)
430 .min_width(Size::px(105.))
431 .width(Size::fill_minimum())
432 .content(Content::fit())
433 .padding(self.padding)
434 .corner_radius(theme.corner_radius)
435 .background(background)
436 .border(border)
437 .color(color)
438 .text_align(TextAlign::Start)
439 .main_align(Alignment::Center)
440 .overflow(Overflow::Clip)
441 .on_pointer_enter(on_pointer_enter)
442 .on_pointer_leave(on_pointer_leave)
443 .on_press(on_press)
444 .children(self.children)
445 }
446
447 fn render_key(&self) -> DiffKey {
448 self.key.clone().or(self.default_key())
449 }
450}
451
452#[derive(Default, Clone, PartialEq)]
465pub struct MenuButton {
466 pub(crate) theme: Option<MenuItemThemePartial>,
467 children: Vec<Element>,
468 on_press: Option<EventHandler<Event<PressEventData>>>,
469 key: DiffKey,
470}
471
472impl ChildrenExt for MenuButton {
473 fn children_mut(&mut self) -> &mut Vec<Element> {
474 &mut self.children
475 }
476}
477
478impl KeyExt for MenuButton {
479 fn key_mut(&mut self) -> &mut DiffKey {
480 &mut self.key
481 }
482}
483
484impl MenuButton {
485 pub fn new() -> Self {
486 Self::default()
487 }
488
489 pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
490 self.on_press = Some(on_press.into());
491 self
492 }
493
494 pub fn theme(mut self, theme: MenuItemThemePartial) -> Self {
496 self.theme = Some(theme);
497 self
498 }
499}
500
501impl ComponentOwned for MenuButton {
502 fn render(self) -> impl IntoElement {
503 let mut menus = use_consume::<State<Vec<MenuId>>>();
504 let parent_menu_id = use_consume::<MenuId>();
505
506 MenuItem::new()
507 .map(self.theme, |el, theme| el.theme(theme))
508 .on_pointer_enter(move |_| close_menus_until(&mut menus, parent_menu_id))
509 .map(self.on_press, |el, on_press| el.on_press(on_press))
510 .children(self.children)
511 }
512
513 fn render_key(&self) -> DiffKey {
514 self.key.clone().or(self.default_key())
515 }
516}
517
518#[derive(Default, Clone, PartialEq)]
531pub struct SubMenu {
532 pub(crate) theme: Option<MenuContainerThemePartial>,
533 label: Option<Element>,
534 items: Vec<Element>,
535 key: DiffKey,
536}
537
538impl KeyExt for SubMenu {
539 fn key_mut(&mut self) -> &mut DiffKey {
540 &mut self.key
541 }
542}
543
544impl SubMenu {
545 pub fn new() -> Self {
546 Self::default()
547 }
548
549 pub fn label(mut self, label: impl IntoElement) -> Self {
550 self.label = Some(label.into_element());
551 self
552 }
553
554 pub fn theme(mut self, theme: MenuContainerThemePartial) -> Self {
556 self.theme = Some(theme);
557 self
558 }
559}
560
561impl ChildrenExt for SubMenu {
562 fn children_mut(&mut self) -> &mut Vec<Element> {
563 &mut self.items
564 }
565}
566
567impl ComponentOwned for SubMenu {
568 fn render(self) -> impl IntoElement {
569 let parent_menu_id = use_consume::<MenuId>();
570 let mut menus = use_consume::<State<Vec<MenuId>>>();
571 let mut menus_ids_generator = use_consume::<State<usize>>();
572
573 let submenu_id = use_hook(|| {
574 *menus_ids_generator.write() += 1;
575 let menu_id = MenuId(*menus_ids_generator.peek());
576 provide_context(menu_id);
577 menu_id
578 });
579
580 let show_submenu = menus.read().contains(&submenu_id);
581
582 let on_pointer_enter = move |_| {
583 close_menus_until(&mut menus, parent_menu_id);
584 push_menu(&mut menus, submenu_id);
585 };
586
587 let on_press = move |_| {
588 close_menus_until(&mut menus, parent_menu_id);
589 push_menu(&mut menus, submenu_id);
590 };
591
592 MenuItem::new()
593 .on_pointer_enter(on_pointer_enter)
594 .on_press(on_press)
595 .child(rect().horizontal().maybe_child(self.label.clone()))
596 .maybe_child(show_submenu.then(|| {
597 rect()
598 .position(Position::new_absolute().top(-8.).right(-10.))
599 .width(Size::px(0.))
600 .height(Size::px(0.))
601 .child(
602 rect().width(Size::window_percent(100.)).child(
603 MenuContainer::new()
604 .map(self.theme, |el, theme| el.theme(theme))
605 .children(self.items),
606 ),
607 )
608 }))
609 }
610
611 fn render_key(&self) -> DiffKey {
612 self.key.clone().or(self.default_key())
613 }
614}
615
616fn overflow_offset(origin: f32, size: f32, window: f32) -> f32 {
619 let overflow = origin + size - window;
620 if overflow > 0.0 {
621 -overflow.min(origin)
622 } else {
623 0.0
624 }
625}
626
627static ROOT_MENU: MenuId = MenuId(0);
628
629#[derive(Clone, Copy, PartialEq, Eq)]
630struct MenuId(usize);
631
632fn close_menus_until(menus: &mut State<Vec<MenuId>>, until: MenuId) {
633 menus.write().retain(|&id| id.0 <= until.0);
634}
635
636fn push_menu(menus: &mut State<Vec<MenuId>>, id: MenuId) {
637 if !menus.read().contains(&id) {
638 menus.write().push(id);
639 }
640}