[libadwaita/wip/exalm/tab-overview: 9/16] Add AdwTabButton




commit bafb3459c3e6acfdf8bf92afcc6b5dc921e313a8
Author: Alexander Mikhaylenko <alexm gnome org>
Date:   Thu Aug 19 18:42:03 2021 +0500

    Add AdwTabButton

 doc/images/tab-button-dark.png                     | Bin 0 -> 831 bytes
 doc/images/tab-button.png                          | Bin 0 -> 841 bytes
 doc/libadwaita.toml.in                             |   2 +
 doc/tools/data/tab-button.ui                       | 151 ++++++
 doc/visual-index.md                                |   7 +-
 src/adw-tab-button.c                               | 524 +++++++++++++++++++++
 src/adw-tab-button.h                               |  36 ++
 src/adw-tab-button.ui                              |  37 ++
 src/adw-tab-view.c                                 |  10 +-
 src/adwaita.gresources.xml                         |   3 +
 src/adwaita.h                                      |   1 +
 .../scalable/status/adw-tab-counter-symbolic.svg   |   1 +
 .../scalable/status/adw-tab-overflow-symbolic.svg  |   1 +
 src/meson.build                                    |   2 +
 src/stylesheet/widgets/_buttons.scss               |  16 +
 src/stylesheet/widgets/_linked.scss                |   1 +
 tests/meson.build                                  |   1 +
 tests/test-tab-button.c                            |  59 +++
 18 files changed, 849 insertions(+), 3 deletions(-)
---
diff --git a/doc/images/tab-button-dark.png b/doc/images/tab-button-dark.png
new file mode 100644
index 00000000..d6aed38f
Binary files /dev/null and b/doc/images/tab-button-dark.png differ
diff --git a/doc/images/tab-button.png b/doc/images/tab-button.png
new file mode 100644
index 00000000..85d5ac0b
Binary files /dev/null and b/doc/images/tab-button.png differ
diff --git a/doc/libadwaita.toml.in b/doc/libadwaita.toml.in
index 27de1b24..d75d00fd 100644
--- a/doc/libadwaita.toml.in
+++ b/doc/libadwaita.toml.in
@@ -198,6 +198,8 @@ content_images = [
   "images/tab-bar-dark.png",
   "images/tab-bar-inline.png",
   "images/tab-bar-inline-dark.png",
+  "images/tab-button.png",
+  "images/tab-button-dark.png",
   "images/toast-action.png",
   "images/toast-action-dark.png",
   "images/toast-overlay.png",
diff --git a/doc/tools/data/tab-button.ui b/doc/tools/data/tab-button.ui
new file mode 100644
index 00000000..b20057fb
--- /dev/null
+++ b/doc/tools/data/tab-button.ui
@@ -0,0 +1,151 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <requires lib="libadwaita" version="1.0"/>
+  <object class="GtkBox" id="widget">
+    <property name="spacing">6</property>
+    <child>
+      <object class="AdwTabButton">
+        <property name="view">view</property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabButton">
+        <property name="view">view2</property>
+      </object>
+    </child>
+  </object>
+  <object class="AdwTabView" id="view">
+    <property name="vexpand">True</property>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+  </object>
+  <object class="AdwTabView" id="view2">
+    <property name="vexpand">True</property>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="needs-attention">True</property>
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+    <child>
+      <object class="AdwTabPage">
+        <property name="child">
+          <object class="AdwBin"/>
+        </property>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/doc/visual-index.md b/doc/visual-index.md
index 97079e66..cd115e6c 100644
--- a/doc/visual-index.md
+++ b/doc/visual-index.md
@@ -133,13 +133,18 @@ Slug: visual-index
   <img src="view-switcher-bar.png" alt="view-switcher-bar">
 </picture>](class.ViewSwitcherBar.html)
 
-### Tab Bar
+### Tabs
 
 [<picture>
   <source srcset="tab-bar-dark.png" media="(prefers-color-scheme: dark)">
   <img src="tab-bar.png" alt="tab-bar">
 </picture>](class.TabBar.html)
 
