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