fractal/utils/toast.rs
1//! Macros and methods to display toasts in the interface.
2
3use std::collections::HashMap;
4
5use adw::prelude::*;
6
7use super::freplace;
8use crate::{
9 Window,
10 components::{LabelWithWidgets, Pill, ToastableDialog},
11 prelude::*,
12};
13
14/// Show a toast with the given message on an ancestor of `widget`.
15///
16/// The simplest way to use this macro is for displaying a simple message. It
17/// can be anything that implements `AsRef<str>`.
18///
19/// ```no_run
20/// use gettextts::gettext;
21///
22/// use crate::toast;
23///
24/// # let widget = unimplemented!();
25/// toast!(widget, gettext("Something happened"));
26/// ```
27///
28/// This macro also supports replacing named variables with their value. It
29/// supports both the `var` and the `var = expr` syntax. The variable value must
30/// implement `ToString`.
31///
32/// ```no_run
33/// use gettextts::gettext;
34///
35/// use crate::toast;
36///
37/// # let widget = unimplemented!();
38/// # let error_nb = 0;
39/// toast!(
40/// widget,
41/// gettext("Error number {n}: {msg}"),
42/// n = error_nb.to_string(),
43/// msg,
44/// );
45/// ```
46///
47/// To add [`Pill`]s to the toast, you can precede a type that implements
48/// [`PillSource`] with `@`.
49///
50/// ```no_run
51/// use gettextts::gettext;
52/// use crate::toast;
53/// use crate::session::{Room, User};
54///
55/// # let session = unimplemented!();
56/// # let room_id = unimplemented!();
57/// # let user_id = unimplemented!();
58/// let room = Room::new(session, room_id);
59/// let member = Member::new(room, user_id);
60///
61/// toast!(
62/// widget,
63/// gettext("Could not contact {user} in {room}"),
64/// @user = member,
65/// @room,
66/// );
67/// ```
68///
69/// For this macro to work, the widget must have one of these ancestors that can
70/// show toasts:
71///
72/// - `ToastableDialog`
73/// - `AdwPreferencesDialog`
74/// - `AdwPreferencesWindow`
75/// - `Window`
76///
77/// [`PillSource`]: crate::components::PillSource
78#[macro_export]
79macro_rules! toast {
80 // Without vars, with or without a trailing comma.
81 ($widget:expr, $message:expr $(,)?) => {
82 {
83 $crate::utils::toast::add_toast(
84 $widget.upcast_ref(),
85 adw::Toast::new($message.as_ref())
86 );
87 }
88 };
89 // With vars.
90 ($widget:expr, $message:expr, $($tail:tt)+) => {
91 {
92 let (string_vars, pill_vars) = $crate::_toast_accum!([], [], $($tail)+);
93 $crate::utils::toast::add_toast_with_vars(
94 $widget.upcast_ref(),
95 $message.as_ref(),
96 &string_vars,
97 &pill_vars.into()
98 );
99 }
100 };
101}
102
103/// Macro to accumulate the variables passed to `toast!`.
104///
105/// Returns a `([(&str, String)],[(&str, Pill)])` tuple. The items in the first
106/// array are `(var_name, var_value)` tuples, and the ones in the second array
107/// are `(var_name, pill)` tuples.
108#[doc(hidden)]
109#[macro_export]
110macro_rules! _toast_accum {
111 // `var = val` syntax, without anything after.
112 ([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident = $val:expr) => {
113 $crate::_toast_accum!([$($string_vars)*], [$($pill_vars)*], $var = $val,)
114 };
115 // `var = val` syntax, with a trailing comma or other vars after.
116 ([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident = $val:expr, $($tail:tt)*) => {
117 $crate::_toast_accum!([$($string_vars)* (stringify!($var), $val.to_string()),], [$($pill_vars)*], $($tail)*)
118 };
119 // `var` syntax, with or without a trailing comma and other vars after.
120 ([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident $($tail:tt)*) => {
121 $crate::_toast_accum!([$($string_vars)* (stringify!($var), $var.to_string()),], [$($pill_vars)*] $($tail)*)
122 };
123 // `@var = val` syntax, without anything after.
124 ([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident = $val:expr) => {
125 $crate::_toast_accum!([$($string_vars)*], [$($pill_vars)*], @$var = $val,)
126 };
127 // `@var = val` syntax, with a trailing comma or other vars after.
128 ([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident = $val:expr, $($tail:tt)*) => {
129 {
130 use $crate::components::PillSourceExt;
131 // We do not need to watch safety settings for pills, rooms will be watched
132 // automatically.
133 let pill: $crate::components::Pill = $val.to_pill($crate::components::AvatarImageSafetySetting::None, None);
134 $crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),], $($tail)*)
135 }
136 };
137 // `@var` syntax, with or without a trailing comma and other vars after.
138 ([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident $($tail:tt)*) => {
139 {
140 use $crate::components::PillSourceExt;
141 // We do not need to watch safety settings for pills, rooms will be watched
142 // automatically.
143 let pill: $crate::components::Pill = $var.to_pill($crate::components::AvatarImageSafetySetting::None, None);
144 $crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),] $($tail)*)
145 }
146 };
147 // No more vars, with or without trailing comma.
148 ([$($string_vars:tt)*], [$($pill_vars:tt)*] $(,)?) => { ([$($string_vars)*], [$($pill_vars)*]) };
149}
150
151/// Add the given `AdwToast` to the ancestor of the given widget.
152///
153/// The widget must have one of these ancestors that can show toasts:
154///
155/// - `ToastableDialog`
156/// - `AdwPreferencesDialog`
157/// - `AdwPreferencesWindow`
158/// - `Window`
159pub(crate) fn add_toast(widget: >k::Widget, toast: adw::Toast) {
160 if let Some(dialog) = widget
161 .ancestor(ToastableDialog::static_type())
162 .and_downcast::<ToastableDialog>()
163 {
164 dialog.add_toast(toast);
165 } else if let Some(dialog) = widget
166 .ancestor(adw::PreferencesDialog::static_type())
167 .and_downcast::<adw::PreferencesDialog>()
168 {
169 dialog.add_toast(toast);
170 } else if let Some(root) = widget.root() {
171 // FIXME: AdwPreferencesWindow is deprecated but RoomDetails uses it.
172 #[allow(deprecated)]
173 if let Some(window) = root.downcast_ref::<adw::PreferencesWindow>() {
174 use adw::prelude::PreferencesWindowExt;
175 window.add_toast(toast);
176 } else if let Some(window) = root.downcast_ref::<Window>() {
177 window.add_toast(toast);
178 } else {
179 panic!("Trying to display a toast when the parent doesn't support it");
180 }
181 }
182}
183
184/// Add a toast with the given message and variables to the ancestor of the
185/// given widget.
186///
187/// The widget must have one of these ancestors that can show toasts:
188///
189/// - `ToastableDialog`
190/// - `AdwPreferencesDialog`
191/// - `AdwPreferencesWindow`
192/// - `Window`
193pub(crate) fn add_toast_with_vars(
194 widget: >k::Widget,
195 message: &str,
196 string_vars: &[(&str, String)],
197 pill_vars: &HashMap<&str, Pill>,
198) {
199 let string_dict: Vec<_> = string_vars
200 .iter()
201 .map(|(key, val)| (*key, val.as_ref()))
202 .collect();
203 let message = freplace(message, &string_dict);
204
205 let toast = if pill_vars.is_empty() {
206 adw::Toast::new(&message)
207 } else {
208 let mut swapped_label = String::new();
209 let mut widgets = Vec::with_capacity(pill_vars.len());
210 let mut last_end = 0;
211
212 // Find the locations of the pills in the message.
213 let mut matches = pill_vars
214 .keys()
215 .flat_map(|key| {
216 message
217 .match_indices(&format!("{{{key}}}"))
218 .map(|(start, _)| (start, *key))
219 .collect::<Vec<_>>()
220 })
221 .collect::<Vec<_>>();
222 // Sort the locations, so we can insert the pills in the right order.
223 matches.sort_unstable();
224
225 for (start, key) in matches {
226 swapped_label.push_str(&message[last_end..start]);
227 swapped_label.push_str(LabelWithWidgets::PLACEHOLDER);
228 last_end = start + key.len() + 2;
229 widgets.push(
230 pill_vars
231 .get(key)
232 .expect("match key should be in map")
233 .clone(),
234 );
235 }
236 swapped_label.push_str(&message[last_end..message.len()]);
237
238 let widget = LabelWithWidgets::new();
239 widget.set_valign(gtk::Align::Center);
240 widget.set_label_and_widgets(swapped_label, widgets);
241
242 adw::Toast::builder().custom_title(&widget).build()
243 };
244
245 add_toast(widget, toast);
246}