diff --git a/demo/adw-demo-window.c b/demo/adw-demo-window.c
index 1337d26cd70b94e3571800402ecbf673e8136123..e1542a3f8f0aabd58205bc287ad44d5cc9f030b6 100644
--- a/demo/adw-demo-window.c
+++ b/demo/adw-demo-window.c
@@ -11,6 +11,7 @@ struct _AdwDemoWindow
AdwApplicationWindow parent_instance;
AdwLeaflet *content_box;
+ AdwToastOverlay *toast_overlay;
GtkBox *right_box;
GtkWidget *color_scheme_button;
GtkStackSidebar *sidebar;
@@ -26,6 +27,8 @@ struct _AdwDemoWindow
GtkButton *avatar_remove_button;
GtkFileChooserNative *avatar_file_chooser;
GtkListBox *avatar_contacts;
+ int toast_undo_items;
+ AdwToast *undo_toast;
};
G_DEFINE_TYPE (AdwDemoWindow, adw_demo_window, ADW_TYPE_APPLICATION_WINDOW)
@@ -398,6 +401,87 @@ style_classes_demo_clicked_cb (GtkButton *btn,
gtk_window_present (GTK_WINDOW (window));
}
+static void
+add_toast_cb (AdwDemoWindow *self)
+{
+ adw_toast_overlay_add_toast (self->toast_overlay,
+ adw_toast_new (_("Simple toast")));
+}
+
+static void
+dismissed_cb (AdwDemoWindow *self)
+{
+ self->undo_toast = NULL;
+ self->toast_undo_items = 0;
+
+ gtk_widget_action_set_enabled (GTK_WIDGET (self), "toast.dismiss", FALSE);
+}
+
+static void
+add_toast_with_button_cb (AdwDemoWindow *self)
+{
+ g_autofree char *title = NULL;
+
+ self->toast_undo_items++;
+
+ if (!self->undo_toast) {
+ title = g_strdup_printf (_("‘%s’ deleted"), "Lorem ipsum");
+
+ self->undo_toast = adw_toast_new (title);
+
+ adw_toast_set_priority (self->undo_toast, ADW_TOAST_PRIORITY_HIGH);
+ adw_toast_set_button_label (self->undo_toast, _("Undo"));
+ adw_toast_set_action_name (self->undo_toast, "toast.undo");
+
+ g_signal_connect_swapped (self->undo_toast, "dismissed", G_CALLBACK (dismissed_cb), self);
+
+ adw_toast_overlay_add_toast (self->toast_overlay, self->undo_toast);
+
+ gtk_widget_action_set_enabled (GTK_WIDGET (self), "toast.dismiss", TRUE);
+
+ return;
+ }
+
+ title =
+ g_strdup_printf (ngettext ("%d item deleted",
+ "%d items deleted",
+ self->toast_undo_items), self->toast_undo_items);
+
+ adw_toast_set_title (self->undo_toast, title);
+}
+
+static void
+add_toast_with_long_title_cb (AdwDemoWindow *self)
+{
+ adw_toast_overlay_add_toast (self->toast_overlay,
+ adw_toast_new (_("Lorem ipsum dolor sit amet, "
+ "consectetur adipiscing elit, "
+ "sed do eiusmod tempor incididunt "
+ "ut labore et dolore magnam aliquam "
+ "quaerat voluptatem.")));
+}
+
+static void
+toast_undo_cb (AdwDemoWindow *self)
+{
+ g_autofree char *title =
+ g_strdup_printf (ngettext ("Undoing deleting %d item…",
+ "Undoing deleting %d items…",
+ self->toast_undo_items), self->toast_undo_items);
+ AdwToast *toast = adw_toast_new (title);
+
+ adw_toast_set_priority (toast, ADW_TOAST_PRIORITY_HIGH);
+
+ adw_toast_overlay_add_toast (self->toast_overlay, toast);
+}
+
+static void
+toast_dismiss_cb (AdwDemoWindow *self)
+{
+ if (self->undo_toast)
+ adw_toast_dismiss (self->undo_toast);
+}
+
static void
adw_demo_window_class_init (AdwDemoWindowClass *klass)
{
@@ -405,6 +489,9 @@ adw_demo_window_class_init (AdwDemoWindowClass *klass)
gtk_widget_class_add_binding_action (widget_class, GDK_KEY_q, GDK_CONTROL_MASK, "window.close", NULL);
+ gtk_widget_class_install_action (widget_class, "toast.undo", NULL, (GtkWidgetActionActivateFunc) toast_undo_cb);
+ gtk_widget_class_install_action (widget_class, "toast.dismiss", NULL, (GtkWidgetActionActivateFunc) toast_dismiss_cb);
+
gtk_widget_class_set_template_from_resource (widget_class, "/org/gnome/Adwaita1/Demo/ui/adw-demo-window.ui");
gtk_widget_class_bind_template_child (widget_class, AdwDemoWindow, content_box);
gtk_widget_class_bind_template_child (widget_class, AdwDemoWindow, right_box);
@@ -421,6 +508,7 @@ adw_demo_window_class_init (AdwDemoWindowClass *klass)
gtk_widget_class_bind_template_child (widget_class, AdwDemoWindow, avatar_file_chooser_label);
gtk_widget_class_bind_template_child (widget_class, AdwDemoWindow, avatar_remove_button);
gtk_widget_class_bind_template_child (widget_class, AdwDemoWindow, avatar_contacts);
+ gtk_widget_class_bind_template_child (widget_class, AdwDemoWindow, toast_overlay);
gtk_widget_class_bind_template_callback (widget_class, notify_visible_child_cb);
gtk_widget_class_bind_template_callback (widget_class, back_clicked_cb);
gtk_widget_class_bind_template_callback (widget_class, leaflet_back_clicked_cb);
@@ -441,6 +529,9 @@ adw_demo_window_class_init (AdwDemoWindowClass *klass)
gtk_widget_class_bind_template_callback (widget_class, flap_demo_clicked_cb);
gtk_widget_class_bind_template_callback (widget_class, tab_view_demo_clicked_cb);
gtk_widget_class_bind_template_callback (widget_class, style_classes_demo_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, add_toast_cb);
+ gtk_widget_class_bind_template_callback (widget_class, add_toast_with_button_cb);
+ gtk_widget_class_bind_template_callback (widget_class, add_toast_with_long_title_cb);
}
static void
@@ -485,4 +576,6 @@ adw_demo_window_init (AdwDemoWindow *self)
avatar_page_init (self);
adw_leaflet_set_visible_child (self->content_box, GTK_WIDGET (self->right_box));
+
+ gtk_widget_action_set_enabled (GTK_WIDGET (self), "toast.dismiss", FALSE);
}
diff --git a/demo/adw-demo-window.ui b/demo/adw-demo-window.ui
index ebd0b867d8bfe0ec979630e242e3321b22af1411..aba5a73e50352109ce1efa15ca5319dbb748cb5b 100644
--- a/demo/adw-demo-window.ui
+++ b/demo/adw-demo-window.ui
@@ -39,944 +39,1017 @@
800
576
-
+
+
+
+ lists
+ Lists
+
+
+ widget-list-symbolic
+ Lists
+ Rows and helpers for GtkListBox.
+
+
+ 400
+ 300
+
+
+ vertical
+ 12
-
- Combo row
-
-
-
- - Foo
- - Bar
- - Baz
-
+
+
+
+ row-preferences-symbolic
+ They also have a subtitle and an icon
+ Rows have a title
-
-
-
-
-
- This combo row was created from an enumeration
- Enumeration combo row
-
-
- GtkLicense
+
+
+
+ frobnicate
+ Rows can have suffix widgets
+
+
+ end
+ Frobnicate
+ center
+
+
+
-
-
-
-
+
-
-
-
-
- Expander Rows
-
- Expander row
+
- A nested row
+ radio_button_1
+ Rows can have prefix widgets
+
+
+ center
+ True
+
+
- Another nested row
+ radio_button_2
+ Rows can have prefix widgets
+
+
+ radio_button_1
+ center
+
+
-
- Expander row with an action
-
-
- center
- row-copy-symbolic
-
-
-
+
+ Combo Rows
-
- A nested row
+
+ Combo row
+
+
+
+ - Foo
+ - Bar
+ - Baz
+
+
+
-
- Another nested row
+
+ This combo row was created from an enumeration
+ Enumeration combo row
+
+
+ GtkLicense
+
+
+
+
+
-
- True
- Toggleable expander row
+
+ Expander Rows
-
- A nested row
+
+ Expander row
+
+
+ A nested row
+
+
+
+
+ Another nested row
+
+
-
- Another nested row
+
+ Expander row with an action
+
+
+ center
+ row-copy-symbolic
+
+
+
+
+
+ A nested row
+
+
+
+
+ Another nested row
+
+
+
+
+
+
+ True
+ Toggleable expander row
+
+
+ A nested row
+
+
+
+
+ Another nested row
+
+
-
+
-
-
-
-
-
- view-switcher
- View Switcher
-
-
- widget-view-switcher-symbolic
+
+
+
+ view-switcher
View Switcher
- Widgets to switch the window's view.
-
-
- Run the demo
- center
-
-
+
+
+ widget-view-switcher-symbolic
+ View Switcher
+ Widgets to switch the window's view.
+
+
+ Run the demo
+ center
+
+
+
+
-
+
-
-
-
-
-
- carousel
- Carousel
-
-
- vertical
-
-
-
-
-
- True
- True
-
-
+
+
+
+ carousel
+ Carousel
+
+
+ vertical
-
- widget-carousel-symbolic
- Carousel
- A widget for paginated scrolling.
- True
-
+
-
- 32
- 12
- 12
- 400
- 300
- center
+
+ True
+ True
+
+
-
+
+ widget-carousel-symbolic
+ Carousel
+ A widget for paginated scrolling.
+ True
+
+
+
+
+ 32
+ 12
+ 12
+ 400
+ 300
+ center
-
- Orientation
-
-
-
- GtkOrientation
+
+
+
+ Orientation
+
+
+
+ GtkOrientation
+
+
+
+
+
-
-
-
-
-
-
-
-
- Page Indicators
-
-
-
-
- - dots
- - lines
-
+
+
+
+ Page Indicators
+
+
+
+
+ - dots
+ - lines
+
+
+
+
+
+
-
-
-
-
-
-
-
-
- Scroll wheel
- carousel_scroll_wheel
+
-
- center
- True
+
+ Scroll wheel
+ carousel_scroll_wheel
+
+
+ center
+ True
+
+
-
-
-
-
- Long swipes
- carousel_long_swipes
-
- center
+
+ Long swipes
+ carousel_long_swipes
+
+
+ center
+
+
-
-
-
-
- Another page
- True
-
-
- _Return to the first page
- True
- center
-
-
+
+
+ Another page
+ True
+
+
+ _Return to the first page
+ True
+ center
+
+
+
+
-
+
-
-
-
-
- False
- 6
- 6
- 6
- 6
-
- dots
-
-
- carousel
-
+
+ False
+ 6
+ 6
+ 6
+ 6
+
+
+ dots
+
+
+ carousel
+
+
+
-
-
-
-
-
- lines
-
-
- carousel
-
+
+
+
+ lines
+
+
+ carousel
+
+
+
-
+
-
+
-
-
-
-
-
- avatar
- Avatar
-
-
- never
+
+
+
+ avatar
+ Avatar
-
- True
+
+ never
-
- vertical
- start
-
-
+
+ True
+
vertical
+ start
+
-
- center
- center
-
-
-
-
-
-
-
- Avatar
- center
- 0
- True
- word-char
- center
-
-
-
-
-
- A user avatar with generated fallback.
- center
- true
- True
-
-
-
-
-
-
-
- 400
- 300
-
- center
vertical
- 12
-
+
+ center
+ center
+
+
+
+
+
+
+
+ Avatar
+ center
+ 0
+ True
+ word-char
+ center
+
+
+
+
+
+ A user avatar with generated fallback.
+ center
+ true
+ True
+
+
+
+
+
+
+
+ 400
+ 300
+
+
+ center
+ vertical
+ 12
-
- Text
+
-
- center
+
+ Text
+
+
+ center
+
+
-
-
-
-
- Show initials
- avatar_show_initials
-
- center
- True
+
+ Show initials
+ avatar_show_initials
+
+
+ center
+ True
+
+
-
-
-
-
- File
-
- center
-
+
+ File
-
- middle
- 20
+
+ center
+
+
+
+ middle
+ 20
+
+
+
+
+
+
+
+ center
+ avatar-delete-symbolic
+
+
-
-
- center
- avatar-delete-symbolic
-
-
+
+ Size
+
+
+ center
+ True
+ avatar_adjustment
+
+
-
-
-
-
- Size
-
- center
- True
- avatar_adjustment
+
+ Export to file
+
+
+ center
+ avatar-save-symbolic
+
+
+
+
-
- Export to file
-
-
- center
- avatar-save-symbolic
-
-
-
-
+
+ none
+
-
-
-
- none
-
-
-
+
-
+
-
+
-
-
-
-
-
- flap
- Flap
-
-
- widget-flap-symbolic
+
+
+
+ flap
Flap
- A widget showing a flap next to or above the content.
-
- Run the demo
- center
-
-
+
+ widget-flap-symbolic
+ Flap
+ A widget showing a flap next to or above the content.
+
+
+ Run the demo
+ center
+
+
+
+
-
-
-
-
-
- tab-view
- Tab View
-
-
- widget-tab-view-symbolic
+
+
+
+ tab-view
Tab View
- A modern tab widget.
-
- Run the demo
- center
-
-
+
+ widget-tab-view-symbolic
+ Tab View
+ A modern tab widget.
+
+
+ Run the demo
+ center
+
+
+
+
-
-
-
-
-
- buttons
- Buttons
-
-
+
+
+
+ buttons
Buttons
- Button helper widgets.
-
- 400
- 300
+
+ Buttons
+ Button helper widgets.
-
- vertical
-
-
- center
- 12
- 12
-
-
- document-open-symbolic
- sample_menu
-
- 0
- 0
-
-
-
-
-
- document-open-symbolic
- sample_menu
-
-
- 0
- 1
-
-
-
-
-
- _Open
- True
- sample_menu
-
- 1
- 0
-
-
-
-
-
- _Open
- True
- sample_menu
-
-
- 1
- 1
-
-
-
+
+ 400
+ 300
+
+
+ vertical
-
-
-
+
+ center
+ 12
+ 12
+
+
document-open-symbolic
+ sample_menu
+
+ 0
+ 0
+
+
+
+
+
+ document-open-symbolic
+ sample_menu
+
+
+ 0
+ 1
+
+
+
+
+
_Open
True
+ sample_menu
+
+ 1
+ 0
+
-
- sample_menu
-
- 2
- 0
-
-
-
-
-
-
-
- document-open-symbolic
+
+
+
_Open
True
+ sample_menu
+
+
+ 1
+ 1
+
-
- sample_menu
-
-
- 2
- 1
-
+
+
+
+
+
+ document-open-symbolic
+ _Open
+ True
+
+
+ sample_menu
+
+ 2
+ 0
+
+
+
+
+
+
+
+ document-open-symbolic
+ _Open
+ True
+
+
+ sample_menu
+
+
+ 2
+ 1
+
+
+
-
+
-
-
-
-
-
- style-classes
- Style Classes
-
-
- style-classes-symbolic
+
+
+
+ style-classes
Style Classes
- Various widget styles available for use.
-
- Run the demo
- center
-
-
+
+ style-classes-symbolic
+ Style Classes
+ Various widget styles available for use.
+
+
+ Run the demo
+ center
+
+
+
+
-
+
+
+
+ toast
+ Toast
+
+
+ preferences-system-notifications-symbolic
+ Toast
+ Transient in-app notifications.
+
+
+ 400
+ 300
+
+
+
+
+ Simple toast
+
+
+ center
+ Show
+
+
+
+
+
+
+
+ Toast with an action
+
+
+ center
+ user-trash-symbolic
+ toast.dismiss
+
+
+
+
+
+ center
+ Show
+
+
+
+
+
+
+
+ Toast with a long title
+
+
+ center
+ Show
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
- vertical
-
-
-
-
- True
- Go back
-
-
- vertical
- center
- 12
-
-
- gesture-touchscreen-swipe-back-symbolic
- 128
-
+
+ vertical
+
+
-
+
-
+
diff --git a/src/adw-enums.c.in b/src/adw-enums.c.in
index b151b1edb56b623f4366970180681024df1f98ca..c36ff6892245f07eaa89047844ddd978a2dcf8b6 100644
--- a/src/adw-enums.c.in
+++ b/src/adw-enums.c.in
@@ -9,6 +9,7 @@
#include "adw-navigation-direction.h"
#include "adw-squeezer.h"
#include "adw-style-manager.h"
+#include "adw-toast.h"
#include "adw-view-switcher.h"
/*** END file-header ***/
diff --git a/src/adw-toast-overlay.c b/src/adw-toast-overlay.c
new file mode 100644
index 0000000000000000000000000000000000000000..9606d69d894777923f04d2253f7fac6ded53725b
--- /dev/null
+++ b/src/adw-toast-overlay.c
@@ -0,0 +1,599 @@
+/*
+ * Copyright (C) 2021 Maximiliano Sandoval
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "adw-toast-overlay.h"
+
+#include "adw-animation-private.h"
+#include "adw-animation-util-private.h"
+#include "adw-toast-private.h"
+#include "adw-toast-widget-private.h"
+#include "adw-widget-utils-private.h"
+
+#define SHOW_DURATION 300
+#define HIDE_DURATION 300
+#define REPLACE_DURATION 500
+#define FINAL_SIZE 0.7
+#define NATURAL_WIDTH 450
+
+/**
+ * AdwToastOverlay:
+ *
+ * A widget showing toasts above its content.
+ *
+ * Toasts can be shown with [method@Adw.ToastOverlay.add_toast].
+ *
+ * See [class@Adw.Toast] for details.
+ *
+ * ## CSS nodes
+ *
+ * ```
+ * toastoverlay
+ * ├── [child]
+ * ├── toast
+ * ┊ ├── label.heading
+ * ├── [button]
+ * ╰── button.circular.flat
+ * ```
+ *
+ * `AdwToastOverlay`'s CSS node is called `toastoverlay`. It contains the child,
+ * as well as zero or more `toast` subnodes.
+ *
+ * Each of the `toast` nodes contains a `label` subnode with the `.heading`
+ * style class, optionally a `button` subnode, and another `button` subnode with
+ * `.circular` and `.flat` style classes.
+ *
+ * ## Accessibility
+ *
+ * `AdwToastOverlay` uses the `GTK_ACCESSIBLE_ROLE_TAB_GROUP` role.
+ *
+ * Since: 1.0
+ */
+
+typedef struct {
+ AdwToastOverlay *overlay;
+
+ AdwToast *toast;
+ GtkWidget *widget;
+
+ AdwAnimation *show_animation;
+ AdwAnimation *hide_animation;
+
+ gulong dismissed_id;
+ gboolean postponing;
+} ToastInfo;
+
+struct _AdwToastOverlay {
+ GtkWidget parent_instance;
+
+ GtkWidget *child;
+
+ GQueue *queue;
+ ToastInfo *current_toast;
+ GList *hiding_toasts;
+};
+
+enum {
+ PROP_0,
+ PROP_CHILD,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void adw_toast_overlay_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (AdwToastOverlay, adw_toast_overlay, GTK_TYPE_WIDGET,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
+ adw_toast_overlay_buildable_init))
+
+static GtkBuildableIface *parent_buildable_iface;
+
+static void
+free_toast_info (ToastInfo *info)
+{
+ if (info->dismissed_id && info->toast)
+ g_signal_handler_disconnect (info->toast, info->dismissed_id);
+
+ g_clear_object (&info->show_animation);
+ g_clear_object (&info->hide_animation);
+ g_clear_pointer (&info->widget, gtk_widget_unparent);
+ g_clear_object (&info->toast);
+
+ g_free (info);
+}
+
+static void
+dismiss_and_free_toast_info (ToastInfo *info)
+{
+ g_signal_handler_disconnect (info->toast, info->dismissed_id);
+ info->dismissed_id = 0;
+
+ adw_toast_dismiss (info->toast);
+
+ free_toast_info (info);
+}
+
+static void
+hide_value_cb (double value,
+ ToastInfo *info)
+{
+ gtk_widget_set_opacity (info->widget, adw_ease_out_cubic (value));
+
+ gtk_widget_queue_allocate (GTK_WIDGET (info->overlay));
+}
+
+static void
+hide_done_cb (ToastInfo *info)
+{
+ AdwToastOverlay *self = info->overlay;
+
+ self->hiding_toasts = g_list_remove (self->hiding_toasts, info);
+
+ /* We don't want to free the toast just yet, just remove the widget as we'll
+ * make a new one later */
+ if (info->postponing && info->dismissed_id) {
+ g_clear_object (&info->show_animation);
+ g_clear_object (&info->hide_animation);
+ g_clear_pointer (&info->widget, gtk_widget_unparent);
+ info->postponing = FALSE;
+
+ return;
+ }
+
+ free_toast_info (info);
+}
+
+static void
+show_value_cb (double value,
+ ToastInfo *info)
+{
+ gtk_widget_queue_allocate (GTK_WIDGET (info->overlay));
+}
+
+static void
+show_done_cb (ToastInfo *info)
+{
+ g_clear_object (&info->show_animation);
+}
+
+static void show_toast (AdwToastOverlay *self,
+ ToastInfo *info);
+
+static void
+hide_current_toast (AdwToastOverlay *self)
+{
+ ToastInfo *info = self->current_toast;
+
+ self->hiding_toasts = g_list_append (self->hiding_toasts, info);
+ self->current_toast = NULL;
+
+ gtk_widget_set_can_target (GTK_WIDGET (info->widget), FALSE);
+ gtk_widget_set_can_focus (GTK_WIDGET (info->widget), FALSE);
+
+ info->hide_animation =
+ adw_animation_new (GTK_WIDGET (self), 1, 0, HIDE_DURATION,
+ (AdwAnimationTargetFunc) hide_value_cb, info);
+
+ g_signal_connect_swapped (info->hide_animation, "done",
+ G_CALLBACK (hide_done_cb), info);
+
+ adw_animation_start (info->hide_animation);
+}
+
+static void
+dismissed_cb (ToastInfo *info)
+{
+ AdwToastOverlay *self = info->overlay;
+
+ if (info->hide_animation && !info->postponing)
+ return;
+
+ /* Protect against repeat emissions */
+ if (info->dismissed_id) {
+ g_signal_handler_disconnect (info->toast, info->dismissed_id);
+ info->dismissed_id = 0;
+ }
+
+ if (info == self->current_toast) {
+ ToastInfo *next_toast;
+
+ hide_current_toast (self);
+
+ next_toast = g_queue_pop_head (self->queue);
+
+ if (next_toast)
+ show_toast (self, next_toast);
+ } else {
+ g_queue_remove (self->queue, info);
+
+ if (!info->hide_animation)
+ free_toast_info (info);
+ }
+}
+
+static void
+show_toast (AdwToastOverlay *self,
+ ToastInfo *info)
+{
+ g_assert (!info->widget);
+
+ self->current_toast = info;
+
+ info->widget = adw_toast_widget_new (info->toast);
+ gtk_widget_insert_before (info->widget, GTK_WIDGET (self), NULL);
+
+ info->show_animation =
+ adw_animation_new (GTK_WIDGET (self), 0, 1,
+ self->hiding_toasts ? REPLACE_DURATION : SHOW_DURATION,
+ (AdwAnimationTargetFunc) show_value_cb, info);
+
+ g_signal_connect_swapped (info->show_animation, "done",
+ G_CALLBACK (show_done_cb), info);
+
+ adw_animation_start (info->show_animation);
+}
+
+static gboolean
+dismiss_cb (AdwToastOverlay *self,
+ GVariant *args)
+{
+ if (self->current_toast) {
+ adw_toast_dismiss (self->current_toast->toast);
+
+ return GDK_EVENT_STOP;
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+adw_toast_overlay_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ GtkWidget *child;
+
+ for (child = gtk_widget_get_first_child (widget);
+ child;
+ child = gtk_widget_get_next_sibling (child)) {
+ int child_min = 0;
+ int child_nat = 0;
+ int child_min_baseline = -1;
+ int child_nat_baseline = -1;
+
+ if (!gtk_widget_should_layout (child))
+ continue;
+
+ gtk_widget_measure (child, orientation, for_size,
+ &child_min, &child_nat,
+ &child_min_baseline, &child_nat_baseline);
+
+ *minimum = MAX (*minimum, child_min);
+ *natural = MAX (*natural, child_nat);
+
+ if (child_min_baseline > -1)
+ *minimum_baseline = MAX (*minimum_baseline, child_min_baseline);
+ if (child_nat_baseline > -1)
+ *natural_baseline = MAX (*natural_baseline, child_nat_baseline);
+ }
+}
+
+static void
+allocate_toast (AdwToastOverlay *self,
+ ToastInfo *info,
+ int width,
+ int height)
+{
+ GtkRequisition size;
+ GskTransform *transform;
+ float x, y;
+
+ gtk_widget_get_preferred_size (info->widget, NULL, &size);
+
+ size.width = MIN (MAX (size.width, NATURAL_WIDTH), width);
+ size.height = MIN (size.height, height);
+
+ x = (width - size.width) / 2;
+ y = height - size.height;
+ transform = gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (x, y));
+
+ if (info->show_animation) {
+ float value = adw_animation_get_value (info->show_animation);
+ float offset = adw_lerp (size.height, 0.0f, value);
+
+ transform = gsk_transform_translate (transform,
+ &GRAPHENE_POINT_INIT (0, offset));
+ }
+
+ if (info->hide_animation) {
+ float value = adw_animation_get_value (info->hide_animation);
+
+ x = size.width / 2.0f;
+ y = size.height / 2.0f;
+
+ value = adw_lerp (FINAL_SIZE, 1.0f, value);
+ transform = gsk_transform_translate (transform,
+ &GRAPHENE_POINT_INIT (x, y));
+ transform = gsk_transform_scale (transform, value, value);
+ transform = gsk_transform_translate (transform,
+ &GRAPHENE_POINT_INIT (-x, -y));
+ }
+
+ gtk_widget_allocate (info->widget, size.width, size.height, -1, transform);
+}
+
+static void
+adw_toast_overlay_size_allocate (GtkWidget *widget,
+ int width,
+ int height,
+ int baseline)
+{
+ AdwToastOverlay *self = ADW_TOAST_OVERLAY (widget);
+ GList *l;
+
+ if (self->child && gtk_widget_should_layout (self->child))
+ gtk_widget_allocate (self->child, width, height, baseline, NULL);
+
+ for (l = self->hiding_toasts; l; l = l->next)
+ allocate_toast (self, l->data, width, height);
+
+ if (self->current_toast)
+ allocate_toast (self, self->current_toast, width, height);
+}
+
+static void
+adw_toast_overlay_dispose (GObject *object)
+{
+ AdwToastOverlay *self = ADW_TOAST_OVERLAY (object);
+
+ adw_toast_overlay_set_child (self, NULL);
+
+ g_clear_list (&self->hiding_toasts, (GDestroyNotify) free_toast_info);
+ g_clear_pointer (&self->current_toast, dismiss_and_free_toast_info);
+ g_queue_foreach (self->queue, (GFunc) dismiss_and_free_toast_info, NULL);
+
+ G_OBJECT_CLASS (adw_toast_overlay_parent_class)->dispose (object);
+}
+
+static void
+adw_toast_overlay_finalize (GObject *object)
+{
+ AdwToastOverlay *self = ADW_TOAST_OVERLAY (object);
+
+ g_queue_free (self->queue);
+
+ G_OBJECT_CLASS (adw_toast_overlay_parent_class)->finalize (object);
+}
+
+static void
+adw_toast_overlay_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ AdwToastOverlay *self = ADW_TOAST_OVERLAY (object);
+
+ switch (prop_id) {
+ case PROP_CHILD:
+ g_value_set_object (value, adw_toast_overlay_get_child (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+adw_toast_overlay_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ AdwToastOverlay *self = ADW_TOAST_OVERLAY (object);
+
+ switch (prop_id) {
+ case PROP_CHILD:
+ adw_toast_overlay_set_child (self, g_value_get_object (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+adw_toast_overlay_class_init (AdwToastOverlayClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = adw_toast_overlay_dispose;
+ object_class->finalize = adw_toast_overlay_finalize;
+ object_class->get_property = adw_toast_overlay_get_property;
+ object_class->set_property = adw_toast_overlay_set_property;
+
+ widget_class->compute_expand = adw_widget_compute_expand;
+ widget_class->measure = adw_toast_overlay_measure;
+ widget_class->size_allocate = adw_toast_overlay_size_allocate;
+
+ /**
+ * AdwToastOverlay:child: (attributes org.gtk.Property.get=adw_toast_overlay_get_child org.gtk.Property.set=adw_toast_overlay_set_child)
+ *
+ * The child widget.
+ *
+ * Since: 1.0
+ */
+ props[PROP_CHILD] =
+ g_param_spec_object ("child",
+ "Child",
+ "The child widget",
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_css_name (widget_class, "toastoverlay");
+ gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_GROUP);
+
+ gtk_widget_class_add_binding (widget_class, GDK_KEY_Escape, 0, (GtkShortcutFunc) dismiss_cb, NULL);
+}
+
+static void
+adw_toast_overlay_init (AdwToastOverlay *self)
+{
+ self->queue = g_queue_new ();
+}
+
+static void
+adw_toast_overlay_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const char *type)
+{
+ AdwToastOverlay *self = ADW_TOAST_OVERLAY (buildable);
+
+ if (!type && GTK_IS_WIDGET (child))
+ adw_toast_overlay_set_child (self, GTK_WIDGET (child));
+ else
+ parent_buildable_iface->add_child (buildable, builder, child, type);
+}
+
+static void
+adw_toast_overlay_buildable_init (GtkBuildableIface *iface)
+{
+ parent_buildable_iface = g_type_interface_peek_parent (iface);
+
+ iface->add_child = adw_toast_overlay_buildable_add_child;
+}
+
+/**
+ * adw_toast_overlay_new:
+ *
+ * Creates a new `AdwToastOverlay`.
+ *
+ * Returns: the new created `AdwToastOverlay`
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+adw_toast_overlay_new (void)
+{
+ return g_object_new (ADW_TYPE_TOAST_OVERLAY, NULL);
+}
+
+/**
+ * adw_toast_overlay_get_child: (attributes org.gtk.Method.get_property=child)
+ * @self: a `AdwToastOverlay`
+ *
+ * Gets the child widget of @self.
+ *
+ * Returns: (nullable) (transfer none): the child widget of @self
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+adw_toast_overlay_get_child (AdwToastOverlay *self)
+{
+ g_return_val_if_fail (ADW_IS_TOAST_OVERLAY (self), NULL);
+
+ return self->child;
+}
+
+/**
+ * adw_toast_overlay_set_child: (attributes org.gtk.Method.set_property=child)
+ * @self: a `AdwToastOverlay`
+ * @child: (nullable): the child widget
+ *
+ * Sets the child widget of @self.
+ *
+ * Since: 1.0
+ */
+void
+adw_toast_overlay_set_child (AdwToastOverlay *self,
+ GtkWidget *child)
+{
+ g_return_if_fail (ADW_IS_TOAST_OVERLAY (self));
+ g_return_if_fail (child == NULL || GTK_IS_WIDGET (child));
+
+ if (self->child == child)
+ return;
+
+ if (self->child)
+ gtk_widget_unparent (self->child);
+
+ self->child = child;
+
+ if (self->child)
+ gtk_widget_insert_after (self->child, GTK_WIDGET (self), NULL);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD]);
+}
+
+/**
+ * adw_toast_overlay_add_toast:
+ * @self: a `AdwToastOverlay`
+ * @toast: (transfer full): a toast
+ *
+ * Displays @toast.
+ *
+ * Only one toast can be shown at a time; if a toast is already being displayed,
+ * either @toast or the original toast will be placed in a queue, depending on
+ * the priority of @toast. See [property@Adw.Toast:priority].
+ *
+ * Since: 1.0
+ */
+void
+adw_toast_overlay_add_toast (AdwToastOverlay *self,
+ AdwToast *toast)
+{
+ ToastInfo *info;
+
+ g_return_if_fail (ADW_IS_TOAST_OVERLAY (self));
+ g_return_if_fail (ADW_IS_TOAST (toast));
+
+ if (adw_toast_get_added (toast)) {
+ g_critical ("Adding toast '%s', but it has already been added to an "
+ "AdwToastOverlay", adw_toast_get_title (toast));
+
+ return;
+ }
+
+ adw_toast_set_added (toast, TRUE);
+
+ info = g_new0 (ToastInfo, 1);
+ info->overlay = self;
+ info->toast = toast;
+ info-> dismissed_id =
+ g_signal_connect_swapped (info->toast, "dismissed",
+ G_CALLBACK (dismissed_cb), info);
+
+ if (!self->current_toast) {
+ show_toast (self, info);
+
+ return;
+ }
+
+ switch (adw_toast_get_priority (toast)) {
+ case ADW_TOAST_PRIORITY_NORMAL:
+ g_queue_push_tail (self->queue, info);
+ break;
+
+ case ADW_TOAST_PRIORITY_HIGH:
+ self->current_toast->postponing = TRUE;
+ g_queue_push_head (self->queue, self->current_toast);
+
+ hide_current_toast (self);
+ show_toast (self, info);
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+}
diff --git a/src/adw-toast-overlay.h b/src/adw-toast-overlay.h
new file mode 100644
index 0000000000000000000000000000000000000000..ca57310c22b78f9f804e01389be7ad3190c58bd2
--- /dev/null
+++ b/src/adw-toast-overlay.h
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2021 Maximiliano Sandoval
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include "adw-version.h"
+#include "adw-toast.h"
+
+#include
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_TOAST_OVERLAY (adw_toast_overlay_get_type())
+
+ADW_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (AdwToastOverlay, adw_toast_overlay, ADW, TOAST_OVERLAY, GtkWidget)
+
+ADW_AVAILABLE_IN_ALL
+GtkWidget *adw_toast_overlay_new (void) G_GNUC_WARN_UNUSED_RESULT;
+
+ADW_AVAILABLE_IN_ALL
+GtkWidget *adw_toast_overlay_get_child (AdwToastOverlay *self);
+ADW_AVAILABLE_IN_ALL
+void adw_toast_overlay_set_child (AdwToastOverlay *self,
+ GtkWidget *child);
+
+ADW_AVAILABLE_IN_ALL
+void adw_toast_overlay_add_toast (AdwToastOverlay *self,
+ AdwToast *toast);
+
+G_END_DECLS
diff --git a/src/adw-toast-private.h b/src/adw-toast-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..b9fa39076b4a25c4688aa01266cffd790a6cd0ec
--- /dev/null
+++ b/src/adw-toast-private.h
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include "adw-toast.h"
+
+G_BEGIN_DECLS
+
+gboolean adw_toast_get_added (AdwToast *self);
+void adw_toast_set_added (AdwToast *self,
+ gboolean added);
+
+G_END_DECLS
diff --git a/src/adw-toast-widget-private.h b/src/adw-toast-widget-private.h
new file mode 100644
index 0000000000000000000000000000000000000000..ff523f716edfc01d30a92950c57e145270f46f82
--- /dev/null
+++ b/src/adw-toast-widget-private.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2018 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include "adw-version.h"
+#include "adw-toast.h"
+
+#include
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_TOAST_WIDGET (adw_toast_widget_get_type())
+
+G_DECLARE_FINAL_TYPE (AdwToastWidget, adw_toast_widget, ADW, TOAST_WIDGET, GtkWidget)
+
+GtkWidget *adw_toast_widget_new (AdwToast *toast) G_GNUC_WARN_UNUSED_RESULT;
+
+G_END_DECLS
diff --git a/src/adw-toast-widget.c b/src/adw-toast-widget.c
new file mode 100644
index 0000000000000000000000000000000000000000..a9fcb0184c7c68a64dd1d708fd2e2c27e7ef843d
--- /dev/null
+++ b/src/adw-toast-widget.c
@@ -0,0 +1,206 @@
+/*
+ * Copyright (C) 2021 Maximiliano Sandoval
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "adw-toast-widget-private.h"
+
+#define TOAST_DURATION 5000
+
+struct _AdwToastWidget {
+ GtkWidget parent_instance;
+
+ AdwToast *toast;
+
+ guint hide_timeout_id;
+ gint inhibit_count;
+};
+
+enum {
+ PROP_0,
+ PROP_TOAST,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+G_DEFINE_TYPE (AdwToastWidget, adw_toast_widget, GTK_TYPE_WIDGET)
+
+static gboolean
+string_is_not_empty (gpointer user_data,
+ const char *string)
+{
+ return string && string[0];
+}
+
+static gboolean
+timeout_cb (AdwToastWidget *self)
+{
+ self->hide_timeout_id = 0;
+
+ adw_toast_dismiss (self->toast);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+start_timeout (AdwToastWidget *self)
+{
+ if (!self->hide_timeout_id)
+ self->hide_timeout_id =
+ g_timeout_add (TOAST_DURATION,
+ G_SOURCE_FUNC (timeout_cb),
+ self);
+}
+
+static void
+end_timeout (AdwToastWidget *self)
+{
+ g_clear_handle_id (&self->hide_timeout_id, g_source_remove);
+}
+
+static void
+inhibit_hide (AdwToastWidget *self)
+{
+ if (self->inhibit_count++ == 0)
+ end_timeout (self);
+}
+
+static void
+uninhibit_hide (AdwToastWidget *self)
+{
+ g_assert (self->inhibit_count);
+
+ if (--self->inhibit_count == 0)
+ start_timeout (self);
+}
+
+static void
+dismiss (AdwToastWidget *self)
+{
+ end_timeout (self);
+
+ adw_toast_dismiss (self->toast);
+}
+
+static gboolean
+close_idle_cb (AdwToastWidget *self)
+{
+ dismiss (self);
+ g_object_unref (self);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+action_clicked_cb (AdwToastWidget *self)
+{
+ end_timeout (self);
+
+ /* Keep the widget alive through the idle. Otherwise it may be immediately
+ * destroyed if animations are disabled */
+ g_idle_add (G_SOURCE_FUNC (close_idle_cb), g_object_ref (self));
+}
+
+static void
+adw_toast_widget_dispose (GObject *object)
+{
+ AdwToastWidget *self = ADW_TOAST_WIDGET (object);
+ GtkWidget *child;
+
+ end_timeout (self);
+
+ while ((child = gtk_widget_get_first_child (GTK_WIDGET (self))))
+ gtk_widget_unparent (child);
+
+ g_clear_pointer (&self->toast, g_object_unref);
+
+ G_OBJECT_CLASS (adw_toast_widget_parent_class)->dispose (object);
+}
+
+static void
+adw_toast_widget_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ AdwToastWidget *self = ADW_TOAST_WIDGET (object);
+
+ switch (prop_id) {
+ case PROP_TOAST:
+ g_value_set_object (value, self->toast);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+adw_toast_widget_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ AdwToastWidget *self = ADW_TOAST_WIDGET (object);
+
+ switch (prop_id) {
+ case PROP_TOAST:
+ g_set_object (&self->toast, g_value_get_object (value));
+ end_timeout (self);
+ start_timeout (self);
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+adw_toast_widget_class_init (AdwToastWidgetClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = adw_toast_widget_dispose;
+ object_class->get_property = adw_toast_widget_get_property;
+ object_class->set_property = adw_toast_widget_set_property;
+
+ props[PROP_TOAST] =
+ g_param_spec_object ("toast",
+ "Toast",
+ "The displayed toast",
+ ADW_TYPE_TOAST,
+ G_PARAM_READWRITE);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/org/gnome/Adwaita/ui/adw-toast-widget.ui");
+
+ gtk_widget_class_bind_template_callback (widget_class, string_is_not_empty);
+ gtk_widget_class_bind_template_callback (widget_class, action_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, dismiss);
+ gtk_widget_class_bind_template_callback (widget_class, inhibit_hide);
+ gtk_widget_class_bind_template_callback (widget_class, uninhibit_hide);
+
+ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BOX_LAYOUT);
+ gtk_widget_class_set_css_name (widget_class, "toast");
+}
+
+static void
+adw_toast_widget_init (AdwToastWidget *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+GtkWidget *
+adw_toast_widget_new (AdwToast *toast)
+{
+ g_assert (ADW_IS_TOAST (toast));
+
+ return g_object_new (ADW_TYPE_TOAST_WIDGET,
+ "toast", toast,
+ NULL);
+}
diff --git a/src/adw-toast-widget.ui b/src/adw-toast-widget.ui
new file mode 100644
index 0000000000000000000000000000000000000000..bb69ba3b69ac0f99b323a8d5fea8a695c394e918
--- /dev/null
+++ b/src/adw-toast-widget.ui
@@ -0,0 +1,80 @@
+
+
+
+
+ False
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ end
+ 0
+ True
+ True
+
+
+ AdwToastWidget
+
+
+
+
+
+
+
+ center
+
+
+
+ AdwToastWidget
+
+
+
+
+
+ AdwToastWidget
+
+
+
+
+ AdwToastWidget
+
+
+
+
+ AdwToastWidget
+
+
+
+
+
+
+
+ center
+ window-close-symbolic
+
+
+
+
+
+
diff --git a/src/adw-toast.c b/src/adw-toast.c
new file mode 100644
index 0000000000000000000000000000000000000000..f8f3276aec333fe6ad4ae673724212f0a6d20b68
--- /dev/null
+++ b/src/adw-toast.c
@@ -0,0 +1,725 @@
+/*
+ * Copyright (C) 2021 Maximiliano Sandoval
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+
+#include "adw-toast-private.h"
+
+/**
+ * AdwToastPriority:
+ * @ADW_TOAST_PRIORITY_NORMAL: the toast will be queued if another toast is
+ * already displayed.
+ * @ADW_TOAST_PRIORITY_HIGH: the toast will be displayed immediately, pushing
+ * the previous toast into the queue instead.
+ *
+ * [class@Adw.Toast] behavior when another toast is already displayed.
+ *
+ * Since: 1.0
+ */
+
+/**
+ * AdwToast:
+ *
+ * A helper object for [class@Adw.ToastOverlay].
+ *
+ * Toasts are meant to be passed into [method@Adw.ToastOverlay.add_toast] as
+ * follows:
+ *
+ * ```c
+ * adw_toast_overlay_add_toast (overlay, adw_toast_new (_("Simple Toast"));
+ * ```
+ *
+ * Toasts always have a close button and a timeout. In both cases, they emit the
+ * [signal@Adw.Toast::dismissed] signal when disappearing.
+ *
+ * [property@Adw.Toast:priority] determines how the toast behaves if another
+ * toast is already being displayed.
+ *
+ * ## Actions
+ *
+ * Toasts can have one button on them, with a label and an attached
+ * [iface@Gio.Action].
+ *
+ * ```c
+ * AdwToast *toast = adw_toast_new (_("Toast with Action"));
+ *
+ * adw_toast_set_button_label (toast, _("Example"));
+ * adw_toast_set_action_name (toast, "win.example");
+ *
+ * adw_toast_overlay_add_toast (overlay, toast);
+ * ```
+ *
+ * ## Modifying toasts
+ *
+ * Toasts can be modified after they have been shown. For this, an `AdwToast`
+ * reference must be kept around while the toast is visible.
+ *
+ * A common use case for this is using toasts as undo prompts that stack with
+ * each other, allowing to batch undo the last deleted items:
+ *
+ * ```c
+ *
+ * static void
+ * toast_undo_cb (GtkWidget *sender,
+ * const char *action,
+ * GVariant *param)
+ * {
+ * // Undo the deletion
+ * }
+ *
+ * static void
+ * dismissed_cb (MyWindow *self)
+ * {
+ * self->undo_toast = NULL;
+ *
+ * // Permanently delete the items
+ * }
+ *
+ * static void
+ * delete_item (MyWindow *self,
+ * MyItem *item)
+ * {
+ * g_autofree char *title = NULL;
+ * int n_items;
+ *
+ * // Mark the item as waiting for deletion
+ * n_items = ... // The number of waiting items
+ *
+ * if (!self->undo_toast) {
+ * title = g_strdup_printf (_("‘%s’ deleted"), ...);
+ *
+ * self->undo_toast = adw_toast_new (title);
+ *
+ * adw_toast_set_priority (self->undo_toast, ADW_TOAST_PRIORITY_HIGH);
+ * adw_toast_set_button_label (self->undo_toast, _("Undo"));
+ * adw_toast_set_action_name (self->undo_toast, "toast.undo");
+ *
+ * g_signal_connect_swapped (self->undo_toast, "dismissed",
+ * G_CALLBACK (dismissed_cb), self);
+ *
+ * adw_toast_overlay_add_toast (self->toast_overlay, self->undo_toast);
+ *
+ * return;
+ * }
+ *
+ * title =
+ * g_strdup_printf (ngettext ("%d item deleted",
+ * "%d items deleted",
+ * n_items), n_items);
+ *
+ * adw_toast_set_title (self->undo_toast, title);
+ * }
+ *
+ * static void
+ * my_window_class_init (MyWindowClass *klass)
+ * {
+ * GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ *
+ * gtk_widget_class_install_action (widget_class, "toast.undo", NULL, toast_undo_cb);
+ * }
+ * ```
+ *
+ * Since: 1.0
+ */
+
+struct _AdwToast {
+ GObject parent_instance;
+
+ char *title;
+ char *button_label;
+ char *action_name;
+ GVariant *action_target;
+ AdwToastPriority priority;
+
+ gboolean added;
+};
+
+enum {
+ PROP_0,
+ PROP_TITLE,
+ PROP_BUTTON_LABEL,
+ PROP_ACTION_NAME,
+ PROP_ACTION_TARGET,
+ PROP_PRIORITY,
+ LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_DISMISSED,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+G_DEFINE_TYPE (AdwToast, adw_toast, G_TYPE_OBJECT)
+
+static void
+dismissed_cb (AdwToast *self)
+{
+ self->added = FALSE;
+}
+
+static void
+adw_toast_finalize (GObject *object)
+{
+ AdwToast *self = ADW_TOAST (object);
+
+ g_clear_pointer (&self->title, g_free);
+ g_clear_pointer (&self->button_label, g_free);
+ g_clear_pointer (&self->action_name, g_free);
+ g_clear_pointer (&self->action_target, g_variant_unref);
+
+ G_OBJECT_CLASS (adw_toast_parent_class)->finalize (object);
+}
+
+static void
+adw_toast_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ AdwToast *self = ADW_TOAST (object);
+
+ switch (prop_id) {
+ case PROP_TITLE:
+ g_value_set_string (value, adw_toast_get_title (self));
+ break;
+ case PROP_BUTTON_LABEL:
+ g_value_set_string (value, adw_toast_get_button_label (self));
+ break;
+ case PROP_ACTION_NAME:
+ g_value_set_string (value, adw_toast_get_action_name (self));
+ break;
+ case PROP_ACTION_TARGET:
+ g_value_set_variant (value, adw_toast_get_action_target_value (self));
+ break;
+ case PROP_PRIORITY:
+ g_value_set_enum (value, adw_toast_get_priority (self));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+adw_toast_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ AdwToast *self = ADW_TOAST (object);
+
+ switch (prop_id) {
+ case PROP_TITLE:
+ adw_toast_set_title (self, g_value_get_string (value));
+ break;
+ case PROP_BUTTON_LABEL:
+ adw_toast_set_button_label (self, g_value_get_string (value));
+ break;
+ case PROP_ACTION_NAME:
+ adw_toast_set_action_name (self, g_value_get_string (value));
+ break;
+ case PROP_ACTION_TARGET:
+ adw_toast_set_action_target_value (self, g_value_get_variant (value));
+ break;
+ case PROP_PRIORITY:
+ adw_toast_set_priority (self, g_value_get_enum (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+adw_toast_class_init (AdwToastClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->finalize = adw_toast_finalize;
+ object_class->get_property = adw_toast_get_property;
+ object_class->set_property = adw_toast_set_property;
+
+ /**
+ * AdwToast:title: (attributes org.gtk.Property.get=adw_toast_get_title org.gtk.Property.set=adw_toast_set_title)
+ *
+ * The title of the toast.
+ *
+ * The title can be marked up with the Pango text markup language.
+ *
+ * Since: 1.0
+ */
+ props[PROP_TITLE] =
+ g_param_spec_string ("title",
+ "Title",
+ "The title of the toast",
+ "",
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwToast:button-label: (attributes org.gtk.Property.get=adw_toast_get_button_label org.gtk.Property.set=adw_toast_set_button_label)
+ *
+ * The label to show on the button.
+ *
+ * If set to `NULL`, the button won't be shown.
+ *
+ * See [property@Adw.Toast:action-name].
+ *
+ * Since: 1.0
+ */
+ props[PROP_BUTTON_LABEL] =
+ g_param_spec_string ("button-label",
+ "Button Label",
+ "The label to show on the button",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwToast:action-name: (attributes org.gtk.Property.get=adw_toast_get_action_name org.gtk.Property.set=adw_toast_set_action_name)
+ *
+ * The name of the associated action.
+ *
+ * It will be activated when clicking the button.
+ *
+ * See [property@Adw.Toast:action-target].
+ *
+ * Since: 1.0
+ */
+ props[PROP_ACTION_NAME] =
+ g_param_spec_string ("action-name",
+ "Action Name",
+ "The name of the associated action",
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwToast:action-target: (attributes org.gtk.Property.get=adw_toast_get_action_target_value org.gtk.Property.set=adw_toast_set_action_target_value)
+ *
+ * The parameter for action invocations.
+ *
+ * See [property@Adw.Toast:action-name].
+ *
+ * Since: 1.0
+ */
+ props[PROP_ACTION_TARGET] =
+ g_param_spec_variant ("action-target",
+ "Action Target Value",
+ "The parameter for action invocations",
+ G_VARIANT_TYPE_ANY,
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwToast:priority: (attributes org.gtk.Property.get=adw_toast_get_priority org.gtk.Property.set=adw_toast_set_priority)
+ *
+ * The priority of the toast.
+ *
+ * Priority controls how the toast behaves when another toast is already
+ * being displayed.
+ *
+ * If the priority is `ADW_TOAST_PRIORITY_NORMAL`, the toast will be queued.
+ *
+ * If the priority is `ADW_TOAST_PRIORITY_HIGH`, the toast will be displayed
+ * immediately, pushing the previous toast into the queue instead.
+ *
+ * Since: 1.0
+ */
+ props[PROP_PRIORITY] =
+ g_param_spec_enum ("priority",
+ "Priority",
+ "The priority of the toast",
+ ADW_TYPE_TOAST_PRIORITY,
+ ADW_TOAST_PRIORITY_NORMAL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ /**
+ * AdwToast::dismissed:
+ *
+ * Emitted when the toast has been dismissed.
+ *
+ * Since: 1.0
+ */
+ signals[SIGNAL_DISMISSED] =
+ g_signal_new ("dismissed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 0);
+}
+
+static void
+adw_toast_init (AdwToast *self)
+{
+ self->title = g_strdup ("");
+ self->priority = ADW_TOAST_PRIORITY_NORMAL;
+
+ g_signal_connect (self, "dismissed", G_CALLBACK (dismissed_cb), self);
+}
+
+/**
+ * adw_toast_new:
+ * @title: the title to be displayed
+ *
+ * Creates a new `AdwToast`.
+ *
+ * The toast will use @title as its title.
+ *
+ * @title can be marked up with the Pango text markup language.
+ *
+ * Returns: the new created `AdwToast`
+ *
+ * Since: 1.0
+ */
+AdwToast *
+adw_toast_new (const char *title)
+{
+ g_return_val_if_fail (title != NULL, NULL);
+
+ return g_object_new (ADW_TYPE_TOAST,
+ "title", title,
+ NULL);
+}
+
+/**
+ * adw_toast_get_title: (attributes org.gtk.Method.get_property=title)
+ * @self: a `AdwToast`
+ *
+ * Gets the title that will be displayed on the toast.
+ *
+ * Returns: the title
+ *
+ * Since: 1.0
+ */
+const char *
+adw_toast_get_title (AdwToast *self)
+{
+ g_return_val_if_fail (ADW_IS_TOAST (self), NULL);
+
+ return self->title;
+}
+
+/**
+ * adw_toast_set_title: (attributes org.gtk.Method.set_property=title)
+ * @self: a `AdwToast`
+ * @title: a title
+ *
+ * Sets the title that will be displayed on the toast.
+ *
+ * Since: 1.0
+ */
+void
+adw_toast_set_title (AdwToast *self,
+ const char *title)
+{
+ g_return_if_fail (ADW_IS_TOAST (self));
+ g_return_if_fail (title != NULL);
+
+ if (!g_strcmp0 (self->title, title))
+ return;
+
+ g_clear_pointer (&self->title, g_free);
+ self->title = g_strdup (title);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+}
+
+/**
+ * adw_toast_get_button_label: (attributes org.gtk.Method.get_property=button-label)
+ * @self: a `AdwToast`
+ *
+ * Gets the label to show on the button.
+ *
+ * Returns: (nullable): the button label
+ *
+ * Since: 1.0
+ */
+const char *
+adw_toast_get_button_label (AdwToast *self)
+{
+ g_return_val_if_fail (ADW_IS_TOAST (self), NULL);
+
+ return self->button_label;
+}
+
+/**
+ * adw_toast_set_button_label: (attributes org.gtk.Method.set_property=button-label)
+ * @self: a `AdwToast`
+ * @button_label: (nullable): a button label
+ *
+ * Sets the label to show on the button.
+ *
+ * It set to `NULL`, the button won't be shown.
+ *
+ * Since: 1.0
+ */
+void
+adw_toast_set_button_label (AdwToast *self,
+ const char *button_label)
+{
+ g_return_if_fail (ADW_IS_TOAST (self));
+
+ if (!g_strcmp0 (self->button_label, button_label))
+ return;
+
+ g_clear_pointer (&self->button_label, g_free);
+ self->button_label = g_strdup (button_label);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BUTTON_LABEL]);
+}
+
+/**
+ * adw_toast_get_action_name: (attributes org.gtk.Method.get_property=action-name)
+ * @self: a `AdwToast`
+ *
+ * Gets the name of the associated action.
+ *
+ * Returns: (nullable): the action name
+ *
+ * Since: 1.0
+ */
+const char *
+adw_toast_get_action_name (AdwToast *self)
+{
+ g_return_val_if_fail (ADW_IS_TOAST (self), NULL);
+
+ return self->action_name;
+}
+
+/**
+ * adw_toast_set_action_name: (attributes org.gtk.Method.set_property=action-name)
+ * @self: a `AdwToast`
+ * @action_name: (nullable): the action name
+ *
+ * Sets the name of the associated action.
+ *
+ * Since: 1.0
+ */
+void
+adw_toast_set_action_name (AdwToast *self,
+ const char *action_name)
+{
+ g_return_if_fail (ADW_IS_TOAST (self));
+
+ if (!g_strcmp0 (self->action_name, action_name))
+ return;
+
+ g_clear_pointer (&self->action_name, g_free);
+ self->action_name = g_strdup (action_name);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTION_NAME]);
+}
+
+/**
+ * adw_toast_get_action_target_value: (attributes org.gtk.Method.get_property=action-target)
+ * @self: a `AdwToast`
+ *
+ * Gets the parameter for action invocations.
+ *
+ * Returns: (transfer none) (nullable): the action target
+ *
+ * Since: 1.0
+ */
+GVariant *
+adw_toast_get_action_target_value (AdwToast *self)
+{
+ g_return_val_if_fail (ADW_IS_TOAST (self), NULL);
+
+ return self->action_target;
+}
+
+/**
+ * adw_toast_set_action_target_value: (attributes org.gtk.Method.get_property=action-target)
+ * @self: a `AdwToast`
+ * @action_target: (nullable): the action target
+ *
+ * Sets the parameter for action invocations.
+ *
+ * Since: 1.0
+ */
+void
+adw_toast_set_action_target_value (AdwToast *self,
+ GVariant *action_target)
+{
+ g_return_if_fail (ADW_IS_TOAST (self));
+
+ if (action_target == self->action_target)
+ return;
+
+ if (action_target && self->action_target &&
+ g_variant_equal (action_target, self->action_target))
+ return;
+
+ g_clear_pointer (&self->action_target, g_variant_unref);
+ self->action_target = g_variant_ref (action_target);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ACTION_TARGET]);
+}
+
+/**
+ * adw_toast_set_action_target: (skip)
+ * @self: a `AdwToast`
+ * @format_string: (nullable): a [struct@GLib.Variant] format string
+ * @...: arguments appropriate for @target_format
+ *
+ * Sets the parameter for action invocations.
+ *
+ * This is a convenience function that calls [ctor@GLib.Variant.new] for
+ * @format_string and uses the result to call
+ * [method@Adw.Toast.set_action_target_value].
+ *
+ * If you are setting a string-valued target and want to set
+ * the action name at the same time, you can use
+ * [method@Adw.Toast.set_detailed_action_name].
+
+ * Since: 1.0
+ */
+void
+adw_toast_set_action_target (AdwToast *self,
+ const char *format_string,
+ ...)
+{
+ va_list args;
+
+ va_start (args, format_string);
+ adw_toast_set_action_target_value (self,
+ g_variant_new_va (format_string,
+ NULL, &args));
+ va_end (args);
+}
+
+/**
+ * adw_toast_set_detailed_action_name:
+ * @self: a `AdwToast`
+ * @detailed_action_name: (nullable): the detailed action name
+ *
+ * Sets the action name and its parameter.
+ *
+ * @detailed_action_name is a string in the format accepted by
+ * [func@Gio.Action.parse_detailed_name].
+ *
+ * Since: 1.0
+ */
+void
+adw_toast_set_detailed_action_name (AdwToast *self,
+ const char *detailed_action_name)
+{
+ g_autofree char *name = NULL;
+ g_autoptr (GVariant) target = NULL;
+ g_autoptr (GError) error = NULL;
+
+ g_return_if_fail (ADW_IS_TOAST (self));
+
+ if (!detailed_action_name) {
+ adw_toast_set_action_name (self, NULL);
+ adw_toast_set_action_target_value (self, NULL);
+
+ return;
+ }
+
+ if (!g_action_parse_detailed_name (detailed_action_name, &name, &target, &error)) {
+ g_critical ("Couldn't parse detailed action name: %s", error->message);
+
+ return;
+ }
+
+ adw_toast_set_action_name (self, name);
+ adw_toast_set_action_target_value (self, target);
+}
+
+/**
+ * adw_toast_get_priority: (attributes org.gtk.Method.get_property=priority)
+ * @self: a `AdwToast`
+ *
+ * Gets the toast priority.
+ *
+ * Returns: the priority
+ *
+ * Since: 1.0
+ */
+AdwToastPriority
+adw_toast_get_priority (AdwToast *self)
+{
+ g_return_val_if_fail (ADW_IS_TOAST (self), ADW_TOAST_PRIORITY_NORMAL);
+
+ return self->priority;
+}
+
+/**
+ * adw_toast_set_priority: (attributes org.gtk.Method.set_property=priority)
+ * @self: a `AdwToast`
+ * @priority: the priority
+ *
+ * Sets the toast priority.
+ *
+ * Priority controls how the toast behaves when another toast is already
+ * being displayed.
+ *
+ * If @priority is `ADW_TOAST_PRIORITY_NORMAL`, the toast will be queued.
+ *
+ * If @priority is `ADW_TOAST_PRIORITY_HIGH`, the toast will be displayed immediately,
+ * pushing the previous toast into the queue instead.
+ *
+ * Since: 1.0
+ */
+void
+adw_toast_set_priority (AdwToast *self,
+ AdwToastPriority priority)
+{
+ g_return_if_fail (ADW_IS_TOAST (self));
+ g_return_if_fail (priority >= ADW_TOAST_PRIORITY_NORMAL &&
+ priority <= ADW_TOAST_PRIORITY_HIGH);
+
+ if (self->priority == priority)
+ return;
+
+ self->priority = priority;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PRIORITY]);
+}
+
+/**
+ * adw_toast_dismiss:
+ * @self: a `AdwToast`
+ *
+ * Dismisses @self.
+ *
+ * Since: 1.0
+ */
+void
+adw_toast_dismiss (AdwToast *self)
+{
+ g_return_if_fail (ADW_IS_TOAST (self));
+
+ if (!self->added) {
+ g_critical ("Trying to dismiss the toast '%s', but it isn't in an "
+ "AdwToastOverlay yet", self->title);
+
+ return;
+ }
+
+ g_signal_emit (self, signals[SIGNAL_DISMISSED], 0, NULL);
+}
+
+gboolean
+adw_toast_get_added (AdwToast *self)
+{
+ g_return_val_if_fail (ADW_IS_TOAST (self), FALSE);
+
+ return self->added;
+}
+
+void
+adw_toast_set_added (AdwToast *self,
+ gboolean added)
+{
+ g_return_if_fail (ADW_IS_TOAST (self));
+
+ self->added = !!added;
+}
diff --git a/src/adw-toast.h b/src/adw-toast.h
new file mode 100644
index 0000000000000000000000000000000000000000..4111a088511a60e3e0eb93394e415cd13e5f37bb
--- /dev/null
+++ b/src/adw-toast.h
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2021 Maximiliano Sandoval
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only can be included directly."
+#endif
+
+#include "adw-version.h"
+
+#include
+#include "adw-enums.h"
+
+G_BEGIN_DECLS
+
+typedef enum {
+ ADW_TOAST_PRIORITY_NORMAL,
+ ADW_TOAST_PRIORITY_HIGH,
+} AdwToastPriority;
+
+#define ADW_TYPE_TOAST (adw_toast_get_type())
+
+ADW_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (AdwToast, adw_toast, ADW, TOAST, GObject)
+
+ADW_AVAILABLE_IN_ALL
+AdwToast *adw_toast_new (const char *title) G_GNUC_WARN_UNUSED_RESULT;
+
+ADW_AVAILABLE_IN_ALL
+const char *adw_toast_get_title (AdwToast *self);
+ADW_AVAILABLE_IN_ALL
+void adw_toast_set_title (AdwToast *self,
+ const char *title);
+
+ADW_AVAILABLE_IN_ALL
+const char *adw_toast_get_button_label (AdwToast *self);
+ADW_AVAILABLE_IN_ALL
+void adw_toast_set_button_label (AdwToast *self,
+ const char *button_label);
+
+ADW_AVAILABLE_IN_ALL
+const char *adw_toast_get_action_name (AdwToast *self);
+ADW_AVAILABLE_IN_ALL
+void adw_toast_set_action_name (AdwToast *self,
+ const char *action_name);
+
+ADW_AVAILABLE_IN_ALL
+GVariant *adw_toast_get_action_target_value (AdwToast *self);
+ADW_AVAILABLE_IN_ALL
+void adw_toast_set_action_target_value (AdwToast *self,
+ GVariant *action_target);
+ADW_AVAILABLE_IN_ALL
+void adw_toast_set_action_target (AdwToast *self,
+ const char *format_string,
+ ...);
+ADW_AVAILABLE_IN_ALL
+void adw_toast_set_detailed_action_name (AdwToast *self,
+ const char *detailed_action_name);
+
+ADW_AVAILABLE_IN_ALL
+AdwToastPriority adw_toast_get_priority (AdwToast *self);
+ADW_AVAILABLE_IN_ALL
+void adw_toast_set_priority (AdwToast *self,
+ AdwToastPriority priority);
+
+ADW_AVAILABLE_IN_ALL
+void adw_toast_dismiss (AdwToast *self);
+
+G_END_DECLS
diff --git a/src/adwaita.gresources.xml b/src/adwaita.gresources.xml
index 69f5f48368f5403ed3cb6ba6951bcf569bda37ed..ea354366c34bae7deec9a1499d05f37e3abd9480 100644
--- a/src/adwaita.gresources.xml
+++ b/src/adwaita.gresources.xml
@@ -17,6 +17,7 @@
adw-status-page.ui
adw-tab.ui
adw-tab-bar.ui
+ adw-toast-widget.ui
adw-view-switcher-bar.ui
adw-view-switcher-button.ui
adw-view-switcher-title.ui
diff --git a/src/adwaita.h b/src/adwaita.h
index 0fca7f8d727e8837c1ad9f4fd0fed9e8c528162b..c5aa01898997e820f2742dc4adbb9c9d8e9ec0f1 100644
--- a/src/adwaita.h
+++ b/src/adwaita.h
@@ -56,6 +56,8 @@ G_BEGIN_DECLS
#include "adw-swipeable.h"
#include "adw-tab-bar.h"
#include "adw-tab-view.h"
+#include "adw-toast-overlay.h"
+#include "adw-toast.h"
#include "adw-view-stack.h"
#include "adw-view-switcher.h"
#include "adw-view-switcher-bar.h"
diff --git a/src/meson.build b/src/meson.build
index 6d3b5e7a61d143ca3303d4859243c1bdeb1270ac..35f3d905434c333960026200b8a46def2fa51b0c 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -17,7 +17,7 @@ adw_public_enum_headers = [
'adw-navigation-direction.h',
'adw-style-manager.h',
'adw-squeezer.h',
- 'adw-tab-bar.h',
+ 'adw-toast.h',
'adw-view-switcher.h',
]
@@ -105,6 +105,8 @@ src_headers = [
'adw-swipeable.h',
'adw-tab-bar.h',
'adw-tab-view.h',
+ 'adw-toast.h',
+ 'adw-toast-overlay.h',
'adw-view-stack.h',
'adw-view-switcher.h',
'adw-view-switcher-bar.h',
@@ -168,6 +170,9 @@ src_sources = [
'adw-tab-bar.c',
'adw-tab-box.c',
'adw-tab-view.c',
+ 'adw-toast.c',
+ 'adw-toast-overlay.c',
+ 'adw-toast-widget.c',
'adw-version.c',
'adw-view-stack.c',
'adw-view-switcher.c',
diff --git a/src/stylesheet/widgets/_misc.scss b/src/stylesheet/widgets/_misc.scss
index fbf5fdd6ac829764e91da79db921b87ff26dd4e1..51f10bb083f5e6a0e7bc21cbf11cd67e3534b2f8 100644
--- a/src/stylesheet/widgets/_misc.scss
+++ b/src/stylesheet/widgets/_misc.scss
@@ -49,6 +49,32 @@ separator {
border { border: none; }
}
+/**********
+ * Toasts *
+ **********/
+
+toast {
+ @extend %osd;
+
+ margin: 12px;
+ margin-bottom: 24px;
+
+ border-radius: 150px;
+ border-spacing: 6px;
+ padding: 6px;
+
+ &:dir(ltr) { padding-left: 12px; }
+ &:dir(rtl) { padding-right: 12px; }
+
+ > label {
+ margin: 0 6px;
+ }
+
+ @if $contrast == 'high' {
+ box-shadow: 0 0 0 1px $border_color;
+ }
+}
+
/**************
* GtkVideo *
**************/
diff --git a/tests/meson.build b/tests/meson.build
index 83c40b69f863224983a6ad9dcc6466aabeeb0e17..2c9ffc4fc7c484aa55dd86b9e58ad0f1e7a702c8 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -43,6 +43,8 @@ test_names = [
'test-style-manager',
'test-tab-bar',
'test-tab-view',
+ 'test-toast',
+ 'test-toast-overlay',
'test-view-switcher',
'test-view-switcher-bar',
'test-window',
diff --git a/tests/test-toast-overlay.c b/tests/test-toast-overlay.c
new file mode 100644
index 0000000000000000000000000000000000000000..1ea5c5fc3a5537c8cc2923e23cf482cdcde64beb
--- /dev/null
+++ b/tests/test-toast-overlay.c
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 Maximiliano Sandoval
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include
+
+int notified;
+
+static void
+notify_cb (GtkWidget *widget, gpointer data)
+{
+ notified++;
+}
+
+static void
+test_adw_toast_overlay_child (void)
+{
+ AdwToastOverlay *toast_overlay = g_object_ref_sink (ADW_TOAST_OVERLAY (adw_toast_overlay_new ()));
+ GtkWidget *widget = NULL;
+
+ g_assert_nonnull (toast_overlay);
+
+ notified = 0;
+ g_signal_connect (toast_overlay, "notify::child", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (toast_overlay, "child", &widget, NULL);
+ g_assert_null (widget);
+
+ adw_toast_overlay_set_child (toast_overlay, NULL);
+ g_assert_cmpint (notified, ==, 0);
+
+ widget = gtk_button_new ();
+ adw_toast_overlay_set_child (toast_overlay, widget);
+ g_assert_true (adw_toast_overlay_get_child (toast_overlay) == widget);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (toast_overlay, "child", NULL, NULL);
+ g_assert_null (adw_toast_overlay_get_child (toast_overlay));
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (toast_overlay);
+}
+
+static void
+test_adw_toast_overlay_add_toast (void)
+{
+ AdwToastOverlay *toast_overlay = g_object_ref_sink (ADW_TOAST_OVERLAY (adw_toast_overlay_new ()));
+ AdwToast *toast = adw_toast_new ("Test notification");
+
+ g_assert_nonnull (toast_overlay);
+ g_assert_nonnull (toast);
+
+ adw_toast_overlay_add_toast (toast_overlay, g_object_ref (toast));
+
+ g_assert_finalize_object (toast_overlay);
+ g_assert_finalize_object (toast);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ adw_init ();
+
+ g_test_add_func ("/Adwaita/ToastOverlay/child", test_adw_toast_overlay_child);
+ g_test_add_func ("/Adwaita/ToastOverlay/add_toast", test_adw_toast_overlay_add_toast);
+
+ return g_test_run ();
+}
diff --git a/tests/test-toast.c b/tests/test-toast.c
new file mode 100644
index 0000000000000000000000000000000000000000..0ce6364b90ced23fa047bc90a15b263f0b903170
--- /dev/null
+++ b/tests/test-toast.c
@@ -0,0 +1,205 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko
+ */
+
+#include
+
+int notified;
+
+static void
+notify_cb (GtkWidget *widget, gpointer data)
+{
+ notified++;
+}
+
+static void
+test_adw_toast_title (void)
+{
+ AdwToast *toast = adw_toast_new ("Title");
+ g_autofree char *title = NULL;
+
+ g_assert_nonnull (toast);
+
+ notified = 0;
+ g_signal_connect (toast, "notify::title", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (toast, "title", &title, NULL);
+ g_assert_cmpstr (title, ==, "Title");
+
+ adw_toast_set_title (toast, "Another title");
+ g_assert_cmpstr (adw_toast_get_title (toast), ==, "Another title");
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (toast, "title", "Title", NULL);
+ g_assert_cmpstr (adw_toast_get_title (toast), ==, "Title");
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (toast);
+}
+
+static void
+test_adw_toast_button_label (void)
+{
+ AdwToast *toast = adw_toast_new ("Title");
+ char *button_label;
+
+ g_assert_nonnull (toast);
+
+ notified = 0;
+ g_signal_connect (toast, "notify::button-label", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (toast, "button-label", &button_label, NULL);
+ g_assert_null (button_label);
+
+ adw_toast_set_button_label (toast, "Button");
+ g_assert_cmpstr (adw_toast_get_button_label (toast), ==, "Button");
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (toast, "button-label", "Button 2", NULL);
+ g_assert_cmpstr (adw_toast_get_button_label (toast), ==, "Button 2");
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (toast);
+}
+
+static void
+test_adw_toast_action_name (void)
+{
+ AdwToast *toast = adw_toast_new ("Title");
+ char *action_name;
+
+ g_assert_nonnull (toast);
+
+ notified = 0;
+ g_signal_connect (toast, "notify::action-name", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (toast, "action-name", &action_name, NULL);
+ g_assert_null (action_name);
+
+ adw_toast_set_action_name (toast, "win.something");
+ g_assert_cmpstr (adw_toast_get_action_name (toast), ==, "win.something");
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (toast, "action-name", "win.something-else", NULL);
+ g_assert_cmpstr (adw_toast_get_action_name (toast), ==, "win.something-else");
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (toast);
+}
+
+static void
+test_adw_toast_action_target (void)
+{
+ AdwToast *toast = adw_toast_new ("Title");
+ GVariant *action_target;
+ g_autoptr (GVariant) variant1 = g_variant_ref_sink (g_variant_new_int32 (1));
+ g_autoptr (GVariant) variant2 = g_variant_ref_sink (g_variant_new_int32 (2));
+ g_autoptr (GVariant) variant3 = g_variant_ref_sink (g_variant_new_int32 (3));
+
+ g_assert_nonnull (toast);
+
+ notified = 0;
+ g_signal_connect (toast, "notify::action-target", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (toast, "action-target", &action_target, NULL);
+ g_assert_null (action_target);
+
+ adw_toast_set_action_target_value (toast, g_variant_new_int32 (1));
+ g_assert_cmpvariant (adw_toast_get_action_target_value (toast), variant1);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (toast, "action-target", g_variant_new_int32 (2), NULL);
+ g_assert_cmpvariant (adw_toast_get_action_target_value (toast), variant2);
+ g_assert_cmpint (notified, ==, 2);
+
+ adw_toast_set_action_target (toast, "i", 3);
+ g_assert_cmpvariant (adw_toast_get_action_target_value (toast), variant3);
+ g_assert_cmpint (notified, ==, 3);
+
+ g_assert_finalize_object (toast);
+}
+
+static void
+test_adw_toast_detailed_action_name (void)
+{
+ AdwToast *toast = adw_toast_new ("Title");
+ g_autoptr (GVariant) variant = g_variant_ref_sink (g_variant_new_int32 (2));
+
+ g_assert_nonnull (toast);
+
+ g_assert_null (adw_toast_get_action_name (toast));
+ g_assert_null (adw_toast_get_action_target_value (toast));
+
+ adw_toast_set_detailed_action_name (toast, "win.something");
+ g_assert_cmpstr (adw_toast_get_action_name (toast), ==, "win.something");
+ g_assert_null (adw_toast_get_action_target_value (toast));
+
+ adw_toast_set_detailed_action_name (toast, "win.something(2)");
+ g_assert_cmpstr (adw_toast_get_action_name (toast), ==, "win.something");
+ g_assert_cmpvariant (adw_toast_get_action_target_value (toast), variant);
+
+ g_assert_finalize_object (toast);
+}
+
+static void
+test_adw_toast_priority (void)
+{
+ AdwToast *toast = adw_toast_new ("Title");
+ AdwToastPriority priority;
+
+ g_assert_nonnull (toast);
+
+ notified = 0;
+ g_signal_connect (toast, "notify::priority", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (toast, "priority", &priority, NULL);
+ g_assert_cmpint (priority, ==, ADW_TOAST_PRIORITY_NORMAL);
+
+ adw_toast_set_priority (toast, ADW_TOAST_PRIORITY_HIGH);
+ g_assert_cmpint (adw_toast_get_priority (toast), ==, ADW_TOAST_PRIORITY_HIGH);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (toast, "priority", ADW_TOAST_PRIORITY_NORMAL, NULL);
+ g_assert_cmpint (adw_toast_get_priority (toast), ==, ADW_TOAST_PRIORITY_NORMAL);
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (toast);
+}
+
+static void
+test_adw_toast_dismiss (void)
+{
+ AdwToast *toast = adw_toast_new ("Title");
+ AdwToastOverlay *overlay = g_object_ref_sink (ADW_TOAST_OVERLAY (adw_toast_overlay_new ()));
+
+ g_assert_nonnull (overlay);
+ g_assert_nonnull (toast);
+
+ adw_toast_overlay_add_toast (overlay, g_object_ref (toast));
+ adw_toast_dismiss (toast);
+
+ g_assert_finalize_object (overlay);
+ g_assert_finalize_object (toast);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ adw_init ();
+
+ g_test_add_func ("/Adwaita/Toast/title", test_adw_toast_title);
+ g_test_add_func ("/Adwaita/Toast/button_label", test_adw_toast_button_label);
+ g_test_add_func ("/Adwaita/Toast/action_name", test_adw_toast_action_name);
+ g_test_add_func ("/Adwaita/Toast/action_target", test_adw_toast_action_target);
+ g_test_add_func ("/Adwaita/Toast/detailed_action_name", test_adw_toast_detailed_action_name);
+ g_test_add_func ("/Adwaita/Toast/priority", test_adw_toast_priority);
+ g_test_add_func ("/Adwaita/Toast/dismiss", test_adw_toast_dismiss);
+
+ return g_test_run ();
+}