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_named_style_fn, 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 IntoUiNode, menu: impl IntoUiNode) -> 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 IntoUiNode, menu: impl IntoVar<WidgetFn<ContextMenuArgs>>) -> 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 IntoUiNode, menu: impl IntoUiNode) -> 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 IntoUiNode, menu: impl IntoVar<WidgetFn<ContextMenuArgs>>) -> UiNode {
54    context_menu_node(child, menu, true)
55}
56
57fn context_menu_node(child: impl IntoUiNode, menu: impl IntoVar<WidgetFn<ContextMenuArgs>>, disabled_only: bool) -> 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                && args.is_context()
72            {
73                let apply = if disabled_only {
74                    args.target.interactivity().is_disabled()
75                } else {
76                    args.target.interactivity().is_enabled()
77                };
78                if apply {
79                    args.propagation().stop();
80
81                    let menu = menu.get()(ContextMenuArgs {
82                        anchor_id: WIDGET.id(),
83                        disabled: disabled_only,
84                    });
85                    let is_shortcut = args.is_from_keyboard();
86                    pop_state = POPUP.open_config(
87                        menu,
88                        CONTEXT_MENU_ANCHOR_VAR.map(move |(c, s)| if is_shortcut { s } else { c }.clone()),
89                        CONTEXT_CAPTURE_VAR.get(),
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 IntoUiNode, click_shortcut: impl IntoVar<(AnchorMode, AnchorMode)>) -> 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            zng_wgt_rule_line::collapse_scope = true;
130        }
131    }
132}
133impl_style_fn!(ContextMenu, DefaultStyle);
134
135/// Arguments for context menu widget functions.
136#[non_exhaustive]
137pub struct ContextMenuArgs {
138    /// ID of the widget the menu is anchored to.
139    pub anchor_id: WidgetId,
140
141    /// Is `true` if the menu is for [`disabled_context_menu_fn`], is `false` for [`context_menu_fn`].
142    ///
143    /// [`context_menu_fn`]: fn@context_menu_fn
144    /// [`disabled_context_menu_fn`]: fn@disabled_context_menu_fn
145    pub disabled: bool,
146}
147impl ContextMenuArgs {
148    /// New args.
149    pub fn new(anchor_id: impl Into<WidgetId>, disabled: bool) -> Self {
150        Self {
151            anchor_id: anchor_id.into(),
152            disabled,
153        }
154    }
155}
156
157context_var! {
158    /// Position of the context widget in relation to the anchor widget.
159    ///
160    /// By default the context widget is shown at the cursor.
161    pub static CONTEXT_MENU_ANCHOR_VAR: (AnchorMode, AnchorMode) = (AnchorMode::context_menu(), AnchorMode::context_menu_shortcut());
162
163    /// Defines the layout widget for [`ContextMenu!`].
164    ///
165    /// Is [`popup::default_panel_fn`] by default.
166    ///
167    /// [`ContextMenu!`]: struct@ContextMenu
168    /// [`popup::default_panel_fn`]: crate::popup::default_panel_fn
169    pub static PANEL_FN_VAR: WidgetFn<zng_wgt_panel::PanelArgs> = WidgetFn::new(crate::popup::default_panel_fn);
170}
171
172/// Widget function that generates the context menu layout.
173///
174/// This property sets [`PANEL_FN_VAR`].
175#[property(CONTEXT, default(PANEL_FN_VAR), widget_impl(ContextMenu, DefaultStyle))]
176pub fn panel_fn(child: impl IntoUiNode, panel: impl IntoVar<WidgetFn<zng_wgt_panel::PanelArgs>>) -> UiNode {
177    with_context_var(child, PANEL_FN_VAR, panel)
178}
179
180/// Context menu popup default style.
181#[widget($crate::context::DefaultStyle)]
182pub struct DefaultStyle(crate::popup::DefaultStyle);
183impl DefaultStyle {
184    fn widget_intrinsic(&mut self) {
185        widget_set! {
186            self;
187            replace = true;
188            zng_wgt_button::style_fn = style_fn!(|_| super::sub::ButtonStyle!());
189            zng_wgt_toggle::style_fn = style_fn!(|_| super::sub::ToggleStyle!());
190            zng_wgt_rule_line::hr::color = BASE_COLOR_VAR.shade(1);
191            zng_wgt_rule_line::vr::color = BASE_COLOR_VAR.shade(1);
192            zng_wgt_rule_line::vr::height = 1.em();
193            zng_wgt_text::icon::ico_size = 18;
194        }
195    }
196}
197
198/// Touch context menu popup default style.
199#[widget($crate::context::TouchStyle)]
200pub struct TouchStyle(crate::popup::DefaultStyle);
201impl_named_style_fn!(touch, TouchStyle);
202impl TouchStyle {
203    fn widget_intrinsic(&mut self) {
204        widget_set! {
205            self;
206            named_style_fn = TOUCH_STYLE_FN_VAR;
207
208            panel_fn = wgt_fn!(|args: zng_wgt_panel::PanelArgs| Stack! {
209                direction = StackDirection::left_to_right();
210                children = args.children;
211            });
212            zng_wgt_button::style_fn = style_fn!(|_| super::sub::TouchButtonStyle!());
213        }
214    }
215}