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#![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#[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 pub on_click(handler: Handler<ClickArgs>);
60
61 pub capture_pointer(mode: impl IntoVar<CaptureMode>);
65 }
66}
67impl_style_fn!(Button, DefaultStyle);
68
69context_var! {
70 pub static CMD_PARAM_VAR: Option<CommandParam> = None;
72
73 pub static CMD_CHILD_FN_VAR: WidgetFn<Command> = WidgetFn::new(default_cmd_child_fn);
75
76 #[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#[derive(Clone)]
88#[non_exhaustive]
89pub struct CmdTooltipArgs {
90 pub tooltip: TooltipArgs,
92 pub cmd: Command,
94}
95
96#[cfg(feature = "tooltip")]
97impl CmdTooltipArgs {
98 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
112pub fn default_cmd_child_fn(cmd: Command) -> UiNode {
114 Text!(cmd.name())
115}
116
117#[cfg(feature = "tooltip")]
118pub 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#[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#[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#[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#[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#[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#[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#[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#[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
424pub struct BUTTON;
426impl BUTTON {
427 pub fn cmd(&self) -> Var<Option<Command>> {
431 CMD_VAR.read_only()
432 }
433
434 pub fn cmd_param(&self) -> Var<Option<CommandParam>> {
438 CMD_PARAM_VAR.read_only()
439 }
440}