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