zng_wgt_menu/
lib.rs

1#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo.png")]
3//!
4//! Menu widgets and properties.
5//!
6//! # Crate
7//!
8#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12zng_wgt::enable_widget_macros!();
13
14use colors::BASE_COLOR_VAR;
15use zng_ext_font::FontNames;
16use zng_ext_input::{focus::FOCUS, mouse::ClickMode};
17use zng_ext_l10n::lang;
18use zng_wgt::{align, base_color, is_disabled, is_mobile, margin, prelude::*};
19use zng_wgt_access::{AccessRole, access_role};
20use zng_wgt_button::BUTTON;
21use zng_wgt_container::{child_align, child_end, padding};
22use zng_wgt_fill::{background_color, foreground_highlight};
23use zng_wgt_filter::{opacity, saturate};
24use zng_wgt_input::{CursorIcon, cursor, focus::alt_focus_scope};
25use zng_wgt_input::{click_mode, focus::is_focused, mouse::on_pre_mouse_enter};
26use zng_wgt_size_offset::size;
27use zng_wgt_style::{Style, StyleMix, impl_style_fn, style_fn};
28use zng_wgt_text::Text;
29
30pub mod context;
31pub mod popup;
32pub mod sub;
33
34/// Menu root panel.
35#[widget($crate::Menu {
36    ($children:expr) => {
37        children = $children;
38    }
39})]
40pub struct Menu(StyleMix<zng_wgt_panel::Panel>);
41impl Menu {
42    fn widget_intrinsic(&mut self) {
43        self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
44        widget_set! {
45            self;
46            alt_focus_scope = true;
47            zng_wgt_panel::panel_fn = PANEL_FN_VAR;
48            style_base_fn = style_fn!(|_| DefaultStyle!());
49            access_role = AccessRole::Menu;
50        }
51    }
52}
53impl_style_fn!(Menu);
54
55context_var! {
56    /// Defines the layout widget for [`Menu!`].
57    ///
58    /// Is a [`Wrap!`] panel by default.
59    ///
60    /// [`Menu!`]: struct@Menu
61    /// [`Wrap!`]: struct@zng_wgt_wrap::Wrap
62    pub static PANEL_FN_VAR: WidgetFn<zng_wgt_panel::PanelArgs> = wgt_fn!(|a: zng_wgt_panel::PanelArgs| {
63        zng_wgt_wrap::Wrap! {
64            children = a.children;
65        }
66    });
67
68    /// Minimum space between a menu item child and the [`shortcut_txt`] child.
69    ///
70    /// The spacing is applied only if the shortcut child is set to a non-zero sized widget and
71    /// there is no other wider menu item.
72    ///
73    /// Is `20` by default.
74    ///
75    /// [`shortcut_txt`]: fn@shortcut_txt
76    pub static SHORTCUT_SPACING_VAR: Length = 20;
77}
78
79/// Widget function that generates the menu layout.
80///
81/// This property can be set in any widget to affect all [`Menu!`] descendants.
82///
83/// This property sets [`PANEL_FN_VAR`].
84///
85/// [`Menu!`]: struct@Menu
86#[property(CONTEXT, default(PANEL_FN_VAR), widget_impl(Menu))]
87pub fn panel_fn(child: impl UiNode, panel: impl IntoVar<WidgetFn<zng_wgt_panel::PanelArgs>>) -> impl UiNode {
88    with_context_var(child, PANEL_FN_VAR, panel)
89}
90
91/// Default [`Menu!`] style.
92///
93/// Gives the button a *menu-item* look.
94///
95/// [`Menu!`]: struct@Menu
96#[widget($crate::DefaultStyle)]
97pub struct DefaultStyle(Style);
98impl DefaultStyle {
99    fn widget_intrinsic(&mut self) {
100        widget_set! {
101            self;
102
103            // also see context::DefaultStyle
104
105            base_color = light_dark(rgb(0.82, 0.82, 0.82), rgb(0.18, 0.18, 0.18));
106            zng_wgt_button::style_fn = style_fn!(|_| ButtonStyle!());
107            zng_wgt_toggle::style_fn = style_fn!(|_| ToggleStyle!());
108            zng_wgt_rule_line::hr::color = BASE_COLOR_VAR.shade(1);
109            zng_wgt_text::icon::ico_size = 18;
110        }
111    }
112}
113
114/// Style applied to all [`Button!`] widgets inside [`Menu!`].
115///
116/// Gives the button a *menu-item* look.
117///
118/// [`Button!`]: struct@zng_wgt_button::Button
119/// [`Menu!`]: struct@Menu
120#[widget($crate::ButtonStyle)]
121pub struct ButtonStyle(Style);
122impl ButtonStyle {
123    fn widget_intrinsic(&mut self) {
124        widget_set! {
125            self;
126            replace = true;
127
128            sub::column_width_padding = true;
129            padding = (4, 0);
130            child_align = Align::START;
131
132            base_color = light_dark(rgb(0.82, 0.82, 0.82), rgb(0.18, 0.18, 0.18));
133            background_color = BASE_COLOR_VAR.rgba();
134            opacity = 90.pct();
135            foreground_highlight = unset!;
136            zng_wgt_tooltip::tooltip_fn = WidgetFn::nil(); // cmd sets tooltip
137
138            click_mode = ClickMode::release();// part of press-and-drag to click (see SubMenuPopup)
139
140            access_role = AccessRole::MenuItem;
141
142            on_pre_mouse_enter = hn!(|_| {
143                FOCUS.focus_widget(WIDGET.id(), false);
144            });
145
146            shortcut_txt = Text! {
147                txt = BUTTON.cmd().flat_map(|c| match c {
148                    Some(c) => c.shortcut_txt(),
149                    None => LocalVar(Txt::from("")).boxed()
150                });
151                align = Align::CENTER;
152            };
153
154            icon_fn = BUTTON.cmd().flat_map(|c| match c {
155                Some(c) => c.icon().boxed(),
156                None => LocalVar(WidgetFn::nil()).boxed()
157            });
158
159            when *#is_focused {
160                background_color = BASE_COLOR_VAR.shade(1);
161                opacity = 100.pct();
162            }
163
164            when *#is_disabled {
165                saturate = false;
166                opacity = 50.pct();
167                cursor = CursorIcon::NotAllowed;
168            }
169
170            when *#is_mobile {
171                shortcut_txt = NilUiNode;
172            }
173        }
174    }
175}
176
177/// Command button for touch.
178///
179/// This a menu button style that has a `cmd` property, it changes the visibility to collapse when the command
180/// is disabled.
181#[widget($crate::TouchButtonStyle)]
182pub struct TouchButtonStyle(Style);
183impl TouchButtonStyle {
184    fn widget_intrinsic(&mut self) {
185        widget_set! {
186            self;
187            zng_wgt::corner_radius = 0;
188            zng_wgt::visibility = BUTTON
189                .cmd()
190                .flat_map(|c| match c {
191                    Some(c) => c.is_enabled().boxed(),
192                    None => LocalVar(true).boxed(),
193                })
194                .map_into();
195        }
196    }
197}
198
199/// Style applied to all [`Button!`] widgets inside [`Menu!`].
200///
201/// Gives the toggle a *menu-item* look, the check mark is placed in the icon position.
202///
203/// [`Button!`]: struct@zng_wgt_button::Button
204/// [`Menu!`]: struct@Menu
205#[widget($crate::ToggleStyle)]
206pub struct ToggleStyle(ButtonStyle);
207impl ToggleStyle {
208    fn widget_intrinsic(&mut self) {
209        widget_set! {
210            self;
211            replace = true;
212
213            click_mode = ClickMode::release();
214            access_role = AccessRole::MenuItemCheckBox;
215
216            sub::start_column_fn = wgt_fn!(|_| Text! {
217                size = 1.2.em();
218                font_family = FontNames::system_ui(&lang!(und));
219                align = Align::CENTER;
220
221                txt = "✓";
222                when #{zng_wgt_toggle::IS_CHECKED_VAR}.is_none() {
223                    txt = "━";
224                }
225
226                font_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.transparent());
227                when #{zng_wgt_toggle::IS_CHECKED_VAR}.unwrap_or(true) {
228                    font_color = zng_wgt_text::FONT_COLOR_VAR;
229                }
230            })
231        }
232    }
233}
234
235/// Menu item icon.
236///
237/// Set on a [`Button!`] inside a sub-menu to define the menu [`Icon!`] for that button.
238///
239/// This property is an alias for [`sub::start_column`].
240///
241/// [`Button!`]: struct@zng_wgt_button::Button
242/// [`Icon!`]: struct@zng_wgt_text::icon::Icon
243/// [`sub::start_column`]: fn@sub::start_column
244#[property(FILL)]
245pub fn icon(child: impl UiNode, icon: impl UiNode) -> impl UiNode {
246    sub::start_column(child, icon)
247}
248
249/// Menu item icon from widget function.
250///
251/// Set on a [`Button!`] inside a sub-menu to define the menu [`Icon!`] for that button.
252///
253/// This property is an alias for [`sub::start_column_fn`].
254///
255/// [`Button!`]: struct@zng_wgt_button::Button
256/// [`Icon!`]: struct@zng_wgt_text::icon::Icon
257/// [`sub::start_column_fn`]: fn@sub::start_column_fn
258#[property(FILL)]
259pub fn icon_fn(child: impl UiNode, icon: impl IntoVar<WidgetFn<()>>) -> impl UiNode {
260    sub::start_column_fn(child, icon)
261}
262
263/// Menu item shortcut text.
264///
265/// Set on a [`Button!`] inside a sub-menu to define the shortcut text.
266///
267/// Note that this does not define the click shortcut, just the display of it.
268///
269/// [`Button!`]: struct@zng_wgt_button::Button
270#[property(CHILD_CONTEXT)]
271pub fn shortcut_txt(child: impl UiNode, shortcut: impl UiNode) -> impl UiNode {
272    let shortcut = margin(shortcut, sub::END_COLUMN_WIDTH_VAR.map(|w| SideOffsets::new(0, w.clone(), 0, 0)));
273    child_end(child, shortcut, SHORTCUT_SPACING_VAR)
274}
275
276/// Minimum space between a menu item child and the [`shortcut_txt`] child.
277///
278/// The spacing is applied only if the shortcut child is set to a non-zero sized widget and
279/// there is no other wider menu item.
280///
281/// This property sets the [`SHORTCUT_SPACING_VAR`].
282///
283/// [`shortcut_txt`]: fn@shortcut_txt
284#[property(CONTEXT, default(SHORTCUT_SPACING_VAR))]
285pub fn shortcut_spacing(child: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
286    with_context_var(child, SHORTCUT_SPACING_VAR, spacing)
287}