zng_wgt_button/
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//! Button widget.
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 std::any::TypeId;
15
16use colors::{ACCENT_COLOR_VAR, BASE_COLOR_VAR};
17use zng_app::event::CommandParam;
18use zng_var::ReadOnlyContextVar;
19use zng_wgt::{base_color, border, corner_radius, is_disabled, prelude::*};
20use zng_wgt_access::{AccessRole, access_role, labelled_by_child};
21use zng_wgt_container::{Container, child_align, padding};
22use zng_wgt_fill::background_color;
23use zng_wgt_filter::{child_opacity, saturate};
24use zng_wgt_input::{
25    CursorIcon, cursor,
26    focus::FocusableMix,
27    gesture::{ClickArgs, on_click, on_disabled_click},
28    is_cap_hovered, is_pressed,
29    pointer_capture::{CaptureMode, capture_pointer},
30};
31use zng_wgt_style::{Style, StyleMix, impl_style_fn, style_fn};
32use zng_wgt_text::{FONT_COLOR_VAR, Text, font_color, txt_selectable_alt_only, underline};
33
34#[cfg(feature = "tooltip")]
35use zng_wgt_tooltip::{Tip, TooltipArgs, tooltip, tooltip_fn};
36
37/// A clickable container.
38///
39/// # Shorthand
40///
41/// The `Button!` macro provides a shorthand init that sets the command, `Button!(SOME_CMD)`.
42#[widget($crate::Button {
43    ($cmd:expr) => {
44        cmd = $cmd;
45    };
46})]
47pub struct Button(FocusableMix<StyleMix<Container>>);
48impl Button {
49    fn widget_intrinsic(&mut self) {
50        self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
51
52        widget_set! {
53            self;
54            style_base_fn = style_fn!(|_| DefaultStyle!());
55            capture_pointer = true;
56            labelled_by_child = true;
57            txt_selectable_alt_only = true;
58        }
59
60        self.widget_builder().push_build_action(|wgt| {
61            if let Some(cmd) = wgt.capture_var::<Command>(property_id!(Self::cmd)) {
62                if wgt.property(property_id!(Self::child)).is_none() {
63                    wgt.set_child(presenter(cmd.clone(), CMD_CHILD_FN_VAR));
64                }
65
66                let enabled = wgt.property(property_id!(zng_wgt::enabled)).is_none();
67                let visibility = wgt.property(property_id!(zng_wgt::visibility)).is_none();
68                wgt.push_intrinsic(
69                    NestGroup::CONTEXT,
70                    "cmd-context",
71                    clmv!(cmd, |mut child| {
72                        if enabled {
73                            child = zng_wgt::enabled(child, cmd.flat_map(|c| c.is_enabled())).boxed();
74                        }
75                        if visibility {
76                            child = zng_wgt::visibility(child, cmd.flat_map(|c| c.has_handlers()).map_into()).boxed();
77                        }
78
79                        with_context_var(child, CMD_VAR, cmd.map(|c| Some(*c)))
80                    }),
81                );
82
83                let on_click = wgt.property(property_id!(Self::on_click)).is_none();
84                let on_disabled_click = wgt.property(property_id!(on_disabled_click)).is_none();
85                #[cfg(feature = "tooltip")]
86                let tooltip = wgt.property(property_id!(tooltip)).is_none() && wgt.property(property_id!(tooltip_fn)).is_none();
87                #[cfg(not(feature = "tooltip"))]
88                let tooltip = false;
89                if on_click || on_disabled_click || tooltip {
90                    wgt.push_intrinsic(
91                        NestGroup::EVENT,
92                        "cmd-event",
93                        clmv!(cmd, |mut child| {
94                            if on_click {
95                                child = self::on_click(
96                                    child,
97                                    hn!(cmd, |args: &ClickArgs| {
98                                        let cmd = cmd.get();
99                                        if cmd.is_enabled_value() {
100                                            if let Some(param) = CMD_PARAM_VAR.get() {
101                                                cmd.notify_param(param);
102                                            } else {
103                                                cmd.notify();
104                                            }
105                                            args.propagation().stop();
106                                        }
107                                    }),
108                                )
109                                .boxed();
110                            }
111                            if on_disabled_click {
112                                child = self::on_disabled_click(
113                                    child,
114                                    hn!(cmd, |args: &ClickArgs| {
115                                        let cmd = cmd.get();
116                                        if !cmd.is_enabled_value() {
117                                            if let Some(param) = CMD_PARAM_VAR.get() {
118                                                cmd.notify_param(param);
119                                            } else {
120                                                cmd.notify();
121                                            }
122                                            args.propagation().stop();
123                                        }
124                                    }),
125                                )
126                                .boxed();
127                            }
128                            #[cfg(feature = "tooltip")]
129                            if tooltip {
130                                child = self::tooltip_fn(
131                                    child,
132                                    merge_var!(cmd, CMD_TOOLTIP_FN_VAR, |cmd, tt_fn| {
133                                        if tt_fn.is_nil() {
134                                            WidgetFn::nil()
135                                        } else {
136                                            wgt_fn!(cmd, tt_fn, |tooltip| { tt_fn(CmdTooltipArgs { tooltip, cmd }) })
137                                        }
138                                    }),
139                                )
140                                .boxed();
141                            }
142                            child
143                        }),
144                    );
145                }
146            }
147        });
148    }
149
150    widget_impl! {
151        /// Button click event.
152        pub on_click(handler: impl WidgetHandler<ClickArgs>);
153
154        /// If pointer interaction with other widgets is blocked while the button is pressed.
155        ///
156        /// Enabled by default in this widget.
157        pub capture_pointer(mode: impl IntoVar<CaptureMode>);
158    }
159}
160impl_style_fn!(Button);
161
162context_var! {
163    /// Optional parameter for the button to use when notifying command.
164    pub static CMD_PARAM_VAR: Option<CommandParam> = None;
165
166    /// Widget function used when `cmd` is set and `child` is not.
167    pub static CMD_CHILD_FN_VAR: WidgetFn<Command> = WidgetFn::new(default_cmd_child_fn);
168
169    /// Widget function used when `cmd` is set and `tooltip_fn`, `tooltip` are not set.
170    #[cfg(feature = "tooltip")]
171    pub static CMD_TOOLTIP_FN_VAR: WidgetFn<CmdTooltipArgs> = WidgetFn::new(default_cmd_tooltip_fn);
172
173    static CMD_VAR: Option<Command> = None;
174}
175
176#[cfg(feature = "tooltip")]
177/// Arguments for [`cmd_tooltip_fn`].
178///
179/// [`cmd_tooltip_fn`]: fn@cmd_tooltip_fn
180#[derive(Clone)]
181#[non_exhaustive]
182pub struct CmdTooltipArgs {
183    /// The tooltip arguments.
184    pub tooltip: TooltipArgs,
185    /// The command.
186    pub cmd: Command,
187}
188
189#[cfg(feature = "tooltip")]
190impl CmdTooltipArgs {
191    /// New args.
192    pub fn new(tooltip: TooltipArgs, cmd: Command) -> Self {
193        Self { tooltip, cmd }
194    }
195}
196#[cfg(feature = "tooltip")]
197impl std::ops::Deref for CmdTooltipArgs {
198    type Target = TooltipArgs;
199
200    fn deref(&self) -> &Self::Target {
201        &self.tooltip
202    }
203}
204
205/// Default [`CMD_CHILD_FN_VAR`].
206pub fn default_cmd_child_fn(cmd: Command) -> impl UiNode {
207    Text!(cmd.name())
208}
209
210#[cfg(feature = "tooltip")]
211/// Default [`CMD_TOOLTIP_FN_VAR`].
212pub fn default_cmd_tooltip_fn(args: CmdTooltipArgs) -> impl UiNode {
213    let info = args.cmd.info();
214    let has_info = info.map(|s| !s.is_empty());
215    let shortcut = args.cmd.shortcut().map(|s| match s.first() {
216        Some(s) => s.to_txt(),
217        None => Txt::from(""),
218    });
219    let has_shortcut = shortcut.map(|s| !s.is_empty());
220    Tip! {
221        child = Text! {
222            zng_wgt::visibility = has_info.map_into();
223            txt = info;
224        };
225        child_bottom = {
226            node: Text! {
227                font_weight = zng_ext_font::FontWeight::BOLD;
228                zng_wgt::visibility = has_shortcut.map_into();
229                txt = shortcut;
230            },
231            spacing: 4,
232        };
233
234        zng_wgt::visibility = expr_var!((*#{has_info} || *#{has_shortcut}).into())
235    }
236}
237
238/// Sets the [`Command`] the button represents.
239///
240/// When this is set the button widget sets these properties if they are not set:
241///
242/// * [`child`]: Set to a widget produced by [`cmd_child_fn`](fn@cmd_child_fn), by default is `Text!(cmd.name())`.
243/// * [`tooltip_fn`]: Set to a widget function provided by [`cmd_tooltip_fn`](fn@cmd_tooltip_fn), by default it
244///    shows the command info and first shortcut.
245/// * [`enabled`]: Set to `cmd.is_enabled()`.
246/// * [`visibility`]: Set to `cmd.has_handlers().into()`.
247/// * [`on_click`]: Set to a handler that notifies the command if `cmd.is_enabled()`.
248/// * [`on_disabled_click`]: Set to a handler that notifies the command if `!cmd.is_enabled()`.
249///
250/// [`child`]: struct@Container#method.child
251/// [`tooltip_fn`]: fn@tooltip_fn
252/// [`Command`]: zng_app::event::Command
253/// [`enabled`]: fn@zng_wgt::enabled
254/// [`visibility`]: fn@zng_wgt::visibility
255/// [`on_click`]: fn@on_click
256/// [`on_disabled_click`]: fn@on_disabled_click
257#[property(CHILD, capture, widget_impl(Button))]
258pub fn cmd(cmd: impl IntoVar<Command>) {}
259
260/// Optional command parameter for the button to use when notifying [`cmd`].
261///
262/// If `T` is `Option<CommandParam>` the param can be dynamically unset, otherwise the value is the param.
263///
264/// [`cmd`]: fn@cmd
265#[property(CONTEXT, default(CMD_PARAM_VAR), widget_impl(Button))]
266pub fn cmd_param<T: VarValue>(child: impl UiNode, cmd_param: impl IntoVar<T>) -> impl UiNode {
267    if TypeId::of::<T>() == TypeId::of::<Option<CommandParam>>() {
268        let cmd_param = *cmd_param
269            .into_var()
270            .boxed_any()
271            .double_boxed_any()
272            .downcast::<BoxedVar<Option<CommandParam>>>()
273            .unwrap();
274        with_context_var(child, CMD_PARAM_VAR, cmd_param).boxed()
275    } else {
276        with_context_var(
277            child,
278            CMD_PARAM_VAR,
279            cmd_param.into_var().map(|p| Some(CommandParam::new(p.clone()))),
280        )
281        .boxed()
282    }
283}
284
285/// Sets the widget function used to produce the button child when [`cmd`] is set and [`child`] is not.
286///
287/// [`cmd`]: fn@cmd
288/// [`child`]: fn@zng_wgt_container::child
289#[property(CONTEXT, default(CMD_CHILD_FN_VAR), widget_impl(Button))]
290pub fn cmd_child_fn(child: impl UiNode, cmd_child: impl IntoVar<WidgetFn<Command>>) -> impl UiNode {
291    with_context_var(child, CMD_CHILD_FN_VAR, cmd_child)
292}
293
294#[cfg(feature = "tooltip")]
295/// Sets the widget function used to produce the button tooltip when [`cmd`] is set and tooltip is not.
296///
297/// [`cmd`]: fn@cmd
298#[property(CONTEXT, default(CMD_TOOLTIP_FN_VAR), widget_impl(Button))]
299pub fn cmd_tooltip_fn(child: impl UiNode, cmd_tooltip: impl IntoVar<WidgetFn<CmdTooltipArgs>>) -> impl UiNode {
300    with_context_var(child, CMD_TOOLTIP_FN_VAR, cmd_tooltip)
301}
302
303/// Button default style.
304#[widget($crate::DefaultStyle)]
305pub struct DefaultStyle(Style);
306impl DefaultStyle {
307    fn widget_intrinsic(&mut self) {
308        widget_set! {
309            self;
310
311            replace = true;
312
313            access_role = AccessRole::Button;
314
315            padding = (7, 15);
316            corner_radius = 4;
317            child_align = Align::CENTER;
318
319            base_color = light_dark(rgb(0.82, 0.82, 0.82), rgb(0.18, 0.18, 0.18));
320
321            #[easing(150.ms())]
322            background_color = BASE_COLOR_VAR.rgba();
323            #[easing(150.ms())]
324            border = {
325                widths: 1,
326                sides: BASE_COLOR_VAR.rgba_into(),
327            };
328
329            when *#is_cap_hovered {
330                #[easing(0.ms())]
331                background_color = BASE_COLOR_VAR.shade(1);
332                #[easing(0.ms())]
333                border = {
334                    widths: 1,
335                    sides: BASE_COLOR_VAR.shade_into(2),
336                };
337            }
338
339            when *#is_pressed {
340                #[easing(0.ms())]
341                background_color = BASE_COLOR_VAR.shade(2);
342            }
343
344            when *#is_disabled {
345                saturate = false;
346                child_opacity = 50.pct();
347                cursor = CursorIcon::NotAllowed;
348            }
349        }
350    }
351}
352
353/// Primary button style.
354#[widget($crate::PrimaryStyle)]
355pub struct PrimaryStyle(DefaultStyle);
356impl PrimaryStyle {
357    fn widget_intrinsic(&mut self) {
358        widget_set! {
359            self;
360
361            base_color = ACCENT_COLOR_VAR.map(|c| c.shade(-2));
362            zng_wgt_text::font_weight = zng_ext_font::FontWeight::BOLD;
363        }
364    }
365}
366
367/// Button light style.
368#[widget($crate::LightStyle)]
369pub struct LightStyle(DefaultStyle);
370impl LightStyle {
371    fn widget_intrinsic(&mut self) {
372        widget_set! {
373            self;
374            border = unset!;
375            padding = 7;
376
377            #[easing(150.ms())]
378            background_color = FONT_COLOR_VAR.map(|c| c.with_alpha(0.pct()));
379
380            when *#is_cap_hovered {
381                #[easing(0.ms())]
382                background_color = FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()));
383            }
384
385            when *#is_pressed {
386                #[easing(0.ms())]
387                background_color = FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
388            }
389
390            when *#is_disabled {
391                saturate = false;
392                child_opacity = 50.pct();
393                cursor = CursorIcon::NotAllowed;
394            }
395        }
396    }
397}
398
399/// Button link style.
400///
401/// Looks like a web hyperlink.
402#[widget($crate::LinkStyle)]
403pub struct LinkStyle(Style);
404impl LinkStyle {
405    fn widget_intrinsic(&mut self) {
406        widget_set! {
407            self;
408            replace = true;
409
410            font_color = light_dark(colors::BLUE, web_colors::LIGHT_BLUE);
411            cursor = CursorIcon::Pointer;
412            access_role = AccessRole::Link;
413
414            when *#is_cap_hovered {
415                underline = 1, LineStyle::Solid;
416            }
417
418            when *#is_pressed {
419                font_color = light_dark(web_colors::BROWN, colors::YELLOW);
420            }
421
422            when *#is_disabled {
423                saturate = false;
424                child_opacity = 50.pct();
425                cursor = CursorIcon::NotAllowed;
426            }
427        }
428    }
429}
430
431/// Button context.
432pub struct BUTTON;
433impl BUTTON {
434    /// The [`cmd`] value, if set.
435    ///
436    /// [`cmd`]: fn@cmd
437    pub fn cmd(&self) -> ReadOnlyContextVar<Option<Command>> {
438        CMD_VAR.read_only()
439    }
440
441    /// The [`cmd_param`] value.
442    ///
443    /// [`cmd_param`]: fn@cmd_param
444    pub fn cmd_param(&self) -> ReadOnlyContextVar<Option<CommandParam>> {
445        CMD_PARAM_VAR.read_only()
446    }
447}