zng_wgt_menu/
sub.rs

1//! Sub-menu widget and properties.
2
3use std::time::Duration;
4
5use super::ButtonStyle;
6use colors::BASE_COLOR_VAR;
7use zng_ext_font::FontNames;
8use zng_ext_input::{
9    focus::{FOCUS, FOCUS_CHANGED_EVENT, WidgetInfoFocusExt as _},
10    gesture::CLICK_EVENT,
11    keyboard::{KEY_INPUT_EVENT, Key, KeyState},
12    mouse::{ClickMode, MOUSE_HOVERED_EVENT},
13};
14use zng_ext_l10n::lang;
15use zng_wgt::{align, is_disabled, is_rtl, prelude::*};
16use zng_wgt_container::padding;
17use zng_wgt_fill::{background, background_color, foreground_highlight};
18use zng_wgt_filter::{opacity, saturate};
19use zng_wgt_input::{
20    CursorIcon, click_mode, cursor,
21    focus::{FocusClickBehavior, focus_click_behavior, focusable, is_focused},
22    is_hovered,
23    pointer_capture::capture_pointer,
24};
25use zng_wgt_layer::{
26    AnchorMode, AnchorOffset, AnchorSize,
27    popup::{POPUP, POPUP_CLOSE_CMD, PopupState},
28};
29use zng_wgt_size_offset::{size, width};
30use zng_wgt_style::{Style, StyleMix, impl_style_fn, style_fn};
31#[doc(hidden)]
32pub use zng_wgt_text::Text;
33
34/// Submenu header and items.
35#[widget($crate::sub::SubMenu {
36    ($header_txt:expr, $children:expr $(,)?) => {
37        header = $crate::sub::Text!($header_txt);
38        children = $children;
39    }
40})]
41pub struct SubMenu(StyleMix<WidgetBase>);
42impl SubMenu {
43    widget_impl! {
44        /// Sub-menu items.
45        pub crate::popup::children(children: impl UiNodeList);
46    }
47
48    fn widget_intrinsic(&mut self) {
49        self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
50        widget_set! {
51            self;
52            style_base_fn = style_fn!(|_| DefaultStyle!());
53            focusable = true;
54            click_mode = ClickMode::press();
55            focus_click_behavior = FocusClickBehavior::Ignore; // we handle clicks.
56            capture_pointer = true; // part of press-and-drag to click (see SubMenuPopup)
57        }
58
59        self.widget_builder().push_build_action(|wgt| {
60            let header = wgt
61                .capture_ui_node(property_id!(Self::header))
62                .unwrap_or_else(|| FillUiNode.boxed());
63
64            let children = wgt
65                .capture_property(property_id!(Self::children))
66                .map(|p| p.args.ui_node_list(0).clone())
67                .unwrap_or_else(|| ArcNodeList::new(ui_vec![].boxed()));
68
69            wgt.set_child(header);
70
71            wgt.push_intrinsic(NestGroup::EVENT, "sub_menu_node", |c| sub_menu_node(c, children));
72        });
73    }
74}
75impl_style_fn!(SubMenu);
76
77/// Sub-menu implementation.
78pub fn sub_menu_node(child: impl UiNode, children: ArcNodeList<BoxedUiNodeList>) -> impl UiNode {
79    let mut open = None::<ReadOnlyArcVar<PopupState>>;
80    let is_open = var(false);
81    let mut open_timer = None;
82    let mut close_timer = None;
83    let child = with_context_var(child, IS_OPEN_VAR, is_open.clone());
84    let mut close_cmd = CommandHandle::dummy();
85
86    match_node(child, move |_, op| {
87        let mut open_pop = false;
88
89        match op {
90            UiNodeOp::Init => {
91                WIDGET
92                    .sub_event(&CLICK_EVENT)
93                    .sub_event(&KEY_INPUT_EVENT)
94                    .sub_event(&FOCUS_CHANGED_EVENT)
95                    .sub_event(&MOUSE_HOVERED_EVENT);
96
97                close_cmd = POPUP_CLOSE_CMD.scoped(WIDGET.id()).subscribe(false);
98            }
99            UiNodeOp::Deinit => {
100                if let Some(v) = open.take() {
101                    POPUP.force_close(&v);
102                    is_open.set(false);
103                }
104                close_cmd = CommandHandle::dummy();
105                open_timer = None;
106                close_timer = None;
107            }
108            UiNodeOp::Info { info } => {
109                info.set_meta(
110                    *SUB_MENU_INFO_ID,
111                    SubMenuInfo {
112                        parent: SUB_MENU_PARENT_CTX.get_clone(),
113                        is_open: is_open.clone(),
114                    },
115                );
116            }
117            UiNodeOp::Event { update } => {
118                if let Some(args) = MOUSE_HOVERED_EVENT.on(update) {
119                    if args.is_mouse_enter() {
120                        let info = WIDGET.info();
121
122                        let is_root = info.submenu_parent().is_none();
123                        let is_open = is_open.get();
124
125                        if is_root {
126                            // menus focus on hover (implemented in sub_menu_popup_node)
127                            // root sub-menus focus on hover only if the menu is focused or a sibling is open (implemented here)
128
129                            if !is_open {
130                                if let (Some(menu), Some(focused)) = (info.menu(), FOCUS.focused().get()) {
131                                    let is_menu_focused = focused.contains(menu.id());
132
133                                    let mut focus_on_hover = is_menu_focused;
134                                    if !focus_on_hover {
135                                        if let Some(focused) = info.tree().get(focused.widget_id()) {
136                                            if let Some(f_menu) = focused.menu() {
137                                                // focused in menu-item, spawned from the same menu.
138                                                focus_on_hover = f_menu.id() == menu.id();
139                                            }
140                                        }
141                                    }
142
143                                    if focus_on_hover {
144                                        // focus, the popup will open on FOCUS_CHANGED_EVENT too.
145                                        FOCUS.focus_widget(WIDGET.id(), false);
146                                    }
147                                }
148                            }
149                        } else if !is_open && open_timer.is_none() {
150                            // non-root sub-menus open after a hover delay.
151                            let t = TIMERS.deadline(HOVER_OPEN_DELAY_VAR.get());
152                            t.subscribe(UpdateOp::Update, WIDGET.id()).perm();
153                            open_timer = Some(t);
154                        }
155                    } else if args.is_mouse_leave() {
156                        open_timer = None;
157                    }
158                } else if let Some(args) = KEY_INPUT_EVENT.on_unhandled(update) {
159                    if let KeyState::Pressed = args.state {
160                        if !is_open.get() {
161                            if let Some(info) = WIDGET.info().into_focusable(true, true) {
162                                if info.info().submenu_parent().is_none() {
163                                    // root, open for arrow keys that do not cause focus move
164                                    if matches!(&args.key, Key::ArrowUp | Key::ArrowDown) {
165                                        open_pop = info.focusable_down().is_none() && info.focusable_up().is_none();
166                                    } else if matches!(&args.key, Key::ArrowLeft | Key::ArrowRight) {
167                                        open_pop = info.focusable_left().is_none() && info.focusable_right().is_none();
168                                    }
169                                } else {
170                                    // sub, open in direction.
171                                    match DIRECTION_VAR.get() {
172                                        LayoutDirection::LTR => open_pop = matches!(&args.key, Key::ArrowRight),
173                                        LayoutDirection::RTL => open_pop = matches!(&args.key, Key::ArrowLeft),
174                                    }
175                                }
176                            }
177
178                            if open_pop {
179                                args.propagation().stop();
180                            }
181                        }
182                    }
183                } else if let Some(args) = FOCUS_CHANGED_EVENT.on(update) {
184                    if args.is_focus_enter(WIDGET.id()) {
185                        close_timer = None;
186                        if !is_open.get() {
187                            // focused when not open
188                            let info = WIDGET.info();
189                            if info.submenu_parent().is_none() {
190                                // is root sub-menu
191                                if let Some(prev_root) = args
192                                    .prev_focus
193                                    .as_ref()
194                                    .and_then(|p| info.tree().get(p.widget_id()))
195                                    .and_then(|w| w.submenu_root())
196                                {
197                                    // prev focus was open
198                                    if prev_root.is_submenu_open().map(|v| v.get()).unwrap_or(false) {
199                                        if let (Some(prev_menu), Some(our_menu)) = (prev_root.menu(), info.menu()) {
200                                            // same menu and sibling was open, open
201                                            open_pop = our_menu.id() == prev_menu.id();
202                                        }
203                                    }
204                                }
205                            }
206                        }
207                    } else if args.is_focus_leave(WIDGET.id()) && is_open.get() {
208                        if let Some(f) = &args.new_focus {
209                            if let Some(f) = WINDOW.info().get(f.widget_id()) {
210                                let id = WIDGET.id();
211                                if !f.submenu_ancestors().any(|s| s.id() == id) {
212                                    // Focus did not move to child sub-menu,
213                                    // close after delay.
214                                    //
215                                    // This covers the case of focus moving back to the sub-menu and then away,
216                                    // `sub_menu_popup_node` covers the case of focus moving to a different sub-menu directly.
217                                    let t = TIMERS.deadline(HOVER_OPEN_DELAY_VAR.get());
218                                    t.subscribe(UpdateOp::Update, id).perm();
219                                    close_timer = Some(t);
220                                }
221                            }
222                        }
223                    }
224                } else if let Some(args) = CLICK_EVENT.on(update) {
225                    if args.is_primary() {
226                        args.propagation().stop();
227
228                        // open if is closed
229                        open_pop = if let Some(s) = open.take() {
230                            let closed = matches!(s.get(), PopupState::Closed);
231                            if !closed {
232                                if WIDGET.info().submenu_parent().is_none() {
233                                    // root sub-menu, close and return focus
234                                    POPUP.force_close(&s);
235                                    FOCUS.focus_exit();
236                                    is_open.set(false);
237                                    close_cmd.set_enabled(false);
238                                } else {
239                                    // nested sub-menu.
240                                    open = Some(s);
241                                }
242                            }
243                            closed
244                        } else {
245                            true
246                        };
247                        if !open_pop && open.is_none() {
248                            is_open.set(false);
249                        }
250                    }
251                } else if let Some(_args) = POPUP_CLOSE_CMD.scoped(WIDGET.id()).on(update) {
252                    if let Some(s) = open.take() {
253                        if !matches!(s.get(), PopupState::Closed) {
254                            POPUP.force_close(&s);
255                            is_open.set(false);
256                            close_cmd.set_enabled(false);
257                        }
258                    }
259                }
260            }
261            UiNodeOp::Update { .. } => {
262                if let Some(s) = &open {
263                    if matches!(s.get(), PopupState::Closed) {
264                        is_open.set(false);
265                        close_cmd.set_enabled(false);
266                        close_timer = None;
267                        open = None;
268                    } else if let Some(t) = &close_timer {
269                        if t.get().has_elapsed() {
270                            if let Some(s) = open.take() {
271                                if !matches!(s.get(), PopupState::Closed) {
272                                    POPUP.force_close(&s);
273                                    is_open.set(false);
274                                    close_cmd.set_enabled(false);
275                                }
276                            }
277                            close_timer = None;
278                        }
279                    }
280                } else if let Some(t) = &open_timer {
281                    if t.get().has_elapsed() {
282                        open_pop = true;
283                    }
284                }
285            }
286            _ => {}
287        }
288        if open_pop {
289            let pop = super::popup::SubMenuPopup! {
290                parent_id = WIDGET.id();
291                children = children.take_on_init().boxed();
292            };
293            let state = POPUP.open(pop);
294            state.subscribe(UpdateOp::Update, WIDGET.id()).perm();
295            if !matches!(state.get(), PopupState::Closed) {
296                is_open.set(true);
297                close_cmd.set_enabled(true);
298            }
299            open = Some(state);
300            open_timer = None;
301        }
302    })
303}
304
305/// Defines the sub-menu header child.
306#[property(CHILD, capture, default(FillUiNode), widget_impl(SubMenu))]
307pub fn header(child: impl UiNode) {}
308
309/// Width of the icon/checkmark column.
310///
311/// This property sets [`START_COLUMN_WIDTH_VAR`].
312#[property(CONTEXT, default(START_COLUMN_WIDTH_VAR), widget_impl(SubMenu))]
313pub fn start_column_width(child: impl UiNode, width: impl IntoVar<Length>) -> impl UiNode {
314    with_context_var(child, START_COLUMN_WIDTH_VAR, width)
315}
316
317/// Width of the sub-menu expand symbol column.
318///
319/// This property sets [`END_COLUMN_WIDTH_VAR`].
320#[property(CONTEXT, default(END_COLUMN_WIDTH_VAR), widget_impl(SubMenu))]
321pub fn end_column_width(child: impl UiNode, width: impl IntoVar<Length>) -> impl UiNode {
322    with_context_var(child, END_COLUMN_WIDTH_VAR, width)
323}
324
325/// Sets the content to the [`Align::START`] side of the button menu item.
326///
327/// The `cell` is an non-interactive background that fills the [`START_COLUMN_WIDTH_VAR`] and button height.
328///
329/// This is usually an icon, or a checkmark.
330///
331/// See also [`start_column_fn`] for use in styles.
332///
333/// [`start_column_fn`]: fn@start_column_fn
334/// [`Align::START`]: zng_wgt::prelude::Align::START
335#[property(FILL)]
336pub fn start_column(child: impl UiNode, cell: impl UiNode) -> impl UiNode {
337    let cell = width(cell, START_COLUMN_WIDTH_VAR);
338    let cell = align(cell, Align::FILL_START);
339    background(child, cell)
340}
341
342/// Sets the content to the [`Align::END`] side of the button menu item.
343///
344/// The `cell` is an non-interactive background that fills the [`END_COLUMN_WIDTH_VAR`] and button height.
345///
346/// This is usually a little arrow for sub-menus.
347///
348/// See also [`end_column_fn`] for use in styles.
349///
350/// [`end_column_fn`]: fn@end_column_fn
351/// [`Align::END`]: zng_wgt::prelude::Align::END
352#[property(FILL)]
353pub fn end_column(child: impl UiNode, cell: impl UiNode) -> impl UiNode {
354    let cell = width(cell, END_COLUMN_WIDTH_VAR);
355    let cell = align(cell, Align::FILL_END);
356    background(child, cell)
357}
358
359/// Sets the content to the [`Align::START`] side of the button menu item generated using a [`WidgetFn<()>`].
360///
361/// This property presents the same visual as [`start_column`], but when used in styles `cell_fn` is called
362/// multiple times to generate duplicates of the start cell.
363///
364/// [`start_column`]: fn@start_column
365/// [`WidgetFn<()>`]: WidgetFn
366/// [`Align::START`]: zng_wgt::prelude::Align::START
367#[property(FILL)]
368pub fn start_column_fn(child: impl UiNode, cell_fn: impl IntoVar<WidgetFn<()>>) -> impl UiNode {
369    start_column(child, presenter((), cell_fn))
370}
371
372/// Sets the content to the [`Align::END`] side of the button menu item generated using a [`WidgetFn<()>`].
373///
374/// This property presents the same visual as [`end_column`], but when used in styles `cell_fn` is called
375/// multiple times to generate duplicates of the start cell.
376///
377/// [`end_column`]: fn@end_column
378/// [`WidgetFn<()>`]: WidgetFn
379/// [`Align::END`]: zng_wgt::prelude::Align::END
380#[property(FILL)]
381pub fn end_column_fn(child: impl UiNode, cell_fn: impl IntoVar<WidgetFn<()>>) -> impl UiNode {
382    end_column(child, presenter((), cell_fn))
383}
384
385/// If the start and end column width is applied as padding.
386///
387/// This property is enabled in menu-item styles to offset the content by [`start_column_width`] and [`end_column_width`].
388///
389/// [`start_column_width`]: fn@start_column_width
390/// [`end_column_width`]: fn@end_column_width
391#[property(CHILD_LAYOUT, default(false))]
392pub fn column_width_padding(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
393    let spacing = merge_var!(
394        START_COLUMN_WIDTH_VAR,
395        END_COLUMN_WIDTH_VAR,
396        DIRECTION_VAR,
397        enabled.into_var(),
398        |s, e, d, enabled| {
399            if *enabled {
400                let s = s.clone();
401                let e = e.clone();
402                if d.is_ltr() {
403                    SideOffsets::new(0, e, 0, s)
404                } else {
405                    SideOffsets::new(0, s, 0, e)
406                }
407            } else {
408                SideOffsets::zero()
409            }
410        }
411    );
412    padding(child, spacing)
413}
414
415context_var! {
416    /// Width of the icon/checkmark column.
417    pub static START_COLUMN_WIDTH_VAR: Length = 32;
418
419    /// Width of the sub-menu expand symbol column.
420    pub static END_COLUMN_WIDTH_VAR: Length = 24;
421
422    /// Delay a sub-menu must be hovered to open the popup.
423    ///
424    /// Is `300.ms()` by default.
425    pub static HOVER_OPEN_DELAY_VAR: Duration = 300.ms();
426
427    static IS_OPEN_VAR: bool = false;
428}
429
430/// If the sub-menu popup is open or opening.
431#[property(EVENT, widget_impl(SubMenu))]
432pub fn is_open(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
433    bind_state(child, IS_OPEN_VAR, state)
434}
435
436/// Delay a sub-menu must be hovered to open the popup.
437///
438/// Is `300.ms()` by default.
439///
440/// This property sets the [`HOVER_OPEN_DELAY_VAR`].
441#[property(CONTEXT, default(HOVER_OPEN_DELAY_VAR), widget_impl(SubMenu))]
442pub fn hover_open_delay(child: impl UiNode, delay: impl IntoVar<Duration>) -> impl UiNode {
443    with_context_var(child, HOVER_OPEN_DELAY_VAR, delay)
444}
445
446/// Style applied to [`SubMenu!`] not inside any other sub-menus.
447///
448/// [`SubMenu!`]: struct@SubMenu
449/// [`Menu!`]: struct@Menu
450#[widget($crate::sub::DefaultStyle)]
451pub struct DefaultStyle(Style);
452impl DefaultStyle {
453    fn widget_intrinsic(&mut self) {
454        widget_set! {
455            self;
456
457            replace = true;
458
459            padding = (4, 10);
460            opacity = 90.pct();
461            foreground_highlight = unset!;
462
463            zng_wgt_layer::popup::anchor_mode = DIRECTION_VAR.map(|d| match d {
464                LayoutDirection::LTR => AnchorMode::popup(AnchorOffset { place: Point::bottom_left(), origin: Point::top_left() }),
465                LayoutDirection::RTL => AnchorMode::popup(AnchorOffset { place: Point::bottom_right(), origin: Point::top_right() }),
466            });
467
468            when *#is_hovered || *#is_focused || *#is_open {
469                background_color = BASE_COLOR_VAR.shade(1);
470                opacity = 100.pct();
471            }
472
473            when *#is_disabled {
474                saturate = false;
475                opacity = 50.pct();
476                cursor = CursorIcon::NotAllowed;
477            }
478        }
479    }
480}
481
482/// Style applied to all [`SubMenu!`] widgets inside other sub-menus.
483///
484/// [`SubMenu!`]: struct@SubMenu
485#[widget($crate::sub::SubMenuStyle)]
486pub struct SubMenuStyle(ButtonStyle);
487impl SubMenuStyle {
488    fn widget_intrinsic(&mut self) {
489        widget_set! {
490            self;
491
492            zng_wgt_layer::popup::anchor_mode = DIRECTION_VAR.map(|d| {
493                match d {
494                    LayoutDirection::LTR => AnchorMode::popup(AnchorOffset {
495                        place: Point::top_right(),
496                        origin: Point::top_left(),
497                    }),
498                    LayoutDirection::RTL => AnchorMode::popup(AnchorOffset {
499                        place: Point::top_left(),
500                        origin: Point::top_right(),
501                    }),
502                }
503                .with_min_size(AnchorSize::Unbounded)
504            });
505
506            when *#is_open {
507                background_color = BASE_COLOR_VAR.shade(1);
508                opacity = 100.pct();
509            }
510
511            end_column_fn = wgt_fn!(|_| zng_wgt_text::Text! {
512                size = 1.2.em();
513                font_family = FontNames::system_ui(&lang!(und));
514                align = Align::CENTER;
515
516                txt = "⏵";
517                when *#is_rtl {
518                    txt = "⏴";
519                }
520            })
521        }
522    }
523}
524
525/// Extension methods for [`WidgetInfo`].
526///
527///  [`WidgetInfo`]: zng_wgt::prelude::WidgetInfo
528pub trait SubMenuWidgetInfoExt {
529    /// If this widget is a [`SubMenu!`] instance.
530    ///
531    /// [`SubMenu!`]: struct@SubMenu
532    fn is_submenu(&self) -> bool;
533
534    /// Gets a variable that tracks if the sub-menu is open.
535    fn is_submenu_open(&self) -> Option<ReadOnlyArcVar<bool>>;
536
537    /// Gets the sub-menu that spawned `self` if [`is_submenu`], otherwise returns the first ancestor
538    /// that is sub-menu.
539    ///
540    /// Note that the returned widget may not be an actual parent in the info-tree as
541    /// sub-menus use popups to present their sub-menus.
542    ///
543    /// [`is_submenu`]: SubMenuWidgetInfoExt::is_submenu
544    fn submenu_parent(&self) -> Option<WidgetInfo>;
545
546    /// Gets an iterator over sub-menu parents until root.
547    fn submenu_ancestors(&self) -> SubMenuAncestors;
548    /// Gets an iterator over the widget, if it is a sub-menu, and sub-menu parents until root.
549    fn submenu_self_and_ancestors(&self) -> SubMenuAncestors;
550
551    /// Gets the last submenu ancestor.
552    fn submenu_root(&self) -> Option<WidgetInfo>;
553
554    /// Gets the alt-scope parent of the `root_submenu`.
555    ///
556    /// This is `None` if the widget is inside a context menu or not inside.
557    fn menu(&self) -> Option<WidgetInfo>;
558}
559impl SubMenuWidgetInfoExt for WidgetInfo {
560    fn is_submenu(&self) -> bool {
561        self.meta().contains(*SUB_MENU_INFO_ID)
562    }
563
564    fn is_submenu_open(&self) -> Option<ReadOnlyArcVar<bool>> {
565        self.meta().get(*SUB_MENU_INFO_ID).map(|s| s.is_open.read_only())
566    }
567
568    fn submenu_parent(&self) -> Option<WidgetInfo> {
569        if let Some(p) = self.meta().get(*SUB_MENU_INFO_ID) {
570            self.tree().get(p.parent?)
571        } else if let Some(p) = self.ancestors().find(|a| a.is_submenu()) {
572            Some(p)
573        } else if let Some(pop) = self.meta().get(*SUB_MENU_POPUP_ID) {
574            self.tree().get(pop.parent?)
575        } else {
576            for anc in self.ancestors() {
577                if let Some(pop) = anc.meta().get(*SUB_MENU_POPUP_ID) {
578                    if let Some(p) = pop.parent {
579                        return self.tree().get(p);
580                    } else {
581                        // context-menu
582                        return Some(anc);
583                    }
584                }
585            }
586            None
587        }
588    }
589
590    fn submenu_ancestors(&self) -> SubMenuAncestors {
591        SubMenuAncestors {
592            node: self.submenu_parent(),
593        }
594    }
595
596    fn submenu_self_and_ancestors(&self) -> SubMenuAncestors {
597        if self.is_submenu() {
598            SubMenuAncestors { node: Some(self.clone()) }
599        } else {
600            self.submenu_ancestors()
601        }
602    }
603
604    fn submenu_root(&self) -> Option<WidgetInfo> {
605        self.submenu_ancestors().last()
606    }
607
608    fn menu(&self) -> Option<WidgetInfo> {
609        let root = self
610            .submenu_root()
611            .or_else(|| if self.is_submenu() { Some(self.clone()) } else { None })?;
612
613        let scope = root.into_focus_info(true, true).scope()?;
614
615        if !scope.is_alt_scope() {
616            return None;
617        }
618
619        Some(scope.info().clone())
620    }
621}
622
623/// Iterator over sub-menu parents.
624///
625/// See [`submenu_ancestors`] for more details.
626///
627/// [`submenu_ancestors`]: SubMenuWidgetInfoExt::submenu_ancestors
628pub struct SubMenuAncestors {
629    node: Option<WidgetInfo>,
630}
631impl Iterator for SubMenuAncestors {
632    type Item = WidgetInfo;
633
634    fn next(&mut self) -> Option<Self::Item> {
635        if let Some(n) = self.node.take() {
636            self.node = n.submenu_parent();
637            Some(n)
638        } else {
639            None
640        }
641    }
642}
643
644pub(super) struct SubMenuInfo {
645    pub parent: Option<WidgetId>,
646    pub is_open: ArcVar<bool>,
647}
648
649pub(super) struct SubMenuPopupInfo {
650    pub parent: Option<WidgetId>,
651}
652
653context_local! {
654    // only set during info
655    pub(super) static SUB_MENU_PARENT_CTX: Option<WidgetId> = None;
656}
657
658static_id! {
659    pub(super) static ref SUB_MENU_INFO_ID: StateId<SubMenuInfo>;
660    pub(super) static ref SUB_MENU_POPUP_ID: StateId<SubMenuPopupInfo>;
661}