Skip to main content

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},
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 `Label!(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///
154/// [`child`]: struct@Container#method.child
155/// [`tooltip_fn`]: fn@tooltip_fn
156/// [`Command`]: zng_app::event::Command
157/// [`enabled`]: fn@zng_wgt::enabled
158/// [`visibility`]: fn@zng_wgt::visibility
159/// [`on_click`]: fn@on_click
160#[property(CHILD, widget_impl(Button))]
161pub fn cmd(wgt: &mut WidgetBuilding, cmd: impl IntoVar<Command>) {
162    let cmd = cmd.into_var();
163
164    if wgt.property(property_id!(zng_wgt_container::child)).is_none() {
165        wgt.set_child(cmd.present(CMD_CHILD_FN_VAR));
166    }
167
168    let enabled = wgt.property(property_id!(zng_wgt::enabled)).is_none();
169    let visibility = wgt.property(property_id!(zng_wgt::visibility)).is_none();
170    wgt.push_intrinsic(
171        NestGroup::CONTEXT,
172        "cmd_context",
173        clmv!(cmd, |mut child| {
174            if enabled {
175                child = zng_wgt::enabled(child, cmd.flat_map(|c| c.is_enabled()));
176            }
177            if visibility {
178                child = zng_wgt::visibility(child, cmd.flat_map(|c| c.has_handlers()).map_into());
179            }
180
181            with_context_var(child, CMD_VAR, cmd.map(|c| Some(*c)))
182        }),
183    );
184
185    let on_click = wgt.property(property_id!(on_click)).is_none();
186    #[cfg(feature = "tooltip")]
187    let tooltip = wgt.property(property_id!(tooltip)).is_none() && wgt.property(property_id!(tooltip_fn)).is_none();
188    #[cfg(not(feature = "tooltip"))]
189    let tooltip = false;
190    if on_click || tooltip {
191        wgt.push_intrinsic(
192            NestGroup::EVENT,
193            "cmd_event",
194            clmv!(cmd, |mut child| {
195                if on_click {
196                    child = self::on_click(
197                        child,
198                        hn!(cmd, |args| {
199                            let cmd = cmd.get();
200                            if cmd.is_enabled().get() {
201                                if let Some(param) = CMD_PARAM_VAR.get() {
202                                    cmd.notify_param(param);
203                                } else {
204                                    cmd.notify();
205                                }
206                                args.propagation.stop();
207                            }
208                        }),
209                    );
210                }
211                #[cfg(feature = "tooltip")]
212                if tooltip {
213                    child = self::tooltip_fn(
214                        child,
215                        merge_var!(cmd, CMD_TOOLTIP_FN_VAR, |cmd, tt_fn| {
216                            if tt_fn.is_nil() {
217                                WidgetFn::nil()
218                            } else {
219                                wgt_fn!(cmd, tt_fn, |tooltip| { tt_fn(CmdTooltipArgs { tooltip, cmd }) })
220                            }
221                        }),
222                    );
223                }
224                child
225            }),
226        );
227    }
228}
229
230/// Optional command parameter for the button to use when notifying [`cmd`].
231///
232/// If `T` is `Option<CommandParam>` the param can be dynamically unset, otherwise the value is the param.
233///
234/// [`cmd`]: fn@cmd
235#[property(CONTEXT, default(CMD_PARAM_VAR), widget_impl(Button))]
236pub fn cmd_param<T: VarValue>(child: impl IntoUiNode, cmd_param: impl IntoVar<T>) -> UiNode {
237    if TypeId::of::<T>() == TypeId::of::<Option<CommandParam>>() {
238        with_context_var(
239            child,
240            CMD_PARAM_VAR,
241            AnyVar::from(cmd_param.into_var())
242                .downcast::<Option<CommandParam>>()
243                .unwrap_or_else(|_| unreachable!()),
244        )
245    } else {
246        with_context_var(
247            child,
248            CMD_PARAM_VAR,
249            cmd_param.into_var().map(|p| Some(CommandParam::new(p.clone()))),
250        )
251    }
252}
253
254/// Sets the widget function used to produce the button child when [`cmd`] is set and [`child`] is not.
255///
256/// [`cmd`]: fn@cmd
257/// [`child`]: fn@zng_wgt_container::child
258#[property(CONTEXT, default(CMD_CHILD_FN_VAR), widget_impl(Button, DefaultStyle))]
259pub fn cmd_child_fn(child: impl IntoUiNode, cmd_child: impl IntoVar<WidgetFn<Command>>) -> UiNode {
260    with_context_var(child, CMD_CHILD_FN_VAR, cmd_child)
261}
262
263#[cfg(feature = "tooltip")]
264/// Sets the widget function used to produce the button tooltip when [`cmd`] is set and tooltip is not.
265///
266/// [`cmd`]: fn@cmd
267#[property(CONTEXT, default(CMD_TOOLTIP_FN_VAR), widget_impl(Button, DefaultStyle))]
268pub fn cmd_tooltip_fn(child: impl IntoUiNode, cmd_tooltip: impl IntoVar<WidgetFn<CmdTooltipArgs>>) -> UiNode {
269    with_context_var(child, CMD_TOOLTIP_FN_VAR, cmd_tooltip)
270}
271
272/// Button default style.
273#[widget($crate::DefaultStyle)]
274pub struct DefaultStyle(Style);
275impl DefaultStyle {
276    fn widget_intrinsic(&mut self) {
277        widget_set! {
278            self;
279
280            replace = true;
281
282            padding = (7, 15);
283            corner_radius = 4;
284            child_align = Align::CENTER;
285
286            base_color = light_dark(rgb(0.82, 0.82, 0.82), rgb(0.18, 0.18, 0.18));
287
288            #[easing(150.ms())]
289            background_color = BASE_COLOR_VAR.rgba();
290            #[easing(150.ms())]
291            border = {
292                widths: 1,
293                sides: BASE_COLOR_VAR.rgba_into(),
294            };
295
296            when *#is_cap_hovered {
297                #[easing(0.ms())]
298                background_color = BASE_COLOR_VAR.shade(1);
299                #[easing(0.ms())]
300                border = {
301                    widths: 1,
302                    sides: BASE_COLOR_VAR.shade_into(2),
303                };
304            }
305
306            when *#is_pressed {
307                #[easing(0.ms())]
308                background_color = BASE_COLOR_VAR.shade(2);
309            }
310
311            when *#is_disabled {
312                saturate = false;
313                child_opacity = 50.pct();
314                cursor = CursorIcon::NotAllowed;
315            }
316        }
317    }
318}
319
320/// Primary button style.
321#[widget($crate::PrimaryStyle)]
322pub struct PrimaryStyle(DefaultStyle);
323impl_named_style_fn!(primary, PrimaryStyle);
324impl PrimaryStyle {
325    fn widget_intrinsic(&mut self) {
326        widget_set! {
327            self;
328            named_style_fn = PRIMARY_STYLE_FN_VAR;
329
330            base_color = ACCENT_COLOR_VAR.map(|c| c.shade(-2));
331            zng_wgt_text::font_weight = zng_ext_font::FontWeight::BOLD;
332        }
333    }
334}
335
336/// Button light style.
337#[widget($crate::LightStyle)]
338pub struct LightStyle(DefaultStyle);
339impl_named_style_fn!(light, LightStyle);
340impl LightStyle {
341    fn widget_intrinsic(&mut self) {
342        widget_set! {
343            self;
344            named_style_fn = LIGHT_STYLE_FN_VAR;
345
346            border = unset!;
347            padding = 7;
348
349            #[easing(150.ms())]
350            background_color = FONT_COLOR_VAR.map(|c| c.with_alpha(0.pct()));
351
352            when *#is_cap_hovered {
353                #[easing(0.ms())]
354                background_color = FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()));
355            }
356
357            when *#is_pressed {
358                #[easing(0.ms())]
359                background_color = FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
360            }
361
362            when *#is_disabled {
363                saturate = false;
364                child_opacity = 50.pct();
365                cursor = CursorIcon::NotAllowed;
366            }
367        }
368    }
369}
370
371/// Button link style.
372///
373/// Looks like a web hyperlink.
374#[widget($crate::LinkStyle)]
375pub struct LinkStyle(Style);
376impl_named_style_fn!(link, LinkStyle);
377impl LinkStyle {
378    fn widget_intrinsic(&mut self) {
379        widget_set! {
380            self;
381            replace = true;
382            named_style_fn = LINK_STYLE_FN_VAR;
383
384            font_color = light_dark(colors::BLUE, web_colors::LIGHT_BLUE);
385            cursor = CursorIcon::Pointer;
386            access_role = AccessRole::Link;
387
388            when *#is_cap_hovered {
389                underline = 1, LineStyle::Solid;
390            }
391
392            when *#is_pressed {
393                font_color = light_dark(web_colors::BROWN, colors::YELLOW);
394            }
395
396            when *#is_disabled {
397                saturate = false;
398                child_opacity = 50.pct();
399                cursor = CursorIcon::NotAllowed;
400            }
401        }
402    }
403}
404
405/// Button context.
406pub struct BUTTON;
407impl BUTTON {
408    /// The [`cmd`] value, if set.
409    ///
410    /// [`cmd`]: fn@cmd
411    pub fn cmd(&self) -> Var<Option<Command>> {
412        CMD_VAR.read_only()
413    }
414
415    /// The [`cmd_param`] value.
416    ///
417    /// [`cmd_param`]: fn@cmd_param
418    pub fn cmd_param(&self) -> Var<Option<CommandParam>> {
419        CMD_PARAM_VAR.read_only()
420    }
421}