1use crate::modifiers::ModalEvent;
2use crate::style::{Abilities, Display};
3use crate::{icons::ICON_CHEVRON_RIGHT, prelude::*};
4
5fn first_focusable_descendant(tree: &Tree<Entity>, style: &Style, root: Entity) -> Option<Entity> {
6 vizia_storage::TreeIterator::subtree(tree, root).skip(1).find(|node| {
7 if style.display.get(*node).copied().unwrap_or_default() == Display::None {
8 return false;
9 }
10 if style.disabled.get(*node).copied().unwrap_or_default() {
11 return false;
12 }
13 style
14 .abilities
15 .get(*node)
16 .map(|abilities| abilities.contains(Abilities::FOCUSABLE))
17 .unwrap_or(false)
18 })
19}
20
21fn is_focusable_item(cx: &EventContext, entity: Entity) -> bool {
22 if cx.style.display.get(entity).copied().unwrap_or_default() == Display::None {
23 return false;
24 }
25
26 if cx.style.disabled.get(entity).copied().unwrap_or_default() {
27 return false;
28 }
29
30 cx.style
31 .abilities
32 .get(entity)
33 .map(|abilities| abilities.contains(Abilities::FOCUSABLE))
34 .unwrap_or(false)
35}
36
37fn first_focusable_child(cx: &EventContext, root: Entity) -> Option<Entity> {
38 let mut child = cx.tree.get_first_child(root);
39 while let Some(entity) = child {
40 if is_focusable_item(cx, entity) {
41 return Some(entity);
42 }
43 child = cx.tree.get_next_sibling(entity);
44 }
45
46 None
47}
48
49fn first_menu_bar_item(cx: &EventContext, root: Entity) -> Option<Entity> {
50 first_focusable_child(cx, root)
51}
52
53pub struct MenuBar {
55 is_open: Signal<bool>,
56 focused_item: Signal<Option<Entity>>,
57}
58
59impl MenuBar {
60 pub fn new(cx: &mut Context, content: impl Fn(&mut Context)) -> Handle<Self> {
62 let is_open = Signal::new(false);
63 let focused_item = Signal::new(None);
64
65 Self { is_open, focused_item }
66 .build(cx, |cx| {
67 cx.add_listener(move |menu_bar: &mut Self, cx, event| {
68 let flag = menu_bar.is_open.get();
69 event.map(
70 |window_event, meta: &mut crate::events::EventMeta| match window_event {
71 WindowEvent::MouseDown(_) => {
72 if flag && meta.origin != cx.current() {
73 if !cx.hovered.is_descendant_of(cx.tree, cx.current) {
75 cx.emit(MenuEvent::CloseAll);
76 }
77 }
78 }
79
80 _ => {}
81 },
82 );
83 });
84
85 (content)(cx);
86 })
87 .layout_type(LayoutType::Row)
88 .role(Role::MenuBar)
89 .orientation(Orientation::Horizontal)
90 .navigable(true)
91 }
92}
93
94impl View for MenuBar {
95 fn element(&self) -> Option<&'static str> {
96 Some("menubar")
97 }
98
99 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
100 event.map(|window_event, meta| match window_event {
101 WindowEvent::FocusIn => {
102 if meta.target == cx.current() {
103 if let Some(first_item) = first_menu_bar_item(cx, cx.current()) {
104 focus_entity(cx, first_item);
105 self.focused_item.set(Some(first_item));
106 meta.consume();
107 }
108 }
109 }
110
111 WindowEvent::KeyDown(Code::ArrowLeft, _) => {
112 if !self.is_open.get() {
113 cx.emit(MenuEvent::FocusPrevMenuBarItem);
114
115 meta.consume();
116 }
117 }
118
119 WindowEvent::KeyDown(Code::ArrowRight, _) => {
120 if !self.is_open.get() {
121 cx.emit(MenuEvent::FocusNextMenuBarItem);
122 meta.consume();
123 }
124 }
125 _ => {}
126 });
127
128 event.map(|menu_event, _| match menu_event {
129 MenuEvent::MenuIsOpen => {
130 self.is_open.set_if_changed(true);
131 }
132
133 MenuEvent::CloseAll => {
134 self.is_open.set_if_changed(false);
135 cx.emit_custom(
136 Event::new(MenuEvent::Close).target(cx.current).propagate(Propagation::Subtree),
137 );
138 }
139
140 MenuEvent::FocusPrevMenuBarItem => {
141 if let Some(current) = self.focused_item.get() {
142 if let Some(next) = prev_sibling_wrapped(cx, current) {
143 focus_entity(cx, next);
144 if self.is_open.get() {
145 cx.emit_custom(
146 Event::new(MenuEvent::Close)
147 .target(current)
148 .propagate(Propagation::Subtree),
149 );
150 cx.emit_custom(
151 Event::new(MenuEvent::TriggerArrowDown)
152 .target(next)
153 .propagate(Propagation::Direct),
154 );
155 }
156 self.focused_item.set(Some(next));
157 }
158 }
159 }
160
161 MenuEvent::FocusNextMenuBarItem => {
162 if let Some(current) = self.focused_item.get() {
163 if let Some(next) = next_sibling_wrapped(cx, current) {
164 focus_entity(cx, next);
165 if self.is_open.get() {
166 cx.emit_custom(
167 Event::new(MenuEvent::Close)
168 .target(current)
169 .propagate(Propagation::Subtree),
170 );
171 cx.emit_custom(
172 Event::new(MenuEvent::TriggerArrowDown)
173 .target(next)
174 .propagate(Propagation::Direct),
175 );
176 }
177 self.focused_item.set(Some(next));
178 }
179 }
180 }
181
182 _ => {}
183 });
184 }
185}
186
187pub enum MenuEvent {
189 ToggleOpen,
191 Open,
193 Close,
195 CloseAndFocusTrigger,
197 CloseAll,
199 MenuIsOpen,
201 FocusNext,
203 FocusPrev,
205 FocusFirst,
207 FocusLast,
209 TriggerArrowDown,
211 TriggerArrowRight,
213 TriggerArrowLeft,
215 FocusNextMenuBarItem,
217 FocusPrevMenuBarItem,
219}
220
221fn focus_entity(cx: &mut EventContext, entity: Entity) {
222 cx.with_current(entity, |cx| cx.focus());
223}
224
225fn next_sibling_wrapped(cx: &EventContext, entity: Entity) -> Option<Entity> {
226 let parent = cx.tree.get_parent(entity)?;
227 let mut next = cx.tree.get_next_sibling(entity).or_else(|| cx.tree.get_first_child(parent));
228
229 while let Some(candidate) = next {
230 if candidate == entity {
231 break;
232 }
233
234 if is_focusable_item(cx, candidate) {
235 return Some(candidate);
236 }
237
238 next = cx.tree.get_next_sibling(candidate).or_else(|| cx.tree.get_first_child(parent));
239 }
240
241 None
242}
243
244fn prev_sibling_wrapped(cx: &EventContext, entity: Entity) -> Option<Entity> {
245 let parent = cx.tree.get_parent(entity)?;
246 let mut prev =
247 cx.tree.get_prev_sibling(entity).or_else(|| cx.tree.get_last_child(parent).copied());
248
249 while let Some(candidate) = prev {
250 if candidate == entity {
251 break;
252 }
253
254 if is_focusable_item(cx, candidate) {
255 return Some(candidate);
256 }
257
258 prev =
259 cx.tree.get_prev_sibling(candidate).or_else(|| cx.tree.get_last_child(parent).copied());
260 }
261
262 None
263}
264
265fn focus_next_sibling_wrapped(cx: &mut EventContext) -> bool {
266 let current = cx.current();
267 if let Some(next) = next_sibling_wrapped(cx, current) {
268 focus_entity(cx, next);
269 return true;
270 }
271
272 false
273}
274
275fn focus_prev_sibling_wrapped(cx: &mut EventContext) -> bool {
276 let current = cx.current();
277 if let Some(prev) = prev_sibling_wrapped(cx, current) {
278 focus_entity(cx, prev);
279 return true;
280 }
281
282 false
283}
284
285fn focus_first_sibling(cx: &mut EventContext) -> bool {
286 let current = cx.current();
287 let Some(parent) = cx.tree.get_parent(current) else {
288 return false;
289 };
290
291 if let Some(first) = cx.tree.get_first_child(parent) {
292 focus_entity(cx, first);
293 return true;
294 }
295
296 false
297}
298
299fn focus_last_sibling(cx: &mut EventContext) -> bool {
300 let current = cx.current();
301 let Some(parent) = cx.tree.get_parent(current) else {
302 return false;
303 };
304
305 if let Some(last) = cx.tree.get_last_child(parent).copied() {
306 focus_entity(cx, last);
307 return true;
308 }
309
310 false
311}
312
313pub struct Menu {}
315
316impl Menu {
317 pub fn new(
319 cx: &mut Context,
320 placement: impl Res<Placement> + 'static,
321 focus_on_open: impl Res<bool> + 'static,
322 content: impl Fn(&mut Context),
323 ) -> Handle<'_, Popover> {
324 let focus_on_open = focus_on_open.to_signal(cx);
325
326 Popover::new(cx, move |cx| {
327 let popup = cx.current();
328
329 Keymap::from(vec![
331 (
332 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
333 KeymapEntry::new("Focus Next", |cx| cx.emit(MenuEvent::FocusNext)),
334 ),
335 (
336 KeyChord::new(Modifiers::empty(), Code::ArrowUp),
337 KeymapEntry::new("Focus Prev", |cx| cx.emit(MenuEvent::FocusPrev)),
338 ),
339 (
340 KeyChord::new(Modifiers::empty(), Code::Home),
341 KeymapEntry::new("Focus First", |cx| cx.emit(MenuEvent::FocusFirst)),
342 ),
343 (
344 KeyChord::new(Modifiers::empty(), Code::End),
345 KeymapEntry::new("Focus Last", |cx| cx.emit(MenuEvent::FocusLast)),
346 ),
347 (
348 KeyChord::new(Modifiers::empty(), Code::Escape),
349 KeymapEntry::new("Close Active Menu", |cx| {
350 cx.emit(MenuEvent::CloseAndFocusTrigger)
351 }),
352 ),
353 (
354 KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
355 KeymapEntry::new("Close", |cx| cx.emit(MenuEvent::TriggerArrowLeft)),
356 ),
357 (
358 KeyChord::new(Modifiers::empty(), Code::Tab),
359 KeymapEntry::new("Close All", |cx| cx.emit(MenuEvent::CloseAll)),
360 ),
361 ])
362 .build(cx);
363
364 (content)(cx);
365
366 if focus_on_open.get() {
367 if let Some(first_item) = first_focusable_descendant(&cx.tree, &cx.style, popup) {
368 cx.with_current(first_item, |cx| cx.focus());
369 }
370 }
371 })
372 .role(Role::Menu)
373 .lock_focus_to_within()
374 .placement(placement)
375 .arrow_size(Pixels(0.0))
376 }
377}
378
379impl View for Menu {
380 fn element(&self) -> Option<&'static str> {
381 Some("menu")
382 }
383}
384
385pub struct Submenu {
387 is_open: Signal<bool>,
388 focus_on_open: Signal<bool>,
389 open_on_hover: bool,
390 is_submenu: bool,
391}
392
393impl Submenu {
394 pub fn new<V: View>(
396 cx: &mut Context,
397 content: impl Fn(&mut Context) -> Handle<V> + 'static,
398 menu: impl Fn(&mut Context) + 'static,
399 ) -> Handle<Self> {
400 let is_submenu = cx.try_data::<Submenu>().is_some();
401 let is_menu_bar_item = cx.try_data::<MenuBar>().is_some();
402
403 let is_open = Signal::new(false);
404 let focus_on_open = Signal::new(false);
405 let submenu_popup_placement =
406 if is_submenu { Placement::RightStart } else { Placement::BottomStart };
407
408 let handle = Self { is_open, focus_on_open, open_on_hover: is_submenu, is_submenu }
409 .build(cx, |cx| {
410 cx.add_listener(move |menu_button: &mut Self, cx, event| {
411 let flag = menu_button.is_open.get();
412 event.map(
413 |window_event, meta: &mut crate::events::EventMeta| match window_event {
414 WindowEvent::MouseDown(_) => {
415 if flag && meta.origin != cx.current() {
416 if !cx.hovered.is_descendant_of(cx.tree, cx.current) {
418 cx.emit(MenuEvent::CloseAll);
419 cx.emit(MenuEvent::Close);
420 }
421 }
422 }
423
424 _ => {}
425 },
426 );
427 });
428
429 Keymap::from(vec![
431 (
432 KeyChord::new(Modifiers::empty(), Code::ArrowDown),
433 KeymapEntry::new("Open Submenu", |cx| cx.emit(MenuEvent::TriggerArrowDown)),
434 ),
435 (
436 KeyChord::new(Modifiers::empty(), Code::Space),
437 KeymapEntry::new("Open Submenu", |cx| cx.emit(MenuEvent::TriggerArrowDown)),
438 ),
439 (
440 KeyChord::new(Modifiers::empty(), Code::Enter),
441 KeymapEntry::new("Open Submenu", |cx| cx.emit(MenuEvent::TriggerArrowDown)),
442 ),
443 (
444 KeyChord::new(Modifiers::empty(), Code::ArrowRight),
445 KeymapEntry::new("Navigate Right", |cx| {
446 cx.emit(MenuEvent::TriggerArrowRight)
447 }),
448 ),
449 ])
450 .build(cx);
451
452 (content)(cx).hoverable(false);
453 Svg::new(cx, ICON_CHEVRON_RIGHT).class("arrow").hoverable(false);
454
455 Binding::new(cx, is_open, move |cx| {
456 let open = is_open.get();
457 if open {
458 Menu::new(cx, submenu_popup_placement, focus_on_open, |cx| (menu)(cx))
459 .checked(is_open)
460 .on_hover(|cx| {
461 cx.emit_custom(
462 Event::new(MenuEvent::Close)
463 .target(cx.current)
464 .propagate(Propagation::Subtree),
465 )
466 });
467 }
468 });
469 })
470 .focusable(true)
471 .navigable(!is_menu_bar_item && !is_submenu)
472 .role(Role::MenuItem)
473 .checked(is_open)
474 .expanded(is_open)
475 .layout_type(LayoutType::Row)
476 .on_press(|cx| cx.emit(MenuEvent::ToggleOpen));
477
478 if handle.try_data::<MenuBar>().is_some() {
479 let menu_bar_open = handle.data::<MenuBar>().is_open;
480 handle.bind(menu_bar_open, move |handle| {
481 let is_open = menu_bar_open.get();
482 handle.modify(|menu_button| menu_button.open_on_hover = is_open);
483 })
484 } else {
485 handle
486 }
487 }
488}
489
490impl View for Submenu {
491 fn element(&self) -> Option<&'static str> {
492 Some("submenu")
493 }
494
495 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
496 event.map(|window_event, meta| match window_event {
497 WindowEvent::MouseEnter => {
498 if meta.target == cx.current && self.open_on_hover {
499 let parent = cx.tree.get_parent(cx.current).unwrap();
501 cx.emit_custom(
502 Event::new(MenuEvent::Close).target(parent).propagate(Propagation::Subtree),
503 );
504 self.focus_on_open.set(false);
506 cx.emit(MenuEvent::Open);
507 }
508 }
509
510 _ => {}
511 });
512
513 event.map(|menu_event, meta| match menu_event {
514 MenuEvent::TriggerArrowDown => {
515 let popup_open = self.is_open.get();
516 if popup_open {
517 meta.consume();
519 } else if !self.is_submenu {
520 self.focus_on_open.set(true);
522 self.is_open.set(true);
523 cx.emit(MenuEvent::MenuIsOpen);
524 meta.consume();
525 }
526 }
527
528 MenuEvent::TriggerArrowRight => {
529 if self.is_submenu {
530 let popup_open = self.is_open.get();
531 if !popup_open {
532 self.focus_on_open.set(true);
533 self.is_open.set(true);
534 cx.emit(MenuEvent::MenuIsOpen);
535 }
536 }
537 meta.consume();
538 }
539
540 MenuEvent::TriggerArrowLeft => {
541 let Some(parent_of_trigger) = cx.tree.get_parent(cx.current()) else {
543 return;
544 };
545
546 let is_direct_submenu_of_menubar =
547 cx.get_view_with::<MenuBar>(parent_of_trigger).is_some();
548
549 if is_direct_submenu_of_menubar {
550 cx.emit(MenuEvent::FocusPrevMenuBarItem);
551 } else {
552 cx.emit(MenuEvent::CloseAndFocusTrigger);
553 }
554
555 meta.consume();
556 }
557 MenuEvent::Open => {
558 if !self.is_open.get() {
559 self.focus_on_open.set(false);
560 self.is_open.set(true);
561 cx.emit(MenuEvent::MenuIsOpen);
562 }
563 meta.consume();
564 }
565
566 MenuEvent::CloseAll => {
567 self.is_open.set_if_changed(false);
568 cx.emit_custom(
569 Event::new(MenuEvent::Close).target(cx.current).propagate(Propagation::Subtree),
570 );
571 }
572
573 MenuEvent::Close => {
574 self.is_open.set_if_changed(false);
575 }
576
577 MenuEvent::CloseAndFocusTrigger => {
578 self.is_open.set_if_changed(false);
579 cx.focus();
580 if !self.is_submenu {
581 cx.emit(MenuEvent::CloseAll);
582 }
583 meta.consume();
584 }
585
586 MenuEvent::FocusNext => {
590 let focused = cx.focused();
591 cx.with_current(focused, |cx| {
592 focus_next_sibling_wrapped(cx);
593 });
594 meta.consume();
595 }
596
597 MenuEvent::FocusPrev => {
598 let focused = cx.focused();
599 cx.with_current(focused, |cx| {
600 focus_prev_sibling_wrapped(cx);
601 });
602 meta.consume();
603 }
604
605 MenuEvent::FocusFirst => {
606 let focused = cx.focused();
607 cx.with_current(focused, |cx| {
608 focus_first_sibling(cx);
609 });
610 meta.consume();
611 }
612
613 MenuEvent::FocusLast => {
614 let focused = cx.focused();
615 cx.with_current(focused, |cx| {
616 focus_last_sibling(cx);
617 });
618 meta.consume();
619 }
620
621 MenuEvent::ToggleOpen => {
622 let is_open = !self.is_open.get();
623 self.is_open.set(is_open);
624 if is_open {
625 self.focus_on_open.set(false);
626 cx.emit(MenuEvent::MenuIsOpen);
627 } else {
628 let parent = cx.tree.get_parent(cx.current).unwrap();
630 cx.emit_custom(
631 Event::new(MenuEvent::CloseAll)
632 .target(parent)
633 .propagate(Propagation::Direct),
634 );
635 }
636 meta.consume();
637 }
638
639 _ => {}
640 });
641 }
642}
643
644pub struct MenuButton {}
646
647impl MenuButton {
648 pub fn new<V: View>(
650 cx: &mut Context,
651 action: impl Fn(&mut EventContext) + Send + Sync + 'static,
652 content: impl Fn(&mut Context) -> Handle<V> + 'static,
653 ) -> Handle<Self> {
654 Self {}
655 .build(cx, |cx| {
656 (content)(cx).hoverable(false);
657 })
658 .on_press(move |cx| {
659 (action)(cx);
660 cx.emit(MenuEvent::CloseAll);
661 cx.emit(ModalEvent::HideMenu);
662 cx.emit(MenuEvent::Close);
663 })
664 .focusable(true)
665 .role(Role::MenuItem)
666 .navigable(false)
667 }
668}
669
670impl View for MenuButton {
671 fn element(&self) -> Option<&'static str> {
672 Some("menubutton")
673 }
674
675 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
676 event.map(|window_event, meta| match window_event {
677 WindowEvent::MouseEnter => {
678 if meta.target == cx.current {
679 let parent = cx.tree.get_parent(cx.current).unwrap();
680 cx.emit_custom(
681 Event::new(MenuEvent::Close).target(parent).propagate(Propagation::Subtree),
682 );
683 }
684 }
685
686 WindowEvent::KeyDown(Code::ArrowRight, _) => {
687 cx.emit(MenuEvent::FocusNextMenuBarItem);
688 }
689
690 _ => {}
691 });
692 }
693}