zng_wgt_menu/
context.rs

1//! Context menu widget and properties.
2
3use colors::BASE_COLOR_VAR;
4use zng_ext_input::gesture::CLICK_EVENT;
5use zng_wgt::prelude::*;
6use zng_wgt_input::focus::alt_focus_scope;
7use zng_wgt_layer::{
8    AnchorMode,
9    popup::{CONTEXT_CAPTURE_VAR, POPUP, PopupState},
10};
11use zng_wgt_stack::{Stack, StackDirection};
12use zng_wgt_style::{impl_style_fn, style_fn};
13
14/// Defines the context menu shown when the widget is enabled and receives a context click.
15///
16/// The `menu` can be any widget, the [`ContextMenu!`] is recommended. The menu widget is open
17/// using [`POPUP`] and is expected to close itself when the context action is finished or it
18/// loses focus.
19///
20/// [`ContextMenu!`]: struct@ContextMenu
21/// [`POPUP`]: zng_wgt_layer::popup::POPUP
22#[property(EVENT)]
23pub fn context_menu(child: impl UiNode, menu: impl UiNode) -> impl UiNode {
24    context_menu_fn(child, WidgetFn::singleton(menu))
25}
26
27/// Defines the context menu function shown when the widget is enabled and receives a context click.
28///
29/// The `menu` can return any widget, the [`ContextMenu!`] is recommended.
30///
31/// [`ContextMenu!`]: struct@ContextMenu
32#[property(EVENT, default(WidgetFn::nil()))]
33pub fn context_menu_fn(child: impl UiNode, menu: impl IntoVar<WidgetFn<ContextMenuArgs>>) -> impl UiNode {
34    context_menu_node(child, menu, false)
35}
36
37/// Defines the context menu shown when the widget is disabled and receives a context click.
38///
39/// The `menu` can be any widget, the [`ContextMenu!`] is recommended.
40///
41/// [`ContextMenu!`]: struct@ContextMenu
42#[property(EVENT)]
43pub fn disabled_context_menu(child: impl UiNode, menu: impl UiNode) -> impl UiNode {
44    disabled_context_menu_fn(child, WidgetFn::singleton(menu))
45}
46
47/// Defines the context menu function shown when the widget is disabled and receives a context click.
48///
49/// The `menu` can return any widget, the [`ContextMenu!`] is recommended.
50///
51/// [`ContextMenu!`]: struct@ContextMenu
52#[property(EVENT, default(WidgetFn::nil()))]
53pub fn disabled_context_menu_fn(child: impl UiNode, menu: impl IntoVar<WidgetFn<ContextMenuArgs>>) -> impl UiNode {
54    context_menu_node(child, menu, true)
55}
56
57fn context_menu_node(child: impl UiNode, menu: impl IntoVar<WidgetFn<ContextMenuArgs>>, disabled_only: bool) -> impl UiNode {
58    let menu = menu.into_var();
59    let mut pop_state = var(PopupState::Closed).read_only();
60
61    match_node(child, move |c, op| match op {
62        UiNodeOp::Init => {
63            WIDGET.sub_var(&menu).sub_event(&CLICK_EVENT);
64        }
65        UiNodeOp::Deinit => {
66            POPUP.close(&pop_state);
67        }
68        UiNodeOp::Event { update } => {
69            c.event(update);
70            if let Some(args) = CLICK_EVENT.on_unhandled(update) {
71                if args.is_context() {
72                    let apply = if disabled_only {
73                        args.target.interactivity().is_disabled()
74                    } else {
75                        args.target.interactivity().is_enabled()
76                    };
77                    if apply {
78                        args.propagation().stop();
79
80                        let menu = menu.get()(ContextMenuArgs {
81                            anchor_id: WIDGET.id(),
82                            disabled: disabled_only,
83                        });
84                        let is_shortcut = args.is_from_keyboard();
85                        pop_state = POPUP.open_config(
86                            menu,
87                            CONTEXT_MENU_ANCHOR_VAR.map_ref(move |(c, s)| if is_shortcut { s } else { c }),
88                            CONTEXT_CAPTURE_VAR.get(),
89                        );
90                    }
91                }
92            }
93        }
94        _ => {}
95    })
96}
97
98/// Set the position of the context-menu widgets opened for the widget or its descendants.
99///
100/// This property defines two positions, `(click, shortcut)`, the first is used for context clicks
101/// from a pointer device, the second is used for context clicks from keyboard shortcut.
102///
103/// By default tips are aligned to cursor position at the time they are opened or the top for shortcut.
104///
105/// This property sets the [`CONTEXT_MENU_ANCHOR_VAR`].
106#[property(CONTEXT, default(CONTEXT_MENU_ANCHOR_VAR))]
107pub fn context_menu_anchor(child: impl UiNode, click_shortcut: impl IntoVar<(AnchorMode, AnchorMode)>) -> impl UiNode {
108    with_context_var(child, CONTEXT_MENU_ANCHOR_VAR, click_shortcut)
109}
110
111/// Context menu popup.
112///
113/// This widget can be set in [`context_menu`] to define a popup menu that shows when the widget receives
114/// a context click.
115///
116/// [`context_menu`]: fn@context_menu
117#[widget($crate::context::ContextMenu {
118    ($children:expr) => {
119        children = $children;
120    }
121})]
122pub struct ContextMenu(crate::popup::SubMenuPopup);
123impl ContextMenu {
124    fn widget_intrinsic(&mut self) {
125        self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
126        widget_set! {
127            self;
128            alt_focus_scope = true;
129            style_base_fn = style_fn!(|_| DefaultStyle!());
130        }
131    }
132}
133impl_style_fn!(ContextMenu);
134
135/// Arguments for context menu widget functions.
136pub struct ContextMenuArgs {
137    /// ID of the widget the menu is anchored to.
138    pub anchor_id: WidgetId,
139
140    /// Is `true` if the menu is for [`disabled_context_menu_fn`], is `false` for [`context_menu_fn`].
141    ///
142    /// [`context_menu_fn`]: fn@context_menu_fn
143    /// [`disabled_context_menu_fn`]: fn@disabled_context_menu_fn
144    pub disabled: bool,
145}
146
147context_var! {
148    /// Position of the context widget in relation to the anchor widget.
149    ///
150    /// By default the context widget is shown at the cursor.
151    pub static CONTEXT_MENU_ANCHOR_VAR: (AnchorMode, AnchorMode) = (AnchorMode::context_menu(), AnchorMode::context_menu_shortcut());
152
153    /// Defines the layout widget for [`ContextMenu!`].
154    ///
155    /// Is [`popup::default_panel_fn`] by default.
156    ///
157    /// [`ContextMenu!`]: struct@ContextMenu
158    /// [`popup::default_panel_fn`]: crate::popup::default_panel_fn
159    pub static PANEL_FN_VAR: WidgetFn<zng_wgt_panel::PanelArgs> = WidgetFn::new(crate::popup::default_panel_fn);
160}
161
162/// Widget function that generates the context menu layout.
163///
164/// This property sets [`PANEL_FN_VAR`].
165#[property(CONTEXT, default(PANEL_FN_VAR), widget_impl(ContextMenu))]
166pub fn panel_fn(child: impl UiNode, panel: impl IntoVar<WidgetFn<zng_wgt_panel::PanelArgs>>) -> impl UiNode {
167    with_context_var(child, PANEL_FN_VAR, panel)
168}
169
170/// Context menu popup default style.
171#[widget($crate::context::DefaultStyle)]
172pub struct DefaultStyle(crate::popup::DefaultStyle);
173impl DefaultStyle {
174    fn widget_intrinsic(&mut self) {
175        widget_set! {
176            self;
177            replace = true;
178            zng_wgt_button::style_fn = style_fn!(|_| super::ButtonStyle!());
179            zng_wgt_toggle::style_fn = style_fn!(|_| super::ToggleStyle!());
180            zng_wgt_rule_line::hr::color = BASE_COLOR_VAR.shade(1);
181            zng_wgt_text::icon::ico_size = 18;
182        }
183    }
184}
185
186/// Touch context menu popup default style.
187#[widget($crate::context::TouchStyle)]
188pub struct TouchStyle(crate::popup::DefaultStyle);
189impl TouchStyle {
190    fn widget_intrinsic(&mut self) {
191        widget_set! {
192            self;
193
194            panel_fn = wgt_fn!(|args: zng_wgt_panel::PanelArgs| Stack! {
195                direction = StackDirection::left_to_right();
196                children = args.children;
197            });
198            zng_wgt_button::style_fn = style_fn!(|_| super::TouchButtonStyle!());
199            zng_wgt_rule_line::vr::color = BASE_COLOR_VAR.shade(1);
200        }
201    }
202}