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
12use std::{any::Any, collections::HashMap, fmt, mem, num::NonZeroU8, ops, sync::Arc};
13
14use zng_color::COLOR_SCHEME_VAR;
15use zng_var::{BoxedAnyVar, types::ContextualizedVar};
16use zng_wgt::prelude::*;
17
18use task::parking_lot::RwLock;
19
20#[property(CONTEXT - 1)]
27pub fn data<T: VarValue>(child: impl UiNode, data: impl IntoVar<T>) -> impl UiNode {
28 with_context_local(child, &DATA_CTX, data.into_var().boxed_any())
29}
30
31#[property(CONTEXT, default(DataNoteLevel::INFO, ""))]
38pub fn data_note(child: impl UiNode, level: impl IntoVar<DataNoteLevel>, note: impl IntoVar<Txt>) -> impl UiNode {
39 let level = level.into_var();
40 let note = note.into_var();
41 let mut _handle = DataNoteHandle::dummy();
42 match_node(child, move |_, op| match op {
43 UiNodeOp::Init => {
44 WIDGET.sub_var(&level).sub_var(¬e);
45
46 let note = note.get();
47 if !note.is_empty() {
48 _handle = DATA.annotate(level.get(), note);
49 }
50 }
51 UiNodeOp::Deinit => {
52 _handle = DataNoteHandle::dummy();
53 }
54 UiNodeOp::Update { .. } => {
55 if level.is_new() || note.is_new() {
56 let note = note.get();
57 _handle = if note.is_empty() {
58 DataNoteHandle::dummy()
59 } else {
60 DATA.annotate(level.get(), note)
61 };
62 }
63 }
64 _ => {}
65 })
66}
67
68#[property(CONTEXT, default(""))]
76pub fn data_info(child: impl UiNode, note: impl IntoVar<Txt>) -> impl UiNode {
77 data_note(child, DataNoteLevel::INFO, note)
78}
79
80#[property(CONTEXT, default(""))]
88pub fn data_warn(child: impl UiNode, note: impl IntoVar<Txt>) -> impl UiNode {
89 data_note(child, DataNoteLevel::WARN, note)
90}
91
92#[property(CONTEXT, default(""))]
100pub fn data_error(child: impl UiNode, note: impl IntoVar<Txt>) -> impl UiNode {
101 data_note(child, DataNoteLevel::ERROR, note)
102}
103
104#[property(CONTEXT - 1)]
106pub fn get_data_notes(child: impl UiNode, notes: impl IntoVar<DataNotes>) -> impl UiNode {
107 let notes = notes.into_var();
108 with_data_notes(child, move |n| {
109 let _ = notes.set(n.clone());
110 })
111}
112
113#[property(CONTEXT - 1)]
115pub fn has_data_notes(child: impl UiNode, any: impl IntoVar<bool>) -> impl UiNode {
116 let any = any.into_var();
117 with_data_notes(child, move |n| {
118 let _ = any.set(!n.is_empty());
119 })
120}
121
122#[property(CONTEXT - 1)]
126pub fn get_data_info(child: impl UiNode, notes: impl IntoVar<DataNotes>) -> impl UiNode {
127 let notes = notes.into_var();
128 with_data_notes(child, move |n| {
129 let _ = notes.set(n.clone_level(DataNoteLevel::INFO));
130 })
131}
132
133#[property(CONTEXT - 1)]
137pub fn get_data_info_txt(child: impl UiNode, notes: impl IntoVar<Txt>) -> impl UiNode {
138 let notes = notes.into_var();
139 with_data_notes(child, move |n| {
140 let _ = notes.set(n.level_txt(DataNoteLevel::INFO));
141 })
142}
143
144#[property(CONTEXT - 1)]
148pub fn has_data_info(child: impl UiNode, any: impl IntoVar<bool>) -> impl UiNode {
149 let any = any.into_var();
150 with_data_notes(child, move |n| {
151 let _ = any.set(n.iter().any(|n| n.level() == DataNoteLevel::INFO));
152 })
153}
154
155#[property(CONTEXT - 1)]
159pub fn get_data_warn(child: impl UiNode, notes: impl IntoVar<DataNotes>) -> impl UiNode {
160 let notes = notes.into_var();
161 with_data_notes(child, move |n| {
162 let _ = notes.set(n.clone_level(DataNoteLevel::WARN));
163 })
164}
165
166#[property(CONTEXT - 1)]
170pub fn get_data_warn_txt(child: impl UiNode, notes: impl IntoVar<Txt>) -> impl UiNode {
171 let notes = notes.into_var();
172 with_data_notes(child, move |n| {
173 let _ = notes.set(n.level_txt(DataNoteLevel::WARN));
174 })
175}
176
177#[property(CONTEXT - 1)]
181pub fn has_data_warn(child: impl UiNode, any: impl IntoVar<bool>) -> impl UiNode {
182 let any = any.into_var();
183 with_data_notes(child, move |n| {
184 let _ = any.set(n.iter().any(|n| n.level() == DataNoteLevel::WARN));
185 })
186}
187
188#[property(CONTEXT - 1)]
192pub fn get_data_error(child: impl UiNode, notes: impl IntoVar<DataNotes>) -> impl UiNode {
193 let notes = notes.into_var();
194 with_data_notes(child, move |n| {
195 let _ = notes.set(n.clone_level(DataNoteLevel::ERROR));
196 })
197}
198
199#[property(CONTEXT - 1)]
203pub fn get_data_error_txt(child: impl UiNode, notes: impl IntoVar<Txt>) -> impl UiNode {
204 let notes = notes.into_var();
205 with_data_notes(child, move |n| {
206 let _ = notes.set(n.level_txt(DataNoteLevel::ERROR));
207 })
208}
209
210#[property(CONTEXT - 1)]
214pub fn has_data_error(child: impl UiNode, any: impl IntoVar<bool>) -> impl UiNode {
215 let any = any.into_var();
216 with_data_notes(child, move |n| {
217 let _ = any.set(n.iter().any(|n| n.level() == DataNoteLevel::ERROR));
218 })
219}
220
221#[property(CONTEXT - 1)]
223pub fn get_data_notes_top(child: impl UiNode, notes: impl IntoVar<DataNotes>) -> impl UiNode {
224 let notes = notes.into_var();
225 with_data_notes(child, move |n| {
226 let _ = notes.set(if let Some(top) = n.iter().map(|n| n.level()).max() {
227 n.clone_level(top)
228 } else {
229 DataNotes::default()
230 });
231 })
232}
233
234context_var! {
235 pub static DATA_NOTE_COLORS_VAR: HashMap<DataNoteLevel, LightDark> = {
241 let mut map = HashMap::new();
242 map.insert(DataNoteLevel::INFO, LightDark::new(colors::AZURE, colors::AZURE));
244 map.insert(DataNoteLevel::WARN, LightDark::new(colors::ORANGE, colors::YELLOW));
245 map.insert(
246 DataNoteLevel::ERROR,
247 LightDark::new(colors::RED, colors::WHITE.with_alpha(20.pct()).mix_normal(colors::RED)),
248 );
249 map
250 };
251}
252
253#[property(CONTEXT, default(DATA_NOTE_COLORS_VAR))]
259pub fn replace_data_note_colors(child: impl UiNode, colors: impl IntoVar<HashMap<DataNoteLevel, LightDark>>) -> impl UiNode {
260 with_context_var(child, DATA_NOTE_COLORS_VAR, colors)
261}
262
263#[property(CONTEXT, default(HashMap::new()))]
269pub fn extend_data_note_colors(child: impl UiNode, colors: impl IntoVar<HashMap<DataNoteLevel, LightDark>>) -> impl UiNode {
270 with_context_var(
271 child,
272 DATA_NOTE_COLORS_VAR,
273 merge_var!(DATA_NOTE_COLORS_VAR, colors.into_var(), |base, over| {
274 let mut base = base.clone();
275 base.extend(over);
276 base
277 }),
278 )
279}
280
281pub fn with_data_note_color(child: impl UiNode, level: DataNoteLevel, color: impl IntoVar<LightDark>) -> impl UiNode {
283 with_context_var(
284 child,
285 DATA_NOTE_COLORS_VAR,
286 merge_var!(DATA_NOTE_COLORS_VAR, color.into_var(), move |base, over| {
287 let mut base = base.clone();
288 base.insert(level, *over);
289 base
290 }),
291 )
292}
293
294#[property(CONTEXT)]
300pub fn data_info_color(child: impl UiNode, color: impl IntoVar<LightDark>) -> impl UiNode {
301 with_data_note_color(child, DataNoteLevel::INFO, color)
302}
303
304#[property(CONTEXT)]
310pub fn data_warn_color(child: impl UiNode, color: impl IntoVar<LightDark>) -> impl UiNode {
311 with_data_note_color(child, DataNoteLevel::WARN, color)
312}
313
314#[property(CONTEXT)]
320pub fn data_error_color(child: impl UiNode, color: impl IntoVar<LightDark>) -> impl UiNode {
321 with_data_note_color(child, DataNoteLevel::ERROR, color)
322}
323
324pub struct DATA;
346impl DATA {
347 pub fn req<T: VarValue>(&self) -> ContextualizedVar<T> {
353 self.get(|| panic!("expected DATA of type `{}`", std::any::type_name::<T>()))
354 }
355
356 pub fn get<T: VarValue>(&self, fallback: impl Fn() -> T + Send + Sync + 'static) -> ContextualizedVar<T> {
358 ContextualizedVar::new(move || {
359 DATA_CTX
360 .get()
361 .clone_any()
362 .double_boxed_any()
363 .downcast::<BoxedVar<T>>()
364 .map(|b| *b)
365 .unwrap_or_else(|_| LocalVar(fallback()).boxed())
366 })
367 }
368
369 pub fn get_any(&self) -> BoxedAnyVar {
375 DATA_CTX.get().clone_any()
376 }
377
378 pub fn annotate(&self, level: DataNoteLevel, note: impl DataNoteValue) -> DataNoteHandle {
382 if !DATA_NOTES_CTX.is_default() {
383 let (note, handle) = DataNote::new(WIDGET.id(), level, note);
384 let notes = DATA_NOTES_CTX.get();
385 let mut notes = notes.write();
386 notes.notes.notes.push(note);
387 notes.changed = true;
388 handle
389 } else {
390 DataNoteHandle::dummy()
391 }
392 }
393
394 pub fn inform(&self, note: impl DataNoteValue) -> DataNoteHandle {
398 self.annotate(DataNoteLevel::INFO, note)
399 }
400
401 pub fn warn(&self, note: impl DataNoteValue) -> DataNoteHandle {
405 self.annotate(DataNoteLevel::WARN, note)
406 }
407
408 pub fn invalidate(&self, note: impl DataNoteValue) -> DataNoteHandle {
412 self.annotate(DataNoteLevel::ERROR, note)
413 }
414
415 pub fn note_color(&self, level: impl IntoVar<DataNoteLevel>) -> impl Var<Rgba> {
422 merge_var!(DATA_NOTE_COLORS_VAR, level.into_var(), COLOR_SCHEME_VAR, |map, level, scheme| {
423 let c = if let Some(c) = map.get(level) {
424 *c
425 } else {
426 let mut nearest = 0u8;
427 let mut color = None;
428
429 for (l, c) in map {
430 if l.0.get() < level.0.get() && l.0.get() > nearest {
431 nearest = l.0.get();
432 color = Some(*c);
433 }
434 }
435
436 color.unwrap_or_else(|| LightDark::new(colors::WHITE, colors::BLACK))
437 };
438 match scheme {
439 ColorScheme::Light => c.light,
440 ColorScheme::Dark => c.dark,
441 _ => c.light,
442 }
443 })
444 }
445
446 pub fn info_color(&self) -> impl Var<Rgba> {
450 self.note_color(DataNoteLevel::INFO)
451 }
452
453 pub fn warn_color(&self) -> impl Var<Rgba> {
457 self.note_color(DataNoteLevel::WARN)
458 }
459
460 pub fn error_color(&self) -> impl Var<Rgba> {
464 self.note_color(DataNoteLevel::ERROR)
465 }
466}
467
468context_local! {
469 static DATA_CTX: BoxedAnyVar = LocalVar(()).boxed_any();
470 static DATA_NOTES_CTX: RwLock<DataNotesProbe> = RwLock::default();
471}
472
473#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
475#[serde(transparent)]
476pub struct DataNoteLevel(pub NonZeroU8);
477impl DataNoteLevel {
478 pub const INFO: Self = Self(NonZeroU8::new(1).unwrap());
480 pub const WARN: Self = Self(NonZeroU8::new(128).unwrap());
482 pub const ERROR: Self = Self(NonZeroU8::new(255).unwrap());
484
485 pub fn name(self) -> &'static str {
487 if self == Self::INFO {
488 "INFO"
489 } else if self == Self::WARN {
490 "WARN"
491 } else if self == Self::ERROR {
492 "ERROR"
493 } else {
494 ""
495 }
496 }
497}
498impl fmt::Debug for DataNoteLevel {
499 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500 let name = self.name();
501 if name.is_empty() {
502 f.debug_tuple("DataNoteLevel").field(&self.0).finish()
503 } else {
504 if f.alternate() {
505 write!(f, "DataNoteLevel::")?;
506 }
507 write!(f, "{name}")
508 }
509 }
510}
511
512#[derive(Clone)]
516pub struct DataNote {
517 source: WidgetId,
518 level: DataNoteLevel,
519 value: std::sync::Weak<dyn DataNoteValue>,
520}
521impl fmt::Debug for DataNote {
522 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
523 f.debug_struct("DataNote")
524 .field("source", &self.source)
525 .field("level", &self.level)
526 .field("value", &self.value())
527 .finish()
528 }
529}
530impl fmt::Display for DataNote {
531 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
532 if let Some(value) = self.value() {
533 write!(f, "{value}")
534 } else {
535 Ok(())
536 }
537 }
538}
539impl PartialEq for DataNote {
540 fn eq(&self, other: &Self) -> bool {
541 self.value.ptr_eq(&other.value) && self.source == other.source && self.level == other.level
542 }
543}
544impl DataNote {
545 pub fn new(source: WidgetId, level: DataNoteLevel, value: impl DataNoteValue + 'static) -> (Self, DataNoteHandle) {
547 let handle = Arc::new(value);
548 let value = Arc::downgrade(&handle);
549 (Self { source, level, value }, DataNoteHandle(Some(handle)))
550 }
551
552 pub fn source(&self) -> WidgetId {
554 self.source
555 }
556
557 pub fn level(&self) -> DataNoteLevel {
559 self.level
560 }
561
562 pub fn value(&self) -> Option<Arc<dyn DataNoteValue>> {
566 self.value.upgrade()
567 }
568
569 pub fn retain(&self) -> bool {
571 self.value.strong_count() > 0
572 }
573}
574
575#[must_use = "dropping the handle drops the data note"]
577pub struct DataNoteHandle(Option<Arc<dyn DataNoteValue>>);
578impl DataNoteHandle {
579 pub fn dummy() -> Self {
581 Self(None)
582 }
583
584 pub fn is_dummy(&self) -> bool {
586 self.0.is_some()
587 }
588}
589
590#[diagnostic::on_unimplemented(note = "`DataNoteValue` is implemented for all `T: Debug + Display + Send + Sync + Any")]
597pub trait DataNoteValue: fmt::Debug + fmt::Display + Send + Sync + Any {
598 fn as_any(&self) -> &dyn Any;
600}
601impl<T: fmt::Debug + fmt::Display + Send + Sync + Any + 'static> DataNoteValue for T {
602 fn as_any(&self) -> &dyn Any {
603 self
604 }
605}
606
607#[derive(Debug, Clone, PartialEq, Default)]
609pub struct DataNotes {
610 notes: Vec<DataNote>,
611}
612impl ops::Deref for DataNotes {
613 type Target = [DataNote];
614
615 fn deref(&self) -> &Self::Target {
616 &self.notes
617 }
618}
619impl DataNotes {
620 pub fn cleanup(&mut self) -> bool {
622 let len = self.notes.len();
623 self.notes.retain(|n| n.retain());
624 len != self.notes.len()
625 }
626
627 pub fn clone_level(&self, level: DataNoteLevel) -> Self {
629 let mut notes = vec![];
630 for note in &self.notes {
631 if note.level == level {
632 notes.push(note.clone())
633 }
634 }
635 Self { notes }
636 }
637
638 pub fn level_txt(&self, level: DataNoteLevel) -> Txt {
642 let mut txt = Txt::from_string(String::new());
643 let mut sep = "";
644 for note in &self.notes {
645 if note.level == level {
646 if let Some(value) = note.value() {
647 use std::fmt::Write;
648 let _ = write!(&mut txt, "{sep}{value}");
649 sep = "\n";
650 }
651 }
652 }
653 txt.end_mut();
654 txt
655 }
656}
657impl fmt::Display for DataNotes {
658 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
659 let mut sep = "";
660 for note in &self.notes {
661 if let Some(value) = note.value() {
662 write!(f, "{sep}{value}")?;
663 sep = "\n";
664 }
665 }
666 Ok(())
667 }
668}
669
670#[derive(Default)]
671struct DataNotesProbe {
672 notes: DataNotes,
673 changed: bool,
674}
675
676pub fn with_data_notes(child: impl UiNode, mut on_changed: impl FnMut(&DataNotes) + Send + 'static) -> impl UiNode {
684 let mut notes = None;
685 match_node(child, move |c, op| {
686 let is_deinit = match &op {
687 UiNodeOp::Init => {
688 notes = Some(Arc::new(RwLock::new(DataNotesProbe::default())));
689 false
690 }
691 UiNodeOp::Deinit => true,
692 _ => false,
693 };
694
695 DATA_NOTES_CTX.with_context(&mut notes, || c.op(op));
696
697 if is_deinit {
698 let n = notes.take().unwrap();
699 let not_empty = !mem::take(&mut n.write().notes).is_empty();
700 if not_empty {
701 on_changed(&DataNotes::default());
702 }
703 } else {
704 let notes = notes.as_ref().unwrap();
705 let mut notes = notes.write();
706
707 let cleaned = notes.notes.cleanup();
708 if mem::take(&mut notes.changed) || cleaned {
709 let notes = task::parking_lot::lock_api::RwLockWriteGuard::downgrade(notes);
710 let notes = ¬es.notes;
711
712 if !DATA_NOTES_CTX.is_default() {
713 let parent = DATA_NOTES_CTX.get();
714 let mut parent = parent.write();
715 for note in notes.iter() {
716 if parent.notes.iter().all(|n| n != note) {
717 parent.notes.notes.push(note.clone());
718 parent.changed = true;
719 }
720 }
721 }
722
723 on_changed(notes);
724 }
725 }
726 })
727}