zng_wgt_toggle/
lib.rs

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