1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
//! Context menu widget and properties.

use colors::BASE_COLOR_VAR;
use zng_ext_input::gesture::CLICK_EVENT;
use zng_wgt::prelude::*;
use zng_wgt_input::focus::alt_focus_scope;
use zng_wgt_layer::{
    popup::{PopupState, CONTEXT_CAPTURE_VAR, POPUP},
    AnchorMode,
};
use zng_wgt_stack::{Stack, StackDirection};
use zng_wgt_style::{impl_style_fn, style_fn};

/// Defines the context menu shown when the widget is enabled and receives a context click.
///
/// The `menu` can be any widget, the [`ContextMenu!`] is recommended. The menu widget is open
/// using [`POPUP`] and is expected to close itself when the context action is finished or it
/// loses focus.
///
/// [`ContextMenu!`]: struct@ContextMenu
/// [`POPUP`]: zng_wgt_layer::popup::POPUP
#[property(EVENT)]
pub fn context_menu(child: impl UiNode, menu: impl UiNode) -> impl UiNode {
    context_menu_fn(child, WidgetFn::singleton(menu))
}

/// Defines the context menu function shown when the widget is enabled and receives a context click.
///
/// The `menu` can return any widget, the [`ContextMenu!`] is recommended.
///
/// [`ContextMenu!`]: struct@ContextMenu
#[property(EVENT, default(WidgetFn::nil()))]
pub fn context_menu_fn(child: impl UiNode, menu: impl IntoVar<WidgetFn<ContextMenuArgs>>) -> impl UiNode {
    context_menu_node(child, menu, false)
}

/// Defines the context menu shown when the widget is disabled and receives a context click.
///
/// The `menu` can be any widget, the [`ContextMenu!`] is recommended.
///
/// [`ContextMenu!`]: struct@ContextMenu
#[property(EVENT)]
pub fn disabled_context_menu(child: impl UiNode, menu: impl UiNode) -> impl UiNode {
    disabled_context_menu_fn(child, WidgetFn::singleton(menu))
}

/// Defines the context menu function shown when the widget is disabled and receives a context click.
///
/// The `menu` can return any widget, the [`ContextMenu!`] is recommended.
///
/// [`ContextMenu!`]: struct@ContextMenu
#[property(EVENT, default(WidgetFn::nil()))]
pub fn disabled_context_menu_fn(child: impl UiNode, menu: impl IntoVar<WidgetFn<ContextMenuArgs>>) -> impl UiNode {
    context_menu_node(child, menu, true)
}

fn context_menu_node(child: impl UiNode, menu: impl IntoVar<WidgetFn<ContextMenuArgs>>, disabled_only: bool) -> impl UiNode {
    let menu = menu.into_var();
    let mut pop_state = var(PopupState::Closed).read_only();

    match_node(child, move |c, op| match op {
        UiNodeOp::Init => {
            WIDGET.sub_var(&menu).sub_event(&CLICK_EVENT);
        }
        UiNodeOp::Deinit => {
            POPUP.close(&pop_state);
        }
        UiNodeOp::Event { update } => {
            c.event(update);
            if let Some(args) = CLICK_EVENT.on_unhandled(update) {
                if args.is_context() {
                    let apply = if disabled_only {
                        args.target.interactivity().is_disabled()
                    } else {
                        args.target.interactivity().is_enabled()
                    };
                    if apply {
                        args.propagation().stop();

                        let menu = menu.get()(ContextMenuArgs {
                            anchor_id: WIDGET.id(),
                            disabled: disabled_only,
                        });
                        let is_shortcut = args.is_from_keyboard();
                        pop_state = POPUP.open_config(
                            menu,
                            CONTEXT_MENU_ANCHOR_VAR.map_ref(move |(c, s)| if is_shortcut { s } else { c }),
                            CONTEXT_CAPTURE_VAR.get(),
                        );
                    }
                }
            }
        }
        _ => {}
    })
}

