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