zng_wgt_toggle/
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//! Toggle widget, properties and commands.
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::ops;
15use std::{error::Error, fmt, marker::PhantomData, sync::Arc};
16
17use colors::BASE_COLOR_VAR;
18use task::parking_lot::Mutex;
19use zng_ext_font::FontNames;
20use zng_ext_input::{
21    gesture::CLICK_EVENT,
22    mouse::{ClickMode, MOUSE_INPUT_EVENT},
23    pointer_capture::CaptureMode,
24};
25use zng_ext_l10n::lang;
26use zng_var::{AnyVar, AnyVarValue, BoxedAnyVar, Var, VarIsReadOnlyError};
27use zng_wgt::{ICONS, Wgt, align, border, border_align, border_over, corner_radius, hit_test_mode, is_inited, prelude::*};
28use zng_wgt_access::{AccessRole, access_role, accessible};
29use zng_wgt_container::{child_align, child_end, child_start, padding};
30use zng_wgt_fill::background_color;
31use zng_wgt_filter::opacity;
32use zng_wgt_input::{click_mode, is_hovered, pointer_capture::capture_pointer_on_init};
33use zng_wgt_layer::popup::{POPUP, PopupState};
34use zng_wgt_size_offset::{size, x, y};
35use zng_wgt_style::{Style, impl_style_fn, style_fn};
36
37pub mod cmd;
38
39/// A toggle button that flips a `bool` or `Option<bool>` variable on click, or selects a value.
40///
41/// This widget has three primary properties, [`checked`], [`checked_opt`] and [`value`], setting one
42/// of the checked properties to a read-write variable enables the widget and it will set the variables
43/// on click, setting [`value`] turns the toggle in a selection item that is inserted/removed in a contextual [`selector`].
44///
45/// [`checked`]: fn@checked
46/// [`checked_opt`]: fn@checked_opt
47/// [`value`]: fn@value
48/// [`selector`]: fn@selector
49#[widget($crate::Toggle)]
50pub struct Toggle(zng_wgt_button::Button);
51impl Toggle {
52    fn widget_intrinsic(&mut self) {
53        self.style_intrinsic(STYLE_FN_VAR, property_id!(self::style_fn));
54        widget_set! {
55            self;
56            style_base_fn = style_fn!(|_| DefaultStyle!());
57        }
58    }
59}
60impl_style_fn!(Toggle);
61
62context_var! {
63    /// The toggle button checked state.
64    pub static IS_CHECKED_VAR: Option<bool> = false;
65
66    /// If toggle button cycles between `None`, `Some(false)` and `Some(true)` on click.
67    pub static IS_TRISTATE_VAR: bool = false;
68}
69
70/// Toggle cycles between `true` and `false`, updating the variable.
71///
72/// Note that you can read the checked state of the widget using [`is_checked`].
73///
74/// [`is_checked`]: fn@is_checked
75#[property(CONTEXT, default(false), widget_impl(Toggle))]
76pub fn checked(child: impl UiNode, checked: impl IntoVar<bool>) -> impl UiNode {
77    let checked = checked.into_var();
78    let mut _toggle_handle = CommandHandle::dummy();
79    let mut access_handle = VarHandle::dummy();
80    let node = match_node(
81        child,
82        clmv!(checked, |child, op| match op {
83            UiNodeOp::Init => {
84                WIDGET.sub_event(&CLICK_EVENT);
85                _toggle_handle = cmd::TOGGLE_CMD.scoped(WIDGET.id()).subscribe(true);
86            }
87            UiNodeOp::Deinit => {
88                _toggle_handle = CommandHandle::dummy();
89                access_handle = VarHandle::dummy();
90            }
91            UiNodeOp::Info { info } => {
92                if let Some(mut a) = info.access() {
93                    if access_handle.is_dummy() {
94                        access_handle = checked.subscribe(UpdateOp::Info, WIDGET.id());
95                    }
96                    a.set_checked(Some(checked.get()));
97                }
98            }
99            UiNodeOp::Event { update } => {
100                child.event(update);
101
102                if let Some(args) = CLICK_EVENT.on(update) {
103                    if args.is_primary()
104                        && checked.capabilities().contains(VarCapability::MODIFY)
105                        && !args.propagation().is_stopped()
106                        && args.is_enabled(WIDGET.id())
107                    {
108                        args.propagation().stop();
109
110                        let _ = checked.set(!checked.get());
111                    }
112                } else if let Some(args) = cmd::TOGGLE_CMD.scoped(WIDGET.id()).on_unhandled(update) {
113                    if let Some(b) = args.param::<bool>() {
114                        args.propagation().stop();
115                        let _ = checked.set(*b);
116                    } else if let Some(b) = args.param::<Option<bool>>() {
117                        if let Some(b) = b {
118                            args.propagation().stop();
119                            let _ = checked.set(*b);
120                        }
121                    } else if args.param.is_none() {
122                        args.propagation().stop();
123                        let _ = checked.set(!checked.get());
124                    }
125                }
126            }
127            _ => {}
128        }),
129    );
130    with_context_var(node, IS_CHECKED_VAR, checked.map_into())
131}
132
133/// Toggle cycles between `Some(true)` and `Some(false)` and accepts `None`, if the
134/// widget is `tristate` also sets to `None` in the toggle cycle.
135#[property(CONTEXT + 1, default(None), widget_impl(Toggle))]
136pub fn checked_opt(child: impl UiNode, checked: impl IntoVar<Option<bool>>) -> impl UiNode {
137    let checked = checked.into_var();
138    let mut _toggle_handle = CommandHandle::dummy();
139    let mut access_handle = VarHandle::dummy();
140
141    let node = match_node(
142        child,
143        clmv!(checked, |child, op| match op {
144            UiNodeOp::Init => {
145                WIDGET.sub_event(&CLICK_EVENT);
146                _toggle_handle = cmd::TOGGLE_CMD.scoped(WIDGET.id()).subscribe(true);
147            }
148            UiNodeOp::Deinit => {
149                _toggle_handle = CommandHandle::dummy();
150                access_handle = VarHandle::dummy();
151            }
152            UiNodeOp::Info { info } => {
153                if let Some(mut a) = info.access() {
154                    if access_handle.is_dummy() {
155                        access_handle = checked.subscribe(UpdateOp::Info, WIDGET.id());
156                    }
157                    a.set_checked(checked.get());
158                }
159            }
160            UiNodeOp::Event { update } => {
161                child.event(update);
162
163                let mut cycle = false;
164
165                if let Some(args) = CLICK_EVENT.on(update) {
166                    if args.is_primary()
167                        && checked.capabilities().contains(VarCapability::MODIFY)
168                        && !args.propagation().is_stopped()
169                        && args.is_enabled(WIDGET.id())
170                    {
171                        args.propagation().stop();
172
173                        cycle = true;
174                    }
175                } else if let Some(args) = cmd::TOGGLE_CMD.scoped(WIDGET.id()).on_unhandled(update) {
176                    if let Some(b) = args.param::<bool>() {
177                        args.propagation().stop();
178                        let _ = checked.set(Some(*b));
179                    } else if let Some(b) = args.param::<Option<bool>>() {
180                        if IS_TRISTATE_VAR.get() {
181                            args.propagation().stop();
182                            let _ = checked.set(*b);
183                        } else if let Some(b) = b {
184                            args.propagation().stop();
185                            let _ = checked.set(Some(*b));
186                        }
187                    } else if args.param.is_none() {
188                        args.propagation().stop();
189
190                        cycle = true;
191                    }
192                }
193
194                if cycle {
195                    if IS_TRISTATE_VAR.get() {
196                        let _ = checked.set(match checked.get() {
197                            Some(true) => None,
198                            Some(false) => Some(true),
199                            None => Some(false),
200                        });
201                    } else {
202                        let _ = checked.set(match checked.get() {
203                            Some(true) | None => Some(false),
204                            Some(false) => Some(true),
205                        });
206                    }
207                }
208            }
209            _ => {}
210        }),
211    );
212
213    with_context_var(node, IS_CHECKED_VAR, checked)
214}
215
216/// Enables `None` as an input value.
217///
218/// Note that `None` is always accepted in `checked_opt`, this property controls if
219/// `None` is one of the values in the toggle cycle. If the widget is bound to the `checked` property
220/// this config is ignored.
221///
222/// This is not enabled by default.
223///
224/// [`checked_opt`]: fn@checked_opt
225#[property(CONTEXT, default(IS_TRISTATE_VAR), widget_impl(Toggle))]
226pub fn tristate(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
227    with_context_var(child, IS_TRISTATE_VAR, enabled)
228}
229
230/// If the toggle is checked from any of the three primary properties.
231///
232/// Note to read the tristate directly use [`IS_CHECKED_VAR`] directly.
233#[property(EVENT, widget_impl(Toggle))]
234pub fn is_checked(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
235    bind_state(child, IS_CHECKED_VAR.map(|s| *s == Some(true)), state)
236}
237
238/// Values that is selected in the contextual [`selector`].
239///
240/// The widget [`is_checked`] when the value is selected, on click and on value update, the selection
241/// is updated according to the behavior defined in the contextual [`selector`]. If no contextual
242/// [`selector`] is the widget is never checked.
243///
244/// Note that the value can be any type, but must be one of the types accepted by the contextual [`selector`], type
245/// validation happens in run-time, an error is logged if the type is not compatible. Because any type can be used in
246/// this property type inference cannot resolve the type automatically and a type annotation is required: `value<T> = t;`.
247///
248/// [`is_checked`]: fn@is_checked
249/// [`selector`]: fn@selector
250///
251/// This property interacts with the contextual [`selector`], when the widget is clicked or the `value` variable changes
252/// the contextual [`Selector`] is used to implement the behavior.
253///
254/// [`selector`]: fn@selector
255#[property(CONTEXT+2, widget_impl(Toggle))]
256pub fn value<T: VarValue>(child: impl UiNode, value: impl IntoVar<T>) -> impl UiNode {
257    value_impl(child, value.into_var().boxed_any())
258}
259fn value_impl(child: impl UiNode, value: BoxedAnyVar) -> impl UiNode {
260    // Returns `true` if selected.
261    fn select(value: &dyn AnyVarValue) -> bool {
262        let selector = SELECTOR.get();
263        match selector.select(value.clone_boxed()) {
264            Ok(()) => true,
265            Err(e) => {
266                let selected = selector.is_selected(value);
267                if selected {
268                    tracing::error!("selected `{value:?}` with error, {e}");
269                } else if let SelectorError::ReadOnly | SelectorError::CannotClear = e {
270                    // ignore
271                } else {
272                    tracing::error!("failed to select `{value:?}`, {e}");
273                }
274                selected
275            }
276        }
277    }
278    // Returns `true` if deselected.
279    fn deselect(value: &dyn AnyVarValue) -> bool {
280        let selector = SELECTOR.get();
281        match selector.deselect(value) {
282            Ok(()) => true,
283            Err(e) => {
284                let deselected = !selector.is_selected(value);
285                if deselected {
286                    tracing::error!("deselected `{value:?}` with error, {e}");
287                } else if let SelectorError::ReadOnly | SelectorError::CannotClear = e {
288                    // ignore
289                } else {
290                    tracing::error!("failed to deselect `{value:?}`, {e}");
291                }
292                deselected
293            }
294        }
295    }
296    fn is_selected(value: &dyn AnyVarValue) -> bool {
297        SELECTOR.get().is_selected(value)
298    }
299
300    let checked = var(Some(false));
301    let child = with_context_var(child, IS_CHECKED_VAR, checked.clone());
302    let mut prev_value = None::<Box<dyn AnyVarValue>>;
303
304    let mut _click_handle = None;
305    let mut _toggle_handle = CommandHandle::dummy();
306    let mut _select_handle = CommandHandle::dummy();
307
308    match_node(child, move |child, op| match op {
309        UiNodeOp::Init => {
310            let id = WIDGET.id();
311            WIDGET.sub_var(&value).sub_var(&DESELECT_ON_NEW_VAR).sub_var(&checked);
312            SELECTOR.get().subscribe();
313
314            value.with_any(&mut |value| {
315                let selected = if SELECT_ON_INIT_VAR.get() {
316                    select(value)
317                } else {
318                    is_selected(value)
319                };
320                checked.set(Some(selected));
321
322                if DESELECT_ON_DEINIT_VAR.get() {
323                    prev_value = Some(value.clone_boxed());
324                }
325            });
326
327            _click_handle = Some(CLICK_EVENT.subscribe(id));
328            _toggle_handle = cmd::TOGGLE_CMD.scoped(id).subscribe(true);
329            _select_handle = cmd::SELECT_CMD.scoped(id).subscribe(true);
330        }
331        UiNodeOp::Deinit => {
332            if checked.get() == Some(true) && DESELECT_ON_DEINIT_VAR.get() {
333                value.with_any(&mut |value| {
334                    if deselect(value) {
335                        checked.set(Some(false));
336                    }
337                });
338            }
339
340            prev_value = None;
341            _click_handle = None;
342            _toggle_handle = CommandHandle::dummy();
343            _select_handle = CommandHandle::dummy();
344        }
345        UiNodeOp::Event { update } => {
346            child.event(update);
347
348            if let Some(args) = CLICK_EVENT.on(update) {
349                if args.is_primary() && !args.propagation().is_stopped() && args.is_enabled(WIDGET.id()) {
350                    args.propagation().stop();
351
352                    value.with_any(&mut |value| {
353                        let selected = if checked.get() == Some(true) {
354                            !deselect(value)
355                        } else {
356                            select(value)
357                        };
358                        checked.set(Some(selected))
359                    });
360                }
361            } else if let Some(args) = cmd::TOGGLE_CMD.scoped(WIDGET.id()).on_unhandled(update) {
362                if args.param.is_none() {
363                    args.propagation().stop();
364
365                    value.with_any(&mut |value| {
366                        let selected = if checked.get() == Some(true) {
367                            !deselect(value)
368                        } else {
369                            select(value)
370                        };
371                        checked.set(Some(selected))
372                    });
373                } else {
374                    let s = if let Some(s) = args.param::<Option<bool>>() {
375                        Some(s.unwrap_or(false))
376                    } else {
377                        args.param::<bool>().copied()
378                    };
379                    if let Some(s) = s {
380                        args.propagation().stop();
381
382                        value.with_any(&mut |value| {
383                            let selected = if s { select(value) } else { !deselect(value) };
384                            checked.set(Some(selected))
385                        });
386                    }
387                }
388            } else if let Some(args) = cmd::SELECT_CMD.scoped(WIDGET.id()).on_unhandled(update) {
389                if args.param.is_none() {
390                    args.propagation().stop();
391                    value.with_any(&mut |value| {
392                        let selected = checked.get() == Some(true);
393                        if !selected && select(value) {
394                            checked.set(Some(true));
395                        }
396                    });
397                }
398            }
399        }
400        UiNodeOp::Update { .. } => {
401            let mut selected = None;
402            value.with_new_any(&mut |new| {
403                // auto select new.
404                selected = Some(if checked.get() == Some(true) && SELECT_ON_NEW_VAR.get() {
405                    select(new)
406                } else {
407                    is_selected(new)
408                });
409
410                // auto deselect prev, need to be done after potential auto select new to avoid `CannotClear` error.
411                if let Some(prev) = prev_value.take() {
412                    if DESELECT_ON_NEW_VAR.get() {
413                        deselect(&*prev);
414                        prev_value = Some(new.clone_boxed());
415                    }
416                }
417            });
418            let selected = selected.unwrap_or_else(|| {
419                // contextual selector can change in any update.
420                let mut s = false;
421                value.with_any(&mut |v| {
422                    s = is_selected(v);
423                });
424                s
425            });
426            checked.set(selected);
427
428            if DESELECT_ON_NEW_VAR.get() && selected {
429                // save a clone of the value to reference it on deselection triggered by variable value changing.
430                if prev_value.is_none() {
431                    prev_value = Some(value.get_any());
432                }
433            } else {
434                prev_value = None;
435            }
436
437            if let Some(Some(true)) = checked.get_new() {
438                if SCROLL_ON_SELECT_VAR.get() {
439                    use zng_wgt_scroll::cmd::*;
440                    scroll_to(WIDGET.id(), ScrollToMode::minimal(10));
441                }
442            }
443        }
444        _ => {}
445    })
446}
447
448/// If the scrolls into view when the [`value`] selected.
449///
450/// This is enabled by default.
451///
452/// [`value`]: fn@value
453#[property(CONTEXT, default(SCROLL_ON_SELECT_VAR), widget_impl(Toggle))]
454pub fn scroll_on_select(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
455    with_context_var(child, SCROLL_ON_SELECT_VAR, enabled)
456}
457
458/// Sets the contextual selector that all inner widgets will target from the [`value`] property.
459///
460/// All [`value`] properties declared in widgets inside `child` will use the [`Selector`] to manipulate
461/// the selection.
462///
463/// Selection in a context can be blocked by setting the selector to [`Selector::nil()`], this is also the default
464/// selector so the [`value`] property only works if a contextual selector is present.
465///
466/// This property sets the [`SELECTOR`] context and handles [`cmd::SelectOp`] requests. It also sets the widget
467/// access role to [`AccessRole::RadioGroup`].
468///
469/// [`value`]: fn@value
470/// [`AccessRole::RadioGroup`]: zng_wgt_access::AccessRole::RadioGroup
471#[property(CONTEXT, default(Selector::nil()), widget_impl(Toggle))]
472pub fn selector(child: impl UiNode, selector: impl IntoValue<Selector>) -> impl UiNode {
473    let mut _select_handle = CommandHandle::dummy();
474    let child = match_node(child, move |c, op| match op {
475        UiNodeOp::Init => {
476            _select_handle = cmd::SELECT_CMD.scoped(WIDGET.id()).subscribe(true);
477        }
478        UiNodeOp::Info { info } => {
479            if let Some(mut info) = info.access() {
480                info.set_role(AccessRole::RadioGroup);
481            }
482        }
483        UiNodeOp::Deinit => {
484            _select_handle = CommandHandle::dummy();
485        }
486        UiNodeOp::Event { update } => {
487            c.event(update);
488
489            if let Some(args) = cmd::SELECT_CMD.scoped(WIDGET.id()).on_unhandled(update) {
490                if let Some(p) = args.param::<cmd::SelectOp>() {
491                    args.propagation().stop();
492
493                    p.call();
494                }
495            }
496        }
497        _ => {}
498    });
499    with_context_local(child, &SELECTOR, selector)
500}
501
502/// If [`value`] is selected when the widget that has the value is inited.
503///
504/// [`value`]: fn@value
505#[property(CONTEXT, default(SELECT_ON_INIT_VAR), widget_impl(Toggle))]
506pub fn select_on_init(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
507    with_context_var(child, SELECT_ON_INIT_VAR, enabled)
508}
509
510/// If [`value`] is deselected when the widget that has the value is deinited and the value was selected.
511///
512/// [`value`]: fn@value
513#[property(CONTEXT, default(DESELECT_ON_DEINIT_VAR), widget_impl(Toggle))]
514pub fn deselect_on_deinit(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
515    with_context_var(child, DESELECT_ON_DEINIT_VAR, enabled)
516}
517
518/// If [`value`] selects the new value when the variable changes and the previous value was selected.
519///
520/// [`value`]: fn@value
521#[property(CONTEXT, default(SELECT_ON_NEW_VAR), widget_impl(Toggle))]
522pub fn select_on_new(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
523    with_context_var(child, SELECT_ON_NEW_VAR, enabled)
524}
525
526/// If [`value`] deselects the previously selected value when the variable changes.
527///
528/// [`value`]: fn@value
529#[property(CONTEXT, default(DESELECT_ON_NEW_VAR), widget_impl(Toggle))]
530pub fn deselect_on_new(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
531    with_context_var(child, DESELECT_ON_NEW_VAR, enabled)
532}
533
534context_local! {
535    /// Contextual [`Selector`].
536    pub static SELECTOR: Selector = Selector::nil();
537}
538
539context_var! {
540    /// If [`value`] is selected when the widget that has the value is inited.
541    ///
542    /// Use the [`select_on_init`] property to set. By default is `false`.
543    ///
544    /// [`value`]: fn@value
545    /// [`select_on_init`]: fn@select_on_init
546    pub static SELECT_ON_INIT_VAR: bool = false;
547
548    /// If [`value`] is deselected when the widget that has the value is deinited and the value was selected.
549    ///
550    /// Use the [`deselect_on_deinit`] property to set. By default is `false`.
551    ///
552    /// [`value`]: fn@value
553    /// [`deselect_on_deinit`]: fn@deselect_on_deinit
554    pub static DESELECT_ON_DEINIT_VAR: bool = false;
555
556    /// If [`value`] selects the new value when the variable changes and the previous value was selected.
557    ///
558    /// Use the [`select_on_new`] property to set. By default is `true`.
559    ///
560    /// [`value`]: fn@value
561    /// [`select_on_new`]: fn@select_on_new
562    pub static SELECT_ON_NEW_VAR: bool = true;
563
564    /// If [`value`] deselects the previously selected value when the variable changes.
565    ///
566    /// Use the [`deselect_on_new`] property to set. By default is `false`.
567    ///
568    /// [`value`]: fn@value
569    /// [`deselect_on_new`]: fn@deselect_on_new
570    pub static DESELECT_ON_NEW_VAR: bool = false;
571
572    /// If [`value`] scrolls into view when selected.
573    ///
574    /// This is enabled by default.
575    ///
576    /// [`value`]: fn@value
577    pub static SCROLL_ON_SELECT_VAR: bool = true;
578}
579
580/// Represents a [`Selector`] implementation.
581pub trait SelectorImpl: Send + 'static {
582    /// Add the selector subscriptions in the [`WIDGET`].
583    ///
584    /// [`WIDGET`]: zng_wgt::prelude::WIDGET
585    fn subscribe(&self);
586
587    /// Insert the `value` in the selection, returns `Ok(())` if the value was inserted or was already selected.
588    fn select(&mut self, value: Box<dyn AnyVarValue>) -> Result<(), SelectorError>;
589
590    /// Remove the `value` from the selection, returns `Ok(())` if the value was removed or was not selected.
591    fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError>;
592
593    /// Returns `true` if the `value` is selected.
594    fn is_selected(&self, value: &dyn AnyVarValue) -> bool;
595}
596
597/// Represents the contextual selector behavior of [`value`] selector.
598///
599/// A selector can be set using [`selector`], all [`value`] widgets in context will target it.
600///
601/// [`value`]: fn@value
602/// [`selector`]: fn@selector
603#[derive(Clone)]
604pub struct Selector(Arc<Mutex<dyn SelectorImpl>>);
605impl Selector {
606    /// New custom selector.
607    pub fn new(selector: impl SelectorImpl) -> Self {
608        Self(Arc::new(Mutex::new(selector)))
609    }
610
611    /// Represents no selector and the inability to select any item.
612    pub fn nil() -> Self {
613        struct NilSel;
614        impl SelectorImpl for NilSel {
615            fn subscribe(&self) {}
616
617            fn select(&mut self, _: Box<dyn AnyVarValue>) -> Result<(), SelectorError> {
618                Err(SelectorError::custom_str("no contextual `selector`"))
619            }
620
621            fn deselect(&mut self, _: &dyn AnyVarValue) -> Result<(), SelectorError> {
622                Ok(())
623            }
624
625            fn is_selected(&self, __r: &dyn AnyVarValue) -> bool {
626                false
627            }
628        }
629        Self::new(NilSel)
630    }
631
632    /// Represents the "radio" selection of a single item.
633    pub fn single<T>(selection: impl IntoVar<T>) -> Self
634    where
635        T: VarValue,
636    {
637        struct SingleSel<T, S> {
638            selection: S,
639            _type: PhantomData<T>,
640        }
641        impl<T, S> SelectorImpl for SingleSel<T, S>
642        where
643            T: VarValue,
644            S: Var<T>,
645        {
646            fn subscribe(&self) {
647                WIDGET.sub_var(&self.selection);
648            }
649
650            fn select(&mut self, value: Box<dyn AnyVarValue>) -> Result<(), SelectorError> {
651                match value.into_any().downcast::<T>() {
652                    Ok(value) => match self.selection.set(*value) {
653                        Ok(_) => Ok(()),
654                        Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
655                    },
656                    Err(_) => Err(SelectorError::WrongType),
657                }
658            }
659
660            fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
661                if self.is_selected(value) {
662                    Err(SelectorError::CannotClear)
663                } else {
664                    Ok(())
665                }
666            }
667
668            fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
669                match value.as_any().downcast_ref::<T>() {
670                    Some(value) => self.selection.with(|t| t == value),
671                    None => false,
672                }
673            }
674        }
675        Self::new(SingleSel {
676            selection: selection.into_var(),
677            _type: PhantomData,
678        })
679    }
680
681    /// Represents the "radio" selection of a single item that is optional.
682    pub fn single_opt<T>(selection: impl IntoVar<Option<T>>) -> Self
683    where
684        T: VarValue,
685    {
686        struct SingleOptSel<T, S> {
687            selection: S,
688            _type: PhantomData<T>,
689        }
690        impl<T, S> SelectorImpl for SingleOptSel<T, S>
691        where
692            T: VarValue,
693            S: Var<Option<T>>,
694        {
695            fn subscribe(&self) {
696                WIDGET.sub_var(&self.selection);
697            }
698
699            fn select(&mut self, value: Box<dyn AnyVarValue>) -> Result<(), SelectorError> {
700                match value.into_any().downcast::<T>() {
701                    Ok(value) => match self.selection.set(Some(*value)) {
702                        Ok(_) => Ok(()),
703                        Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
704                    },
705                    Err(value) => match value.downcast::<Option<T>>() {
706                        Ok(value) => match self.selection.set(*value) {
707                            Ok(_) => Ok(()),
708                            Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
709                        },
710                        Err(_) => Err(SelectorError::WrongType),
711                    },
712                }
713            }
714
715            fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
716                match value.as_any().downcast_ref::<T>() {
717                    Some(value) => {
718                        if self.selection.with(|t| t.as_ref() == Some(value)) {
719                            match self.selection.set(None) {
720                                Ok(_) => Ok(()),
721                                Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
722                            }
723                        } else {
724                            Ok(())
725                        }
726                    }
727                    None => match value.as_any().downcast_ref::<Option<T>>() {
728                        Some(value) => {
729                            if self.selection.with(|t| t == value) {
730                                if value.is_none() {
731                                    Ok(())
732                                } else {
733                                    match self.selection.set(None) {
734                                        Ok(_) => Ok(()),
735                                        Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
736                                    }
737                                }
738                            } else {
739                                Ok(())
740                            }
741                        }
742                        None => Ok(()),
743                    },
744                }
745            }
746
747            fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
748                match value.as_any().downcast_ref::<T>() {
749                    Some(value) => self.selection.with(|t| t.as_ref() == Some(value)),
750                    None => match value.as_any().downcast_ref::<Option<T>>() {
751                        Some(value) => self.selection.with(|t| t == value),
752                        None => false,
753                    },
754                }
755            }
756        }
757        Self::new(SingleOptSel {
758            selection: selection.into_var(),
759            _type: PhantomData,
760        })
761    }
762
763    /// Represents the "check list" selection of bitflags.
764    pub fn bitflags<T>(selection: impl IntoVar<T>) -> Self
765    where
766        T: VarValue + ops::BitOr<Output = T> + ops::BitAnd<Output = T> + ops::Not<Output = T>,
767    {
768        struct BitflagsSel<T, S> {
769            selection: S,
770            _type: PhantomData<T>,
771        }
772        impl<T, S> SelectorImpl for BitflagsSel<T, S>
773        where
774            T: VarValue + ops::BitOr<Output = T> + ops::BitAnd<Output = T> + ops::Not<Output = T>,
775            S: Var<T>,
776        {
777            fn subscribe(&self) {
778                WIDGET.sub_var(&self.selection);
779            }
780
781            fn select(&mut self, value: Box<dyn AnyVarValue>) -> Result<(), SelectorError> {
782                match value.into_any().downcast::<T>() {
783                    Ok(value) => self
784                        .selection
785                        .modify(move |m| {
786                            let value = *value;
787                            let new = m.as_ref().clone() | value;
788                            if m.as_ref() != &new {
789                                m.set(new);
790                            }
791                        })
792                        .map_err(|_| SelectorError::ReadOnly),
793                    Err(_) => Err(SelectorError::WrongType),
794                }
795            }
796
797            fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
798                match value.as_any().downcast_ref::<T>() {
799                    Some(value) => self
800                        .selection
801                        .modify(clmv!(value, |m| {
802                            let new = m.as_ref().clone() & !value;
803                            if m.as_ref() != &new {
804                                m.set(new);
805                            }
806                        }))
807                        .map_err(|_| SelectorError::ReadOnly),
808                    None => Err(SelectorError::WrongType),
809                }
810            }
811
812            fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
813                match value.as_any().downcast_ref::<T>() {
814                    Some(value) => &(self.selection.get() & value.clone()) == value,
815                    None => false,
816                }
817            }
818        }
819
820        Self::new(BitflagsSel {
821            selection: selection.into_var(),
822            _type: PhantomData,
823        })
824    }
825
826    /// Add the selector subscriptions in [`WIDGET`].
827    ///
828    /// [`WIDGET`]: zng_wgt::prelude::WIDGET
829    pub fn subscribe(&self) {
830        self.0.lock().subscribe();
831    }
832
833    /// Insert the `value` in the selection, returns `Ok(())` if the value was inserted or was already selected.
834    pub fn select(&self, value: Box<dyn AnyVarValue>) -> Result<(), SelectorError> {
835        self.0.lock().select(value)
836    }
837
838    /// Remove the `value` from the selection, returns `Ok(())` if the value was removed or was not selected.
839    pub fn deselect(&self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
840        self.0.lock().deselect(value)
841    }
842
843    /// Returns `true` if the `value` is selected.
844    pub fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
845        self.0.lock().is_selected(value)
846    }
847}
848impl<S: SelectorImpl> From<S> for Selector {
849    fn from(sel: S) -> Self {
850        Selector::new(sel)
851    }
852}
853impl fmt::Debug for Selector {
854    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
855        write!(f, "Selector(_)")
856    }
857}
858impl PartialEq for Selector {
859    fn eq(&self, other: &Self) -> bool {
860        Arc::ptr_eq(&self.0, &other.0)
861    }
862}
863
864/// Error for [`Selector`] operations.
865#[derive(Debug, Clone)]
866pub enum SelectorError {
867    /// Cannot select item because it is not of type that the selector can handle.
868    WrongType,
869    /// Cannot (de)select item because the selection is read-only.
870    ReadOnly,
871    /// Cannot deselect item because the selection cannot be empty.
872    CannotClear,
873    /// Cannot select item because of a selector specific reason.
874    Custom(Arc<dyn Error + Send + Sync>),
875}
876impl SelectorError {
877    /// New custom error from string.
878    pub fn custom_str(str: impl Into<String>) -> SelectorError {
879        let str = str.into();
880        let e: Box<dyn Error + Send + Sync> = str.into();
881        let e: Arc<dyn Error + Send + Sync> = e.into();
882        SelectorError::Custom(e)
883    }
884}
885impl fmt::Display for SelectorError {
886    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
887        match self {
888            SelectorError::WrongType => write!(f, "wrong value type for selector"),
889            SelectorError::ReadOnly => write!(f, "selection is read-only"),
890            SelectorError::CannotClear => write!(f, "selection cannot be empty"),
891            SelectorError::Custom(e) => fmt::Display::fmt(e, f),
892        }
893    }
894}
895impl Error for SelectorError {
896    fn source(&self) -> Option<&(dyn Error + 'static)> {
897        match self {
898            SelectorError::WrongType => None,
899            SelectorError::ReadOnly => None,
900            SelectorError::CannotClear => None,
901            SelectorError::Custom(e) => Some(&**e),
902        }
903    }
904}
905impl From<VarIsReadOnlyError> for SelectorError {
906    fn from(_: VarIsReadOnlyError) -> Self {
907        SelectorError::ReadOnly
908    }
909}
910
911/// Default toggle style.
912///
913/// Extends the [`button::DefaultStyle`] to have the *pressed* look when [`is_checked`].
914///
915/// [`button::DefaultStyle`]: struct@zng_wgt_button::DefaultStyle
916/// [`is_checked`]: fn@is_checked
917#[widget($crate::DefaultStyle)]
918pub struct DefaultStyle(zng_wgt_button::DefaultStyle);
919impl DefaultStyle {
920    fn widget_intrinsic(&mut self) {
921        widget_set! {
922            self;
923            replace = true;
924            when *#is_checked {
925                background_color = BASE_COLOR_VAR.shade(2);
926                border = {
927                    widths: 1,
928                    sides: BASE_COLOR_VAR.shade_into(2),
929                };
930            }
931        }
932    }
933}
934
935/// Toggle light style.
936#[widget($crate::LightStyle)]
937pub struct LightStyle(zng_wgt_button::LightStyle);
938impl LightStyle {
939    fn widget_intrinsic(&mut self) {
940        widget_set! {
941            self;
942            when *#is_checked {
943                #[easing(0.ms())]
944                background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
945            }
946        }
947    }
948}
949
950/// Checkmark toggle style.
951///
952/// Style a [`Toggle!`] widget to look like a *checkbox*.
953///
954/// [`Toggle!`]: struct@Toggle
955#[widget($crate::CheckStyle)]
956pub struct CheckStyle(Style);
957impl CheckStyle {
958    fn widget_intrinsic(&mut self) {
959        widget_set! {
960            self;
961            replace = true;
962            child_start = {
963                node: {
964                    let parent_hovered = var(false);
965                    is_hovered(checkmark_visual(parent_hovered.clone()), parent_hovered)
966                },
967                spacing: CHECK_SPACING_VAR,
968            };
969            access_role = AccessRole::CheckBox;
970        }
971    }
972}
973context_var! {
974    /// Spacing between the checkmark and the content.
975    pub static CHECK_SPACING_VAR: Length = 4;
976}
977
978/// Spacing between the checkmark and the content.
979#[property(CONTEXT, default(CHECK_SPACING_VAR), widget_impl(CheckStyle))]
980pub fn check_spacing(child: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
981    with_context_var(child, CHECK_SPACING_VAR, spacing)
982}
983
984fn checkmark_visual(parent_hovered: impl Var<bool>) -> impl UiNode {
985    let checked = ICONS.get_or(["toggle.checked", "check"], || {
986        zng_wgt_text::Text! {
987            txt = "✓";
988            font_family = FontNames::system_ui(&lang!(und));
989            txt_align = Align::CENTER;
990        }
991    });
992    let indeterminate = ICONS.get_or(["toggle.indeterminate"], || {
993        zng_wgt::Wgt! {
994            align = Align::CENTER;
995            background_color = zng_wgt_text::FONT_COLOR_VAR;
996            size = (6, 2);
997            corner_radius = 0;
998        }
999    });
1000    zng_wgt_container::Container! {
1001        hit_test_mode = false;
1002        accessible = false;
1003        size = 1.2.em();
1004        corner_radius = 0.1.em();
1005        align = Align::CENTER;
1006
1007        #[easing(150.ms())]
1008        background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()));
1009        when *#{parent_hovered} {
1010            #[easing(0.ms())]
1011            background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
1012        }
1013
1014        when #{IS_CHECKED_VAR}.is_none() {
1015            child = indeterminate;
1016        }
1017        when *#{IS_CHECKED_VAR} == Some(true) {
1018            child = checked;
1019            #[easing(0.ms())]
1020            background_color = colors::ACCENT_COLOR_VAR.shade(-1);
1021        }
1022    }
1023}
1024
1025/// Popup toggle style.
1026///
1027/// Style a [`Toggle!`] widget to look like the popup toggle button in a *combo-box*.
1028///
1029/// [`Toggle!`]: struct@Toggle
1030#[widget($crate::ComboStyle)]
1031pub struct ComboStyle(DefaultStyle);
1032impl ComboStyle {
1033    fn widget_intrinsic(&mut self) {
1034        widget_set! {
1035            self;
1036            replace = true;
1037            access_role = AccessRole::ComboBox;
1038            child_align = Align::FILL;
1039            border_over = false;
1040            border_align = 1.fct();
1041            padding = -1;
1042            checked = var(false);
1043            child_end = {
1044                node: combomark_visual(),
1045                spacing: COMBO_SPACING_VAR,
1046            };
1047
1048            click_mode = ClickMode::press();
1049
1050            zng_wgt_button::style_fn = Style! {
1051                // button in child.
1052                click_mode = ClickMode::default();
1053                corner_radius = (4, 0, 0, 4);
1054            };
1055
1056            zng_wgt_layer::popup::style_fn = Style! {
1057                zng_wgt_button::style_fn = Style! {
1058                    click_mode = ClickMode::release();
1059
1060                    corner_radius = 0;
1061                    padding = 2;
1062                    border = unset!;
1063                };
1064                crate::style_fn = Style! {
1065                    click_mode = ClickMode::release();
1066
1067                    corner_radius = 0;
1068                    padding = 2;
1069                    border = unset!;
1070                };
1071
1072                // supports gesture of press-and-drag to select.
1073                //
1074                // - `Toggle!` inherits `capture_pointer = true` from `Button!`.
1075                // - `ComboStyle!` sets `click_mode = press`.
1076                // - `ComboStyle!` sets popup descendant `Button!` to `click_mode = release`.
1077                //
1078                // So the user can press to open the drop-down, then drag over an option and release to select it.
1079                capture_pointer_on_init = CaptureMode::Subtree;
1080
1081                #[easing(100.ms())]
1082                opacity = 0.pct();
1083                #[easing(100.ms())]
1084                y = -10;
1085
1086                when *#is_inited {
1087                    opacity = 100.pct();
1088                    y = 0;
1089                }
1090
1091                zng_wgt_layer::popup::close_delay = 100.ms();
1092                when *#zng_wgt_layer::popup::is_close_delaying {
1093                    opacity = 0.pct();
1094                    y = -10;
1095                }
1096            };
1097        }
1098    }
1099}
1100context_var! {
1101    /// Spacing between the arrow symbol and the content.
1102    pub static COMBO_SPACING_VAR: Length = 0;
1103}
1104
1105/// Spacing between the arrow symbol and the content.
1106#[property(CONTEXT, default(COMBO_SPACING_VAR), widget_impl(ComboStyle))]
1107pub fn combo_spacing(child: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
1108    with_context_var(child, COMBO_SPACING_VAR, spacing)
1109}
1110
1111/// Popup open when the toggle button is checked.
1112///
1113/// This property can be used together with the [`ComboStyle!`] to implement a *combo-box* flyout widget.
1114///
1115/// The `popup` can be any widget, that will be open using [`POPUP`], a [`Popup!`] or derived widget is recommended.
1116///
1117/// Note that if the checked property is not set the toggle will never be checked, to implement a drop-down menu
1118/// set `checked = var(false);`.
1119///
1120/// [`ComboStyle!`]: struct@ComboStyle
1121/// [`Popup!`]: struct@zng_wgt_layer::popup::Popup
1122#[property(CHILD, widget_impl(Toggle))]
1123pub fn checked_popup(child: impl UiNode, popup: impl IntoVar<WidgetFn<()>>) -> impl UiNode {
1124    let popup = popup.into_var();
1125    let mut state = var(PopupState::Closed).read_only();
1126    let mut _state_handle = VarHandle::dummy();
1127    match_node(child, move |_, op| {
1128        let new = match op {
1129            UiNodeOp::Init => {
1130                WIDGET.sub_var(&IS_CHECKED_VAR).sub_event(&MOUSE_INPUT_EVENT);
1131                IS_CHECKED_VAR.get()
1132            }
1133            UiNodeOp::Deinit => {
1134                _state_handle = VarHandle::dummy();
1135                Some(false)
1136            }
1137            UiNodeOp::Event { update } => {
1138                if let Some(args) = MOUSE_INPUT_EVENT.on(update) {
1139                    // close on mouse down to avoid issue when the popup closes on mouse-down (due to focus loss),
1140                    // but a click is formed (down+up) on the toggle that immediately opens the popup again.
1141                    if args.is_mouse_down() && args.is_primary() && IS_CHECKED_VAR.get() == Some(true) {
1142                        args.propagation().stop();
1143                        cmd::TOGGLE_CMD.scoped(WIDGET.id()).notify_param(Some(false));
1144                    }
1145                }
1146                None
1147            }
1148            UiNodeOp::Update { .. } => {
1149                if let Some(s) = state.get_new() {
1150                    if matches!(s, PopupState::Closed) {
1151                        if IS_CHECKED_VAR.get() != Some(false) {
1152                            cmd::TOGGLE_CMD.scoped(WIDGET.id()).notify_param(Some(false));
1153                        }
1154                        _state_handle = VarHandle::dummy();
1155                    }
1156                    None
1157                } else {
1158                    IS_CHECKED_VAR.get_new().map(|o| o.unwrap_or(false))
1159                }
1160            }
1161            _ => None,
1162        };
1163        if let Some(open) = new {
1164            if open {
1165                if matches!(state.get(), PopupState::Closed) {
1166                    state = POPUP.open(popup.get()(()));
1167                    _state_handle = state.subscribe(UpdateOp::Update, WIDGET.id());
1168                }
1169            } else if let PopupState::Open(id) = state.get() {
1170                POPUP.close_id(id);
1171            }
1172        }
1173    })
1174}
1175
1176fn combomark_visual() -> impl UiNode {
1177    let dropdown = ICONS.get_or(
1178        ["toggle.dropdown", "material/rounded/keyboard-arrow-down", "keyboard-arrow-down"],
1179        combomark_visual_fallback,
1180    );
1181    Wgt! {
1182        size = 12;
1183        zng_wgt_fill::background = dropdown;
1184        align = Align::CENTER;
1185
1186        zng_wgt_transform::rotate_x = 0.deg();
1187        when #is_checked {
1188            zng_wgt_transform::rotate_x = 180.deg();
1189        }
1190    }
1191}
1192fn combomark_visual_fallback() -> impl UiNode {
1193    let color_key = FrameValueKey::new_unique();
1194    let mut size = PxSize::zero();
1195    let mut bounds = PxBox::zero();
1196    let mut transform = PxTransform::identity();
1197
1198    // (8x8) at 45º, scaled-x 70%
1199    fn layout() -> (PxSize, PxTransform, PxBox) {
1200        let size = Size::from(8).layout();
1201        let center = size.to_vector() * 0.5.fct();
1202        let transform = Transform::new_translate(-center.x, -center.y)
1203            .rotate(45.deg())
1204            .scale_x(0.7)
1205            .translate(center.x, center.y)
1206            .translate_x(Length::from(2).layout_x())
1207            .layout();
1208        let bounds = transform.outer_transformed(PxBox::from_size(size)).unwrap_or_default();
1209        (size, transform, bounds)
1210    }
1211
1212    match_node_leaf(move |op| match op {
1213        UiNodeOp::Init => {
1214            WIDGET.sub_var_render_update(&zng_wgt_text::FONT_COLOR_VAR);
1215        }
1216        UiNodeOp::Measure { desired_size, .. } => {
1217            let (s, _, _) = layout();
1218            *desired_size = s;
1219        }
1220        UiNodeOp::Layout { final_size, .. } => {
1221            (size, transform, bounds) = layout();
1222            *final_size = size;
1223        }
1224        UiNodeOp::Render { frame } => {
1225            let mut clip = bounds.to_rect();
1226            clip.size.height *= 0.5.fct();
1227            clip.origin.y += clip.size.height;
1228
1229            frame.push_clip_rect(clip, false, false, |frame| {
1230                frame.push_reference_frame((WIDGET.id(), 0).into(), transform.into(), false, false, |frame| {
1231                    frame.push_color(PxRect::from_size(size), color_key.bind_var(&zng_wgt_text::FONT_COLOR_VAR, |&c| c));
1232                })
1233            });
1234        }
1235        UiNodeOp::RenderUpdate { update } => {
1236            update.update_color_opt(color_key.update_var(&zng_wgt_text::FONT_COLOR_VAR, |&c| c));
1237        }
1238        _ => {}
1239    })
1240}
1241
1242/// Switch toggle style.
1243///
1244/// Style a [`Toggle!`] widget to look like a *switch*.
1245///
1246/// [`Toggle!`]: struct@crate::Toggle
1247#[widget($crate::SwitchStyle)]
1248pub struct SwitchStyle(Style);
1249impl SwitchStyle {
1250    fn widget_intrinsic(&mut self) {
1251        widget_set! {
1252            self;
1253            replace = true;
1254
1255            child_start = {
1256                node: {
1257                    let parent_hovered = var(false);
1258                    is_hovered(switch_visual(parent_hovered.clone()), parent_hovered)
1259                },
1260                spacing: SWITCH_SPACING_VAR,
1261            };
1262        }
1263    }
1264}
1265context_var! {
1266    /// Spacing between the switch and the content.
1267    pub static SWITCH_SPACING_VAR: Length = 2;
1268}
1269
1270/// Spacing between the switch and the content.
1271#[property(CONTEXT, default(SWITCH_SPACING_VAR), widget_impl(SwitchStyle))]
1272pub fn switch_spacing(child: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
1273    with_context_var(child, SWITCH_SPACING_VAR, spacing)
1274}
1275
1276fn switch_visual(parent_hovered: impl Var<bool>) -> impl UiNode {
1277    zng_wgt_container::Container! {
1278        hit_test_mode = false;
1279        size = (2.em(), 1.em());
1280        align = Align::CENTER;
1281        corner_radius = 1.em();
1282        padding = 2;
1283        child = Wgt! {
1284            size = 1.em() - Length::from(4);
1285            align = Align::LEFT;
1286            background_color = zng_wgt_text::FONT_COLOR_VAR;
1287
1288            #[easing(150.ms())]
1289            x = 0.em();
1290            when *#is_checked {
1291                x = 1.em();
1292            }
1293        };
1294
1295        #[easing(150.ms())]
1296        background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()));
1297        when *#{parent_hovered} {
1298            #[easing(0.ms())]
1299            background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
1300        }
1301        when #is_checked {
1302            background_color = colors::ACCENT_COLOR_VAR.shade(-1);
1303        }
1304    }
1305}
1306
1307/// Radio toggle style.
1308///
1309/// Style a [`Toggle!`] widget to look like a *radio button*.
1310///
1311/// [`Toggle!`]: struct@Toggle
1312#[widget($crate::RadioStyle)]
1313pub struct RadioStyle(Style);
1314impl RadioStyle {
1315    fn widget_intrinsic(&mut self) {
1316        widget_set! {
1317            self;
1318            replace = true;
1319
1320            access_role = AccessRole::Radio;
1321            child_start = {
1322                node: {
1323                    let parent_hovered = var(false);
1324                    is_hovered(radio_visual(parent_hovered.clone()), parent_hovered)
1325                },
1326                spacing: RADIO_SPACING_VAR,
1327            };
1328        }
1329    }
1330}
1331
1332context_var! {
1333    /// Spacing between the radio and the content.
1334    pub static RADIO_SPACING_VAR: Length = 2;
1335}
1336
1337/// Spacing between the radio and the content.
1338#[property(CONTEXT, default(RADIO_SPACING_VAR), widget_impl(RadioStyle))]
1339pub fn radio_spacing(child: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
1340    with_context_var(child, RADIO_SPACING_VAR, spacing)
1341}
1342
1343fn radio_visual(parent_hovered: impl Var<bool>) -> impl UiNode {
1344    Wgt! {
1345        hit_test_mode = false;
1346        size = 0.9.em();
1347        corner_radius = 0.9.em();
1348        align = Align::CENTER;
1349        border_align = 100.pct();
1350
1351        #[easing(150.ms())]
1352        background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()));
1353        when *#{parent_hovered} {
1354            #[easing(0.ms())]
1355            background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
1356        }
1357
1358        when *#is_checked {
1359            border = {
1360                widths: 2,
1361                sides: colors::ACCENT_COLOR_VAR.shade_into(-2),
1362            };
1363            #[easing(0.ms())]
1364            background_color = zng_wgt_text::FONT_COLOR_VAR;
1365        }
1366    }
1367}