/// Set the position of the context-menu widgets opened for the widget or its descendants.
///
/// This property defines two positions, `(click, shortcut)`, the first is used for context clicks
/// from a pointer device, the second is used for context clicks from keyboard shortcut.
///
/// By default tips are aligned to cursor position at the time they are opened or the top for shortcut.
///
/// This property sets the [`CONTEXT_MENU_ANCHOR_VAR`].
#[property(CONTEXT, default(CONTEXT_MENU_ANCHOR_VAR))]
pub fn context_menu_anchor(child: impl UiNode, click_shortcut: impl IntoVar<(AnchorMode, AnchorMode)>) -> impl UiNode {
    with_context_var(child, CONTEXT_MENU_ANCHOR_VAR, click_shortcut)
}

/// Context menu popup.
///
/// This widget can be set in [`context_menu`] to define a popup menu that shows when the widget receives
/// a context click.
///
/// [`context_menu`]: fn@context_menu
#[widget($crate::context::ContextMenu {
    ($children:expr) => {
        children = $children;
    }
})]
pub struct ContextMenu(crate::popup::SubMenuPopup);
impl ContextMenu {
    fn widget_intrinsic(&mut self) {
        self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
        widget_set! {
            self;
            alt_focus_scope = true;
            style_base_fn = style_fn!(|_| DefaultStyle!());
        }
    }
}
impl_style_fn!(ContextMenu);

/// Arguments for context menu widget functions.
pub struct ContextMenuArgs {
    /// ID of the widget the menu is anchored to.
    pub anchor_id: WidgetId,

    /// Is `true` if the menu is for [`disabled_context_menu_fn`], is `false` for [`context_menu_fn`].
    ///
    /// [`context_menu_fn`]: fn@context_menu_fn
    /// [`disabled_context_menu_fn`]: fn@disabled_context_menu_fn
    pub disabled: bool,
}

context_var! {
    /// Position of the context widget in relation to the anchor widget.
    ///
    /// By default the context widget is shown at the cursor.
    pub static CONTEXT_MENU_ANCHOR_VAR: (AnchorMode, AnchorMode) = (AnchorMode::context_menu(), AnchorMode::context_menu_shortcut());

    /// Defines the layout widget for [`ContextMenu!`].
    ///
    /// Is [`popup::default_panel_fn`] by default.
    ///
    /// [`ContextMenu!`]: struct@ContextMenu
    /// [`popup::default_panel_fn`]: crate::popup::default_panel_fn
    pub static PANEL_FN_VAR: WidgetFn<zng_wgt_panel::PanelArgs> = WidgetFn::new(crate::popup::default_panel_fn);
}

/// Widget function that generates the context menu layout.
///
/// This property sets [`PANEL_FN_VAR`].
#[property(CONTEXT, default(PANEL_FN_VAR), widget_impl(ContextMenu))]
pub fn panel_fn(child: impl UiNode, panel: impl IntoVar<WidgetFn<zng_wgt_panel::PanelArgs>>) -> impl UiNode {
    with_context_var(child, PANEL_FN_VAR, panel)
}

/// Context menu popup default style.
#[widget($crate::context::DefaultStyle)]
pub struct DefaultStyle(crate::popup::DefaultStyle);
impl DefaultStyle {
    fn widget_intrinsic(&mut self) {
        widget_set! {
            self;
            replace = true;
            zng_wgt_button::style_fn = style_fn!(|_| super::ButtonStyle!());
            zng_wgt_toggle::style_fn = style_fn!(|_| super::ToggleStyle!());
            zng_wgt_rule_line::hr::color = BASE_COLOR_VAR.shade(1);
            zng_wgt_text::icon::ico_size = 18;
        }
    }
}

/// Touch context menu popup default style.
#[widget($crate::context::TouchStyle)]
pub struct TouchStyle(crate::popup::DefaultStyle);
impl TouchStyle {
    fn widget_intrinsic(&mut self) {
        widget_set! {
            self;

            panel_fn = wgt_fn!(|args: zng_wgt_panel::PanelArgs| Stack! {
                direction = StackDirection::left_to_right();
                children = args.children;
            });
            zng_wgt_button::style_fn = style_fn!(|_| super::TouchButtonStyle!());
            zng_wgt_rule_line::vr::color = BASE_COLOR_VAR.shade(1);
        }
    }
}