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