1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12zng_wgt::enable_widget_macros!();
13
14use std::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#[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 pub static IS_CHECKED_VAR: Option<bool> = false;
65
66 pub static IS_TRISTATE_VAR: bool = false;
68}
69
70#[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#[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#[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#[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#[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 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 } else {
274 tracing::error!("failed to select `{value:?}`, {e}");
275 }
276 selected
277 }
278 }
279 }
280 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 } 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 app_local! {
322 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 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 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; if let Some(selector) = selector.upgrade()
371 && zng_ext_window::WINDOWS.widget_info(id).is_none()
372 {
373 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 selected = Some(if checked.get() == Some(true) && SELECT_ON_NEW_VAR.get() {
453 select(new)
454 } else {
455 is_selected(new)
456 });
457
458 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 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 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#[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#[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#[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#[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#[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#[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 pub static SELECTOR: Selector = Selector::nil();
589}
590
591context_var! {
592 pub static SELECT_ON_INIT_VAR: bool = false;
599
600 pub static DESELECT_ON_DEINIT_VAR: bool = false;
607
608 pub static SELECT_ON_NEW_VAR: bool = true;
615
616 pub static DESELECT_ON_NEW_VAR: bool = false;
623
624 pub static SCROLL_ON_SELECT_VAR: bool = true;
630}
631
632pub trait SelectorImpl: Send + 'static {
634 fn subscribe(&self);
638
639 fn select(&mut self, value: BoxAnyVarValue) -> Result<(), SelectorError>;
641
642 fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError>;
644
645 fn is_selected(&self, value: &dyn AnyVarValue) -> bool;
647}
648
649#[derive(Clone)]
656pub struct Selector(Arc<Mutex<dyn SelectorImpl>>);
657impl Selector {
658 pub fn new(selector: impl SelectorImpl) -> Self {
660 Self(Arc::new(Mutex::new(selector)))
661 }
662
663 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 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 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 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 pub fn subscribe(&self) {
866 self.0.lock().subscribe();
867 }
868
869 pub fn select(&self, value: BoxAnyVarValue) -> Result<(), SelectorError> {
871 self.0.lock().select(value)
872 }
873
874 pub fn deselect(&self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
876 self.0.lock().deselect(value)
877 }
878
879 pub fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
881 self.0.lock().is_selected(value)
882 }
883
884 pub fn downgrade(&self) -> WeakSelector {
886 WeakSelector(Arc::downgrade(&self.0))
887 }
888
889 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
910pub struct WeakSelector(std::sync::Weak<Mutex<dyn SelectorImpl>>);
912impl WeakSelector {
913 pub fn upgrade(&self) -> Option<Selector> {
915 self.0.upgrade().map(Selector)
916 }
917
918 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#[derive(Debug, Clone)]
936#[non_exhaustive]
937pub enum SelectorError {
938 WrongType,
940 ReadOnly,
942 CannotClear,
944 Custom(Arc<dyn Error + Send + Sync>),
946}
947impl SelectorError {
948 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#[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#[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#[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#[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 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 capture_pointer_on_init = CaptureMode::Subtree;
1153
1154 #[easing(100.ms())]
1155 opacity = 0.pct();
1156 #[easing(100.ms())]
1157 y = -10;
1158 #[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#[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 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 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#[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#[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}