Skip to main content

zng_wgt_menu/
sub.rs

1//! Sub-menu widget and properties.
2
3use std::time::Duration;
4
5use super::{icon_fn, shortcut_txt};
6use colors::BASE_COLOR_VAR;
7use zng_ext_font::FONTS;
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, base_color, is_disabled, is_mobile, is_rtl, prelude::*};
16use zng_wgt_access::{AccessRole, access_role};
17use zng_wgt_button::BUTTON;
18use zng_wgt_container::{child_align, padding};
19use zng_wgt_fill::{background, background_color, foreground_highlight};
20use zng_wgt_filter::{opacity, saturate};
21use zng_wgt_input::{
22    CursorIcon, click_mode, cursor,
23    focus::{FocusClickBehavior, focus_click_behavior, focusable, is_focused},
24    gesture::mnemonic,
25    is_hovered,
26    mouse::on_pre_mouse_enter,
27    pointer_capture::capture_pointer,
28};
29use zng_wgt_layer::{
30    AnchorMode, AnchorOffset, AnchorSize,
31    popup::{POPUP, POPUP_CLOSE_CMD, PopupState},
32};
33use zng_wgt_shortcut::ShortcutText;
34use zng_wgt_size_offset::{size, width};
35use zng_wgt_style::{Style, StyleMix, impl_style_fn, style_fn};
36
37/// Submenu header and items.
38#[widget($crate::sub::SubMenu {
39    ($header_txt:expr, $children:expr $(,)?) => {
40        header = $crate::MENU_TEXT_INPUT.label($header_txt);
41        children = $children;
42    }
43})]
44pub struct SubMenu(StyleMix<WidgetBase>);
45impl SubMenu {
46    widget_impl! {
47        /// Sub-menu items.
48        pub crate::popup::children(children: impl IntoUiNode);
49    }
50
51    fn widget_intrinsic(&mut self) {
52        self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
53        widget_set! {
54            self;
55            focusable = true;
56            mnemonic = true;
57            click_mode = ClickMode::press();
58            focus_click_behavior = FocusClickBehavior::Ignore; // we handle clicks.
59            capture_pointer = true; // part of press-and-drag to click (see SubMenuPopup)
60        }
61
62        self.widget_builder().push_build_action(|wgt| {
63            let header = wgt
64                .capture_ui_node(property_id!(Self::header))
65                .unwrap_or_else(|| FillUiNode.into_node());
66
67            let children = wgt
68                .capture_property(property_id!(Self::children))
69                .map(|p| p.args.ui_node(0).clone())
70                .unwrap_or_else(|| ArcNode::new(ui_vec![]));
71
72            wgt.set_child(header);
73
74            wgt.push_intrinsic(NestGroup::EVENT, "sub_menu_node", |c| sub_menu_node(c, children));
75        });
76    }
77}
78impl_style_fn!(SubMenu, DefaultStyle);
79
80/// Sub-menu implementation.
81pub fn sub_menu_node(child: impl IntoUiNode, children: ArcNode) -> UiNode {
82    let mut open = None::<Var<PopupState>>;
83    let is_open = var(false);
84    let mut open_timer = None::<DeadlineVar>;
85    let mut close_timer = None::<DeadlineVar>;
86    let child = with_context_var(child, IS_OPEN_VAR, is_open.clone());
87    let mut close_cmd = CommandHandle::dummy();
88
89    match_node(child, move |_, op| {
90        let mut open_pop = false;
91
92        match op {
93            UiNodeOp::Init => {
94                WIDGET
95                    .sub_event(&CLICK_EVENT)
96                    .sub_event(&KEY_INPUT_EVENT)
97                    .sub_event(&FOCUS_CHANGED_EVENT)
98                    .sub_event(&MOUSE_HOVERED_EVENT);
99
100                close_cmd = POPUP_CLOSE_CMD.scoped(WIDGET.id()).subscribe(false);
101
102                let has_open = super::OPEN_SUBMENU_VAR.current_context();
103                if !has_open.capabilities().is_const() {
104                    let handle = is_open.hook(move |v| {
105                        if *v.value() {
106                            has_open.modify(|v| **v += 1);
107                        } else {
108                            has_open.modify(|v| **v -= 1);
109                        }
110                        true
111                    });
112                    WIDGET.push_var_handle(handle);
113                }
114            }
115            UiNodeOp::Deinit => {
116                if let Some(v) = open.take() {
117                    POPUP.force_close(&v);
118                    is_open.set(false);
119                }
120                close_cmd = CommandHandle::dummy();
121                open_timer = None;
122                close_timer = None;
123            }
124            UiNodeOp::Info { info } => {
125                info.set_meta(
126                    *SUB_MENU_INFO_ID,
127                    SubMenuInfo {
128                        parent: SUB_MENU_PARENT_CTX.get_clone(),
129                        is_open: is_open.clone(),
130                    },
131                );
132            }
133
134            UiNodeOp::Update { .. } => {
135                if let Some(s) = &open {
136                    if matches!(s.get(), PopupState::Closed) {
137                        is_open.set(false);
138                        close_cmd.enabled().set(false);
139                        close_timer = None;
140                        open = None;
141                    } else if let Some(t) = &close_timer
142                        && t.get().has_elapsed()
143                    {
144                        if let Some(s) = open.take()
145                            && !matches!(s.get(), PopupState::Closed)
146                        {
147                            POPUP.force_close(&s);
148                            is_open.set(false);
149                            close_cmd.enabled().set(false);
150                        }
151                        close_timer = None;
152                    }
153                } else if let Some(t) = &open_timer
154                    && t.get().has_elapsed()
155                {
156                    open_pop = true;
157                }
158
159                MOUSE_HOVERED_EVENT.each_update(true, |args| {
160                    let wgt = (WINDOW.id(), WIDGET.id());
161                    if args.is_mouse_enter_enabled(wgt) {
162                        let info = WIDGET.info();
163
164                        let is_root = info.submenu_parent().is_none();
165                        let is_open = is_open.get();
166
167                        if is_root {
168                            // menus focus on hover (implemented in sub_menu_popup_node)
169                            // root sub-menus focus on hover only if the menu is focused or a sibling is open (implemented here)
170
171                            if !is_open
172                                && let Some(menu) = info.menu()
173                                && let Some(focused) = FOCUS.focused().get()
174                            {
175                                let is_menu_focused = focused.contains(menu.id());
176
177                                let mut focus_on_hover = is_menu_focused;
178                                if !focus_on_hover
179                                    && let Some(focused) = info.tree().get(focused.widget_id())
180                                    && let Some(f_menu) = focused.menu()
181                                {
182                                    // focused in menu-item, spawned from the same menu.
183                                    focus_on_hover = f_menu.id() == menu.id();
184                                }
185
186                                if focus_on_hover {
187                                    // focus, the popup will open on FOCUS_CHANGED_EVENT too.
188                                    FOCUS.focus_widget(WIDGET.id(), false);
189                                }
190                            }
191                        } else if !is_open && open_timer.is_none() {
192                            // non-root sub-menus open after a hover delay.
193                            let t = TIMERS.deadline(HOVER_OPEN_DELAY_VAR.get());
194                            t.subscribe(UpdateOp::Update, WIDGET.id()).perm();
195                            open_timer = Some(t);
196                        }
197                    } else if args.is_mouse_leave_enabled(wgt) {
198                        open_timer = None;
199                    }
200                });
201                KEY_INPUT_EVENT.each_update(true, |args| {
202                    if let KeyState::Pressed = args.state
203                        && args.target.contains_enabled(WIDGET.id())
204                        && !is_open.get()
205                    {
206                        if let Some(info) = WIDGET.info().into_focusable(true, true) {
207                            if info.info().submenu_parent().is_none() {
208                                // root, open for arrow keys that do not cause focus move
209                                if matches!(&args.key, Key::ArrowUp | Key::ArrowDown) {
210                                    open_pop = info.focusable_down().is_none() && info.focusable_up().is_none();
211                                } else if matches!(&args.key, Key::ArrowLeft | Key::ArrowRight) {
212                                    open_pop = info.focusable_left().is_none() && info.focusable_right().is_none();
213                                }
214                            } else {
215                                // sub, open in direction.
216                                match DIRECTION_VAR.get() {
217                                    LayoutDirection::LTR => open_pop = matches!(&args.key, Key::ArrowRight),
218                                    LayoutDirection::RTL => open_pop = matches!(&args.key, Key::ArrowLeft),
219                                }
220                            }
221                        }
222
223                        if open_pop {
224                            args.propagation.stop();
225                        }
226                    }
227                });
228                FOCUS_CHANGED_EVENT.each_update(true, |args| {
229                    if args.is_focus_enter_enabled(WIDGET.id()) {
230                        close_timer = None;
231                        if !is_open.get() {
232                            // focused when not open
233
234                            let info = WIDGET.info();
235                            if info.submenu_parent().is_none() // is root sub-menu
236                                && let Some(prev_root) = args
237                                    .prev_focus
238                                    .as_ref()
239                                    .and_then(|p| info.tree().get(p.widget_id()))
240                                    .and_then(|w| w.submenu_root())  // prev focus was open
241                                && prev_root.is_submenu_open().map(|v| v.get()).unwrap_or(false)
242                                && let Some(prev_menu) = prev_root.menu()
243                                && let Some(our_menu) = info.menu()
244                            {
245                                // same menu and sibling was open, open
246                                open_pop = our_menu.id() == prev_menu.id();
247                            }
248                        }
249                    } else if args.is_focus_leave_enabled(WIDGET.id())
250                        && is_open.get()
251                        && let Some(f) = &args.new_focus
252                        && let Some(f) = WINDOW.info().get(f.widget_id())
253                    {
254                        let id = WIDGET.id();
255                        if !f.submenu_ancestors().any(|s| s.id() == id) {
256                            // Focus did not move to child sub-menu,
257                            // close after delay.
258                            //
259                            // This covers the case of focus moving back to the sub-menu and then away,
260                            // `sub_menu_popup_node` covers the case of focus moving to a different sub-menu directly.
261                            let t = TIMERS.deadline(HOVER_OPEN_DELAY_VAR.get());
262                            t.subscribe(UpdateOp::Update, id).perm();
263                            close_timer = Some(t);
264                        }
265                    }
266                });
267
268                CLICK_EVENT.each_update(true, |args| {
269                    if args.is_primary() && args.target.contains_enabled(WIDGET.id()) {
270                        args.propagation.stop();
271
272                        // open if is closed
273                        open_pop = if let Some(s) = open.take() {
274                            let closed = matches!(s.get(), PopupState::Closed);
275                            if !closed {
276                                if WIDGET.info().submenu_parent().is_none() {
277                                    // root sub-menu, close and return focus
278                                    POPUP.force_close(&s);
279                                    FOCUS.focus_exit(true);
280                                    is_open.set(false);
281                                    close_cmd.enabled().set(false);
282                                } else {
283                                    // nested sub-menu.
284                                    open = Some(s);
285                                }
286                            }
287                            closed
288                        } else {
289                            true
290                        };
291                        if !open_pop && open.is_none() {
292                            is_open.set(false);
293                        }
294                    }
295                });
296
297                if POPUP_CLOSE_CMD.scoped(WIDGET.id()).has_update(true, true)
298                    && let Some(s) = open.take()
299                    && !matches!(s.get(), PopupState::Closed)
300                {
301                    POPUP.force_close(&s);
302                    is_open.set(false);
303                    close_cmd.enabled().set(false);
304                }
305            }
306            _ => {}
307        }
308        if open_pop {
309            let pop = super::popup::SubMenuPopup! {
310                parent_id = WIDGET.id();
311                children = children.take_on_init();
312            };
313            let state = POPUP.open(pop);
314            state.subscribe(UpdateOp::Update, WIDGET.id()).perm();
315            if !matches!(state.get(), PopupState::Closed) {
316                is_open.set(true);
317                close_cmd.enabled().set(true);
318            }
319            open = Some(state);
320            open_timer = None;
321        }
322    })
323}
324
325/// Defines the sub-menu header child.
326#[property(CHILD, default(FillUiNode), widget_impl(SubMenu))]
327pub fn header(wgt: &mut WidgetBuilding, child: impl IntoUiNode) {
328    let _ = child;
329    wgt.expect_property_capture();
330}
331
332/// Width of the icon/checkmark column.
333///
334/// This property sets [`START_COLUMN_WIDTH_VAR`].
335#[property(CONTEXT, default(START_COLUMN_WIDTH_VAR), widget_impl(SubMenu, DefaultStyle))]
336pub fn start_column_width(child: impl IntoUiNode, width: impl IntoVar<Length>) -> UiNode {
337    with_context_var(child, START_COLUMN_WIDTH_VAR, width)
338}
339
340/// Width of the sub-menu expand symbol column.
341///
342/// This property sets [`END_COLUMN_WIDTH_VAR`].
343#[property(CONTEXT, default(END_COLUMN_WIDTH_VAR), widget_impl(SubMenu, DefaultStyle))]
344pub fn end_column_width(child: impl IntoUiNode, width: impl IntoVar<Length>) -> UiNode {
345    with_context_var(child, END_COLUMN_WIDTH_VAR, width)
346}
347
348/// Sets the content to the [`Align::START`] side of the button menu item.
349///
350/// The `cell` is an non-interactive background that fills the [`START_COLUMN_WIDTH_VAR`] and button height.
351///
352/// This is usually an icon, or a checkmark.
353///
354/// See also [`start_column_fn`] for use in styles.
355///
356/// [`start_column_fn`]: fn@start_column_fn
357/// [`Align::START`]: zng_wgt::prelude::Align::START
358#[property(FILL)]
359pub fn start_column(child: impl IntoUiNode, cell: impl IntoUiNode) -> UiNode {
360    let cell = width(cell, START_COLUMN_WIDTH_VAR);
361    let cell = align(cell, Align::FILL_START);
362    background(child, cell)
363}
364
365/// Sets the content to the [`Align::END`] side of the button menu item.
366///
367/// The `cell` is an non-interactive background that fills the [`END_COLUMN_WIDTH_VAR`] and button height.
368///
369/// This is usually a little arrow for sub-menus.
370///
371/// See also [`end_column_fn`] for use in styles.
372///
373/// [`end_column_fn`]: fn@end_column_fn
374/// [`Align::END`]: zng_wgt::prelude::Align::END
375#[property(FILL)]
376pub fn end_column(child: impl IntoUiNode, cell: impl IntoUiNode) -> UiNode {
377    let cell = width(cell, END_COLUMN_WIDTH_VAR);
378    let cell = align(cell, Align::FILL_END);
379    background(child, cell)
380}
381
382/// Sets the content to the [`Align::START`] side of the button menu item generated using a [`WidgetFn<()>`].
383///
384/// This property presents the same visual as [`start_column`], but when used in styles `cell_fn` is called
385/// multiple times to generate duplicates of the start cell.
386///
387/// [`start_column`]: fn@start_column
388/// [`WidgetFn<()>`]: WidgetFn
389/// [`Align::START`]: zng_wgt::prelude::Align::START
390#[property(FILL)]
391pub fn start_column_fn(child: impl IntoUiNode, cell_fn: impl IntoVar<WidgetFn<()>>) -> UiNode {
392    start_column(child, presenter((), cell_fn))
393}
394
395/// Sets the content to the [`Align::END`] side of the button menu item generated using a [`WidgetFn<()>`].
396///
397/// This property presents the same visual as [`end_column`], but when used in styles `cell_fn` is called
398/// multiple times to generate duplicates of the start cell.
399///
400/// [`end_column`]: fn@end_column
401/// [`WidgetFn<()>`]: WidgetFn
402/// [`Align::END`]: zng_wgt::prelude::Align::END
403#[property(FILL)]
404pub fn end_column_fn(child: impl IntoUiNode, cell_fn: impl IntoVar<WidgetFn<()>>) -> UiNode {
405    end_column(child, presenter((), cell_fn))
406}
407
408/// If the start and end column width is applied as padding.
409///
410/// This property is enabled in menu-item styles to offset the content by [`start_column_width`] and [`end_column_width`].
411///
412/// [`start_column_width`]: fn@start_column_width
413/// [`end_column_width`]: fn@end_column_width
414#[property(CHILD_LAYOUT, default(false))]
415pub fn column_width_padding(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
416    let spacing = merge_var!(
417        START_COLUMN_WIDTH_VAR,
418        END_COLUMN_WIDTH_VAR,
419        DIRECTION_VAR,
420        enabled.into_var(),
421        |s, e, d, enabled| {
422            if *enabled {
423                let s = s.clone();
424                let e = e.clone();
425                if d.is_ltr() {
426                    SideOffsets::new(0, e, 0, s)
427                } else {
428                    SideOffsets::new(0, s, 0, e)
429                }
430            } else {
431                SideOffsets::zero()
432            }
433        }
434    );
435    padding(child, spacing)
436}
437
438context_var! {
439    /// Width of the icon/checkmark column.
440    pub static START_COLUMN_WIDTH_VAR: Length = 32;
441
442    /// Width of the sub-menu expand symbol column.
443    pub static END_COLUMN_WIDTH_VAR: Length = 24;
444
445    /// Delay a sub-menu must be hovered to open the popup.
446    ///
447    /// Is `300.ms()` by default.
448    pub static HOVER_OPEN_DELAY_VAR: Duration = 150.ms();
449
450    static IS_OPEN_VAR: bool = false;
451}
452
453/// If the sub-menu popup is open or opening.
454#[property(EVENT, widget_impl(SubMenu, DefaultStyle))]
455pub fn is_open(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
456    bind_state(child, IS_OPEN_VAR, state)
457}
458
459/// Delay a sub-menu must be hovered to open the popup.
460///
461/// Is `300.ms()` by default.
462///
463/// This property sets the [`HOVER_OPEN_DELAY_VAR`].
464#[property(CONTEXT, default(HOVER_OPEN_DELAY_VAR), widget_impl(SubMenu, DefaultStyle))]
465pub fn hover_open_delay(child: impl IntoUiNode, delay: impl IntoVar<Duration>) -> UiNode {
466    with_context_var(child, HOVER_OPEN_DELAY_VAR, delay)
467}
468
469/// Style applied to [`SubMenu!`] not inside any other sub-menus.
470///
471/// [`SubMenu!`]: struct@SubMenu
472/// [`Menu!`]: struct@Menu
473#[widget($crate::sub::DefaultStyle)]
474pub struct DefaultStyle(Style);
475impl DefaultStyle {
476    fn widget_intrinsic(&mut self) {
477        widget_set! {
478            self;
479
480            replace = true;
481
482            padding = (4, 10);
483            opacity = 90.pct();
484            foreground_highlight = unset!;
485
486            zng_wgt_layer::popup::anchor_mode = DIRECTION_VAR.map(|d| match d {
487                LayoutDirection::LTR => AnchorMode::popup(AnchorOffset {
488                    place: Point::bottom_left(),
489                    origin: Point::top_left(),
490                }),
491                LayoutDirection::RTL => AnchorMode::popup(AnchorOffset {
492                    place: Point::bottom_right(),
493                    origin: Point::top_right(),
494                }),
495            });
496
497            zng_wgt_button::style_fn = style_fn!(|_| ButtonStyle!());
498            zng_wgt_toggle::style_fn = style_fn!(|_| ToggleStyle!());
499            zng_wgt_rule_line::hr::color = BASE_COLOR_VAR.shade(1);
500            zng_wgt_rule_line::vr::color = BASE_COLOR_VAR.shade(1);
501            zng_wgt_rule_line::vr::height = 1.em();
502            zng_wgt_text::icon::ico_size = 18;
503
504            when *#is_hovered || *#is_focused || *#is_open {
505                background_color = BASE_COLOR_VAR.shade(1);
506                opacity = 100.pct();
507            }
508
509            when *#is_disabled {
510                saturate = false;
511                opacity = 50.pct();
512                cursor = CursorIcon::NotAllowed;
513            }
514        }
515    }
516}
517
518/// Style applied to all [`SubMenu!`] widgets inside other sub-menus.
519///
520/// [`SubMenu!`]: struct@SubMenu
521#[widget($crate::sub::SubMenuStyle)]
522pub struct SubMenuStyle(ButtonStyle);
523impl SubMenuStyle {
524    fn widget_intrinsic(&mut self) {
525        widget_set! {
526            self;
527
528            zng_wgt_layer::popup::anchor_mode = DIRECTION_VAR.map(|d| {
529                match d {
530                    LayoutDirection::LTR => AnchorMode::popup(AnchorOffset {
531                        place: Point::top_right(),
532                        origin: Point::top_left(),
533                    }),
534                    LayoutDirection::RTL => AnchorMode::popup(AnchorOffset {
535                        place: Point::top_left(),
536                        origin: Point::top_right(),
537                    }),
538                }
539                .with_min_size(AnchorSize::Unbounded)
540            });
541
542            when *#is_open {
543                background_color = BASE_COLOR_VAR.shade(1);
544                opacity = 100.pct();
545            }
546
547            end_column_fn = wgt_fn!(|_| zng_wgt_text::Text! {
548                size = 1.2.em();
549                font_family = FONTS.generics().system_ui(&lang!(und));
550                align = Align::CENTER;
551
552                txt = "⏵";
553                when *#is_rtl {
554                    txt = "⏴";
555                }
556            });
557        }
558    }
559}
560
561/// Extension methods for [`WidgetInfo`].
562///
563///  [`WidgetInfo`]: zng_wgt::prelude::WidgetInfo
564pub trait SubMenuWidgetInfoExt {
565    /// If this widget is a [`SubMenu!`] instance.
566    ///
567    /// [`SubMenu!`]: struct@SubMenu
568    fn is_submenu(&self) -> bool;
569
570    /// Gets a variable that tracks if the sub-menu is open.
571    fn is_submenu_open(&self) -> Option<Var<bool>>;
572
573    /// Gets the sub-menu that spawned `self` if [`is_submenu`], otherwise returns the first ancestor
574    /// that is sub-menu.
575    ///
576    /// Note that the returned widget may not be an actual parent in the info-tree as
577    /// sub-menus use popups to present their sub-menus.
578    ///
579    /// [`is_submenu`]: SubMenuWidgetInfoExt::is_submenu
580    fn submenu_parent(&self) -> Option<WidgetInfo>;
581
582    /// Gets an iterator over sub-menu parents until root.
583    fn submenu_ancestors(&self) -> SubMenuAncestors;
584    /// Gets an iterator over the widget, if it is a sub-menu, and sub-menu parents until root.
585    fn submenu_self_and_ancestors(&self) -> SubMenuAncestors;
586
587    /// Gets the last submenu ancestor.
588    fn submenu_root(&self) -> Option<WidgetInfo>;
589
590    /// Gets the alt-scope parent of the `root_submenu`.
591    ///
592    /// This is `None` if the widget is inside a context menu or not inside.
593    fn menu(&self) -> Option<WidgetInfo>;
594}
595impl SubMenuWidgetInfoExt for WidgetInfo {
596    fn is_submenu(&self) -> bool {
597        self.meta().contains(*SUB_MENU_INFO_ID)
598    }
599
600    fn is_submenu_open(&self) -> Option<Var<bool>> {
601        self.meta().get(*SUB_MENU_INFO_ID).map(|s| s.is_open.read_only())
602    }
603
604    fn submenu_parent(&self) -> Option<WidgetInfo> {
605        if let Some(p) = self.meta().get(*SUB_MENU_INFO_ID) {
606            self.tree().get(p.parent?)
607        } else if let Some(p) = self.ancestors().find(|a| a.is_submenu()) {
608            Some(p)
609        } else if let Some(pop) = self.meta().get(*SUB_MENU_POPUP_ID) {
610            self.tree().get(pop.parent?)
611        } else {
612            for anc in self.ancestors() {
613                if let Some(pop) = anc.meta().get(*SUB_MENU_POPUP_ID) {
614                    if let Some(p) = pop.parent {
615                        return self.tree().get(p);
616                    } else {
617                        // context-menu
618                        return Some(anc);
619                    }
620                }
621            }
622            None
623        }
624    }
625
626    fn submenu_ancestors(&self) -> SubMenuAncestors {
627        SubMenuAncestors {
628            node: self.submenu_parent(),
629        }
630    }
631
632    fn submenu_self_and_ancestors(&self) -> SubMenuAncestors {
633        if self.is_submenu() {
634            SubMenuAncestors { node: Some(self.clone()) }
635        } else {
636            self.submenu_ancestors()
637        }
638    }
639
640    fn submenu_root(&self) -> Option<WidgetInfo> {
641        self.submenu_ancestors().last()
642    }
643
644    fn menu(&self) -> Option<WidgetInfo> {
645        let root = self
646            .submenu_root()
647            .or_else(|| if self.is_submenu() { Some(self.clone()) } else { None })?;
648
649        let scope = root.into_focus_info(true, true).scope()?;
650
651        if !scope.is_alt_scope() {
652            return None;
653        }
654
655        Some(scope.info().clone())
656    }
657}
658
659/// Iterator over sub-menu parents.
660///
661/// See [`submenu_ancestors`] for more details.
662///
663/// [`submenu_ancestors`]: SubMenuWidgetInfoExt::submenu_ancestors
664pub struct SubMenuAncestors {
665    node: Option<WidgetInfo>,
666}
667impl Iterator for SubMenuAncestors {
668    type Item = WidgetInfo;
669
670    fn next(&mut self) -> Option<Self::Item> {
671        if let Some(n) = self.node.take() {
672            self.node = n.submenu_parent();
673            Some(n)
674        } else {
675            None
676        }
677    }
678}
679
680pub(super) struct SubMenuInfo {
681    pub parent: Option<WidgetId>,
682    pub is_open: Var<bool>,
683}
684
685pub(super) struct SubMenuPopupInfo {
686    pub parent: Option<WidgetId>,
687}
688
689context_local! {
690    // only set during info
691    pub(super) static SUB_MENU_PARENT_CTX: Option<WidgetId> = None;
692}
693
694static_id! {
695    pub(super) static ref SUB_MENU_INFO_ID: StateId<SubMenuInfo>;
696    pub(super) static ref SUB_MENU_POPUP_ID: StateId<SubMenuPopupInfo>;
697}
698
699/// Style applied to all [`Button!`] widgets inside [`SubMenu!`] and [`ContextMenu!`].
700///
701/// Gives the button a *menu-item* look.
702///
703/// [`Button!`]: struct@zng_wgt_button::Button
704/// [`SubMenu!`]: struct@SubMenu
705/// [`ContextMenu!`]: struct@crate::context::ContextMenu
706#[widget($crate::sub::ButtonStyle)]
707pub struct ButtonStyle(Style);
708impl ButtonStyle {
709    fn widget_intrinsic(&mut self) {
710        widget_set! {
711            self;
712            replace = true;
713
714            column_width_padding = true;
715            padding = (4, 0);
716            child_align = Align::START;
717
718            base_color = light_dark(rgb(0.82, 0.82, 0.82), rgb(0.18, 0.18, 0.18));
719            background_color = BASE_COLOR_VAR.rgba();
720            opacity = 90.pct();
721            foreground_highlight = unset!;
722            zng_wgt_tooltip::tooltip_fn = WidgetFn::nil(); // cmd sets tooltip
723
724            click_mode = ClickMode::release();// part of press-and-drag to click (see SubMenuPopup)
725
726            access_role = AccessRole::MenuItem;
727
728            on_pre_mouse_enter = hn!(|_| {
729                FOCUS.focus_widget(WIDGET.id(), false);
730            });
731
732            shortcut_txt = ShortcutText! {
733                shortcut = BUTTON.cmd().flat_map(|c| match c {
734                    Some(c) => c.shortcut(),
735                    None => const_var(Shortcuts::default()),
736                });
737                align = Align::CENTER;
738            };
739
740            icon_fn = BUTTON.cmd().flat_map(|c| match c {
741                Some(c) => c.icon(),
742                None => const_var(WidgetFn::nil()),
743            });
744
745            zng_wgt_input::gesture::mnemonic = true;
746            zng_wgt_button::cmd_child_fn = wgt_fn!(|cmd: Command| crate::MENU_TEXT_INPUT.label(cmd.name()));
747
748            when *#is_focused {
749                background_color = BASE_COLOR_VAR.shade(1);
750                opacity = 100.pct();
751            }
752
753            when *#is_disabled {
754                saturate = false;
755                opacity = 50.pct();
756                cursor = CursorIcon::NotAllowed;
757            }
758
759            when *#is_mobile {
760                shortcut_txt = UiNode::nil();
761            }
762        }
763    }
764}
765
766/// Style applied to all [`Button!`] widgets inside [`SubMenu!`] and [`ContextMenu!`] in touch contexts.
767///
768/// Gives the button a *menu-item* look.
769///
770/// [`Button!`]: struct@zng_wgt_button::Button
771/// [`SubMenu!`]: struct@SubMenu
772/// [`ContextMenu!`]: struct@crate::context::ContextMenu
773#[widget($crate::sub::TouchButtonStyle)]
774pub struct TouchButtonStyle(Style);
775impl TouchButtonStyle {
776    fn widget_intrinsic(&mut self) {
777        widget_set! {
778            self;
779            zng_wgt::corner_radius = 0;
780            zng_wgt::visibility =
781                BUTTON
782                    .cmd()
783                    .flat_map(|c| match c {
784                        Some(c) => c.is_enabled(),
785                        None => const_var(true),
786                    })
787                    .map_into(),
788            ;
789        }
790    }
791}
792
793/// Style applied to all [`Toggle!`] widgets inside [`SubMenu!`] and [`ContextMenu!`].
794///
795/// Gives the toggle a *menu-item* look, the check mark is placed in the icon position.
796///
797/// [`Toggle!`]: struct@zng_wgt_toggle::Toggle
798/// [`SubMenu!`]: struct@SubMenu
799/// [`ContextMenu!`]: struct@crate::context::ContextMenu
800#[widget($crate::sub::ToggleStyle)]
801pub struct ToggleStyle(ButtonStyle);
802impl ToggleStyle {
803    fn widget_intrinsic(&mut self) {
804        widget_set! {
805            self;
806            replace = true;
807
808            click_mode = ClickMode::release();
809            access_role = AccessRole::MenuItemCheckBox;
810
811            start_column_fn = wgt_fn!(|_| zng_wgt_text::Text! {
812                size = 1.2.em();
813                font_family = FONTS.generics().system_ui(&lang!(und));
814                align = Align::CENTER;
815
816                txt = "✓";
817                when #{zng_wgt_toggle::IS_CHECKED_VAR}.is_none() {
818                    txt = "━";
819                }
820
821                font_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.transparent());
822                when #{zng_wgt_toggle::IS_CHECKED_VAR}.unwrap_or(true) {
823                    font_color = zng_wgt_text::FONT_COLOR_VAR;
824                }
825            });
826        }
827    }
828}