1use freya_core::prelude::*;
2use torin::{
3 content::Content,
4 prelude::{
5 Alignment,
6 Area,
7 Position,
8 },
9 size::Size,
10};
11
12use crate::{
13 get_theme,
14 theming::component_themes::{
15 MenuContainerThemePartial,
16 MenuItemThemePartial,
17 },
18};
19
20#[cfg_attr(feature = "docs",
70 doc = embed_doc_image::embed_image!("menu", "images/gallery_menu.png"),
71)]
72#[derive(Default, Clone, PartialEq)]
73pub struct Menu {
74 children: Vec<Element>,
75 on_close: Option<EventHandler<()>>,
76 key: DiffKey,
77}
78
79impl ChildrenExt for Menu {
80 fn get_children(&mut self) -> &mut Vec<Element> {
81 &mut self.children
82 }
83}
84
85impl KeyExt for Menu {
86 fn write_key(&mut self) -> &mut DiffKey {
87 &mut self.key
88 }
89}
90
91impl Menu {
92 pub fn new() -> Self {
93 Self::default()
94 }
95
96 pub fn on_close<F>(mut self, f: F) -> Self
97 where
98 F: Into<EventHandler<()>>,
99 {
100 self.on_close = Some(f.into());
101 self
102 }
103}
104
105impl ComponentOwned for Menu {
106 fn render(self) -> impl IntoElement {
107 use_provide_context(|| State::create(ROOT_MENU.0));
109 use_provide_context::<State<Vec<MenuId>>>(|| State::create(vec![ROOT_MENU]));
111 use_provide_context(|| ROOT_MENU);
113
114 rect()
115 .layer(Layer::Overlay)
116 .corner_radius(8.0)
117 .on_press(move |ev: Event<PressEventData>| {
118 ev.stop_propagation();
119 })
120 .on_global_pointer_press(move |_: Event<PointerEventData>| {
121 if let Some(on_close) = &self.on_close {
122 on_close.call(());
123 }
124 })
125 .child(MenuContainer::new().children(self.children))
126 }
127 fn render_key(&self) -> DiffKey {
128 self.key.clone().or(self.default_key())
129 }
130}
131
132#[derive(Default, Clone, PartialEq)]
145pub struct MenuContainer {
146 pub(crate) theme: Option<MenuContainerThemePartial>,
147 children: Vec<Element>,
148 key: DiffKey,
149}
150
151impl KeyExt for MenuContainer {
152 fn write_key(&mut self) -> &mut DiffKey {
153 &mut self.key
154 }
155}
156
157impl ChildrenExt for MenuContainer {
158 fn get_children(&mut self) -> &mut Vec<Element> {
159 &mut self.children
160 }
161}
162
163impl MenuContainer {
164 pub fn new() -> Self {
165 Self::default()
166 }
167}
168
169impl ComponentOwned for MenuContainer {
170 fn render(self) -> impl IntoElement {
171 let focus = use_focus();
172 let theme = get_theme!(self.theme, menu_container);
173 let mut measured = use_state(|| None::<(Area, f32, f32)>);
174
175 use_provide_context(move || MenuGroup {
176 group_id: focus.a11y_id(),
177 });
178
179 let (offset_x, offset_y, opacity) = match *measured.read() {
180 None => (0.0, 0.0, 0.0),
181 Some((area, win_w, win_h)) => (
182 overflow_offset(area.origin.x, area.size.width, win_w),
183 overflow_offset(area.origin.y, area.size.height, win_h),
184 1.0,
185 ),
186 };
187
188 rect()
189 .content(Content::fit())
190 .opacity(opacity)
191 .offset_x(offset_x)
192 .offset_y(offset_y)
193 .on_sized(move |e: Event<SizedEventData>| {
194 if measured.peek().is_none() {
195 let window = Platform::get().root_size.peek();
196 measured.set(Some((e.area, window.width, window.height)));
197 }
198 })
199 .child(
200 rect()
201 .a11y_id(focus.a11y_id())
202 .a11y_member_of(focus.a11y_id())
203 .a11y_focusable(true)
204 .a11y_role(AccessibilityRole::Menu)
205 .shadow((0.0, 4.0, 10.0, 0., theme.shadow))
206 .background(theme.background)
207 .corner_radius(theme.corner_radius)
208 .padding(theme.padding)
209 .border(Border::new().width(1.).fill(theme.border_fill))
210 .content(Content::fit())
211 .children(self.children),
212 )
213 }
214
215 fn render_key(&self) -> DiffKey {
216 self.key.clone().or(self.default_key())
217 }
218}
219
220#[derive(Clone)]
221pub struct MenuGroup {
222 pub group_id: AccessibilityId,
223}
224
225#[derive(Default, Clone, PartialEq)]
240pub struct MenuItem {
241 pub(crate) theme: Option<MenuItemThemePartial>,
242 children: Vec<Element>,
243 on_press: Option<EventHandler<Event<PressEventData>>>,
244 on_pointer_enter: Option<EventHandler<Event<PointerEventData>>>,
245 selected: bool,
246 key: DiffKey,
247}
248
249impl KeyExt for MenuItem {
250 fn write_key(&mut self) -> &mut DiffKey {
251 &mut self.key
252 }
253}
254
255impl MenuItem {
256 pub fn new() -> Self {
257 Self::default()
258 }
259
260 pub fn on_press<F>(mut self, f: F) -> Self
261 where
262 F: Into<EventHandler<Event<PressEventData>>>,
263 {
264 self.on_press = Some(f.into());
265 self
266 }
267
268 pub fn on_pointer_enter<F>(mut self, f: F) -> Self
269 where
270 F: Into<EventHandler<Event<PointerEventData>>>,
271 {
272 self.on_pointer_enter = Some(f.into());
273 self
274 }
275
276 pub fn selected(mut self, selected: bool) -> Self {
277 self.selected = selected;
278 self
279 }
280}
281
282impl ChildrenExt for MenuItem {
283 fn get_children(&mut self) -> &mut Vec<Element> {
284 &mut self.children
285 }
286}
287
288impl ComponentOwned for MenuItem {
289 fn render(self) -> impl IntoElement {
290 let theme = get_theme!(self.theme, menu_item);
291 let mut hovering = use_state(|| false);
292 let focus = use_focus();
293 let focus_status = use_focus_status(focus);
294 let MenuGroup { group_id } = use_consume::<MenuGroup>();
295
296 let background = if self.selected {
297 theme.select_background
298 } else if hovering() {
299 theme.hover_background
300 } else {
301 theme.background
302 };
303
304 let border = if focus_status() == FocusStatus::Keyboard {
305 Border::new()
306 .fill(theme.select_border_fill)
307 .width(2.)
308 .alignment(BorderAlignment::Inner)
309 } else {
310 Border::new()
311 .fill(theme.border_fill)
312 .width(1.)
313 .alignment(BorderAlignment::Inner)
314 };
315
316 let on_pointer_enter = move |e: Event<PointerEventData>| {
317 hovering.set(true);
318 if let Some(on_pointer_enter) = &self.on_pointer_enter {
319 on_pointer_enter.call(e);
320 }
321 };
322
323 let on_pointer_leave = move |_| {
324 hovering.set(false);
325 };
326
327 let on_press = move |e: Event<PressEventData>| {
328 let prevent_default = e.get_prevent_default();
329 if let Some(on_press) = &self.on_press {
330 on_press.call(e);
331 }
332 if *prevent_default.borrow() {
333 focus.request_focus();
334 }
335 };
336
337 rect()
338 .a11y_role(AccessibilityRole::MenuItem)
339 .a11y_id(focus.a11y_id())
340 .a11y_focusable(true)
341 .a11y_member_of(group_id)
342 .min_width(Size::px(105.))
343 .width(Size::fill_minimum())
344 .padding((4.0, 10.0))
345 .corner_radius(theme.corner_radius)
346 .background(background)
347 .border(border)
348 .color(theme.color)
349 .text_align(TextAlign::Start)
350 .main_align(Alignment::Center)
351 .on_pointer_enter(on_pointer_enter)
352 .on_pointer_leave(on_pointer_leave)
353 .on_press(on_press)
354 .children(self.children)
355 }
356
357 fn render_key(&self) -> DiffKey {
358 self.key.clone().or(self.default_key())
359 }
360}
361
362#[derive(Default, Clone, PartialEq)]
375pub struct MenuButton {
376 children: Vec<Element>,
377 on_press: Option<EventHandler<Event<PressEventData>>>,
378 key: DiffKey,
379}
380
381impl ChildrenExt for MenuButton {
382 fn get_children(&mut self) -> &mut Vec<Element> {
383 &mut self.children
384 }
385}
386
387impl KeyExt for MenuButton {
388 fn write_key(&mut self) -> &mut DiffKey {
389 &mut self.key
390 }
391}
392
393impl MenuButton {
394 pub fn new() -> Self {
395 Self::default()
396 }
397
398 pub fn on_press(mut self, on_press: impl Into<EventHandler<Event<PressEventData>>>) -> Self {
399 self.on_press = Some(on_press.into());
400 self
401 }
402}
403
404impl ComponentOwned for MenuButton {
405 fn render(self) -> impl IntoElement {
406 let mut menus = use_consume::<State<Vec<MenuId>>>();
407 let parent_menu_id = use_consume::<MenuId>();
408
409 MenuItem::new()
410 .on_pointer_enter(move |_| close_menus_until(&mut menus, parent_menu_id))
411 .map(self.on_press.clone(), |el, on_press| el.on_press(on_press))
412 .children(self.children)
413 }
414
415 fn render_key(&self) -> DiffKey {
416 self.key.clone().or(self.default_key())
417 }
418}
419
420#[derive(Default, Clone, PartialEq)]
433pub struct SubMenu {
434 label: Option<Element>,
435 items: Vec<Element>,
436 key: DiffKey,
437}
438
439impl KeyExt for SubMenu {
440 fn write_key(&mut self) -> &mut DiffKey {
441 &mut self.key
442 }
443}
444
445impl SubMenu {
446 pub fn new() -> Self {
447 Self::default()
448 }
449
450 pub fn label(mut self, label: impl IntoElement) -> Self {
451 self.label = Some(label.into_element());
452 self
453 }
454}
455
456impl ChildrenExt for SubMenu {
457 fn get_children(&mut self) -> &mut Vec<Element> {
458 &mut self.items
459 }
460}
461
462impl ComponentOwned for SubMenu {
463 fn render(self) -> impl IntoElement {
464 let parent_menu_id = use_consume::<MenuId>();
465 let mut menus = use_consume::<State<Vec<MenuId>>>();
466 let mut menus_ids_generator = use_consume::<State<usize>>();
467
468 let submenu_id = use_hook(|| {
469 *menus_ids_generator.write() += 1;
470 let menu_id = MenuId(*menus_ids_generator.peek());
471 provide_context(menu_id);
472 menu_id
473 });
474
475 let show_submenu = menus.read().contains(&submenu_id);
476
477 let on_pointer_enter = move |_| {
478 close_menus_until(&mut menus, parent_menu_id);
479 push_menu(&mut menus, submenu_id);
480 };
481
482 let on_press = move |_| {
483 close_menus_until(&mut menus, parent_menu_id);
484 push_menu(&mut menus, submenu_id);
485 };
486
487 MenuItem::new()
488 .on_pointer_enter(on_pointer_enter)
489 .on_press(on_press)
490 .child(rect().horizontal().maybe_child(self.label.clone()))
491 .maybe_child(show_submenu.then(|| {
492 rect()
493 .position(Position::new_absolute().top(-8.).right(-10.))
494 .width(Size::px(0.))
495 .height(Size::px(0.))
496 .child(
497 rect()
498 .width(Size::window_percent(100.))
499 .child(MenuContainer::new().children(self.items)),
500 )
501 }))
502 }
503
504 fn render_key(&self) -> DiffKey {
505 self.key.clone().or(self.default_key())
506 }
507}
508
509fn overflow_offset(origin: f32, size: f32, window: f32) -> f32 {
512 let overflow = origin + size - window;
513 if overflow > 0.0 {
514 -overflow.min(origin)
515 } else {
516 0.0
517 }
518}
519
520static ROOT_MENU: MenuId = MenuId(0);
521
522#[derive(Clone, Copy, PartialEq, Eq)]
523struct MenuId(usize);
524
525fn close_menus_until(menus: &mut State<Vec<MenuId>>, until: MenuId) {
526 menus.write().retain(|&id| id.0 <= until.0);
527}
528
529fn push_menu(menus: &mut State<Vec<MenuId>>, id: MenuId) {
530 if !menus.read().contains(&id) {
531 menus.write().push(id);
532 }
533}