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