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::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#[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 pub static IS_CHECKED_VAR: Option<bool> = false;
61
62 pub static IS_TRISTATE_VAR: bool = false;
64}
65
66#[property(CONTEXT, default(false), widget_impl(Toggle))]
72pub fn checked(child: impl IntoUiNode, checked: impl IntoVar<bool>) -> UiNode {
73 let checked = checked.into_var();
74 let mut _toggle_handle = CommandHandle::dummy();
75 let mut access_handle = VarHandle::dummy();
76 let node = match_node(
77 child,
78 clmv!(checked, |child, op| match op {
79 UiNodeOp::Init => {
80 WIDGET.sub_event(&CLICK_EVENT);
81 _toggle_handle = cmd::TOGGLE_CMD.scoped(WIDGET.id()).subscribe(true);
82 }
83 UiNodeOp::Deinit => {
84 _toggle_handle = CommandHandle::dummy();
85 access_handle = VarHandle::dummy();
86 }
87 UiNodeOp::Info { info } => {
88 if let Some(mut a) = info.access() {
89 if access_handle.is_dummy() {
90 access_handle = checked.subscribe(UpdateOp::Info, WIDGET.id());
91 }
92 a.set_checked(Some(checked.get()));
93 }
94 }
95 UiNodeOp::Event { update } => {
96 child.event(update);
97
98 if let Some(args) = CLICK_EVENT.on(update) {
99 if args.is_primary()
100 && checked.capabilities().contains(VarCapability::MODIFY)
101 && !args.propagation().is_stopped()
102 && args.target.contains_enabled(WIDGET.id())
103 {
104 args.propagation().stop();
105
106 checked.set(!checked.get());
107 }
108 } else if let Some(args) = cmd::TOGGLE_CMD.scoped(WIDGET.id()).on_unhandled(update) {
109 if let Some(b) = args.param::<bool>() {
110 args.propagation().stop();
111 checked.set(*b);
112 } else if let Some(b) = args.param::<Option<bool>>() {
113 if let Some(b) = b {
114 args.propagation().stop();
115 checked.set(*b);
116 }
117 } else if args.param.is_none() {
118 args.propagation().stop();
119 checked.set(!checked.get());
120 }
121 }
122 }
123 _ => {}
124 }),
125 );
126 with_context_var(node, IS_CHECKED_VAR, checked.map_into())
127}
128
129#[property(CONTEXT + 1, default(None), widget_impl(Toggle))]
132pub fn checked_opt(child: impl IntoUiNode, checked: impl IntoVar<Option<bool>>) -> UiNode {
133 let checked = checked.into_var();
134 let mut _toggle_handle = CommandHandle::dummy();
135 let mut access_handle = VarHandle::dummy();
136
137 let node = match_node(
138 child,
139 clmv!(checked, |child, op| match op {
140 UiNodeOp::Init => {
141 WIDGET.sub_event(&CLICK_EVENT);
142 _toggle_handle = cmd::TOGGLE_CMD.scoped(WIDGET.id()).subscribe(true);
143 }
144 UiNodeOp::Deinit => {
145 _toggle_handle = CommandHandle::dummy();
146 access_handle = VarHandle::dummy();
147 }
148 UiNodeOp::Info { info } => {
149 if let Some(mut a) = info.access() {
150 if access_handle.is_dummy() {
151 access_handle = checked.subscribe(UpdateOp::Info, WIDGET.id());
152 }
153 a.set_checked(checked.get());
154 }
155 }
156 UiNodeOp::Event { update } => {
157 child.event(update);
158
159 let mut cycle = false;
160
161 if let Some(args) = CLICK_EVENT.on(update) {
162 if args.is_primary()
163 && checked.capabilities().contains(VarCapability::MODIFY)
164 && !args.propagation().is_stopped()
165 && args.target.contains_enabled(WIDGET.id())
166 {
167 args.propagation().stop();
168
169 cycle = true;
170 }
171 } else if let Some(args) = cmd::TOGGLE_CMD.scoped(WIDGET.id()).on_unhandled(update) {
172 if let Some(b) = args.param::<bool>() {
173 args.propagation().stop();
174 checked.set(Some(*b));
175 } else if let Some(b) = args.param::<Option<bool>>() {
176 if IS_TRISTATE_VAR.get() {
177 args.propagation().stop();
178 checked.set(*b);
179 } else if let Some(b) = b {
180 args.propagation().stop();
181 checked.set(Some(*b));
182 }
183 } else if args.param.is_none() {
184 args.propagation().stop();
185
186 cycle = true;
187 }
188 }
189
190 if cycle {
191 if IS_TRISTATE_VAR.get() {
192 checked.set(match checked.get() {
193 Some(true) => None,
194 Some(false) => Some(true),
195 None => Some(false),
196 });
197 } else {
198 checked.set(match checked.get() {
199 Some(true) | None => Some(false),
200 Some(false) => Some(true),
201 });
202 }
203 }
204 }
205 _ => {}
206 }),
207 );
208
209 with_context_var(node, IS_CHECKED_VAR, checked)
210}
211
212#[property(CONTEXT, default(IS_TRISTATE_VAR), widget_impl(Toggle))]
222pub fn tristate(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
223 with_context_var(child, IS_TRISTATE_VAR, enabled)
224}
225
226#[property(EVENT, widget_impl(Toggle, DefaultStyle))]
230pub fn is_checked(child: impl IntoUiNode, state: impl IntoVar<bool>) -> UiNode {
231 bind_state(child, IS_CHECKED_VAR.map(|s| *s == Some(true)), state)
232}
233
234#[property(CONTEXT+2, widget_impl(Toggle))]
252pub fn value<T: VarValue>(child: impl IntoUiNode, value: impl IntoVar<T>) -> UiNode {
253 value_impl(child, value.into_var().into())
254}
255fn value_impl(child: impl IntoUiNode, value: AnyVar) -> UiNode {
256 fn select(value: &dyn AnyVarValue) -> bool {
258 let selector = SELECTOR.get();
259 match selector.select(value.clone_boxed()) {
260 Ok(()) => true,
261 Err(e) => {
262 let selected = selector.is_selected(value);
263 if selected {
264 tracing::error!("selected `{value:?}` with error, {e}");
265 } else if let SelectorError::ReadOnly | SelectorError::CannotClear = e {
266 } else {
268 tracing::error!("failed to select `{value:?}`, {e}");
269 }
270 selected
271 }
272 }
273 }
274 fn deselect(value: &dyn AnyVarValue) -> bool {
276 let selector = SELECTOR.get();
277 match selector.deselect(value) {
278 Ok(()) => true,
279 Err(e) => {
280 let deselected = !selector.is_selected(value);
281 if deselected {
282 tracing::error!("deselected `{value:?}` with error, {e}");
283 } else if let SelectorError::ReadOnly | SelectorError::CannotClear = e {
284 } else {
286 tracing::error!("failed to deselect `{value:?}`, {e}");
287 }
288 deselected
289 }
290 }
291 }
292 fn is_selected(value: &dyn AnyVarValue) -> bool {
293 SELECTOR.get().is_selected(value)
294 }
295
296 let checked = var(Some(false));
297 let child = with_context_var(child, IS_CHECKED_VAR, checked.clone());
298 let mut prev_value = None::<BoxAnyVarValue>;
299
300 let mut _click_handle = None;
301 let mut _toggle_handle = CommandHandle::dummy();
302 let mut _select_handle = CommandHandle::dummy();
303
304 match_node(child, move |child, op| match op {
305 UiNodeOp::Init => {
306 let id = WIDGET.id();
307 WIDGET.sub_var(&value).sub_var(&DESELECT_ON_NEW_VAR).sub_var(&checked);
308 let selector = SELECTOR.get();
309 selector.subscribe();
310
311 value.with(|value| {
312 let select_on_init = SELECT_ON_INIT_VAR.get() && {
313 app_local! {
316 static SELECTED_ON_INIT: IdMap<WidgetId, WeakSelector> = IdMap::new();
318 }
319 let mut map = SELECTED_ON_INIT.write();
320 map.retain(|_, v| v.strong_count() > 0);
321 let selector_wk = selector.downgrade();
322 match map.entry(id) {
323 hashbrown::hash_map::Entry::Occupied(mut e) => {
324 let changed_ctx = e.get() != &selector_wk;
325 if changed_ctx {
326 e.insert(selector_wk);
327 }
328 changed_ctx
330 }
331 hashbrown::hash_map::Entry::Vacant(e) => {
332 e.insert(selector_wk);
333 true
334 }
335 }
336 };
337
338 let selected = if select_on_init { select(value) } else { is_selected(value) };
339
340 checked.set(Some(selected));
341
342 if DESELECT_ON_DEINIT_VAR.get() {
343 prev_value = Some(value.clone_boxed());
344 }
345 });
346
347 _click_handle = Some(CLICK_EVENT.subscribe(id));
348 _toggle_handle = cmd::TOGGLE_CMD.scoped(id).subscribe(true);
349 _select_handle = cmd::SELECT_CMD.scoped(id).subscribe(true);
350 }
351 UiNodeOp::Deinit => {
352 if checked.get() == Some(true) && DESELECT_ON_DEINIT_VAR.get() {
353 let value = value.get();
355 let selector = SELECTOR.get().downgrade();
356 let checked = checked.downgrade();
357 let id = WIDGET.id();
358 UPDATES
359 .run(async move {
360 task::yield_now().await; if let Some(selector) = selector.upgrade()
363 && zng_ext_window::WINDOWS.widget_info(id).is_none()
364 {
365 let deselected = match selector.deselect(&*value) {
367 Ok(()) => true,
368 Err(_) => !selector.is_selected(&*value),
369 };
370 if deselected && let Some(c) = checked.upgrade() {
371 c.set(false);
372 }
373 }
374 })
375 .perm();
376 }
377
378 prev_value = None;
379 _click_handle = None;
380 _toggle_handle = CommandHandle::dummy();
381 _select_handle = CommandHandle::dummy();
382 }
383 UiNodeOp::Event { update } => {
384 child.event(update);
385
386 if let Some(args) = CLICK_EVENT.on(update) {
387 if args.is_primary() && !args.propagation().is_stopped() && args.target.contains_enabled(WIDGET.id()) {
388 args.propagation().stop();
389
390 value.with(|value| {
391 let selected = if checked.get() == Some(true) {
392 !deselect(value)
393 } else {
394 select(value)
395 };
396 checked.set(Some(selected))
397 });
398 }
399 } else if let Some(args) = cmd::TOGGLE_CMD.scoped(WIDGET.id()).on_unhandled(update) {
400 if args.param.is_none() {
401 args.propagation().stop();
402
403 value.with(|value| {
404 let selected = if checked.get() == Some(true) {
405 !deselect(value)
406 } else {
407 select(value)
408 };
409 checked.set(Some(selected))
410 });
411 } else {
412 let s = if let Some(s) = args.param::<Option<bool>>() {
413 Some(s.unwrap_or(false))
414 } else {
415 args.param::<bool>().copied()
416 };
417 if let Some(s) = s {
418 args.propagation().stop();
419
420 value.with(|value| {
421 let selected = if s { select(value) } else { !deselect(value) };
422 checked.set(Some(selected))
423 });
424 }
425 }
426 } else if let Some(args) = cmd::SELECT_CMD.scoped(WIDGET.id()).on_unhandled(update)
427 && args.param.is_none()
428 {
429 args.propagation().stop();
430 value.with(|value| {
431 let selected = checked.get() == Some(true);
432 if !selected && select(value) {
433 checked.set(Some(true));
434 }
435 });
436 }
437 }
438 UiNodeOp::Update { .. } => {
439 let mut selected = None;
440 value.with_new(|new| {
441 selected = Some(if checked.get() == Some(true) && SELECT_ON_NEW_VAR.get() {
443 select(new)
444 } else {
445 is_selected(new)
446 });
447
448 if let Some(prev) = prev_value.take()
450 && DESELECT_ON_NEW_VAR.get()
451 {
452 deselect(&*prev);
453 prev_value = Some(new.clone_boxed());
454 }
455 });
456 let selected = selected.unwrap_or_else(|| {
457 let mut s = false;
459 value.with(|v| {
460 s = is_selected(v);
461 });
462 s
463 });
464 checked.set(selected);
465
466 if DESELECT_ON_NEW_VAR.get() && selected {
467 if prev_value.is_none() {
469 prev_value = Some(value.get());
470 }
471 } else {
472 prev_value = None;
473 }
474
475 if let Some(Some(true)) = checked.get_new()
476 && SCROLL_ON_SELECT_VAR.get()
477 {
478 use zng_wgt_scroll::cmd::*;
479 scroll_to(WIDGET.id(), ScrollToMode::minimal(10));
480 }
481 }
482 _ => {}
483 })
484}
485
486#[property(CONTEXT, default(SCROLL_ON_SELECT_VAR), widget_impl(Toggle, DefaultStyle))]
492pub fn scroll_on_select(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
493 with_context_var(child, SCROLL_ON_SELECT_VAR, enabled)
494}
495
496#[property(CONTEXT, default(Selector::nil()), widget_impl(Toggle))]
510pub fn selector(child: impl IntoUiNode, selector: impl IntoValue<Selector>) -> UiNode {
511 let mut _select_handle = CommandHandle::dummy();
512 let child = match_node(child, move |c, op| match op {
513 UiNodeOp::Init => {
514 _select_handle = cmd::SELECT_CMD.scoped(WIDGET.id()).subscribe(true);
515 }
516 UiNodeOp::Info { info } => {
517 if let Some(mut info) = info.access() {
518 info.set_role(AccessRole::RadioGroup);
519 }
520 }
521 UiNodeOp::Deinit => {
522 _select_handle = CommandHandle::dummy();
523 }
524 UiNodeOp::Event { update } => {
525 c.event(update);
526
527 if let Some(args) = cmd::SELECT_CMD.scoped(WIDGET.id()).on_unhandled(update)
528 && let Some(p) = args.param::<cmd::SelectOp>()
529 {
530 args.propagation().stop();
531
532 p.call();
533 }
534 }
535 _ => {}
536 });
537 with_context_local(child, &SELECTOR, selector)
538}
539
540#[property(CONTEXT, default(SELECT_ON_INIT_VAR), widget_impl(Toggle))]
546pub fn select_on_init(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
547 with_context_var(child, SELECT_ON_INIT_VAR, enabled)
548}
549
550#[property(CONTEXT, default(DESELECT_ON_DEINIT_VAR), widget_impl(Toggle))]
556pub fn deselect_on_deinit(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
557 with_context_var(child, DESELECT_ON_DEINIT_VAR, enabled)
558}
559
560#[property(CONTEXT, default(SELECT_ON_NEW_VAR), widget_impl(Toggle))]
564pub fn select_on_new(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
565 with_context_var(child, SELECT_ON_NEW_VAR, enabled)
566}
567
568#[property(CONTEXT, default(DESELECT_ON_NEW_VAR), widget_impl(Toggle))]
572pub fn deselect_on_new(child: impl IntoUiNode, enabled: impl IntoVar<bool>) -> UiNode {
573 with_context_var(child, DESELECT_ON_NEW_VAR, enabled)
574}
575
576context_local! {
577 pub static SELECTOR: Selector = Selector::nil();
579}
580
581context_var! {
582 pub static SELECT_ON_INIT_VAR: bool = false;
589
590 pub static DESELECT_ON_DEINIT_VAR: bool = false;
597
598 pub static SELECT_ON_NEW_VAR: bool = true;
605
606 pub static DESELECT_ON_NEW_VAR: bool = false;
613
614 pub static SCROLL_ON_SELECT_VAR: bool = true;
620}
621
622pub trait SelectorImpl: Send + 'static {
624 fn subscribe(&self);
628
629 fn select(&mut self, value: BoxAnyVarValue) -> Result<(), SelectorError>;
631
632 fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError>;
634
635 fn is_selected(&self, value: &dyn AnyVarValue) -> bool;
637}
638
639#[derive(Clone)]
646pub struct Selector(Arc<Mutex<dyn SelectorImpl>>);
647impl Selector {
648 pub fn new(selector: impl SelectorImpl) -> Self {
650 Self(Arc::new(Mutex::new(selector)))
651 }
652
653 pub fn nil() -> Self {
655 struct NilSel;
656 impl SelectorImpl for NilSel {
657 fn subscribe(&self) {}
658
659 fn select(&mut self, _: BoxAnyVarValue) -> Result<(), SelectorError> {
660 Err(SelectorError::custom_str("no contextual `selector`"))
661 }
662
663 fn deselect(&mut self, _: &dyn AnyVarValue) -> Result<(), SelectorError> {
664 Ok(())
665 }
666
667 fn is_selected(&self, __r: &dyn AnyVarValue) -> bool {
668 false
669 }
670 }
671 Self::new(NilSel)
672 }
673
674 pub fn single<T>(selection: impl IntoVar<T>) -> Self
676 where
677 T: VarValue,
678 {
679 struct SingleSel<T: VarValue> {
680 selection: Var<T>,
681 }
682 impl<T: VarValue> SelectorImpl for SingleSel<T> {
683 fn subscribe(&self) {
684 WIDGET.sub_var(&self.selection);
685 }
686
687 fn select(&mut self, value: BoxAnyVarValue) -> Result<(), SelectorError> {
688 match value.downcast::<T>() {
689 Ok(value) => match self.selection.try_set(value) {
690 Ok(_) => Ok(()),
691 Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
692 },
693 Err(_) => Err(SelectorError::WrongType),
694 }
695 }
696
697 fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
698 if self.is_selected(value) {
699 Err(SelectorError::CannotClear)
700 } else {
701 Ok(())
702 }
703 }
704
705 fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
706 match value.downcast_ref::<T>() {
707 Some(value) => self.selection.with(|t| t == value),
708 None => false,
709 }
710 }
711 }
712 Self::new(SingleSel {
713 selection: selection.into_var(),
714 })
715 }
716
717 pub fn single_opt<T>(selection: impl IntoVar<Option<T>>) -> Self
719 where
720 T: VarValue,
721 {
722 struct SingleOptSel<T: VarValue> {
723 selection: Var<Option<T>>,
724 }
725 impl<T: VarValue> SelectorImpl for SingleOptSel<T> {
726 fn subscribe(&self) {
727 WIDGET.sub_var(&self.selection);
728 }
729
730 fn select(&mut self, value: BoxAnyVarValue) -> Result<(), SelectorError> {
731 match value.downcast::<T>() {
732 Ok(value) => match self.selection.try_set(Some(value)) {
733 Ok(_) => Ok(()),
734 Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
735 },
736 Err(value) => match value.downcast::<Option<T>>() {
737 Ok(value) => match self.selection.try_set(value) {
738 Ok(_) => Ok(()),
739 Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
740 },
741 Err(_) => Err(SelectorError::WrongType),
742 },
743 }
744 }
745
746 fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
747 match value.downcast_ref::<T>() {
748 Some(value) => {
749 if self.selection.with(|t| t.as_ref() == Some(value)) {
750 match self.selection.try_set(None) {
751 Ok(_) => Ok(()),
752 Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
753 }
754 } else {
755 Ok(())
756 }
757 }
758 None => match value.downcast_ref::<Option<T>>() {
759 Some(value) => {
760 if self.selection.with(|t| t == value) {
761 if value.is_none() {
762 Ok(())
763 } else {
764 match self.selection.try_set(None) {
765 Ok(_) => Ok(()),
766 Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
767 }
768 }
769 } else {
770 Ok(())
771 }
772 }
773 None => Ok(()),
774 },
775 }
776 }
777
778 fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
779 match value.downcast_ref::<T>() {
780 Some(value) => self.selection.with(|t| t.as_ref() == Some(value)),
781 None => match value.downcast_ref::<Option<T>>() {
782 Some(value) => self.selection.with(|t| t == value),
783 None => false,
784 },
785 }
786 }
787 }
788 Self::new(SingleOptSel {
789 selection: selection.into_var(),
790 })
791 }
792
793 pub fn bitflags<T>(selection: impl IntoVar<T>) -> Self
795 where
796 T: VarValue + ops::BitOr<Output = T> + ops::BitAnd<Output = T> + ops::Not<Output = T>,
797 {
798 struct BitflagsSel<T: VarValue> {
799 selection: Var<T>,
800 }
801 impl<T> SelectorImpl for BitflagsSel<T>
802 where
803 T: VarValue + ops::BitOr<Output = T> + ops::BitAnd<Output = T> + ops::Not<Output = T>,
804 {
805 fn subscribe(&self) {
806 WIDGET.sub_var(&self.selection);
807 }
808
809 fn select(&mut self, value: BoxAnyVarValue) -> Result<(), SelectorError> {
810 match value.downcast::<T>() {
811 Ok(value) => self
812 .selection
813 .try_modify(move |m| {
814 let new = m.clone() | value;
815 if m.value() != &new {
816 m.set(new);
817 }
818 })
819 .map_err(|_| SelectorError::ReadOnly),
820 Err(_) => Err(SelectorError::WrongType),
821 }
822 }
823
824 fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
825 match value.downcast_ref::<T>() {
826 Some(value) => self
827 .selection
828 .try_modify(clmv!(value, |m| {
829 let new = m.value().clone() & !value;
830 if m.value() != &new {
831 m.set(new);
832 }
833 }))
834 .map_err(|_| SelectorError::ReadOnly),
835 None => Err(SelectorError::WrongType),
836 }
837 }
838
839 fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
840 match value.downcast_ref::<T>() {
841 Some(value) => &(self.selection.get() & value.clone()) == value,
842 None => false,
843 }
844 }
845 }
846
847 Self::new(BitflagsSel {
848 selection: selection.into_var(),
849 })
850 }
851
852 pub fn subscribe(&self) {
856 self.0.lock().subscribe();
857 }
858
859 pub fn select(&self, value: BoxAnyVarValue) -> Result<(), SelectorError> {
861 self.0.lock().select(value)
862 }
863
864 pub fn deselect(&self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
866 self.0.lock().deselect(value)
867 }
868
869 pub fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
871 self.0.lock().is_selected(value)
872 }
873
874 pub fn downgrade(&self) -> WeakSelector {
876 WeakSelector(Arc::downgrade(&self.0))
877 }
878
879 pub fn strong_count(&self) -> usize {
881 Arc::strong_count(&self.0)
882 }
883}
884impl<S: SelectorImpl> From<S> for Selector {
885 fn from(sel: S) -> Self {
886 Selector::new(sel)
887 }
888}
889impl fmt::Debug for Selector {
890 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
891 write!(f, "Selector(_)")
892 }
893}
894impl PartialEq for Selector {
895 fn eq(&self, other: &Self) -> bool {
896 Arc::ptr_eq(&self.0, &other.0)
897 }
898}
899
900pub struct WeakSelector(std::sync::Weak<Mutex<dyn SelectorImpl>>);
902impl WeakSelector {
903 pub fn upgrade(&self) -> Option<Selector> {
905 self.0.upgrade().map(Selector)
906 }
907
908 pub fn strong_count(&self) -> usize {
910 self.0.strong_count()
911 }
912}
913impl fmt::Debug for WeakSelector {
914 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
915 write!(f, "WeakSelector(_)")
916 }
917}
918impl PartialEq for WeakSelector {
919 fn eq(&self, other: &Self) -> bool {
920 self.0.ptr_eq(&other.0)
921 }
922}
923
924#[derive(Debug, Clone)]
926#[non_exhaustive]
927pub enum SelectorError {
928 WrongType,
930 ReadOnly,
932 CannotClear,
934 Custom(Arc<dyn Error + Send + Sync>),
936}
937impl SelectorError {
938 pub fn custom_str(str: impl Into<String>) -> SelectorError {
940 let str = str.into();
941 let e: Box<dyn Error + Send + Sync> = str.into();
942 let e: Arc<dyn Error + Send + Sync> = e.into();
943 SelectorError::Custom(e)
944 }
945}
946impl fmt::Display for SelectorError {
947 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
948 match self {
949 SelectorError::WrongType => write!(f, "wrong value type for selector"),
950 SelectorError::ReadOnly => write!(f, "selection is read-only"),
951 SelectorError::CannotClear => write!(f, "selection cannot be empty"),
952 SelectorError::Custom(e) => fmt::Display::fmt(e, f),
953 }
954 }
955}
956impl Error for SelectorError {
957 fn source(&self) -> Option<&(dyn Error + 'static)> {
958 match self {
959 SelectorError::WrongType => None,
960 SelectorError::ReadOnly => None,
961 SelectorError::CannotClear => None,
962 SelectorError::Custom(e) => Some(&**e),
963 }
964 }
965}
966impl From<VarIsReadOnlyError> for SelectorError {
967 fn from(_: VarIsReadOnlyError) -> Self {
968 SelectorError::ReadOnly
969 }
970}
971
972#[widget($crate::DefaultStyle)]
979pub struct DefaultStyle(zng_wgt_button::DefaultStyle);
980impl DefaultStyle {
981 fn widget_intrinsic(&mut self) {
982 widget_set! {
983 self;
984 replace = true;
985 when *#is_checked {
986 background_color = BASE_COLOR_VAR.shade(2);
987 border = {
988 widths: 1,
989 sides: BASE_COLOR_VAR.shade_into(2),
990 };
991 }
992 }
993 }
994}
995
996#[widget($crate::LightStyle)]
998pub struct LightStyle(zng_wgt_button::LightStyle);
999impl_named_style_fn!(light, LightStyle);
1000impl LightStyle {
1001 fn widget_intrinsic(&mut self) {
1002 widget_set! {
1003 self;
1004 named_style_fn = LIGHT_STYLE_FN_VAR;
1005 when *#is_checked {
1006 #[easing(0.ms())]
1007 background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
1008 }
1009 }
1010 }
1011}
1012
1013#[widget($crate::CheckStyle)]
1019pub struct CheckStyle(Style);
1020impl_named_style_fn!(check, CheckStyle);
1021impl CheckStyle {
1022 fn widget_intrinsic(&mut self) {
1023 widget_set! {
1024 self;
1025 replace = true;
1026 named_style_fn = CHECK_STYLE_FN_VAR;
1027 child_spacing = 4;
1028 child_start = {
1029 let parent_hovered = var(false);
1030 is_hovered(checkmark_visual(parent_hovered.clone()), parent_hovered)
1031 };
1032 access_role = AccessRole::CheckBox;
1033 }
1034 }
1035}
1036
1037fn checkmark_visual(parent_hovered: Var<bool>) -> UiNode {
1038 let checked = ICONS.get_or(["toggle.checked", "check"], || {
1039 zng_wgt_text::Text! {
1040 txt = "✓";
1041 font_family = FontNames::system_ui(&lang!(und));
1042 txt_align = Align::CENTER;
1043 }
1044 });
1045 let indeterminate = ICONS.get_or(["toggle.indeterminate"], || {
1046 zng_wgt::Wgt! {
1047 align = Align::CENTER;
1048 background_color = zng_wgt_text::FONT_COLOR_VAR;
1049 size = (6, 2);
1050 corner_radius = 0;
1051 }
1052 });
1053 zng_wgt_container::Container! {
1054 hit_test_mode = false;
1055 accessible = false;
1056 size = 1.2.em();
1057 corner_radius = 0.1.em();
1058 align = Align::CENTER;
1059
1060 #[easing(150.ms())]
1061 background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()));
1062 when *#{parent_hovered} {
1063 #[easing(0.ms())]
1064 background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
1065 }
1066
1067 when #{IS_CHECKED_VAR}.is_none() {
1068 child = indeterminate;
1069 }
1070 when *#{IS_CHECKED_VAR} == Some(true) {
1071 child = checked;
1072 #[easing(0.ms())]
1073 background_color = colors::ACCENT_COLOR_VAR.shade(-1);
1074 }
1075 }
1076}
1077
1078#[widget($crate::ComboStyle)]
1084pub struct ComboStyle(DefaultStyle);
1085impl_named_style_fn!(combo, ComboStyle);
1086impl ComboStyle {
1087 fn widget_intrinsic(&mut self) {
1088 widget_set! {
1089 self;
1090 replace = true;
1091 named_style_fn = COMBO_STYLE_FN_VAR;
1092
1093 access_role = AccessRole::ComboBox;
1094 child_align = Align::FILL;
1095 border_over = false;
1096 border_align = 1.fct();
1097 padding = -1;
1098 checked = var(false);
1099 child_end = combomark_visual();
1100
1101 click_mode = ClickMode::press();
1102
1103 zng_wgt_button::style_fn = Style! {
1104 click_mode = ClickMode::default();
1106 corner_radius = (4, 0, 0, 4);
1107 };
1108
1109 zng_wgt_layer::popup::style_fn = Style! {
1110 zng_wgt_button::style_fn = Style! {
1111 click_mode = ClickMode::release();
1112
1113 corner_radius = 0;
1114 padding = 2;
1115 border = unset!;
1116 };
1117 crate::style_fn = Style! {
1118 click_mode = ClickMode::release();
1119
1120 corner_radius = 0;
1121 padding = 2;
1122 border = unset!;
1123 };
1124
1125 capture_pointer_on_init = CaptureMode::Subtree;
1133
1134 #[easing(100.ms())]
1135 opacity = 0.pct();
1136 #[easing(100.ms())]
1137 y = -10;
1138
1139 when *#is_inited {
1140 opacity = 100.pct();
1141 y = 0;
1142 }
1143
1144 zng_wgt_layer::popup::close_delay = 100.ms();
1145 when *#zng_wgt_layer::popup::is_close_delaying {
1146 opacity = 0.pct();
1147 y = -10;
1148 }
1149 };
1150 }
1151 }
1152}
1153
1154#[property(CHILD, widget_impl(Toggle))]
1166pub fn checked_popup(child: impl IntoUiNode, popup: impl IntoVar<WidgetFn<()>>) -> UiNode {
1167 let popup = popup.into_var();
1168 let mut state = var(PopupState::Closed).read_only();
1169 let mut _state_handle = VarHandle::dummy();
1170 match_node(child, move |_, op| {
1171 let new = match op {
1172 UiNodeOp::Init => {
1173 WIDGET.sub_var(&IS_CHECKED_VAR).sub_event(&MOUSE_INPUT_EVENT);
1174 IS_CHECKED_VAR.get()
1175 }
1176 UiNodeOp::Deinit => {
1177 _state_handle = VarHandle::dummy();
1178 Some(false)
1179 }
1180 UiNodeOp::Event { update } => {
1181 if let Some(args) = MOUSE_INPUT_EVENT.on(update) {
1182 if args.is_mouse_down() && args.is_primary() && IS_CHECKED_VAR.get() == Some(true) {
1185 args.propagation().stop();
1186 cmd::TOGGLE_CMD.scoped(WIDGET.id()).notify_param(Some(false));
1187 }
1188 }
1189 None
1190 }
1191 UiNodeOp::Update { .. } => {
1192 if let Some(s) = state.get_new() {
1193 if matches!(s, PopupState::Closed) {
1194 if IS_CHECKED_VAR.get() != Some(false) {
1195 cmd::TOGGLE_CMD.scoped(WIDGET.id()).notify_param(Some(false));
1196 }
1197 _state_handle = VarHandle::dummy();
1198 }
1199 None
1200 } else {
1201 IS_CHECKED_VAR.get_new().map(|o| o.unwrap_or(false))
1202 }
1203 }
1204 _ => None,
1205 };
1206 if let Some(open) = new {
1207 if open {
1208 if matches!(state.get(), PopupState::Closed) {
1209 state = POPUP.open(popup.get()(()));
1210 _state_handle = state.subscribe(UpdateOp::Update, WIDGET.id());
1211 }
1212 } else if let PopupState::Open(id) = state.get() {
1213 POPUP.close_id(id);
1214 }
1215 }
1216 })
1217}
1218
1219fn combomark_visual() -> UiNode {
1220 let dropdown = ICONS.get_or(
1221 ["toggle.dropdown", "material/rounded/keyboard-arrow-down", "keyboard-arrow-down"],
1222 combomark_visual_fallback,
1223 );
1224 Wgt! {
1225 size = 12;
1226 zng_wgt_fill::background = dropdown;
1227 align = Align::CENTER;
1228
1229 zng_wgt_transform::rotate_x = 0.deg();
1230 when #is_checked {
1231 zng_wgt_transform::rotate_x = 180.deg();
1232 }
1233 }
1234}
1235fn combomark_visual_fallback() -> UiNode {
1236 let color_key = FrameValueKey::new_unique();
1237 let mut size = PxSize::zero();
1238 let mut bounds = PxBox::zero();
1239 let mut transform = PxTransform::identity();
1240
1241 fn layout() -> (PxSize, PxTransform, PxBox) {
1243 let size = Size::from(8).layout();
1244 let center = size.to_vector() * 0.5.fct();
1245 let transform = Transform::new_translate(-center.x, -center.y)
1246 .rotate(45.deg())
1247 .scale_x(0.7)
1248 .translate(center.x, center.y)
1249 .translate_x(Length::from(2).layout_x())
1250 .layout();
1251 let bounds = transform.outer_transformed(PxBox::from_size(size)).unwrap_or_default();
1252 (size, transform, bounds)
1253 }
1254
1255 match_node_leaf(move |op| match op {
1256 UiNodeOp::Init => {
1257 WIDGET.sub_var_render_update(&zng_wgt_text::FONT_COLOR_VAR);
1258 }
1259 UiNodeOp::Measure { desired_size, .. } => {
1260 let (s, _, _) = layout();
1261 *desired_size = s;
1262 }
1263 UiNodeOp::Layout { final_size, .. } => {
1264 (size, transform, bounds) = layout();
1265 *final_size = size;
1266 }
1267 UiNodeOp::Render { frame } => {
1268 let mut clip = bounds.to_rect();
1269 clip.size.height *= 0.5.fct();
1270 clip.origin.y += clip.size.height;
1271
1272 frame.push_clip_rect(clip, false, false, |frame| {
1273 frame.push_reference_frame((WIDGET.id(), 0).into(), transform.into(), false, false, |frame| {
1274 frame.push_color(PxRect::from_size(size), color_key.bind_var(&zng_wgt_text::FONT_COLOR_VAR, |&c| c));
1275 })
1276 });
1277 }
1278 UiNodeOp::RenderUpdate { update } => {
1279 update.update_color_opt(color_key.update_var(&zng_wgt_text::FONT_COLOR_VAR, |&c| c));
1280 }
1281 _ => {}
1282 })
1283}
1284
1285#[widget($crate::SwitchStyle)]
1291pub struct SwitchStyle(Style);
1292impl_named_style_fn!(switch, SwitchStyle);
1293impl SwitchStyle {
1294 fn widget_intrinsic(&mut self) {
1295 widget_set! {
1296 self;
1297 replace = true;
1298 named_style_fn = SWITCH_STYLE_FN_VAR;
1299
1300 child_spacing = 2;
1301 child_start = {
1302 let parent_hovered = var(false);
1303 is_hovered(switch_visual(parent_hovered.clone()), parent_hovered)
1304 };
1305 }
1306 }
1307}
1308
1309fn switch_visual(parent_hovered: Var<bool>) -> UiNode {
1310 zng_wgt_container::Container! {
1311 hit_test_mode = false;
1312 size = (2.em(), 1.em());
1313 align = Align::CENTER;
1314 corner_radius = 1.em();
1315 padding = 2;
1316 child = Wgt! {
1317 size = 1.em() - Length::from(4);
1318 align = Align::LEFT;
1319 background_color = zng_wgt_text::FONT_COLOR_VAR;
1320
1321 #[easing(150.ms())]
1322 x = 0.em();
1323 when *#is_checked {
1324 x = 1.em();
1325 }
1326 };
1327
1328 #[easing(150.ms())]
1329 background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()));
1330 when *#{parent_hovered} {
1331 #[easing(0.ms())]
1332 background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
1333 }
1334 when #is_checked {
1335 background_color = colors::ACCENT_COLOR_VAR.shade(-1);
1336 }
1337 }
1338}
1339
1340#[widget($crate::RadioStyle)]
1346pub struct RadioStyle(Style);
1347impl_named_style_fn!(radio, RadioStyle);
1348impl RadioStyle {
1349 fn widget_intrinsic(&mut self) {
1350 widget_set! {
1351 self;
1352 replace = true;
1353 named_style_fn = RADIO_STYLE_FN_VAR;
1354
1355 access_role = AccessRole::Radio;
1356 child_spacing = 2;
1357 child_start = {
1358 let parent_hovered = var(false);
1359 is_hovered(radio_visual(parent_hovered.clone()), parent_hovered)
1360 };
1361 }
1362 }
1363}
1364
1365fn radio_visual(parent_hovered: Var<bool>) -> UiNode {
1366 Wgt! {
1367 hit_test_mode = false;
1368 size = 0.9.em();
1369 corner_radius = 0.9.em();
1370 align = Align::CENTER;
1371 border_align = 100.pct();
1372
1373 #[easing(150.ms())]
1374 background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()));
1375 when *#{parent_hovered} {
1376 #[easing(0.ms())]
1377 background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
1378 }
1379
1380 when *#is_checked {
1381 border = {
1382 widths: 2,
1383 sides: colors::ACCENT_COLOR_VAR.shade_into(-2),
1384 };
1385 #[easing(0.ms())]
1386 background_color = zng_wgt_text::FONT_COLOR_VAR;
1387 }
1388 }
1389}