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 %[component]
20 pub MenuContainer {
21 %[fields]
22 background: Color,
23 padding: Gaps,
24 shadow: Color,
25 border_fill: Color,
26 corner_radius: CornerRadius,
27 }
28}
29
30define_theme! {
31 %[component]
32 pub MenuItem {
33 %[fields]
34 background: Color,
35 hover_background: Color,
36 select_background: Color,
37 border_fill: Color,
38 select_border_fill: Color,
39 corner_radius: CornerRadius,
40 color: Color,
41 }
42}
43
44#[cfg_attr(feature = "docs",
94 doc = embed_doc_image::embed_image!("menu", "images/gallery_menu.png"),
95)]
96#[derive(Default, Clone, PartialEq)]
97pub struct Menu {
98 children: Vec<Element>,
99 on_close: Option<EventHandler<()>>,
100 key: DiffKey,
101}
102
103impl ChildrenExt for Menu {
104 fn get_children(&mut self) -> &mut Vec<Element> {
105 &mut self.children
106 }
107}
108
109impl KeyExt for Menu {
110 fn write_key(&mut self) -> &mut DiffKey {
111 &mut self.key
112 }
113}
114
115impl Menu {
116 pub fn new() -> Self {
117 Self::default()
118 }
119
120 pub fn on_close<F>(mut self, f: F) -> Self
121 where
122 F: Into<EventHandler<()>>,
123 {
124 self.on_close = Some(f.into());
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 use_provide_context::<State<Vec<MenuId>>>(|| State::create(vec![ROOT_MENU]));
135 use_provide_context(|| ROOT_MENU);
137
138 rect()
139 .layer(Layer::Overlay)
140 .corner_radius(8.0)
141 .on_press(move |ev: Event<PressEventData>| {
142 ev.stop_propagation();
143 })
144 .on_global_pointer_press(move |_: Event<PointerEventData>| {
145 if let Some(on_close) = &self.on_close {
146 on_close.call(());
147 }
148 })
149 .child(MenuContainer::new().children(self.children))
150 }
151 fn render_key(&self) -> DiffKey {
152 self.key.clone().or(self.default_key())
153 }
154}
155
156#[derive(Default, Clone, PartialEq)]
169pub struct MenuContainer {
170 pub(crate) theme: Option<MenuContainerThemePartial>,
171 children: Vec<Element>,
172 key: DiffKey,
173}
174
175impl KeyExt for MenuContainer {
176 fn write_key(&mut self) -> &mut DiffKey {
177 &mut self.key
178 }
179}
180
181impl ChildrenExt for MenuContainer {
182 fn get_children(&mut self) -> &mut Vec<Element> {
183 &mut self.children
184 }
185}
186
187impl MenuContainer {
188 pub fn new() -> Self {
189 Self::default()
190 }
191}
192
193impl ComponentOwned for MenuContainer {
194 fn render(self) -> impl IntoElement {
195 let focus = use_focus();
196 let theme = get_theme!(self.theme, MenuContainerThemePreference, "menu_container");
197 let mut measured = use_state(|| None::<(Area, f32, f32)>);
198
199 use_provide_context(move || MenuGroup {
200 group_id: focus.a11y_id(),
201 });
202
203 let (offset_x, offset_y, opacity) = match *measured.read() {
204 None => (0.0, 0.0, 0.0),
205 Some((area, win_w, win_h)) => (
206 overflow_offset(area.origin.x, area.size.width, win_w),
207 overflow_offset(area.origin.y, area.size.height, win_h),
208 1.0,
209 ),
210 };
211
212 rect()
213 .layer(Layer::Overlay)
214 .content(Content::fit())
215 .opacity(opacity)
216 .offset_x(offset_x)
217 .offset_y(offset_y)
218 .on_sized(move |e: Event<SizedEventData>| {
219 if measured.peek().is_none() {
220 let window = Platform::get().root_size.peek();
221 measured.set(Some((e.area, window.width, window.height)));
222 }
223 })
224 .child(
225 rect()
226 .a11y_id(focus.a11y_id())
227 .a11y_member_of(focus.a11y_id())
228 .a11y_focusable(true)
229 .a11y_role(AccessibilityRole::Menu)
230 .shadow((0.0, 4.0, 10.0, 0., theme.shadow))
231 .background(theme.background)
232 .corner_radius(theme.corner_radius)
233 .padding(theme.padding)
234 .border(Border::new().width(1.).fill(theme.border_fill))
235 .content(Content::fit())
236 .children(self.children),
237 )
238 }
239
240 fn render_key(&self) -> DiffKey {
241 self.key.clone().or(self.default_key())
242 }
243}
244
245#[derive(Clone)]
246pub struct MenuGroup {
247 pub group_id: AccessibilityId,
248}
249
250#[derive(Clone, PartialEq)]
265pub struct MenuItem {
266 pub(crate) theme: Option<MenuItemThemePartial>,
267 children: Vec<Element>,
268 on_press: Option<EventHandler<Event<PressEventData>>>,
269 on_pointer_enter: Option<EventHandler<Event<PointerEventData>>>,
270 selected: bool,
271 padding: Gaps,
272 key: DiffKey,
273}
274
275impl Default for MenuItem {
276 fn default() -> Self {
277 Self {
278 theme: None,
279 children: Vec::new(),
280 on_press: None,
281 on_pointer_enter: None,
282 selected: false,
283 padding: (6.0, 12.0).into(),
284 key: DiffKey::None,
285 }
286 }
287}
288
289impl KeyExt for MenuItem {
290 fn write_key(&mut self) -> &mut DiffKey {
291 &mut self.key
292 }
293}
294
295impl MenuItem {
296 pub fn new() -> Self {
297 Self::default()
298 }
299
300 pub fn on_press<F>(mut self, f: F) -> Self
301 where
302 F: Into<EventHandler<Event<PressEventData>>>,
303 {
304 self.on_press = Some(f.into());
305 self
306 }
307
308 pub fn on_pointer_enter<F>(mut self, f: F) -> Self
309 where
310 F: Into<EventHandler<Event<PointerEventData>>>,
311 {
312 self.on_pointer_enter = Some(f.into());
313 self
314 }
315
316 pub fn selected(mut self, selected: bool) -> Self {
317 self.selected = selected;
318 self
319 }
320
321 pub fn padding(mut self, padding: impl Into<Gaps>) -> Self {
323 self.padding = padding.into();
324 self
325 }
326
327 pub fn get_padding(&self) -> Gaps {
329 self.padding
330 }
331
332 pub fn get_theme(&self) -> Option<&MenuItemThemePartial> {
334 self.theme.as_ref()
335 }
336
337 pub fn theme(mut self, theme: MenuItemThemePartial) -> Self {
339 self.theme = Some(theme);
340 self
341 }
342}
343
344impl ChildrenExt for MenuItem {
345 fn get_children(&mut self) -> &mut Vec<Element> {
346 &mut self.children
347 }
348}
349
350impl ComponentOwned for MenuItem {
351 fn render(self) -> impl IntoElement {
352 let theme = get_theme!(self.theme, MenuItemThemePreference, "menu_item");
353 let mut hovering = use_state(|| false);
354 let focus = use_focus();
355 let focus_status = use_focus_status(focus);
356 let MenuGroup { group_id } = use_consume::<MenuGroup>();
357
358 let background = if self.selected {
359 theme.select_background
360 } else if hovering() {
361 theme.hover_background
362 } else {
363 theme.background
364 };
365
366 let border = if focus_status() == FocusStatus::Keyboard {
367 Border::new()
368 .fill(theme.select_border_fill)
369 .width(2.)
370 .alignment(BorderAlignment::Inner)
371 } else {
372 Border::new()
373 .fill(theme.border_fill)
374 .width(1.)
375 .alignment(BorderAlignment::Inner)
376 };
377
378 let on_pointer_enter = move |e: Event<PointerEventData>| {
379 hovering.set(true);
380 if let Some(on_pointer_enter) = &self.on_pointer_enter {
381 on_pointer_enter.call(e);
382 }
383 };
384
385 let on_pointer_leave = move |_| {
386 hovering.set(false);
387 };
388
389 let on_press = move |e: Event<PressEventData>| {
390 let prevent_default = e.get_prevent_default();
391 if let Some(on_press) = &self.on_press {
392 on_press.call(e);
393 }
394 if *prevent_default.borrow() {
395 focus.request_focus();
396 }
397 };
398
399 rect()
400 .a11y_role(AccessibilityRole::MenuItem)
401 .a11y_id(focus.a11y_id())
402 .a11y_focusable(true)
403 .a11y_member_of(group_id)
404 .min_width(Size::px(105.))
405 .width(Size::fill_minimum())
406 .content(Content::fit())
407 .padding(self.padding)
408 .corner_radius(theme.corner_radius)
409 .background(background)
410 .border(border)
411 .color(theme.color)
412 .text_align(TextAlign::Start)
413 .main_align(Alignment::Center)
414 .overflow(Overflow::Clip)
415 .on_pointer_enter(on_pointer_enter)
416 .on_pointer_leave(on_pointer_leave)
417 .on_press(on_press)
418 .children(self.children)
419 }
420
421 fn render_key(&self) -> DiffKey {
422 self.key.clone().or(self.default_key())
423 }
424}
425
426#[derive(Default, Clone, PartialEq)]
439pub struct MenuButton {
440 children: Vec<Element>,
441 on_press: Option<EventHandler<Event<PressEventData>>>,
442 key: DiffKey,
443}
444
445impl ChildrenExt for MenuButton {
446 fn get_children(&mut self) -> &mut Vec<Element> {
447 &mut self.children
448 }
449}
450
451impl KeyExt for MenuButton {
452 fn write_key(&mut self) -> &mut DiffKey {
453 &mut self.key
454 }
455}
456
457impl MenuButton {
458 pub fn new() -> Self {
459 Self::default()
460 }
461
462 pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
463 self.on_press = Some(on_press.into());
464 self
465 }
466}
467
468impl ComponentOwned for MenuButton {
469 fn render(self) -> impl IntoElement {
470 let mut menus = use_consume::<State<Vec<MenuId>>>();
471 let parent_menu_id = use_consume::<MenuId>();
472
473 MenuItem::new()
474 .on_pointer_enter(move |_| close_menus_until(&mut menus, parent_menu_id))
475 .map(self.on_press.clone(), |el, on_press| el.on_press(on_press))
476 .children(self.children)
477 }
478
479 fn render_key(&self) -> DiffKey {
480 self.key.clone().or(self.default_key())
481 }
482}
483
484#[derive(Default, Clone, PartialEq)]
497pub struct SubMenu {
498 label: Option<Element>,
499 items: Vec<Element>,
500 key: DiffKey,
501}
502
503impl KeyExt for SubMenu {
504 fn write_key(&mut self) -> &mut DiffKey {
505 &mut self.key
506 }
507}
508
509impl SubMenu {
510 pub fn new() -> Self {
511 Self::default()
512 }
513
514 pub fn label(mut self, label: impl IntoElement) -> Self {
515 self.label = Some(label.into_element());
516 self
517 }
518}
519
520impl ChildrenExt for SubMenu {
521 fn get_children(&mut self) -> &mut Vec<Element> {
522 &mut self.items
523 }
524}
525
526impl ComponentOwned for SubMenu {
527 fn render(self) -> impl IntoElement {
528 let parent_menu_id = use_consume::<MenuId>();
529 let mut menus = use_consume::<State<Vec<MenuId>>>();
530 let mut menus_ids_generator = use_consume::<State<usize>>();
531
532 let submenu_id = use_hook(|| {
533 *menus_ids_generator.write() += 1;
534 let menu_id = MenuId(*menus_ids_generator.peek());
535 provide_context(menu_id);
536 menu_id
537 });
538
539 let show_submenu = menus.read().contains(&submenu_id);
540
541 let on_pointer_enter = move |_| {
542 close_menus_until(&mut menus, parent_menu_id);
543 push_menu(&mut menus, submenu_id);
544 };
545
546 let on_press = move |_| {
547 close_menus_until(&mut menus, parent_menu_id);
548 push_menu(&mut menus, submenu_id);
549 };
550
551 MenuItem::new()
552 .on_pointer_enter(on_pointer_enter)
553 .on_press(on_press)
554 .child(rect().horizontal().maybe_child(self.label.clone()))
555 .maybe_child(show_submenu.then(|| {
556 rect()
557 .position(Position::new_absolute().top(-8.).right(-10.))
558 .width(Size::px(0.))
559 .height(Size::px(0.))
560 .child(
561 rect()
562 .width(Size::window_percent(100.))
563 .child(MenuContainer::new().children(self.items)),
564 )
565 }))
566 }
567
568 fn render_key(&self) -> DiffKey {
569 self.key.clone().or(self.default_key())
570 }
571}
572
573fn overflow_offset(origin: f32, size: f32, window: f32) -> f32 {
576 let overflow = origin + size - window;
577 if overflow > 0.0 {
578 -overflow.min(origin)
579 } else {
580 0.0
581 }
582}
583
584static ROOT_MENU: MenuId = MenuId(0);
585
586#[derive(Clone, Copy, PartialEq, Eq)]
587struct MenuId(usize);
588
589fn close_menus_until(menus: &mut State<Vec<MenuId>>, until: MenuId) {
590 menus.write().retain(|&id| id.0 <= until.0);
591}
592
593fn push_menu(menus: &mut State<Vec<MenuId>>, id: MenuId) {
594 if !menus.read().contains(&id) {
595 menus.write().push(id);
596 }
597}