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, 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 }
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 pub on_click(handler: impl WidgetHandler<ClickArgs>);
152
153 pub capture_pointer(mode: impl IntoVar<CaptureMode>);
157 }
158}
159impl_style_fn!(Button);
160
161context_var! {
162 pub static CMD_PARAM_VAR: Option<CommandParam> = None;
164
165 pub static CMD_CHILD_FN_VAR: WidgetFn<Command> = WidgetFn::new(default_cmd_child_fn);
167
168 #[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#[derive(Clone)]
180pub struct CmdTooltipArgs {
181 pub tooltip: TooltipArgs,
183 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
195pub fn default_cmd_child_fn(cmd: Command) -> impl UiNode {
197 Text!(cmd.name())
198}
199
200#[cfg(feature = "tooltip")]
201pub 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#[property(CHILD, capture, widget_impl(Button))]
248pub fn cmd(cmd: impl IntoVar<Command>) {}
249
250#[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#[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#[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#[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#[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#[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#[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
421pub struct BUTTON;
423impl BUTTON {
424 pub fn cmd(&self) -> ReadOnlyContextVar<Option<Command>> {
428 CMD_VAR.read_only()
429 }
430
431 pub fn cmd_param(&self) -> ReadOnlyContextVar<Option<CommandParam>> {
435 CMD_PARAM_VAR.read_only()
436 }
437}