zng_wgt_button/
lib.rs

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