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