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