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 focus = use_focus();
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 {
235 group_id: focus.a11y_id(),
236 });
237
238 let (offset_x, offset_y, opacity) = match *measured.read() {
239 None => (0.0, 0.0, 0.0),
240 Some((area, win_w, win_h)) => (
241 overflow_offset(area.origin.x, area.size.width, win_w),
242 overflow_offset(area.origin.y, area.size.height, win_h),
243 1.0,
244 ),
245 };
246
247 rect()
248 .layer(Layer::Overlay)
249 .content(Content::fit())
250 .opacity(opacity)
251 .offset_x(offset_x)
252 .offset_y(offset_y)
253 .on_sized(move |e: Event<SizedEventData>| {
254 if measured.peek().is_none() {
255 let window = Platform::get().root_size.peek();
256 measured.set(Some((e.area, window.width, window.height)));
257 }
258 })
259 .child(
260 rect()
261 .a11y_id(focus.a11y_id())
262 .a11y_member_of(focus.a11y_id())
263 .a11y_focusable(true)
264 .a11y_role(AccessibilityRole::Menu)
265 .shadow((0.0, 4.0, 10.0, 0., theme.shadow))
266 .background(theme.background)
267 .corner_radius(theme.corner_radius)
268 .padding(theme.padding)
269 .border(Border::new().width(1.).fill(theme.border_fill))
270 .content(Content::fit())
271 .children(self.children),
272 )
273 }
274
275 fn render_key(&self) -> DiffKey {
276 self.key.clone().or(self.default_key())
277 }
278}
279
280#[derive(Clone)]
281pub struct MenuGroup {
282 pub group_id: AccessibilityId,
283}
284
285#[derive(Clone, PartialEq)]
300pub struct MenuItem {
301 pub(crate) theme: Option<MenuItemThemePartial>,
302 children: Vec<Element>,
303 on_press: Option<EventHandler<Event<PressEventData>>>,
304 on_pointer_enter: Option<EventHandler<Event<PointerEventData>>>,
305 selected: bool,
306 padding: Gaps,
307 key: DiffKey,
308}
309
310impl Default for MenuItem {
311 fn default() -> Self {
312 Self {
313 theme: None,
314 children: Vec::new(),
315 on_press: None,
316 on_pointer_enter: None,
317 selected: false,
318 padding: (6.0, 12.0).into(),
319 key: DiffKey::None,
320 }
321 }
322}
323
324impl KeyExt for MenuItem {
325 fn write_key(&mut self) -> &mut DiffKey {
326 &mut self.key
327 }
328}
329
330impl MenuItem {
331 pub fn new() -> Self {
332 Self::default()
333 }
334
335 pub fn on_press<F>(mut self, f: F) -> Self
336 where
337 F: Into<EventHandler<Event<PressEventData>>>,
338 {
339 self.on_press = Some(f.into());
340 self
341 }
342
343 pub fn on_pointer_enter<F>(mut self, f: F) -> Self
344 where
345 F: Into<EventHandler<Event<PointerEventData>>>,
346 {
347 self.on_pointer_enter = Some(f.into());
348 self
349 }
350
351 pub fn selected(mut self, selected: bool) -> Self {
352 self.selected = selected;
353 self
354 }
355
356 pub fn padding(mut self, padding: impl Into<Gaps>) -> Self {
358 self.padding = padding.into();
359 self
360 }
361
362 pub fn get_padding(&self) -> Gaps {
364 self.padding
365 }
366
367 pub fn get_theme(&self) -> Option<&MenuItemThemePartial> {
369 self.theme.as_ref()
370 }
371
372 pub fn theme(mut self, theme: MenuItemThemePartial) -> Self {
374 self.theme = Some(theme);
375 self
376 }
377}
378
379impl ChildrenExt for MenuItem {
380 fn get_children(&mut self) -> &mut Vec<Element> {
381 &mut self.children
382 }
383}
384
385impl ComponentOwned for MenuItem {
386 fn render(self) -> impl IntoElement {
387 let theme = get_theme!(self.theme, MenuItemThemePreference, "menu_item");
388 let mut hovering = use_state(|| false);
389 let focus = use_focus();
390 let focus_status = use_focus_status(focus);
391 let MenuGroup { group_id } = use_consume::<MenuGroup>();
392
393 let background = if self.selected {
394 theme.select_background
395 } else if hovering() {
396 theme.hover_background
397 } else {
398 theme.background
399 };
400
401 let border = if focus_status() == FocusStatus::Keyboard {
402 Border::new()
403 .fill(theme.select_border_fill)
404 .width(2.)
405 .alignment(BorderAlignment::Inner)
406 } else {
407 Border::new()
408 .fill(theme.border_fill)
409 .width(1.)
410 .alignment(BorderAlignment::Inner)
411 };
412
413 let on_pointer_enter = move |e: Event<PointerEventData>| {
414 hovering.set(true);
415 if let Some(on_pointer_enter) = &self.on_pointer_enter {
416 on_pointer_enter.call(e);
417 }
418 };
419
420 let on_pointer_leave = move |_| {
421 hovering.set(false);
422 };
423
424 let on_press = move |e: Event<PressEventData>| {
425 let prevent_default = e.get_prevent_default();
426 if let Some(on_press) = &self.on_press {
427 on_press.call(e);
428 }
429 if *prevent_default.borrow() {
430 focus.request_focus();
431 }
432 };
433
434 rect()
435 .a11y_role(AccessibilityRole::MenuItem)
436 .a11y_id(focus.a11y_id())
437 .a11y_focusable(true)
438 .a11y_member_of(group_id)
439 .min_width(Size::px(105.))
440 .width(Size::fill_minimum())
441 .content(Content::fit())
442 .padding(self.padding)
443 .corner_radius(theme.corner_radius)
444 .background(background)
445 .border(border)
446 .color(theme.color)
447 .text_align(TextAlign::Start)
448 .main_align(Alignment::Center)
449 .overflow(Overflow::Clip)
450 .on_pointer_enter(on_pointer_enter)
451 .on_pointer_leave(on_pointer_leave)
452 .on_press(on_press)
453 .children(self.children)
454 }
455
456 fn render_key(&self) -> DiffKey {
457 self.key.clone().or(self.default_key())
458 }
459}
460
461#[derive(Default, Clone, PartialEq)]
474pub struct MenuButton {
475 pub(crate) theme: Option<MenuItemThemePartial>,
476 children: Vec<Element>,
477 on_press: Option<EventHandler<Event<PressEventData>>>,
478 key: DiffKey,
479}
480
481impl ChildrenExt for MenuButton {
482 fn get_children(&mut self) -> &mut Vec<Element> {
483 &mut self.children
484 }
485}
486
487impl KeyExt for MenuButton {
488 fn write_key(&mut self) -> &mut DiffKey {
489 &mut self.key
490 }
491}
492
493impl MenuButton {
494 pub fn new() -> Self {
495 Self::default()
496 }
497
498 pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
499 self.on_press = Some(on_press.into());
500 self
501 }
502
503 pub fn theme(mut self, theme: MenuItemThemePartial) -> Self {
505 self.theme = Some(theme);
506 self
507 }
508}
509
510impl ComponentOwned for MenuButton {
511 fn render(self) -> impl IntoElement {
512 let mut menus = use_consume::<State<Vec<MenuId>>>();
513 let parent_menu_id = use_consume::<MenuId>();
514
515 MenuItem::new()
516 .map(self.theme, |el, theme| el.theme(theme))
517 .on_pointer_enter(move |_| close_menus_until(&mut menus, parent_menu_id))
518 .map(self.on_press, |el, on_press| el.on_press(on_press))
519 .children(self.children)
520 }
521
522 fn render_key(&self) -> DiffKey {
523 self.key.clone().or(self.default_key())
524 }
525}
526
527#[derive(Default, Clone, PartialEq)]
540pub struct SubMenu {
541 pub(crate) theme: Option<MenuContainerThemePartial>,
542 label: Option<Element>,
543 items: Vec<Element>,
544 key: DiffKey,
545}
546
547impl KeyExt for SubMenu {
548 fn write_key(&mut self) -> &mut DiffKey {
549 &mut self.key
550 }
551}
552
553impl SubMenu {
554 pub fn new() -> Self {
555 Self::default()
556 }
557
558 pub fn label(mut self, label: impl IntoElement) -> Self {
559 self.label = Some(label.into_element());
560 self
561 }
562
563 pub fn theme(mut self, theme: MenuContainerThemePartial) -> Self {
565 self.theme = Some(theme);
566 self
567 }
568}
569
570impl ChildrenExt for SubMenu {
571 fn get_children(&mut self) -> &mut Vec<Element> {
572 &mut self.items
573 }
574}
575
576impl ComponentOwned for SubMenu {
577 fn render(self) -> impl IntoElement {
578 let parent_menu_id = use_consume::<MenuId>();
579 let mut menus = use_consume::<State<Vec<MenuId>>>();
580 let mut menus_ids_generator = use_consume::<State<usize>>();
581
582 let submenu_id = use_hook(|| {
583 *menus_ids_generator.write() += 1;
584 let menu_id = MenuId(*menus_ids_generator.peek());
585 provide_context(menu_id);
586 menu_id
587 });
588
589 let show_submenu = menus.read().contains(&submenu_id);
590
591 let on_pointer_enter = move |_| {
592 close_menus_until(&mut menus, parent_menu_id);
593 push_menu(&mut menus, submenu_id);
594 };
595
596 let on_press = move |_| {
597 close_menus_until(&mut menus, parent_menu_id);
598 push_menu(&mut menus, submenu_id);
599 };
600
601 MenuItem::new()
602 .on_pointer_enter(on_pointer_enter)
603 .on_press(on_press)
604 .child(rect().horizontal().maybe_child(self.label.clone()))
605 .maybe_child(show_submenu.then(|| {
606 rect()
607 .position(Position::new_absolute().top(-8.).right(-10.))
608 .width(Size::px(0.))
609 .height(Size::px(0.))
610 .child(
611 rect().width(Size::window_percent(100.)).child(
612 MenuContainer::new()
613 .map(self.theme, |el, theme| el.theme(theme))
614 .children(self.items),
615 ),
616 )
617 }))
618 }
619
620 fn render_key(&self) -> DiffKey {
621 self.key.clone().or(self.default_key())
622 }
623}
624
625fn overflow_offset(origin: f32, size: f32, window: f32) -> f32 {
628 let overflow = origin + size - window;
629 if overflow > 0.0 {
630 -overflow.min(origin)
631 } else {
632 0.0
633 }
634}
635
636static ROOT_MENU: MenuId = MenuId(0);
637
638#[derive(Clone, Copy, PartialEq, Eq)]
639struct MenuId(usize);
640
641fn close_menus_until(menus: &mut State<Vec<MenuId>>, until: MenuId) {
642 menus.write().retain(|&id| id.0 <= until.0);
643}
644
645fn push_menu(menus: &mut State<Vec<MenuId>>, id: MenuId) {
646 if !menus.read().contains(&id) {
647 menus.write().push(id);
648 }
649}