+[<picture>
+  <source srcset="tab-button-dark.png" media="(prefers-color-scheme: dark)">
+  <img src="tab-button.png" alt="tab-button">
+</picture>](class.TabButton.html)
+
 ## Adaptive Containers
 
 ### Clamp
diff --git a/src/adw-tab-button.c b/src/adw-tab-button.c
new file mode 100644
index 00000000..2024cf89
--- /dev/null
+++ b/src/adw-tab-button.c
@@ -0,0 +1,524 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659 gmail com>
+ * Copyright (C) 2021-2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "adw-tab-button.h"
+
+#include "adw-indicator-bin-private.h"
+#include "adw-macros-private.h"
+
+/* Copied from GtkInspector code */
+#define XFT_DPI_MULTIPLIER (96.0 * PANGO_SCALE)
+
+/**
+ * AdwTabButton:
+ *
+ * A button that displays the number of [class@TabView] pages.
+ *
+ * <picture>
+ *   <source srcset="tab-button-dark.png" media="(prefers-color-scheme: dark)">
+ *   <img src="tab-button.png" alt="tab-button">
+ * </picture>
+ *
+ * `AdwTabButton` is a button that displays the number of pages in a given
+ * `AdwTabView`, as well as whether one of the inactive pages needs attention.
+ *
+ * It's intended to be used as a visible indicator when there's no visible tab
+ * bar, typically opening an [class@TabOverview] on click, e.g. via the
+ * `overview.open` action name:
+ *
+ * ```xml
+ * <object class="AdwTabButton">
+ *   <property name="view">view</property>
+ *   <property name="action-name">overview.open</property>
+ * </object>
+ * ```
+ *
+ * ## CSS nodes
+ *
+ * `AdwTabButton` has a main CSS node with name `tabbutton`.
+ *
+ * # Accessibility
+ *
+ * `AdwTabButton` uses the `GTK_ACCESSIBLE_ROLE_BUTTON` role.
+ *
+ * Since: 1.3
+ */
+
+struct _AdwTabButton
+{
+  GtkWidget parent_instance;
+
+  GtkWidget *button;
+  GtkLabel *label;
+  GtkImage *icon;
+  AdwIndicatorBin *indicator;
+
+  AdwTabView *view;
+
+  int needs_attention;
+};
+
+static void adw_tab_button_actionable_init (GtkActionableInterface *iface);
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (AdwTabButton, adw_tab_button, GTK_TYPE_WIDGET,
+                               G_IMPLEMENT_INTERFACE (GTK_TYPE_ACTIONABLE, adw_tab_button_actionable_init))
+
+enum {
+  PROP_0,
+  PROP_VIEW,
+
+  /* actionable properties */
+  PROP_ACTION_NAME,
+  PROP_ACTION_TARGET,
+  LAST_PROP = PROP_ACTION_NAME
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+  SIGNAL_CLICKED,
+  SIGNAL_ACTIVATE,
+  SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+clicked_cb (AdwTabButton *self)
+{
+  g_signal_emit (self, signals[SIGNAL_CLICKED], 0);
+}
+
+static void
+activate_cb (AdwTabButton *self)
+{
+  g_signal_emit_by_name (self->button, "activate");
+}
+
+static void
+update_label_scale (AdwTabButton *self,
+                    GtkSettings  *settings)
+{
+  int xft_dpi;
+  PangoAttrList *attrs;
+  PangoAttribute *scale_attribute;
+
+  g_object_get (settings, "gtk-xft-dpi", &xft_dpi, NULL);
+
+  attrs = pango_attr_list_new ();
+
+  scale_attribute = pango_attr_scale_new (XFT_DPI_MULTIPLIER / (double) xft_dpi);
+
+  pango_attr_list_change (attrs, scale_attribute);
+
+  gtk_label_set_attributes (self->label, attrs);
+
+  pango_attr_list_unref (attrs);
+}
+
+static void
+xft_dpi_changed (AdwTabButton *self,
+                 GParamSpec   *pspec,
+                 GtkSettings  *settings)
+{
+  update_label_scale (self, settings);
+}
+
+static void
+update_icon (AdwTabButton *self)
+{
+  gboolean display_label = FALSE;
+  gboolean small_label = FALSE;
+  const char *icon_name = "adw-tab-counter-symbolic";
+  char *label_text = NULL;
+
+  if (self->view) {
+    guint n_pages = adw_tab_view_get_n_pages (self->view);
+
+    small_label = n_pages >= 10;
+
+    if (n_pages < 100) {
+      display_label = TRUE;
+      label_text = g_strdup_printf ("%u", n_pages);
+    } else {
+      icon_name = "adw-tab-overflow-symbolic";
+    }
+  }
+
+  if (small_label)
+    gtk_widget_add_css_class (GTK_WIDGET (self->label), "small");
+  else
+    gtk_widget_remove_css_class (GTK_WIDGET (self->label), "small");
+
+  gtk_widget_set_visible (GTK_WIDGET (self->label), display_label);
+  gtk_label_set_text (self->label, label_text);
+  gtk_image_set_from_icon_name (self->icon, icon_name);
+
+  g_free (label_text);
+}
+
+static void
+update_needs_attention (AdwTabButton *self)
+{
+  int needs_attention = self->needs_attention;
+
+  if (self->view) {
+    AdwTabPage *selected_page = adw_tab_view_get_selected_page (self->view);
+
+    if (selected_page && adw_tab_page_get_needs_attention (selected_page))
+      needs_attention--;
+  }
+
+  adw_indicator_bin_set_needs_attention (ADW_INDICATOR_BIN (self->indicator),
+                                         needs_attention > 0);
+}
+
+static void
+notify_needs_attention_cb (AdwTabButton *self,
+                           GParamSpec   *pspec,
+                           AdwTabPage   *page)
+{
+  if (adw_tab_page_get_needs_attention (page))
+    self->needs_attention++;
+  else
+    self->needs_attention--;
+
+  update_needs_attention (self);
+}
+
+static void
+page_attached_cb (AdwTabButton *self,
+                  AdwTabPage   *page)
+{
+  g_signal_connect_object (page, "notify::needs-attention",
+                           G_CALLBACK (notify_needs_attention_cb), self,
+                           G_CONNECT_SWAPPED);
+
+  if (adw_tab_page_get_needs_attention (page))
+    self->needs_attention++;
+
+  update_needs_attention (self);
+}
+
+static void
+page_detached_cb (AdwTabButton *self,
+                  AdwTabPage   *page)
+{
+  g_signal_handlers_disconnect_by_func (page, notify_needs_attention_cb, self);
+
+  if (adw_tab_page_get_needs_attention (page))
+    self->needs_attention--;
+
+  update_needs_attention (self);
+}
+
+static void
+adw_tab_button_dispose (GObject *object)
+{
+  AdwTabButton *self = ADW_TAB_BUTTON (object);
+
+  adw_tab_button_set_view (self, NULL);
+
+  gtk_widget_unparent (GTK_WIDGET (self->button));
+
+  G_OBJECT_CLASS (adw_tab_button_parent_class)->dispose (object);
+}
+
+static void
+adw_tab_button_get_property (GObject    *object,
+                             guint       prop_id,
+                             GValue     *value,
+                             GParamSpec *pspec)
+{
+  AdwTabButton *self = ADW_TAB_BUTTON (object);
+
+  switch (prop_id) {
+  case PROP_VIEW:
+    g_value_set_object (value, adw_tab_button_get_view (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);
+  }
+}
+
+static void
+adw_tab_button_set_property (GObject      *object,
+                             guint         prop_id,
+                             const GValue *value,
+                             GParamSpec   *pspec)
+{
+  AdwTabButton *self = ADW_TAB_BUTTON (object);
+
+  switch (prop_id) {
+  case PROP_VIEW:
+    adw_tab_button_set_view (self, g_value_get_object (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);
+  }
+}
+
+static void
+adw_tab_button_class_init (AdwTabButtonClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = adw_tab_button_dispose;
+  object_class->get_property = adw_tab_button_get_property;
+  object_class->set_property = adw_tab_button_set_property;
+
+  /**
+   * AdwTabButton:view: (attributes org.gtk.Property.get=adw_tab_button_get_view 
org.gtk.Property.set=adw_tab_button_set_view)
+   *
+   * The view the tab button displays.
+   *
+   * Since: 1.3
+   */
+  props[PROP_VIEW] =
+    g_param_spec_object ("view", NULL, NULL,
+                         ADW_TYPE_TAB_VIEW,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  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");
+
+  /**
+   * AdwTabButton::clicked:
+   * @self: the object that received the signal
+   *
+   * Emitted when the button has been activated (pressed and released).
+   *
+   * Since: 1.3
+   */
+  signals[SIGNAL_CLICKED] =
+    g_signal_new ("clicked",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  0);
+
+  /**
+   * AdwTabButton::activate:
+   * @self: the object which received the signal.
+   *
+   * Emitted to animate press then release.
+   *
+   * This is an action signal. Applications should never connect to this signal,
+   * but use the [signal@TabButton::clicked] signal.
+   *
+   * Since: 1.3
+   */
+  signals[SIGNAL_ACTIVATE] =
+    g_signal_new ("activate",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  0);
+
+  gtk_widget_class_set_activate_signal (widget_class, signals[SIGNAL_ACTIVATE]);
+
+  g_signal_override_class_handler ("activate",
+                                   G_TYPE_FROM_CLASS (klass),
+                                   G_CALLBACK (activate_cb));
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/org/gnome/Adwaita/ui/adw-tab-button.ui");
+
+  gtk_widget_class_bind_template_child (widget_class, AdwTabButton, button);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabButton, label);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabButton, icon);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabButton, indicator);
+  gtk_widget_class_bind_template_callback (widget_class, clicked_cb);
+
+  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+  gtk_widget_class_set_css_name (widget_class, "tabbutton");
+  gtk_widget_class_set_accessible_role (widget_class, GTK_ACCESSIBLE_ROLE_BUTTON);
+
+  g_type_ensure (ADW_TYPE_INDICATOR_BIN);
+}
+
+static void
+adw_tab_button_init (AdwTabButton *self)
+{
+  GtkSettings *settings;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  update_icon (self);
+
+  settings = gtk_widget_get_settings (GTK_WIDGET (self));
+
+  update_label_scale (self, settings);
+  g_signal_connect_object (settings, "notify::gtk-xft-dpi",
+                           G_CALLBACK (xft_dpi_changed), self,
+                           G_CONNECT_SWAPPED);
+}
+
+static const char *
+adw_tab_button_get_action_name (GtkActionable *actionable)
+{
+  AdwTabButton *self = ADW_TAB_BUTTON (actionable);
+
+  return gtk_actionable_get_action_name (GTK_ACTIONABLE (self->button));
+}
+
+static void
+adw_tab_button_set_action_name (GtkActionable *actionable,
+                                const char    *action_name)
+{
+  AdwTabButton *self = ADW_TAB_BUTTON (actionable);
+
+  return gtk_actionable_set_action_name (GTK_ACTIONABLE (self->button),
+                                         action_name);
+}
+
+static GVariant *
+adw_tab_button_get_action_target_value (GtkActionable *actionable)
+{
+  AdwTabButton *self = ADW_TAB_BUTTON (actionable);
+
+  return gtk_actionable_get_action_target_value (GTK_ACTIONABLE (self->button));
+}
+
+static void
+adw_tab_button_set_action_target_value (GtkActionable *actionable,
+                                        GVariant      *action_target)
+{
+  AdwTabButton *self = ADW_TAB_BUTTON (actionable);
+
+  return gtk_actionable_set_action_target_value (GTK_ACTIONABLE (self->button),
+                                                 action_target);
+}
+
+static void
+adw_tab_button_actionable_init (GtkActionableInterface *iface)
+{
+  iface->get_action_name = adw_tab_button_get_action_name;
+  iface->set_action_name = adw_tab_button_set_action_name;
+  iface->get_action_target_value = adw_tab_button_get_action_target_value;
+  iface->set_action_target_value = adw_tab_button_set_action_target_value;
+}
+
+/**
+ * adw_tab_button_new:
+ *
+ * Creates a new `AdwTabButton`.
+ *
+ * Returns: the newly created `AdwTabButton`
+ *
+ * Since: 1.3
+ */
+GtkWidget *
+adw_tab_button_new (void)
+{
+  return g_object_new (ADW_TYPE_TAB_BUTTON, NULL);
+}
+
+/**
+ * adw_tab_button_get_view: (attributes org.gtk.Method.get_property=view)
+ * @self: a tab button
+ *
+ * Gets the tab view @self displays.
+ *
+ * Returns: (transfer none) (nullable): the tab view
+ *
+ * Since: 1.3
+ */
+AdwTabView *
+adw_tab_button_get_view (AdwTabButton *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_BUTTON (self), NULL);
+
+  return self->view;
+}
+
+/**
+ * adw_tab_button_set_view: (attributes org.gtk.Method.set_property=view)
+ * @self: a tab button
+ * @view: (nullable): a tab view
+ *
+ * Sets the tab view to display.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_button_set_view (AdwTabButton *self,
+                         AdwTabView   *view)
+{
+  g_return_if_fail (ADW_IS_TAB_BUTTON (self));
+  g_return_if_fail (view == NULL || ADW_IS_TAB_VIEW (view));
+
+  if (self->view == view)
+    return;
+
+  if (self->view) {
+    int i, n;
+
+    g_signal_handlers_disconnect_by_func (self->view, update_icon, self);
+    g_signal_handlers_disconnect_by_func (self->view, update_needs_attention, self);
+    g_signal_handlers_disconnect_by_func (self->view, page_attached_cb, self);
+    g_signal_handlers_disconnect_by_func (self->view, page_detached_cb, self);
+
+    n = adw_tab_view_get_n_pages (self->view);
+
+    for (i = 0; i < n; i++)
+      page_detached_cb (self, adw_tab_view_get_nth_page (self->view, i));
+  }
+
+  g_set_object (&self->view, view);
+
+  if (self->view) {
+    int i, n;
+
+    g_signal_connect_object (self->view, "notify::n-pages",
+                             G_CALLBACK (update_icon), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->view, "notify::selected-page",
+                             G_CALLBACK (update_needs_attention), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->view, "page-attached",
+                             G_CALLBACK (page_attached_cb), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->view, "page-detached",
+                             G_CALLBACK (page_detached_cb), self,
+                             G_CONNECT_SWAPPED);
+
+    n = adw_tab_view_get_n_pages (self->view);
+
+    for (i = 0; i < n; i++)
+      page_attached_cb (self, adw_tab_view_get_nth_page (self->view, i));
+  }
+
+  update_icon (self);
+  update_needs_attention (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW]);
+}
diff --git a/src/adw-tab-button.h b/src/adw-tab-button.h
new file mode 100644
index 00000000..cc467578
--- /dev/null
+++ b/src/adw-tab-button.h
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include "adw-version.h"
+
+#include <gtk/gtk.h>
+#include "adw-tab-view.h"
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_TAB_BUTTON (adw_tab_button_get_type())
+
+ADW_AVAILABLE_IN_1_3
+G_DECLARE_FINAL_TYPE (AdwTabButton, adw_tab_button, ADW, TAB_BUTTON, GtkWidget)
+
+ADW_AVAILABLE_IN_1_3
+GtkWidget *adw_tab_button_new (void) G_GNUC_WARN_UNUSED_RESULT;
+
+ADW_AVAILABLE_IN_1_3
+AdwTabView *adw_tab_button_get_view (AdwTabButton *self);
+ADW_AVAILABLE_IN_1_3
+void        adw_tab_button_set_view (AdwTabButton *self,
+                                     AdwTabView   *view);
+
+G_END_DECLS
diff --git a/src/adw-tab-button.ui b/src/adw-tab-button.ui
new file mode 100644
index 00000000..04aa227c
--- /dev/null
+++ b/src/adw-tab-button.ui
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <template class="AdwTabButton" parent="GtkWidget">
+    <child>
+      <object class="GtkButton" id="button">
+        <property name="tooltip-text" translatable="yes">View Open Tabs</property>
+        <signal name="clicked" handler="clicked_cb" swapped="yes"/>
+        <property name="child">
+          <object class="AdwIndicatorBin" id="indicator">
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <property name="child">
+              <object class="GtkOverlay">
+                <child>
+                  <object class="GtkImage" id="icon"/>
+                </child>
+                <child type="overlay">
+                  <object class="GtkLabel" id="label">
+                    <property name="halign">center</property>
+                    <property name="justify">center</property>
+                    <style>
+                      <class name="numeric"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+            </property>
+          </object>
+        </property>
+        <style>
+          <class name="image-button"/>
+        </style>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/adw-tab-view.c b/src/adw-tab-view.c
index a785f9d7..8470291d 100644
--- a/src/adw-tab-view.c
+++ b/src/adw-tab-view.c
@@ -25,8 +25,8 @@ static GSList *tab_view_list;
  *
  * `AdwTabView` is a container which shows one child at a time. While it
  * provides keyboard shortcuts for switching between pages, it does not provide
- * a visible tab bar and relies on external widgets for that, such as
- * [class@TabBar].
+ * a visible tab switcher and relies on external widgets for that, such as
+ * [class@TabBar] and [class@TabButton].
  *
  * `AdwTabView` maintains a [class@TabPage] object for each page, which holds
  * additional per-page properties. You can obtain the `AdwTabPage` for a page
@@ -627,6 +627,9 @@ adw_tab_page_class_init (AdwTabPageClass *klass)
    * set to `TRUE`. If the tab is not visible, the corresponding edge of the tab
    * bar will be highlighted.
    *
+   * [class@TabButton] will display a dot if any of the pages that aren't
+   * selected have this property set to `TRUE`.
+   *
    * Since: 1.0
    */
   page_props[PAGE_PROP_NEEDS_ATTENTION] =
