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 }
442 })
443 }
444
445 pub fn info_color(&self) -> impl Var<Rgba> {
449 self.note_color(DataNoteLevel::INFO)
450 }
451
452 pub fn warn_color(&self) -> impl Var<Rgba> {
456 self.note_color(DataNoteLevel::WARN)
457 }
458
459 pub fn error_color(&self) -> impl Var<Rgba> {
463 self.note_color(DataNoteLevel::ERROR)
464 }
465}
466
467context_local! {
468 static DATA_CTX: BoxedAnyVar = LocalVar(()).boxed_any();
469 static DATA_NOTES_CTX: RwLock<DataNotesProbe> = RwLock::default();
470}
471
472#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize)]
474#[serde(transparent)]
475pub struct DataNoteLevel(pub NonZeroU8);
476impl DataNoteLevel {
477 pub const INFO: Self = Self(NonZeroU8::new(1).unwrap());
481 pub const WARN: Self = Self(NonZeroU8::new(128).unwrap());
483 pub const ERROR: Self = Self(NonZeroU8::new(255).unwrap());
485
486 pub fn name(self) -> &'static str {
488 if self == Self::INFO {
489 "INFO"
490 } else if self == Self::WARN {
491 "WARN"
492 } else if self == Self::ERROR {
493 "ERROR"
494 } else {
495 ""
496 }
497 }
498}
499impl fmt::Debug for DataNoteLevel {
500 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
501 let name = self.name();
502 if name.is_empty() {
503 f.debug_tuple("DataNoteLevel").field(&self.0).finish()
504 } else {
505 if f.alternate() {
506 write!(f, "DataNoteLevel::")?;
507 }
508 write!(f, "{name}")
509 }
510 }
511}
512
513#[derive(Clone)]
517pub struct DataNote {
518 source: WidgetId,
519 level: DataNoteLevel,
520 value: std::sync::Weak<dyn DataNoteValue>,
521}
522impl fmt::Debug for DataNote {
523 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
524 f.debug_struct("DataNote")
525 .field("source", &self.source)
526 .field("level", &self.level)
527 .field("value", &self.value())
528 .finish()
529 }
530}
531impl fmt::Display for DataNote {
532 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
533 if let Some(value) = self.value() {
534 write!(f, "{value}")
535 } else {
536 Ok(())
537 }
538 }
539}
540impl PartialEq for DataNote {
541 fn eq(&self, other: &Self) -> bool {
542 self.value.ptr_eq(&other.value) && self.source == other.source && self.level == other.level
543 }
544}
545impl DataNote {
546 pub fn new(source: WidgetId, level: DataNoteLevel, value: impl DataNoteValue + 'static) -> (Self, DataNoteHandle) {
548 let handle = Arc::new(value);
549 let value = Arc::downgrade(&handle);
550 (Self { source, level, value }, DataNoteHandle(Some(handle)))
551 }
552
553 pub fn source(&self) -> WidgetId {
555 self.source
556 }
557
558 pub fn level(&self) -> DataNoteLevel {
560 self.level
561 }
562
563 pub fn value(&self) -> Option<Arc<dyn DataNoteValue>> {
567 self.value.upgrade()
568 }
569
570 pub fn retain(&self) -> bool {
572 self.value.strong_count() > 0
573 }
574}
575
576#[must_use = "dropping the handle drops the data note"]
578pub struct DataNoteHandle(Option<Arc<dyn DataNoteValue>>);
579impl DataNoteHandle {
580 pub fn dummy() -> Self {
582 Self(None)
583 }
584
585 pub fn is_dummy(&self) -> bool {
587 self.0.is_some()
588 }
589}
590
591#[diagnostic::on_unimplemented(note = "`DataNoteValue` is implemented for all `T: Debug + Display + Send + Sync + Any")]
598pub trait DataNoteValue: fmt::Debug + fmt::Display + Send + Sync + Any {
599 fn as_any(&self) -> &dyn Any;
601}
602impl<T: fmt::Debug + fmt::Display + Send + Sync + Any + 'static> DataNoteValue for T {
603 fn as_any(&self) -> &dyn Any {
604 self
605 }
606}
607
608#[derive(Debug, Clone, PartialEq, Default)]
610pub struct DataNotes {
611 notes: Vec<DataNote>,
612}
613impl ops::Deref for DataNotes {
614 type Target = [DataNote];
615
616 fn deref(&self) -> &Self::Target {
617 &self.notes
618 }
619}
620impl DataNotes {
621 pub fn cleanup(&mut self) -> bool {
623 let len = self.notes.len();
624 self.notes.retain(|n| n.retain());
625 len != self.notes.len()
626 }
627
628 pub fn clone_level(&self, level: DataNoteLevel) -> Self {
630 let mut notes = vec![];
631 for note in &self.notes {
632 if note.level == level {
633 notes.push(note.clone())
634 }
635 }
636 Self { notes }
637 }
638
639 pub fn level_txt(&self, level: DataNoteLevel) -> Txt {
643 let mut txt = Txt::from_string(String::new());
644 let mut sep = "";
645 for note in &self.notes {
646 if note.level == level {
647 if let Some(value) = note.value() {
648 use std::fmt::Write;
649 let _ = write!(&mut txt, "{sep}{value}");
650 sep = "\n";
651 }
652 }
653 }
654 txt.end_mut();
655 txt
656 }
657}
658impl fmt::Display for DataNotes {
659 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
660 let mut sep = "";
661 for note in &self.notes {
662 if let Some(value) = note.value() {
663 write!(f, "{sep}{value}")?;
664 sep = "\n";
665 }
666 }
667 Ok(())
668 }
669}
670
671#[derive(Default)]
672struct DataNotesProbe {
673 notes: DataNotes,
674 changed: bool,
675}
676
677pub fn with_data_notes(child: impl UiNode, mut on_changed: impl FnMut(&DataNotes) + Send + 'static) -> impl UiNode {
685 let mut notes = None;
686 match_node(child, move |c, op| {
687 let is_deinit = match &op {
688 UiNodeOp::Init => {
689 notes = Some(Arc::new(RwLock::new(DataNotesProbe::default())));
690 false
691 }
692 UiNodeOp::Deinit => true,
693 _ => false,
694 };
695
696 DATA_NOTES_CTX.with_context(&mut notes, || c.op(op));
697
698 if is_deinit {
699 let n = notes.take().unwrap();
700 let not_empty = !mem::take(&mut n.write().notes).is_empty();
701 if not_empty {
702 on_changed(&DataNotes::default());
703 }
704 } else {
705 let notes = notes.as_ref().unwrap();
706 let mut notes = notes.write();
707
708 let cleaned = notes.notes.cleanup();
709 if mem::take(&mut notes.changed) || cleaned {
710 let notes = task::parking_lot::lock_api::RwLockWriteGuard::downgrade(notes);
711 let notes = ¬es.notes;
712
713 if !DATA_NOTES_CTX.is_default() {
714 let parent = DATA_NOTES_CTX.get();
715 let mut parent = parent.write();
716 for note in notes.iter() {
717 if parent.notes.iter().all(|n| n != note) {
718 parent.notes.notes.push(note.clone());
719 parent.changed = true;
720 }
721 }
722 }
723
724 on_changed(notes);
725 }
726 }
727 })
728}