Skip to main content

compose_rust/
message.rs

1//! Transient messages: the one sentence an application says after something happened.
2//!
3//! A message is not part of the tree. It has a lifetime rather than a position, and that
4//! lifetime belongs to the Renderer: how long it stays, where it sits, what it does when a
5//! second one arrives while the first is still up. Modelling it as a node would mean the
6//! Host holding "showing until four seconds from now" and running the VirtualDom again to
7//! take it away, which is a render for an animation nobody asked Rust about.
8//!
9//! So a message is posted and forgotten. It rides out on the batch the current call
10//! produces, in the same way every other record does.
11
12use crate::schema::MessageDuration;
13use dioxus_core::Callback;
14use std::cell::{Cell, RefCell};
15use std::collections::VecDeque;
16
17/// One message, ready to be written into a batch.
18pub(crate) struct PendingMessage {
19    pub(crate) handler_id: u64,
20    pub(crate) text: String,
21    pub(crate) action: String,
22    pub(crate) duration: MessageDuration,
23}
24
25/// Where message action ids start.
26///
27/// Handler ids for listeners on nodes are handed out from 1 upwards, so starting here
28/// leaves the two sets of ids unable to meet: an event carrying an id this large named an
29/// action on a message, and one below it named a listener on a node.
30const ACTION_HANDLER_BASE: u64 = 1 << 63;
31
32/// How many action callbacks are kept alive at once.
33///
34/// A message that has left the screen can never be pressed again, but nothing tells the
35/// Host when that happened, and adding a record that did would put the Renderer's own
36/// timer back on the wire. The bound solves it from the other end: the Renderer shows one
37/// message at a time and queues at most eight, so an action older than that is already
38/// unreachable and its callback can be dropped.
39const LIVE_ACTIONS: usize = 16;
40
41thread_local! {
42    static QUEUE: RefCell<Vec<PendingMessage>> = const { RefCell::new(Vec::new()) };
43    static ACTIONS: RefCell<VecDeque<(u64, Callback<()>)>> = const {
44        RefCell::new(VecDeque::new())
45    };
46    static NEXT_ACTION_ID: Cell<u64> = const { Cell::new(ACTION_HANDLER_BASE) };
47}
48
49/// A sentence to show the user once, with an optional thing to do about it.
50///
51/// ```ignore
52/// Message::new("Task deleted")
53///     .with_action("Undo", move |()| restore())
54///     .with_duration(MessageDuration::Long)
55///     .show();
56/// ```
57///
58/// Nothing here says where the message appears or what it looks like. Material calls it a
59/// snackbar, Apple a transient overlay, GNOME a toast: the design system decides, the same
60/// way it decides what a `Dialog` looks like.
61pub struct Message {
62    text: String,
63    action: String,
64    on_action: Option<Callback<()>>,
65    duration: MessageDuration,
66}
67
68impl Message {
69    pub fn new(text: impl Into<String>) -> Self {
70        Self {
71            text: text.into(),
72            action: String::new(),
73            on_action: None,
74            duration: MessageDuration::Short,
75        }
76    }
77
78    /// Adds the one thing the user can do about the message, such as undoing it.
79    ///
80    /// Call this from a component or an event handler: the callback is owned by the scope
81    /// it is created in, which is what lets it touch that scope's signals when it runs.
82    pub fn with_action(
83        mut self,
84        label: impl Into<String>,
85        on_action: impl FnMut(()) + 'static,
86    ) -> Self {
87        self.action = label.into();
88        self.on_action = Some(Callback::new(on_action));
89        self
90    }
91
92    pub fn with_duration(mut self, duration: MessageDuration) -> Self {
93        self.duration = duration;
94        self
95    }
96
97    /// Hands the message over. It goes out with the batch this call produces.
98    pub fn show(self) {
99        let handler_id = match self.on_action {
100            Some(callback) => {
101                let id = NEXT_ACTION_ID.with(|next| {
102                    let id = next.get();
103                    next.set(id.wrapping_add(1).max(ACTION_HANDLER_BASE));
104                    id
105                });
106                ACTIONS.with_borrow_mut(|actions| {
107                    while actions.len() >= LIVE_ACTIONS {
108                        actions.pop_front();
109                    }
110                    actions.push_back((id, callback));
111                });
112                id
113            }
114            None => 0,
115        };
116        QUEUE.with_borrow_mut(|queue| {
117            queue.push(PendingMessage {
118                handler_id,
119                text: self.text,
120                action: self.action,
121                duration: self.duration,
122            });
123        });
124    }
125}
126
127/// Shows a message with no action, which is the common case.
128pub fn show_message(text: impl Into<String>) {
129    Message::new(text).show();
130}
131
132/// Hands every queued message to `emit` and empties the queue.
133///
134/// The buffer is moved out and put back, so a steady stream of messages reuses the same
135/// allocation rather than asking for a new one each frame.
136pub(crate) fn drain(mut emit: impl FnMut(&PendingMessage)) {
137    let mut taken = QUEUE.with_borrow_mut(std::mem::take);
138    if taken.is_empty() {
139        return;
140    }
141    for message in &taken {
142        emit(message);
143    }
144    taken.clear();
145    QUEUE.with_borrow_mut(|queue| {
146        if queue.is_empty() {
147            *queue = taken;
148        }
149    });
150}
151
152/// The callback behind a message action id, removed as it is handed over.
153///
154/// Removed, because an action is a thing the user does once: the message goes away when it
155/// is pressed, so a second press would be a press on something that is no longer there.
156pub(crate) fn take_action(handler_id: u64) -> Option<Callback<()>> {
157    if handler_id < ACTION_HANDLER_BASE {
158        return None;
159    }
160    ACTIONS.with_borrow_mut(|actions| {
161        let position = actions.iter().position(|(id, _)| *id == handler_id)?;
162        actions.remove(position).map(|(_, callback)| callback)
163    })
164}
165
166/// Forgets every queued message and every live action. Used between tests and when a new
167/// `Host` takes over the thread.
168#[doc(hidden)]
169pub fn reset_messages() {
170    QUEUE.with_borrow_mut(Vec::clear);
171    ACTIONS.with_borrow_mut(VecDeque::clear);
172    NEXT_ACTION_ID.with(|next| next.set(ACTION_HANDLER_BASE));
173}