@@ -2334,6 +2337,9 @@ adw_tab_page_get_needs_attention (AdwTabPage *self)
  * set to `TRUE`. If the tab is not visible, the corresponding edge of the tab
  * bar will be highlighted.
  *
+ * [class@TabButton] will display a dot if any of the pages that aren't
+ * selected have [property@TabPage:needs-attention] set to `TRUE`.
+ *
  * Since: 1.0
  */
 void
diff --git a/src/adwaita.gresources.xml b/src/adwaita.gresources.xml
index 58a0d7c4..095d250e 100644
--- a/src/adwaita.gresources.xml
+++ b/src/adwaita.gresources.xml
@@ -8,7 +8,9 @@
     <file preprocess="xml-stripblanks">icons/scalable/actions/adw-expander-arrow-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/adw-mail-send-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/status/avatar-default-symbolic.svg</file>
+    <file preprocess="xml-stripblanks">icons/scalable/status/adw-tab-counter-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/status/adw-tab-icon-missing-symbolic.svg</file>
+    <file preprocess="xml-stripblanks">icons/scalable/status/adw-tab-overflow-symbolic.svg</file>
   </gresource>
   <gresource prefix="/org/gnome/Adwaita/ui">
     <file preprocess="xml-stripblanks">adw-about-window.ui</file>
