Skip to main content

compose_rust/
notification.rs

1//! Notifications: calling the user from outside the window.
2//!
3//! A transient message speaks inside the window, for a few seconds, about something the user
4//! just did. A notification speaks outside it, about something the user did not do (a long
5//! job finished, something is waiting for an answer), and stays until the user clears it.
6//! Its moment is precisely when nobody is looking at the window.
7//!
8//! The Host does not call the platform. The Renderer owns the platform, so what crosses is a
9//! command record in an ordinary batch, and what comes back is an ordinary event. That keeps
10//! one way in to the Host (the Renderer's UI thread) and keeps every platform binding on the
11//! side that already has the window.
12//!
13//! A notification is usually a worker's news: the session ended on a worker thread, so that
14//! is where the application learns it. [`NotificationSender`] is `Send` for that reason.
15//! What it posts waits in a queue inside the Host, the Host asks for a frame the way it does
16//! for any worker's change, and the next batch carries it. Nothing here is a queue on the
17//! boundary: the boundary still only sees a batch handed over inside a call.
18
19use crate::schema::{NotificationImportance, NotificationPermission, NotificationPresentation};
20use std::cell::{Cell, RefCell};
21use std::rc::Rc;
22use std::sync::atomic::{AtomicBool, Ordering};
23use std::sync::{Arc, Mutex, MutexGuard};
24
25/// One notification, built and then posted.
26///
27/// ```ignore
28/// Notification::new("Session finished")
29///     .body("refactor-parser: 214 tests passed")
30///     .key(format!("session/{id}"))
31///     .channel("Sessions")
32///     .presentation(NotificationPresentation::WhenInactive)
33///     .post();
34/// ```
35///
36/// Nothing here names the application or its icon. The platform puts those on from the
37/// application's own bundle, which is where the window's icon comes from as well.
38#[derive(Clone, Debug, PartialEq, Eq)]
39pub struct Notification {
40    pub(crate) key: String,
41    pub(crate) title: String,
42    pub(crate) body: String,
43    pub(crate) channel: String,
44    pub(crate) actions: [String; 2],
45    pub(crate) importance: NotificationImportance,
46    pub(crate) presentation: NotificationPresentation,
47}
48
49impl Notification {
50    pub fn new(title: impl Into<String>) -> Self {
51        Self {
52            key: String::new(),
53            title: title.into(),
54            body: String::new(),
55            channel: String::new(),
56            actions: [String::new(), String::new()],
57            importance: NotificationImportance::Normal,
58            presentation: NotificationPresentation::Always,
59        }
60    }
61
62    /// The text under the title.
63    pub fn body(mut self, body: impl Into<String>) -> Self {
64        self.body = body.into();
65        self
66    }
67
68    /// The application's own name for this notification.
69    ///
70    /// Posting again under the same key replaces the one already showing, so a session
71    /// whose state keeps changing has one notification that is kept current rather than a
72    /// pile of them. The key is also what comes back when the user presses it, and what
73    /// [`withdraw_notification`] takes.
74    ///
75    /// Without one the Renderer names it, and a notification named that way can be neither
76    /// replaced nor withdrawn.
77    pub fn key(mut self, key: impl Into<String>) -> Self {
78        self.key = key.into();
79        self
80    }
81
82    /// The group the user can turn off on its own, in the application's own words.
83    ///
84    /// Android lists these in its settings, so the name is something a person reads. Empty
85    /// is the application's default group. A platform without groups ignores it.
86    pub fn channel(mut self, channel: impl Into<String>) -> Self {
87        self.channel = channel.into();
88        self
89    }
90
91    /// The first button. Pressing it reports action 1, and does not bring the window up:
92    /// a button on a notification exists to be dealt with without opening anything.
93    pub fn action_1(mut self, label: impl Into<String>) -> Self {
94        self.actions[0] = label.into();
95        self
96    }
97
98    /// The second button, which reports action 2. Two is the most every platform shows
99    /// the same way.
100    pub fn action_2(mut self, label: impl Into<String>) -> Self {
101        self.actions[1] = label.into();
102        self
103    }
104
105    /// How much this matters. What that is worth in sound or in getting through Do Not
106    /// Disturb is the platform's answer.
107    pub fn importance(mut self, importance: NotificationImportance) -> Self {
108        self.importance = importance;
109        self
110    }
111
112    /// Whether it is shown while the application's window is the active one.
113    pub fn presentation(mut self, presentation: NotificationPresentation) -> Self {
114        self.presentation = presentation;
115        self
116    }
117
118    /// Hands the notification over.
119    ///
120    /// From a component or an event handler it goes out with the batch that call produces.
121    /// From any other thread it waits in the Host and the Host asks for a frame, which is
122    /// what [`NotificationSender::post`] does as well.
123    pub fn post(self) {
124        submit(NotificationCommand::Post(self));
125    }
126}
127
128/// Takes back the notification posted under `key`.
129///
130/// An approval the user already gave inside the window leaves "waiting for approval" in the
131/// notification centre saying something that is no longer true. This is how that stops.
132pub fn withdraw_notification(key: impl Into<String>) {
133    submit(NotificationCommand::Withdraw(key.into()));
134}
135
136/// Asks for permission to show notifications now, rather than on the first one posted.
137///
138/// For a settings screen with a switch that says "notify me when a session finishes":
139/// asking at the moment the user turns it on is asking when the user knows what for. The
140/// answer arrives through [`notification_permission`] and the readers registered with
141/// [`subscribe_permission`].
142///
143/// A browser only lets a page ask from inside something the user did. Called from the
144/// handler of that press, the request rides out in the batch the press produced and is
145/// applied on the same call stack, so it is still inside it.
146pub fn request_notification_permission() {
147    submit(NotificationCommand::RequestPermission);
148}
149
150/// A handle a worker thread posts notifications through.
151///
152/// `Send`, `Copy` and free to make: the queue it feeds belongs to the Host, so every handle
153/// reaches the same one. Nothing it does calls the Renderer. It queues, and the Host asks
154/// for a frame in which the queue is written into the batch.
155///
156/// ```ignore
157/// let notifications = NotificationSender::new();
158/// std::thread::spawn(move || {
159///     run_session();
160///     notifications.post(Notification::new("Session finished").key("session/7"));
161/// });
162/// ```
163#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
164pub struct NotificationSender {
165    _private: (),
166}
167
168impl NotificationSender {
169    pub const fn new() -> Self {
170        Self { _private: () }
171    }
172
173    pub fn post(&self, notification: Notification) {
174        submit(NotificationCommand::Post(notification));
175    }
176
177    pub fn withdraw(&self, key: impl Into<String>) {
178        submit(NotificationCommand::Withdraw(key.into()));
179    }
180
181    pub fn request_permission(&self) {
182        submit(NotificationCommand::RequestPermission);
183    }
184}
185
186/// What the queue holds, in the order it was asked for. A post followed by a withdrawal of
187/// the same key has to reach the Renderer in that order, so the three share one queue.
188#[derive(Clone, Debug, PartialEq, Eq)]
189pub(crate) enum NotificationCommand {
190    Post(Notification),
191    Withdraw(String),
192    RequestPermission,
193}
194
195/// Commands waiting for the next batch. Shared across threads, because a worker posts.
196static QUEUE: Mutex<Vec<NotificationCommand>> = Mutex::new(Vec::new());
197
198/// Whether the queue has anything in it, read without taking the lock.
199///
200/// Every batch looks, and almost every batch finds nothing. A flag read first keeps that
201/// look to one atomic load and no lock and no allocation.
202static PENDING: AtomicBool = AtomicBool::new(false);
203
204fn queue() -> MutexGuard<'static, Vec<NotificationCommand>> {
205    // A worker that panicked while holding the lock does not make the queue wrong: a push
206    // either happened or did not. Refusing every later notification would.
207    QUEUE.lock().unwrap_or_else(|poison| poison.into_inner())
208}
209
210fn submit(command: NotificationCommand) {
211    queue().push(command);
212    PENDING.store(true, Ordering::Release);
213    // Inside a Host call on the UI thread, the batch that call is about to produce carries
214    // it, so asking for a frame would only buy an empty one.
215    if !crate::boundary::in_host_call() {
216        crate::boundary::request_frame_for_notifications();
217    }
218}
219
220/// Hands every queued command to `emit` and empties the queue.
221///
222/// The buffer is moved out and put back, so posting through a steady stream reuses one
223/// allocation. Returns how many posts went out, so the Host can say once, in a debug build,
224/// that a platform which cannot show them is taking them.
225pub(crate) fn drain(mut emit: impl FnMut(&NotificationCommand)) -> usize {
226    if !PENDING.swap(false, Ordering::AcqRel) {
227        return 0;
228    }
229    let mut taken = std::mem::take(&mut *queue());
230    let mut posts = 0;
231    for command in &taken {
232        if matches!(command, NotificationCommand::Post(_)) {
233            posts += 1;
234        }
235        emit(command);
236    }
237    taken.clear();
238    let mut guard = queue();
239    if guard.is_empty() {
240        *guard = taken;
241    } else {
242        // Something was posted while these were being written. It is still waiting, so the
243        // flag has to say so.
244        PENDING.store(true, Ordering::Release);
245    }
246    posts
247}
248
249/// What the user pressed.
250#[derive(Clone, Debug, PartialEq, Eq)]
251pub struct NotificationActivation {
252    /// The key the notification was posted under.
253    pub key: String,
254    /// 0 for the notification itself, 1 or 2 for one of its buttons.
255    pub action: u32,
256}
257
258impl NotificationActivation {
259    /// True when the notification itself was pressed rather than one of its buttons. The
260    /// Renderer has already brought the window up.
261    pub fn is_body(&self) -> bool {
262        self.action == 0
263    }
264
265    /// Which button was pressed, if one was.
266    pub fn action_button(&self) -> Option<u32> {
267        (self.action != 0).then_some(self.action)
268    }
269}
270
271struct Subscriber {
272    id: u64,
273    notify: Arc<dyn Fn() + Send + Sync>,
274}
275
276/// What an activation is handed to. Reference counted so the list can be copied out
277/// before any of them runs: a handler may add or remove one.
278type ActivationHandler = Rc<dyn Fn(NotificationActivation)>;
279
280thread_local! {
281    /// Both sides start from here, so the Renderer only has to report a difference.
282    static PERMISSION: Cell<NotificationPermission> =
283        const { Cell::new(NotificationPermission::NotDetermined) };
284    static PERMISSION_SUBSCRIBERS: RefCell<Vec<Subscriber>> = const { RefCell::new(Vec::new()) };
285    static ACTIVATION_HANDLERS: RefCell<Vec<(u64, ActivationHandler)>> =
286        const { RefCell::new(Vec::new()) };
287    static NEXT_ID: Cell<u64> = const { Cell::new(1) };
288}
289
290/// Whether a post that a platform could not show has already been explained.
291static EXPLAINED: AtomicBool = AtomicBool::new(false);
292/// Whether anything has been posted yet, so an `Unsupported` that arrives afterwards can
293/// still be explained once.
294static POSTED: AtomicBool = AtomicBool::new(false);
295
296fn next_id() -> u64 {
297    NEXT_ID.with(|next| {
298        let id = next.get();
299        next.set(id + 1);
300        id
301    })
302}
303
304/// The permission the Renderer last reported. Available outside a component too.
305pub fn notification_permission() -> NotificationPermission {
306    PERMISSION.with(Cell::get)
307}
308
309/// Records the Renderer's answer and wakes the components that asked about it.
310///
311/// Returns whether any component was woken, which is what decides whether the Host renders.
312pub(crate) fn publish_permission(state: NotificationPermission) -> bool {
313    if state == NotificationPermission::Unsupported && POSTED.load(Ordering::Acquire) {
314        explain_unsupported();
315    }
316    if PERMISSION.with(Cell::get) == state {
317        return false;
318    }
319    PERMISSION.with(|current| current.set(state));
320    PERMISSION_SUBSCRIBERS.with_borrow(|subscribers| {
321        for subscriber in subscribers {
322            (subscriber.notify)();
323        }
324        !subscribers.is_empty()
325    })
326}
327
328/// Records that `posts` notifications went out in a batch, and says once, in a debug build,
329/// why none of them will appear when this run cannot show any.
330pub(crate) fn note_posted(posts: usize) {
331    if posts == 0 {
332        return;
333    }
334    POSTED.store(true, Ordering::Release);
335    if notification_permission() == NotificationPermission::Unsupported {
336        explain_unsupported();
337    }
338}
339
340fn explain_unsupported() {
341    if cfg!(debug_assertions) && !EXPLAINED.swap(true, Ordering::AcqRel) {
342        eprintln!("compose-rust: {}", unsupported_reason());
343    }
344}
345
346/// What usually stands between this platform and a notification, for the one debug line.
347///
348/// Worked out from the platform this was built for, because the Host is never told the
349/// Renderer's reason, only that there is one.
350pub fn unsupported_reason() -> &'static str {
351    if cfg!(target_os = "macos") || cfg!(target_os = "ios") {
352        "notifications are not shown in this run: it has no bundle identifier, so the \
353         notification centre will not take them. A signed application bundle has one; a \
354         development run of a bare executable does not."
355    } else if cfg!(target_os = "windows") {
356        "notifications are not shown in this run: no Start menu shortcut carries an \
357         AppUserModelID for this executable, and toasts need one. An installed package \
358         (MSIX, or an installer that makes the shortcut) has it; an unpacked executable \
359         does not."
360    } else if cfg!(target_os = "android") {
361        "notifications are not shown in this run: the system has no notification service \
362         for this application."
363    } else if cfg!(target_family = "wasm") {
364        "notifications are not shown in this run: the page is not a secure context (HTTPS \
365         or localhost), or this browser has no Notifications API."
366    } else {
367        "notifications are not shown in this run: nothing on the session bus owns \
368         org.freedesktop.Notifications, so there is no notification daemon to show them."
369    }
370}
371
372/// Hands an activation to every component that asked for one.
373///
374/// Returns whether anyone was listening. Nobody listening is not an error: the event is
375/// dropped, which is what an application that never asked expects.
376pub(crate) fn activate(activation: NotificationActivation) -> bool {
377    // Copied out first. A handler may mount or unmount a component that listens, and that
378    // edits the list being walked.
379    let handlers: Vec<ActivationHandler> = ACTIVATION_HANDLERS
380        .with_borrow(|handlers| handlers.iter().map(|(_, h)| h.clone()).collect());
381    for handler in &handlers {
382        handler(activation.clone());
383    }
384    !handlers.is_empty()
385}
386
387/// Forgets the permission, the listeners and anything still queued. Used between tests.
388#[doc(hidden)]
389pub fn reset_notifications() {
390    queue().clear();
391    PENDING.store(false, Ordering::Release);
392    PERMISSION.with(|current| current.set(NotificationPermission::NotDetermined));
393    PERMISSION_SUBSCRIBERS.with_borrow_mut(Vec::clear);
394    ACTIVATION_HANDLERS.with_borrow_mut(Vec::clear);
395    POSTED.store(false, Ordering::Release);
396    EXPLAINED.store(false, Ordering::Release);
397}
398
399/// A registration for changes of the notification permission. Dropping it ends the
400/// registration, so a runtime keeps it for as long as the reader it wakes is alive.
401pub struct PermissionSubscription {
402    id: u64,
403}
404
405/// Registers `notify` to be called each time the Renderer reports a different permission.
406/// The Dioxus adapter's `use_notification_permission` hook holds one.
407pub fn subscribe_permission(notify: Arc<dyn Fn() + Send + Sync>) -> PermissionSubscription {
408    let id = next_id();
409    PERMISSION_SUBSCRIBERS
410        .with_borrow_mut(|subscribers| subscribers.push(Subscriber { id, notify }));
411    PermissionSubscription { id }
412}
413
414impl Drop for PermissionSubscription {
415    fn drop(&mut self) {
416        PERMISSION_SUBSCRIBERS.with_borrow_mut(|subscribers| {
417            subscribers.retain(|subscriber| subscriber.id != self.id);
418        });
419    }
420}
421
422/// An activation handler's registration. Dropping it removes the handler.
423pub struct ActivationSubscription {
424    id: u64,
425}
426
427impl Drop for ActivationSubscription {
428    fn drop(&mut self) {
429        ACTIVATION_HANDLERS.with_borrow_mut(|handlers| handlers.retain(|(id, _)| *id != self.id));
430    }
431}
432
433/// Runs `handler` when the user presses a notification this application posted, for as
434/// long as the returned registration is kept.
435///
436/// One registration for the whole application rather than a callback per notification. A
437/// notification stays in the notification centre for hours, long after whatever posted it
438/// is gone, so what it carries back is the key it was posted under. The Dioxus adapter's
439/// `use_notification_activated` hook holds one of these for a component.
440///
441/// With no handler registered, an activation is dropped.
442pub fn on_activation(handler: Rc<dyn Fn(NotificationActivation)>) -> ActivationSubscription {
443    let id = next_id();
444    ACTIVATION_HANDLERS.with_borrow_mut(|handlers| handlers.push((id, handler)));
445    ActivationSubscription { id }
446}
447
448#[cfg(test)]
449mod tests {
450    use super::*;
451
452    #[test]
453    fn fr36_a_notification_says_nothing_it_was_not_told() {
454        let notification = Notification::new("title");
455        assert_eq!(notification.key, "");
456        assert_eq!(notification.body, "");
457        assert_eq!(notification.channel, "");
458        assert_eq!(notification.actions, [String::new(), String::new()]);
459        assert_eq!(notification.importance, NotificationImportance::Normal);
460        assert_eq!(notification.presentation, NotificationPresentation::Always);
461    }
462
463    #[test]
464    fn fr36_an_activation_knows_whether_a_button_was_pressed() {
465        let body = NotificationActivation {
466            key: "k".to_owned(),
467            action: 0,
468        };
469        let button = NotificationActivation {
470            key: "k".to_owned(),
471            action: 2,
472        };
473        assert!(body.is_body());
474        assert_eq!(body.action_button(), None);
475        assert!(!button.is_body());
476        assert_eq!(button.action_button(), Some(2));
477    }
478}