[libadwaita/wip/exalm/tabs: 17/19] Add AdwTabBar




commit c4fa4cfbfcc344387a0a4cfde683cc3295ee883c
Author: Alexander Mikhaylenko <alexm gnome org>
Date:   Sun Sep 13 02:27:59 2020 +0500

    Add AdwTabBar

 doc/adwaita-docs.xml       |    1 +
 doc/meson.build            |    1 +
 src/adw-tab-bar-private.h  |   21 +
 src/adw-tab-bar.c          | 1107 ++++++++++++++++++++++++++++++++++++++++++++
 src/adw-tab-bar.h          |   79 ++++
 src/adw-tab-bar.ui         |   77 +++
 src/adwaita.gresources.xml |    1 +
 src/adwaita.h              |    1 +
 src/meson.build            |    3 +
 tests/meson.build          |    1 +
 tests/test-tab-bar.c       |  253 ++++++++++
 11 files changed, 1545 insertions(+)
---
diff --git a/doc/adwaita-docs.xml b/doc/adwaita-docs.xml
index e46adc1..e048981 100644
--- a/doc/adwaita-docs.xml
+++ b/doc/adwaita-docs.xml
@@ -63,6 +63,7 @@
     <xi:include href="xml/adw-swipeable.xml"/>
     <xi:include href="xml/adw-swipe-group.xml"/>
     <xi:include href="xml/adw-swipe-tracker.xml"/>
+    <xi:include href="xml/adw-tab-bar.xml"/>
     <xi:include href="xml/adw-tab-view.xml"/>
     <xi:include href="xml/adw-value-object.xml"/>
     <xi:include href="xml/adw-view-switcher.xml"/>