@@ -24,6 +26,7 @@
     <file preprocess="xml-stripblanks">adw-status-page.ui</file>
     <file preprocess="xml-stripblanks">adw-tab.ui</file>
     <file preprocess="xml-stripblanks">adw-tab-bar.ui</file>
+    <file preprocess="xml-stripblanks">adw-tab-button.ui</file>
     <file preprocess="xml-stripblanks">adw-toast-widget.ui</file>
     <file preprocess="xml-stripblanks">adw-view-switcher-bar.ui</file>
     <file preprocess="xml-stripblanks">adw-view-switcher-button.ui</file>
diff --git a/src/adwaita.h b/src/adwaita.h
index 509a9740..6707b57c 100644
--- a/src/adwaita.h
+++ b/src/adwaita.h
@@ -64,6 +64,7 @@ G_BEGIN_DECLS
 #include "adw-swipe-tracker.h"
 #include "adw-swipeable.h"
 #include "adw-tab-bar.h"
+#include "adw-tab-button.h"
 #include "adw-tab-view.h"
 #include "adw-timed-animation.h"
 #include "adw-toast-overlay.h"
diff --git a/src/icons/scalable/status/adw-tab-counter-symbolic.svg 
b/src/icons/scalable/status/adw-tab-counter-symbolic.svg
new file mode 100644
index 00000000..3ad5d18e
--- /dev/null
+++ b/src/icons/scalable/status/adw-tab-counter-symbolic.svg
@@ -0,0 +1 @@
+<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg";><path d="M3.006 0c-1.645 0-3 1.355-3 3v10c0 
1.645 1.355 3 3 3h10c1.645 0 3-1.355 3-3V3c0-1.645-1.355-3-3-3zm0 2h10c.571 0 1 .429 1 1v10c0 .571-.429 1-1 
1h-10c-.571 0-1-.429-1-1V3c0-.571.429-1 1-1z" style="fill:#2e3436;fill-opacity:1"/></svg>
\ No newline at end of file
diff --git a/src/icons/scalable/status/adw-tab-overflow-symbolic.svg 
b/src/icons/scalable/status/adw-tab-overflow-symbolic.svg
new file mode 100644
index 00000000..2f89e9ab
--- /dev/null
+++ b/src/icons/scalable/status/adw-tab-overflow-symbolic.svg
@@ -0,0 +1 @@
+<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg";><path style="fill:#2e3436;fill-opacity:1" 
d="M3.006 0c-1.645 0-3 1.355-3 3v10c0 1.645 1.355 3 3 3h10c1.645 0 3-1.355 3-3V3c0-1.645-1.355-3-3-3h-10zm0 
2h10c.571 0 1 .429 1 1v10c0 .571-.429 1-1 1h-10c-.571 0-1-.429-1-1V3c0-.571.429-1 1-1zm2.469 3.402c-.349 
0-.673.074-.973.22a2.16 2.16 0 0 0-.77.58 2.755 2.755 0 0 0-.507.87A3.33 3.33 0 0 0 3.05 8.16c0 
.397.057.765.174 1.104.125.329.295.62.507.87.213.243.47.436.77.58a2.311 2.311 0 0 0 
2.191-.158c.407-.25.829-.639 1.264-1.161.426.56.852.963 1.277 1.205.436.242.866.363 1.291.363.349 0 
.673-.068.973-.203.3-.145.557-.34.77-.582.212-.252.376-.546.492-.885.126-.339.19-.7.19-1.088 
0-.387-.064-.751-.19-1.09a2.545 2.545 0 0 0-.492-.869 2.165 2.165 0 0 0-.77-.582 2.203 2.203 0 0 
0-.973-.217c-.416 0-.828.126-1.234.377-.397.242-.813.624-1.248 
1.147-.426-.561-.856-.963-1.291-1.205-.426-.242-.852-.364-1.277-.364zm.115 1.743c.193 0 
.392.067.596.203.203.125.46.392.77.798a7.62 7.62 0 0 1-.43
 6.522 2.505 2.505 0 0 1-.348.32 1.232 1.232 0 0 1-.305.145 1.013 1.013 0 0 1-.29.045.729.729 0 0 
