[fractal] utils: Add a macro to create toasts



commit 3cea24d36d4d2af1fbb8492b7aad13359d8d3f93
Author: Kévin Commaille <zecakeh tedomum fr>
Date:   Tue May 31 13:43:55 2022 +0200

    utils: Add a macro to create toasts

 src/components/label_with_widgets.rs |   2 +-
 src/components/mod.rs                |   2 +-
 src/components/toast.rs              |   6 +-
 src/i18n.rs                          |  10 +--
 src/session/room/mod.rs              |  25 +++---
 src/session/user.rs                  |   7 ++
 src/utils.rs                         | 158 +++++++++++++++++++++++++++++++++++
 7 files changed, 186 insertions(+), 24 deletions(-)
---
diff --git a/src/components/label_with_widgets.rs b/src/components/label_with_widgets.rs
index 737de3cd1..ae3dea646 100644
--- a/src/components/label_with_widgets.rs
+++ b/src/components/label_with_widgets.rs
@@ -2,7 +2,7 @@ use std::cmp::max;
 
 use gtk::{glib, glib::clone, pango, prelude::*, subclass::prelude::*};
 
-const DEFAULT_PLACEHOLDER: &str = "<widget>";
+pub const DEFAULT_PLACEHOLDER: &str = "<widget>";
 const PANGO_SCALE: i32 = 1024;
 const OBJECT_REPLACEMENT_CHARACTER: &str = "\u{FFFC}";
 fn pango_pixels(d: i32) -> i32 {
diff --git a/src/components/mod.rs b/src/components/mod.rs
index 4c151b617..ffa03ef06 100644
--- a/src/components/mod.rs
+++ b/src/components/mod.rs
@@ -36,7 +36,7 @@ pub use self::{
     editable_avatar::EditableAvatar,
     entry_row::EntryRow,
     in_app_notification::InAppNotification,
-    label_with_widgets::LabelWithWidgets,
+    label_with_widgets::{LabelWithWidgets, DEFAULT_PLACEHOLDER},
     loading_listbox_row::LoadingListBoxRow,
     location_viewer::LocationViewer,
     media_content_viewer::{ContentType, MediaContentViewer},
diff --git a/src/components/toast.rs b/src/components/toast.rs
index 6581445a2..e7b2c6223 100644
--- a/src/components/toast.rs
+++ b/src/components/toast.rs
@@ -131,12 +131,12 @@ impl ToastBuilder {
         Self::default()
     }
 
-    pub fn title(mut self, title: &str) -> Self {
-        self.title = Some(title.to_owned());
+    pub fn title(mut self, title: String) -> Self {
+        self.title = Some(title);
         self
     }
 
-    pub fn widgets(mut self, widgets: &[&impl IsA<gtk::Widget>]) -> Self {
+    pub fn widgets(mut self, widgets: &[impl IsA<gtk::Widget>]) -> Self {
         self.widgets = Some(widgets.iter().map(|w| w.upcast_ref().clone()).collect());
         self
     }
diff --git a/src/i18n.rs b/src/i18n.rs
index ea704f949..2daef5f01 100644
--- a/src/i18n.rs
+++ b/src/i18n.rs
@@ -1,14 +1,6 @@
 use gettextrs::{gettext, ngettext};
 
-fn freplace(s: String, args: &[(&str, &str)]) -> String {
-    let mut s = s;
-
-    for (k, v) in args {
-        s = s.replace(&format!("{{{}}}", k), v);
-    }
-
-    s
-}
+use crate::utils::freplace;
 
 /// Like `gettext`, but replaces named variables with the given dictionary.
 ///
diff --git a/src/session/room/mod.rs b/src/session/room/mod.rs
index 412af38df..0b1b507eb 100644
--- a/src/session/room/mod.rs
+++ b/src/session/room/mod.rs
@@ -463,8 +463,8 @@ impl Room {
                             let room_pill = Pill::for_room(&obj);
                             let error = Toast::builder()
                                 // Translators: Do NOT translate the content between '{' and '}', this is a 
variable name.
-                                .title(&gettext_f("Failed to forget {room}.", &[("room", "<widget>")]))
-                                .widgets(&[&room_pill])
+                                .title(gettext_f("Failed to forget {room}.", &[("room", "<widget>")]))
+                                .widgets(&[room_pill])
                                 .build();
 
                             if let Some(window) = obj.session().parent_window() {
@@ -703,12 +703,12 @@ impl Room {
 
                                 let room_pill = Pill::for_room(&obj);
                                 let error = Toast::builder()
-                                    .title(&gettext_f(
+                                    .title(gettext_f(
                                         // Translators: Do NOT translate the content between '{' and '}', 
this is a variable name.
                                         "Failed to move {room} from {previous_category} to {new_category}.",
                                         &[("room", "<widget>"),("previous_category", 
&previous_category.to_string()), ("new_category", &category.to_string())],
                                     ))
-                                    .widgets(&[&room_pill])
+                                    .widgets(&[room_pill])
                                     .build();
 
                                 if let Some(window) = obj.session().parent_window() {
@@ -1438,13 +1438,13 @@ impl Room {
 
                     let room_pill = Pill::for_room(self);
                     let error = Toast::builder()
-                        .title(&gettext_f(
+                        .title(gettext_f(
                             // Translators: Do NOT translate the content between '{' and '}', this
                             // is a variable name.
                             "Failed to accept invitation for {room}. Try again later.",
                             &[("room", "<widget>")],
                         ))
-                        .widgets(&[&room_pill])
+                        .widgets(&[room_pill])
                         .build();
 
                     if let Some(window) = self.session().parent_window() {
@@ -1472,13 +1472,13 @@ impl Room {
 
                     let room_pill = Pill::for_room(self);
                     let error = Toast::builder()
-                        .title(&gettext_f(
+                        .title(gettext_f(
                             // Translators: Do NOT translate the content between '{' and '}', this
                             // is a variable name.
                             "Failed to reject invitation for {room}. Try again later.",
                             &[("room", "<widget>")],
                         ))
-                        .widgets(&[&room_pill])
+                        .widgets(&[room_pill])
                         .build();
 
                     if let Some(window) = self.session().parent_window() {
@@ -1662,8 +1662,8 @@ impl Room {
                 let user_pill = Pill::for_user(first_failed);
                 let room_pill = Pill::for_room(self);
                 let error = Toast::builder()
-                    .title(&error_message)
-                    .widgets(&[&user_pill, &room_pill])
+                    .title(error_message)
+                    .widgets(&[user_pill, room_pill])
                     .build();
 
                 if let Some(window) = self.session().parent_window() {
@@ -1769,6 +1769,11 @@ impl Room {
             None
         })
     }
+
+    /// Get a `Pill` representing this `Room`.
+    pub fn to_pill(&self) -> Pill {
+        Pill::for_room(self)
+    }
 }
 
 /// Whether the given event can count as an unread message.
diff --git a/src/session/user.rs b/src/session/user.rs
index b94eb6436..0c62129f8 100644
--- a/src/session/user.rs
+++ b/src/session/user.rs
@@ -6,6 +6,7 @@ use matrix_sdk::{
 };
 
 use crate::{
+    components::Pill,
     session::{
         verification::{IdentityVerification, VerificationState},
         Avatar, Session,
@@ -265,6 +266,12 @@ pub trait UserExt: IsA<User> {
             UserActions::NONE
         }
     }
+
+    /// Get a `Pill` representing this `User`.
+    fn to_pill(&self) -> Pill {
+        let user = self.upcast_ref();
+        Pill::for_user(user)
+    }
 }
 
 impl<T: IsA<User>> UserExt for T {}
diff --git a/src/utils.rs b/src/utils.rs
index b5505b5ba..745aa3ad2 100644
--- a/src/utils.rs
+++ b/src/utils.rs
@@ -26,6 +26,150 @@ macro_rules! spawn_tokio {
     };
 }
 
+/// Show a toast with the given message on the ancestor window of `widget`.
+///
+/// The simplest way to use this macros is for displaying a simple message. It
+/// can be anything that implements `AsRef<str>`.
+///
+/// ```ignore
+/// toast!(widget, gettext("Something happened"));
+/// ```
+///
+/// This macro also supports replacing named variables with their value. It
+/// supports both the `var` and the `var = expr` syntax. In this case the
+/// message and the variables must be `String`s.
+///
+/// ```ignore
+/// toast!(
+///     widget,
+///     gettext("Error number {n}: {msg}"),
+///     n = error_nb.to_string(),
+///     msg,
+/// );
+/// ```
+///
+/// To add `Pill`s to the toast, you can precede a [`Room`] or [`User`] with
+/// `@`.
+///
+/// ```ignore
+/// let room = Room::new(session, room_id);
+/// let member = Member::new(room, user_id);
+///
+/// toast!(
+///     widget,
+///     gettext("Could not contact {user} in {room}",
+///     @user = member,
+///     @room,
+/// );
+/// ```
+///
+/// For this macro to work, the ancestor window be a [`Window`](crate::Window)
+/// or an [`adw::PreferencesWindow`].
+///
+/// [`Room`]: crate::session::room::Room
+/// [`User`]: crate::session::user::User
+#[macro_export]
+macro_rules! toast {
+    ($widget:expr, $message:expr) => {
+        {
+            let message = $message;
+            if let Some(root) = $widget.root() {
+                if let Some(window) = root.downcast_ref::<$crate::Window>() {
+                    window.add_toast(&$crate::components::Toast::new(message.as_ref()));
+                } else if let Some(window) = root.downcast_ref::<adw::PreferencesWindow>() {
+                    use adw::prelude::PreferencesWindowExt;
+                    window.add_toast(&adw::Toast::new(message.as_ref()));
+                } else {
+                    log::error!("Trying to display a toast when the parent doesn't support it");
+                }
+            } else {
+                log::warn!("Could not display toast with message: {message}");
+            }
+        }
+    };
+    ($widget:expr, $message:expr, $($tail:tt)+) => {
+        {
+            let (string_vars, pill_vars) = $crate::_toast_accum!([], [], $($tail)+);
+            let string_dict: Vec<_> = string_vars
+                .iter()
+                .map(|(key, val): &(&str, String)| (key.as_ref(), val.as_ref()))
+                .collect();
+            let message = $crate::utils::freplace($message.into(), &*string_dict);
+
+            if let Some(root) = $widget.root() {
+                if pill_vars.is_empty() {
+                    if let Some(window) = root.downcast_ref::<$crate::Window>() {
+                        window.add_toast(&$crate::components::Toast::new(&message));
+                    } else if let Some(window) = root.downcast_ref::<adw::PreferencesWindow>() {
+                        use adw::prelude::PreferencesWindowExt;
+                        window.add_toast(&adw::Toast::new(&message));
+                    } else {
+                        log::error!("Trying to display a toast when the parent doesn't support it");
+                    }
+                } else if let Some(window) = root.downcast_ref::<$crate::Window>() {
+                    let pill_vars = std::collections::HashMap::<&str, 
$crate::components::Pill>::from(pill_vars);
+                    let mut swapped_label = String::new();
+                    let mut widgets = Vec::with_capacity(pill_vars.len());
+                    let mut last_end = 0;
+
+                    let mut matches = pill_vars
+                        .keys()
+                        .map(|key: &&str| {
+                            message
+                                .match_indices(&format!("{{{key}}}"))
+                                .map(|(start, _)| (start, key))
+                                .collect::<Vec<_>>()
+                        })
+                        .flatten()
+                        .collect::<Vec<_>>();
+                    matches.sort_unstable();
+
+                    for (start, key) in matches {
+                        swapped_label.push_str(&message[last_end..start]);
+                        swapped_label.push_str($crate::components::DEFAULT_PLACEHOLDER);
+                        last_end = start + key.len() + 2;
+                        widgets.push(pill_vars.get(key).unwrap().clone())
+                    }
+                    swapped_label.push_str(&message[last_end..message.len()]);
+
+                    let toast = $crate::components::Toast::builder()
+                        .title(swapped_label)
+                        .widgets(&widgets)
+                        .build();
+                    window.add_toast(&toast);
+                } else {
+                    log::error!("Trying to display a toast with pills when the parent doesn't support it");
+                }
+            } else {
+                log::warn!("Could not display toast with message: {message}");
+            }
+        }
+    };
+}
+#[doc(hidden)]
+#[macro_export]
+macro_rules! _toast_accum {
+    ([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident, $($tail:tt)*) => {
+        $crate::_toast_accum!([$($string_vars)* (stringify!($var), $var),], [$($pill_vars)*], $($tail)*)
+    };
+    ([$($string_vars:tt)*], [$($pill_vars:tt)*], $var:ident = $val:expr, $($tail:tt)*) => {
+        $crate::_toast_accum!([$($string_vars)* (stringify!($var), $val),], [$($pill_vars)*], $($tail)*)
+    };
+    ([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident, $($tail:tt)*) => {
+        {
+            let pill: $crate::components::Pill = $var.to_pill();
+            $crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),], $($tail)*)
+        }
+    };
+    ([$($string_vars:tt)*], [$($pill_vars:tt)*], @$var:ident = $val:expr, $($tail:tt)*) => {
+        {
+            let pill: $crate::components::Pill = $val.to_pill();
+            $crate::_toast_accum!([$($string_vars)*], [$($pill_vars)* (stringify!($var), pill),], $($tail)*)
+        }
+    };
+    ([$($string_vars:tt)*], [$($pill_vars:tt)*],) => { ([$($string_vars)*], [$($pill_vars)*]) };
+}
+
 use std::{convert::TryInto, path::PathBuf, str::FromStr};
 
 use gettextrs::gettext;
@@ -284,3 +428,17 @@ pub fn validate_password(password: &str) -> PasswordValidity {
 
     validity
 }
+
+/// Replace variables in the given string with the given dictionary.
+///
+/// The expected format to replace is `{name}`, where `name` is the first string
+/// in the dictionary entry tuple.
+pub fn freplace(s: String, args: &[(&str, &str)]) -> String {
+    let mut s = s;
+
+    for (k, v) in args {
+        s = s.replace(&format!("{{{}}}", k), v);
+    }
+
+    s
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]