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// }