1#![doc(html_favicon_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://raw.githubusercontent.com/zng-ui/zng/main/examples/image/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, marker::PhantomData, 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, BoxedAnyVar, 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_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_style_fn, 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 widget_set! {
55 self;
56 style_base_fn = style_fn!(|_| DefaultStyle!());
57 }
58 }
59}
60impl_style_fn!(Toggle);
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 UiNode, checked: impl IntoVar<bool>) -> impl 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 WIDGET.sub_event(&CLICK_EVENT);
85 _toggle_handle = cmd::TOGGLE_CMD.scoped(WIDGET.id()).subscribe(true);
86 }
87 UiNodeOp::Deinit => {
88 _toggle_handle = CommandHandle::dummy();
89 access_handle = VarHandle::dummy();
90 }
91 UiNodeOp::Info { info } => {
92 if let Some(mut a) = info.access() {
93 if access_handle.is_dummy() {
94 access_handle = checked.subscribe(UpdateOp::Info, WIDGET.id());
95 }
96 a.set_checked(Some(checked.get()));
97 }
98 }
99 UiNodeOp::Event { update } => {
100 child.event(update);
101
102 if let Some(args) = CLICK_EVENT.on(update) {
103 if args.is_primary()
104 && checked.capabilities().contains(VarCapability::MODIFY)
105 && !args.propagation().is_stopped()
106 && args.is_enabled(WIDGET.id())
107 {
108 args.propagation().stop();
109
110 let _ = checked.set(!checked.get());
111 }
112 } else if let Some(args) = cmd::TOGGLE_CMD.scoped(WIDGET.id()).on_unhandled(update) {
113 if let Some(b) = args.param::<bool>() {
114 args.propagation().stop();
115 let _ = checked.set(*b);
116 } else if let Some(b) = args.param::<Option<bool>>() {
117 if let Some(b) = b {
118 args.propagation().stop();
119 let _ = checked.set(*b);
120 }
121 } else if args.param.is_none() {
122 args.propagation().stop();
123 let _ = checked.set(!checked.get());
124 }
125 }
126 }
127 _ => {}
128 }),
129 );
130 with_context_var(node, IS_CHECKED_VAR, checked.map_into())
131}
132
133#[property(CONTEXT + 1, default(None), widget_impl(Toggle))]
136pub fn checked_opt(child: impl UiNode, checked: impl IntoVar<Option<bool>>) -> impl UiNode {
137 let checked = checked.into_var();
138 let mut _toggle_handle = CommandHandle::dummy();
139 let mut access_handle = VarHandle::dummy();
140
141 let node = match_node(
142 child,
143 clmv!(checked, |child, op| match op {
144 UiNodeOp::Init => {
145 WIDGET.sub_event(&CLICK_EVENT);
146 _toggle_handle = cmd::TOGGLE_CMD.scoped(WIDGET.id()).subscribe(true);
147 }
148 UiNodeOp::Deinit => {
149 _toggle_handle = CommandHandle::dummy();
150 access_handle = VarHandle::dummy();
151 }
152 UiNodeOp::Info { info } => {
153 if let Some(mut a) = info.access() {
154 if access_handle.is_dummy() {
155 access_handle = checked.subscribe(UpdateOp::Info, WIDGET.id());
156 }
157 a.set_checked(checked.get());
158 }
159 }
160 UiNodeOp::Event { update } => {
161 child.event(update);
162
163 let mut cycle = false;
164
165 if let Some(args) = CLICK_EVENT.on(update) {
166 if args.is_primary()
167 && checked.capabilities().contains(VarCapability::MODIFY)
168 && !args.propagation().is_stopped()
169 && args.is_enabled(WIDGET.id())
170 {
171 args.propagation().stop();
172
173 cycle = true;
174 }
175 } else if let Some(args) = cmd::TOGGLE_CMD.scoped(WIDGET.id()).on_unhandled(update) {
176 if let Some(b) = args.param::<bool>() {
177 args.propagation().stop();
178 let _ = checked.set(Some(*b));
179 } else if let Some(b) = args.param::<Option<bool>>() {
180 if IS_TRISTATE_VAR.get() {
181 args.propagation().stop();
182 let _ = checked.set(*b);
183 } else if let Some(b) = b {
184 args.propagation().stop();
185 let _ = checked.set(Some(*b));
186 }
187 } else if args.param.is_none() {
188 args.propagation().stop();
189
190 cycle = true;
191 }
192 }
193
194 if cycle {
195 if IS_TRISTATE_VAR.get() {
196 let _ = checked.set(match checked.get() {
197 Some(true) => None,
198 Some(false) => Some(true),
199 None => Some(false),
200 });
201 } else {
202 let _ = checked.set(match checked.get() {
203 Some(true) | None => Some(false),
204 Some(false) => Some(true),
205 });
206 }
207 }
208 }
209 _ => {}
210 }),
211 );
212
213 with_context_var(node, IS_CHECKED_VAR, checked)
214}
215
216#[property(CONTEXT, default(IS_TRISTATE_VAR), widget_impl(Toggle))]
226pub fn tristate(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
227 with_context_var(child, IS_TRISTATE_VAR, enabled)
228}
229
230#[property(EVENT, widget_impl(Toggle))]
234pub fn is_checked(child: impl UiNode, state: impl IntoVar<bool>) -> impl UiNode {
235 bind_state(child, IS_CHECKED_VAR.map(|s| *s == Some(true)), state)
236}
237
238#[property(CONTEXT+2, widget_impl(Toggle))]
256pub fn value<T: VarValue>(child: impl UiNode, value: impl IntoVar<T>) -> impl UiNode {
257 value_impl(child, value.into_var().boxed_any())
258}
259fn value_impl(child: impl UiNode, value: BoxedAnyVar) -> impl UiNode {
260 fn select(value: &dyn AnyVarValue) -> bool {
262 let selector = SELECTOR.get();
263 match selector.select(value.clone_boxed()) {
264 Ok(()) => true,
265 Err(e) => {
266 let selected = selector.is_selected(value);
267 if selected {
268 tracing::error!("selected `{value:?}` with error, {e}");
269 } else if let SelectorError::ReadOnly | SelectorError::CannotClear = e {
270 } else {
272 tracing::error!("failed to select `{value:?}`, {e}");
273 }
274 selected
275 }
276 }
277 }
278 fn deselect(value: &dyn AnyVarValue) -> bool {
280 let selector = SELECTOR.get();
281 match selector.deselect(value) {
282 Ok(()) => true,
283 Err(e) => {
284 let deselected = !selector.is_selected(value);
285 if deselected {
286 tracing::error!("deselected `{value:?}` with error, {e}");
287 } else if let SelectorError::ReadOnly | SelectorError::CannotClear = e {
288 } else {
290 tracing::error!("failed to deselect `{value:?}`, {e}");
291 }
292 deselected
293 }
294 }
295 }
296 fn is_selected(value: &dyn AnyVarValue) -> bool {
297 SELECTOR.get().is_selected(value)
298 }
299
300 let checked = var(Some(false));
301 let child = with_context_var(child, IS_CHECKED_VAR, checked.clone());
302 let mut prev_value = None::<Box<dyn AnyVarValue>>;
303
304 let mut _click_handle = None;
305 let mut _toggle_handle = CommandHandle::dummy();
306 let mut _select_handle = CommandHandle::dummy();
307
308 match_node(child, move |child, op| match op {
309 UiNodeOp::Init => {
310 let id = WIDGET.id();
311 WIDGET.sub_var(&value).sub_var(&DESELECT_ON_NEW_VAR).sub_var(&checked);
312 SELECTOR.get().subscribe();
313
314 value.with_any(&mut |value| {
315 let selected = if SELECT_ON_INIT_VAR.get() {
316 select(value)
317 } else {
318 is_selected(value)
319 };
320 checked.set(Some(selected));
321
322 if DESELECT_ON_DEINIT_VAR.get() {
323 prev_value = Some(value.clone_boxed());
324 }
325 });
326
327 _click_handle = Some(CLICK_EVENT.subscribe(id));
328 _toggle_handle = cmd::TOGGLE_CMD.scoped(id).subscribe(true);
329 _select_handle = cmd::SELECT_CMD.scoped(id).subscribe(true);
330 }
331 UiNodeOp::Deinit => {
332 if checked.get() == Some(true) && DESELECT_ON_DEINIT_VAR.get() {
333 value.with_any(&mut |value| {
334 if deselect(value) {
335 checked.set(Some(false));
336 }
337 });
338 }
339
340 prev_value = None;
341 _click_handle = None;
342 _toggle_handle = CommandHandle::dummy();
343 _select_handle = CommandHandle::dummy();
344 }
345 UiNodeOp::Event { update } => {
346 child.event(update);
347
348 if let Some(args) = CLICK_EVENT.on(update) {
349 if args.is_primary() && !args.propagation().is_stopped() && args.is_enabled(WIDGET.id()) {
350 args.propagation().stop();
351
352 value.with_any(&mut |value| {
353 let selected = if checked.get() == Some(true) {
354 !deselect(value)
355 } else {
356 select(value)
357 };
358 checked.set(Some(selected))
359 });
360 }
361 } else if let Some(args) = cmd::TOGGLE_CMD.scoped(WIDGET.id()).on_unhandled(update) {
362 if args.param.is_none() {
363 args.propagation().stop();
364
365 value.with_any(&mut |value| {
366 let selected = if checked.get() == Some(true) {
367 !deselect(value)
368 } else {
369 select(value)
370 };
371 checked.set(Some(selected))
372 });
373 } else {
374 let s = if let Some(s) = args.param::<Option<bool>>() {
375 Some(s.unwrap_or(false))
376 } else {
377 args.param::<bool>().copied()
378 };
379 if let Some(s) = s {
380 args.propagation().stop();
381
382 value.with_any(&mut |value| {
383 let selected = if s { select(value) } else { !deselect(value) };
384 checked.set(Some(selected))
385 });
386 }
387 }
388 } else if let Some(args) = cmd::SELECT_CMD.scoped(WIDGET.id()).on_unhandled(update) {
389 if args.param.is_none() {
390 args.propagation().stop();
391 value.with_any(&mut |value| {
392 let selected = checked.get() == Some(true);
393 if !selected && select(value) {
394 checked.set(Some(true));
395 }
396 });
397 }
398 }
399 }
400 UiNodeOp::Update { .. } => {
401 let mut selected = None;
402 value.with_new_any(&mut |new| {
403 selected = Some(if checked.get() == Some(true) && SELECT_ON_NEW_VAR.get() {
405 select(new)
406 } else {
407 is_selected(new)
408 });
409
410 if let Some(prev) = prev_value.take() {
412 if DESELECT_ON_NEW_VAR.get() {
413 deselect(&*prev);
414 prev_value = Some(new.clone_boxed());
415 }
416 }
417 });
418 let selected = selected.unwrap_or_else(|| {
419 let mut s = false;
421 value.with_any(&mut |v| {
422 s = is_selected(v);
423 });
424 s
425 });
426 checked.set(selected);
427
428 if DESELECT_ON_NEW_VAR.get() && selected {
429 if prev_value.is_none() {
431 prev_value = Some(value.get_any());
432 }
433 } else {
434 prev_value = None;
435 }
436
437 if let Some(Some(true)) = checked.get_new() {
438 if SCROLL_ON_SELECT_VAR.get() {
439 use zng_wgt_scroll::cmd::*;
440 scroll_to(WIDGET.id(), ScrollToMode::minimal(10));
441 }
442 }
443 }
444 _ => {}
445 })
446}
447
448#[property(CONTEXT, default(SCROLL_ON_SELECT_VAR), widget_impl(Toggle))]
454pub fn scroll_on_select(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
455 with_context_var(child, SCROLL_ON_SELECT_VAR, enabled)
456}
457
458#[property(CONTEXT, default(Selector::nil()), widget_impl(Toggle))]
472pub fn selector(child: impl UiNode, selector: impl IntoValue<Selector>) -> impl UiNode {
473 let mut _select_handle = CommandHandle::dummy();
474 let child = match_node(child, move |c, op| match op {
475 UiNodeOp::Init => {
476 _select_handle = cmd::SELECT_CMD.scoped(WIDGET.id()).subscribe(true);
477 }
478 UiNodeOp::Info { info } => {
479 if let Some(mut info) = info.access() {
480 info.set_role(AccessRole::RadioGroup);
481 }
482 }
483 UiNodeOp::Deinit => {
484 _select_handle = CommandHandle::dummy();
485 }
486 UiNodeOp::Event { update } => {
487 c.event(update);
488
489 if let Some(args) = cmd::SELECT_CMD.scoped(WIDGET.id()).on_unhandled(update) {
490 if let Some(p) = args.param::<cmd::SelectOp>() {
491 args.propagation().stop();
492
493 p.call();
494 }
495 }
496 }
497 _ => {}
498 });
499 with_context_local(child, &SELECTOR, selector)
500}
501
502#[property(CONTEXT, default(SELECT_ON_INIT_VAR), widget_impl(Toggle))]
506pub fn select_on_init(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
507 with_context_var(child, SELECT_ON_INIT_VAR, enabled)
508}
509
510#[property(CONTEXT, default(DESELECT_ON_DEINIT_VAR), widget_impl(Toggle))]
514pub fn deselect_on_deinit(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
515 with_context_var(child, DESELECT_ON_DEINIT_VAR, enabled)
516}
517
518#[property(CONTEXT, default(SELECT_ON_NEW_VAR), widget_impl(Toggle))]
522pub fn select_on_new(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
523 with_context_var(child, SELECT_ON_NEW_VAR, enabled)
524}
525
526#[property(CONTEXT, default(DESELECT_ON_NEW_VAR), widget_impl(Toggle))]
530pub fn deselect_on_new(child: impl UiNode, enabled: impl IntoVar<bool>) -> impl UiNode {
531 with_context_var(child, DESELECT_ON_NEW_VAR, enabled)
532}
533
534context_local! {
535 pub static SELECTOR: Selector = Selector::nil();
537}
538
539context_var! {
540 pub static SELECT_ON_INIT_VAR: bool = false;
547
548 pub static DESELECT_ON_DEINIT_VAR: bool = false;
555
556 pub static SELECT_ON_NEW_VAR: bool = true;
563
564 pub static DESELECT_ON_NEW_VAR: bool = false;
571
572 pub static SCROLL_ON_SELECT_VAR: bool = true;
578}
579
580pub trait SelectorImpl: Send + 'static {
582 fn subscribe(&self);
586
587 fn select(&mut self, value: Box<dyn AnyVarValue>) -> Result<(), SelectorError>;
589
590 fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError>;
592
593 fn is_selected(&self, value: &dyn AnyVarValue) -> bool;
595}
596
597#[derive(Clone)]
604pub struct Selector(Arc<Mutex<dyn SelectorImpl>>);
605impl Selector {
606 pub fn new(selector: impl SelectorImpl) -> Self {
608 Self(Arc::new(Mutex::new(selector)))
609 }
610
611 pub fn nil() -> Self {
613 struct NilSel;
614 impl SelectorImpl for NilSel {
615 fn subscribe(&self) {}
616
617 fn select(&mut self, _: Box<dyn AnyVarValue>) -> Result<(), SelectorError> {
618 Err(SelectorError::custom_str("no contextual `selector`"))
619 }
620
621 fn deselect(&mut self, _: &dyn AnyVarValue) -> Result<(), SelectorError> {
622 Ok(())
623 }
624
625 fn is_selected(&self, __r: &dyn AnyVarValue) -> bool {
626 false
627 }
628 }
629 Self::new(NilSel)
630 }
631
632 pub fn single<T>(selection: impl IntoVar<T>) -> Self
634 where
635 T: VarValue,
636 {
637 struct SingleSel<T, S> {
638 selection: S,
639 _type: PhantomData<T>,
640 }
641 impl<T, S> SelectorImpl for SingleSel<T, S>
642 where
643 T: VarValue,
644 S: Var<T>,
645 {
646 fn subscribe(&self) {
647 WIDGET.sub_var(&self.selection);
648 }
649
650 fn select(&mut self, value: Box<dyn AnyVarValue>) -> Result<(), SelectorError> {
651 match value.into_any().downcast::<T>() {
652 Ok(value) => match self.selection.set(*value) {
653 Ok(_) => Ok(()),
654 Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
655 },
656 Err(_) => Err(SelectorError::WrongType),
657 }
658 }
659
660 fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
661 if self.is_selected(value) {
662 Err(SelectorError::CannotClear)
663 } else {
664 Ok(())
665 }
666 }
667
668 fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
669 match value.as_any().downcast_ref::<T>() {
670 Some(value) => self.selection.with(|t| t == value),
671 None => false,
672 }
673 }
674 }
675 Self::new(SingleSel {
676 selection: selection.into_var(),
677 _type: PhantomData,
678 })
679 }
680
681 pub fn single_opt<T>(selection: impl IntoVar<Option<T>>) -> Self
683 where
684 T: VarValue,
685 {
686 struct SingleOptSel<T, S> {
687 selection: S,
688 _type: PhantomData<T>,
689 }
690 impl<T, S> SelectorImpl for SingleOptSel<T, S>
691 where
692 T: VarValue,
693 S: Var<Option<T>>,
694 {
695 fn subscribe(&self) {
696 WIDGET.sub_var(&self.selection);
697 }
698
699 fn select(&mut self, value: Box<dyn AnyVarValue>) -> Result<(), SelectorError> {
700 match value.into_any().downcast::<T>() {
701 Ok(value) => match self.selection.set(Some(*value)) {
702 Ok(_) => Ok(()),
703 Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
704 },
705 Err(value) => match value.downcast::<Option<T>>() {
706 Ok(value) => match self.selection.set(*value) {
707 Ok(_) => Ok(()),
708 Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
709 },
710 Err(_) => Err(SelectorError::WrongType),
711 },
712 }
713 }
714
715 fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
716 match value.as_any().downcast_ref::<T>() {
717 Some(value) => {
718 if self.selection.with(|t| t.as_ref() == Some(value)) {
719 match self.selection.set(None) {
720 Ok(_) => Ok(()),
721 Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
722 }
723 } else {
724 Ok(())
725 }
726 }
727 None => match value.as_any().downcast_ref::<Option<T>>() {
728 Some(value) => {
729 if self.selection.with(|t| t == value) {
730 if value.is_none() {
731 Ok(())
732 } else {
733 match self.selection.set(None) {
734 Ok(_) => Ok(()),
735 Err(VarIsReadOnlyError { .. }) => Err(SelectorError::ReadOnly),
736 }
737 }
738 } else {
739 Ok(())
740 }
741 }
742 None => Ok(()),
743 },
744 }
745 }
746
747 fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
748 match value.as_any().downcast_ref::<T>() {
749 Some(value) => self.selection.with(|t| t.as_ref() == Some(value)),
750 None => match value.as_any().downcast_ref::<Option<T>>() {
751 Some(value) => self.selection.with(|t| t == value),
752 None => false,
753 },
754 }
755 }
756 }
757 Self::new(SingleOptSel {
758 selection: selection.into_var(),
759 _type: PhantomData,
760 })
761 }
762
763 pub fn bitflags<T>(selection: impl IntoVar<T>) -> Self
765 where
766 T: VarValue + ops::BitOr<Output = T> + ops::BitAnd<Output = T> + ops::Not<Output = T>,
767 {
768 struct BitflagsSel<T, S> {
769 selection: S,
770 _type: PhantomData<T>,
771 }
772 impl<T, S> SelectorImpl for BitflagsSel<T, S>
773 where
774 T: VarValue + ops::BitOr<Output = T> + ops::BitAnd<Output = T> + ops::Not<Output = T>,
775 S: Var<T>,
776 {
777 fn subscribe(&self) {
778 WIDGET.sub_var(&self.selection);
779 }
780
781 fn select(&mut self, value: Box<dyn AnyVarValue>) -> Result<(), SelectorError> {
782 match value.into_any().downcast::<T>() {
783 Ok(value) => self
784 .selection
785 .modify(move |m| {
786 let value = *value;
787 let new = m.as_ref().clone() | value;
788 if m.as_ref() != &new {
789 m.set(new);
790 }
791 })
792 .map_err(|_| SelectorError::ReadOnly),
793 Err(_) => Err(SelectorError::WrongType),
794 }
795 }
796
797 fn deselect(&mut self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
798 match value.as_any().downcast_ref::<T>() {
799 Some(value) => self
800 .selection
801 .modify(clmv!(value, |m| {
802 let new = m.as_ref().clone() & !value;
803 if m.as_ref() != &new {
804 m.set(new);
805 }
806 }))
807 .map_err(|_| SelectorError::ReadOnly),
808 None => Err(SelectorError::WrongType),
809 }
810 }
811
812 fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
813 match value.as_any().downcast_ref::<T>() {
814 Some(value) => &(self.selection.get() & value.clone()) == value,
815 None => false,
816 }
817 }
818 }
819
820 Self::new(BitflagsSel {
821 selection: selection.into_var(),
822 _type: PhantomData,
823 })
824 }
825
826 pub fn subscribe(&self) {
830 self.0.lock().subscribe();
831 }
832
833 pub fn select(&self, value: Box<dyn AnyVarValue>) -> Result<(), SelectorError> {
835 self.0.lock().select(value)
836 }
837
838 pub fn deselect(&self, value: &dyn AnyVarValue) -> Result<(), SelectorError> {
840 self.0.lock().deselect(value)
841 }
842
843 pub fn is_selected(&self, value: &dyn AnyVarValue) -> bool {
845 self.0.lock().is_selected(value)
846 }
847}
848impl<S: SelectorImpl> From<S> for Selector {
849 fn from(sel: S) -> Self {
850 Selector::new(sel)
851 }
852}
853impl fmt::Debug for Selector {
854 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
855 write!(f, "Selector(_)")
856 }
857}
858impl PartialEq for Selector {
859 fn eq(&self, other: &Self) -> bool {
860 Arc::ptr_eq(&self.0, &other.0)
861 }
862}
863
864#[derive(Debug, Clone)]
866pub enum SelectorError {
867 WrongType,
869 ReadOnly,
871 CannotClear,
873 Custom(Arc<dyn Error + Send + Sync>),
875}
876impl SelectorError {
877 pub fn custom_str(str: impl Into<String>) -> SelectorError {
879 let str = str.into();
880 let e: Box<dyn Error + Send + Sync> = str.into();
881 let e: Arc<dyn Error + Send + Sync> = e.into();
882 SelectorError::Custom(e)
883 }
884}
885impl fmt::Display for SelectorError {
886 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
887 match self {
888 SelectorError::WrongType => write!(f, "wrong value type for selector"),
889 SelectorError::ReadOnly => write!(f, "selection is read-only"),
890 SelectorError::CannotClear => write!(f, "selection cannot be empty"),
891 SelectorError::Custom(e) => fmt::Display::fmt(e, f),
892 }
893 }
894}
895impl Error for SelectorError {
896 fn source(&self) -> Option<&(dyn Error + 'static)> {
897 match self {
898 SelectorError::WrongType => None,
899 SelectorError::ReadOnly => None,
900 SelectorError::CannotClear => None,
901 SelectorError::Custom(e) => Some(&**e),
902 }
903 }
904}
905impl From<VarIsReadOnlyError> for SelectorError {
906 fn from(_: VarIsReadOnlyError) -> Self {
907 SelectorError::ReadOnly
908 }
909}
910
911#[widget($crate::DefaultStyle)]
918pub struct DefaultStyle(zng_wgt_button::DefaultStyle);
919impl DefaultStyle {
920 fn widget_intrinsic(&mut self) {
921 widget_set! {
922 self;
923 replace = true;
924 when *#is_checked {
925 background_color = BASE_COLOR_VAR.shade(2);
926 border = {
927 widths: 1,
928 sides: BASE_COLOR_VAR.shade_into(2),
929 };
930 }
931 }
932 }
933}
934
935#[widget($crate::LightStyle)]
937pub struct LightStyle(zng_wgt_button::LightStyle);
938impl LightStyle {
939 fn widget_intrinsic(&mut self) {
940 widget_set! {
941 self;
942 when *#is_checked {
943 #[easing(0.ms())]
944 background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
945 }
946 }
947 }
948}
949
950#[widget($crate::CheckStyle)]
956pub struct CheckStyle(Style);
957impl CheckStyle {
958 fn widget_intrinsic(&mut self) {
959 widget_set! {
960 self;
961 replace = true;
962 child_start = {
963 node: {
964 let parent_hovered = var(false);
965 is_hovered(checkmark_visual(parent_hovered.clone()), parent_hovered)
966 },
967 spacing: CHECK_SPACING_VAR,
968 };
969 access_role = AccessRole::CheckBox;
970 }
971 }
972}
973context_var! {
974 pub static CHECK_SPACING_VAR: Length = 4;
976}
977
978#[property(CONTEXT, default(CHECK_SPACING_VAR), widget_impl(CheckStyle))]
980pub fn check_spacing(child: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
981 with_context_var(child, CHECK_SPACING_VAR, spacing)
982}
983
984fn checkmark_visual(parent_hovered: impl Var<bool>) -> impl UiNode {
985 let checked = ICONS.get_or(["toggle.checked", "check"], || {
986 zng_wgt_text::Text! {
987 txt = "✓";
988 font_family = FontNames::system_ui(&lang!(und));
989 txt_align = Align::CENTER;
990 }
991 });
992 let indeterminate = ICONS.get_or(["toggle.indeterminate"], || {
993 zng_wgt::Wgt! {
994 align = Align::CENTER;
995 background_color = zng_wgt_text::FONT_COLOR_VAR;
996 size = (6, 2);
997 corner_radius = 0;
998 }
999 });
1000 zng_wgt_container::Container! {
1001 hit_test_mode = false;
1002 accessible = false;
1003 size = 1.2.em();
1004 corner_radius = 0.1.em();
1005 align = Align::CENTER;
1006
1007 #[easing(150.ms())]
1008 background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()));
1009 when *#{parent_hovered} {
1010 #[easing(0.ms())]
1011 background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
1012 }
1013
1014 when #{IS_CHECKED_VAR}.is_none() {
1015 child = indeterminate;
1016 }
1017 when *#{IS_CHECKED_VAR} == Some(true) {
1018 child = checked;
1019 #[easing(0.ms())]
1020 background_color = colors::ACCENT_COLOR_VAR.shade(-1);
1021 }
1022 }
1023}
1024
1025#[widget($crate::ComboStyle)]
1031pub struct ComboStyle(DefaultStyle);
1032impl ComboStyle {
1033 fn widget_intrinsic(&mut self) {
1034 widget_set! {
1035 self;
1036 replace = true;
1037 access_role = AccessRole::ComboBox;
1038 child_align = Align::FILL;
1039 border_over = false;
1040 border_align = 1.fct();
1041 padding = -1;
1042 checked = var(false);
1043 child_end = {
1044 node: combomark_visual(),
1045 spacing: COMBO_SPACING_VAR,
1046 };
1047
1048 click_mode = ClickMode::press();
1049
1050 zng_wgt_button::style_fn = Style! {
1051 click_mode = ClickMode::default();
1053 corner_radius = (4, 0, 0, 4);
1054 };
1055
1056 zng_wgt_layer::popup::style_fn = Style! {
1057 zng_wgt_button::style_fn = Style! {
1058 click_mode = ClickMode::release();
1059
1060 corner_radius = 0;
1061 padding = 2;
1062 border = unset!;
1063 };
1064 crate::style_fn = Style! {
1065 click_mode = ClickMode::release();
1066
1067 corner_radius = 0;
1068 padding = 2;
1069 border = unset!;
1070 };
1071
1072 capture_pointer_on_init = CaptureMode::Subtree;
1080
1081 #[easing(100.ms())]
1082 opacity = 0.pct();
1083 #[easing(100.ms())]
1084 y = -10;
1085
1086 when *#is_inited {
1087 opacity = 100.pct();
1088 y = 0;
1089 }
1090
1091 zng_wgt_layer::popup::close_delay = 100.ms();
1092 when *#zng_wgt_layer::popup::is_close_delaying {
1093 opacity = 0.pct();
1094 y = -10;
1095 }
1096 };
1097 }
1098 }
1099}
1100context_var! {
1101 pub static COMBO_SPACING_VAR: Length = 0;
1103}
1104
1105#[property(CONTEXT, default(COMBO_SPACING_VAR), widget_impl(ComboStyle))]
1107pub fn combo_spacing(child: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
1108 with_context_var(child, COMBO_SPACING_VAR, spacing)
1109}
1110
1111#[property(CHILD, widget_impl(Toggle))]
1123pub fn checked_popup(child: impl UiNode, popup: impl IntoVar<WidgetFn<()>>) -> impl UiNode {
1124 let popup = popup.into_var();
1125 let mut state = var(PopupState::Closed).read_only();
1126 let mut _state_handle = VarHandle::dummy();
1127 match_node(child, move |_, op| {
1128 let new = match op {
1129 UiNodeOp::Init => {
1130 WIDGET.sub_var(&IS_CHECKED_VAR).sub_event(&MOUSE_INPUT_EVENT);
1131 IS_CHECKED_VAR.get()
1132 }
1133 UiNodeOp::Deinit => {
1134 _state_handle = VarHandle::dummy();
1135 Some(false)
1136 }
1137 UiNodeOp::Event { update } => {
1138 if let Some(args) = MOUSE_INPUT_EVENT.on(update) {
1139 if args.is_mouse_down() && args.is_primary() && IS_CHECKED_VAR.get() == Some(true) {
1142 args.propagation().stop();
1143 cmd::TOGGLE_CMD.scoped(WIDGET.id()).notify_param(Some(false));
1144 }
1145 }
1146 None
1147 }
1148 UiNodeOp::Update { .. } => {
1149 if let Some(s) = state.get_new() {
1150 if matches!(s, PopupState::Closed) {
1151 if IS_CHECKED_VAR.get() != Some(false) {
1152 cmd::TOGGLE_CMD.scoped(WIDGET.id()).notify_param(Some(false));
1153 }
1154 _state_handle = VarHandle::dummy();
1155 }
1156 None
1157 } else {
1158 IS_CHECKED_VAR.get_new().map(|o| o.unwrap_or(false))
1159 }
1160 }
1161 _ => None,
1162 };
1163 if let Some(open) = new {
1164 if open {
1165 if matches!(state.get(), PopupState::Closed) {
1166 state = POPUP.open(popup.get()(()));
1167 _state_handle = state.subscribe(UpdateOp::Update, WIDGET.id());
1168 }
1169 } else if let PopupState::Open(id) = state.get() {
1170 POPUP.close_id(id);
1171 }
1172 }
1173 })
1174}
1175
1176fn combomark_visual() -> impl UiNode {
1177 let dropdown = ICONS.get_or(
1178 ["toggle.dropdown", "material/rounded/keyboard-arrow-down", "keyboard-arrow-down"],
1179 combomark_visual_fallback,
1180 );
1181 Wgt! {
1182 size = 12;
1183 zng_wgt_fill::background = dropdown;
1184 align = Align::CENTER;
1185
1186 zng_wgt_transform::rotate_x = 0.deg();
1187 when #is_checked {
1188 zng_wgt_transform::rotate_x = 180.deg();
1189 }
1190 }
1191}
1192fn combomark_visual_fallback() -> impl UiNode {
1193 let color_key = FrameValueKey::new_unique();
1194 let mut size = PxSize::zero();
1195 let mut bounds = PxBox::zero();
1196 let mut transform = PxTransform::identity();
1197
1198 fn layout() -> (PxSize, PxTransform, PxBox) {
1200 let size = Size::from(8).layout();
1201 let center = size.to_vector() * 0.5.fct();
1202 let transform = Transform::new_translate(-center.x, -center.y)
1203 .rotate(45.deg())
1204 .scale_x(0.7)
1205 .translate(center.x, center.y)
1206 .translate_x(Length::from(2).layout_x())
1207 .layout();
1208 let bounds = transform.outer_transformed(PxBox::from_size(size)).unwrap_or_default();
1209 (size, transform, bounds)
1210 }
1211
1212 match_node_leaf(move |op| match op {
1213 UiNodeOp::Init => {
1214 WIDGET.sub_var_render_update(&zng_wgt_text::FONT_COLOR_VAR);
1215 }
1216 UiNodeOp::Measure { desired_size, .. } => {
1217 let (s, _, _) = layout();
1218 *desired_size = s;
1219 }
1220 UiNodeOp::Layout { final_size, .. } => {
1221 (size, transform, bounds) = layout();
1222 *final_size = size;
1223 }
1224 UiNodeOp::Render { frame } => {
1225 let mut clip = bounds.to_rect();
1226 clip.size.height *= 0.5.fct();
1227 clip.origin.y += clip.size.height;
1228
1229 frame.push_clip_rect(clip, false, false, |frame| {
1230 frame.push_reference_frame((WIDGET.id(), 0).into(), transform.into(), false, false, |frame| {
1231 frame.push_color(PxRect::from_size(size), color_key.bind_var(&zng_wgt_text::FONT_COLOR_VAR, |&c| c));
1232 })
1233 });
1234 }
1235 UiNodeOp::RenderUpdate { update } => {
1236 update.update_color_opt(color_key.update_var(&zng_wgt_text::FONT_COLOR_VAR, |&c| c));
1237 }
1238 _ => {}
1239 })
1240}
1241
1242#[widget($crate::SwitchStyle)]
1248pub struct SwitchStyle(Style);
1249impl SwitchStyle {
1250 fn widget_intrinsic(&mut self) {
1251 widget_set! {
1252 self;
1253 replace = true;
1254
1255 child_start = {
1256 node: {
1257 let parent_hovered = var(false);
1258 is_hovered(switch_visual(parent_hovered.clone()), parent_hovered)
1259 },
1260 spacing: SWITCH_SPACING_VAR,
1261 };
1262 }
1263 }
1264}
1265context_var! {
1266 pub static SWITCH_SPACING_VAR: Length = 2;
1268}
1269
1270#[property(CONTEXT, default(SWITCH_SPACING_VAR), widget_impl(SwitchStyle))]
1272pub fn switch_spacing(child: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
1273 with_context_var(child, SWITCH_SPACING_VAR, spacing)
1274}
1275
1276fn switch_visual(parent_hovered: impl Var<bool>) -> impl UiNode {
1277 zng_wgt_container::Container! {
1278 hit_test_mode = false;
1279 size = (2.em(), 1.em());
1280 align = Align::CENTER;
1281 corner_radius = 1.em();
1282 padding = 2;
1283 child = Wgt! {
1284 size = 1.em() - Length::from(4);
1285 align = Align::LEFT;
1286 background_color = zng_wgt_text::FONT_COLOR_VAR;
1287
1288 #[easing(150.ms())]
1289 x = 0.em();
1290 when *#is_checked {
1291 x = 1.em();
1292 }
1293 };
1294
1295 #[easing(150.ms())]
1296 background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()));
1297 when *#{parent_hovered} {
1298 #[easing(0.ms())]
1299 background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
1300 }
1301 when #is_checked {
1302 background_color = colors::ACCENT_COLOR_VAR.shade(-1);
1303 }
1304 }
1305}
1306
1307#[widget($crate::RadioStyle)]
1313pub struct RadioStyle(Style);
1314impl RadioStyle {
1315 fn widget_intrinsic(&mut self) {
1316 widget_set! {
1317 self;
1318 replace = true;
1319
1320 access_role = AccessRole::Radio;
1321 child_start = {
1322 node: {
1323 let parent_hovered = var(false);
1324 is_hovered(radio_visual(parent_hovered.clone()), parent_hovered)
1325 },
1326 spacing: RADIO_SPACING_VAR,
1327 };
1328 }
1329 }
1330}
1331
1332context_var! {
1333 pub static RADIO_SPACING_VAR: Length = 2;
1335}
1336
1337#[property(CONTEXT, default(RADIO_SPACING_VAR), widget_impl(RadioStyle))]
1339pub fn radio_spacing(child: impl UiNode, spacing: impl IntoVar<Length>) -> impl UiNode {
1340 with_context_var(child, RADIO_SPACING_VAR, spacing)
1341}
1342
1343fn radio_visual(parent_hovered: impl Var<bool>) -> impl UiNode {
1344 Wgt! {
1345 hit_test_mode = false;
1346 size = 0.9.em();
1347 corner_radius = 0.9.em();
1348 align = Align::CENTER;
1349 border_align = 100.pct();
1350
1351 #[easing(150.ms())]
1352 background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(10.pct()));
1353 when *#{parent_hovered} {
1354 #[easing(0.ms())]
1355 background_color = zng_wgt_text::FONT_COLOR_VAR.map(|c| c.with_alpha(20.pct()));
1356 }
1357
1358 when *#is_checked {
1359 border = {
1360 widths: 2,
1361 sides: colors::ACCENT_COLOR_VAR.shade_into(-2),
1362 };
1363 #[easing(0.ms())]
1364 background_color = zng_wgt_text::FONT_COLOR_VAR;
1365 }
1366 }
1367}