Skip to main content

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