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