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#![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#[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 pub on_click(handler: impl WidgetHandler<ClickArgs>);
153
154 pub capture_pointer(mode: impl IntoVar<CaptureMode>);
158 }
159}
160impl_style_fn!(Button);
161
162context_var! {
163 pub static CMD_PARAM_VAR: Option<CommandParam> = None;
165
166 pub static CMD_CHILD_FN_VAR: WidgetFn<Command> = WidgetFn::new(default_cmd_child_fn);
168
169 #[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#[derive(Clone)]
181#[non_exhaustive]
182pub struct CmdTooltipArgs {
183 pub tooltip: TooltipArgs,
185 pub cmd: Command,
187}
188
189#[cfg(feature = "tooltip")]
190impl CmdTooltipArgs {
191 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
205pub fn default_cmd_child_fn(cmd: Command) -> impl UiNode {
207 Text!(cmd.name())
208}
209
210#[cfg(feature = "tooltip")]
211pub 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#[property(CHILD, capture, widget_impl(Button))]
258pub fn cmd(cmd: impl IntoVar<Command>) {}
259
260#[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#[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#[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#[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#[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#[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#[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
431pub struct BUTTON;
433impl BUTTON {
434 pub fn cmd(&self) -> ReadOnlyContextVar<Option<Command>> {
438 CMD_VAR.read_only()
439 }
440
441 pub fn cmd_param(&self) -> ReadOnlyContextVar<Option<CommandParam>> {
445 CMD_PARAM_VAR.read_only()
446 }
447}