diff --git a/demo/adw-demo-window.c b/demo/adw-demo-window.c
index 58fe91ea625ad042301c2d49ad36659805661c62..b90a389f9174794ff90a58a5e12f40aa682f9be9 100644
--- a/demo/adw-demo-window.c
+++ b/demo/adw-demo-window.c
@@ -5,6 +5,7 @@
#include "pages/about/adw-demo-page-about.h"
#include "pages/animations/adw-demo-page-animations.h"
#include "pages/avatar/adw-demo-page-avatar.h"
+#include "pages/banners/adw-demo-page-banners.h"
#include "pages/buttons/adw-demo-page-buttons.h"
#include "pages/carousel/adw-demo-page-carousel.h"
#include "pages/clamp/adw-demo-page-clamp.h"
@@ -119,6 +120,7 @@ adw_demo_window_init (AdwDemoWindow *self)
g_type_ensure (ADW_TYPE_DEMO_PAGE_ABOUT);
g_type_ensure (ADW_TYPE_DEMO_PAGE_ANIMATIONS);
+ g_type_ensure (ADW_TYPE_DEMO_PAGE_BANNERS);
g_type_ensure (ADW_TYPE_DEMO_PAGE_AVATAR);
g_type_ensure (ADW_TYPE_DEMO_PAGE_BUTTONS);
g_type_ensure (ADW_TYPE_DEMO_PAGE_CAROUSEL);
diff --git a/demo/adw-demo-window.ui b/demo/adw-demo-window.ui
index 6cd37fc43da8b372c9756972db742b0ce251522d..2b991b1c7471e9722d264ca135771d8b13e1c813 100644
--- a/demo/adw-demo-window.ui
+++ b/demo/adw-demo-window.ui
@@ -236,6 +236,16 @@
+
+
+
diff --git a/demo/adwaita-demo.gresources.xml b/demo/adwaita-demo.gresources.xml
index f470e937e911eea0553e835544a375c011a39edf..eb839f9b1c45cb6ff205f6317b8036e086c4f2eb 100644
--- a/demo/adwaita-demo.gresources.xml
+++ b/demo/adwaita-demo.gresources.xml
@@ -26,6 +26,7 @@
icons/scalable/actions/view-sidebar-end-symbolic.svg
icons/scalable/actions/view-sidebar-end-symbolic-rtl.svg
icons/scalable/actions/widget-about-symbolic.svg
+ icons/scalable/actions/widget-banner-symbolic.svg
icons/scalable/actions/widget-carousel-symbolic.svg
icons/scalable/actions/widget-clamp-symbolic.svg
icons/scalable/actions/widget-dialog-symbolic.svg
@@ -48,6 +49,7 @@
pages/about/adw-demo-page-about.ui
pages/animations/adw-demo-page-animations.ui
pages/avatar/adw-demo-page-avatar.ui
+ pages/banners/adw-demo-page-banners.ui
pages/buttons/adw-demo-page-buttons.ui
pages/carousel/adw-demo-page-carousel.ui
pages/clamp/adw-demo-page-clamp.ui
diff --git a/demo/icons/scalable/actions/widget-banner-symbolic.svg b/demo/icons/scalable/actions/widget-banner-symbolic.svg
new file mode 100644
index 0000000000000000000000000000000000000000..ccf644625267be5ffafed44bd94f86bcbfbf7702
--- /dev/null
+++ b/demo/icons/scalable/actions/widget-banner-symbolic.svg
@@ -0,0 +1,9 @@
+
+
diff --git a/demo/meson.build b/demo/meson.build
index ee805e78efe973b55453fb87566abcf99bcdbdf6..8127a179117670db15a03287ef28023c1eb89218 100644
--- a/demo/meson.build
+++ b/demo/meson.build
@@ -26,6 +26,7 @@ adwaita_demo_sources = [
'pages/about/adw-demo-page-about.c',
'pages/animations/adw-demo-page-animations.c',
'pages/avatar/adw-demo-page-avatar.c',
+ 'pages/banners/adw-demo-page-banners.c',
'pages/buttons/adw-demo-page-buttons.c',
'pages/carousel/adw-demo-page-carousel.c',
'pages/clamp/adw-demo-page-clamp.c',
diff --git a/demo/pages/banners/adw-demo-page-banners.c b/demo/pages/banners/adw-demo-page-banners.c
new file mode 100644
index 0000000000000000000000000000000000000000..fd1806a0bc10969ce46a21966b8a09a0c2fff874
--- /dev/null
+++ b/demo/pages/banners/adw-demo-page-banners.c
@@ -0,0 +1,67 @@
+#include "adw-demo-page-banners.h"
+
+#include
+
+struct _AdwDemoPageBanners
+{
+ AdwBin parent_instance;
+
+ AdwBanner *banner;
+ AdwEntryRow *button_label_row;
+};
+
+enum {
+ SIGNAL_ADD_TOAST,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+G_DEFINE_TYPE (AdwDemoPageBanners, adw_demo_page_banners, ADW_TYPE_BIN)
+
+static void
+toggle_button_cb (AdwDemoPageBanners *self)
+{
+ if (g_strcmp0 (adw_banner_get_button_label (self->banner), "") == 0) {
+ adw_banner_set_button_label (self->banner, gtk_editable_get_text (GTK_EDITABLE (self->button_label_row)));
+ } else {
+ adw_banner_set_button_label (self->banner, NULL);
+ }
+}
+
+static void
+banner_activate_cb (AdwDemoPageBanners *self)
+{
+ AdwToast *toast = adw_toast_new (_("Banner action triggered"));
+
+ g_signal_emit (self, signals[SIGNAL_ADD_TOAST], 0, toast);
+}
+
+static void
+adw_demo_page_banners_class_init (AdwDemoPageBannersClass *klass)
+{
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ signals[SIGNAL_ADD_TOAST] =
+ g_signal_new ("add-toast",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_FIRST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 1,
+ ADW_TYPE_TOAST);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/org/gnome/Adwaita1/Demo/ui/pages/banners/adw-demo-page-banners.ui");
+ gtk_widget_class_bind_template_child (widget_class, AdwDemoPageBanners, banner);
+ gtk_widget_class_bind_template_child (widget_class, AdwDemoPageBanners, button_label_row);
+
+ gtk_widget_class_install_action (widget_class, "demo.toggle-button", NULL, (GtkWidgetActionActivateFunc) toggle_button_cb);
+ gtk_widget_class_install_action (widget_class, "demo.activate", NULL, (GtkWidgetActionActivateFunc) banner_activate_cb);
+}
+
+static void
+adw_demo_page_banners_init (AdwDemoPageBanners *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
diff --git a/demo/pages/banners/adw-demo-page-banners.h b/demo/pages/banners/adw-demo-page-banners.h
new file mode 100644
index 0000000000000000000000000000000000000000..20787d663c45ebed2ffd3c8f9a01e8d4506e7cae
--- /dev/null
+++ b/demo/pages/banners/adw-demo-page-banners.h
@@ -0,0 +1,11 @@
+#pragma once
+
+#include
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_DEMO_PAGE_BANNERS (adw_demo_page_banners_get_type())
+
+G_DECLARE_FINAL_TYPE (AdwDemoPageBanners, adw_demo_page_banners, ADW, DEMO_PAGE_BANNERS, AdwBin)
+
+G_END_DECLS
diff --git a/demo/pages/banners/adw-demo-page-banners.ui b/demo/pages/banners/adw-demo-page-banners.ui
new file mode 100644
index 0000000000000000000000000000000000000000..e66a7ef743ad049d49f46a8eb0888b85beb8c579
--- /dev/null
+++ b/demo/pages/banners/adw-demo-page-banners.ui
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+ True
+ vertical
+
+
+
+
+
+ demo.activate
+
+
+
+
+ widget-banner-symbolic
+ Banner
+ A bar with contextual information.
+
+
+ 400
+ 300
+
+
+
+
+ Show Banner
+ show_banner_switch
+
+
+ center
+
+
+
+
+
+
+ Title
+ True
+ Metered connection – updates paused
+
+
+
+
+ Button Label
+ True
+ _Network Settings
+
+
+
+ center
+ True
+ demo.toggle-button
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/doc/images/banner-dark.png b/doc/images/banner-dark.png
new file mode 100644
index 0000000000000000000000000000000000000000..795f4d63bdc8f5ac9037e24f1c58ded6717a70ca
Binary files /dev/null and b/doc/images/banner-dark.png differ
diff --git a/doc/images/banner.png b/doc/images/banner.png
new file mode 100644
index 0000000000000000000000000000000000000000..75d4770325734e59ca9bbcd69570bca00abed7f1
Binary files /dev/null and b/doc/images/banner.png differ
diff --git a/doc/tools/data/banner.ui b/doc/tools/data/banner.ui
new file mode 100644
index 0000000000000000000000000000000000000000..bb2f3900bd4e265b2e9f8687198177efd8b3784b
--- /dev/null
+++ b/doc/tools/data/banner.ui
@@ -0,0 +1,25 @@
+
+
+
+
+
+ Banner
+ 600
+ 150
+
+
+ vertical
+
+
+
+
+
+ True
+ Unlock to change settings
+ Unlock
+
+
+
+
+
+
diff --git a/doc/widget-gallery.md b/doc/widget-gallery.md
index 88767e7a7f855b3aafa4e1d64c5e595b73a049c9..4145cf7f79652a5f45508e64ce06c82ba3ea24c3 100644
--- a/doc/widget-gallery.md
+++ b/doc/widget-gallery.md
@@ -19,6 +19,13 @@ Slug: widget-gallery
](class.ToastOverlay.html)
+### Banner
+
+[
+
+
+](class.Banner.html)
+
### Avatar
[
diff --git a/src/adw-banner.c b/src/adw-banner.c
new file mode 100644
index 0000000000000000000000000000000000000000..daeb71f3ca21ef272f24c72076c8b48a51bb14fd
--- /dev/null
+++ b/src/adw-banner.c
@@ -0,0 +1,652 @@
+/*
+ * Copyright (C) 2022 Jamie Murphy
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include "config.h"
+#include "adw-banner.h"
+#include "adw-gizmo-private.h"
+
+#include "adw-macros-private.h"
+
+#define SPACING 6
+#define LABEL_MAX_WIDTH 500
+#define BUTTON_MAX_WIDTH 160
+
+/**
+ * AdwBanner:
+ *
+ * A bar with contextual information.
+ *
+ *
+ *
+ *
+ *
+ *
+ * Banners are hidden by default, use [property@Banner:revealed] to show them.
+ *
+ * Banners have a title, set with [property@Banner:title]. Titles can be marked
+ * up with Pango markup, use [property@Banner:use-markup] to enable it.
+ *
+ * Title can be shown centered or left-aligned depending on available space.
+ *
+ * Banners can optionally have a button with text on it, set through
+ * [property@Banner:button-label]. The button can be used with a `GAction`,
+ * or with the [signal@Banner::button-clicked] signal.
+ *
+ * ## CSS nodes
+ *
+ * `AdwBanner` has a main CSS node with the name `banner`.
+ *
+ * Since: 1.3
+ */
+
+struct _AdwBanner
+{
+ GtkWidget parent_instance;
+
+ AdwGizmo *gizmo;
+ GtkLabel *title;
+ GtkRevealer *revealer;
+ GtkButton *button;
+};
+
+static void adw_banner_actionable_init (GtkActionableInterface *iface);
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (AdwBanner, adw_banner, GTK_TYPE_WIDGET,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTIONABLE, adw_banner_actionable_init))
+
+enum {
+ PROP_0,
+ PROP_TITLE,
+ PROP_BUTTON_LABEL,
+ PROP_REVEALED,
+ PROP_USE_MARKUP,
+
+ /* Actionable properties */
+ PROP_ACTION_NAME,
+ PROP_ACTION_TARGET,
+ LAST_PROP = PROP_ACTION_NAME,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_BUTTON_CLICKED,
+ SIGNAL_LAST_SIGNAL
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+button_clicked (AdwBanner *self)
+{
+ g_assert (ADW_IS_BANNER (self));
+
+ g_signal_emit (self, signals[SIGNAL_BUTTON_CLICKED], 0);
+}
+
+static void
+adw_banner_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ AdwBanner *self = ADW_BANNER (object);
+
+ switch (prop_id) {
+ case PROP_TITLE:
+ g_value_set_string (value, adw_banner_get_title (self));
+ break;
+ case PROP_BUTTON_LABEL:
+ g_value_set_string (value, adw_banner_get_button_label (self));
+ break;
+ case PROP_REVEALED:
+ g_value_set_boolean (value, adw_banner_get_revealed (self));
+ break;
+ case PROP_USE_MARKUP:
+ g_value_set_boolean (value, adw_banner_get_use_markup (self));
+ break;
+ case PROP_ACTION_NAME:
+ g_value_set_string (value, gtk_actionable_get_action_name (GTK_ACTIONABLE (self)));
+ break;
+ case PROP_ACTION_TARGET:
+ g_value_set_variant (value, gtk_actionable_get_action_target_value (GTK_ACTIONABLE (self)));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+adw_banner_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ AdwBanner *self = ADW_BANNER (object);
+
+ switch (prop_id) {
+ case PROP_TITLE:
+ adw_banner_set_title (self, g_value_get_string (value));
+ break;
+ case PROP_BUTTON_LABEL:
+ adw_banner_set_button_label (self, g_value_get_string (value));
+ break;
+ case PROP_REVEALED:
+ adw_banner_set_revealed (self, g_value_get_boolean (value));
+ break;
+ case PROP_USE_MARKUP:
+ adw_banner_set_use_markup (self, g_value_get_boolean (value));
+ break;
+ case PROP_ACTION_NAME:
+ gtk_actionable_set_action_name (GTK_ACTIONABLE (self), g_value_get_string (value));
+ break;
+ case PROP_ACTION_TARGET:
+ gtk_actionable_set_action_target_value (GTK_ACTIONABLE (self), g_value_get_variant (value));
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+adw_banner_dispose (GObject *object)
+{
+ AdwBanner *self = ADW_BANNER (object);
+
+ gtk_widget_dispose_template (GTK_WIDGET (self), ADW_TYPE_BANNER);
+
+ G_OBJECT_CLASS (adw_banner_parent_class)->dispose (object);
+}
+
+static void
+measure_content (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ AdwBanner *self = ADW_BANNER (gtk_widget_get_ancestor (widget, ADW_TYPE_BANNER));
+ gboolean button_shown = gtk_widget_is_visible (GTK_WIDGET (self->button));
+ int label_min, label_nat;
+ int button_min, button_nat;
+ int min = 0, nat = 0;
+
+ gtk_widget_measure (GTK_WIDGET (self->title), orientation, for_size,
+ &label_min, &label_nat, NULL, NULL);
+ gtk_widget_measure (GTK_WIDGET (self->button), orientation, for_size,
+ &button_min, &button_nat, NULL, NULL);
+
+ if (orientation == GTK_ORIENTATION_VERTICAL) {
+ if (button_shown) {
+ if (for_size > 0) {
+ int label_width_nat, button_width_min;
+ int avail;
+
+ gtk_widget_measure (GTK_WIDGET (self->title), GTK_ORIENTATION_HORIZONTAL, -1,
+ NULL, &label_width_nat, NULL, NULL);
+ gtk_widget_measure (GTK_WIDGET (self->button), GTK_ORIENTATION_HORIZONTAL, -1,
+ &button_width_min, NULL, NULL, NULL);
+
+ avail = (for_size - MIN (label_width_nat, for_size));
+
+ if (avail <= button_width_min + SPACING) {
+ min = label_min + SPACING + button_min;
+ nat = label_nat + SPACING + button_nat;
+ } else {
+ min = MAX (label_min, button_min);
+ nat = MAX (label_nat, button_nat);
+ }
+ } else {
+ min = MAX (label_min, button_min);
+ nat = MAX (label_nat, button_nat);
+ }
+ } else {
+ min = label_min;
+ nat = label_nat;
+ }
+ } else {
+ if (button_shown) {
+ min = MAX (label_min + SPACING + button_min, BUTTON_MAX_WIDTH);
+ nat = MAX (label_nat + SPACING + button_nat, BUTTON_MAX_WIDTH);
+ } else {
+ min = label_min;
+ nat = label_nat;
+ }
+ }
+
+ if (minimum)
+ *minimum = min;
+ if (natural)
+ *natural = nat;
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+ if (natural_baseline)
+ *natural_baseline = -1;
+}
+
+static void
+allocate_content (GtkWidget *widget,
+ int width,
+ int height,
+ int baseline)
+{
+ AdwBanner *self = ADW_BANNER (gtk_widget_get_ancestor (widget, ADW_TYPE_BANNER));
+ gboolean button_shown = gtk_widget_is_visible (GTK_WIDGET (self->button));
+ int button_width, button_height;
+ int button_x, button_y;
+ int label_width_min, label_width, label_height;
+ int label_x, label_y;
+ int avail;
+ gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+ gtk_widget_measure (GTK_WIDGET (self->title), GTK_ORIENTATION_HORIZONTAL,
+ -1, &label_width_min, &label_width,
+ NULL, NULL);
+ gtk_widget_measure (GTK_WIDGET (self->button), GTK_ORIENTATION_HORIZONTAL,
+ -1, &button_width, NULL,
+ NULL, NULL);
+ gtk_widget_measure (GTK_WIDGET (self->title), GTK_ORIENTATION_VERTICAL,
+ width, NULL, &label_height,
+ NULL, NULL);
+ gtk_widget_measure (GTK_WIDGET (self->button), GTK_ORIENTATION_VERTICAL,
+ width, &button_height, NULL,
+ NULL, NULL);
+
+ label_width = MIN (label_width, width);
+ label_x = (width / 2) - (label_width / 2);
+ label_y = (height / 2) - (label_height / 2);
+ button_x = is_rtl ? 0 : width - button_width;
+ button_y = (height / 2) - (button_height / 2);
+
+ avail = (width - label_width) / 2;
+ if (avail <= button_width + SPACING && button_shown) {
+ label_x = is_rtl ? (width - label_width - SPACING) : SPACING;
+
+ avail = (width - label_width);
+ if (avail <= button_width + SPACING) {
+ button_width = CLAMP (button_width, BUTTON_MAX_WIDTH, width);
+ label_x = (width - label_width) / 2;
+ label_y = 0;
+ button_x = (width / 2) - (button_width / 2);
+ button_y = height - button_height;
+ }
+ }
+
+ gtk_widget_allocate (GTK_WIDGET (self->title),
+ label_width, label_height, -1,
+ gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (label_x, label_y)));
+
+ gtk_widget_allocate (GTK_WIDGET (self->button),
+ button_width, button_height, -1,
+ gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (button_x, button_y)));
+}
+
+static GtkSizeRequestMode
+get_content_request_mode (GtkWidget *widget)
+{
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static void
+adw_banner_class_init (AdwBannerClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->get_property = adw_banner_get_property;
+ object_class->set_property = adw_banner_set_property;
+ object_class->dispose = adw_banner_dispose;
+
+ /**
+ * AdwBanner:title: (attributes org.gtk.Property.get=adw_banner_get_title org.gtk.Property.set=adw_banner_set_title)
+ *
+ * The title for this banner.
+ *
+ * See also: [property@Banner:use-markup].
+ *
+ * Since: 1.3
+ */
+ props[PROP_TITLE] =
+ g_param_spec_string ("title", NULL, NULL,
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwBanner:button-label: (attributes org.gtk.Property.get=adw_banner_get_button_label org.gtk.Property.set=adw_banner_set_button_label)
+ *
+ * The label to show on the button.
+ *
+ * If set to `""` or `NULL`, the button won't be shown.
+ *
+ * The button can be used with a `GAction`, or with the
+ * [signal@Banner::button-clicked] signal.
+ *
+ * Since: 1.3
+ */
+ props[PROP_BUTTON_LABEL] =
+ g_param_spec_string ("button-label", NULL, NULL,
+ "",
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwBanner:use-markup: (attributes org.gtk.Property.get=adw_banner_get_use_markup org.gtk.Property.set=adw_banner_set_use_markup)
+ *
+ * Whether to use Pango markup for the banner title.
+ *
+ * See also [func@Pango.parse_markup].
+ *
+ * Since: 1.3
+ */
+ props[PROP_USE_MARKUP] =
+ g_param_spec_boolean ("use-markup", NULL, NULL,
+ TRUE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwBanner:revealed: (attributes org.gtk.Property.get=adw_banner_get_revealed org.gtk.Property.set=adw_banner_set_revealed)
+ *
+ * Whether the banner is currently revealed.
+ *
+ * Since: 1.3
+ */
+ props[PROP_REVEALED] =
+ g_param_spec_boolean ("revealed", NULL, NULL,
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwBanner::button-clicked:
+ *
+ * This signal is emitted after the action button has been clicked.
+ *
+ * It can be used as an alternative to setting an action.
+ *
+ * Since: 1.3
+ */
+ signals[SIGNAL_BUTTON_CLICKED] =
+ g_signal_new ("button-clicked",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+ g_object_class_override_property (object_class, PROP_ACTION_NAME, "action-name");
+ g_object_class_override_property (object_class, PROP_ACTION_TARGET, "action-target");
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/org/gnome/Adwaita/ui/adw-banner.ui");
+ gtk_widget_class_bind_template_child (widget_class, AdwBanner, gizmo);
+ gtk_widget_class_bind_template_child (widget_class, AdwBanner, title);
+ gtk_widget_class_bind_template_child (widget_class, AdwBanner, revealer);
+ gtk_widget_class_bind_template_child (widget_class, AdwBanner, button);
+
+ gtk_widget_class_bind_template_callback (widget_class, button_clicked);
+
+ gtk_widget_class_set_css_name (widget_class, "banner");
+ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+ gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_GROUP);
+
+ g_type_ensure (ADW_TYPE_GIZMO);
+}
+
+static const char *
+adw_banner_get_action_name (GtkActionable *actionable)
+{
+ AdwBanner *self = ADW_BANNER (actionable);
+
+ return gtk_actionable_get_action_name (GTK_ACTIONABLE (self->button));
+}
+
+static void
+adw_banner_set_action_name (GtkActionable *actionable,
+ const char *action_name)
+{
+ AdwBanner *self = ADW_BANNER (actionable);
+
+ gtk_actionable_set_action_name (GTK_ACTIONABLE (self->button), action_name);
+}
+
+static GVariant *
+adw_banner_get_action_target_value (GtkActionable *actionable)
+{
+ AdwBanner *self = ADW_BANNER (actionable);
+
+ return gtk_actionable_get_action_target_value (GTK_ACTIONABLE (self->button));
+}
+
+static void
+adw_banner_set_action_target_value (GtkActionable *actionable,
+ GVariant *action_target)
+{
+ AdwBanner *self = ADW_BANNER (actionable);
+
+ gtk_actionable_set_action_target_value (GTK_ACTIONABLE (self->button), action_target);
+}
+
+static void
+adw_banner_actionable_init (GtkActionableInterface *iface)
+{
+ iface->get_action_name = adw_banner_get_action_name;
+ iface->set_action_name = adw_banner_set_action_name;
+ iface->get_action_target_value = adw_banner_get_action_target_value;
+ iface->set_action_target_value = adw_banner_set_action_target_value;
+}
+
+static void
+adw_banner_init (AdwBanner *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+ gtk_widget_set_layout_manager (GTK_WIDGET (self->gizmo), gtk_custom_layout_new (get_content_request_mode,
+ measure_content,
+ allocate_content));
+}
+
+/**
+ * adw_banner_new:
+ * @title: the banner title
+ *
+ * Creates a new `AdwBanner`.
+ *
+ * Returns: the newly created `AdwBanner`
+ *
+ * Since: 1.3
+ */
+GtkWidget *
+adw_banner_new (const char *title)
+{
+ g_return_val_if_fail (title != NULL, NULL);
+
+ return g_object_new (ADW_TYPE_BANNER,
+ "title", title,
+ NULL);
+}
+
+/**
+ * adw_banner_get_title: (attributes org.gtk.Method.get_property=title)
+ * @self: a banner
+ *
+ * Gets the title for @self.
+ *
+ * Returns: the title for @self
+ *
+ * Since: 1.3
+ */
+const char *
+adw_banner_get_title (AdwBanner *self)
+{
+ g_return_val_if_fail (ADW_IS_BANNER (self), NULL);
+
+ return gtk_label_get_label (self->title);
+}
+
+/**
+ * adw_banner_set_title: (attributes org.gtk.Method.set_property=title)
+ * @self: a banner
+ * @title: the title
+ *
+ * Sets the title for this banner.
+ *
+ * See also: [property@Banner:use-markup].
+ *
+ * Since: 1.3
+ */
+void
+adw_banner_set_title (AdwBanner *self,
+ const char *title)
+{
+ g_return_if_fail (ADW_IS_BANNER (self));
+ g_return_if_fail (title != NULL);
+
+ if (g_strcmp0 (gtk_label_get_label (self->title), title) == 0)
+ return;
+
+ gtk_label_set_label (self->title, title);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]);
+}
+
+/**
+ * adw_banner_get_button_label: (attributes org.gtk.Method.get_property=button-label)
+ * @self: a banner
+ *
+ * Gets the button label for @self.
+ *
+ * Returns: (nullable): the button label for @self
+ *
+ * Since: 1.3
+ */
+const char *
+adw_banner_get_button_label (AdwBanner *self)
+{
+ g_return_val_if_fail (ADW_IS_BANNER (self), NULL);
+
+ return gtk_button_get_label (self->button);
+}
+
+/**
+ * adw_banner_set_button_label: (attributes org.gtk.Method.set_property=button-label)
+ * @self: a banner
+ * @label: (nullable): the label
+ *
+ * Sets the button label for @self.
+ *
+ * If set to `""` or `NULL`, the button won't be shown.
+ *
+ * The button can be used with a `GAction`, or with the
+ * [signal@Banner::button-clicked] signal.
+ *
+ * Since: 1.3
+ */
+void
+adw_banner_set_button_label (AdwBanner *self,
+ const char *label)
+{
+ g_return_if_fail (ADW_IS_BANNER (self));
+
+ if (g_strcmp0 (gtk_button_get_label (self->button), label) == 0)
+ return;
+
+ gtk_widget_set_visible (GTK_WIDGET (self->button), label && label[0]);
+
+ gtk_button_set_label (self->button, label);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_BUTTON_LABEL]);
+}
+
+/**
+ * adw_banner_get_use_markup: (attributes org.gtk.Method.get_property=use-markup)
+ * @self: a banner
+ *
+ * Gets whether to use Pango markup for the banner title.
+ *
+ * Returns: whether to use markup
+ *
+ * Since: 1.3
+ */
+gboolean
+adw_banner_get_use_markup (AdwBanner *self)
+{
+ g_return_val_if_fail (ADW_IS_BANNER (self), FALSE);
+
+ return gtk_label_get_use_markup (self->title);
+}
+
+/**
+ * adw_banner_set_use_markup: (attributes org.gtk.Method.set_property=use-markup)
+ * @self: a banner
+ * @use_markup: whether to use markup
+ *
+ * Sets whether to use Pango markup for the banner title.
+ *
+ * See also [func@Pango.parse_markup].
+ *
+ * Since: 1.3
+ */
+void
+adw_banner_set_use_markup (AdwBanner *self,
+ gboolean use_markup)
+{
+ g_return_if_fail (ADW_IS_BANNER (self));
+
+ use_markup = !!use_markup;
+
+ if (gtk_label_get_use_markup (self->title) == use_markup)
+ return;
+
+ gtk_label_set_use_markup (self->title, use_markup);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_USE_MARKUP]);
+}
+
+/**
+ * adw_banner_get_revealed: (attributes org.gtk.Method.get_property=revealed)
+ * @self: a banner
+ *
+ * Gets if a banner is revealed
+ *
+ * Returns: Whether a banner is revealed
+ *
+ * Since: 1.3
+ */
+gboolean
+adw_banner_get_revealed (AdwBanner *self)
+{
+ g_return_val_if_fail (ADW_IS_BANNER (self), FALSE);
+
+ return gtk_revealer_get_reveal_child (GTK_REVEALER (self->revealer));
+}
+
+/**
+ * adw_banner_set_revealed: (attributes org.gtk.Method.get_property=revealed)
+ * @self: a banner
+ * @revealed: whether a banner should be revealed
+ *
+ * Sets whether a banner should be revealed
+ *
+ * Since: 1.3
+ */
+void
+adw_banner_set_revealed (AdwBanner *self,
+ gboolean revealed)
+{
+ g_return_if_fail (ADW_IS_BANNER (self));
+
+ revealed = !!revealed;
+
+ if (gtk_revealer_get_reveal_child (GTK_REVEALER (self->revealer)) == revealed)
+ return;
+
+ gtk_revealer_set_reveal_child (GTK_REVEALER (self->revealer), revealed);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVEALED]);
+}
diff --git a/src/adw-banner.h b/src/adw-banner.h
new file mode 100644
index 0000000000000000000000000000000000000000..57525a2cdc6398aa88ae3789945f2b0e57fd4f14
--- /dev/null
+++ b/src/adw-banner.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2022 Jamie Murphy
+ *
+ * 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
+
+#define ADW_TYPE_BANNER (adw_banner_get_type())
+
+ADW_AVAILABLE_IN_1_3
+G_DECLARE_FINAL_TYPE (AdwBanner, adw_banner, ADW, BANNER, GtkWidget)
+
+ADW_AVAILABLE_IN_1_3
+GtkWidget *adw_banner_new (const char *title) G_GNUC_WARN_UNUSED_RESULT;
+
+ADW_AVAILABLE_IN_1_3
+const char *adw_banner_get_title (AdwBanner *self);
+ADW_AVAILABLE_IN_1_3
+void adw_banner_set_title (AdwBanner *self,
+ const char *title);
+
+ADW_AVAILABLE_IN_1_3
+const char *adw_banner_get_button_label (AdwBanner *self);
+ADW_AVAILABLE_IN_1_3
+void adw_banner_set_button_label (AdwBanner *self,
+ const char *label);
+
+ADW_AVAILABLE_IN_1_3
+gboolean adw_banner_get_revealed (AdwBanner *self);
+ADW_AVAILABLE_IN_1_3
+void adw_banner_set_revealed (AdwBanner *self,
+ gboolean revealed);
+
+ADW_AVAILABLE_IN_1_3
+gboolean adw_banner_get_use_markup (AdwBanner *self);
+ADW_AVAILABLE_IN_1_3
+void adw_banner_set_use_markup (AdwBanner *self,
+ gboolean use_markup);
+
+G_END_DECLS
diff --git a/src/adw-banner.ui b/src/adw-banner.ui
new file mode 100644
index 0000000000000000000000000000000000000000..04e7c615a3ec0c4e3948cb91692eed9ff94dcd3b
--- /dev/null
+++ b/src/adw-banner.ui
@@ -0,0 +1,42 @@
+
+
+
+
+
+ title
+
+
+
+ slide-down
+
+
+ True
+
+
+ True
+ center
+ end
+ 3
+ False
+ False
+ True
+ word-char
+
+
+
+
+
+ center
+ False
+ True
+
+
+
+
+
+
+
+
+
diff --git a/src/adwaita.gresources.xml b/src/adwaita.gresources.xml
index 159fc0e7238dcf6bfd832d1748f8e3e90e5bf37d..4169e278f1ec2cbca2481046ebcabeb50ff109a5 100644
--- a/src/adwaita.gresources.xml
+++ b/src/adwaita.gresources.xml
@@ -16,6 +16,7 @@
adw-about-window.ui
adw-action-row.ui
+ adw-banner.ui
adw-combo-row.ui
adw-entry-row.ui
adw-expander-row.ui
diff --git a/src/adwaita.h b/src/adwaita.h
index b70b2f29da51f7b4013f6ff3c39b1c99ba51e153..4524b8b274f399df6dd399286679416bd19b6bc1 100644
--- a/src/adwaita.h
+++ b/src/adwaita.h
@@ -29,6 +29,7 @@ G_BEGIN_DECLS
#include "adw-application.h"
#include "adw-application-window.h"
#include "adw-avatar.h"
+#include "adw-banner.h"
#include "adw-bin.h"
#include "adw-button-content.h"
#include "adw-carousel.h"
diff --git a/src/meson.build b/src/meson.build
index 4701a2cd40d74e1b2100f1a1623bf40945c32f55..1c8e7234494f321f1abe7c9502b4bdde4c9ea31b 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -11,6 +11,7 @@ libadwaita_resources = gnome.compile_resources(
adw_public_enum_headers = [
'adw-animation.h',
+ 'adw-banner.h',
'adw-flap.h',
'adw-fold-threshold-policy.h',
'adw-easing.h',
@@ -90,6 +91,7 @@ src_headers = [
'adw-application.h',
'adw-application-window.h',
'adw-avatar.h',
+ 'adw-banner.h',
'adw-bin.h',
'adw-button-content.h',
'adw-carousel.h',
@@ -156,6 +158,7 @@ src_sources = [
'adw-application.c',
'adw-application-window.c',
'adw-avatar.c',
+ 'adw-banner.c',
'adw-bin.c',
'adw-button-content.c',
'adw-carousel.c',
diff --git a/src/stylesheet/widgets/_misc.scss b/src/stylesheet/widgets/_misc.scss
index d3c30ee22b8515b81a4469e956ba3524b14568af..8d4465f91ae71aa49aaf4c2ce6e26e1bc31bb4ce 100644
--- a/src/stylesheet/widgets/_misc.scss
+++ b/src/stylesheet/widgets/_misc.scss
@@ -1,3 +1,15 @@
+/***********
+ * Banners *
+ ***********/
+banner {
+ background-color: gtkmix($accent_bg_color, $window_bg_color, 30%);
+ color: $window_fg_color;
+
+ > revealer > widget {
+ padding: 6px;
+ }
+}
+
/**********
* Frames *
**********/
diff --git a/tests/meson.build b/tests/meson.build
index 393719ee91e6865d2a497b02f9a781d05ccc406f..f1dbfe9b9a143254539d471c0651d5ec09c73d53 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -32,6 +32,7 @@ test_names = [
'test-animation-target',
'test-application-window',
'test-avatar',
+ 'test-banner',
'test-bin',
'test-button-content',
'test-carousel',
diff --git a/tests/test-banner.c b/tests/test-banner.c
new file mode 100644
index 0000000000000000000000000000000000000000..53676c847fdfb219e12ef5113800481b33fd5da6
--- /dev/null
+++ b/tests/test-banner.c
@@ -0,0 +1,81 @@
+/*
+ * Copyright (C) 2022 Jamie Murphy
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ */
+
+#include
+
+static void
+test_adw_banner_revealed (void)
+{
+ AdwBanner *banner = g_object_ref_sink (ADW_BANNER (adw_banner_new ("")));
+
+ g_assert_nonnull (banner);
+
+ g_assert_false (adw_banner_get_revealed (banner));
+
+ adw_banner_set_revealed (banner, TRUE);
+ g_assert_true (adw_banner_get_revealed (banner));
+
+ adw_banner_set_revealed (banner, FALSE);
+ g_assert_false (adw_banner_get_revealed (banner));
+
+ g_assert_finalize_object (banner);
+}
+
+static void
+test_adw_banner_title (void)
+{
+ AdwBanner *banner = g_object_ref_sink (ADW_BANNER (adw_banner_new ("")));
+
+ g_assert_nonnull (banner);
+
+ g_assert_cmpstr (adw_banner_get_title (banner), ==, "");
+
+ adw_banner_set_title (banner, "Dummy title");
+ g_assert_cmpstr (adw_banner_get_title (banner), ==, "Dummy title");
+
+ adw_banner_set_use_markup (banner, FALSE);
+ adw_banner_set_title (banner, "Invalid markup");
+ g_assert_cmpstr (adw_banner_get_title (banner), ==, "Invalid markup");
+
+ g_assert_finalize_object (banner);
+}
+
+static void
+test_adw_banner_button_label (void)
+{
+ AdwBanner *banner = g_object_ref_sink (ADW_BANNER (adw_banner_new ("")));
+ char *button_label;
+
+ g_assert_nonnull (banner);
+
+ g_object_get (banner, "button-label", &button_label, NULL);
+ g_assert_null (button_label);
+
+ adw_banner_set_button_label (banner, "Dummy label");
+ g_assert_cmpstr (adw_banner_get_button_label (banner), ==, "Dummy label");
+
+ adw_banner_set_button_label (banner, NULL);
+ g_assert_cmpstr (adw_banner_get_button_label (banner), ==, "");
+
+ g_object_set (banner, "button-label", "Button 2", NULL);
+ g_assert_cmpstr (adw_banner_get_button_label (banner), ==, "Button 2");
+
+ g_assert_finalize_object (banner);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ adw_init ();
+
+ g_test_add_func ("/Adwaita/Banner/revealed", test_adw_banner_revealed);
+ g_test_add_func ("/Adwaita/Banner/title", test_adw_banner_title);
+ g_test_add_func ("/Adwaita/Banner/button_label", test_adw_banner_button_label);
+
+ return g_test_run ();
+}