diff --git a/doc/meson.build b/doc/meson.build
index 29f5c44..1e5053f 100644
--- a/doc/meson.build
+++ b/doc/meson.build
@@ -21,6 +21,7 @@ private_headers = [
     'adw-shadow-helper-private.h',
     'adw-swipe-tracker-private.h',
     'adw-tab-private.h',
+    'adw-tab-bar-private.h',
     'adw-tab-box-private.h',
     'adw-tab-view-private.h',
     'adw-types.h',
diff --git a/src/adw-tab-bar-private.h b/src/adw-tab-bar-private.h
new file mode 100644
index 0000000..ccc8076
--- /dev/null
+++ b/src/adw-tab-bar-private.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2020 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-tab-bar.h"
+
+G_BEGIN_DECLS
+
+gboolean adw_tab_bar_tabs_have_visible_focus (AdwTabBar *self);
+
+G_END_DECLS
diff --git a/src/adw-tab-bar.c b/src/adw-tab-bar.c
new file mode 100644
index 0000000..697cc10
--- /dev/null
+++ b/src/adw-tab-bar.c
@@ -0,0 +1,1107 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#include "config.h"
+
+#include "adw-tab-bar-private.h"
+
+#include "adw-bin.h"
+#include "adw-tab-box-private.h"
+
+/**
+ * SECTION:adw-tab-bar
+ * @short_description: A tab bar for #AdwTabView
+ * @title: AdwTabBar
+ * @See_also: #AdwTabView
+ *
+ * The #AdwTabBar widget is a tab bar that can be used with conjunction with
+ * #AdwTabView.
+ *
+ * #AdwTabBar can autohide and can optionally contain action widgets on both
+ * sides of the tabs.
+ *
+ * When there's not enough space to show all the tabs, #AdwTabBar will scroll
+ * them. Pinned tabs always stay visible and aren't a part of the scrollable
+ * area.
+ *
+ * # CSS nodes
+ *
+ * #AdwTabBar has a single CSS node with name tabbar.
+ *
+ * Since: 1.0
+ */
+
+struct _AdwTabBar
+{
+  GtkWidget parent_instance;
+
+  GtkRevealer *revealer;
+  AdwBin *start_action_bin;
+  AdwBin *end_action_bin;
+
+  AdwTabBox *box;
+  GtkScrolledWindow *scrolled_window;
+
+  AdwTabBox *pinned_box;
+  GtkScrolledWindow *pinned_scrolled_window;
+
+  AdwTabView *view;
+  gboolean autohide;
+
+  gboolean is_overflowing;
+  gboolean resize_frozen;
+};
+
+static void adw_tab_bar_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (AdwTabBar, adw_tab_bar, GTK_TYPE_WIDGET,
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
+                         adw_tab_bar_buildable_init))
+
+enum {
+  PROP_0,
+  PROP_VIEW,
+  PROP_START_ACTION_WIDGET,
+  PROP_END_ACTION_WIDGET,
+  PROP_AUTOHIDE,
+  PROP_TABS_REVEALED,
+  PROP_EXPAND_TABS,
+  PROP_INVERTED,
+  PROP_IS_OVERFLOWING,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+  SIGNAL_EXTRA_DRAG_DROP,
+  SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+set_tabs_revealed (AdwTabBar *self,
+                   gboolean   tabs_revealed)
+{
+  if (tabs_revealed == adw_tab_bar_get_tabs_revealed (self))
+    return;
+
+  gtk_revealer_set_reveal_child (self->revealer, tabs_revealed);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TABS_REVEALED]);
+}
+
+static void
+update_autohide_cb (AdwTabBar *self)
+{
+  int n_tabs = 0, n_pinned_tabs = 0;
+  gboolean is_transferring_page;
+
+  if (!self->view) {
+    set_tabs_revealed (self, FALSE);
+
+    return;
+  }
+
+  if (!self->autohide) {
+    set_tabs_revealed (self, TRUE);
+
+    return;
+  }
+
+  n_tabs = adw_tab_view_get_n_pages (self->view);
+  n_pinned_tabs = adw_tab_view_get_n_pinned_pages (self->view);
+  is_transferring_page = adw_tab_view_get_is_transferring_page (self->view);
+
+  set_tabs_revealed (self, n_tabs > 1 || n_pinned_tabs >= 1 || is_transferring_page);
+}
+
+static void
+notify_selected_page_cb (AdwTabBar *self)
+{
+  AdwTabPage *page = adw_tab_view_get_selected_page (self->view);
+
+  if (!page)
+    return;
+
+  if (adw_tab_page_get_pinned (page)) {
+    adw_tab_box_select_page (self->pinned_box, page);
+    adw_tab_box_select_page (self->box, page);
+  } else {
+    adw_tab_box_select_page (self->box, page);
+    adw_tab_box_select_page (self->pinned_box, page);
+  }
+}
+
+static void
+notify_pinned_cb (AdwTabPage *page,
+                  GParamSpec *pspec,
+                  AdwTabBar  *self)
+{
+  AdwTabBox *from, *to;
+  gboolean should_focus;
+
+  if (adw_tab_page_get_pinned (page)) {
+    from = self->box;
+    to = self->pinned_box;
+  } else {
+    from = self->pinned_box;
+    to = self->box;
+  }
+
+  should_focus = adw_tab_box_is_page_focused (from, page);
+
+  adw_tab_box_detach_page (from, page);
+  adw_tab_box_attach_page (to, page, adw_tab_view_get_n_pinned_pages (self->view));
+
+  if (should_focus)
+    adw_tab_box_try_focus_selected_tab (to);
+}
+
+static void
+page_attached_cb (AdwTabBar  *self,
+                  AdwTabPage *page,
+                  int         position)
+{
+  g_signal_connect_object (page, "notify::pinned",
+                           G_CALLBACK (notify_pinned_cb), self,
+                           0);
+}
+
+static void
+page_detached_cb (AdwTabBar  *self,
+                  AdwTabPage *page,
+                  int         position)
+{
+  g_signal_handlers_disconnect_by_func (page, notify_pinned_cb, self);
+}
+
+static void
+update_needs_attention (AdwTabBar *self,
+                        gboolean   pinned)
+{
+  GtkStyleContext *context;
+  gboolean left, right;
+
+  g_object_get (pinned ? self->pinned_box : self->box,
+                "needs-attention-left", &left,
+                "needs-attention-right", &right,
+                NULL);
+
+  if (pinned)
+    context = gtk_widget_get_style_context (GTK_WIDGET (self->pinned_scrolled_window));
+  else
+    context = gtk_widget_get_style_context (GTK_WIDGET (self->scrolled_window));
+
+  if (left)
+    gtk_style_context_add_class (context, "needs-attention-left");
+  else
+    gtk_style_context_remove_class (context, "needs-attention-left");
+
+  if (right)
+    gtk_style_context_add_class (context, "needs-attention-right");
+  else
+    gtk_style_context_remove_class (context, "needs-attention-right");
+}
+
+static void
+notify_needs_attention_cb (AdwTabBar *self)
+{
+  update_needs_attention (self, FALSE);
+}
+
+static void
+notify_needs_attention_pinned_cb (AdwTabBar *self)
+{
+  update_needs_attention (self, TRUE);
+}
+
+static inline gboolean
+is_overflowing (GtkAdjustment *adj)
+{
+  double lower, upper, page_size;
+
+  lower = gtk_adjustment_get_lower (adj);
+  upper = gtk_adjustment_get_upper (adj);
+  page_size = gtk_adjustment_get_page_size (adj);
+  return upper - lower > page_size;
+}
+
+static void
+update_is_overflowing (AdwTabBar *self)
+{
+  GtkAdjustment *adj = gtk_scrolled_window_get_hadjustment (self->scrolled_window);
+  GtkAdjustment *pinned_adj = gtk_scrolled_window_get_hadjustment (self->pinned_scrolled_window);
+  gboolean overflowing = is_overflowing (adj) || is_overflowing (pinned_adj);
+
+  if (overflowing == self->is_overflowing)
+    return;
+
+  overflowing |= self->resize_frozen;
+
+  if (overflowing == self->is_overflowing)
+    return;
+
+  self->is_overflowing = overflowing;
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IS_OVERFLOWING]);
+}
+
+static void
+notify_resize_frozen_cb (AdwTabBar *self)
+{
+  gboolean frozen, pinned_frozen;
+
+  g_object_get (self->box, "resize-frozen", &frozen, NULL);
+  g_object_get (self->pinned_box, "resize-frozen", &pinned_frozen, NULL);
+
+  self->resize_frozen = frozen || pinned_frozen;
+
+  update_is_overflowing (self);
+}
+
+static void
+stop_kinetic_scrolling_cb (GtkScrolledWindow *scrolled_window)
+{
+  /* HACK: Need to cancel kinetic scrolling. If only the built-in adjustment
+   * animation API was public, we wouldn't have to do any of this... */
+  gtk_scrolled_window_set_kinetic_scrolling (scrolled_window, FALSE);
+  gtk_scrolled_window_set_kinetic_scrolling (scrolled_window, TRUE);
+}
+
+static gboolean
+extra_drag_drop_cb (AdwTabBar  *self,
+                    AdwTabPage *page,
+                    GValue     *value)
+{
+  gboolean ret = GDK_EVENT_PROPAGATE;
+
+  g_signal_emit (self, signals[SIGNAL_EXTRA_DRAG_DROP], 0, page, value, &ret);
+
+  return ret;
+}
+
+static void
+view_destroy_cb (AdwTabBar *self)
+{
+  adw_tab_bar_set_view (self, NULL);
+}
+
+static gboolean
+adw_tab_bar_focus (GtkWidget        *widget,
+                   GtkDirectionType  direction)
+{
+  AdwTabBar *self = ADW_TAB_BAR (widget);
+  gboolean is_rtl;
+  GtkDirectionType start, end;
+
+  if (!adw_tab_bar_get_tabs_revealed (self))
+    return GDK_EVENT_PROPAGATE;
+
+  if (!gtk_widget_get_focus_child (widget))
+    return gtk_widget_child_focus (GTK_WIDGET (self->pinned_box), direction) ||
+           gtk_widget_child_focus (GTK_WIDGET (self->box), direction);
+
+  is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
+  start = is_rtl ? GTK_DIR_RIGHT : GTK_DIR_LEFT;
+  end = is_rtl ? GTK_DIR_LEFT : GTK_DIR_RIGHT;
+
+  if (direction == start) {
+    if (adw_tab_view_select_previous_page (self->view))
+      return GDK_EVENT_STOP;
+
+    return gtk_widget_keynav_failed (widget, direction);
+  }
+
+  if (direction == end) {
+    if (adw_tab_view_select_next_page (self->view))
+      return GDK_EVENT_STOP;
+
+    return gtk_widget_keynav_failed (widget, direction);
+  }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+adw_tab_bar_dispose (GObject *object)
+{
+  AdwTabBar *self = ADW_TAB_BAR (object);
+
+  adw_tab_bar_set_view (self, NULL);
+
+  gtk_widget_unparent (GTK_WIDGET (self->revealer));
+
+  G_OBJECT_CLASS (adw_tab_bar_parent_class)->dispose (object);
+}
+
+static void
+adw_tab_bar_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  AdwTabBar *self = ADW_TAB_BAR (object);
+
+  switch (prop_id) {
+  case PROP_VIEW:
+    g_value_set_object (value, adw_tab_bar_get_view (self));
+    break;
+
+  case PROP_START_ACTION_WIDGET:
+    g_value_set_object (value, adw_tab_bar_get_start_action_widget (self));
+    break;
+
+  case PROP_END_ACTION_WIDGET:
+    g_value_set_object (value, adw_tab_bar_get_end_action_widget (self));
+    break;
+
+  case PROP_AUTOHIDE:
+    g_value_set_boolean (value, adw_tab_bar_get_autohide (self));
+    break;
+
+  case PROP_TABS_REVEALED:
+    g_value_set_boolean (value, adw_tab_bar_get_tabs_revealed (self));
+    break;
+
+  case PROP_EXPAND_TABS:
+    g_value_set_boolean (value, adw_tab_bar_get_expand_tabs (self));
+    break;
+
+  case PROP_INVERTED:
+    g_value_set_boolean (value, adw_tab_bar_get_inverted (self));
+    break;
+
+  case PROP_IS_OVERFLOWING:
+    g_value_set_boolean (value, adw_tab_bar_get_is_overflowing (self));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_tab_bar_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  AdwTabBar *self = ADW_TAB_BAR (object);
+
+  switch (prop_id) {
+  case PROP_VIEW:
+    adw_tab_bar_set_view (self, g_value_get_object (value));
+    break;
+
+  case PROP_START_ACTION_WIDGET:
+    adw_tab_bar_set_start_action_widget (self, g_value_get_object (value));
+    break;
+
+  case PROP_END_ACTION_WIDGET:
+    adw_tab_bar_set_end_action_widget (self, g_value_get_object (value));
+    break;
+
+  case PROP_AUTOHIDE:
+    adw_tab_bar_set_autohide (self, g_value_get_boolean (value));
+    break;
+
+  case PROP_EXPAND_TABS:
+    adw_tab_bar_set_expand_tabs (self, g_value_get_boolean (value));
+    break;
+
+  case PROP_INVERTED:
+    adw_tab_bar_set_inverted (self, g_value_get_boolean (value));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_tab_bar_class_init (AdwTabBarClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = adw_tab_bar_dispose;
+  object_class->get_property = adw_tab_bar_get_property;
+  object_class->set_property = adw_tab_bar_set_property;
+
+  widget_class->focus = adw_tab_bar_focus;
+
+  /**
+   * AdwTabBar:view:
+   *
+   * The #AdwTabView the tab bar controls.
+   *
+   * Since: 1.0
+   */
+  props[PROP_VIEW] =
+    g_param_spec_object ("view",
+                         "View",
+                         "The view the tab bar controls.",
+                         ADW_TYPE_TAB_VIEW,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabBar:start-action-widget:
+   *
+   * The widget shown before the tabs.
+   *
+   * Since: 1.0
+   */
+  props[PROP_START_ACTION_WIDGET] =
+    g_param_spec_object ("start-action-widget",
+                         "Start action widget",
+                         "The widget shown before the tabs",
+                         GTK_TYPE_WIDGET,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabBar:end-action-widget:
+   *
+   * The widget shown after the tabs.
+   *
+   * Since: 1.0
+   */
+  props[PROP_END_ACTION_WIDGET] =
+    g_param_spec_object ("end-action-widget",
+                         "End action widget",
+                         "The widget shown after the tabs",
+                         GTK_TYPE_WIDGET,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabBar:autohide:
+   *
+   * Whether tabs automatically hide.
+   *
+   * If set to %TRUE, the tab bar disappears when the associated #AdwTabView
+   * has 0 or 1 tab, no pinned tabs, and no tab is being transferred.
+   *
+   * See #AdwTabBar:tabs-revealed.
+   *
+   * Since: 1.0
+   */
+  props[PROP_AUTOHIDE] =
+    g_param_spec_boolean ("autohide",
+                          "Autohide",
+                          "Whether the tabs automatically hide",
+                          TRUE,
+                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabBar:tabs-revealed:
+   *
+   * Whether tabs are currently revealed.
+   *
+   * See AdwTabBar:autohide.
+   *
+   * Since: 1.0
+   */
+  props[PROP_TABS_REVEALED] =
+    g_param_spec_boolean ("tabs-revealed",
+                          "Tabs revealed",
+                          "Whether the tabs are currently revealed",
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabBar:expand-tabs:
+   *
+   * Whether tabs should expand.
+   *
+   * If set to %TRUE, the tabs will always vary width filling the whole width
+   * when possible, otherwise tabs will always have the minimum possible size.
+   *
+   * Since: 1.0
+   */
+  props[PROP_EXPAND_TABS] =
+    g_param_spec_boolean ("expand-tabs",
+                          "Expand tabs",
+                          "Whether tabs expand to full width",
+                          TRUE,
+                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabBar:inverted:
+   *
+   * Whether tabs use inverted layout.
+   *
+   * If set to %TRUE, non-pinned tabs will have the close button at the
+   * beginning and the indicator at the end rather than the opposite.
+   *
+   * Since: 1.0
+   */
+  props[PROP_INVERTED] =
+    g_param_spec_boolean ("inverted",
+                          "Inverted",
+                          "Whether tabs use inverted layout",
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabBar:is-overflowing:
+   *
+   * Whether the tab bar is overflowing.
+   *
+   * If set to %TRUE, all tabs cannot be displayed at once and require
+   * scrolling.
+   *
+   * Since: 1.0
+   */
+  props[PROP_IS_OVERFLOWING] =
+    g_param_spec_boolean ("is-overflowing",
+                          "Is overflowing",
+                          "Whether the tab bar is overflowing",
+                          FALSE,
+                          G_PARAM_READABLE);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  /**
+   * AdwTabBar::extra-drag-drop:
+   * @self: a #AdwTabBar
+   * @page: the #AdwTabPage matching the tab the content was dropped onto
+   * @value: the #GValue being dropped
+   *
+   * This signal is emitted when content allowed via
+   * #adw_tab_bar_setup_extra_drop_target() is dropped onto a tab representing
+   * @page.
+   *
+   * See #GtkDropTarget::drop.
+   *
+   * Returns: whether the drop was accepted for the given page
+   *
+   * Since: 1.0
+   */
+  signals[SIGNAL_EXTRA_DRAG_DROP] =
+    g_signal_new ("extra-drag-drop",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  g_signal_accumulator_first_wins, NULL, NULL,
+                  G_TYPE_BOOLEAN,
+                  2,
+                  ADW_TYPE_TAB_PAGE,
+                  G_TYPE_VALUE);
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/org/gnome/Adwaita/ui/adw-tab-bar.ui");
+  gtk_widget_class_bind_template_child (widget_class, AdwTabBar, revealer);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabBar, pinned_box);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabBar, box);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabBar, scrolled_window);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabBar, pinned_scrolled_window);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabBar, start_action_bin);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabBar, end_action_bin);
+  gtk_widget_class_bind_template_callback (widget_class, notify_needs_attention_cb);
+  gtk_widget_class_bind_template_callback (widget_class, notify_needs_attention_pinned_cb);
+  gtk_widget_class_bind_template_callback (widget_class, notify_resize_frozen_cb);
+  gtk_widget_class_bind_template_callback (widget_class, stop_kinetic_scrolling_cb);
+  gtk_widget_class_bind_template_callback (widget_class, extra_drag_drop_cb);
+
+  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+  gtk_widget_class_set_css_name (widget_class, "tabbar");
+}
+
+static void
+adw_tab_bar_init (AdwTabBar *self)
+{
+  GtkAdjustment *adj;
+
+  self->autohide = TRUE;
+
+  g_type_ensure (ADW_TYPE_TAB_BOX);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  adj = gtk_scrolled_window_get_hadjustment (self->scrolled_window);
+  adw_tab_box_set_adjustment (self->box, adj);
+  g_signal_connect_object (adj, "changed", G_CALLBACK (update_is_overflowing),
+                           self, G_CONNECT_SWAPPED);
+
+  adj = gtk_scrolled_window_get_hadjustment (self->pinned_scrolled_window);
+  adw_tab_box_set_adjustment (self->pinned_box, adj);
+  g_signal_connect_object (adj, "changed", G_CALLBACK (update_is_overflowing),
+                           self, G_CONNECT_SWAPPED);
+}
+
+static void
+adw_tab_bar_buildable_add_child (GtkBuildable *buildable,
+                                 GtkBuilder   *builder,
+                                 GObject      *child,
+                                 const char   *type)
+{
+  AdwTabBar *self = ADW_TAB_BAR (buildable);
+
+  if (!self->revealer) {
+    gtk_widget_set_parent (GTK_WIDGET (child), GTK_WIDGET (self));
+
+    return;
+  }
+
+  if (!type || !g_strcmp0 (type, "start"))
+    adw_tab_bar_set_start_action_widget (self, GTK_WIDGET (child));
+  else if (!g_strcmp0 (type, "end"))
+    adw_tab_bar_set_end_action_widget (self, GTK_WIDGET (child));
+  else
+    GTK_BUILDER_WARN_INVALID_CHILD_TYPE (ADW_TAB_BAR (self), type);
+}
+
+static void
+adw_tab_bar_buildable_init (GtkBuildableIface *iface)
+{
+  iface->add_child = adw_tab_bar_buildable_add_child;
+}
+
+gboolean
+adw_tab_bar_tabs_have_visible_focus (AdwTabBar *self)
+{
+  GtkWidget *pinned_focus_child, *scroll_focus_child;
+
+  g_return_val_if_fail (ADW_IS_TAB_BAR (self), FALSE);
+
+  pinned_focus_child = gtk_widget_get_focus_child (GTK_WIDGET (self->pinned_box));
+  scroll_focus_child = gtk_widget_get_focus_child (GTK_WIDGET (self->box));
+
+  if (pinned_focus_child && gtk_widget_has_visible_focus (pinned_focus_child))
+    return TRUE;
+
+  if (scroll_focus_child && gtk_widget_has_visible_focus (scroll_focus_child))
+    return TRUE;
+
+  return FALSE;
+}
+
+/**
+ * adw_tab_bar_new:
+ *
+ * Creates a new #AdwTabBar widget.
+ *
+ * Returns: a new #AdwTabBar
+ *
+ * Since: 1.0
+ */
+AdwTabBar *
+adw_tab_bar_new (void)
+{
+  return g_object_new (ADW_TYPE_TAB_BAR, NULL);
+}
+
+/**
+ * adw_tab_bar_get_view:
+ * @self: a #AdwTabBar
+ *
+ * Gets the #AdwTabView @self controls.
+ *
+ * Returns: (transfer none) (nullable): the #AdwTabView @self controls
+ *
+ * Since: 1.0
+ */
+AdwTabView *
+adw_tab_bar_get_view (AdwTabBar *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_BAR (self), NULL);
+
+  return self->view;
+}
+
+/**
+ * adw_tab_bar_set_view:
+ * @self: a #AdwTabBar
+ * @view: (nullable): a #AdwTabView
+ *
+ * Sets the #AdwTabView @self controls.
+ *
+ * Since: 1.0
+ */
+void
+adw_tab_bar_set_view (AdwTabBar  *self,
+                      AdwTabView *view)
+{
+  g_return_if_fail (ADW_IS_TAB_BAR (self));
+  g_return_if_fail (ADW_IS_TAB_VIEW (view) || view == NULL);
+
+  if (self->view == view)
+    return;
+
+  if (self->view) {
+    int i, n;
+
+    g_signal_handlers_disconnect_by_func (self->view, update_autohide_cb, self);
+    g_signal_handlers_disconnect_by_func (self->view, notify_selected_page_cb, 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);
+    g_signal_handlers_disconnect_by_func (self->view, view_destroy_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), i);
+
+    adw_tab_box_set_view (self->pinned_box, NULL);
+    adw_tab_box_set_view (self->box, NULL);
+  }
+
+  g_set_object (&self->view, view);
+
+  if (self->view) {
+    int i, n;
+
+    adw_tab_box_set_view (self->pinned_box, view);
+    adw_tab_box_set_view (self->box, view);
+
+    g_signal_connect_object (self->view, "notify::is-transferring-page",
+                             G_CALLBACK (update_autohide_cb), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->view, "notify::n-pages",
+                             G_CALLBACK (update_autohide_cb), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->view, "notify::n-pinned-pages",
+                             G_CALLBACK (update_autohide_cb), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->view, "notify::selected-page",
+                             G_CALLBACK (notify_selected_page_cb), 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);
+    g_signal_connect_object (self->view, "destroy",
+                             G_CALLBACK (view_destroy_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), i);
+  }
+
+  update_autohide_cb (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW]);
+}
+
+/**
+ * adw_tab_bar_get_start_action_widget:
+ * @self: a #AdwTabBar
+ *
+ * Gets the widget shown before the tabs.
+ *
+ * Returns: (transfer none) (nullable): the widget shown before the tabs, or %NULL
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+adw_tab_bar_get_start_action_widget (AdwTabBar *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_BAR (self), NULL);
+
+  return self->start_action_bin ? adw_bin_get_child (self->start_action_bin) : NULL;
+}
+
+/**
+ * adw_tab_bar_set_start_action_widget:
+ * @self: a #AdwTabBar
+ * @widget: (transfer none) (nullable): the widget to show before the tabs, or %NULL
+ *
+ * Sets the widget to show before the tabs.
+ *
+ * Since: 1.0
+ */
+void
+adw_tab_bar_set_start_action_widget (AdwTabBar *self,
+                                     GtkWidget *widget)
+{
+  GtkWidget *old_widget;
+
+  g_return_if_fail (ADW_IS_TAB_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget) || widget == NULL);
+
+  old_widget = adw_bin_get_child (self->start_action_bin);
+
+  if (old_widget == widget)
+    return;
+
+  adw_bin_set_child (self->start_action_bin, widget);
+  gtk_widget_set_visible (GTK_WIDGET (self->start_action_bin), widget != NULL);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_START_ACTION_WIDGET]);
+}
+
+/**
+ * adw_tab_bar_get_end_action_widget:
+ * @self: a #AdwTabBar
+ *
+ * Gets the widget shown after the tabs.
+ *
+ * Returns: (transfer none) (nullable): the widget shown after the tabs, or %NULL
+ *
+ * Since: 1.0
+ */
+GtkWidget *
+adw_tab_bar_get_end_action_widget (AdwTabBar *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_BAR (self), NULL);
+
+  return self->end_action_bin ? adw_bin_get_child (self->end_action_bin) : NULL;
+}
+
+/**
+ * adw_tab_bar_set_end_action_widget:
+ * @self: a #AdwTabBar
+ * @widget: (transfer none) (nullable): the widget to show after the tabs, or %NULL
+ *
+ * Sets the widget to show after the tabs.
+ *
+ * Since: 1.0
+ */
+void
+adw_tab_bar_set_end_action_widget (AdwTabBar *self,
+                                   GtkWidget *widget)
+{
+  GtkWidget *old_widget;
+
+  g_return_if_fail (ADW_IS_TAB_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget) || widget == NULL);
+
+  old_widget = adw_bin_get_child (self->end_action_bin);
+
+  if (old_widget == widget)
+    return;
+
+  adw_bin_set_child (self->end_action_bin, widget);
+  gtk_widget_set_visible (GTK_WIDGET (self->end_action_bin), widget != NULL);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_END_ACTION_WIDGET]);
+}
+
+/**
+ * adw_tab_bar_get_autohide:
+ * @self: a #AdwTabBar
+ *
+ * Gets whether the tabs automatically hide, see adw_tab_bar_set_autohide().
+ *
+ * Returns: whether the tabs automatically hide
+ *
+ * Since: 1.0
+ */
+gboolean
+adw_tab_bar_get_autohide (AdwTabBar *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_BAR (self), FALSE);
+
+  return self->autohide;
+}
+
+/**
+ * adw_tab_bar_set_autohide:
+ * @self: a #AdwTabBar
+ * @autohide: whether the tabs automatically hide
+ *
+ * Sets whether the tabs automatically hide.
+ *
+ * If @autohide is %TRUE, the tab bar disappears when the associated #AdwTabView
+ * has 0 or 1 tab, no pinned tabs, and no tab is being transferred.
+ *
+ * Autohide is enabled by default.
+ *
+ * See #AdwTabBar:tabs-revealed.
+ *
+ * Since: 1.0
+ */
+void
+adw_tab_bar_set_autohide (AdwTabBar *self,
+                          gboolean   autohide)
+{
+  g_return_if_fail (ADW_IS_TAB_BAR (self));
+
+  autohide = !!autohide;
+
+  if (autohide == self->autohide)
+    return;
+
+  self->autohide = autohide;
+
+  update_autohide_cb (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_AUTOHIDE]);
+}
+
+/**
+ * adw_tab_bar_get_tabs_revealed:
+ * @self: a #AdwTabBar
+ *
+ * Gets the value of the #AdwTabBar:tabs-revealed property.
+ *
+ * Returns: whether the tabs are current revealed
+ *
+ * Since: 1.0
+ */
+gboolean
+adw_tab_bar_get_tabs_revealed (AdwTabBar *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_BAR (self), FALSE);
+
+  return gtk_revealer_get_reveal_child (self->revealer);
+}
+
+/**
+ * adw_tab_bar_get_expand_tabs:
+ * @self: a #AdwTabBar
+ *
+ * Gets whether tabs should expand, see adw_tab_bar_set_expand_tabs().
+ *
+ * Returns: whether tabs should expand
+ *
+ * Since: 1.0
+ */
+gboolean
+adw_tab_bar_get_expand_tabs (AdwTabBar *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_BAR (self), FALSE);
+
+  return adw_tab_box_get_expand_tabs (self->box);
+}
+
+/**
+ * adw_tab_bar_set_expand_tabs:
+ * @self: a #AdwTabBar
+ * @expand_tabs: whether to expand tabs
+ *
+ * Sets whether tabs should expand.
+ *
+ * If @expand_tabs is %TRUE, the tabs will always vary width filling the whole
+ * width when possible, otherwise tabs will always have the minimum possible
+ * size.
+ *
+ * Expand is enabled by default.
+ *
+ * Since: 1.0
+ */
+void
+adw_tab_bar_set_expand_tabs (AdwTabBar *self,
+                             gboolean   expand_tabs)
+{
+  g_return_if_fail (ADW_IS_TAB_BAR (self));
+
+  expand_tabs = !!expand_tabs;
+
+  if (adw_tab_bar_get_expand_tabs (self) == expand_tabs)
+    return;
+
+  adw_tab_box_set_expand_tabs (self->box, expand_tabs);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_EXPAND_TABS]);
+}
+
+/**
+ * adw_tab_bar_get_inverted:
+ * @self: a #AdwTabBar
+ *
+ * Gets whether tabs use inverted layout, see adw_tab_bar_set_inverted().
+ *
+ * Returns: whether tabs use inverted layout
+ *
+ * Since: 1.0
+ */
+gboolean
+adw_tab_bar_get_inverted (AdwTabBar *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_BAR (self), FALSE);
+
+  return adw_tab_box_get_inverted (self->box);
+}
+
+/**
+ * adw_tab_bar_set_inverted:
+ * @self: a #AdwTabBar
+ * @inverted: whether tabs use inverted layout
+ *
+ * Sets whether tabs tabs use inverted layout.
+ *
+ * If @inverted is %TRUE, non-pinned tabs will have the close button at the
+ * beginning and the indicator at the end rather than the opposite.
+ *
+ * Since: 1.0
+ */
+void
+adw_tab_bar_set_inverted (AdwTabBar *self,
+                          gboolean   inverted)
+{
+  g_return_if_fail (ADW_IS_TAB_BAR (self));
+
+  inverted = !!inverted;
+
+  if (adw_tab_bar_get_inverted (self) == inverted)
+    return;
+
+  adw_tab_box_set_inverted (self->box, inverted);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INVERTED]);
+}
+
+/**
+ * adw_tab_bar_setup_extra_drop_target:
+ * @self: a #AdwTabBar
+ * @actions: the supported actions
+ * @types: (nullable) (transfer none) (array length=n_types):
+ *     all supported #GTypes that can be dropped
+ * @n_types: number of @types
+ *
+ * Sets the supported #GTypes for this drop target.
+ *
+ * Sets up an extra drop target on tabs.
+ *
+ * This allows to drag arbitrary content onto tabs, for example URLs in a web
+ * browser.
+ *
+ * If a tab is hovered for a certain period of time while dragging the content,
+ * it will be automatically selected.
+ *
+ * After content is dropped, the #AdwTabBar::extra-drag-data-received signal can
+ * be used to retrieve and process the drag data.
+ *
+ * Since: 1.0
+ */
+void
+adw_tab_bar_setup_extra_drop_target (AdwTabBar     *self,
+                                     GdkDragAction  actions,
+                                     GType         *types,
+                                     gsize          n_types)
+{
+  g_return_if_fail (ADW_IS_TAB_BAR (self));
+  g_return_if_fail (n_types == 0 || types != NULL);
+
+  adw_tab_box_setup_extra_drop_target (self->box, actions, types, n_types);
+  adw_tab_box_setup_extra_drop_target (self->pinned_box, actions, types, n_types);
+}
+
+/**
+ * adw_tab_bar_get_is_overflowing:
+ * @self: a #AdwTabBar
+ *
+ * Gets whether @self is overflowing.
+ *
+ * Returns: whether @self is overflowing
+ *
+ * Since: 1.0
+ */
+gboolean
+adw_tab_bar_get_is_overflowing (AdwTabBar *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_BAR (self), FALSE);
+
+  return self->is_overflowing;
+}
diff --git a/src/adw-tab-bar.h b/src/adw-tab-bar.h
new file mode 100644
index 0000000..c7135cb
--- /dev/null
+++ b/src/adw-tab-bar.h
@@ -0,0 +1,79 @@
+/*
+ * Copyright (C) 2020 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-enums.h"
+#include "adw-tab-view.h"
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_TAB_BAR (adw_tab_bar_get_type())
+
+ADW_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (AdwTabBar, adw_tab_bar, ADW, TAB_BAR, GtkWidget)
+
+ADW_AVAILABLE_IN_ALL
+AdwTabBar *adw_tab_bar_new (void);
+
+ADW_AVAILABLE_IN_ALL
+AdwTabView *adw_tab_bar_get_view (AdwTabBar *self);
+ADW_AVAILABLE_IN_ALL
+void        adw_tab_bar_set_view (AdwTabBar  *self,
+                                  AdwTabView *view);
+
+ADW_AVAILABLE_IN_ALL
+GtkWidget *adw_tab_bar_get_start_action_widget (AdwTabBar *self);
+ADW_AVAILABLE_IN_ALL
+void       adw_tab_bar_set_start_action_widget (AdwTabBar *self,
+                                                GtkWidget *widget);
+
+ADW_AVAILABLE_IN_ALL
+GtkWidget *adw_tab_bar_get_end_action_widget (AdwTabBar *self);
+ADW_AVAILABLE_IN_ALL
+void       adw_tab_bar_set_end_action_widget (AdwTabBar *self,
+                                              GtkWidget *widget);
+
+ADW_AVAILABLE_IN_ALL
+gboolean adw_tab_bar_get_autohide (AdwTabBar *self);
+ADW_AVAILABLE_IN_ALL
+void     adw_tab_bar_set_autohide (AdwTabBar *self,
+                                   gboolean   autohide);
+
+ADW_AVAILABLE_IN_ALL
+gboolean adw_tab_bar_get_tabs_revealed (AdwTabBar *self);
+
+ADW_AVAILABLE_IN_ALL
+gboolean adw_tab_bar_get_expand_tabs (AdwTabBar *self);
+ADW_AVAILABLE_IN_ALL
+void     adw_tab_bar_set_expand_tabs (AdwTabBar *self,
+                                      gboolean   expand_tabs);
+
+ADW_AVAILABLE_IN_ALL
+gboolean adw_tab_bar_get_inverted (AdwTabBar *self);
+ADW_AVAILABLE_IN_ALL
+void     adw_tab_bar_set_inverted (AdwTabBar *self,
+                                   gboolean   inverted);
+
+ADW_AVAILABLE_IN_ALL
+void adw_tab_bar_setup_extra_drop_target (AdwTabBar     *self,
+                                          GdkDragAction  actions,
+                                          GType         *types,
+                                          gsize          n_types);
+
+ADW_AVAILABLE_IN_ALL
+gboolean adw_tab_bar_get_is_overflowing (AdwTabBar *self);
+
+G_END_DECLS
diff --git a/src/adw-tab-bar.ui b/src/adw-tab-bar.ui
new file mode 100644
index 0000000..f92f419
--- /dev/null
+++ b/src/adw-tab-bar.ui
@@ -0,0 +1,77 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.24"/>
+  <template class="AdwTabBar" parent="GtkWidget">
+    <child>
+      <object class="GtkRevealer" id="revealer">
+        <property name="transition-duration">200</property>
+        <property name="transition-type">slide-down</property>
+        <child>
+          <object class="GtkBox">
+            <style>
+              <class name="box"/>
+            </style>
+            <child>
+              <object class="AdwBin" id="start_action_bin">
+                <property name="visible">False</property>
+                <style>
+                  <class name="start-action"/>
+                </style>
+              </object>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow" id="pinned_scrolled_window">
+                <property name="hscrollbar-policy">external</property>
+                <property name="vscrollbar-policy">never</property>
+                <property name="overlay-scrolling">False</property>
+                <property name="propagate-natural-width">True</property>
+                <property name="hexpand">False</property>
+                <style>
+                  <class name="pinned"/>
+                </style>
+                <child>
+                  <object class="AdwTabBox" id="pinned_box">
+                    <property name="pinned">True</property>
+                    <property name="tab-bar">AdwTabBar</property>
+                    <signal name="notify::needs-attention-left" handler="notify_needs_attention_pinned_cb" 
swapped="true"/>
+                    <signal name="notify::needs-attention-right" handler="notify_needs_attention_pinned_cb" 
swapped="true"/>
+                    <signal name="notify::resize-frozen" handler="notify_resize_frozen_cb" swapped="true"/>
+                    <signal name="stop-kinetic-scrolling" handler="stop_kinetic_scrolling_cb" 
object="pinned_scrolled_window" swapped="true"/>
+                    <signal name="extra-drag-drop" handler="extra_drag_drop_cb" swapped="true"/>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow" id="scrolled_window">
+                <property name="hscrollbar-policy">external</property>
+                <property name="vscrollbar-policy">never</property>
+                <property name="overlay-scrolling">False</property>
+                <property name="hexpand">True</property>
+                <property name="min-content-width">100</property>
+                <child>
+                  <object class="AdwTabBox" id="box">
+                    <property name="tab-bar">AdwTabBar</property>
+                    <signal name="notify::needs-attention-left" handler="notify_needs_attention_cb" 
swapped="true"/>
+                    <signal name="notify::needs-attention-right" handler="notify_needs_attention_cb" 
swapped="true"/>
+                    <signal name="notify::resize-frozen" handler="notify_resize_frozen_cb" swapped="true"/>
+                    <signal name="stop-kinetic-scrolling" handler="stop_kinetic_scrolling_cb" 
object="scrolled_window" swapped="true"/>
+                    <signal name="extra-drag-drop" handler="extra_drag_drop_cb" swapped="true"/>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="AdwBin" id="end_action_bin">
+                <property name="visible">False</property>
+                <style>
+                  <class name="end-action"/>
+                </style>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/adwaita.gresources.xml b/src/adwaita.gresources.xml
index 4a9398c..3353ec9 100644
--- a/src/adwaita.gresources.xml
+++ b/src/adwaita.gresources.xml
@@ -16,6 +16,7 @@
     <file preprocess="xml-stripblanks">adw-preferences-window.ui</file>
     <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-view-switcher-bar.ui</file>
     <file preprocess="xml-stripblanks">adw-view-switcher-button.ui</file>
     <file preprocess="xml-stripblanks">adw-view-switcher-title.ui</file>
diff --git a/src/adwaita.h b/src/adwaita.h
index 55db882..ff9115c 100644
--- a/src/adwaita.h
+++ b/src/adwaita.h
@@ -51,6 +51,7 @@ G_BEGIN_DECLS
 #include "adw-swipe-group.h"
 #include "adw-swipe-tracker.h"
 #include "adw-swipeable.h"
+#include "adw-tab-bar.h"
 #include "adw-tab-view.h"
 #include "adw-types.h"
 #include "adw-value-object.h"
diff --git a/src/meson.build b/src/meson.build
index 471b808..0ffdcf2 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -15,6 +15,7 @@ adw_public_enum_headers = [
   'adw-leaflet.h',
   'adw-navigation-direction.h',
   'adw-squeezer.h',
+  'adw-tab-bar.h',
   'adw-view-switcher.h',
 ]
 
@@ -94,6 +95,7 @@ src_headers = [
   'adw-swipe-group.h',
   'adw-swipe-tracker.h',
   'adw-swipeable.h',
+  'adw-tab-bar.h',
   'adw-tab-view.h',
   'adw-types.h',
   'adw-value-object.h',
@@ -152,6 +154,7 @@ src_sources = [
   'adw-swipe-tracker.c',
   'adw-swipeable.c',
   'adw-tab.c',
+  'adw-tab-bar.c',
   'adw-tab-box.c',
   'adw-tab-view.c',
   'adw-value-object.c',
diff --git a/tests/meson.build b/tests/meson.build
index 3c424f4..b99b054 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -39,6 +39,7 @@ test_names = [
   'test-squeezer',
   'test-status-page',
   'test-swipe-group',
+  'test-tab-bar',
   'test-tab-view',
   'test-value-object',
   'test-view-switcher',
diff --git a/tests/test-tab-bar.c b/tests/test-tab-bar.c
new file mode 100644
index 0000000..4f994f4
--- /dev/null
+++ b/tests/test-tab-bar.c
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * 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_bar_view (void)
+{
+  g_autoptr (AdwTabBar) bar = NULL;
+  g_autoptr (AdwTabView) view = NULL;
+
+  bar = g_object_ref_sink (ADW_TAB_BAR (adw_tab_bar_new ()));
+  g_assert_nonnull (bar);
+
+  notified = 0;
+  g_signal_connect (bar, "notify::view", G_CALLBACK (notify_cb), NULL);
+
+  g_object_get (bar, "view", &view, NULL);
+  g_assert_null (view);
+
+  adw_tab_bar_set_view (bar, NULL);
+  g_assert_cmpint (notified, ==, 0);
+
+  view = g_object_ref_sink (ADW_TAB_VIEW (adw_tab_view_new ()));
+  adw_tab_bar_set_view (bar, view);
+  g_assert_true (adw_tab_bar_get_view (bar) == view);
+  g_assert_cmpint (notified, ==, 1);
+
+  g_object_set (bar, "view", NULL, NULL);
+  g_assert_null (adw_tab_bar_get_view (bar));
+  g_assert_cmpint (notified, ==, 2);
+}
+
+static void
+test_adw_tab_bar_start_action_widget (void)
+{
+  g_autoptr (AdwTabBar) bar = NULL;
+  GtkWidget *widget = NULL;
+
+  bar = g_object_ref_sink (ADW_TAB_BAR (adw_tab_bar_new ()));
+  g_assert_nonnull (bar);
+
+  notified = 0;
+  g_signal_connect (bar, "notify::start-action-widget", G_CALLBACK (notify_cb), NULL);
+
+  g_object_get (bar, "start-action-widget", &widget, NULL);
+  g_assert_null (widget);
+
+  adw_tab_bar_set_start_action_widget (bar, NULL);
+  g_assert_cmpint (notified, ==, 0);
+
+  widget = gtk_button_new ();
+  adw_tab_bar_set_start_action_widget (bar, widget);
+  g_assert_true (adw_tab_bar_get_start_action_widget (bar) == widget);
+  g_assert_cmpint (notified, ==, 1);
+
+  g_object_set (bar, "start-action-widget", NULL, NULL);
+  g_assert_null (adw_tab_bar_get_start_action_widget (bar));
+  g_assert_cmpint (notified, ==, 2);
+}
+
+static void
+test_adw_tab_bar_end_action_widget (void)
+{
+  g_autoptr (AdwTabBar) bar = NULL;
+  GtkWidget *widget = NULL;
+
+  bar = g_object_ref_sink (ADW_TAB_BAR (adw_tab_bar_new ()));
+  g_assert_nonnull (bar);
+
+  notified = 0;
+  g_signal_connect (bar, "notify::end-action-widget", G_CALLBACK (notify_cb), NULL);
+
+  g_object_get (bar, "end-action-widget", &widget, NULL);
+  g_assert_null (widget);
+
+  adw_tab_bar_set_end_action_widget (bar, NULL);
+  g_assert_cmpint (notified, ==, 0);
+
+  widget = gtk_button_new ();
+  adw_tab_bar_set_end_action_widget (bar, widget);
+  g_assert_true (adw_tab_bar_get_end_action_widget (bar) == widget);
+  g_assert_cmpint (notified, ==, 1);
+
+  g_object_set (bar, "end-action-widget", NULL, NULL);
+  g_assert_null (adw_tab_bar_get_end_action_widget (bar));
+  g_assert_cmpint (notified, ==, 2);
+}
+
+static void
+test_adw_tab_bar_autohide (void)
+{
+  g_autoptr (AdwTabBar) bar = NULL;
+  gboolean autohide = FALSE;
+
+  bar = g_object_ref_sink (ADW_TAB_BAR (adw_tab_bar_new ()));
+  g_assert_nonnull (bar);
+
+  notified = 0;
+  g_signal_connect (bar, "notify::autohide", G_CALLBACK (notify_cb), NULL);
+
+  g_object_get (bar, "autohide", &autohide, NULL);
+  g_assert_true (autohide);
+
+  adw_tab_bar_set_autohide (bar, TRUE);
+  g_assert_cmpint (notified, ==, 0);
+
+  adw_tab_bar_set_autohide (bar, FALSE);
+  g_assert_false (adw_tab_bar_get_autohide (bar));
+  g_assert_cmpint (notified, ==, 1);
+
+  g_object_set (bar, "autohide", TRUE, NULL);
+  g_assert_true (adw_tab_bar_get_autohide (bar));
+  g_assert_cmpint (notified, ==, 2);
+}
+
+static void
+test_adw_tab_bar_tabs_revealed (void)
+{
+  g_autoptr (AdwTabBar) bar = NULL;
+  g_autoptr (AdwTabView) view = NULL;
+  gboolean tabs_revealed = FALSE;
+  AdwTabPage *page;
+
+  bar = g_object_ref_sink (ADW_TAB_BAR (adw_tab_bar_new ()));
+  g_assert_nonnull (bar);
+
+  notified = 0;
+  g_signal_connect (bar, "notify::tabs-revealed", G_CALLBACK (notify_cb), NULL);
+
+  g_object_get (bar, "tabs-revealed", &tabs_revealed, NULL);
+  g_assert_false (tabs_revealed);
+  g_assert_false (adw_tab_bar_get_tabs_revealed (bar));
+  g_assert_cmpint (notified, ==, 0);
+
+  adw_tab_bar_set_autohide (bar, FALSE);
+  g_assert_false (adw_tab_bar_get_tabs_revealed (bar));
+  g_assert_cmpint (notified, ==, 0);
+
+  view = g_object_ref_sink (ADW_TAB_VIEW (adw_tab_view_new ()));
+  adw_tab_bar_set_view (bar, view);
+  g_assert_true (adw_tab_bar_get_tabs_revealed (bar));
+  g_assert_cmpint (notified, ==, 1);
+
+  adw_tab_bar_set_autohide (bar, TRUE);
+  g_assert_false (adw_tab_bar_get_tabs_revealed (bar));
+  g_assert_cmpint (notified, ==, 2);
+
+  page = adw_tab_view_append_pinned (view, gtk_button_new ());
+  g_assert_true (adw_tab_bar_get_tabs_revealed (bar));
+  g_assert_cmpint (notified, ==, 3);
+
+  adw_tab_view_set_page_pinned (view, page, FALSE);
+  g_assert_false (adw_tab_bar_get_tabs_revealed (bar));
+  g_assert_cmpint (notified, ==, 4);
+
+  adw_tab_view_append (view, gtk_button_new ());
+  g_assert_true (adw_tab_bar_get_tabs_revealed (bar));
+  g_assert_cmpint (notified, ==, 5);
+
+  adw_tab_view_close_page (view, page);
+  g_assert_false (adw_tab_bar_get_tabs_revealed (bar));
+  g_assert_cmpint (notified, ==, 6);
+
+  adw_tab_bar_set_autohide (bar, FALSE);
+  g_assert_true (adw_tab_bar_get_tabs_revealed (bar));
+  g_assert_cmpint (notified, ==, 7);
+}
+
+static void
+test_adw_tab_bar_expand_tabs (void)
+{
+  g_autoptr (AdwTabBar) bar = NULL;
+  gboolean expand_tabs = FALSE;
+
+  bar = g_object_ref_sink (ADW_TAB_BAR (adw_tab_bar_new ()));
+  g_assert_nonnull (bar);
+
+  notified = 0;
+  g_signal_connect (bar, "notify::expand-tabs", G_CALLBACK (notify_cb), NULL);
+
+  g_object_get (bar, "expand-tabs", &expand_tabs, NULL);
+  g_assert_true (expand_tabs);
+
+  adw_tab_bar_set_expand_tabs (bar, TRUE);
+  g_assert_cmpint (notified, ==, 0);
+
+  adw_tab_bar_set_expand_tabs (bar, FALSE);
+  g_assert_false (adw_tab_bar_get_expand_tabs (bar));
+  g_assert_cmpint (notified, ==, 1);
+
+  g_object_set (bar, "expand-tabs", TRUE, NULL);
+  g_assert_true (adw_tab_bar_get_expand_tabs (bar));
+  g_assert_cmpint (notified, ==, 2);
+}
+
+static void
+test_adw_tab_bar_inverted (void)
+{
+  g_autoptr (AdwTabBar) bar = NULL;
+  gboolean inverted = FALSE;
+
+  bar = g_object_ref_sink (ADW_TAB_BAR (adw_tab_bar_new ()));
+  g_assert_nonnull (bar);
+
+  notified = 0;
+  g_signal_connect (bar, "notify::inverted", G_CALLBACK (notify_cb), NULL);
+
+  g_object_get (bar, "inverted", &inverted, NULL);
+  g_assert_false (inverted);
+
+  adw_tab_bar_set_inverted (bar, FALSE);
+  g_assert_cmpint (notified, ==, 0);
+
+  adw_tab_bar_set_inverted (bar, TRUE);
+  g_assert_true (adw_tab_bar_get_inverted (bar));
+  g_assert_cmpint (notified, ==, 1);
+
+  g_object_set (bar, "inverted", FALSE, NULL);
+  g_assert_false (adw_tab_bar_get_inverted (bar));
+  g_assert_cmpint (notified, ==, 2);
+}
+
+int
+main (int   argc,
+      char *argv[])
+{
+  gtk_test_init (&argc, &argv, NULL);
+  adw_init ();
+
+  g_test_add_func ("/Handy/TabBar/view", test_adw_tab_bar_view);
+  g_test_add_func ("/Handy/TabBar/start_action_widget", test_adw_tab_bar_start_action_widget);
+  g_test_add_func ("/Handy/TabBar/end_action_widget", test_adw_tab_bar_end_action_widget);
+  g_test_add_func ("/Handy/TabBar/autohide", test_adw_tab_bar_autohide);
+  g_test_add_func ("/Handy/TabBar/tabs_revealed", test_adw_tab_bar_tabs_revealed);
+  g_test_add_func ("/Handy/TabBar/expand_tabs", test_adw_tab_bar_expand_tabs);
+  g_test_add_func ("/Handy/TabBar/inverted", test_adw_tab_bar_inverted);
+
+  return g_test_run ();
+}


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