all_is_cubes_ui/ui_content/notification.rs
1//! Types used to create notifications displayed to the user.
2
3use alloc::sync::{Arc, Weak};
4use alloc::vec::Vec;
5use std::sync::Mutex;
6
7use all_is_cubes::arcstr::ArcStr;
8use all_is_cubes::listen;
9
10#[cfg(doc)]
11use crate::apps::{MainTaskContext, Session};
12use crate::vui::widgets::ProgressBarState;
13
14// ---- Types --------------------------------------------------------------------------------------
15
16/// User-visible contents of a [`Notification`].
17///
18/// This value type is cheap to clone and comparing it compares the entire content.
19// TODO: don't expose this enum directly, for future-proofing
20#[allow(missing_docs)]
21#[derive(Clone, Debug, PartialEq)]
22#[non_exhaustive]
23#[expect(clippy::module_name_repetitions)] // TODO: rename?
24pub enum NotificationContent {
25 // TODO: Not implemented:
26 // /// The message may be multi-line.
27 // Message(ArcStr),
28 Progress {
29 /// The activity this is the progress of.
30 title: ArcStr,
31
32 /// The amount of progress.
33 progress: ProgressBarState,
34
35 /// The particular piece of the overall work that is currently being done
36 /// (or was just finished, if that is all that is available).
37 part: ArcStr,
38 },
39}
40
41// impl From<ArcStr> for NotificationContent {
42// fn from(message: ArcStr) -> Self {
43// Self::Message(message)
44// }
45// }
46
47/// A piece of information for the user's attention.
48///
49/// The carried message is displayed until the user dismisses it,
50/// or this [`Notification`] value is dropped.
51///
52/// To create and display a notification, call [`Session::show_notification()`] or
53/// [`MainTaskContext::show_notification()`].
54#[derive(Debug)]
55pub struct Notification {
56 shared: Arc<Shared>,
57}
58
59/// Reasons a notification could not be created.
60#[derive(Clone, Debug, Eq, PartialEq, displaydoc::Display)]
61#[non_exhaustive]
62pub enum Error {
63 /// no UI is available to display a notification
64 NoUi,
65 /// too many notifications
66 Overflow,
67}
68
69/// Data shared between [`Notification`] and [`Receiver`].
70#[derive(Debug)]
71struct Shared {
72 content: Mutex<NotificationContent>,
73 notifier: listen::Notifier<()>,
74}
75
76/// Receiving end of a [`Notification`] channel, owned by [`Hub`].
77#[derive(Debug)]
78pub(crate) struct Receiver {
79 shared: Weak<Shared>,
80}
81
82/// Collects input from [`Notification`]s to determine what should be displayed to the user.
83#[derive(Debug)]
84pub(crate) struct Hub {
85 notifications: Vec<Receiver>,
86
87 /// TODO: kludge to get progress UI up and going; eventually everything should be more dynamic
88 /// and be able to display however many notifications.
89 primary_content: listen::Cell<Option<NotificationContent>>,
90
91 has_interrupt: bool,
92}
93
94// --- Implementations -----------------------------------------------------------------------------
95
96impl Notification {
97 pub(crate) fn new(content: NotificationContent) -> (Self, Receiver) {
98 let shared = Arc::new(Shared {
99 content: Mutex::new(content),
100 notifier: listen::Notifier::new(),
101 });
102
103 let receiver = Receiver {
104 shared: Arc::downgrade(&shared),
105 };
106 let notif = Notification { shared };
107 (notif, receiver)
108 }
109
110 /// Replace the existing content of the notification.
111 pub fn set_content(&self, content: NotificationContent) {
112 *self.shared.content.lock().unwrap() = content;
113 self.shared.notifier.notify(&());
114 }
115}
116
117impl Receiver {
118 /// Returns the current content of this notification.
119 ///
120 /// Returns `None` if the notification was dropped or its state became poisoned.
121 /// In that case, this [`Receiver`] should be discarded.
122 pub(crate) fn read_content(&self) -> Option<NotificationContent> {
123 let shared = self.shared.upgrade()?;
124 let content = shared.content.lock().ok()?.clone();
125 Some(content)
126 }
127}
128
129impl Hub {
130 pub fn new() -> Self {
131 Self {
132 notifications: Vec::new(),
133 primary_content: listen::Cell::new(None),
134 has_interrupt: false,
135 }
136 }
137
138 pub(crate) fn update(&mut self) {
139 let mut primary = None;
140 self.notifications.retain(|n| {
141 if let Some(content) = n.read_content() {
142 primary = Some(content);
143 true
144 } else {
145 false
146 }
147 });
148 self.has_interrupt = primary.is_some();
149 self.primary_content.set_if_unequal(primary);
150 }
151
152 pub(crate) fn insert(&mut self, content: NotificationContent) -> Notification {
153 let (notification, nrec) = Notification::new(content);
154 // TODO: limit maximum number of notifications
155 self.notifications.push(nrec);
156 notification
157 }
158
159 pub(crate) fn primary_content(&self) -> listen::DynSource<Option<NotificationContent>> {
160 self.primary_content.as_source()
161 }
162
163 pub(crate) fn has_interrupt(&self) -> bool {
164 self.has_interrupt
165 }
166}
167
168// fn dummy_content() -> NotificationContent {
169// // TODO: should be not Progress but that is all we have right now
170// NotificationContent::Progress {
171// title: ArcStr::new(),
172// progress: ProgressBarState::new(0.0),
173// part: ArcStr::new(),
174// }
175// }