1-.58-.278c-.146-.183-.22-.43-.22-.74s.074-.556.22-.74a.74.74 0 0 1 .593-.275zm4.834.044c.232 0 
.421.092.566.276.155.184.233.43.233.74s-.078.556-.233.74a.701.701 0 0 1-.58.276c-.193 
0-.392-.064-.596-.19-.203-.135-.46-.406-.77-.812.165-.213.31-.386.436-.522a1.86 1.86 0 0 1 .348-.304 1.11 
1.11 0 0 1 .291-.16c.097-.03.198-.044.305-.044z"/></svg>
\ No newline at end of file
diff --git a/src/meson.build b/src/meson.build
index 54664c1e..dd6536a0 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -125,6 +125,7 @@ src_headers = [
   'adw-swipe-tracker.h',
   'adw-swipeable.h',
   'adw-tab-bar.h',
+  'adw-tab-button.h',
   'adw-tab-view.h',
   'adw-timed-animation.h',
   'adw-toast.h',
@@ -189,6 +190,7 @@ src_sources = [
   'adw-swipe-tracker.c',
   'adw-swipeable.c',
   'adw-tab-bar.c',
+  'adw-tab-button.c',
   'adw-tab-view.c',
   'adw-timed-animation.c',
   'adw-toast.c',
diff --git a/src/stylesheet/widgets/_buttons.scss b/src/stylesheet/widgets/_buttons.scss
index 8ee170d4..d2ca6e81 100644
--- a/src/stylesheet/widgets/_buttons.scss
+++ b/src/stylesheet/widgets/_buttons.scss
@@ -594,3 +594,19 @@ buttoncontent {
     }
   }
 }
+
+tabbutton {
+  label {
+    font-weight: 800;
+    font-size: 8pt;
+
+    &.small {
+      font-size: 6pt;
+    }
+  }
+
+  indicatorbin > indicator,
+  indicatorbin > mask {
+    transform: translate(-1px, 1px);
+  }
+}
diff --git a/src/stylesheet/widgets/_linked.scss b/src/stylesheet/widgets/_linked.scss
index 1e29c3bc..dba474b8 100644
--- a/src/stylesheet/widgets/_linked.scss
+++ b/src/stylesheet/widgets/_linked.scss
@@ -5,6 +5,7 @@ $_linked_widgets: ("%button",          ""),
                   ("dropdown",         "> button"),
                   ("colorbutton",      "> button"),
                   ("fontbutton",       "> button"),
+                  ("tabbutton",        "> button"),
                   ("combobox",         "> box > button.combo"),
                   ("appchooserbutton", "> combobox > box > button.combo"),
                   ("%entry",           ""),
diff --git a/tests/meson.build b/tests/meson.build
index 2f17a06c..9bddd07a 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -53,6 +53,7 @@ test_names = [
   'test-status-page',
   'test-style-manager',
   'test-tab-bar',
+  'test-tab-button',
   'test-tab-view',
   'test-timed-animation',
   'test-toast',
diff --git a/tests/test-tab-button.c b/tests/test-tab-button.c
new file mode 100644
index 00000000..8bdae22d
--- /dev/null
+++ b/tests/test-tab-button.c
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#include <adwaita.h>
+
+int notified;
+
+static void
+notify_cb (GtkWidget *widget, gpointer data)
+{
+  notified++;
+}
+
+static void
+test_adw_tab_button_view (void)
+{
+  AdwTabButton *button = g_object_ref_sink (ADW_TAB_BUTTON (adw_tab_button_new ()));
+  AdwTabView *view;
+
+  g_assert_nonnull (button);
+
+  notified = 0;
+  g_signal_connect (button, "notify::view", G_CALLBACK (notify_cb), NULL);
+
+  g_object_get (button, "view", &view, NULL);
+  g_assert_null (view);
+
+  adw_tab_button_set_view (button, NULL);
+  g_assert_cmpint (notified, ==, 0);
+
+  view = g_object_ref_sink (ADW_TAB_VIEW (adw_tab_view_new ()));
+  adw_tab_button_set_view (button, view);
+  g_assert_true (adw_tab_button_get_view (button) == view);
+  g_assert_cmpint (notified, ==, 1);
+
+  g_object_set (button, "view", NULL, NULL);
+  g_assert_null (adw_tab_button_get_view (button));
+  g_assert_cmpint (notified, ==, 2);
+
+  g_assert_finalize_object (button);
+  g_assert_finalize_object (view);
+}
+
+int
+main (int   argc,
+      char *argv[])
+{
+  gtk_test_init (&argc, &argv, NULL);
+  adw_init ();
+
+  g_test_add_func ("/Adwaita/TabButton/view", test_adw_tab_button_view);
+
+  return g_test_run ();
+}


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