[libhandy/tabs: 1/62] t




commit 2167c9cf9eb7bdafa4ecaea0fd058d4044487683
Author: Alexander Mikhaylenko <alexm gnome org>
Date:   Wed Sep 2 20:52:25 2020 +0500

    t

 doc/meson.build                             |    4 +
 src/handy.gresources.xml                    |    3 +
 src/handy.h                                 |    2 +
 src/hdy-animation-private.h                 |   21 +
 src/hdy-animation.c                         |  156 ++
 src/hdy-css-private.h                       |    9 +
 src/hdy-css.c                               |   78 +-
 src/hdy-enums.c.in                          |    1 +
 src/hdy-header-bar.c                        |    1 +
 src/hdy-tab-bar-private.h                   |   19 +
 src/hdy-tab-bar.c                           |  622 +++++
 src/hdy-tab-bar.h                           |   58 +
 src/hdy-tab-bar.ui                          |   75 +
 src/hdy-tab-box-private.h                   |   41 +
 src/hdy-tab-box.c                           | 3318 +++++++++++++++++++++++++++
 src/hdy-tab-private.h                       |   45 +
 src/hdy-tab-view-private.h                  |   26 +
 src/hdy-tab-view.c                          | 1791 +++++++++++++++
 src/hdy-tab-view.h                          |  202 ++
 src/hdy-tab.c                               |  774 +++++++
 src/hdy-tab.ui                              |  135 ++
 src/icons/hdy-tab-icon-missing-symbolic.svg |   32 +
 src/meson.build                             |    8 +
 src/themes/Adwaita-dark.css                 |   72 +-
 src/themes/Adwaita.css                      |   72 +-
 src/themes/HighContrast.css                 |   72 +-
 src/themes/HighContrastInverse.css          |   72 +-
 src/themes/_Adwaita-base.scss               |  231 ++
 28 files changed, 7922 insertions(+), 18 deletions(-)
---
diff --git a/doc/meson.build b/doc/meson.build
index eeab57a4..e22d073e 100644
--- a/doc/meson.build
+++ b/doc/meson.build
@@ -19,6 +19,10 @@ private_headers = [
     'hdy-shadow-helper-private.h',
     'hdy-stackable-box-private.h',
     'hdy-swipe-tracker-private.h',
+    'hdy-tab-private.h',
+    'hdy-tab-bar-private.h',
+    'hdy-tab-box-private.h',
+    'hdy-tab-view-private.h',
     'hdy-types.h',
     'hdy-view-switcher-button-private.h',
     'hdy-window-handle-controller-private.h',
diff --git a/src/handy.gresources.xml b/src/handy.gresources.xml
index b96444b5..affa0e27 100644
--- a/src/handy.gresources.xml
+++ b/src/handy.gresources.xml
@@ -3,6 +3,7 @@
   <gresource prefix="/sm/puri/handy">
     <file preprocess="xml-stripblanks">icons/avatar-default-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/hdy-expander-arrow-symbolic.svg</file>
+    <file preprocess="xml-stripblanks">icons/hdy-tab-icon-missing-symbolic.svg</file>
     <file compressed="true">themes/Adwaita.css</file>
     <file compressed="true">themes/Adwaita-dark.css</file>
     <file compressed="true">themes/fallback.css</file>
@@ -21,6 +22,8 @@
     <file preprocess="xml-stripblanks">hdy-preferences-page.ui</file>
     <file preprocess="xml-stripblanks">hdy-preferences-window.ui</file>
     <file preprocess="xml-stripblanks">hdy-search-bar.ui</file>
+    <file preprocess="xml-stripblanks">hdy-tab.ui</file>
+    <file preprocess="xml-stripblanks">hdy-tab-bar.ui</file>
     <file preprocess="xml-stripblanks">hdy-view-switcher-bar.ui</file>
     <file preprocess="xml-stripblanks">hdy-view-switcher-button.ui</file>
     <file preprocess="xml-stripblanks">hdy-view-switcher-title.ui</file>
diff --git a/src/handy.h b/src/handy.h
index 1ea48a7e..4b59c598 100644
--- a/src/handy.h
+++ b/src/handy.h
@@ -49,6 +49,8 @@ G_BEGIN_DECLS
 #include "hdy-swipe-group.h"
 #include "hdy-swipe-tracker.h"
 #include "hdy-swipeable.h"
+#include "hdy-tab-bar.h"
+#include "hdy-tab-view.h"
 #include "hdy-title-bar.h"
 #include "hdy-types.h"
 #include "hdy-value-object.h"
diff --git a/src/hdy-animation-private.h b/src/hdy-animation-private.h
index f31002ad..8eb0400a 100644
--- a/src/hdy-animation-private.h
+++ b/src/hdy-animation-private.h
@@ -14,6 +14,27 @@
 
 G_BEGIN_DECLS
 
+#define HDY_TYPE_ANIMATION (hdy_animation_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyAnimation, hdy_animation, HDY, ANIMATION, GObject)
+
+typedef void (*HdyAnimationValueCallback) (gdouble value, gpointer user_data);
+typedef void (*HdyAnimationDoneCallback) (gpointer user_data);
+
+HdyAnimation *hdy_animation_new (GtkWidget                 *widget,
+                                 gdouble                    from,
+                                 gdouble                    to,
+                                 gint64                     duration,
+                                 HdyAnimationValueCallback  value_cb,
+                                 HdyAnimationDoneCallback   done_cb,
+                                 gpointer                   user_data);
+
+void hdy_animation_start (HdyAnimation *self);
+void hdy_animation_stop (HdyAnimation *self);
+
+gdouble hdy_animation_get_value (HdyAnimation *self);
+
 gdouble hdy_lerp (gdouble a, gdouble b, gdouble t);
+gdouble hdy_ease_in_cubic (gdouble t);
 
 G_END_DECLS
diff --git a/src/hdy-animation.c b/src/hdy-animation.c
index ce5bf64f..f5292f1b 100644
--- a/src/hdy-animation.c
+++ b/src/hdy-animation.c
@@ -18,6 +18,156 @@
  * Since: 0.0.11
  */
 
+struct _HdyAnimation
+{
+  GObject parent_instance;
+
+  GtkWidget *widget;
+
+  gdouble value;
+
+  gdouble value_from;
+  gdouble value_to;
+  gint64 duration;
+
+  gint64 start_time;
+  guint tick_cb_id;
+
+  HdyAnimationValueCallback value_cb;
+  HdyAnimationDoneCallback done_cb;
+  gpointer user_data;
+};
+
+G_DEFINE_TYPE (HdyAnimation, hdy_animation, G_TYPE_OBJECT)
+
+static void
+set_value (HdyAnimation *self,
+           gdouble       value)
+{
+  self->value = value;
+  self->value_cb (value, self->user_data);
+}
+
+static gboolean
+tick_cb (GtkWidget     *widget,
+         GdkFrameClock *frame_clock,
+         HdyAnimation  *self)
+{
+  gint64 frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000;
+  gdouble t = (gdouble) (frame_time - self->start_time) / self->duration;
+
+  if (t >= 1) {
+    self->tick_cb_id = 0;
+
+    set_value (self, self->value_to);
+    self->done_cb (self->user_data);
+
+    return G_SOURCE_REMOVE;
+  }
+
+  set_value (self, hdy_lerp (self->value_from, self->value_to, hdy_ease_out_cubic (t)));
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+hdy_animation_dispose (GObject *object)
+{
+  HdyAnimation *self = HDY_ANIMATION (object);
+
+  hdy_animation_stop (self);
+
+  G_OBJECT_CLASS (hdy_animation_parent_class)->finalize (object);
+}
+
+static void
+hdy_animation_class_init (HdyAnimationClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = hdy_animation_dispose;
+}
+
+static void
+hdy_animation_init (HdyAnimation *self)
+{
+}
+
+HdyAnimation *
+hdy_animation_new (GtkWidget                 *widget,
+                   gdouble                    from,
+                   gdouble                    to,
+                   gint64                     duration,
+                   HdyAnimationValueCallback  value_cb,
+                   HdyAnimationDoneCallback   done_cb,
+                   gpointer                   user_data)
+{
+  HdyAnimation *anim = g_object_new (HDY_TYPE_ANIMATION, NULL);
+
+  anim->widget = widget;
+  anim->value_from = from;
+  anim->value_to = to;
+  anim->duration = duration;
+  anim->value_cb = value_cb;
+  anim->done_cb = done_cb;
+  anim->user_data = user_data;
+
+  anim->value = from;
+
+  return anim;
+}
+
+void
+hdy_animation_start (HdyAnimation *self)
+{
+  g_return_if_fail (HDY_IS_ANIMATION (self));
+
+  if (!hdy_get_enable_animations (self->widget) ||
+      !gtk_widget_get_mapped (self->widget) ||
+      self->duration <= 0) {
+    set_value (self, self->value_to);
+
+    self->done_cb (self->user_data);
+
+    return;
+  }
+
+  if (self->tick_cb_id) {
+    gtk_widget_remove_tick_callback (self->widget, self->tick_cb_id);
+    self->tick_cb_id = 0;
+  }
+
+  self->start_time = gdk_frame_clock_get_frame_time (gtk_widget_get_frame_clock (self->widget)) / 1000;
+  self->tick_cb_id = gtk_widget_add_tick_callback (self->widget, (GtkTickCallback) tick_cb, self, NULL);
+
+  g_signal_connect_object (self->widget, "unmap", G_CALLBACK (hdy_animation_stop), self, G_CONNECT_SWAPPED);
+}
+
+void
+hdy_animation_stop (HdyAnimation *self)
+{
+  g_return_if_fail (HDY_IS_ANIMATION (self));
+
+  if (!self->tick_cb_id)
+    return;
+
+  gtk_widget_remove_tick_callback (self->widget, self->tick_cb_id);
+  self->tick_cb_id = 0;
+
+  g_signal_handlers_disconnect_by_func (self->widget, hdy_animation_stop, self);
+
+  self->done_cb (self->user_data);
+}
+
+gdouble
+hdy_animation_get_value (HdyAnimation *self)
+{
+  g_return_val_if_fail (HDY_IS_ANIMATION (self), 0.0);
+
+  return self->value;
+}
+
+
 /**
  * hdy_get_enable_animations:
  * @widget: a #GtkWidget
@@ -81,3 +231,9 @@ hdy_ease_out_cubic (gdouble t)
   gdouble p = t - 1;
   return p * p * p + 1;
 }
+
+gdouble
+hdy_ease_in_cubic (gdouble t)
+{
+  return t * t * t;
+}
diff --git a/src/hdy-css-private.h b/src/hdy-css-private.h
index d8190b57..09a5197c 100644
--- a/src/hdy-css-private.h
+++ b/src/hdy-css-private.h
@@ -22,4 +22,13 @@ void hdy_css_measure (GtkWidget      *widget,
 void hdy_css_size_allocate (GtkWidget     *widget,
                             GtkAllocation *allocation);
 
+void hdy_css_size_allocate_self (GtkWidget     *widget,
+                                 GtkAllocation *allocation);
+
+void hdy_css_size_allocate_children (GtkWidget     *widget,
+                                     GtkAllocation *allocation);
+
+void hdy_css_draw (GtkWidget *widget,
+                   cairo_t   *cr);
+
 G_END_DECLS
diff --git a/src/hdy-css.c b/src/hdy-css.c
index 7a056e29..5d6e54de 100644
--- a/src/hdy-css.c
+++ b/src/hdy-css.c
@@ -44,30 +44,96 @@ hdy_css_measure (GtkWidget      *widget,
                border.left + margin.left + padding.left +
                border.right + margin.right + padding.right;
   }
+
+  *minimum = MAX (*minimum, 0);
+  *natural = MAX (*natural, 0);
 }
 
 void
 hdy_css_size_allocate (GtkWidget     *widget,
                        GtkAllocation *allocation)
+{
+  hdy_css_size_allocate_self (widget, allocation);
+  hdy_css_size_allocate_children (widget, allocation);
+}
+
+void
+hdy_css_size_allocate_self (GtkWidget     *widget,
+                            GtkAllocation *allocation)
 {
   GtkStyleContext *style_context;
   GtkStateFlags state_flags;
-  GtkBorder border, margin, padding;
+  GtkBorder margin;
 
   /* Manually apply the border, the padding and the margin as we can't use the
    * private GtkGagdet.
    */
   style_context = gtk_widget_get_style_context (widget);
   state_flags = gtk_widget_get_state_flags (widget);
-  gtk_style_context_get_border (style_context, state_flags, &border);
+
   gtk_style_context_get_margin (style_context, state_flags, &margin);
+
+  allocation->width -= margin.left + margin.right;
+  allocation->height -= margin.top + margin.bottom;
+  allocation->x += margin.left;
+  allocation->y += margin.top;
+}
+
+void
+hdy_css_size_allocate_children (GtkWidget     *widget,
+                                GtkAllocation *allocation)
+{
+  GtkStyleContext *style_context;
+  GtkStateFlags state_flags;
+  GtkBorder border, padding;
+
+  /* Manually apply the border, the padding and the margin as we can't use the
+   * private GtkGagdet.
+   */
+  style_context = gtk_widget_get_style_context (widget);
+  state_flags = gtk_widget_get_state_flags (widget);
+
+  gtk_style_context_get_border (style_context, state_flags, &border);
   gtk_style_context_get_padding (style_context, state_flags, &padding);
+
   allocation->width -= border.left + border.right +
-                       margin.left + margin.right +
                        padding.left + padding.right;
   allocation->height -= border.top + border.bottom +
-                        margin.top + margin.bottom +
                         padding.top + padding.bottom;
-  allocation->x += border.left + margin.left + padding.left;
-  allocation->y += border.top + margin.top + padding.top;
+  allocation->x += border.left + padding.left;
+  allocation->y += border.top + padding.top;
+}
+
+void
+hdy_css_draw (GtkWidget *widget,
+              cairo_t   *cr)
+{
+  gint width = gtk_widget_get_allocated_width (widget);
+  gint height = gtk_widget_get_allocated_height (widget);
+  GtkStyleContext *style_context;
+
+  if (width <= 0 || height <= 0)
+    return;
+
+  /* Manually apply the border, the padding and the margin as we can't use the
+   * private GtkGagdet.
+   */
+  style_context = gtk_widget_get_style_context (widget);
+
+  gtk_render_background (style_context, cr, 0, 0, width, height);
+  gtk_render_frame (style_context, cr, 0, 0, width, height);
+
+  if (gtk_widget_has_visible_focus (widget)) {
+    GtkStateFlags state_flags;
+    GtkBorder border;
+
+    state_flags = gtk_widget_get_state_flags (widget);
+
+    gtk_style_context_get_border (style_context, state_flags, &border);
+
+    gtk_render_focus (style_context, cr,
+                      border.left, border.top,
+                      width - border.left - border.right,
+                      height - border.top - border.bottom);
+  }
 }
diff --git a/src/hdy-enums.c.in b/src/hdy-enums.c.in
index a6305554..8b773024 100644
--- a/src/hdy-enums.c.in
+++ b/src/hdy-enums.c.in
@@ -8,6 +8,7 @@
 #include "hdy-leaflet.h"
 #include "hdy-navigation-direction.h"
 #include "hdy-squeezer.h"
+#include "hdy-tab-bar.h"
 #include "hdy-view-switcher.h"
 
 /*** END file-header ***/
diff --git a/src/hdy-header-bar.c b/src/hdy-header-bar.c
index 32833006..73fd7ab8 100644
--- a/src/hdy-header-bar.c
+++ b/src/hdy-header-bar.c
@@ -1545,6 +1545,7 @@ hdy_header_bar_destroy (GtkWidget *widget)
 {
   HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (HDY_HEADER_BAR (widget));
 
+  g_print ("destroy?\n");
   if (priv->label_sizing_box) {
     gtk_widget_destroy (priv->label_sizing_box);
     g_clear_object (&priv->label_sizing_box);
diff --git a/src/hdy-tab-bar-private.h b/src/hdy-tab-bar-private.h
new file mode 100644
index 00000000..f3990c2d
--- /dev/null
+++ b/src/hdy-tab-bar-private.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-tab-bar.h"
+
+G_BEGIN_DECLS
+
+gboolean hdy_tab_bar_tabs_have_visible_focus (HdyTabBar *self);
+
+G_END_DECLS
diff --git a/src/hdy-tab-bar.c b/src/hdy-tab-bar.c
new file mode 100644
index 00000000..02abbcc6
--- /dev/null
+++ b/src/hdy-tab-bar.c
@@ -0,0 +1,622 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-tab-bar-private.h"
+#include "hdy-tab-box-private.h"
+
+struct _HdyTabBar
+{
+  GtkBin parent_instance;
+
+  GtkRevealer *revealer;
+  HdyTabBox *pinned_box;
+  HdyTabBox *scroll_box;
+  GtkViewport *viewport;
+  GtkScrolledWindow *scrolled_window;
+  GtkBin *start_action_bin;
+  GtkBin *end_action_bin;
+
+  HdyTabView *view;
+  HdyTabBarPosition position;
+};
+
+static void hdy_tab_bar_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyTabBar, hdy_tab_bar, GTK_TYPE_BIN,
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
+                         hdy_tab_bar_buildable_init))
+
+enum {
+  PROP_0,
+  PROP_VIEW,
+  PROP_START_ACTION_WIDGET,
+  PROP_END_ACTION_WIDGET,
+  PROP_POSITION,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void
+update_autohide_cb (HdyTabBar *self)
+{
+  guint n_tabs = 0, n_pinned_tabs = 0;
+  gboolean is_dragging;
+
+  if (!self->view) {
+    gtk_revealer_set_reveal_child (self->revealer, FALSE);
+
+    return;
+  }
+
+  n_tabs = hdy_tab_view_get_n_pages (self->view);
+  n_pinned_tabs = hdy_tab_view_get_n_pinned_pages (self->view);
+  is_dragging = hdy_tab_view_get_is_dragging (self->view);
+
+  gtk_revealer_set_reveal_child (self->revealer,
+                                 n_tabs > 1 ||
+                                 n_pinned_tabs >= 1 ||
+                                 is_dragging);
+}
+
+static void
+notify_selected_page_cb (HdyTabBar *self)
+{
+  HdyTabPage *page = hdy_tab_view_get_selected_page (self->view);
+
+  if (hdy_tab_page_get_pinned (page)) {
+    hdy_tab_box_select_page (self->pinned_box, page);
+    hdy_tab_box_select_page (self->scroll_box, page);
+  } else {
+    hdy_tab_box_select_page (self->scroll_box, page);
+    hdy_tab_box_select_page (self->pinned_box, page);
+  }
+
+  /* FIXME HACK elementary active tab shadows */
+/*
+  var animation = new Animation (this, 0, 1, 300, () => {
+      pinned_box.queue_draw ();
+      scroll_box.queue_draw ();
+  }, null);
+  animation.start ();
+*/
+}
+
+static void
+page_pinned_cb (HdyTabBar  *self,
+                HdyTabPage *page)
+{
+  gboolean should_focus = hdy_tab_box_is_page_focused (self->scroll_box, page);
+
+  hdy_tab_box_remove_page (self->scroll_box, page);
+  hdy_tab_box_add_page (self->pinned_box, page,
+                        hdy_tab_view_get_n_pinned_pages (self->view));
+
+  if (should_focus)
+    hdy_tab_box_try_focus_selected_tab (self->pinned_box);
+}
+
+static void
+page_unpinned_cb (HdyTabBar  *self,
+                  HdyTabPage *page)
+{
+  gboolean should_focus = hdy_tab_box_is_page_focused (self->pinned_box, page);
+
+  hdy_tab_box_remove_page (self->pinned_box, page);
+  hdy_tab_box_add_page (self->scroll_box, page,
+                        hdy_tab_view_get_n_pinned_pages (self->view));
+
+  if (should_focus)
+    hdy_tab_box_try_focus_selected_tab (self->scroll_box);
+}
+
+static void
+notify_needs_attention_cb (HdyTabBar *self)
+{
+  GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  gboolean left, right;
+
+  g_object_get (self->scroll_box,
+                "needs-attention-left", &left,
+                "needs-attention-right", &right,
+                NULL);
+
+  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");
+
+  /* FIXME HACK: Undershoot indicator doesn't redraw on ist own, do a
+   * manual animation */
+/*
+  var animation = new Animation (this, 0, 1, 300, () => {
+      pinned_box.queue_draw ();
+      scroll_box.queue_draw ();
+  }, null);
+  animation.start ();
+*/
+}
+
+static void
+stop_kinetic_scrolling_cb (HdyTabBar *self)
+{
+  /* 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 (self->scrolled_window, FALSE);
+  gtk_scrolled_window_set_kinetic_scrolling (self->scrolled_window, TRUE);
+}
+
+static void
+view_destroy_cb (HdyTabBar *self)
+{
+  hdy_tab_bar_set_view (self, NULL);
+}
+
+static void
+destroy_widget (GtkWidget *widget,
+                gpointer   user_data)
+{
+  gtk_widget_destroy (widget);
+}
+
+static void
+hdy_tab_bar_destroy (GtkWidget *widget)
+{
+  gtk_container_forall (GTK_CONTAINER (widget), destroy_widget, NULL);
+
+  GTK_WIDGET_CLASS (hdy_tab_bar_parent_class)->destroy (widget);
+}
+
+static gboolean
+hdy_tab_bar_focus (GtkWidget        *widget,
+                   GtkDirectionType  direction)
+{
+  HdyTabBar *self = HDY_TAB_BAR (widget);
+  gboolean is_rtl;
+  GtkDirectionType start, end;
+
+  if (!self->view)
+    return GDK_EVENT_PROPAGATE;
+
+  if (!gtk_container_get_focus_child (GTK_CONTAINER (self)))
+    return gtk_widget_child_focus (GTK_WIDGET (self->pinned_box), direction) ||
+           gtk_widget_child_focus (GTK_WIDGET (self->scroll_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 (hdy_tab_view_select_previous_page (self->view))
+          return GDK_EVENT_STOP;
+
+      return gtk_widget_keynav_failed (widget, direction);
+  }
+
+  if (direction == end) {
+      if (hdy_tab_view_select_next_page (self->view))
+          return GDK_EVENT_STOP;
+
+      return gtk_widget_keynav_failed (widget, direction);
+  }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+hdy_tab_bar_size_allocate (GtkWidget     *widget,
+                           GtkAllocation *allocation)
+{
+  HdyTabBar *self = HDY_TAB_BAR (widget);
+
+  /* On RTL, the adjustment value is modified and will interfere with animations */
+  hdy_tab_box_set_block_scrolling (self->scroll_box, TRUE);
+
+  GTK_WIDGET_CLASS (hdy_tab_bar_parent_class)->size_allocate (widget,
+                                                              allocation);
+
+  hdy_tab_box_set_block_scrolling (self->scroll_box, FALSE);
+}
+
+static void
+hdy_tab_bar_forall (GtkContainer *container,
+                    gboolean      include_internals,
+                    GtkCallback   callback,
+                    gpointer      callback_data)
+{
+  HdyTabBar *self = HDY_TAB_BAR (container);
+  GtkWidget *start, *end;
+
+  if (include_internals) {
+    GTK_CONTAINER_CLASS (hdy_tab_bar_parent_class)->forall (container,
+                                                            include_internals,
+                                                            callback,
+                                                            callback_data);
+
+    return;
+  }
+
+  start = hdy_tab_bar_get_start_action_widget (self);
+  end = hdy_tab_bar_get_end_action_widget (self);
+
+  if (start)
+    callback (start, callback_data);
+
+  if (end)
+    callback (end, callback_data);
+}
+
+static void
+hdy_tab_bar_dispose (GObject *object)
+{
+  HdyTabBar *self = HDY_TAB_BAR (object);
+
+  hdy_tab_bar_set_view (self, NULL);
+
+  G_OBJECT_CLASS (hdy_tab_bar_parent_class)->dispose (object);
+}
+
+static void
+hdy_tab_bar_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  HdyTabBar *self = HDY_TAB_BAR (object);
+
+  switch (prop_id) {
+  case PROP_VIEW:
+    g_value_set_object (value, hdy_tab_bar_get_view (self));
+    break;
+
+  case PROP_START_ACTION_WIDGET:
+    g_value_set_object (value, hdy_tab_bar_get_start_action_widget (self));
+    break;
+
+  case PROP_END_ACTION_WIDGET:
+    g_value_set_object (value, hdy_tab_bar_get_end_action_widget (self));
+    break;
+
+  case PROP_POSITION:
+    g_value_set_enum (value, hdy_tab_bar_get_position (self));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+hdy_tab_bar_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  HdyTabBar *self = HDY_TAB_BAR (object);
+
+  switch (prop_id) {
+  case PROP_VIEW:
+    hdy_tab_bar_set_view (self, g_value_get_object (value));
+    break;
+
+  case PROP_START_ACTION_WIDGET:
+    hdy_tab_bar_set_start_action_widget (self, g_value_get_object (value));
+    break;
+
+  case PROP_END_ACTION_WIDGET:
+    hdy_tab_bar_set_end_action_widget (self, g_value_get_object (value));
+    break;
+
+  case PROP_POSITION:
+    hdy_tab_bar_set_position (self, g_value_get_enum (value));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+hdy_tab_bar_class_init (HdyTabBarClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->dispose = hdy_tab_bar_dispose;
+  object_class->get_property = hdy_tab_bar_get_property;
+  object_class->set_property = hdy_tab_bar_set_property;
+
+  widget_class->destroy = hdy_tab_bar_destroy;
+  widget_class->focus = hdy_tab_bar_focus;
+  widget_class->size_allocate = hdy_tab_bar_size_allocate;
+
+  container_class->forall = hdy_tab_bar_forall;
+
+  props[PROP_VIEW] =
+    g_param_spec_object ("view",
+                         _("View"),
+                         _("View"),
+                         HDY_TYPE_TAB_VIEW,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_START_ACTION_WIDGET] =
+    g_param_spec_object ("start-action-widget",
+                         _("Start Action Widget"),
+                         _("Start Action Widget"),
+                         GTK_TYPE_WIDGET,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_END_ACTION_WIDGET] =
+    g_param_spec_object ("end-action-widget",
+                         _("End Action Widget"),
+                         _("End Action Widget"),
+                         GTK_TYPE_WIDGET,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_POSITION] =
+    g_param_spec_enum ("position",
+                       _("Position"),
+                       _("Position"),
+                       HDY_TYPE_TAB_BAR_POSITION,
+                       HDY_TAB_BAR_POSITION_TOP,
+                       G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/sm/puri/handy/ui/hdy-tab-bar.ui");
+  gtk_widget_class_bind_template_child (widget_class, HdyTabBar, revealer);
+  gtk_widget_class_bind_template_child (widget_class, HdyTabBar, pinned_box);
+  gtk_widget_class_bind_template_child (widget_class, HdyTabBar, scroll_box);
+  gtk_widget_class_bind_template_child (widget_class, HdyTabBar, viewport);
+  gtk_widget_class_bind_template_child (widget_class, HdyTabBar, scrolled_window);
+  gtk_widget_class_bind_template_child (widget_class, HdyTabBar, start_action_bin);
+  gtk_widget_class_bind_template_child (widget_class, HdyTabBar, end_action_bin);
+  gtk_widget_class_bind_template_callback (widget_class, notify_needs_attention_cb);
+  gtk_widget_class_bind_template_callback (widget_class, stop_kinetic_scrolling_cb);
+
+  gtk_widget_class_set_css_name (widget_class, "tabbar");
+}
+
+static void
+hdy_tab_bar_init (HdyTabBar *self)
+{
+  g_type_ensure (HDY_TYPE_TAB_BOX);
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  hdy_tab_box_set_adjustment (self->scroll_box,
+                              gtk_scrolled_window_get_hadjustment (self->scrolled_window));
+
+  /* HdyTabBox scrolls on focus itself, and does it better than GtkViewport */
+  gtk_container_set_focus_hadjustment (GTK_CONTAINER (self->viewport), NULL);
+}
+
+static void
+hdy_tab_bar_buildable_add_child (GtkBuildable *buildable,
+                                 GtkBuilder   *builder,
+                                 GObject      *child,
+                                 const gchar  *type)
+{
+  HdyTabBar *self = HDY_TAB_BAR (buildable);
+
+  if (!self->revealer) {
+    gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (child));
+
+    return;
+  }
+
+  if (!type || !g_strcmp0 (type, "start"))
+    hdy_tab_bar_set_start_action_widget (self, GTK_WIDGET (child));
+  else if (!g_strcmp0 (type, "end"))
+    hdy_tab_bar_set_end_action_widget (self, GTK_WIDGET (child));
+  else
+    GTK_BUILDER_WARN_INVALID_CHILD_TYPE (HDY_TAB_BAR (self), type);
+}
+
+static void
+hdy_tab_bar_buildable_init (GtkBuildableIface *iface)
+{
+  iface->add_child = hdy_tab_bar_buildable_add_child;
+}
+
+gboolean
+hdy_tab_bar_tabs_have_visible_focus (HdyTabBar *self)
+{
+  GtkWidget *pinned_focus_child, *scroll_focus_child;
+
+  pinned_focus_child = gtk_container_get_focus_child (GTK_CONTAINER (self->pinned_box));
+  scroll_focus_child = gtk_container_get_focus_child (GTK_CONTAINER (self->scroll_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;
+}
+
+HdyTabBar *
+hdy_tab_bar_new (void)
+{
+  return g_object_new (HDY_TYPE_TAB_BAR, NULL);
+}
+
+HdyTabView *
+hdy_tab_bar_get_view (HdyTabBar *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_BAR (self), NULL);
+
+  return self->view;
+}
+
+void
+hdy_tab_bar_set_view (HdyTabBar  *self,
+                      HdyTabView *view)
+{
+  g_return_if_fail (HDY_IS_TAB_BAR (self));
+  g_return_if_fail (HDY_IS_TAB_VIEW (view) || view == NULL);
+
+  if (self->view == view)
+    return;
+
+  if (self->view) {
+    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_pinned_cb, self);
+    g_signal_handlers_disconnect_by_func (self->view, page_unpinned_cb, self);
+  }
+
+  g_set_object (&self->view, view);
+
+  if (self->view) {
+    g_signal_connect_object (self->view, "notify::is-dragging",
+                             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-pinned",
+                             G_CALLBACK (page_pinned_cb), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->view, "page-unpinned",
+                             G_CALLBACK (page_unpinned_cb), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->view, "destroy",
+                             G_CALLBACK (view_destroy_cb), self,
+                             G_CONNECT_SWAPPED);
+  }
+
+  hdy_tab_box_set_view (self->pinned_box, view);
+  hdy_tab_box_set_view (self->scroll_box, view);
+
+  update_autohide_cb (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW]);
+}
+
+GtkWidget *
+hdy_tab_bar_get_start_action_widget (HdyTabBar *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_BAR (self), NULL);
+
+  return gtk_bin_get_child (self->start_action_bin);
+}
+
+void
+hdy_tab_bar_set_start_action_widget (HdyTabBar *self,
+                                     GtkWidget *widget)
+{
+  GtkWidget *old_widget;
+
+  g_return_if_fail (HDY_IS_TAB_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget) || widget == NULL);
+
+  old_widget = gtk_bin_get_child (self->start_action_bin);
+
+  if (old_widget == widget)
+    return;
+
+  if (old_widget)
+    gtk_container_remove (GTK_CONTAINER (self->start_action_bin), old_widget);
+
+  if (widget)
+    gtk_container_add (GTK_CONTAINER (self->start_action_bin), widget);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_START_ACTION_WIDGET]);
+}
+
+GtkWidget *
+hdy_tab_bar_get_end_action_widget (HdyTabBar *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_BAR (self), NULL);
+
+  return gtk_bin_get_child (self->end_action_bin);
+}
+
+void
+hdy_tab_bar_set_end_action_widget (HdyTabBar *self,
+                                   GtkWidget *widget)
+{
+  GtkWidget *old_widget;
+
+  g_return_if_fail (HDY_IS_TAB_BAR (self));
+  g_return_if_fail (GTK_IS_WIDGET (widget) || widget == NULL);
+
+  old_widget = gtk_bin_get_child (self->end_action_bin);
+
+  if (old_widget == widget)
+    return;
+
+  if (old_widget)
+    gtk_container_remove (GTK_CONTAINER (self->end_action_bin), old_widget);
+
+  if (widget)
+    gtk_container_add (GTK_CONTAINER (self->end_action_bin), widget);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_END_ACTION_WIDGET]);
+}
+
+HdyTabBarPosition
+hdy_tab_bar_get_position (HdyTabBar *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_BAR (self), 0);
+
+  return self->position;
+}
+
+void
+hdy_tab_bar_set_position (HdyTabBar         *self,
+                          HdyTabBarPosition  position)
+{
+  GtkStyleContext *context;
+
+  g_return_if_fail (HDY_IS_TAB_BAR (self));
+
+  if (self->position == position)
+    return;
+
+  context = gtk_widget_get_style_context (GTK_WIDGET (self));
+
+  self->position = position;
+
+  switch (position) {
+  case HDY_TAB_BAR_POSITION_TOP:
+    gtk_style_context_add_class (context, "top");
+    gtk_style_context_remove_class (context, "bottom");
+    gtk_revealer_set_transition_type (self->revealer,
+                                      GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
+    break;
+
+  case HDY_TAB_BAR_POSITION_BOTTOM:
+    gtk_style_context_add_class (context, "bottom");
+    gtk_style_context_remove_class (context, "top");
+    gtk_revealer_set_transition_type (self->revealer,
+                                      GTK_REVEALER_TRANSITION_TYPE_SLIDE_UP);
+    break;
+
+  default:
+    g_assert_not_reached ();
+  }
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POSITION]);
+}
diff --git a/src/hdy-tab-bar.h b/src/hdy-tab-bar.h
new file mode 100644
index 00000000..b1ec9c19
--- /dev/null
+++ b/src/hdy-tab-bar.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-enums.h"
+#include "hdy-tab-view.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_TAB_BAR (hdy_tab_bar_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyTabBar, hdy_tab_bar, HDY, TAB_BAR, GtkBin)
+
+typedef enum {
+  HDY_TAB_BAR_POSITION_TOP,
+  HDY_TAB_BAR_POSITION_BOTTOM,
+} HdyTabBarPosition;
+
+HDY_AVAILABLE_IN_ALL
+HdyTabBar *hdy_tab_bar_new (void);
+
+HDY_AVAILABLE_IN_ALL
+HdyTabView *hdy_tab_bar_get_view (HdyTabBar *self);
+HDY_AVAILABLE_IN_ALL
+void        hdy_tab_bar_set_view (HdyTabBar  *self,
+                                  HdyTabView *view);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_tab_bar_get_start_action_widget (HdyTabBar *self);
+HDY_AVAILABLE_IN_ALL
+void       hdy_tab_bar_set_start_action_widget (HdyTabBar *self,
+                                                GtkWidget *widget);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_tab_bar_get_end_action_widget (HdyTabBar *self);
+HDY_AVAILABLE_IN_ALL
+void       hdy_tab_bar_set_end_action_widget (HdyTabBar *self,
+                                              GtkWidget *widget);
+
+HDY_AVAILABLE_IN_ALL
+HdyTabBarPosition hdy_tab_bar_get_position (HdyTabBar *self);
+HDY_AVAILABLE_IN_ALL
+void              hdy_tab_bar_set_position (HdyTabBar         *self,
+                                            HdyTabBarPosition  position);
+
+G_END_DECLS
diff --git a/src/hdy-tab-bar.ui b/src/hdy-tab-bar.ui
new file mode 100644
index 00000000..f63b2dc6
--- /dev/null
+++ b/src/hdy-tab-bar.ui
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.24"/>
+  <template class="HdyTabBar" parent="GtkBin">
+    <style>
+      <class name="top"/>
+    </style>
+    <child>
+      <object class="GtkRevealer" id="revealer">
+        <property name="visible">True</property>
+        <property name="transition-duration">200</property>
+        <property name="transition-type">slide-down</property>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <style>
+              <class name="box"/>
+            </style>
+            <child>
+              <object class="GtkEventBox" id="start_action_bin">
+                <property name="visible">True</property>
+                <style>
+                  <class name="start-action"/>
+                </style>
+              </object>
+            </child>
+            <child>
+              <object class="HdyTabBox" id="pinned_box">
+                <property name="visible">True</property>
+                <property name="pinned">True</property>
+                <property name="hexpand">False</property>
+                <property name="tab-bar">HdyTabBar</property>
+                <style>
+                  <class name="pinned"/>
+                </style>
+              </object>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow" id="scrolled_window">
+                <property name="visible">True</property>
+                <property name="can-focus">False</property>
+                <property name="hscrollbar-policy">external</property>
+                <property name="vscrollbar-policy">never</property>
+                <property name="overlay-scrolling">False</property>
+                <property name="hexpand">True</property>
+                <child>
+                  <object class="GtkViewport" id="viewport">
+                    <property name="visible">True</property>
+                    <child>
+                      <object class="HdyTabBox" id="scroll_box">
+                        <property name="visible">True</property>
+                        <property name="tab-bar">HdyTabBar</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="stop-kinetic-scrolling" handler="stop_kinetic_scrolling_cb" 
swapped="true"/>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkEventBox" id="end_action_bin">
+                <property name="visible">True</property>
+                <style>
+                  <class name="end-action"/>
+                </style>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/hdy-tab-box-private.h b/src/hdy-tab-box-private.h
new file mode 100644
index 00000000..8f5ac06a
--- /dev/null
+++ b/src/hdy-tab-box-private.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include "hdy-tab-view.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_TAB_BOX (hdy_tab_box_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyTabBox, hdy_tab_box, HDY, TAB_BOX, GtkContainer)
+
+void hdy_tab_box_set_view (HdyTabBox  *self,
+                           HdyTabView *view);
+void hdy_tab_box_set_adjustment (HdyTabBox     *self,
+                                 GtkAdjustment *adjustment);
+void hdy_tab_box_set_block_scrolling (HdyTabBox *self,
+                                      gboolean   block_scrolling);
+
+void hdy_tab_box_add_page (HdyTabBox  *self,
+                           HdyTabPage *page,
+                           guint       position);
+void hdy_tab_box_remove_page (HdyTabBox  *self,
+                              HdyTabPage *page);
+void hdy_tab_box_select_page (HdyTabBox  *self,
+                              HdyTabPage *page);
+
+void hdy_tab_box_try_focus_selected_tab (HdyTabBox  *self);
+gboolean hdy_tab_box_is_page_focused (HdyTabBox  *self,
+                                      HdyTabPage *page);
+
+G_END_DECLS
diff --git a/src/hdy-tab-box.c b/src/hdy-tab-box.c
new file mode 100644
index 00000000..f2c58414
--- /dev/null
+++ b/src/hdy-tab-box.c
@@ -0,0 +1,3318 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-tab-box-private.h"
+#include "hdy-animation-private.h"
+#include "hdy-css-private.h"
+#include "hdy-tab-private.h"
+#include "hdy-tab-bar-private.h"
+#include "hdy-tab-view-private.h"
+#include <math.h>
+
+/* Border collapsing without glitches */
+#define OVERLAP 1
+#define DND_THRESHOLD_MULTIPLIER 4
+
+#define AUTOSCROLL_AREA_WIDTH 65
+#define AUTOSCROLL_SPEED 2.5
+
+#define OPEN_ANIMATION_DURATION 200
+#define CLOSE_ANIMATION_DURATION 200
+#define FOCUS_ANIMATION_DURATION 200
+#define SCROLL_ANIMATION_DURATION 200
+#define RESIZE_ANIMATION_DURATION 200
+#define REORDER_ANIMATION_DURATION 250
+#define ICON_RESIZE_ANIMATION_DURATION 200
+
+typedef enum {
+  TAB_RESIZE_NORMAL,
+  TAB_RESIZE_FIXED_TAB_WIDTH,
+  TAB_RESIZE_FIXED_END_PADDING
+} TabResizeMode;
+
+static const GtkTargetEntry src_targets [] = {
+  { "HDY_TAB", GTK_TARGET_SAME_APP, 0 },
+  { "application/x-rootwindow-drop", 0, 0 },
+};
+
+static const GtkTargetEntry dst_targets [] = {
+  { "HDY_TAB", GTK_TARGET_SAME_APP, 0 },
+};
+
+typedef struct {
+  GtkWidget *window;
+  GdkDragContext *context;
+
+  HdyTab *tab;
+  GtkBorder tab_margin;
+
+  gint hotspot_x;
+  gint hotspot_y;
+
+  gint width;
+  gint target_width;
+  HdyAnimation *resize_animation;
+} DragIcon;
+
+typedef struct {
+  HdyTabPage *page;
+  HdyTab *tab;
+
+  gint pos;
+  gint width;
+  gint last_width;
+
+  gdouble end_reorder_offset;
+  gdouble reorder_offset;
+
+  HdyAnimation *reorder_animation;
+  gboolean reorder_ignore_bounds;
+
+  gdouble appear_progress;
+  HdyAnimation *appear_animation;
+
+  gulong notify_needs_attention_id;
+} TabInfo;
+
+struct _HdyTabBox
+{
+  GtkContainer parent_instance;
+
+  gboolean pinned;
+  HdyTabBar *tab_bar;
+  HdyTabView *view;
+  GtkAdjustment *adjustment;
+  gboolean needs_attention_left;
+  gboolean needs_attention_right;
+
+  GList *tabs;
+  gint n_tabs;
+
+  GdkWindow *window;
+  GdkWindow *reorder_window;
+
+  GtkMenu *context_menu;
+  GtkPopover *touch_menu;
+  GtkGesture *touch_menu_gesture;
+
+  gint allocated_width;
+  gint last_width;
+  gint end_padding;
+  gint initial_end_padding;
+  TabResizeMode tab_resize_mode;
+  HdyAnimation *resize_animation;
+
+  TabInfo *selected_tab;
+
+  gboolean hovering;
+  gdouble hover_x;
+  gdouble hover_y;
+  TabInfo *hovered_tab;
+
+  TabInfo *reordered_tab;
+  HdyAnimation *reorder_animation;
+  gboolean pressed;
+
+  gint reorder_x;
+  gint reorder_y;
+  guint reorder_index;
+  gint reorder_window_x;
+  gboolean continue_reorder;
+  gboolean indirect_reordering;
+  gint pressed_button;
+
+  gboolean dragging;
+  gdouble drag_begin_x;
+  gdouble drag_begin_y;
+  gdouble drag_offset_x;
+  gdouble drag_offset_y;
+  GdkSeat *drag_seat;
+
+  guint drag_autoscroll_cb_id;
+  gint64 drag_autoscroll_prev_time;
+
+  HdyTabPage *detached_page;
+  guint detached_index;
+  TabInfo *reorder_placeholder;
+  HdyTabPage *placeholder_page;
+  gboolean can_remove_placeholder;
+  DragIcon *drag_icon;
+  gboolean should_detach_into_new_window;
+  GtkTargetList *source_targets;
+
+  HdyAnimation *scroll_animation;
+  gboolean scroll_animation_done;
+  gdouble scroll_animation_from;
+  gdouble scroll_animation_offset;
+  TabInfo *scroll_animation_tab;
+  gboolean block_scrolling;
+  gdouble adjustment_prev_value;
+};
+
+G_DEFINE_TYPE (HdyTabBox, hdy_tab_box, GTK_TYPE_CONTAINER)
+
+enum {
+  PROP_0,
+  PROP_PINNED,
+  PROP_TAB_BAR,
+  PROP_VIEW,
+  PROP_ADJUSTMENT,
+  PROP_NEEDS_ATTENTION_LEFT,
+  PROP_NEEDS_ATTENTION_RIGHT,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+  SIGNAL_STOP_KINETIC_SCROLLING,
+  SIGNAL_ACTIVATE_TAB,
+  SIGNAL_FOCUS_TAB,
+  SIGNAL_REORDER_TAB,
+  SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+/* Helpers */
+
+static void
+remove_and_free_tab_info (TabInfo *info)
+{
+  gtk_widget_unparent (GTK_WIDGET (info->tab));
+
+  g_free (info);
+}
+
+static inline gint
+get_tab_position (HdyTabBox *self,
+                  TabInfo   *info)
+{
+  if (info == self->reordered_tab) {
+    gint pos = 0;
+    gdk_window_get_position (self->reorder_window, &pos, NULL);
+
+    return pos;
+  }
+
+  return info->pos;
+}
+
+static inline TabInfo *
+find_tab_info_at (HdyTabBox *self,
+                  double     x)
+{
+  GList *l;
+
+  if (self->reordered_tab) {
+    gint pos = get_tab_position (self, self->reordered_tab);
+
+    if (pos <= x && x < pos + self->reordered_tab->width)
+      return self->reordered_tab;
+  }
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (info != self->reordered_tab &&
+        info->pos <= x && x < info->pos + info->width)
+      return info;
+  }
+
+  return NULL;
+}
+
+static inline GList *
+find_link_for_page (HdyTabBox  *self,
+                    HdyTabPage *page)
+{
+  GList *l;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (info->page == page)
+      return l;
+  }
+
+  return NULL;
+}
+
+static inline TabInfo *
+find_info_for_page (HdyTabBox  *self,
+                    HdyTabPage *page)
+{
+  GList *l = find_link_for_page (self, page);
+
+  return l ? l->data : NULL;
+}
+
+static GList *
+find_nth_alive_tab (HdyTabBox *self,
+                    uint       position)
+{
+  GList *l;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (!info->page)
+        continue;
+
+    if (!position--)
+        return l;
+  }
+
+  return NULL;
+}
+
+static inline gint
+calculate_tab_width (TabInfo *info,
+                     gint     base_width)
+{
+  return OVERLAP + (gint) ((base_width - OVERLAP) * info->appear_progress);
+}
+
+static gint
+get_base_tab_width (HdyTabBox *self)
+{
+  gdouble max_progress = 0;
+  gdouble n = 0;
+  gdouble used_width;
+  GList *l;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    max_progress = MAX (max_progress, info->appear_progress);
+    n += info->appear_progress;
+  }
+
+  used_width = (self->allocated_width + (n + 1) * OVERLAP - self->end_padding) * max_progress;
+
+  return (gint) floor (used_width / n);
+}
+
+static gint
+predict_tab_width (HdyTabBox *self,
+                   TabInfo   *info,
+                   gboolean   assume_placeholder)
+{
+  gint n = self->n_tabs;
+  gint width = self->allocated_width;
+  gint min;
+
+  if (assume_placeholder && !self->reorder_placeholder)
+      n++;
+
+  width += OVERLAP * (n + 1) - self->end_padding;
+
+  min = hdy_tab_get_child_min_width (info->tab);
+
+  return MAX ((gint) floor (width / (gdouble) n), min);
+}
+
+static gint
+calculate_tab_offset (HdyTabBox *self,
+                      TabInfo   *info)
+{
+  gint width;
+
+  if (!self->reordered_tab)
+      return 0;
+
+  width = self->reordered_tab->width - OVERLAP;
+
+  if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+      width = -width;
+
+  return (gint) round (width * info->reorder_offset);
+}
+
+static gboolean
+get_widget_coordinates (HdyTabBox *self,
+                        GdkEvent  *event,
+                        gdouble   *x,
+                        gdouble   *y)
+{
+  GdkWindow *window = gdk_event_get_window (event);
+  gdouble tx, ty, out_x = -1, out_y = -1;
+
+  if (!gdk_event_get_coords (event, &tx, &ty))
+    goto out;
+
+  while (window && window != self->window) {
+    gint window_x, window_y;
+
+    gdk_window_get_position (window, &window_x, &window_y);
+
+    tx += window_x;
+    ty += window_y;
+
+    window = gdk_window_get_parent (window);
+  }
+
+  if (window) {
+    out_x = tx;
+    out_y = ty;
+    goto out;
+  }
+
+out:
+  if (x)
+    *x = out_x;
+
+  if (y)
+    *y = out_y;
+
+  return out_x >= 0 && out_y >= 0;
+}
+
+static void
+get_visible_range (HdyTabBox *self,
+                   gint      *lower,
+                   gint      *upper)
+{
+  gint min, max;
+  GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  GtkStateFlags flags = gtk_widget_get_state_flags (GTK_WIDGET (self));
+  GtkBorder border, padding;
+
+  gtk_style_context_get_border (context, flags, &border);
+  gtk_style_context_get_padding (context, flags, &padding);
+
+  min = border.left + padding.left - OVERLAP;
+  max = border.left + padding.left + self->allocated_width + OVERLAP;
+
+  if (self->adjustment) {
+    GtkBorder margin;
+    gint scroll_min, scroll_max;
+    gdouble value, page_size;
+
+    gtk_style_context_get_margin (context, flags, &margin);
+
+    value = gtk_adjustment_get_value (self->adjustment);
+    page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+    scroll_min = (gint) floor (value);
+    scroll_max = (gint) ceil (value + page_size);
+
+    min = MAX (min, scroll_min - margin.left - OVERLAP);
+    max = MIN (max, scroll_max - margin.left + OVERLAP);
+  }
+
+  if (lower)
+    *lower = min;
+
+  if (upper)
+    *upper = max;
+}
+
+/* Tab resize delay */
+
+static void
+resize_animation_value_cb (gdouble  value,
+                           gpointer user_data)
+{
+  HdyTabBox *self = HDY_TAB_BOX (user_data);
+
+  self->end_padding = (gint) floor (self->initial_end_padding * value);
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+resize_animation_done_cb (gpointer user_data)
+{
+  HdyTabBox *self = HDY_TAB_BOX (user_data);
+
+  g_clear_object (&self->resize_animation);
+}
+
+static void
+set_tab_resize_mode (HdyTabBox     *self,
+                     TabResizeMode  mode)
+{
+  if (self->tab_resize_mode == mode)
+    return;
+
+  if (mode == TAB_RESIZE_FIXED_TAB_WIDTH) {
+    GList *l;
+
+    self->last_width = self->allocated_width;
+
+    for (l = self->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+
+      if (info->appear_animation)
+        info->last_width = hdy_tab_get_display_width (info->tab);
+      else
+        info->last_width = info->width;
+    }
+  } else {
+    self->last_width = 0;
+  }
+
+  if (mode == TAB_RESIZE_NORMAL) {
+    self->initial_end_padding = self->end_padding;
+
+    self->resize_animation =
+      hdy_animation_new (GTK_WIDGET (self), 1, 0,
+                         RESIZE_ANIMATION_DURATION,
+                         resize_animation_value_cb,
+                         resize_animation_done_cb,
+                         self);
+
+    hdy_animation_start (self->resize_animation);
+  }
+
+  self->tab_resize_mode = mode;
+}
+
+/* Hover */
+
+static void
+update_hover (HdyTabBox *self)
+{
+  TabInfo *info;
+
+  if (self->dragging)
+    return;
+
+  if (!self->hovering) {
+    set_tab_resize_mode (self, TAB_RESIZE_NORMAL);
+
+    if (self->hovered_tab) {
+      hdy_tab_set_hovering (self->hovered_tab->tab, FALSE);
+      self->hovered_tab = NULL;
+    }
+
+    return;
+  }
+
+  info = find_tab_info_at (self, self->hover_x);
+
+  if (info != self->hovered_tab) {
+    if (self->hovered_tab)
+      hdy_tab_set_hovering (self->hovered_tab->tab, FALSE);
+
+    self->hovered_tab = info;
+
+    if (self->hovered_tab)
+      hdy_tab_set_hovering (self->hovered_tab->tab, TRUE);
+  }
+}
+
+/* Keybindings */
+
+static void
+add_focus_bindings (GtkBindingSet    *binding_set,
+                    guint             keysym,
+                    GtkDirectionType  direction,
+                    gboolean          last)
+{
+  /* All keypad keysyms are aligned at the same order as non-keypad ones */
+  guint keypad_keysym = keysym - GDK_KEY_Left + GDK_KEY_KP_Left;
+
+  gtk_binding_entry_add_signal (binding_set, keysym, 0,
+                                "focus-tab", 2,
+                                GTK_TYPE_DIRECTION_TYPE, direction,
+                                G_TYPE_BOOLEAN, last);
+  gtk_binding_entry_add_signal (binding_set, keypad_keysym, 0,
+                                "focus-tab", 2,
+                                GTK_TYPE_DIRECTION_TYPE, direction,
+                                G_TYPE_BOOLEAN, last);
+}
+
+static void
+add_reorder_bindings (GtkBindingSet    *binding_set,
+                      guint             keysym,
+                      GtkDirectionType  direction,
+                      gboolean          last)
+{
+  /* All keypad keysyms are aligned at the same order as non-keypad ones */
+  guint keypad_keysym = keysym - GDK_KEY_Left + GDK_KEY_KP_Left;
+
+  gtk_binding_entry_add_signal (binding_set, keysym, GDK_MOD1_MASK,
+                                "reorder-tab", 2,
+                                GTK_TYPE_DIRECTION_TYPE, direction,
+                                G_TYPE_BOOLEAN, last);
+  gtk_binding_entry_add_signal (binding_set, keypad_keysym, GDK_MOD1_MASK,
+                                "reorder-tab", 2,
+                                GTK_TYPE_DIRECTION_TYPE, direction,
+                                G_TYPE_BOOLEAN, last);
+}
+
+static void
+activate_tab (HdyTabBox *self)
+{
+  GtkWidget *content;
+
+  if (!self->selected_tab || !self->selected_tab->page)
+    return;
+
+  content = hdy_tab_page_get_content (self->selected_tab->page);
+
+  // FIXME this does nothing somehow
+  gtk_widget_grab_focus (content);
+}
+
+static void
+focus_tab_cb (HdyTabBox        *self,
+              GtkDirectionType  direction,
+              gboolean          last)
+{
+  gboolean is_rtl, success = last;
+
+  if (!self->view || !self->selected_tab)
+    return;
+
+  is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+  if (direction == GTK_DIR_LEFT)
+    direction = is_rtl ? GTK_DIR_TAB_FORWARD : GTK_DIR_TAB_BACKWARD;
+  else if (direction == GTK_DIR_RIGHT)
+    direction = is_rtl ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD;
+
+  if (direction == GTK_DIR_TAB_BACKWARD) {
+    if (last)
+      success = hdy_tab_view_select_first_page (self->view);
+    else
+      success = hdy_tab_view_select_previous_page (self->view);
+  } else if (direction == GTK_DIR_TAB_FORWARD) {
+    if (last)
+      success = hdy_tab_view_select_last_page (self->view);
+    else
+      success = hdy_tab_view_select_next_page (self->view);
+  }
+
+  if (!success)
+    gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+static void
+reorder_tab_cb (HdyTabBox        *self,
+                GtkDirectionType  direction,
+                gboolean          last)
+{
+  gboolean is_rtl, success = last;
+
+  if (!self->view || !self->selected_tab || !self->selected_tab->page)
+    return;
+
+  is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+  if (direction == GTK_DIR_LEFT)
+    direction = is_rtl ? GTK_DIR_TAB_FORWARD : GTK_DIR_TAB_BACKWARD;
+  else if (direction == GTK_DIR_RIGHT)
+    direction = is_rtl ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD;
+
+  if (direction == GTK_DIR_TAB_BACKWARD) {
+    if (last)
+      success = hdy_tab_view_reorder_first (self->view, self->selected_tab->page);
+    else
+      success = hdy_tab_view_reorder_backward (self->view, self->selected_tab->page);
+  } else if (direction == GTK_DIR_TAB_FORWARD) {
+    if (last)
+      success = hdy_tab_view_reorder_last (self->view, self->selected_tab->page);
+    else
+      success = hdy_tab_view_reorder_forward (self->view, self->selected_tab->page);
+  }
+
+  if (!success)
+    gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+/* Scrolling */
+
+static void
+update_needs_attention (HdyTabBox *self)
+{
+  gboolean left = FALSE, right = FALSE;
+  GList *l;
+  gdouble value, page_size;
+
+  if (!self->adjustment)
+    return;
+
+  value = gtk_adjustment_get_value (self->adjustment);
+  page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+  if (!self->adjustment)
+      return;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+    gint pos;
+
+    if (!info->page)
+      continue;
+
+    if (!hdy_tab_page_get_needs_attention (info->page))
+      continue;
+
+    pos = get_tab_position (self, info);
+
+    if (pos + info->width / 2.0 <= value)
+      left = TRUE;
+
+    if (pos + info->width / 2.0 >= value + page_size)
+      right = TRUE;
+  }
+
+  if (self->needs_attention_left != left) {
+    self->needs_attention_left = left;
+    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NEEDS_ATTENTION_LEFT]);
+  }
+
+  if (self->needs_attention_right != right) {
+    self->needs_attention_right = right;
+    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NEEDS_ATTENTION_RIGHT]);
+  }
+}
+
+static gdouble
+get_scroll_animation_value (HdyTabBox *self)
+{
+  gdouble to, value;
+
+  g_assert (self->scroll_animation);
+
+  to = self->scroll_animation_offset;
+
+  if (self->scroll_animation_tab) {
+    gdouble lower, upper, page_size;
+
+    to += get_tab_position (self, self->scroll_animation_tab);
+
+    g_object_get (self->adjustment,
+                  "lower", &lower,
+                  "upper", &upper,
+                  "page-size", &page_size,
+                  NULL);
+
+    to = CLAMP (to, lower, upper - page_size);
+  }
+
+  value = hdy_animation_get_value (self->scroll_animation);
+
+  return round (hdy_lerp (self->scroll_animation_from, to, value));
+}
+
+static void
+adjustment_value_changed_cb (HdyTabBox *self)
+{
+  gdouble value = gtk_adjustment_get_value (self->adjustment);
+
+  self->hover_x += (value - self->adjustment_prev_value);
+
+  update_hover (self);
+  update_needs_attention (self);
+
+  self->adjustment_prev_value = value;
+
+  if (self->block_scrolling)
+      return;
+
+  if (self->scroll_animation)
+    hdy_animation_stop (self->scroll_animation);
+}
+
+static void
+scroll_animation_value_cb (gdouble  value,
+                           gpointer user_data)
+{
+  gtk_widget_queue_resize (GTK_WIDGET (user_data));
+}
+
+static void
+scroll_animation_done_cb (gpointer user_data)
+{
+  HdyTabBox *self = HDY_TAB_BOX (user_data);
+
+  self->scroll_animation_done = TRUE;
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+animate_scroll (HdyTabBox *self,
+                TabInfo   *info,
+                gdouble    offset,
+                gint64     duration)
+{
+  if (!self->adjustment)
+    return;
+
+  g_signal_emit (self, signals[SIGNAL_STOP_KINETIC_SCROLLING], 0);
+
+  if (self->scroll_animation)
+    hdy_animation_stop (self->scroll_animation);
+
+  g_clear_object (&self->scroll_animation);
+  self->scroll_animation_done = FALSE;
+  self->scroll_animation_from = gtk_adjustment_get_value (self->adjustment);
+  self->scroll_animation_tab = info;
+  self->scroll_animation_offset = offset;
+
+  /* The actual update will be done in size_allocate (). After the animation
+   * finishes, don't remove it right away, it will be done in size-allocate as
+   * well after one last update, so that we don't miss the last frame.
+   */
+
+  self->scroll_animation =
+    hdy_animation_new (GTK_WIDGET (self), 0, 1, duration,
+                       scroll_animation_value_cb,
+                       scroll_animation_done_cb,
+                       self);
+
+  hdy_animation_start (self->scroll_animation);
+}
+
+static void
+animate_scroll_relative (HdyTabBox *self,
+                         gdouble    delta,
+                         gint64     duration)
+{
+  gdouble current_value = gtk_adjustment_get_value (self->adjustment);
+
+  if (self->scroll_animation) {
+    current_value = self->scroll_animation_offset;
+
+    if (self->scroll_animation_tab)
+      current_value += get_tab_position (self, self->scroll_animation_tab);
+  }
+
+  animate_scroll (self, NULL, current_value + delta, duration);
+}
+
+static void
+scroll_to_tab_with_pos (HdyTabBox *self,
+                        TabInfo   *info,
+                        gint       pos,
+                        gint64     duration)
+{
+  gint tab_width;
+  gdouble padding, value, page_size;
+
+  if (!self->adjustment)
+    return;
+
+  tab_width = info->width;
+
+  if (info->appear_animation)
+    tab_width = hdy_tab_get_display_width (info->tab);
+
+  value = gtk_adjustment_get_value (self->adjustment);
+  page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+  padding = MIN (info->width, page_size - tab_width) / 2.0;
+
+  if (pos + OVERLAP < value)
+    animate_scroll (self, info, -padding, duration);
+  else if (pos + tab_width - OVERLAP > value + page_size)
+    animate_scroll (self, info, tab_width + padding - page_size, duration);
+}
+
+static void
+scroll_to_tab (HdyTabBox *self,
+               TabInfo   *info,
+               gint64     duration)
+{
+  scroll_to_tab_with_pos (self, info, get_tab_position (self, info), duration);
+}
+
+/* Reordering */
+
+static void
+force_end_reordering (HdyTabBox *self)
+{
+  GList *l;
+
+  if (self->dragging || !self->reordered_tab)
+    return;
+
+  if (self->reorder_animation)
+    hdy_animation_stop (self->reorder_animation);
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (info->reorder_animation)
+      hdy_animation_stop (info->reorder_animation);
+  }
+}
+
+static void
+check_end_reordering (HdyTabBox *self)
+{
+  gboolean should_focus;
+  GtkWidget *tab_widget;
+  GList *l;
+
+  if (self->dragging || !self->reordered_tab || self->continue_reorder)
+    return;
+
+  if (self->reorder_animation)
+    return;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (info->reorder_animation)
+      return;
+  }
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    info->end_reorder_offset = 0;
+    info->reorder_offset = 0;
+  }
+
+  tab_widget = GTK_WIDGET (self->reordered_tab->tab);
+
+  should_focus = gtk_widget_has_visible_focus (tab_widget);
+
+  gtk_widget_set_child_visible (tab_widget, FALSE);
+  gtk_widget_unrealize (tab_widget);
+  gtk_widget_set_parent_window (tab_widget, self->window);
+  gtk_widget_set_child_visible (tab_widget, TRUE);
+  gtk_widget_set_has_tooltip (tab_widget, TRUE);
+
+  self->reordered_tab->reorder_ignore_bounds = FALSE;
+
+  if (should_focus)
+    gtk_widget_grab_focus (tab_widget);
+
+  gdk_window_hide (self->reorder_window);
+
+  self->tabs = g_list_remove (self->tabs, self->reordered_tab);
+  self->tabs = g_list_insert (self->tabs, self->reordered_tab, self->reorder_index);
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+  self->reordered_tab = NULL;
+}
+
+static void
+start_reordering (HdyTabBox *self,
+                  TabInfo   *info)
+{
+  gboolean should_focus;
+  GtkWidget *tab_widget;
+
+  self->reordered_tab = info;
+
+  tab_widget = GTK_WIDGET (self->reordered_tab->tab);
+
+  should_focus = gtk_widget_has_visible_focus (tab_widget);
+
+  gtk_widget_set_has_tooltip (tab_widget, FALSE);
+  gtk_widget_set_child_visible (tab_widget, FALSE);
+  gtk_widget_unrealize (tab_widget);
+  gtk_widget_set_parent_window (tab_widget, self->reorder_window);
+  gtk_widget_set_child_visible (tab_widget, TRUE);
+
+  if (should_focus)
+    gtk_widget_grab_focus (tab_widget);
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static gint
+get_reorder_position (HdyTabBox *self)
+{
+  gint lower, upper;
+
+  if (self->reordered_tab->reorder_ignore_bounds)
+    return self->reorder_x;
+
+  get_visible_range (self, &lower, &upper);
+
+  return CLAMP (self->reorder_x, lower, upper - self->reordered_tab->width);
+}
+
+static void
+reorder_animation_value_cb (gdouble  value,
+                            gpointer user_data)
+{
+  TabInfo *dest_tab = user_data;
+  GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (dest_tab->tab));
+  HdyTabBox *self = HDY_TAB_BOX (parent);
+  gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+  gdouble x1, x2;
+
+  x1 = get_reorder_position (self);
+  x2 = dest_tab->pos - calculate_tab_offset (self, dest_tab);
+
+  if (dest_tab->end_reorder_offset * (is_rtl ? 1 : -1) > 0)
+    x2 += dest_tab->width - self->reordered_tab->width;
+
+  self->reorder_window_x = (gint) round (hdy_lerp (x1, x2, value));
+
+  gdk_window_move_resize (self->reorder_window,
+                          self->reorder_window_x,
+                          0,
+                          self->reordered_tab->width,
+                          gtk_widget_get_allocated_height (GTK_WIDGET (self)));
+
+  update_hover (self);
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+reorder_animation_done_cb (gpointer user_data)
+{
+  TabInfo *dest_tab = user_data;
+  GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (dest_tab->tab));
+  HdyTabBox *self = HDY_TAB_BOX (parent);
+
+  g_clear_object (&self->reorder_animation);
+  check_end_reordering (self);
+}
+
+static void
+animate_reordering (HdyTabBox *self,
+                    TabInfo   *dest_tab)
+{
+  if (self->reorder_animation)
+    hdy_animation_stop (self->reorder_animation);
+
+  self->reorder_animation =
+    hdy_animation_new (GTK_WIDGET (self), 0, 1,
+                       REORDER_ANIMATION_DURATION,
+                       reorder_animation_value_cb,
+                       reorder_animation_done_cb,
+                       dest_tab);
+
+  hdy_animation_start (self->reorder_animation);
+
+  check_end_reordering (self);
+}
+
+static void
+reorder_offset_animation_value_cb (gdouble  value,
+                                   gpointer user_data)
+{
+  TabInfo *info = user_data;
+  GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (info->tab));
+
+  info->reorder_offset = value;
+  gtk_widget_queue_allocate (parent);
+}
+
+static void
+reorder_offset_animation_done_cb (gpointer user_data)
+{
+  TabInfo *info = user_data;
+  GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (info->tab));
+  HdyTabBox *self = HDY_TAB_BOX (parent);
+
+  g_clear_object (&info->reorder_animation);
+  check_end_reordering (self);
+}
+
+static void
+animate_reorder_offset (HdyTabBox *self,
+                        TabInfo   *info,
+                        gdouble    offset)
+{
+  gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+  offset *= (is_rtl ? -1 : 1);
+
+  if (info->end_reorder_offset == offset)
+    return;
+
+  info->end_reorder_offset = offset;
+
+  if (info->reorder_animation)
+    hdy_animation_stop (info->reorder_animation);
+
+  info->reorder_animation =
+    hdy_animation_new (GTK_WIDGET (self), info->reorder_offset, offset,
+                       REORDER_ANIMATION_DURATION,
+                       reorder_offset_animation_value_cb,
+                       reorder_offset_animation_done_cb,
+                       info);
+
+  hdy_animation_start (info->reorder_animation);
+}
+
+static void
+reset_reorder_animations (HdyTabBox *self)
+{
+  guint i, original_index;
+  GList *l;
+
+  if (!hdy_get_enable_animations (GTK_WIDGET (self)))
+      return;
+
+  l = find_link_for_page (self, self->reordered_tab->page);
+  original_index = g_list_position (self->tabs, l);
+
+  if (self->reorder_index > original_index)
+    for (i = 0; i < self->reorder_index - original_index; i++) {
+      l = l->next;
+      animate_reorder_offset (self, l->data, 0);
+    }
+
+  if (self->reorder_index < original_index)
+    for (i = 0; i < original_index - self->reorder_index; i++) {
+      l = l->prev;
+      animate_reorder_offset (self, l->data, 0);
+    }
+}
+
+static void
+reorder_page (HdyTabBox  *self,
+              HdyTabPage *page,
+              guint       index)
+{
+  GList *link;
+  guint original_index;
+  TabInfo *info, *dest_tab;
+  gboolean is_rtl;
+
+  if (hdy_tab_page_get_pinned (page) != self->pinned)
+    return;
+
+  self->continue_reorder = self->reordered_tab && page == self->reordered_tab->page;
+
+  if (self->continue_reorder)
+    reset_reorder_animations (self);
+  else
+    force_end_reordering (self);
+
+  link = find_link_for_page (self, page);
+  info = link->data;
+  original_index = g_list_position (self->tabs, link);
+
+  if (!self->continue_reorder)
+    start_reordering (self, info);
+
+  gdk_window_show (self->reorder_window);
+
+  if (self->continue_reorder)
+    self->reorder_x = self->reorder_window_x;
+  else
+    self->reorder_x = info->pos;
+
+  self->reorder_index = index;
+
+  if (!self->pinned)
+    self->reorder_index -= hdy_tab_view_get_n_pinned_pages (self->view);
+
+  dest_tab = g_list_nth_data (self->tabs, self->reorder_index);
+
+  if (info == self->selected_tab)
+    scroll_to_tab_with_pos (self, self->selected_tab, dest_tab->pos, REORDER_ANIMATION_DURATION);
+
+  animate_reordering (self, dest_tab);
+
+  is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+  /* If animations are disabled, animate_reordering() animation will have
+   * already finished and called check_end_reordering () by this point, so
+   * it's too late to animate these, so we get a crash.
+   */
+
+  if (hdy_get_enable_animations (GTK_WIDGET (self)) &&
+      gtk_widget_get_mapped (GTK_WIDGET (self))) {
+    guint i;
+
+    if (self->reorder_index > original_index)
+      for (i = 0; i < self->reorder_index - original_index; i++) {
+        link = link->next;
+        animate_reorder_offset (self, link->data, is_rtl ? 1 : -1);
+      }
+
+    if (self->reorder_index < original_index)
+      for (i = 0; i < original_index - self->reorder_index; i++) {
+        link = link->prev;
+        animate_reorder_offset (self, link->data, is_rtl ? -1 : 1);
+      }
+  }
+
+  self->continue_reorder = FALSE;
+}
+
+static void
+prepare_drag_window (GdkSeat   *seat,
+                     GdkWindow *window,
+                     gpointer   user_data)
+{
+  gdk_window_show (window);
+}
+
+static void
+update_dragging (HdyTabBox *self)
+{
+  gboolean is_rtl, after_selected, found_index;
+  gint x;
+  guint i = 0;
+  GList *l;
+
+  if (!self->dragging)
+    return;
+
+  x = get_reorder_position (self);
+
+  gdk_window_move_resize (self->reorder_window,
+                          x, 0,
+                          self->reordered_tab->width,
+                          gtk_widget_get_allocated_height (GTK_WIDGET (self)));
+
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+
+  is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+  after_selected = FALSE;
+  found_index = FALSE;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+    gint center = info->pos - calculate_tab_offset (self, info) + info->width / 2;
+    gdouble offset = 0;
+
+    if (x + self->reordered_tab->width > center &&
+        x < center &&
+        (!found_index || after_selected)) {
+      self->reorder_index = i;
+      found_index = TRUE;
+    }
+
+    i++;
+
+    if (info == self->reordered_tab) {
+        after_selected = TRUE;
+        continue;
+    }
+
+    if (after_selected != is_rtl && x + self->reordered_tab->width > center)
+        offset = -1;
+    else if (after_selected == is_rtl && x < center)
+        offset = 1;
+
+    animate_reorder_offset (self, info, offset);
+  }
+}
+
+static gboolean
+drag_autoscroll_cb (GtkWidget     *widget,
+                    GdkFrameClock *frame_clock,
+                    HdyTabBox     *self)
+{
+  gdouble value, lower, upper, page_size;
+  gdouble x, delta_ms, start_threshold, end_threshold, autoscroll_factor;
+  gint64 time;
+  gint offset = 0;
+
+  g_object_get (self->adjustment,
+                "value", &value,
+                "lower", &lower,
+                "upper", &upper,
+                "page-size", &page_size,
+                NULL);
+
+  x = CLAMP ((gdouble) self->reorder_x,
+             lower + AUTOSCROLL_AREA_WIDTH,
+             upper - self->reordered_tab->width - AUTOSCROLL_AREA_WIDTH);
+
+  time = gdk_frame_clock_get_frame_time (frame_clock);
+  delta_ms = (time - self->drag_autoscroll_prev_time) / 1000.0;
+
+  start_threshold = value + AUTOSCROLL_AREA_WIDTH;
+  end_threshold = value + page_size - self->reordered_tab->width - AUTOSCROLL_AREA_WIDTH;
+  autoscroll_factor = 0;
+
+  if (x < start_threshold)
+    autoscroll_factor = -(start_threshold - x) / AUTOSCROLL_AREA_WIDTH;
+  else if (x > end_threshold)
+    autoscroll_factor = (x - end_threshold) / AUTOSCROLL_AREA_WIDTH;
+
+  autoscroll_factor = CLAMP (autoscroll_factor, -1, 1);
+  autoscroll_factor = hdy_ease_in_cubic (autoscroll_factor);
+  self->drag_autoscroll_prev_time = time;
+
+  if (autoscroll_factor == 0)
+    return G_SOURCE_CONTINUE;
+
+  if (autoscroll_factor > 0)
+    offset = (gint) ceil (autoscroll_factor * delta_ms * AUTOSCROLL_SPEED);
+  else
+    offset = (gint) floor (autoscroll_factor * delta_ms * AUTOSCROLL_SPEED);
+
+  self->reorder_x += offset;
+  gtk_adjustment_set_value (self->adjustment, value + offset);
+  update_dragging (self);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+start_dragging (HdyTabBox *self,
+                GdkEvent  *event,
+                TabInfo   *info)
+{
+  if (self->dragging)
+    return;
+
+  if (!info)
+    return;
+
+  self->continue_reorder = info == self->reordered_tab;
+
+  if (self->continue_reorder) {
+      if (self->reorder_animation)
+        hdy_animation_stop (self->reorder_animation);
+
+    reset_reorder_animations (self);
+
+    self->reorder_x = (gint) round (self->hover_x - self->drag_offset_x);
+    self->reorder_y = (gint) round (self->hover_y - self->drag_offset_y);
+  } else
+    force_end_reordering (self);
+
+  if (self->adjustment) {
+    GdkFrameClock *frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
+
+    self->drag_autoscroll_prev_time = gdk_frame_clock_get_frame_time (frame_clock);
+    self->drag_autoscroll_cb_id =
+      gtk_widget_add_tick_callback (GTK_WIDGET (self),
+                                    (GtkTickCallback) drag_autoscroll_cb,
+                                    self, NULL);
+  }
+
+  self->dragging = TRUE;
+
+  if (!self->continue_reorder)
+    start_reordering (self, info);
+
+  if (!self->indirect_reordering) {
+    GdkDevice *device = gdk_event_get_device (event);
+
+    self->drag_seat = gdk_device_get_seat (device);
+    gdk_seat_grab (self->drag_seat,
+                   self->reorder_window,
+                   GDK_SEAT_CAPABILITY_ALL,
+                   FALSE,
+                   NULL, // FIXME maybe use an actual cursor
+                   event,
+                   prepare_drag_window,
+                   self);
+  }
+}
+
+static void
+end_dragging (HdyTabBox *self)
+{
+  TabInfo *dest_tab;
+
+  if (!self->dragging)
+    return;
+
+  self->dragging = FALSE;
+
+  if (self->drag_autoscroll_cb_id) {
+    gtk_widget_remove_tick_callback (GTK_WIDGET (self),
+                                     self->drag_autoscroll_cb_id);
+    self->drag_autoscroll_cb_id = 0;
+  }
+
+  dest_tab = g_list_nth_data (self->tabs, self->reorder_index);
+
+  if (!self->indirect_reordering) {
+    guint index;
+
+    gdk_seat_ungrab (self->drag_seat);
+    self->drag_seat = NULL;
+
+    index = self->reorder_index;
+
+    if (!self->pinned)
+      index += hdy_tab_view_get_n_pinned_pages (self->view);
+
+    /* We've already reordered the tab here, no need to do it again */
+    g_signal_handlers_block_by_func (self->view, reorder_page, self);
+
+    hdy_tab_view_reorder_page (self->view, self->reordered_tab->page, index);
+
+    g_signal_handlers_unblock_by_func (self->view, reorder_page, self);
+  }
+
+  animate_reordering (self, dest_tab);
+}
+
+/* Selection */
+
+static void
+select_page (HdyTabBox  *self,
+             HdyTabPage *page)
+{
+  if (!page) {
+    self->selected_tab = NULL;
+
+    gtk_container_set_focus_child (GTK_CONTAINER (self), NULL);
+
+    return;
+  }
+
+  self->selected_tab = find_info_for_page (self, page);
+
+  if (!self->selected_tab)
+    return;
+
+  if (hdy_tab_bar_tabs_have_visible_focus (self->tab_bar))
+    gtk_widget_grab_focus (GTK_WIDGET (self->selected_tab->tab));
+
+  gtk_container_set_focus_child (GTK_CONTAINER (self),
+                                 GTK_WIDGET (self->selected_tab->tab));
+
+  if (self->selected_tab->width >= 0)
+    scroll_to_tab (self, self->selected_tab, FOCUS_ANIMATION_DURATION);
+}
+
+/* Opening */
+
+static void
+appear_animation_value_cb (gdouble  value,
+                           gpointer user_data)
+{
+  TabInfo *info = user_data;
+  GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (info->tab));
+
+  info->appear_progress = value;
+  gtk_widget_queue_resize (parent);
+}
+
+static void
+open_animation_done_cb (gpointer user_data)
+{
+  TabInfo *info = user_data;
+
+  g_clear_object (&info->appear_animation);
+}
+
+static TabInfo *
+create_tab_info (HdyTabBox  *self,
+                 HdyTabPage *page)
+{
+  TabInfo *info;
+
+  info = g_new0 (TabInfo, 1);
+  info->page = page;
+  info->pos = -1;
+  info->width = -1;
+  info->tab = hdy_tab_new (self->view, self->pinned, FALSE);
+
+  hdy_tab_set_page (info->tab, page);
+
+  gtk_widget_set_parent (GTK_WIDGET (info->tab), GTK_WIDGET (self));
+
+  if (self->window)
+    gtk_widget_set_parent_window (GTK_WIDGET (info->tab), self->window);
+
+  return info;
+}
+
+static void
+add_page (HdyTabBox  *self,
+          HdyTabPage *page,
+          guint       position)
+{
+  TabInfo *info;
+  GList *l;
+
+  if (hdy_tab_page_get_pinned (page) != self->pinned)
+    return;
+
+  if (!self->pinned)
+    position -= hdy_tab_view_get_n_pinned_pages (self->view);
+
+  force_end_reordering (self);
+
+  info = create_tab_info (self, page);
+
+  gtk_widget_show (GTK_WIDGET (info->tab));
+
+  if (!self->pinned)
+    info->notify_needs_attention_id =
+      g_signal_connect_object (page,
+                               "notify::needs-attention",
+                               G_CALLBACK (update_needs_attention),
+                               self,
+                               G_CONNECT_SWAPPED);
+
+  info->appear_animation =
+    hdy_animation_new (GTK_WIDGET (self), 0, 1,
+                       OPEN_ANIMATION_DURATION,
+                       appear_animation_value_cb,
+                       open_animation_done_cb,
+                       info);
+
+  l = find_nth_alive_tab (self, position);
+  self->tabs = g_list_insert_before (self->tabs, l, info);
+
+  self->n_tabs++;
+
+  hdy_animation_start (info->appear_animation);
+
+  if (page == hdy_tab_view_get_selected_page (self->view))
+    hdy_tab_box_select_page (self, page);
+}
+
+/* Closing */
+
+static void
+close_animation_done_cb (gpointer user_data)
+{
+  TabInfo *info = user_data;
+  GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (info->tab));
+  HdyTabBox *self = HDY_TAB_BOX (parent);
+
+  g_clear_object (&info->appear_animation);
+
+  self->tabs = g_list_remove (self->tabs, info);
+
+  if (info == self->hovered_tab)
+    self->hovered_tab = NULL;
+
+  remove_and_free_tab_info (info);
+
+  self->n_tabs--;
+}
+
+static void
+remove_page (HdyTabBox  *self,
+             HdyTabPage *page)
+{
+  TabInfo *info;
+  GList *page_link;
+
+  page_link = find_link_for_page (self, page);
+
+  if (!page_link)
+    return;
+
+  info = page_link->data;
+
+  force_end_reordering (self);
+
+  if (self->hovering) {
+    gboolean is_last = TRUE;
+    GList *l;
+
+    for (l = page_link; l; l = l->next) {
+      TabInfo *i = l->data;
+
+      if (l != page_link && i->page)
+        is_last = FALSE;
+    }
+
+    if (is_last && self->tab_resize_mode != TAB_RESIZE_NORMAL)
+      set_tab_resize_mode (self, TAB_RESIZE_FIXED_END_PADDING);
+    else if (!is_last)
+      set_tab_resize_mode (self, TAB_RESIZE_FIXED_TAB_WIDTH);
+  }
+
+  g_assert (info->page);
+
+  if (gtk_widget_is_focus (GTK_WIDGET (info->tab)))
+    hdy_tab_box_try_focus_selected_tab (self);
+
+  if (info == self->selected_tab)
+    hdy_tab_box_select_page (self, NULL);
+
+  hdy_tab_set_page (info->tab, NULL);
+
+  if (info->notify_needs_attention_id > 0) {
+    g_signal_handler_disconnect (info->page, info->notify_needs_attention_id);
+    info->notify_needs_attention_id = 0;
+  }
+
+  info->page = NULL;
+
+  info->appear_animation =
+    hdy_animation_new (GTK_WIDGET (self), info->appear_progress, 0,
+                       CLOSE_ANIMATION_DURATION,
+                       appear_animation_value_cb,
+                       close_animation_done_cb,
+                       info);
+
+  hdy_animation_start (info->appear_animation);
+}
+
+/* DND */
+
+static gboolean
+check_dnd_threshold (HdyTabBox *self)
+{
+  gint threshold;
+  GtkAllocation alloc;
+
+  g_object_get (gtk_settings_get_default (),
+                "gtk-dnd-drag-threshold", &threshold,
+                NULL);
+
+  threshold *= DND_THRESHOLD_MULTIPLIER;
+
+  gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+  alloc.x -= threshold;
+  alloc.y -= threshold;
+  alloc.width += 2 * threshold;
+  alloc.height += 2 * threshold;
+
+  return self->hover_x < alloc.x - threshold ||
+         self->hover_y < alloc.y - threshold ||
+         self->hover_x > alloc.x + alloc.width + threshold ||
+         self->hover_y > alloc.x + alloc.height + threshold;
+}
+
+static gint
+calculate_placeholder_index (HdyTabBox *self,
+                             gint       x)
+{
+  gint lower, upper, pos, i;
+  gboolean is_rtl;
+  GList *l;
+
+  get_visible_range (self, &lower, &upper);
+
+  x = CLAMP (x, lower, upper);
+
+  is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+  pos = (is_rtl ? self->allocated_width + OVERLAP : -OVERLAP);
+  i = 0;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+    int tab_width = predict_tab_width (self, info, TRUE) * (is_rtl ? -1 : 1);
+
+    int end = pos + tab_width + calculate_tab_offset (self, info);
+
+    if ((x <= end && !is_rtl) || (x >= end && is_rtl))
+      break;
+
+    pos += tab_width + (is_rtl ? OVERLAP : -OVERLAP);
+    i++;
+  }
+
+  return i;
+}
+
+static void
+insert_placeholder (HdyTabBox  *self,
+                    HdyTabPage *page,
+                    gint        pos)
+{
+  gdouble initial_progress = 0;
+  TabInfo *info = self->reorder_placeholder;
+
+  if (info) {
+    initial_progress = info->appear_progress;
+
+    if (info->appear_animation)
+      hdy_animation_stop (info->appear_animation);
+  } else {
+    gint index;
+
+    self->placeholder_page = page;
+
+    info = create_tab_info (self, page);
+
+    info->reorder_ignore_bounds = TRUE;
+
+    index = calculate_placeholder_index (self, pos);
+
+    self->tabs = g_list_insert (self->tabs, info, index);
+    self->n_tabs++;
+
+    self->reorder_placeholder = info;
+    self->reorder_index = g_list_index (self->tabs, info);
+  }
+
+  info->appear_animation =
+    hdy_animation_new (GTK_WIDGET (self), initial_progress, 1,
+                       OPEN_ANIMATION_DURATION,
+                       appear_animation_value_cb,
+                       open_animation_done_cb,
+                       info);
+
+  hdy_animation_start (info->appear_animation);
+}
+
+static void
+replace_animation_done_cb (gpointer user_data)
+{
+  TabInfo *info = user_data;
+  GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (info->tab));
+  HdyTabBox *self = HDY_TAB_BOX (parent);
+
+  g_clear_object (&info->appear_animation);
+  self->reorder_placeholder = NULL;
+  self->can_remove_placeholder = TRUE;
+}
+
+static void
+replace_placeholder (HdyTabBox  *self,
+                     HdyTabPage *page)
+{
+  TabInfo *info = self->reorder_placeholder;
+  gdouble initial_progress;
+
+  gtk_widget_show (GTK_WIDGET (self->reorder_placeholder->tab));
+
+  if (!info->appear_animation) {
+    self->reorder_placeholder = NULL;
+
+    return;
+  }
+
+  initial_progress = info->appear_progress;
+
+  self->can_remove_placeholder = FALSE;
+
+  hdy_tab_set_page (info->tab, page);
+  info->page = page;
+
+  hdy_animation_stop (info->appear_animation);
+
+  info->appear_animation =
+    hdy_animation_new (GTK_WIDGET (self), initial_progress, 1,
+                       OPEN_ANIMATION_DURATION,
+                       appear_animation_value_cb,
+                       replace_animation_done_cb,
+                       info);
+
+  hdy_animation_start (info->appear_animation);
+}
+
+static void
+remove_animation_done_cb (gpointer user_data)
+{
+  TabInfo *info = user_data;
+  GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (info->tab));
+  HdyTabBox *self = HDY_TAB_BOX (parent);
+
+  if (!self->can_remove_placeholder) {
+    hdy_tab_set_page (info->tab, self->placeholder_page);
+    info->page = self->placeholder_page;
+
+    return;
+  }
+
+  if (self->reordered_tab == info) {
+    force_end_reordering (self);
+
+    if (self->reorder_animation)
+      hdy_animation_stop (info->reorder_animation);
+
+    self->reordered_tab = NULL;
+  }
+
+  self->tabs = g_list_remove (self->tabs, info);
+
+  remove_and_free_tab_info (info);
+
+  self->n_tabs--;
+
+  self->reorder_placeholder = NULL;
+}
+
+static void
+remove_placeholder (HdyTabBox *self)
+{
+  TabInfo *info = self->reorder_placeholder;
+
+  if (!info || !info->page)
+    return;
+
+  hdy_tab_set_page (info->tab, NULL);
+  info->page = NULL;
+
+  if (info->appear_animation)
+    hdy_animation_stop (info->appear_animation);
+
+  info->appear_animation =
+    hdy_animation_new (GTK_WIDGET (self), info->appear_progress, 0,
+                       CLOSE_ANIMATION_DURATION,
+                       appear_animation_value_cb,
+                       remove_animation_done_cb,
+                       info);
+
+  hdy_animation_start (info->appear_animation);
+}
+
+static HdyTabBox *
+get_source_tab_box (GdkDragContext *context)
+{
+  GtkWidget *source = gtk_drag_get_source_widget (context);
+
+  if (!source || !HDY_IS_TAB_BOX (source))
+    return NULL;
+
+  return HDY_TAB_BOX (source);
+}
+
+static gboolean
+do_drag_drop (HdyTabBox      *self,
+              GdkDragContext *context,
+              guint           time)
+{
+  GdkAtom target, tab_target;
+  HdyTabBox *source_tab_box;
+  HdyTabPage *page;
+
+  target = gtk_drag_dest_find_target (GTK_WIDGET (self), context, NULL);
+  tab_target = gdk_atom_intern_static_string ("HDY_TAB");
+
+  if (target != tab_target)
+    return GDK_EVENT_PROPAGATE;
+
+  source_tab_box = get_source_tab_box (context);
+
+  if (!source_tab_box)
+    return GDK_EVENT_PROPAGATE;
+
+  page = source_tab_box->detached_page;
+
+  if (self->reorder_placeholder) {
+    replace_placeholder (self, page);
+    end_dragging (self);
+
+    g_signal_handlers_block_by_func (self->view, add_page, self);
+
+    hdy_tab_view_attach_page (self->view, page, self->reorder_index);
+
+    g_signal_handlers_unblock_by_func (self->view, add_page, self);
+  } else {
+    hdy_tab_view_attach_page (self->view, page, self->reorder_index);
+  }
+
+  source_tab_box->detached_page = NULL;
+
+  self->indirect_reordering = FALSE;
+  gtk_drag_finish (context, TRUE, FALSE, time);
+
+  return GDK_EVENT_STOP;
+}
+
+static void
+detach_into_new_window (HdyTabBox      *self,
+                        GdkDragContext *context)
+{
+  HdyTabBox *source_tab_box;
+  HdyTabPage *page;
+  HdyTabView *new_view;
+
+  source_tab_box = get_source_tab_box (context);
+
+  if (!source_tab_box)
+    return;
+
+  page = source_tab_box->detached_page;
+
+  // FIXME
+  g_signal_emit_by_name (source_tab_box->view, "create-window", &new_view);
+
+  if (!HDY_IS_TAB_VIEW (new_view))
+    g_error ("Cannot detach into new window");
+
+  hdy_tab_view_attach_page (new_view, page, 0);
+
+  self->should_detach_into_new_window = FALSE;
+}
+
+static gboolean
+is_view_in_the_same_group (HdyTabBox  *self,
+                           HdyTabView *other_view)
+{
+  GSList *group = hdy_tab_view_get_group (self->view);
+
+  return g_slist_index (group, other_view) >= 0;
+}
+
+static gboolean
+view_drag_drop_cb (HdyTabBox      *self,
+                   GdkDragContext *context,
+                   gint            x,
+                   gint            y,
+                   guint           time)
+{
+  HdyTabBox *source_tab_box;
+
+  if (self->pinned)
+    return GDK_EVENT_PROPAGATE;
+
+  source_tab_box = get_source_tab_box (context);
+
+  if (!source_tab_box)
+    return GDK_EVENT_PROPAGATE;
+
+  if (!self->view || !is_view_in_the_same_group (self, source_tab_box->view))
+    return GDK_EVENT_PROPAGATE;
+
+  self->reorder_index = hdy_tab_view_get_n_pages (self->view) -
+                        hdy_tab_view_get_n_pinned_pages (self->view);
+
+  return do_drag_drop (self, context, time);
+}
+
+static void
+create_drag_icon (HdyTabBox      *self,
+                  GdkDragContext *context)
+{
+  DragIcon *icon;
+
+  icon = g_new0 (DragIcon, 1);
+
+  icon->window = gtk_window_new (GTK_WINDOW_POPUP);
+  icon->context = context;
+
+  icon->width = predict_tab_width (self, self->reordered_tab, FALSE);
+  icon->target_width = icon->width;
+
+  gtk_widget_set_app_paintable (icon->window, TRUE);
+  gtk_window_set_resizable (GTK_WINDOW (icon->window), FALSE);
+  gtk_window_set_decorated (GTK_WINDOW (icon->window), FALSE);
+
+  gtk_style_context_add_class (gtk_widget_get_style_context (icon->window),
+                               "tab-drag-icon");
+
+  icon->tab = hdy_tab_new (self->view, FALSE, TRUE);
+  hdy_tab_set_page (icon->tab, self->reordered_tab->page);
+  gtk_widget_show (GTK_WIDGET (icon->tab));
+  gtk_widget_set_halign (GTK_WIDGET (icon->tab), GTK_ALIGN_START);
+
+  gtk_container_add (GTK_CONTAINER (icon->window), GTK_WIDGET (icon->tab));
+
+  gtk_style_context_get_margin (gtk_widget_get_style_context (GTK_WIDGET (icon->tab)),
+                                gtk_widget_get_state_flags (GTK_WIDGET (icon->tab)),
+                                &icon->tab_margin);
+
+  hdy_tab_set_display_width (icon->tab, icon->width);
+  gtk_widget_set_size_request (GTK_WIDGET (icon->tab),
+                               icon->width + icon->tab_margin.left + icon->tab_margin.right,
+                               -1);
+
+  gtk_window_set_screen (GTK_WINDOW (icon->window),
+                         gtk_widget_get_screen (GTK_WIDGET (self)));
+
+  icon->hotspot_x = (gint) self->drag_offset_x;
+  icon->hotspot_y = (gint) self->drag_offset_y;
+
+  gtk_drag_set_icon_widget (context, icon->window,
+                            icon->hotspot_x + icon->tab_margin.left,
+                            icon->hotspot_y + icon->tab_margin.top);
+
+  self->drag_icon = icon;
+}
+
+static void
+icon_resize_animation_value_cb (gdouble  value,
+                                gpointer user_data)
+{
+  DragIcon *icon = user_data;
+  gdouble relative_pos;
+
+  relative_pos = (gdouble) icon->hotspot_x / icon->width;
+
+  icon->width = (gint) round (value);
+
+  hdy_tab_set_display_width (icon->tab, icon->width);
+  gtk_widget_set_size_request (GTK_WIDGET (icon->tab),
+                               icon->width + icon->tab_margin.left + icon->tab_margin.right,
+                               -1);
+
+  icon->hotspot_x = (gint) round (icon->width * relative_pos);
+
+  gdk_drag_context_set_hotspot (icon->context,
+                                icon->hotspot_x + icon->tab_margin.left,
+                                icon->hotspot_y + icon->tab_margin.top);
+
+  gtk_widget_queue_resize (GTK_WIDGET (icon->window));
+}
+
+static void
+icon_resize_animation_done_cb (gpointer user_data)
+{
+  DragIcon *icon = user_data;
+
+  g_clear_object (&icon->resize_animation);
+}
+
+static void
+resize_drag_icon (HdyTabBox *self,
+                  gint       width)
+{
+  DragIcon *icon = self->drag_icon;
+
+  if (width == icon->target_width)
+    return;
+
+  if (icon->resize_animation)
+    hdy_animation_stop (icon->resize_animation);
+
+  icon->target_width = width;
+
+  icon->resize_animation =
+    hdy_animation_new (icon->window, icon->width, width,
+                       ICON_RESIZE_ANIMATION_DURATION,
+                       icon_resize_animation_value_cb,
+                       icon_resize_animation_done_cb,
+                       icon);
+
+  hdy_animation_start (icon->resize_animation);
+}
+
+/* Context menu */
+
+static void
+do_touch_popup (HdyTabBox *self,
+                TabInfo   *info)
+{
+  GMenuModel *model = hdy_tab_view_get_menu_model (self->view);
+
+  if (!G_IS_MENU_MODEL (model))
+    return;
+
+  // FIXME
+  g_signal_emit_by_name (self->view, "setup-menu", info->page, NULL);
+
+  if (!self->touch_menu)
+    self->touch_menu = GTK_POPOVER (gtk_popover_new_from_model (GTK_WIDGET (info->tab), model));
+  else
+    gtk_popover_set_relative_to (self->touch_menu, GTK_WIDGET (info->tab));
+
+  gtk_popover_popup (self->touch_menu);
+}
+
+static void
+touch_menu_gesture_pressed (HdyTabBox *self)
+{
+  self->pressed = FALSE;
+  end_dragging (self);
+
+  do_touch_popup (self, self->hovered_tab);
+  gtk_gesture_set_state (self->touch_menu_gesture,
+                         GTK_EVENT_SEQUENCE_CLAIMED);
+}
+
+static void
+popup_menu_detach (HdyTabBox *self,
+                   GtkMenu   *menu)
+{
+  self->context_menu = NULL;
+}
+
+static void
+popup_menu_deactivate_cb (HdyTabBox *self)
+{
+  self->hovering = FALSE;
+  update_hover (self);
+}
+
+static void
+do_popup (HdyTabBox *self,
+          TabInfo   *info,
+          GdkEvent  *event)
+{
+  GMenuModel *model = hdy_tab_view_get_menu_model (self->view);
+
+  if (!G_IS_MENU_MODEL (model))
+    return;
+
+  // FIXME
+  g_signal_emit_by_name (self->view, "setup-menu", info->page, NULL);
+
+  if (!self->context_menu) {
+    self->context_menu = GTK_MENU (gtk_menu_new_from_model (model));
+    gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self->context_menu)),
+                                 GTK_STYLE_CLASS_CONTEXT_MENU);
+
+    g_signal_connect_object (self->context_menu,
+                             "deactivate",
+                             G_CALLBACK (popup_menu_deactivate_cb),
+                             self,
+                             G_CONNECT_SWAPPED);
+
+    gtk_menu_attach_to_widget (self->context_menu, GTK_WIDGET (self),
+                               (GtkMenuDetachFunc) popup_menu_detach);
+  }
+
+  if (event && gdk_event_triggers_context_menu (event))
+    gtk_menu_popup_at_pointer (self->context_menu, event);
+  else {
+    gtk_menu_popup_at_widget (self->context_menu,
+                              hdy_tab_get_child (info->tab),
+                              GDK_GRAVITY_SOUTH_WEST,
+                              GDK_GRAVITY_NORTH_WEST,
+                              event);
+
+    gtk_menu_shell_select_first (GTK_MENU_SHELL (self->context_menu), FALSE);
+  }
+}
+
+/* Overrides */
+
+static void
+hdy_tab_box_measure (GtkWidget      *widget,
+                     GtkOrientation  orientation,
+                     gint            for_size,
+                     gint           *minimum,
+                     gint           *natural,
+                     gint           *minimum_baseline,
+                     gint           *natural_baseline)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+  gint min, nat;
+
+  if (self->n_tabs == 0) {
+    if (minimum)
+      *minimum = 0;
+
+    if (natural)
+      *natural = 0;
+
+    if (minimum_baseline)
+      *minimum_baseline = -1;
+
+    if (natural_baseline)
+      *natural_baseline = -1;
+
+    return;
+  }
+
+  if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+    gint width = self->end_padding - OVERLAP;
+    GList *l;
+
+    for (l = self->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+
+      gint child_width = hdy_tab_get_child_min_width (info->tab);
+
+      width += calculate_tab_width (info, child_width) - OVERLAP;
+    }
+
+    min = nat = MAX (self->last_width, width);
+  } else {
+    GList *l;
+
+    min = nat = 0;
+
+    for (l = self->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+      gint child_min, child_nat;
+
+      gtk_widget_get_preferred_height (GTK_WIDGET (info->tab),
+                                       &child_min,
+                                       &child_nat);
+
+      if (child_min > min)
+        min = child_min;
+
+      if (child_nat > nat)
+        nat = child_nat;
+    }
+  }
+
+  hdy_css_measure (widget, orientation, &min, &nat);
+
+  if (minimum)
+    *minimum = min;
+
+  if (natural)
+    *natural = nat;
+
+  if (minimum_baseline)
+    *minimum_baseline = -1;
+
+  if (natural_baseline)
+    *natural_baseline = -1;
+}
+
+static void
+hdy_tab_box_get_preferred_width (GtkWidget *widget,
+                                 gint      *minimum,
+                                 gint      *natural)
+{
+  hdy_tab_box_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+                       minimum, natural,
+                       NULL, NULL);
+}
+
+static void
+hdy_tab_box_get_preferred_height (GtkWidget *widget,
+                                  gint      *minimum,
+                                  gint      *natural)
+{
+  hdy_tab_box_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+                       minimum, natural,
+                       NULL, NULL);
+}
+
+static void
+hdy_tab_box_get_preferred_width_for_height (GtkWidget *widget,
+                                            gint       height,
+                                            gint      *minimum,
+                                            gint      *natural)
+{
+  hdy_tab_box_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+                       minimum, natural,
+                       NULL, NULL);
+}
+
+static void
+hdy_tab_box_get_preferred_height_for_width (GtkWidget *widget,
+                                            gint       width,
+                                            gint      *minimum,
+                                            gint      *natural)
+{
+  hdy_tab_box_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+                       minimum, natural,
+                       NULL, NULL);
+}
+
+static void
+hdy_tab_box_size_allocate (GtkWidget     *widget,
+                           GtkAllocation *allocation)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+  gboolean is_rtl, should_scroll_to_selected;
+  GList *l;
+  GtkAllocation child_allocation;
+  gint pos;
+
+  is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
+  should_scroll_to_selected = self->selected_tab && self->selected_tab->width < 0;
+
+  hdy_css_size_allocate_self (widget, allocation);
+
+  GTK_WIDGET_CLASS (hdy_tab_box_parent_class)->size_allocate (widget, allocation);
+
+  if (gtk_widget_get_realized (widget))
+    gdk_window_move_resize (self->window,
+                            allocation->x, allocation->y,
+                            allocation->width, allocation->height);
+
+  allocation->x = 0;
+  allocation->y = 0;
+  hdy_css_size_allocate_children (widget, allocation);
+
+  self->allocated_width = allocation->width;
+
+  if (!self->n_tabs)
+      return;
+
+  if (self->pinned) {
+    for (l = self->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+      gdouble min_width = hdy_tab_get_child_min_width (info->tab);
+
+      info->width = calculate_tab_width (info, min_width);
+    }
+  } else if (self->tab_resize_mode == TAB_RESIZE_FIXED_TAB_WIDTH) {
+    self->end_padding = allocation->width + OVERLAP;
+
+    for (l = self->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+
+      info->width = calculate_tab_width (info, info->last_width);
+      self->end_padding -= info->width - OVERLAP;
+    }
+  } else {
+    gint tab_width = get_base_tab_width (self);
+    gint excess = allocation->width + OVERLAP - self->end_padding;
+
+    for (l = self->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+
+      info->width = calculate_tab_width (info, tab_width);
+      excess -= info->width - OVERLAP;
+    }
+
+    /* Now spread excess width across the tabs */
+    for (l = self->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+
+      if (excess <= 0)
+          break;
+
+      info->width++;
+      excess--;
+    }
+  }
+
+  pos = allocation->x + (is_rtl ? allocation->width + OVERLAP : -OVERLAP);
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (!info->appear_animation)
+      hdy_tab_set_display_width (info->tab, info->width);
+    else if (info->page)
+      hdy_tab_set_display_width (info->tab, predict_tab_width (self, info, FALSE));
+
+    info->pos = pos + calculate_tab_offset (self, info);
+
+    if (is_rtl)
+      info->pos -= info->width;
+
+    child_allocation.x = (info == self->reordered_tab) ? 0 : info->pos;
+    child_allocation.y = allocation->y;
+    child_allocation.width = info->width;
+    child_allocation.height = allocation->height;
+
+    gtk_widget_size_allocate (GTK_WIDGET (info->tab), &child_allocation);
+
+    pos += (is_rtl ? -1 : 1) * (info->width - OVERLAP);
+  }
+
+  if (should_scroll_to_selected)
+      scroll_to_tab (self, self->selected_tab, FOCUS_ANIMATION_DURATION);
+
+  if (self->scroll_animation) {
+    hdy_tab_box_set_block_scrolling (self, TRUE);
+    gtk_adjustment_set_value (self->adjustment,
+                              get_scroll_animation_value (self));
+    hdy_tab_box_set_block_scrolling (self, FALSE);
+
+    if (self->scroll_animation_done) {
+        self->scroll_animation_done = FALSE;
+        self->scroll_animation_tab = NULL;
+        g_clear_object (&self->scroll_animation);
+    }
+  }
+
+  update_hover (self);
+  update_needs_attention (self);
+}
+
+static gboolean
+hdy_tab_box_focus (GtkWidget        *widget,
+                   GtkDirectionType  direction)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+
+  if (!self->selected_tab)
+    return GDK_EVENT_PROPAGATE;
+
+  return gtk_widget_child_focus (GTK_WIDGET (self->selected_tab->tab), direction);
+}
+
+static void
+hdy_tab_box_realize (GtkWidget *widget)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+  GtkAllocation allocation;
+  GdkWindowAttr attributes;
+  GdkWindowAttributesType attributes_mask;
+  GList *l;
+
+  gtk_widget_set_realized (widget, TRUE);
+
+  gtk_widget_get_allocation (widget, &allocation);
+
+  attributes.x = allocation.x;
+  attributes.y = allocation.y;
+  attributes.width = allocation.width;
+  attributes.height = allocation.height;
+  attributes.window_type = GDK_WINDOW_CHILD;
+  attributes.wclass = GDK_INPUT_OUTPUT;
+  attributes.visual = gtk_widget_get_visual (widget);
+  attributes.event_mask = gtk_widget_get_events (widget);
+  attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
+
+  self->window = gdk_window_new (gtk_widget_get_parent_window (widget),
+                                 &attributes,
+                                 attributes_mask);
+
+  gtk_widget_set_window (widget, self->window);
+  gtk_widget_register_window (widget, self->window);
+
+  self->reorder_window = gdk_window_new (self->window, &attributes, attributes_mask);
+  gtk_widget_register_window (widget, self->reorder_window);
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    gtk_widget_set_parent_window (GTK_WIDGET (info->tab), self->window);
+  }
+}
+
+static void
+hdy_tab_box_unrealize (GtkWidget *widget)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+
+  self->window = NULL;
+
+  if (self->reorder_window) {
+    gtk_widget_unregister_window (widget, self->reorder_window);
+    gdk_window_destroy (self->reorder_window);
+    self->reorder_window = NULL;
+  }
+
+  if (self->context_menu) {
+    gtk_widget_destroy (GTK_WIDGET (self->context_menu));
+    self->context_menu = NULL;
+  }
+
+  if (self->touch_menu) {
+    gtk_widget_destroy (GTK_WIDGET (self->touch_menu));
+    self->touch_menu = NULL;
+  }
+
+  GTK_WIDGET_CLASS (hdy_tab_box_parent_class)->unrealize (widget);
+}
+
+static void
+hdy_tab_box_map (GtkWidget *widget)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+
+  GTK_WIDGET_CLASS (hdy_tab_box_parent_class)->map (widget);
+
+  gdk_window_show_unraised (self->window);
+
+  if (self->reordered_tab)
+    gdk_window_show (self->reorder_window);
+}
+
+static void
+hdy_tab_box_unmap (GtkWidget *widget)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+
+  force_end_reordering (self);
+
+  if (self->drag_autoscroll_cb_id) {
+    gtk_widget_remove_tick_callback (widget, self->drag_autoscroll_cb_id);
+    self->drag_autoscroll_cb_id = 0;
+  }
+
+  if (self->reordered_tab)
+    gdk_window_hide (self->reorder_window);
+
+  self->hovering = FALSE;
+  update_hover (self);
+
+  gdk_window_hide (self->window);
+
+  GTK_WIDGET_CLASS (hdy_tab_box_parent_class)->unmap (widget);
+}
+
+static void
+hdy_tab_box_direction_changed (GtkWidget        *widget,
+                               GtkTextDirection  previous_direction)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+  gdouble upper, page_size;
+
+  if (!self->adjustment)
+      return;
+
+  if (gtk_widget_get_direction (widget) == previous_direction)
+      return;
+
+  upper = gtk_adjustment_get_value (self->adjustment);
+  page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+  gtk_adjustment_set_value (self->adjustment,
+                            upper - page_size - self->adjustment_prev_value);
+}
+
+static gboolean
+hdy_tab_box_draw (GtkWidget *widget,
+                  cairo_t   *cr)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+
+  if (!self->n_tabs)
+    return GDK_EVENT_PROPAGATE;
+
+  hdy_css_draw (widget, cr);
+
+  return GTK_WIDGET_CLASS (hdy_tab_box_parent_class)->draw (widget, cr);
+}
+
+static gboolean
+hdy_tab_box_popup_menu (GtkWidget *widget)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+
+  if (self->selected_tab && self->selected_tab->page) {
+    do_popup (self, self->selected_tab, NULL);
+
+    return GDK_EVENT_STOP;
+  }
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+hdy_tab_box_enter_notify_event (GtkWidget        *widget,
+                                GdkEventCrossing *event)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+
+  if (event->window != self->window || event->detail == GDK_NOTIFY_INFERIOR)
+    return GDK_EVENT_PROPAGATE;
+
+  self->hovering = TRUE;
+
+  get_widget_coordinates (self, (GdkEvent *) event, &self->hover_x, &self->hover_y);
+  update_hover (self);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+hdy_tab_box_leave_notify_event (GtkWidget        *widget,
+                                GdkEventCrossing *event)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+
+  if (event->window != self->window || event->detail == GDK_NOTIFY_INFERIOR)
+    return GDK_EVENT_PROPAGATE;
+
+  self->hovering = FALSE;
+  update_hover (self);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+hdy_tab_box_motion_notify_event (GtkWidget      *widget,
+                                 GdkEventMotion *event)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+
+  self->hovering = TRUE;
+
+  get_widget_coordinates (self, (GdkEvent *) event, &self->hover_x, &self->hover_y);
+
+  update_hover (self);
+
+  if (!self->pressed)
+    return GDK_EVENT_PROPAGATE;
+
+  if (self->hovered_tab &&
+      gtk_drag_check_threshold (widget,
+                                (gint) self->drag_begin_x,
+                                (gint) self->drag_begin_y,
+                                (gint) self->hover_x,
+                                (gint) self->hover_y))
+    start_dragging (self, (GdkEvent *) event, self->hovered_tab);
+
+  if (!self->dragging)
+    return GDK_EVENT_PROPAGATE;
+
+  self->reorder_x = (gint) round (self->hover_x - self->drag_offset_x);
+  self->reorder_y = (gint) round (self->hover_y - self->drag_offset_y);
+
+  if (!self->pinned &&
+      self->hovered_tab &&
+      self->hovered_tab != self->reorder_placeholder &&
+      self->hovered_tab->page &&
+      hdy_tab_view_get_n_pages (self->view) > 1 &&
+      check_dnd_threshold (self)) {
+    gtk_drag_begin_with_coordinates (widget,
+                                     self->source_targets,
+                                     GDK_ACTION_MOVE,
+                                     (gint) self->pressed_button,
+                                     (GdkEvent *) event,
+                                     self->reorder_x,
+                                     self->reorder_y);
+
+    return GDK_EVENT_STOP;
+  }
+
+  update_dragging (self);
+
+  return GDK_EVENT_STOP;
+}
+
+static gboolean
+hdy_tab_box_button_press_event (GtkWidget      *widget,
+                                GdkEventButton *event)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+  gboolean can_grab_focus;
+
+  get_widget_coordinates (self, (GdkEvent *) event, &self->hover_x, &self->hover_y);
+
+  update_hover (self);
+
+  if (!self->hovered_tab || !self->hovered_tab->page)
+      return GDK_EVENT_PROPAGATE;
+
+  if (gdk_event_triggers_context_menu ((GdkEvent *) event)) {
+    do_popup (self, self->hovered_tab, (GdkEvent *) event);
+
+    return GDK_EVENT_STOP;
+  }
+
+  self->pressed_button = event->button;
+
+  if (self->pressed_button == GDK_BUTTON_MIDDLE && !self->pinned) {
+    hdy_tab_view_close_page (self->view, self->hovered_tab->page);
+
+    return GDK_EVENT_STOP;
+  }
+
+  if (self->pressed_button != GDK_BUTTON_PRIMARY)
+    return GDK_EVENT_PROPAGATE;
+
+  if (self->adjustment) {
+    gint pos = get_tab_position (self, self->hovered_tab);
+    gdouble value = gtk_adjustment_get_value (self->adjustment);
+    gdouble page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+    if (pos + OVERLAP < value ||
+        pos + self->hovered_tab->width - OVERLAP > value + page_size) {
+      scroll_to_tab (self, self->hovered_tab, SCROLL_ANIMATION_DURATION);
+
+      return GDK_EVENT_STOP;
+    }
+  }
+
+  can_grab_focus = hdy_tab_bar_tabs_have_visible_focus (self->tab_bar);
+
+  if (self->hovered_tab == self->selected_tab)
+    can_grab_focus = TRUE;
+  else
+    hdy_tab_view_set_selected_page (self->view, self->hovered_tab->page);
+
+  if (can_grab_focus)
+    gtk_widget_grab_focus (GTK_WIDGET (self->hovered_tab->tab));
+  else
+    activate_tab (self);
+
+  self->pressed = TRUE;
+  self->drag_begin_x = self->hover_x;
+  self->drag_begin_y = self->hover_y;
+  self->drag_offset_x = self->drag_begin_x - get_tab_position (self, self->hovered_tab);
+  self->drag_offset_y = self->drag_begin_y;
+
+  if (!self->reorder_animation) {
+    self->reorder_x = (gint) round (self->hover_x - self->drag_offset_x);
+    self->reorder_y = (gint) round (self->hover_y - self->drag_offset_y);
+  }
+
+  return GDK_EVENT_STOP;
+}
+
+static gboolean
+hdy_tab_box_button_release_event (GtkWidget      *widget,
+                                  GdkEventButton *event)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+
+  self->pressed = FALSE;
+  self->pressed_button = 0;
+
+  end_dragging (self);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+hdy_tab_box_scroll_event (GtkWidget      *widget,
+                          GdkEventScroll *event)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+  gdouble page_size, pow_unit, scroll_unit;
+  GdkDevice *source_device;
+  GdkInputSource input_source;
+  gdouble dx, dy;
+
+  if (!self->adjustment)
+    return GDK_EVENT_PROPAGATE;
+
+  source_device = gdk_event_get_source_device ((GdkEvent *) event);
+  input_source = gdk_device_get_source (source_device);
+
+  if (input_source != GDK_SOURCE_MOUSE)
+    return GDK_EVENT_PROPAGATE;
+
+  if (!gdk_event_get_scroll_deltas ((GdkEvent *) event, &dx, &dy)) {
+    switch (event->direction) {
+    case GDK_SCROLL_UP:
+      dy = -1;
+      break;
+
+    case GDK_SCROLL_DOWN:
+      dy = 1;
+      break;
+
+    case GDK_SCROLL_LEFT:
+      dx = -1;
+      break;
+
+    case GDK_SCROLL_RIGHT:
+      dx = 1;
+      break;
+
+    case GDK_SCROLL_SMOOTH:
+    default:
+      g_assert_not_reached ();
+    }
+  }
+
+  if (dx != 0)
+    return GDK_EVENT_PROPAGATE;
+
+  page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+  /* Copied from gtkrange.c, _gtk_range_get_wheel_delta() */
+  pow_unit = pow (page_size, 2.0 / 3.0);
+  scroll_unit = MIN (pow_unit, page_size / 2.0);
+
+  if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
+    dy = -dy;
+
+  animate_scroll_relative (self, dy * scroll_unit, SCROLL_ANIMATION_DURATION);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+hdy_tab_box_drag_begin (GtkWidget      *widget,
+                        GdkDragContext *context)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+  TabInfo *detached_tab;
+
+  if (self->pinned)
+    return;
+
+  create_drag_icon (self, context);
+
+  self->hovering = TRUE;
+  self->pressed = FALSE;
+  self->pressed_button = 0;
+
+  detached_tab = self->reordered_tab;
+  self->detached_page = detached_tab->page;
+
+  self->indirect_reordering = TRUE;
+
+  end_dragging (self);
+  update_hover (self);
+
+  gtk_widget_hide (GTK_WIDGET (detached_tab->tab));
+  self->detached_index = g_list_index (self->tabs, detached_tab);
+
+  hdy_tab_view_start_drag (self->view);
+  hdy_tab_view_detach_page (self->view, self->detached_page);
+
+  self->indirect_reordering = FALSE;
+}
+
+static void
+hdy_tab_box_drag_end (GtkWidget      *widget,
+                      GdkDragContext *context)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+
+  if (self->pinned)
+    return;
+
+  if (self->should_detach_into_new_window)
+    detach_into_new_window (self, context);
+
+  self->detached_page = NULL;
+
+  gtk_widget_destroy (self->drag_icon->window);
+  g_clear_pointer (&self->drag_icon, g_free);
+
+  hdy_tab_view_end_drag (self->view);
+}
+
+static gboolean
+hdy_tab_box_drag_motion (GtkWidget      *widget,
+                         GdkDragContext *context,
+                         gint            x,
+                         gint            y,
+                         guint           time)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+  HdyTabBox *source_tab_box;
+  GdkAtom target, tab_target;
+  gdouble center, display_width;
+
+  if (self->pinned)
+    return GDK_EVENT_PROPAGATE;
+
+  target = gtk_drag_dest_find_target (GTK_WIDGET (self), context, NULL);
+  tab_target = gdk_atom_intern_static_string ("HDY_TAB");
+
+  if (target != tab_target)
+    return GDK_EVENT_PROPAGATE;
+
+  source_tab_box = get_source_tab_box (context);
+
+  if (!source_tab_box)
+    return GDK_EVENT_PROPAGATE;
+
+  if (!self->view || !is_view_in_the_same_group (self, source_tab_box->view))
+    return GDK_EVENT_PROPAGATE;
+
+  center = x - source_tab_box->drag_icon->hotspot_x + source_tab_box->drag_icon->width / 2;
+
+  self->can_remove_placeholder = FALSE;
+
+  if (!self->reorder_placeholder || !self->reorder_placeholder->page) {
+    HdyTabPage *page = source_tab_box->detached_page;
+
+    insert_placeholder (self, page, center);
+
+    self->indirect_reordering = TRUE;
+
+    self->drag_offset_x = source_tab_box->drag_icon->hotspot_x;
+    self->drag_offset_y = source_tab_box->drag_icon->hotspot_y;
+
+    display_width = hdy_tab_get_display_width (self->reorder_placeholder->tab);
+    self->reorder_x = (gint) round (center - display_width / 2);
+
+    start_dragging (self, gtk_get_current_event (), self->reorder_placeholder);
+
+    resize_drag_icon (source_tab_box, predict_tab_width (self, self->reorder_placeholder, TRUE));
+
+    gdk_drag_status (context, GDK_ACTION_MOVE, time);
+
+    return GDK_EVENT_PROPAGATE;
+  }
+
+  display_width = hdy_tab_get_display_width (self->reorder_placeholder->tab);
+  self->reorder_x = (gint) round (center - display_width / 2);
+
+  update_dragging (self);
+
+  gdk_drag_status (context, GDK_ACTION_MOVE, time);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+hdy_tab_box_drag_leave (GtkWidget      *widget,
+                        GdkDragContext *context,
+                        guint           time)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+  HdyTabBox *source_tab_box;
+  GdkAtom target, tab_target;
+
+  if (self->pinned)
+    return;
+
+  source_tab_box = get_source_tab_box (context);
+
+  if (!source_tab_box)
+    return;
+
+  if (!self->view || !is_view_in_the_same_group (self, source_tab_box->view))
+    return;
+
+  target = gtk_drag_dest_find_target (GTK_WIDGET (self), context, NULL);
+  tab_target = gdk_atom_intern_static_string ("HDY_TAB");
+
+  if (target != tab_target)
+    return;
+
+  self->can_remove_placeholder = TRUE;
+
+  end_dragging (self);
+  remove_placeholder (self);
+
+  self->indirect_reordering = FALSE;
+}
+
+static gboolean
+hdy_tab_box_drag_drop (GtkWidget      *widget,
+                       GdkDragContext *context,
+                       gint            x,
+                       gint            y,
+                       guint           time)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+  HdyTabBox *source_tab_box;
+
+  if (self->pinned)
+    return GDK_EVENT_PROPAGATE;
+
+  source_tab_box = get_source_tab_box (context);
+
+  if (!source_tab_box)
+    return GDK_EVENT_PROPAGATE;
+
+  if (!self->view || !is_view_in_the_same_group (self, source_tab_box->view))
+    return GDK_EVENT_PROPAGATE;
+
+  return do_drag_drop (self, context, time);
+}
+
+static gboolean
+hdy_tab_box_drag_failed (GtkWidget *widget,
+                         GdkDragContext *context,
+                         GtkDragResult result)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+
+  self->should_detach_into_new_window = FALSE;
+
+  if (result == GTK_DRAG_RESULT_NO_TARGET) {
+    detach_into_new_window (self, context);
+
+    return GDK_EVENT_STOP;
+  }
+
+  hdy_tab_view_attach_page (self->view,
+                            self->detached_page,
+                            self->detached_index);
+
+  self->indirect_reordering = FALSE;
+
+  return GDK_EVENT_STOP;
+}
+
+static void
+hdy_tab_box_drag_data_get (GtkWidget        *widget,
+                           GdkDragContext   *context,
+                           GtkSelectionData *data,
+                           guint             info,
+                           guint             time)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+  GdkAtom target, rootwindow_target;
+
+  target = gtk_selection_data_get_target (data);
+  rootwindow_target = gdk_atom_intern_static_string ("application/x-rootwindow-drop");
+
+  if (target == rootwindow_target) {
+    self->should_detach_into_new_window = TRUE;
+    gtk_selection_data_set (data, target, 8, NULL, 0);
+  }
+}
+
+static void
+hdy_tab_box_forall (GtkContainer *container,
+                    gboolean      include_internals,
+                    GtkCallback   callback,
+                    gpointer      callback_data)
+{
+  HdyTabBox *self = HDY_TAB_BOX (container);
+  GList *l;
+
+  if (!include_internals)
+    return;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    callback (GTK_WIDGET (info->tab), callback_data);
+  }
+}
+
+static void
+hdy_tab_box_dispose (GObject *object)
+{
+  HdyTabBox *self = HDY_TAB_BOX (object);
+
+  self->tab_bar = NULL;
+  hdy_tab_box_set_view (self, NULL);
+  hdy_tab_box_set_adjustment (self, NULL);
+
+  G_OBJECT_CLASS (hdy_tab_box_parent_class)->dispose (object);
+}
+
+static void
+hdy_tab_box_finalize (GObject *object)
+{
+  HdyTabBox *self = (HdyTabBox *) object;
+
+  g_clear_object (&self->touch_menu_gesture);
+
+  G_OBJECT_CLASS (hdy_tab_box_parent_class)->finalize (object);
+}
+
+static void
+hdy_tab_box_get_property (GObject    *object,
+                          guint       prop_id,
+                          GValue     *value,
+                          GParamSpec *pspec)
+{
+  HdyTabBox *self = HDY_TAB_BOX (object);
+
+  switch (prop_id) {
+  case PROP_PINNED:
+    g_value_set_boolean (value, self->pinned);
+    break;
+
+  case PROP_TAB_BAR:
+    g_value_set_object (value, self->tab_bar);
+    break;
+
+  case PROP_VIEW:
+    g_value_set_object (value, self->view);
+    break;
+
+  case PROP_ADJUSTMENT:
+    g_value_set_object (value, self->adjustment);
+    break;
+
+  case PROP_NEEDS_ATTENTION_LEFT:
+    g_value_set_boolean (value, self->needs_attention_left);
+    break;
+
+  case PROP_NEEDS_ATTENTION_RIGHT:
+    g_value_set_boolean (value, self->needs_attention_right);
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+hdy_tab_box_set_property (GObject      *object,
+                          guint         prop_id,
+                          const GValue *value,
+                          GParamSpec   *pspec)
+{
+  HdyTabBox *self = HDY_TAB_BOX (object);
+
+  switch (prop_id) {
+  case PROP_PINNED:
+    self->pinned = g_value_get_boolean (value);
+    break;
+
+  case PROP_TAB_BAR:
+    self->tab_bar = g_value_get_object (value);
+    break;
+
+  case PROP_VIEW:
+    hdy_tab_box_set_view (self, g_value_get_object (value));
+    break;
+
+  case PROP_ADJUSTMENT:
+    hdy_tab_box_set_adjustment (self, g_value_get_object (value));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+hdy_tab_box_class_init (HdyTabBoxClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+  GtkBindingSet *binding_set;
+
+  object_class->dispose = hdy_tab_box_dispose;
+  object_class->finalize = hdy_tab_box_finalize;
+  object_class->get_property = hdy_tab_box_get_property;
+  object_class->set_property = hdy_tab_box_set_property;
+
+  widget_class->get_preferred_width = hdy_tab_box_get_preferred_width;
+  widget_class->get_preferred_height = hdy_tab_box_get_preferred_height;
+  widget_class->get_preferred_width_for_height = hdy_tab_box_get_preferred_width_for_height;
+  widget_class->get_preferred_height_for_width = hdy_tab_box_get_preferred_height_for_width;
+  widget_class->size_allocate = hdy_tab_box_size_allocate;
+  widget_class->focus = hdy_tab_box_focus;
+  widget_class->realize = hdy_tab_box_realize;
+  widget_class->unrealize = hdy_tab_box_unrealize;
+  widget_class->map = hdy_tab_box_map;
+  widget_class->unmap = hdy_tab_box_unmap;
+  widget_class->direction_changed = hdy_tab_box_direction_changed;
+  widget_class->draw = hdy_tab_box_draw;
+  widget_class->popup_menu = hdy_tab_box_popup_menu;
+  widget_class->enter_notify_event = hdy_tab_box_enter_notify_event;
+  widget_class->leave_notify_event = hdy_tab_box_leave_notify_event;
+  widget_class->motion_notify_event = hdy_tab_box_motion_notify_event;
+  widget_class->button_press_event = hdy_tab_box_button_press_event;
+  widget_class->button_release_event = hdy_tab_box_button_release_event;
+  widget_class->scroll_event = hdy_tab_box_scroll_event;
+  widget_class->drag_begin = hdy_tab_box_drag_begin;
+  widget_class->drag_end = hdy_tab_box_drag_end;
+  widget_class->drag_motion = hdy_tab_box_drag_motion;
+  widget_class->drag_leave = hdy_tab_box_drag_leave;
+  widget_class->drag_drop = hdy_tab_box_drag_drop;
+  widget_class->drag_failed = hdy_tab_box_drag_failed;
+  widget_class->drag_data_get = hdy_tab_box_drag_data_get;
+
+  container_class->forall = hdy_tab_box_forall;
+
+  props[PROP_PINNED] =
+    g_param_spec_boolean ("pinned",
+                          _("Pinned"),
+                          _("Pinned"),
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  props[PROP_TAB_BAR] =
+    g_param_spec_object ("tab-bar",
+                         _("Tab Bar"),
+                         _("Tab Bar"),
+                         HDY_TYPE_TAB_BAR,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  props[PROP_VIEW] =
+    g_param_spec_object ("view",
+                         _("View"),
+                         _("View"),
+                         HDY_TYPE_TAB_VIEW,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_ADJUSTMENT] =
+    g_param_spec_object ("adjustment",
+                         _("Adjustment"),
+                         _("Adjustment"),
+                         GTK_TYPE_ADJUSTMENT,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_NEEDS_ATTENTION_LEFT] =
+    g_param_spec_boolean ("needs-attention-left",
+                          _("Needs Attention Left"),
+                          _("Needs Attention Left"),
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_NEEDS_ATTENTION_RIGHT] =
+    g_param_spec_boolean ("needs-attention-right",
+                          _("Needs Attention Right"),
+                          _("Needs Attention Right"),
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  signals[SIGNAL_STOP_KINETIC_SCROLLING] =
+    g_signal_new ("stop-kinetic-scrolling",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  0);
+
+  signals[SIGNAL_ACTIVATE_TAB] =
+    g_signal_new ("activate-tab",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  0);
+
+  signals[SIGNAL_FOCUS_TAB] =
+    g_signal_new ("focus-tab",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2, GTK_TYPE_DIRECTION_TYPE, G_TYPE_BOOLEAN);
+
+  signals[SIGNAL_REORDER_TAB] =
+    g_signal_new ("reorder-tab",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2, GTK_TYPE_DIRECTION_TYPE, G_TYPE_BOOLEAN);
+
+  binding_set = gtk_binding_set_by_class (klass);
+
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_space,     0, "activate-tab", 0);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Space,  0, "activate-tab", 0);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_Return,    0, "activate-tab", 0);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_ISO_Enter, 0, "activate-tab", 0);
+  gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Enter,  0, "activate-tab", 0);
+
+  add_focus_bindings (binding_set, GDK_KEY_Page_Up,   GTK_DIR_TAB_BACKWARD, FALSE);
+  add_focus_bindings (binding_set, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD,  FALSE);
+  add_focus_bindings (binding_set, GDK_KEY_Home,      GTK_DIR_TAB_BACKWARD, TRUE);
+  add_focus_bindings (binding_set, GDK_KEY_End,       GTK_DIR_TAB_FORWARD,  TRUE);
+
+  add_reorder_bindings (binding_set, GDK_KEY_Left,      GTK_DIR_LEFT,         FALSE);
+  add_reorder_bindings (binding_set, GDK_KEY_Right,     GTK_DIR_RIGHT,        FALSE);
+  add_reorder_bindings (binding_set, GDK_KEY_Page_Up,   GTK_DIR_TAB_BACKWARD, FALSE);
+  add_reorder_bindings (binding_set, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD,  FALSE);
+  add_reorder_bindings (binding_set, GDK_KEY_Home,      GTK_DIR_TAB_BACKWARD, TRUE);
+  add_reorder_bindings (binding_set, GDK_KEY_End,       GTK_DIR_TAB_FORWARD,  TRUE);
+
+  gtk_widget_class_set_css_name (widget_class, "tabbox");
+}
+
+static void
+hdy_tab_box_init (HdyTabBox *self)
+{
+  self->can_remove_placeholder = TRUE;
+
+  gtk_widget_add_events (GTK_WIDGET (self),
+                         GDK_BUTTON_PRESS_MASK |
+                         GDK_BUTTON_RELEASE_MASK |
+                         GDK_BUTTON_MOTION_MASK |
+                         GDK_POINTER_MOTION_MASK |
+                         GDK_TOUCH_MASK |
+                         GDK_ENTER_NOTIFY_MASK |
+                         GDK_LEAVE_NOTIFY_MASK |
+                         GDK_SCROLL_MASK |
+                         GDK_SMOOTH_SCROLL_MASK);
+
+  self->touch_menu_gesture = g_object_new (GTK_TYPE_GESTURE_LONG_PRESS,
+                                           "widget", GTK_WIDGET (self),
+                                           "propagation-phase", GTK_PHASE_TARGET,
+                                           "touch-only", TRUE,
+                                           NULL);
+
+  g_signal_connect_object (self->touch_menu_gesture,
+                           "pressed",
+                           G_CALLBACK (touch_menu_gesture_pressed),
+                           self,
+                           G_CONNECT_SWAPPED);
+
+  gtk_drag_dest_set (GTK_WIDGET (self),
+                     GTK_DEST_DEFAULT_MOTION,
+                     dst_targets,
+                     G_N_ELEMENTS (dst_targets),
+                     GDK_ACTION_MOVE);
+
+  self->source_targets = gtk_target_list_new (src_targets,
+                                              G_N_ELEMENTS (src_targets));
+
+  g_signal_connect_object (self, "activate-tab", G_CALLBACK (activate_tab), self, G_CONNECT_AFTER);
+  g_signal_connect_object (self, "focus-tab", G_CALLBACK (focus_tab_cb), self, 0);
+  g_signal_connect_object (self, "reorder-tab", G_CALLBACK (reorder_tab_cb), self, 0);
+}
+
+void
+hdy_tab_box_set_view (HdyTabBox  *self,
+                      HdyTabView *view)
+{
+  g_return_if_fail (HDY_IS_TAB_BOX (self));
+  g_return_if_fail (HDY_IS_TAB_VIEW (view) || view == NULL);
+
+  if (view == self->view)
+    return;
+
+  if (self->view) {
+    force_end_reordering (self);
+
+    g_signal_handlers_disconnect_by_func (self->view, add_page, self);
+    g_signal_handlers_disconnect_by_func (self->view, remove_page, self);
+    g_signal_handlers_disconnect_by_func (self->view, reorder_page, self);
+
+    if (!self->pinned)
+      g_signal_handlers_disconnect_by_func (self->view, view_drag_drop_cb, self);
+
+    g_list_free_full (self->tabs, (GDestroyNotify) remove_and_free_tab_info);
+    self->tabs = NULL;
+    self->n_tabs = 0;
+
+    gtk_widget_queue_allocate (GTK_WIDGET (self));
+  }
+
+  self->view = view;
+
+  if (self->view) {
+    int i, n_pages = hdy_tab_view_get_n_pages (self->view);
+
+    for (i = n_pages - 1; i >= 0; i--)
+      add_page (self, hdy_tab_view_get_nth_page (self->view, i), 0);
+
+    g_signal_connect_object (self->view, "page-added", G_CALLBACK (add_page), self, G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->view, "page-removed", G_CALLBACK (remove_page), self, G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->view, "page-reordered", G_CALLBACK (reorder_page), self, 
G_CONNECT_SWAPPED);
+
+    if (!self->pinned)
+      g_signal_connect_object (self->view, "drag-drop", G_CALLBACK (view_drag_drop_cb), self, 
G_CONNECT_SWAPPED);
+  }
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW]);
+}
+
+void
+hdy_tab_box_set_adjustment (HdyTabBox     *self,
+                            GtkAdjustment *adjustment)
+{
+  g_return_if_fail (HDY_IS_TAB_BOX (self));
+  g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment) || adjustment == NULL);
+
+  if (adjustment == self->adjustment)
+    return;
+
+  if (self->adjustment) {
+    g_signal_handlers_disconnect_by_func (self->adjustment, adjustment_value_changed_cb, self);
+    g_signal_handlers_disconnect_by_func (self->adjustment, update_needs_attention, self);
+  }
+
+  g_set_object (&self->adjustment, adjustment);
+
+  if (self->adjustment) {
+    g_signal_connect_object (self->adjustment, "value-changed", G_CALLBACK (adjustment_value_changed_cb), 
self, G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->adjustment, "notify::page-size", G_CALLBACK (update_needs_attention), 
self, G_CONNECT_SWAPPED);
+  }
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ADJUSTMENT]);
+}
+
+void
+hdy_tab_box_set_block_scrolling (HdyTabBox *self,
+                                 gboolean   block_scrolling)
+{
+  self->block_scrolling = block_scrolling;
+}
+
+void
+hdy_tab_box_add_page (HdyTabBox  *self,
+                      HdyTabPage *page,
+                      guint       position)
+{
+  add_page (self, page, position);
+}
+
+void
+hdy_tab_box_remove_page (HdyTabBox  *self,
+                         HdyTabPage *page)
+{
+  remove_page (self, page);
+}
+
+void
+hdy_tab_box_select_page (HdyTabBox  *self,
+                         HdyTabPage *page)
+{
+  select_page (self, page);
+}
+
+void
+hdy_tab_box_try_focus_selected_tab (HdyTabBox  *self)
+{
+  if (self->selected_tab)
+    gtk_widget_grab_focus (GTK_WIDGET (self->selected_tab->tab));
+}
+
+gboolean
+hdy_tab_box_is_page_focused (HdyTabBox  *self,
+                             HdyTabPage *page)
+{
+  TabInfo *info = find_info_for_page (self, page);
+
+  return info && gtk_widget_is_focus (GTK_WIDGET (info->tab));
+}
diff --git a/src/hdy-tab-private.h b/src/hdy-tab-private.h
new file mode 100644
index 00000000..0ee2c856
--- /dev/null
+++ b/src/hdy-tab-private.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include "hdy-tab-view.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_TAB (hdy_tab_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyTab, hdy_tab, HDY, TAB, GtkContainer)
+
+HdyTab *hdy_tab_new (HdyTabView *view,
+                     gboolean    pinned,
+                     gboolean    dragging);
+
+void hdy_tab_set_page (HdyTab     *self,
+                       HdyTabPage *page);
+
+GtkWidget *hdy_tab_get_child (HdyTab *self);
+
+gint hdy_tab_get_child_min_width (HdyTab *self);
+
+gint hdy_tab_get_display_width (HdyTab *self);
+void hdy_tab_set_display_width (HdyTab *self,
+                                gint    width);
+
+gboolean hdy_tab_get_hovering (HdyTab *self);
+void     hdy_tab_set_hovering (HdyTab   *self,
+                               gboolean  hovering);
+
+gboolean hdy_tab_get_selected (HdyTab *self);
+void     hdy_tab_set_selected (HdyTab   *self,
+                               gboolean  selected);
+
+G_END_DECLS
diff --git a/src/hdy-tab-view-private.h b/src/hdy-tab-view-private.h
new file mode 100644
index 00000000..6ffce07e
--- /dev/null
+++ b/src/hdy-tab-view-private.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-tab-view.h"
+
+G_BEGIN_DECLS
+
+void hdy_tab_view_start_drag (HdyTabView *self);
+void hdy_tab_view_end_drag (HdyTabView *self);
+
+void hdy_tab_view_detach_page   (HdyTabView *self,
+                                 HdyTabPage *page);
+void hdy_tab_view_attach_page   (HdyTabView *self,
+                                 HdyTabPage *page,
+                                 guint       position);
+
+G_END_DECLS
diff --git a/src/hdy-tab-view.c b/src/hdy-tab-view.c
new file mode 100644
index 00000000..f890097a
--- /dev/null
+++ b/src/hdy-tab-view.c
@@ -0,0 +1,1791 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-tab-view-private.h"
+
+static const GtkTargetEntry dst_targets [] = {
+  { "HDY_TAB", GTK_TARGET_SAME_APP, 0 },
+};
+
+struct _HdyTabPage
+{
+  GObject parent_instance;
+
+  GtkWidget *content;
+  gboolean selected;
+  gboolean pinned;
+  gchar *title;
+  gchar *tooltip;
+  GIcon *icon;
+  gboolean loading;
+  GIcon *secondary_icon;
+  gboolean needs_attention;
+};
+
+G_DEFINE_TYPE (HdyTabPage, hdy_tab_page, G_TYPE_OBJECT)
+
+enum {
+  PAGE_PROP_0,
+  PAGE_PROP_CONTENT,
+  PAGE_PROP_SELECTED,
+  PAGE_PROP_PINNED,
+  PAGE_PROP_TITLE,
+  PAGE_PROP_TOOLTIP,
+  PAGE_PROP_ICON,
+  PAGE_PROP_LOADING,
+  PAGE_PROP_SECONDARY_ICON,
+  PAGE_PROP_NEEDS_ATTENTION,
+  LAST_PAGE_PROP
+};
+
+static GParamSpec *page_props[LAST_PAGE_PROP];
+
+struct _HdyTabView
+{
+  GtkBin parent_instance;
+
+  GtkStack *stack;
+  GListStore *pages;
+
+  guint n_pages;
+  guint n_pinned_pages;
+  HdyTabPage *selected_page;
+  GIcon *default_icon;
+  GMenuModel *menu_model;
+
+  GSList *group;
+  gboolean is_dragging;
+};
+
+G_DEFINE_TYPE (HdyTabView, hdy_tab_view, GTK_TYPE_BIN)
+
+enum {
+  PROP_0,
+  PROP_N_PAGES,
+  PROP_N_PINNED_PAGES,
+  PROP_IS_DRAGGING,
+  PROP_SELECTED_PAGE,
+  PROP_DEFAULT_ICON,
+  PROP_MENU_MODEL,
+  PROP_GROUP,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+  SIGNAL_PAGE_ADDED,
+  SIGNAL_PAGE_REMOVED,
+  SIGNAL_PAGE_REORDERED,
+  SIGNAL_PAGE_PINNED,
+  SIGNAL_PAGE_UNPINNED,
+  SIGNAL_SETUP_MENU,
+  SIGNAL_CREATE_WINDOW,
+  SIGNAL_SELECT_PAGE,
+  SIGNAL_REORDER_PAGE,
+  SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+set_page_selected (HdyTabPage *self,
+                   gboolean    selected)
+{
+  g_return_if_fail (HDY_IS_TAB_PAGE (self));
+
+  selected = !!selected;
+
+  if (self->selected == selected)
+    return;
+
+  self->selected = selected;
+
+  g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_SELECTED]);
+}
+
+static void
+set_page_pinned (HdyTabPage *self,
+                 gboolean    pinned)
+{
+  g_return_if_fail (HDY_IS_TAB_PAGE (self));
+
+  pinned = !!pinned;
+
+  if (self->pinned == pinned)
+    return;
+
+  self->pinned = pinned;
+
+  g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_PINNED]);
+}
+
+static void
+hdy_tab_page_dispose (GObject *object)
+{
+  HdyTabPage *self = HDY_TAB_PAGE (object);
+
+  g_clear_object (&self->content);
+
+  G_OBJECT_CLASS (hdy_tab_page_parent_class)->dispose (object);
+}
+
+static void
+hdy_tab_page_finalize (GObject *object)
+{
+  HdyTabPage *self = (HdyTabPage *)object;
+
+  g_clear_pointer (&self->title, g_free);
+  g_clear_pointer (&self->tooltip, g_free);
+  g_clear_object (&self->icon);
+  g_clear_object (&self->secondary_icon);
+
+  G_OBJECT_CLASS (hdy_tab_page_parent_class)->finalize (object);
+}
+
+static void
+hdy_tab_page_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  HdyTabPage *self = HDY_TAB_PAGE (object);
+
+  switch (prop_id) {
+  case PAGE_PROP_CONTENT:
+    g_value_set_object (value, hdy_tab_page_get_content (self));
+    break;
+
+  case PAGE_PROP_SELECTED:
+    g_value_set_boolean (value, hdy_tab_page_get_selected (self));
+    break;
+
+  case PAGE_PROP_PINNED:
+    g_value_set_boolean (value, hdy_tab_page_get_pinned (self));
+    break;
+
+  case PAGE_PROP_TITLE:
+    g_value_set_string (value, hdy_tab_page_get_title (self));
+    break;
+
+  case PAGE_PROP_TOOLTIP:
+    g_value_set_string (value, hdy_tab_page_get_tooltip (self));
+    break;
+
+  case PAGE_PROP_ICON:
+    g_value_set_object (value, hdy_tab_page_get_icon (self));
+    break;
+
+  case PAGE_PROP_LOADING:
+    g_value_set_boolean (value, hdy_tab_page_get_loading (self));
+    break;
+
+  case PAGE_PROP_SECONDARY_ICON:
+    g_value_set_object (value, hdy_tab_page_get_secondary_icon (self));
+    break;
+
+  case PAGE_PROP_NEEDS_ATTENTION:
+    g_value_set_boolean (value, hdy_tab_page_get_needs_attention (self));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+hdy_tab_page_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  HdyTabPage *self = HDY_TAB_PAGE (object);
+
+  switch (prop_id) {
+  case PAGE_PROP_CONTENT:
+    g_set_object (&self->content, g_value_get_object (value));
+    break;
+
+  case PAGE_PROP_TITLE:
+    hdy_tab_page_set_title (self, g_value_get_string (value));
+    break;
+
+  case PAGE_PROP_TOOLTIP:
+    hdy_tab_page_set_tooltip (self, g_value_get_string (value));
+    break;
+
+  case PAGE_PROP_ICON:
+    hdy_tab_page_set_icon (self, g_value_get_object (value));
+    break;
+
+  case PAGE_PROP_LOADING:
+    hdy_tab_page_set_loading (self, g_value_get_boolean (value));
+    break;
+
+  case PAGE_PROP_SECONDARY_ICON:
+    hdy_tab_page_set_secondary_icon (self, g_value_get_object (value));
+    break;
+
+  case PAGE_PROP_NEEDS_ATTENTION:
+    hdy_tab_page_set_needs_attention (self, g_value_get_boolean (value));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+hdy_tab_page_class_init (HdyTabPageClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = hdy_tab_page_dispose;
+  object_class->finalize = hdy_tab_page_finalize;
+  object_class->get_property = hdy_tab_page_get_property;
+  object_class->set_property = hdy_tab_page_set_property;
+
+  page_props[PAGE_PROP_CONTENT] =
+    g_param_spec_object ("content",
+                         _("Content"),
+                         _("Content"),
+                         GTK_TYPE_WIDGET,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  page_props[PAGE_PROP_SELECTED] =
+    g_param_spec_boolean ("selected",
+                         _("Selected"),
+                         _("Selected"),
+                         FALSE,
+                         G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+  page_props[PAGE_PROP_PINNED] =
+    g_param_spec_boolean ("pinned",
+                         _("Pinned"),
+                         _("Pinned"),
+                         FALSE,
+                         G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+  page_props[PAGE_PROP_TITLE] =
+    g_param_spec_string ("title",
+                         _("Title"),
+                         _("Title"),
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  page_props[PAGE_PROP_TOOLTIP] =
+    g_param_spec_string ("tooltip",
+                         _("Tooltip"),
+                         _("Tooltip"),
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  page_props[PAGE_PROP_ICON] =
+    g_param_spec_object ("icon",
+                         _("Icon"),
+                         _("Icon"),
+                         G_TYPE_ICON,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  page_props[PAGE_PROP_LOADING] =
+    g_param_spec_boolean ("loading",
+                         _("Loading"),
+                         _("Loading"),
+                         FALSE,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  page_props[PAGE_PROP_SECONDARY_ICON] =
+    g_param_spec_object ("secondary-icon",
+                         _("Secondary Icon"),
+                         _("Secondary Icon"),
+                         G_TYPE_ICON,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  page_props[PAGE_PROP_NEEDS_ATTENTION] =
+    g_param_spec_boolean ("needs-attention",
+                         _("Needs Attention"),
+                         _("Needs Attention"),
+                         FALSE,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PAGE_PROP, page_props);
+}
+
+static void
+hdy_tab_page_init (HdyTabPage *self)
+{
+}
+
+static gboolean
+object_handled_accumulator (GSignalInvocationHint *ihint,
+                            GValue                *return_accu,
+                            const GValue          *handler_return,
+                            gpointer               data)
+{
+  GObject *object = g_value_get_object (handler_return);
+
+  g_value_set_object (return_accu, object);
+
+  return !object;
+}
+
+static void
+set_is_dragging (HdyTabView *self,
+                 gboolean    is_dragging)
+{
+  if (is_dragging == self->is_dragging)
+    return;
+
+  self->is_dragging = is_dragging;
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IS_DRAGGING]);
+}
+
+static void
+set_n_pages (HdyTabView *self,
+             guint       n_pages)
+{
+  if (n_pages == self->n_pages)
+    return;
+
+  self->n_pages = n_pages;
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_PAGES]);
+}
+
+static void
+set_n_pinned_pages (HdyTabView *self,
+                    guint       n_pinned_pages)
+{
+  if (n_pinned_pages == self->n_pinned_pages)
+    return;
+
+  self->n_pinned_pages = n_pinned_pages;
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_PINNED_PAGES]);
+}
+
+static void
+check_close_window (HdyTabView *self)
+{
+  GtkWidget *toplevel;
+
+  if (self->n_pages > 0)
+    return;
+
+  toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
+
+  if (!GTK_IS_WINDOW (toplevel))
+    return;
+
+  gtk_window_close (GTK_WINDOW (toplevel));
+}
+
+static void
+attach_page (HdyTabView *self,
+             HdyTabPage *page,
+             guint       position)
+{
+  gboolean pinned = hdy_tab_page_get_pinned (page);
+  GtkWidget *content = hdy_tab_page_get_content (page);
+
+  if (!pinned)
+    position += self->n_pinned_pages;
+
+  g_list_store_insert (self->pages, position, page);
+
+  gtk_container_add (GTK_CONTAINER (self->stack), content);
+  gtk_container_child_set (GTK_CONTAINER (self->stack), content,
+                           "position", position,
+                           NULL);
+
+  set_n_pages (self, self->n_pages + 1);
+  if (pinned)
+    set_n_pinned_pages (self, self->n_pages + 1);
+
+  g_signal_emit (self, signals[SIGNAL_PAGE_ADDED], 0, page, position);
+}
+
+static void
+detach_page (HdyTabView *self,
+             HdyTabPage *page)
+{
+  guint pos = hdy_tab_view_get_page_position (self, page);
+
+  if (page == self->selected_page)
+    if (!hdy_tab_view_select_next_page (self))
+      hdy_tab_view_select_previous_page (self);
+
+  g_list_store_remove (self->pages, pos);
+  set_n_pages (self, self->n_pages - 1);
+
+  if (hdy_tab_page_get_pinned (page))
+    set_n_pinned_pages (self, self->n_pinned_pages - 1);
+
+  gtk_container_remove (GTK_CONTAINER (self->stack),
+                        hdy_tab_page_get_content (page));
+
+  g_signal_emit (self, signals[SIGNAL_PAGE_REMOVED], 0, page);
+
+  check_close_window (self);
+}
+
+static HdyTabPage *
+insert_page (HdyTabView *self,
+             GtkWidget  *content,
+             guint       position,
+             gboolean    pinned)
+{
+  HdyTabPage *page;
+
+  g_assert (position <= self->n_pages);
+
+  page = g_object_new (HDY_TYPE_TAB_PAGE, "content", content, NULL);
+
+  set_page_pinned (page, pinned);
+
+  attach_page (self, page, position);
+
+  if (!self->selected_page)
+    hdy_tab_view_set_selected_page (self, page);
+
+  return page;
+}
+
+static void
+set_group_from_view (HdyTabView *self,
+                     HdyTabView *other_view)
+{
+  GSList *slist;
+
+  if (other_view)
+    slist = hdy_tab_view_get_group (other_view);
+  else
+    slist = NULL;
+
+  hdy_tab_view_set_group (self, slist);
+}
+
+static void
+close_page (HdyTabView *self,
+            HdyTabPage *page)
+{
+  detach_page (self, page);
+
+  g_object_unref (page);
+}
+
+static void
+add_select_bindings (GtkBindingSet    *binding_set,
+                     guint             keysym,
+                     GtkDirectionType  direction,
+                     gboolean          last)
+{
+  /* All keypad keysyms are aligned at the same order as non-keypad ones */
+  guint keypad_keysym = keysym - GDK_KEY_Left + GDK_KEY_KP_Left;
+
+  gtk_binding_entry_add_signal (binding_set, keysym, GDK_CONTROL_MASK,
+                                "select-page", 2,
+                                GTK_TYPE_DIRECTION_TYPE, direction,
+                                G_TYPE_BOOLEAN, last);
+  gtk_binding_entry_add_signal (binding_set, keypad_keysym, GDK_CONTROL_MASK,
+                                "select-page", 2,
+                                GTK_TYPE_DIRECTION_TYPE, direction,
+                                G_TYPE_BOOLEAN, last);
+}
+
+static void
+add_reorder_bindings (GtkBindingSet    *binding_set,
+                      guint             keysym,
+                      GtkDirectionType  direction,
+                      gboolean          last)
+{
+  /* All keypad keysyms are aligned at the same order as non-keypad ones */
+  guint keypad_keysym = keysym - GDK_KEY_Left + GDK_KEY_KP_Left;
+
+  gtk_binding_entry_add_signal (binding_set, keysym,
+                                GDK_CONTROL_MASK | GDK_MOD1_MASK,
+                                "reorder-page", 2,
+                                GTK_TYPE_DIRECTION_TYPE, direction,
+                                G_TYPE_BOOLEAN, last);
+  gtk_binding_entry_add_signal (binding_set, keypad_keysym,
+                                GDK_CONTROL_MASK | GDK_MOD1_MASK,
+                                "reorder-page", 2,
+                                GTK_TYPE_DIRECTION_TYPE, direction,
+                                G_TYPE_BOOLEAN, last);
+}
+
+static void
+select_page_cb (HdyTabView       *self,
+                GtkDirectionType  direction,
+                gboolean          last)
+{
+  gboolean is_rtl, success = last;
+
+  if (!self->selected_page)
+    return;
+
+  is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+  if (direction == GTK_DIR_LEFT)
+    direction = is_rtl ? GTK_DIR_TAB_FORWARD : GTK_DIR_TAB_BACKWARD;
+  else if (direction == GTK_DIR_RIGHT)
+    direction = is_rtl ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD;
+
+  if (direction == GTK_DIR_TAB_BACKWARD) {
+    if (last)
+      success = hdy_tab_view_select_first_page (self);
+    else
+      success = hdy_tab_view_select_previous_page (self);
+  } else if (direction == GTK_DIR_TAB_FORWARD) {
+    if (last)
+      success = hdy_tab_view_select_last_page (self);
+    else
+      success = hdy_tab_view_select_next_page (self);
+  }
+
+  gtk_widget_grab_focus (hdy_tab_page_get_content (self->selected_page));
+
+  if (!success)
+    gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+static void
+reorder_page_cb (HdyTabView       *self,
+                 GtkDirectionType  direction,
+                 gboolean          last)
+{
+  gboolean is_rtl, success = last;
+
+  if (!self->selected_page)
+    return;
+
+  is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+  if (direction == GTK_DIR_LEFT)
+    direction = is_rtl ? GTK_DIR_TAB_FORWARD : GTK_DIR_TAB_BACKWARD;
+  else if (direction == GTK_DIR_RIGHT)
+    direction = is_rtl ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD;
+
+  if (direction == GTK_DIR_TAB_BACKWARD) {
+    if (last)
+      success = hdy_tab_view_reorder_first (self, self->selected_page);
+    else
+      success = hdy_tab_view_reorder_backward (self, self->selected_page);
+  } else if (direction == GTK_DIR_TAB_FORWARD) {
+    if (last)
+      success = hdy_tab_view_reorder_last (self, self->selected_page);
+    else
+      success = hdy_tab_view_reorder_forward (self, self->selected_page);
+  }
+
+  if (!success)
+    gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+static void
+hdy_tab_view_dispose (GObject *object)
+{
+  HdyTabView *self = HDY_TAB_VIEW (object);
+  GSList *l;
+
+  self->group = g_slist_remove (self->group, self);
+
+  for (l = self->group; l; l = l->next) {
+    HdyTabView *view = l->data;
+
+    view->group = self->group;
+  }
+
+  self->group = NULL;
+
+  if (self->pages) {
+    while (self->n_pages) {
+      HdyTabPage *page = hdy_tab_view_get_nth_page (self, 0);
+
+      close_page (self, page);
+
+      // FIXME why is this needed
+      g_object_unref (page);
+    }
+
+    g_clear_object (&self->pages);
+  }
+
+  G_OBJECT_CLASS (hdy_tab_view_parent_class)->dispose (object);
+}
+
+static void
+hdy_tab_view_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  HdyTabView *self = HDY_TAB_VIEW (object);
+
+  switch (prop_id) {
+  case PROP_N_PAGES:
+    g_value_set_uint (value, hdy_tab_view_get_n_pages (self));
+    break;
+
+  case PROP_N_PINNED_PAGES:
+    g_value_set_uint (value, hdy_tab_view_get_n_pinned_pages (self));
+    break;
+
+  case PROP_IS_DRAGGING:
+    g_value_set_boolean (value, hdy_tab_view_get_is_dragging (self));
+    break;
+
+  case PROP_SELECTED_PAGE:
+    g_value_set_object (value, hdy_tab_view_get_selected_page (self));
+    break;
+
+  case PROP_DEFAULT_ICON:
+    g_value_set_object (value, hdy_tab_view_get_default_icon (self));
+    break;
+
+  case PROP_MENU_MODEL:
+    g_value_set_object (value, hdy_tab_view_get_menu_model (self));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+hdy_tab_view_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  HdyTabView *self = HDY_TAB_VIEW (object);
+
+  switch (prop_id) {
+  case PROP_SELECTED_PAGE:
+    hdy_tab_view_set_selected_page (self, g_value_get_object (value));
+    break;
+
+  case PROP_DEFAULT_ICON:
+    hdy_tab_view_set_default_icon (self, g_value_get_object (value));
+    break;
+
+  case PROP_MENU_MODEL:
+    hdy_tab_view_set_menu_model (self, g_value_get_object (value));
+    break;
+
+  case PROP_GROUP:
+    set_group_from_view (self, g_value_get_object (value));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+hdy_tab_view_class_init (HdyTabViewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkBindingSet *binding_set;
+
+  object_class->dispose = hdy_tab_view_dispose;
+  object_class->get_property = hdy_tab_view_get_property;
+  object_class->set_property = hdy_tab_view_set_property;
+
+  props[PROP_N_PAGES] =
+    g_param_spec_uint ("n-pages",
+                       _("Number of Pages"),
+                       _("Number of Pages"),
+                       0, G_MAXUINT, 0,
+                       G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_N_PINNED_PAGES] =
+    g_param_spec_uint ("n-pinned-pages",
+                       _("Number of Pinned Pages"),
+                       _("Number of Pinned Pages"),
+                       0, G_MAXUINT, 0,
+                       G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_IS_DRAGGING] =
+    g_param_spec_boolean ("is-dragging",
+                          _("Is Dragging"),
+                          _("Is Dragging"),
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_SELECTED_PAGE] =
+    g_param_spec_object ("selected-page",
+                         _("Selected Page"),
+                         _("Selected Page"),
+                         HDY_TYPE_TAB_PAGE,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_DEFAULT_ICON] =
+    g_param_spec_object ("default-icon",
+                         _("Default Icon"),
+                         _("Default Icon"),
+                         G_TYPE_ICON,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_MENU_MODEL] =
+    g_param_spec_object ("menu-model",
+                         _("Menu Model"),
+                         _("Menu Model"),
+                         G_TYPE_MENU_MODEL,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_GROUP] =
+    g_param_spec_object ("group",
+                         _("Group"),
+                         _("Group"),
+                         HDY_TYPE_TAB_VIEW,
+                         G_PARAM_WRITABLE);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  signals[SIGNAL_PAGE_ADDED] =
+    g_signal_new ("page-added",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2,
+                  HDY_TYPE_TAB_PAGE, G_TYPE_UINT);
+
+  signals[SIGNAL_PAGE_REMOVED] =
+    g_signal_new ("page-removed",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  1,
+                  HDY_TYPE_TAB_PAGE);
+
+  signals[SIGNAL_PAGE_REORDERED] =
+    g_signal_new ("page-reordered",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2,
+                  HDY_TYPE_TAB_PAGE, G_TYPE_UINT);
+
+  signals[SIGNAL_PAGE_PINNED] =
+    g_signal_new ("page-pinned",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  1,
+                  HDY_TYPE_TAB_PAGE);
+
+  signals[SIGNAL_PAGE_UNPINNED] =
+    g_signal_new ("page-unpinned",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  1,
+                  HDY_TYPE_TAB_PAGE);
+
+  signals[SIGNAL_SETUP_MENU] =
+    g_signal_new ("setup-menu",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  1,
+                  HDY_TYPE_TAB_PAGE);
+
+  signals[SIGNAL_CREATE_WINDOW] =
+    g_signal_new ("create-window",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  object_handled_accumulator,
+                  NULL, NULL,
+                  HDY_TYPE_TAB_VIEW,
+                  0);
+
+  signals[SIGNAL_SELECT_PAGE] =
+    g_signal_new ("select-page",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 2,
+                  GTK_TYPE_DIRECTION_TYPE, G_TYPE_BOOLEAN);
+
+  signals[SIGNAL_REORDER_PAGE] =
+    g_signal_new ("reorder-page",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE, 2,
+                  GTK_TYPE_DIRECTION_TYPE, G_TYPE_BOOLEAN);
+
+  binding_set = gtk_binding_set_by_class (klass);
+
+  add_select_bindings (binding_set, GDK_KEY_Page_Up,   GTK_DIR_TAB_BACKWARD, FALSE);
+  add_select_bindings (binding_set, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD,  FALSE);
+  add_select_bindings (binding_set, GDK_KEY_Home,      GTK_DIR_TAB_BACKWARD, TRUE);
+  add_select_bindings (binding_set, GDK_KEY_End,       GTK_DIR_TAB_FORWARD,  TRUE);
+
+  add_reorder_bindings (binding_set, GDK_KEY_Page_Up,   GTK_DIR_TAB_BACKWARD, FALSE);
+  add_reorder_bindings (binding_set, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD,  FALSE);
+  add_reorder_bindings (binding_set, GDK_KEY_Home,      GTK_DIR_TAB_BACKWARD, TRUE);
+  add_reorder_bindings (binding_set, GDK_KEY_End,       GTK_DIR_TAB_FORWARD,  TRUE);
+
+  gtk_widget_class_set_css_name (widget_class, "tabview");
+}
+
+static void
+hdy_tab_view_init (HdyTabView *self)
+{
+  GtkWidget *overlay, *drag_shield;
+
+  self->pages = g_list_store_new (HDY_TYPE_TAB_PAGE);
+  self->group = g_slist_prepend (NULL, self);
+  self->default_icon = G_ICON (g_themed_icon_new ("hdy-tab-icon-missing-symbolic"));
+
+  overlay = gtk_overlay_new ();
+  gtk_widget_show (overlay);
+  gtk_container_add (GTK_CONTAINER (self), overlay);
+
+  self->stack = GTK_STACK (gtk_stack_new ());
+  gtk_widget_show (GTK_WIDGET (self->stack));
+  gtk_container_add (GTK_CONTAINER (overlay), GTK_WIDGET (self->stack));
+
+  drag_shield = gtk_event_box_new ();
+  gtk_widget_add_events (drag_shield, GDK_ALL_EVENTS_MASK);
+  gtk_overlay_add_overlay (GTK_OVERLAY (overlay), drag_shield);
+
+  g_object_bind_property (self, "is-dragging",
+                          drag_shield, "visible",
+                          G_BINDING_DEFAULT);
+
+  gtk_drag_dest_set (GTK_WIDGET (self),
+                     GTK_DEST_DEFAULT_MOTION,
+                     dst_targets,
+                     G_N_ELEMENTS (dst_targets),
+                     GDK_ACTION_MOVE);
+
+  g_signal_connect_object (self, "select-page", G_CALLBACK (select_page_cb), self, 0);
+  g_signal_connect_object (self, "reorder-page", G_CALLBACK (reorder_page_cb), self, 0);
+}
+
+GtkWidget *
+hdy_tab_page_get_content (HdyTabPage *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (self), NULL);
+
+  return self->content;
+}
+
+gboolean
+hdy_tab_page_get_selected (HdyTabPage *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (self), FALSE);
+
+  return self->selected;
+}
+
+gboolean
+hdy_tab_page_get_pinned (HdyTabPage *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (self), FALSE);
+
+  return self->pinned;
+}
+
+const gchar *
+hdy_tab_page_get_title (HdyTabPage *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (self), NULL);
+
+  return self->title;
+}
+
+void
+hdy_tab_page_set_title (HdyTabPage  *self,
+                        const gchar *title)
+{
+  g_return_if_fail (HDY_IS_TAB_PAGE (self));
+
+  if (!g_strcmp0 (title, self->title))
+    return;
+
+  if (self->title)
+    g_clear_pointer (&self->title, g_free);
+
+  if (title)
+    self->title = g_strdup (title);
+
+  g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_TITLE]);
+}
+
+const gchar *
+hdy_tab_page_get_tooltip (HdyTabPage *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (self), NULL);
+
+  return self->tooltip;
+}
+
+void
+hdy_tab_page_set_tooltip (HdyTabPage  *self,
+                          const gchar *tooltip)
+{
+  g_return_if_fail (HDY_IS_TAB_PAGE (self));
+
+  if (!g_strcmp0 (tooltip, self->tooltip))
+    return;
+
+  if (self->tooltip)
+    g_clear_pointer (&self->tooltip, g_free);
+
+  if (tooltip)
+    self->tooltip = g_strdup (tooltip);
+
+  g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_TOOLTIP]);
+}
+
+GIcon *
+hdy_tab_page_get_icon (HdyTabPage *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (self), NULL);
+
+  return self->icon;
+}
+
+void
+hdy_tab_page_set_icon (HdyTabPage *self,
+                       GIcon      *icon)
+{
+  g_return_if_fail (HDY_IS_TAB_PAGE (self));
+  g_return_if_fail (G_IS_ICON (icon) || icon == NULL);
+
+  if (self->icon == icon)
+    return;
+
+  g_set_object (&self->icon, icon);
+
+  g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_ICON]);
+}
+
+gboolean
+hdy_tab_page_get_loading (HdyTabPage *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (self), FALSE);
+
+  return self->loading;
+}
+
+void
+hdy_tab_page_set_loading (HdyTabPage *self,
+                          gboolean    loading)
+{
+  g_return_if_fail (HDY_IS_TAB_PAGE (self));
+
+  loading = !!loading;
+
+  if (self->loading == loading)
+    return;
+
+  self->loading = loading;
+
+  g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_LOADING]);
+}
+
+GIcon *
+hdy_tab_page_get_secondary_icon (HdyTabPage *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (self), NULL);
+
+  return self->secondary_icon;
+}
+
+void
+hdy_tab_page_set_secondary_icon (HdyTabPage *self,
+                                 GIcon      *secondary_icon)
+{
+  g_return_if_fail (HDY_IS_TAB_PAGE (self));
+  g_return_if_fail (G_IS_ICON (secondary_icon) || secondary_icon == NULL);
+
+  if (self->secondary_icon == secondary_icon)
+    return;
+
+  g_set_object (&self->secondary_icon, secondary_icon);
+
+  g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_SECONDARY_ICON]);
+}
+
+gboolean
+hdy_tab_page_get_needs_attention (HdyTabPage *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (self), FALSE);
+
+  return self->needs_attention;
+}
+
+void
+hdy_tab_page_set_needs_attention (HdyTabPage *self,
+                                  gboolean    needs_attention)
+{
+  g_return_if_fail (HDY_IS_TAB_PAGE (self));
+
+  needs_attention = !!needs_attention;
+
+  if (self->needs_attention == needs_attention)
+    return;
+
+  self->needs_attention = needs_attention;
+
+  g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_NEEDS_ATTENTION]);
+}
+
+HdyTabView *
+hdy_tab_view_new (void)
+{
+  return g_object_new (HDY_TYPE_TAB_VIEW, NULL);
+}
+
+guint
+hdy_tab_view_get_n_pages (HdyTabView *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), 0);
+
+  return self->n_pages;
+}
+
+guint
+hdy_tab_view_get_n_pinned_pages (HdyTabView *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), 0);
+
+  return self->n_pinned_pages;
+}
+
+gboolean
+hdy_tab_view_get_is_dragging (HdyTabView *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+
+  return self->is_dragging;
+}
+
+void
+hdy_tab_view_start_drag (HdyTabView *self)
+{
+  GSList *l;
+
+  g_return_if_fail (HDY_IS_TAB_VIEW (self));
+
+  for (l = self->group; l; l = l->next) {
+    HdyTabView *view = l->data;
+
+    set_is_dragging (view, TRUE);
+  }
+}
+
+void
+hdy_tab_view_end_drag (HdyTabView *self)
+{
+  GSList *l;
+
+  g_return_if_fail (HDY_IS_TAB_VIEW (self));
+
+  for (l = self->group; l; l = l->next) {
+    HdyTabView *view = l->data;
+
+    set_is_dragging (view, FALSE);
+  }
+}
+
+HdyTabPage *
+hdy_tab_view_get_selected_page (HdyTabView *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+
+  return self->selected_page;
+}
+
+void
+hdy_tab_view_set_selected_page (HdyTabView *self,
+                                HdyTabPage *selected_page)
+{
+  g_return_if_fail (HDY_IS_TAB_VIEW (self));
+  g_return_if_fail (HDY_IS_TAB_PAGE (selected_page));
+
+  if (self->selected_page == selected_page)
+    return;
+
+  if (self->selected_page)
+    set_page_selected (self->selected_page, FALSE);
+
+  self->selected_page = selected_page;
+  gtk_stack_set_visible_child (self->stack,
+                               hdy_tab_page_get_content (selected_page));
+
+  if (self->selected_page)
+    set_page_selected (self->selected_page, TRUE);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_PAGE]);
+}
+
+gboolean
+hdy_tab_view_select_previous_page (HdyTabView *self)
+{
+  HdyTabPage *page;
+  guint pos;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+
+  if (!self->selected_page)
+    return FALSE;
+
+  pos = hdy_tab_view_get_page_position (self, self->selected_page);
+
+  if (pos <= 0)
+    return FALSE;
+
+  page = hdy_tab_view_get_nth_page (self, pos - 1);
+
+  hdy_tab_view_set_selected_page (self, page);
+
+  return TRUE;
+}
+
+gboolean
+hdy_tab_view_select_next_page (HdyTabView *self)
+{
+  HdyTabPage *page;
+  guint pos;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+
+  if (!self->selected_page)
+    return FALSE;
+
+  pos = hdy_tab_view_get_page_position (self, self->selected_page);
+
+  if (pos + 1 >= self->n_pages)
+    return FALSE;
+
+  page = hdy_tab_view_get_nth_page (self, pos + 1);
+
+  hdy_tab_view_set_selected_page (self, page);
+
+  return TRUE;
+}
+
+gboolean
+hdy_tab_view_select_first_page (HdyTabView *self)
+{
+  HdyTabPage *page;
+  guint pos;
+  gboolean pinned;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+
+  if (!self->selected_page)
+    return FALSE;
+
+  pinned = hdy_tab_page_get_pinned (self->selected_page);
+  pos = pinned ? 0 : self->n_pinned_pages;
+
+  page = hdy_tab_view_get_nth_page (self, pos);
+
+  /* If we're on the first non-pinned tab, go to the first pinned tab */
+  if (page == self->selected_page && !pinned)
+    page = hdy_tab_view_get_nth_page (self, 0);
+
+  if (page == self->selected_page)
+    return FALSE;
+
+  hdy_tab_view_set_selected_page (self, page);
+
+  return TRUE;
+}
+
+gboolean
+hdy_tab_view_select_last_page (HdyTabView *self)
+{
+  HdyTabPage *page;
+  guint pos;
+  gboolean pinned;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+
+  if (!self->selected_page)
+    return FALSE;
+
+  pinned = hdy_tab_page_get_pinned (self->selected_page);
+  pos = (pinned ? self->n_pinned_pages : self->n_pages) - 1;
+
+  page = hdy_tab_view_get_nth_page (self, pos);
+
+  /* If we're on the last pinned tab, go to the last non-pinned tab */
+  if (page == self->selected_page && pinned)
+    page = hdy_tab_view_get_nth_page (self, self->n_pages - 1);
+
+  if (page == self->selected_page)
+    return FALSE;
+
+  hdy_tab_view_set_selected_page (self, page);
+
+  return TRUE;
+}
+
+GIcon *
+hdy_tab_view_get_default_icon (HdyTabView *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+
+  return self->default_icon;
+}
+
+void
+hdy_tab_view_set_default_icon (HdyTabView *self,
+                               GIcon      *default_icon)
+{
+  g_return_if_fail (HDY_IS_TAB_VIEW (self));
+  g_return_if_fail (G_IS_ICON (default_icon));
+
+  if (self->default_icon == default_icon)
+    return;
+
+  g_set_object (&self->default_icon, default_icon);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEFAULT_ICON]);
+}
+
+GMenuModel *
+hdy_tab_view_get_menu_model (HdyTabView *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+
+  return self->menu_model;
+}
+
+void
+hdy_tab_view_set_menu_model (HdyTabView *self,
+                             GMenuModel *menu_model)
+{
+  g_return_if_fail (HDY_IS_TAB_VIEW (self));
+  g_return_if_fail (G_IS_MENU_MODEL (menu_model));
+
+  if (self->menu_model == menu_model)
+    return;
+
+  g_set_object (&self->menu_model, menu_model);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MENU_MODEL]);
+}
+
+GSList *
+hdy_tab_view_get_group (HdyTabView *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+
+  return self->group;
+}
+
+void
+hdy_tab_view_set_group (HdyTabView *self,
+                        GSList     *group)
+{
+  g_return_if_fail (HDY_IS_TAB_VIEW (self));
+
+  if (g_slist_find (group, self))
+    return;
+
+  if (self->group) {
+    GSList *l;
+
+    self->group = g_slist_remove (self->group, self);
+
+    for (l = self->group; l; l = l->next) {
+      HdyTabView *view = l->data;
+
+      view->group = self->group;
+    }
+  }
+
+  self->group = g_slist_prepend (group, self);
+
+  if (group) {
+    GSList *l;
+
+    for (l = group; l; l = l->next) {
+      HdyTabView *view = l->data;
+
+      view->group = self->group;
+    }
+  }
+}
+
+void
+hdy_tab_view_join_group (HdyTabView *self,
+                         HdyTabView *source)
+{
+  g_return_if_fail (HDY_IS_TAB_VIEW (self));
+  g_return_if_fail (source == NULL || HDY_IS_TAB_VIEW (source));
+
+  if (source) {
+    GSList *group = hdy_tab_view_get_group (source);
+
+    if (!group) {
+        hdy_tab_view_set_group (source, NULL);
+        group = hdy_tab_view_get_group (source);
+      }
+
+    hdy_tab_view_set_group (self, group);
+  } else
+    hdy_tab_view_set_group (self, NULL);
+}
+
+void
+hdy_tab_view_set_page_pinned (HdyTabView *self,
+                              HdyTabPage *page,
+                              gboolean    pinned)
+{
+  guint pos;
+
+  g_return_if_fail (HDY_IS_TAB_VIEW (self));
+  g_return_if_fail (HDY_IS_TAB_PAGE (page));
+
+  pinned = !!pinned;
+
+  if (hdy_tab_page_get_pinned (page) == pinned)
+    return;
+
+  pos = hdy_tab_view_get_page_position (self, page);
+
+  g_list_store_remove (self->pages, pos);
+
+  pos = self->n_pinned_pages;
+
+  if (!pinned)
+      pos--;
+
+  g_list_store_insert (self->pages, pos, page);
+
+  set_page_pinned (page, pinned);
+
+  if (pinned)
+    pos++;
+
+  gtk_container_child_set (GTK_CONTAINER (self->stack),
+                           hdy_tab_page_get_content (page),
+                           "position", self->n_pinned_pages,
+                           NULL);
+
+  set_n_pinned_pages (self, pos);
+
+  if (pinned)
+    g_signal_emit (self, signals[SIGNAL_PAGE_PINNED], 0, page);
+  else
+    g_signal_emit (self, signals[SIGNAL_PAGE_UNPINNED], 0, page);
+}
+
+HdyTabPage *
+hdy_tab_view_get_page (HdyTabView *self,
+                       GtkWidget  *content)
+{
+  guint i;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+  g_return_val_if_fail (GTK_IS_WIDGET (content), NULL);
+
+  for (i = 0; i < self->n_pages; i++) {
+    HdyTabPage *page = g_list_model_get_item (G_LIST_MODEL (self->pages), i);
+
+    if (hdy_tab_page_get_content (page) == content)
+      return page;
+  }
+
+  return NULL;
+}
+
+HdyTabPage *
+hdy_tab_view_get_nth_page (HdyTabView *self,
+                           guint       position)
+{
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+  g_return_val_if_fail (position <= self->n_pages, NULL);
+
+  return g_list_model_get_item (G_LIST_MODEL (self->pages), position);
+}
+
+guint
+hdy_tab_view_get_page_position (HdyTabView *self,
+                                HdyTabPage *page)
+{
+  guint pos;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), 0);
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (page), 0);
+
+  g_return_val_if_fail (g_list_store_find (self->pages, page, &pos), 0);
+
+  return pos;
+}
+
+HdyTabPage *
+hdy_tab_view_insert (HdyTabView *self,
+                     GtkWidget  *content,
+                     guint       position)
+{
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+  g_return_val_if_fail (GTK_IS_WIDGET (content), NULL);
+  g_return_val_if_fail (position <= self->n_pages, NULL);
+
+  return insert_page (self, content, position, FALSE);
+}
+
+HdyTabPage *
+hdy_tab_view_prepend (HdyTabView *self,
+                      GtkWidget  *content)
+{
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+  g_return_val_if_fail (GTK_IS_WIDGET (content), NULL);
+
+  return insert_page (self, content, 0, FALSE);
+}
+
+HdyTabPage *
+hdy_tab_view_append (HdyTabView *self,
+                     GtkWidget  *content)
+{
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+  g_return_val_if_fail (GTK_IS_WIDGET (content), NULL);
+
+  return insert_page (self, content, self->n_pages - self->n_pinned_pages, FALSE);
+}
+
+HdyTabPage *
+hdy_tab_view_insert_pinned (HdyTabView *self,
+                            GtkWidget  *content,
+                            guint       position)
+{
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+  g_return_val_if_fail (GTK_IS_WIDGET (content), NULL);
+  g_return_val_if_fail (position <= self->n_pages, NULL);
+
+  return insert_page (self, content, position, TRUE);
+}
+
+HdyTabPage *
+hdy_tab_view_prepend_pinned (HdyTabView *self,
+                             GtkWidget  *content)
+{
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+  g_return_val_if_fail (GTK_IS_WIDGET (content), NULL);
+
+  return insert_page (self, content, 0, TRUE);
+}
+
+HdyTabPage *
+hdy_tab_view_append_pinned (HdyTabView *self,
+                            GtkWidget  *content)
+{
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+  g_return_val_if_fail (GTK_IS_WIDGET (content), NULL);
+
+  return insert_page (self, content, self->n_pinned_pages, TRUE);
+}
+
+gboolean
+hdy_tab_view_close_page (HdyTabView *self,
+                         HdyTabPage *page)
+{
+  gboolean can_close;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+//  g_signal_emit_by_name (page, "closing", &can_close);
+  can_close = TRUE;
+
+  if (can_close)
+    close_page (self, page);
+
+  return can_close;
+}
+
+gboolean
+hdy_tab_view_close_pages (HdyTabView *self,
+                          GSList     *pages)
+{
+  gboolean can_close = TRUE;
+  GSList *l;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+
+  if (!pages)
+    return TRUE;
+
+  for (l = pages; l; l = l->next) {
+    HdyTabPage *page = l->data;
+    gboolean can_close_tab;
+
+//    g_signal_emit_by_name (page, "closing", &can_close_tab);
+    can_close_tab = TRUE;
+
+    can_close &= can_close_tab;
+  }
+
+  if (can_close)
+    for (l = pages; l; l = l->next) {
+      HdyTabPage *page = l->data;
+
+      close_page (self, page);
+    }
+
+  g_free (pages);
+
+  return can_close;
+}
+
+gboolean
+hdy_tab_view_close_other_pages (HdyTabView *self,
+                                HdyTabPage *page)
+{
+  GSList *pages = NULL;
+  guint i;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+  for (i = self->n_pinned_pages; i < self->n_pages; i++) {
+    HdyTabPage *p = hdy_tab_view_get_nth_page (self, i);
+
+    if (p == page)
+      continue;
+
+    pages = g_slist_prepend (pages, p);
+  }
+
+  pages = g_slist_reverse (pages);
+
+  return hdy_tab_view_close_pages (self, pages);
+}
+
+gboolean
+hdy_tab_view_close_pages_before (HdyTabView *self,
+                                 HdyTabPage *page)
+{
+  GSList *pages = NULL;
+  guint pos, i;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+  pos = hdy_tab_view_get_page_position (self, page);
+
+  for (i = self->n_pinned_pages; i < pos; i++) {
+    HdyTabPage *p = hdy_tab_view_get_nth_page (self, i);
+
+    pages = g_slist_prepend (pages, p);
+  }
+
+  pages = g_slist_reverse (pages);
+
+  return hdy_tab_view_close_pages (self, pages);
+}
+
+gboolean
+hdy_tab_view_close_pages_after (HdyTabView *self,
+                                HdyTabPage *page)
+{
+  GSList *pages = NULL;
+  guint pos, i;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+  pos = hdy_tab_view_get_page_position (self, page);
+
+  /* Skip pinned tabs */
+  pos = MAX (self->n_pinned_pages, pos);
+
+  for (i = self->n_pages - 1; i > pos; i--) {
+    HdyTabPage *p = hdy_tab_view_get_nth_page (self, i);
+
+    pages = g_slist_prepend (pages, p);
+  }
+
+  return hdy_tab_view_close_pages (self, pages);
+}
+
+gboolean
+hdy_tab_view_reorder_page (HdyTabView *self,
+                           HdyTabPage *page,
+                           guint       position)
+{
+  guint original_pos, pinned;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+  pinned = hdy_tab_page_get_pinned (page);
+
+  g_return_val_if_fail (!pinned || position < self->n_pinned_pages, FALSE);
+  g_return_val_if_fail (pinned || position >= self->n_pinned_pages, FALSE);
+  g_return_val_if_fail (pinned || position < self->n_pages, FALSE);
+
+  original_pos = hdy_tab_view_get_page_position (self, page);
+
+  if (original_pos == position)
+    return FALSE;
+
+  g_list_store_remove (self->pages, original_pos);
+  g_list_store_insert (self->pages, position, page);
+
+  gtk_container_child_set (GTK_CONTAINER (self->stack),
+                           hdy_tab_page_get_content (page),
+                           "position", position,
+                           NULL);
+
+  g_signal_emit (self, signals[SIGNAL_PAGE_REORDERED], 0, page, position);
+
+  return TRUE;
+}
+
+gboolean
+hdy_tab_view_reorder_backward (HdyTabView *self,
+                               HdyTabPage *page)
+{
+  gboolean pinned;
+  guint pos, first;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+  pos = hdy_tab_view_get_page_position (self, page);
+
+  pinned = hdy_tab_page_get_pinned (page);
+  first = pinned ? 0 : self->n_pinned_pages;
+
+  if (pos <= first)
+    return FALSE;
+
+  return hdy_tab_view_reorder_page (self, page, pos - 1);
+}
+
+gboolean
+hdy_tab_view_reorder_forward (HdyTabView *self,
+                              HdyTabPage *page)
+{
+  gboolean pinned;
+  guint pos, last;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+  pos = hdy_tab_view_get_page_position (self, page);
+
+  pinned = hdy_tab_page_get_pinned (page);
+  last = (pinned ? self->n_pinned_pages : self->n_pages) - 1;
+
+  if (pos >= last)
+    return FALSE;
+
+  return hdy_tab_view_reorder_page (self, page, pos + 1);
+}
+
+gboolean
+hdy_tab_view_reorder_first (HdyTabView *self,
+                            HdyTabPage *page)
+{
+  gboolean pinned;
+  guint pos;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+  pinned = hdy_tab_page_get_pinned (page);
+  pos = pinned ? 0 : self->n_pinned_pages;
+
+  return hdy_tab_view_reorder_page (self, page, pos);
+}
+
+gboolean
+hdy_tab_view_reorder_last (HdyTabView *self,
+                           HdyTabPage *page)
+{
+  gboolean pinned;
+  guint pos;
+
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+  pinned = hdy_tab_page_get_pinned (page);
+  pos = (pinned ? self->n_pinned_pages : self->n_pages) - 1;
+
+  return hdy_tab_view_reorder_page (self, page, pos);
+}
+
+void
+hdy_tab_view_detach_page (HdyTabView *self,
+                          HdyTabPage *page)
+{
+  g_return_if_fail (HDY_IS_TAB_VIEW (self));
+  g_return_if_fail (HDY_IS_TAB_PAGE (page));
+
+  detach_page (self, page);
+}
+
+void
+hdy_tab_view_attach_page (HdyTabView *self,
+                          HdyTabPage *page,
+                          guint       position)
+{
+  g_return_if_fail (HDY_IS_TAB_VIEW (self));
+  g_return_if_fail (HDY_IS_TAB_PAGE (page));
+  g_return_if_fail (position <= self->n_pages);
+
+  attach_page (self, page, position);
+
+  hdy_tab_view_set_selected_page (self, page);
+}
+
+void
+hdy_tab_view_transfer_page (HdyTabView *self,
+                            HdyTabPage *page,
+                            HdyTabView *other_view,
+                            guint       position)
+{
+  g_return_if_fail (HDY_IS_TAB_VIEW (self));
+  g_return_if_fail (HDY_IS_TAB_PAGE (page));
+  g_return_if_fail (HDY_IS_TAB_VIEW (other_view));
+  g_return_if_fail (position <= other_view->n_pages);
+
+  hdy_tab_view_detach_page (self, page);
+  hdy_tab_view_attach_page (other_view, page, position);
+}
+
+GListModel *
+hdy_tab_view_get_pages (HdyTabView *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+
+  return G_LIST_MODEL (self->pages);
+}
+
diff --git a/src/hdy-tab-view.h b/src/hdy-tab-view.h
new file mode 100644
index 00000000..bf14fb3d
--- /dev/null
+++ b/src/hdy-tab-view.h
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_TAB_PAGE (hdy_tab_page_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyTabPage, hdy_tab_page, HDY, TAB_PAGE, GObject)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_tab_page_get_content (HdyTabPage *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_page_get_selected (HdyTabPage *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_page_get_pinned (HdyTabPage *self);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_tab_page_get_title (HdyTabPage  *self);
+HDY_AVAILABLE_IN_ALL
+void         hdy_tab_page_set_title (HdyTabPage  *self,
+                                     const gchar *title);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_tab_page_get_tooltip (HdyTabPage  *self);
+HDY_AVAILABLE_IN_ALL
+void         hdy_tab_page_set_tooltip (HdyTabPage  *self,
+                                       const gchar *title);
+
+HDY_AVAILABLE_IN_ALL
+GIcon *hdy_tab_page_get_icon (HdyTabPage *self);
+HDY_AVAILABLE_IN_ALL
+void   hdy_tab_page_set_icon (HdyTabPage *self,
+                              GIcon      *icon);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_page_get_loading (HdyTabPage *self);
+HDY_AVAILABLE_IN_ALL
+void     hdy_tab_page_set_loading (HdyTabPage *self,
+                                   gboolean    loading);
+
+HDY_AVAILABLE_IN_ALL
+GIcon *hdy_tab_page_get_secondary_icon (HdyTabPage *self);
+HDY_AVAILABLE_IN_ALL
+void   hdy_tab_page_set_secondary_icon (HdyTabPage *self,
+                                        GIcon      *secondary_icon);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_page_get_needs_attention (HdyTabPage *self);
+HDY_AVAILABLE_IN_ALL
+void     hdy_tab_page_set_needs_attention (HdyTabPage *self,
+                                           gboolean    needs_attention);
+
+#define HDY_TYPE_TAB_VIEW (hdy_tab_view_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyTabView, hdy_tab_view, HDY, TAB_VIEW, GtkBin)
+
+HDY_AVAILABLE_IN_ALL
+HdyTabView *hdy_tab_view_new (void);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_tab_view_get_n_pages (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+guint hdy_tab_view_get_n_pinned_pages (HdyTabView *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_get_is_dragging (HdyTabView *self);
+
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_get_selected_page (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+void        hdy_tab_view_set_selected_page (HdyTabView *self,
+                                            HdyTabPage *selected_page);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_select_previous_page (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_select_next_page     (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_select_first_page    (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_select_last_page     (HdyTabView *self);
+
+HDY_AVAILABLE_IN_ALL
+GIcon *hdy_tab_view_get_default_icon (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+void   hdy_tab_view_set_default_icon (HdyTabView *self,
+                                      GIcon      *default_icon);
+
+HDY_AVAILABLE_IN_ALL
+GMenuModel *hdy_tab_view_get_menu_model (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+void        hdy_tab_view_set_menu_model (HdyTabView *self,
+                                         GMenuModel *menu_model);
+
+HDY_AVAILABLE_IN_ALL
+GSList *hdy_tab_view_get_group  (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+void    hdy_tab_view_set_group  (HdyTabView *self,
+                                 GSList     *group);
+HDY_AVAILABLE_IN_ALL
+void    hdy_tab_view_join_group (HdyTabView *self,
+                                 HdyTabView *source);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_view_set_page_pinned (HdyTabView *self,
+                                   HdyTabPage *page,
+                                   gboolean    pinned);
+
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_get_page (HdyTabView *self,
+                                   GtkWidget  *content);
+
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_get_nth_page (HdyTabView *self,
+                                       guint       position);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_tab_view_get_page_position (HdyTabView *self,
+                                      HdyTabPage *page);
+
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_insert  (HdyTabView *self,
+                                  GtkWidget  *content,
+                                  guint       position);
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_prepend (HdyTabView *self,
+                                  GtkWidget  *content);
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_append  (HdyTabView *self,
+                                  GtkWidget  *content);
+
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_insert_pinned  (HdyTabView *self,
+                                         GtkWidget  *content,
+                                         guint       position);
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_prepend_pinned (HdyTabView *self,
+                                         GtkWidget  *content);
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_append_pinned  (HdyTabView *self,
+                                         GtkWidget  *content);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_close_page         (HdyTabView *self,
+                                          HdyTabPage *page);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_close_pages        (HdyTabView *self,
+                                          GSList     *pages);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_close_other_pages  (HdyTabView *self,
+                                          HdyTabPage *page);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_close_pages_before (HdyTabView *self,
+                                          HdyTabPage *page);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_close_pages_after  (HdyTabView *self,
+                                          HdyTabPage *page);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_reorder_page     (HdyTabView *self,
+                                        HdyTabPage *page,
+                                        guint       position);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_reorder_backward (HdyTabView *self,
+                                        HdyTabPage *page);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_reorder_forward  (HdyTabView *self,
+                                        HdyTabPage *page);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_reorder_first    (HdyTabView *self,
+                                        HdyTabPage *page);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_reorder_last     (HdyTabView *self,
+                                        HdyTabPage *page);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_view_transfer_page (HdyTabView *self,
+                                 HdyTabPage *page,
+                                 HdyTabView *other_view,
+                                 guint       position);
+
+HDY_AVAILABLE_IN_ALL
+GListModel *hdy_tab_view_get_pages (HdyTabView *self);
+
+G_END_DECLS
diff --git a/src/hdy-tab.c b/src/hdy-tab.c
new file mode 100644
index 00000000..9f4fa2ec
--- /dev/null
+++ b/src/hdy-tab.c
@@ -0,0 +1,774 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-tab-private.h"
+#include "hdy-css-private.h"
+
+struct _HdyTab
+{
+  GtkContainer parent_instance;
+
+  GtkLabel *title;
+  GtkImage *icon;
+  GtkImage *pinned_icon;
+  GtkStack *stack;
+  GtkStack *icon_stack;
+  GtkStack *pinned_icon_stack;
+  GtkImage *secondary_icon;
+
+  GtkWidget *child;
+  GdkWindow *window;
+  gboolean draw_child;
+
+  HdyTabView *view;
+  HdyTabPage *page;
+  gboolean pinned;
+  gboolean dragging;
+  gint display_width;
+
+  gboolean hovering;
+  gboolean selected;
+
+  GBinding *selected_binding;
+  GBinding *title_binding;
+};
+
+G_DEFINE_TYPE (HdyTab, hdy_tab, GTK_TYPE_CONTAINER)
+
+enum {
+  PROP_0,
+  PROP_VIEW,
+  PROP_PINNED,
+  PROP_DRAGGING,
+  PROP_PAGE,
+  PROP_DISPLAY_WIDTH,
+  PROP_HOVERING,
+  PROP_SELECTED,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void
+update_state (HdyTab *self)
+{
+  GtkStateFlags new_state;
+
+  new_state = gtk_widget_get_state_flags (GTK_WIDGET (self)) &
+    ~(GTK_STATE_FLAG_PRELIGHT | GTK_STATE_FLAG_CHECKED);
+
+  if (self->hovering)
+    new_state |= GTK_STATE_FLAG_PRELIGHT;
+
+  if (self->selected)
+    new_state |= GTK_STATE_FLAG_CHECKED;
+
+  gtk_widget_set_state_flags (GTK_WIDGET (self), new_state, TRUE);
+}
+
+static void
+update_tooltip (HdyTab *self)
+{
+  const gchar *tooltip = hdy_tab_page_get_tooltip (self->page);
+
+  if (tooltip)
+    gtk_widget_set_tooltip_markup (GTK_WIDGET (self), tooltip);
+  else
+    gtk_widget_set_tooltip_text (GTK_WIDGET (self),
+                                 hdy_tab_page_get_title (self->page));
+}
+
+static void
+update_icon (HdyTab *self)
+{
+  GIcon *gicon = hdy_tab_page_get_icon (self->page);
+  gboolean loading = hdy_tab_page_get_loading (self->page);
+  const gchar *name = loading ? "spinner" : "icon";
+
+  gtk_image_set_from_gicon (self->icon, gicon, GTK_ICON_SIZE_BUTTON);
+  gtk_widget_set_visible (GTK_WIDGET (self->icon_stack),
+                          gicon != NULL || loading);
+  gtk_stack_set_visible_child_name (self->icon_stack, name);
+
+  if (gicon)
+    gtk_image_set_from_gicon (self->pinned_icon, gicon, GTK_ICON_SIZE_BUTTON);
+  else {
+    gicon = hdy_tab_view_get_default_icon (self->view);
+
+    gtk_image_set_from_gicon (self->pinned_icon, gicon, GTK_ICON_SIZE_BUTTON);
+  }
+
+  gtk_stack_set_visible_child_name (self->pinned_icon_stack, name);
+}
+
+static void
+update_secondary_icon (HdyTab *self)
+{
+  GIcon *gicon = hdy_tab_page_get_secondary_icon (self->page);
+
+  gtk_image_set_from_gicon (self->secondary_icon, gicon, GTK_ICON_SIZE_BUTTON);
+  gtk_widget_set_visible (GTK_WIDGET (self->secondary_icon), gicon != NULL);
+}
+
+static void
+update_needs_attention (HdyTab *self)
+{
+  GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+
+  if (hdy_tab_page_get_needs_attention (self->page))
+    gtk_style_context_add_class (context, "needs-attention");
+  else
+    gtk_style_context_remove_class (context, "needs-attention");
+}
+
+static void
+update_loading (HdyTab *self)
+{
+  GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  update_icon (self);
+
+  if (hdy_tab_page_get_loading (self->page))
+    gtk_style_context_add_class (context, "loading");
+  else
+    gtk_style_context_remove_class (context, "loading");
+}
+
+static gboolean
+close_idle_cb (HdyTab *self)
+{
+  hdy_tab_view_close_page (self->view, self->page);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+close_clicked_cb (HdyTab *self)
+{
+  if (!self->page)
+    return;
+
+  /* When animations are disabled, we don't want to immediately remove the
+   * whole tab mid-click. Instead, defer it until the click has happened.
+   */
+  g_idle_add ((GSourceFunc) close_idle_cb, self);
+}
+
+static void
+hdy_tab_measure (GtkWidget      *widget,
+                 GtkOrientation  orientation,
+                 gint            for_size,
+                 gint           *minimum,
+                 gint           *natural,
+                 gint           *minimum_baseline,
+                 gint           *natural_baseline)
+{
+  HdyTab *self = HDY_TAB (widget);
+
+  if (!self->child) {
+    if (minimum)
+      *minimum = 0;
+
+    if (natural)
+      *natural = 0;
+  } else if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+    if (minimum)
+      *minimum = 0;
+
+    gtk_widget_get_preferred_width (self->child, NULL, natural);
+  } else {
+    gtk_widget_get_preferred_height (self->child, minimum, natural);
+    hdy_css_measure (widget, orientation, minimum, natural);
+  }
+
+  if (minimum_baseline)
+    *minimum_baseline = -1;
+
+  if (natural_baseline)
+    *natural_baseline = -1;
+}
+
+static void
+hdy_tab_get_preferred_width (GtkWidget *widget,
+                             gint      *minimum,
+                             gint      *natural)
+{
+  hdy_tab_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+                   minimum, natural,
+                   NULL, NULL);
+}
+
+static void
+hdy_tab_get_preferred_height (GtkWidget *widget,
+                              gint      *minimum,
+                              gint      *natural)
+{
+  hdy_tab_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+                   minimum, natural,
+                   NULL, NULL);
+}
+
+static void
+hdy_tab_get_preferred_width_for_height (GtkWidget *widget,
+                                        gint       height,
+                                        gint      *minimum,
+                                        gint      *natural)
+{
+  hdy_tab_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+                   minimum, natural,
+                   NULL, NULL);
+}
+
+static void
+hdy_tab_get_preferred_height_for_width (GtkWidget *widget,
+                                        gint       width,
+                                        gint      *minimum,
+                                        gint      *natural)
+{
+  hdy_tab_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+                   minimum, natural,
+                   NULL, NULL);
+}
+
+static void
+hdy_tab_size_allocate (GtkWidget     *widget,
+                       GtkAllocation *allocation)
+{
+  HdyTab *self = HDY_TAB (widget);
+  gint width_diff = allocation->width;
+  GtkAllocation child_alloc, clip;
+
+  hdy_css_size_allocate_self (widget, allocation);
+
+  gtk_widget_set_allocation (widget, allocation);
+
+  if (self->window)
+    gdk_window_move_resize (self->window,
+                            allocation->x, allocation->y,
+                            allocation->width, allocation->height);
+
+  child_alloc = *allocation;
+  child_alloc.x = 0;
+  child_alloc.y = 0;
+
+  hdy_css_size_allocate_children (widget, &child_alloc);
+
+  width_diff = MAX (0, width_diff - child_alloc.width);
+
+  if (self->child) {
+    gint width = MAX (child_alloc.width, self->display_width - width_diff);
+    gint min;
+
+    gtk_widget_get_preferred_width (self->child, &min, NULL);
+
+    self->draw_child = width >= min && child_alloc.height > 0;
+
+    gtk_widget_set_child_visible (self->child, self->draw_child);
+
+    if (self->draw_child) {
+      child_alloc.x += (child_alloc.width - width) / 2;
+      child_alloc.width = width;
+
+      gtk_widget_size_allocate (self->child, &child_alloc);
+    }
+  } else
+    self->draw_child = FALSE;
+
+  if (self->draw_child) {
+    child_alloc.x = 0;
+    child_alloc.y = 0;
+    child_alloc.width = allocation->width;
+    child_alloc.height = allocation->height;
+
+    gtk_widget_set_clip (self->child, &child_alloc);
+  }
+
+  gtk_render_background_get_clip (gtk_widget_get_style_context (widget),
+                                  allocation->x, allocation->y,
+                                  allocation->width, allocation->height,
+                                  &clip);
+
+  gtk_widget_set_clip (widget, &clip);
+}
+
+static void
+hdy_tab_realize (GtkWidget *widget)
+{
+  HdyTab *self = HDY_TAB (widget);
+  GtkAllocation allocation;
+  GdkWindowAttr attributes;
+  GdkWindowAttributesType attributes_mask;
+
+  gtk_widget_set_realized (widget, TRUE);
+
+  gtk_widget_get_allocation (widget, &allocation);
+
+  attributes.x = allocation.x;
+  attributes.y = allocation.y;
+  attributes.width = allocation.width;
+  attributes.height = allocation.height;
+  attributes.window_type = GDK_WINDOW_CHILD;
+  attributes.wclass = GDK_INPUT_OUTPUT;
+  attributes.visual = gtk_widget_get_visual (widget);
+  attributes.event_mask = gtk_widget_get_events (widget);
+  attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
+
+  self->window = gdk_window_new (gtk_widget_get_parent_window (widget),
+                                 &attributes,
+                                 attributes_mask);
+
+  gtk_widget_set_window (widget, self->window);
+  gtk_widget_register_window (widget, self->window);
+
+  if (self->child)
+    gtk_widget_set_parent_window (self->child, self->window);
+}
+
+static void
+hdy_tab_unrealize (GtkWidget *widget)
+{
+  HdyTab *self = HDY_TAB (widget);
+
+  GTK_WIDGET_CLASS (hdy_tab_parent_class)->unrealize (widget);
+
+  self->window = NULL;
+}
+
+static gboolean
+hdy_tab_draw (GtkWidget *widget,
+              cairo_t   *cr)
+{
+  HdyTab *self = HDY_TAB (widget);
+
+  hdy_css_draw (widget, cr);
+
+  if (self->draw_child)
+    gtk_container_propagate_draw (GTK_CONTAINER (self), self->child, cr);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static void
+hdy_tab_add (GtkContainer *container,
+             GtkWidget    *widget)
+{
+  HdyTab *self = HDY_TAB (container);
+
+  if (self->child)
+    return;
+
+  self->child = widget;
+
+  if (self->child) {
+    gtk_widget_set_parent (self->child, GTK_WIDGET (self));
+
+    if (self->window)
+      gtk_widget_set_parent_window (self->child, self->window);
+  }
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+hdy_tab_remove (GtkContainer *container,
+                GtkWidget    *widget)
+{
+  HdyTab *self = HDY_TAB (container);
+
+  if (widget != self->child)
+    return;
+
+  g_clear_pointer (&self->child, gtk_widget_unparent);
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+hdy_tab_forall (GtkContainer *container,
+                gboolean      include_internals,
+                GtkCallback   callback,
+                gpointer      callback_data)
+{
+  HdyTab *self = HDY_TAB (container);
+
+  if (!include_internals)
+    return;
+
+  if (self->child)
+    callback (self->child, callback_data);
+}
+
+static void
+hdy_tab_constructed (GObject *object)
+{
+  HdyTab *self = HDY_TAB (object);
+
+  G_OBJECT_CLASS (hdy_tab_parent_class)->constructed (object);
+
+  gtk_stack_set_visible_child_name (self->stack,
+                                    self->pinned ? "pinned" : "regular");
+
+  if (self->pinned)
+    gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self)),
+                                 "pinned");
+
+  if (self->dragging)
+    g_object_set (self, "selected", TRUE, NULL);
+
+  g_signal_connect_object (self->view, "notify::default-icon",
+                           G_CALLBACK (update_icon), self,
+                           G_CONNECT_SWAPPED);
+}
+
+static void
+hdy_tab_get_property (GObject    *object,
+                      guint       prop_id,
+                      GValue     *value,
+                      GParamSpec *pspec)
+{
+  HdyTab *self = HDY_TAB (object);
+
+  switch (prop_id) {
+  case PROP_VIEW:
+    g_value_set_object (value, self->view);
+    break;
+
+  case PROP_PAGE:
+    g_value_set_object (value, self->page);
+    break;
+
+  case PROP_PINNED:
+    g_value_set_boolean (value, self->pinned);
+    break;
+
+  case PROP_DRAGGING:
+    g_value_set_boolean (value, self->dragging);
+    break;
+
+  case PROP_DISPLAY_WIDTH:
+    g_value_set_int (value, hdy_tab_get_display_width (self));
+    break;
+
+  case PROP_HOVERING:
+    g_value_set_boolean (value, hdy_tab_get_hovering (self));
+    break;
+
+  case PROP_SELECTED:
+    g_value_set_boolean (value, hdy_tab_get_selected (self));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+hdy_tab_set_property (GObject      *object,
+                      guint         prop_id,
+                      const GValue *value,
+                      GParamSpec   *pspec)
+{
+  HdyTab *self = HDY_TAB (object);
+
+  switch (prop_id) {
+  case PROP_VIEW:
+    self->view = g_value_get_object (value);
+    break;
+
+  case PROP_PAGE:
+    hdy_tab_set_page (self, g_value_get_object (value));
+    break;
+
+  case PROP_PINNED:
+    self->pinned = g_value_get_boolean (value);
+    break;
+
+  case PROP_DRAGGING:
+    self->dragging = g_value_get_boolean (value);
+    break;
+
+  case PROP_DISPLAY_WIDTH:
+    hdy_tab_set_display_width (self, g_value_get_int (value));
+    break;
+
+  case PROP_HOVERING:
+    hdy_tab_set_hovering (self, g_value_get_boolean (value));
+    break;
+
+  case PROP_SELECTED:
+    hdy_tab_set_selected (self, g_value_get_boolean (value));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+hdy_tab_dispose (GObject *object)
+{
+  HdyTab *self = HDY_TAB (object);
+
+  hdy_tab_set_page (self, NULL);
+
+  G_OBJECT_CLASS (hdy_tab_parent_class)->dispose (object);
+}
+
+static void
+hdy_tab_class_init (HdyTabClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+  GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+  object_class->dispose = hdy_tab_dispose;
+  object_class->constructed = hdy_tab_constructed;
+  object_class->get_property = hdy_tab_get_property;
+  object_class->set_property = hdy_tab_set_property;
+
+  widget_class->get_preferred_width = hdy_tab_get_preferred_width;
+  widget_class->get_preferred_height = hdy_tab_get_preferred_height;
+  widget_class->get_preferred_width_for_height = hdy_tab_get_preferred_width_for_height;
+  widget_class->get_preferred_height_for_width = hdy_tab_get_preferred_height_for_width;
+  widget_class->size_allocate = hdy_tab_size_allocate;
+  widget_class->realize = hdy_tab_realize;
+  widget_class->unrealize = hdy_tab_unrealize;
+  widget_class->draw = hdy_tab_draw;
+
+  container_class->add = hdy_tab_add;
+  container_class->remove = hdy_tab_remove;
+  container_class->forall = hdy_tab_forall;
+
+  props[PROP_VIEW] =
+    g_param_spec_object ("view",
+                         _("View"),
+                         _("View"),
+                         HDY_TYPE_TAB_VIEW,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  props[PROP_PINNED] =
+    g_param_spec_boolean ("pinned",
+                          _("Pinned"),
+                          _("Pinned"),
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  props[PROP_DRAGGING] =
+    g_param_spec_boolean ("dragging",
+                          _("Dragging"),
+                          _("Dragging"),
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  props[PROP_PAGE] =
+    g_param_spec_object ("page",
+                         _("Page"),
+                         _("Page"),
+                         HDY_TYPE_TAB_PAGE,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_DISPLAY_WIDTH] =
+    g_param_spec_int ("display-width",
+                      _("Display Width"),
+                      _("Display Width"),
+                      0, G_MAXINT, 0,
+                      G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_HOVERING] =
+    g_param_spec_boolean ("hovering",
+                          _("Hovering"),
+                          _("Hovering"),
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_SELECTED] =
+    g_param_spec_boolean ("selected",
+                          _("Selected"),
+                          _("Selected"),
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/sm/puri/handy/ui/hdy-tab.ui");
+  gtk_widget_class_bind_template_child (widget_class, HdyTab, title);
+  gtk_widget_class_bind_template_child (widget_class, HdyTab, icon);
+  gtk_widget_class_bind_template_child (widget_class, HdyTab, pinned_icon);
+  gtk_widget_class_bind_template_child (widget_class, HdyTab, stack);
+  gtk_widget_class_bind_template_child (widget_class, HdyTab, icon_stack);
+  gtk_widget_class_bind_template_child (widget_class, HdyTab, pinned_icon_stack);
+  gtk_widget_class_bind_template_child (widget_class, HdyTab, secondary_icon);
+  gtk_widget_class_bind_template_callback (widget_class, close_clicked_cb);
+
+  gtk_widget_class_set_css_name (widget_class, "tab");
+}
+
+static void
+hdy_tab_init (HdyTab *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+HdyTab *
+hdy_tab_new (HdyTabView *view,
+             gboolean    pinned,
+             gboolean    dragging)
+{
+  g_assert (HDY_IS_TAB_VIEW (view));
+
+  return g_object_new (HDY_TYPE_TAB,
+                       "view", view,
+                       "pinned", pinned,
+                       "dragging", dragging,
+                       NULL);
+}
+
+void
+hdy_tab_set_page (HdyTab     *self,
+                  HdyTabPage *page)
+{
+  if (self->page == page)
+    return;
+
+  if (self->page) {
+    g_signal_handlers_disconnect_by_func (self->page, update_tooltip, self);
+    g_signal_handlers_disconnect_by_func (self->page, update_icon, self);
+    g_signal_handlers_disconnect_by_func (self->page, update_secondary_icon, self);
+    g_signal_handlers_disconnect_by_func (self->page, update_needs_attention, self);
+    g_signal_handlers_disconnect_by_func (self->page, update_loading, self);
+    g_clear_pointer (&self->selected_binding, g_binding_unbind);
+    g_clear_pointer (&self->title_binding, g_binding_unbind);
+  }
+
+  g_set_object (&self->page, page);
+
+  if (self->page) {
+    update_state (self);
+    update_tooltip (self);
+    update_icon (self);
+    update_secondary_icon (self);
+    update_needs_attention (self);
+    update_loading (self);
+
+    g_signal_connect_object (self->page, "notify::title",
+                             G_CALLBACK (update_tooltip), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::tooltip",
+                             G_CALLBACK (update_tooltip), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::icon",
+                             G_CALLBACK (update_icon), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::secondary-icon",
+                             G_CALLBACK (update_secondary_icon), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::needs-attention",
+                             G_CALLBACK (update_needs_attention), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::loading",
+                             G_CALLBACK (update_loading), self,
+                           G_CONNECT_SWAPPED);
+
+    if (!self->dragging)
+      self->selected_binding = g_object_bind_property (self->page, "selected",
+                                                       self, "selected",
+                                                       G_BINDING_SYNC_CREATE);
+
+    self->title_binding = g_object_bind_property (self->page, "title",
+                                                  self->title, "label",
+                                                  G_BINDING_SYNC_CREATE);
+  }
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PAGE]);
+}
+
+GtkWidget *
+hdy_tab_get_child (HdyTab *self)
+{
+  return self->child;
+}
+
+gint
+hdy_tab_get_child_min_width (HdyTab *self)
+{
+  GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  GtkStateFlags flags = gtk_widget_get_state_flags (GTK_WIDGET (self));
+  gint min, css_width;
+
+  if (self->child)
+    gtk_widget_get_preferred_width (self->child, &min, NULL);
+  else
+    min = 0;
+
+  gtk_style_context_get (context, flags,
+                         "min-width", &css_width,
+                         NULL);
+
+  return MAX (min, css_width);
+}
+
+gint
+hdy_tab_get_display_width (HdyTab *self)
+{
+  return self->display_width;
+}
+
+void
+hdy_tab_set_display_width (HdyTab *self,
+                           gint    width)
+{
+  if (self->display_width == width)
+    return;
+
+  self->display_width = width;
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DISPLAY_WIDTH]);
+}
+
+gboolean
+hdy_tab_get_hovering (HdyTab *self)
+{
+  return self->hovering;
+}
+
+void
+hdy_tab_set_hovering (HdyTab   *self,
+                      gboolean  hovering)
+{
+  if (self->hovering == hovering)
+    return;
+
+  self->hovering = hovering;
+
+  update_state (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HOVERING]);
+}
+
+gboolean
+hdy_tab_get_selected (HdyTab *self)
+{
+  return self->selected;
+}
+
+void
+hdy_tab_set_selected (HdyTab   *self,
+                      gboolean  selected)
+{
+  if (self->selected == selected)
+    return;
+
+  self->selected = selected;
+
+  update_state (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED]);
+}
diff --git a/src/hdy-tab.ui b/src/hdy-tab.ui
new file mode 100644
index 00000000..ac26e22c
--- /dev/null
+++ b/src/hdy-tab.ui
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk+" version="3.24"/>
+  <template class="HdyTab" parent="GtkContainer">
+    <property name="can-focus">True</property>
+    <child>
+      <object class="GtkStack" id="stack">
+        <property name="visible">True</property>
+        <property name="hhomogeneous">False</property>
+        <style>
+          <class name="tab-contents"/>
+        </style>
+        <child>
+          <object class="GtkBox">
+            <property name="visible">True</property>
+            <child type="center">
+              <object class="GtkBox">
+                <property name="visible">True</property>
+                <child>
+                  <object class="GtkStack" id="icon_stack">
+                    <property name="visible">True</property>
+                    <property name="margin-start">6</property>
+                    <property name="margin-end">6</property>
+                    <child>
+                      <object class="GtkImage" id="icon">
+                        <property name="visible">True</property>
+                        <style>
+                          <class name="tab-icon"/>
+                        </style>
+                      </object>
+                      <packing>
+                        <property name="name">icon</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkSpinner">
+                        <property name="visible">True</property>
+                        <property name="active">True</property>
+                      </object>
+                      <packing>
+                        <property name="name">spinner</property>
+                      </packing>
+                    </child>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="title">
+                    <property name="visible">True</property>
+                    <property name="hexpand">True</property>
+                    <property name="halign">center</property>
+                    <property name="xalign">0</property>
+                    <property name="ellipsize">end</property>
+                    <property name="single-line-mode">True</property>
+                    <style>
+                      <class name="tab-title"/>
+                    </style>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="GtkButton" id="close_btn">
+                <property name="visible" bind-source="HdyTab" bind-property="selected" 
bind-flags="sync-create"/>
+                <property name="valign">center</property>
+                <property name="halign">end</property>
+                <property name="can-focus">False</property>
+                <signal name="clicked" handler="close_clicked_cb" swapped="true"/>
+                <style>
+                  <class name="tab-close-button"/>
+                  <class name="image-button"/>
+                  <class name="flat"/>
+                </style>
+                <child>
+                  <object class="GtkImage">
+                    <property name="visible">True</property>
+                    <property name="icon-name">window-close-symbolic</property>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="pack-type">end</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkImage" id="secondary_icon">
+                <property name="visible">True</property>
+                <property name="margin-start">6</property>
+                <property name="margin-end">6</property>
+                <style>
+                  <class name="tab-secondary-icon"/>
+                </style>
+              </object>
+              <packing>
+                <property name="pack-type">end</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="name">regular</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkStack" id="pinned_icon_stack">
+            <property name="visible">True</property>
+            <property name="halign">center</property>
+            <property name="hexpand">True</property>
+            <child>
+              <object class="GtkImage" id="pinned_icon">
+                <property name="visible">True</property>
+                <style>
+                  <class name="tab-icon"/>
+                </style>
+              </object>
+              <packing>
+                <property name="name">icon</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkSpinner">
+                <property name="visible">True</property>
+                <property name="active">True</property>
+              </object>
+              <packing>
+                <property name="name">spinner</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="name">pinned</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/icons/hdy-tab-icon-missing-symbolic.svg b/src/icons/hdy-tab-icon-missing-symbolic.svg
new file mode 100644
index 00000000..48807915
--- /dev/null
+++ b/src/icons/hdy-tab-icon-missing-symbolic.svg
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/";
+   xmlns:cc="http://creativecommons.org/ns#";
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+   xmlns:svg="http://www.w3.org/2000/svg";
+   xmlns="http://www.w3.org/2000/svg";
+   height="16"
+   id="svg7384"
+   version="1.1"
+   width="16">
+  <metadata
+     id="metadata90">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage"; />
+        <dc:title>Gnome Symbolic Icon Theme</dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <title
+     id="title9167">Gnome Symbolic Icon Theme</title>
+  <defs
+     id="defs7386" />
+  <path
+     
style="color:#bebebe;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#241f31;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;st
 
roke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+     d="M 2.1992188 0 C 0.94446875 0 -2.9605947e-16 1.0997689 0 2.3183594 L 0 4 L 2 4 L 2 2.3183594 C 2 
2.0752625 2.1256288 2 2.1992188 2 L 4 2 L 4 0 L 2.1992188 0 z M 6 0 L 6 2 L 10 2 L 10 0 L 6 0 z M 12 0 L 12 2 
L 13.800781 2 C 13.874371 2 14 2.0752716 14 2.3183594 L 14 4 L 16 4 L 16 2.3183594 C 16 1.0997598 15.055531 
-2.9605947e-16 13.800781 0 L 12 0 z M 0 6 L 0 10 L 2 10 L 2 6 L 0 6 z M 14 6 L 14 10 L 16 10 L 16 6 L 14 6 z 
M 0 12 L 0 13.681641 C 0 14.900231 0.94447875 16 2.1992188 16 L 4 16 L 4 14 L 2.1992188 14 C 2.1256188 14 2 
13.924738 2 13.681641 L 2 12 L 0 12 z M 14 12 L 14 13.681641 C 14 13.924729 13.874381 14 13.800781 14 L 12 14 
L 12 16 L 13.800781 16 C 15.055521 16 16 14.90024 16 13.681641 L 16 12 L 14 12 z M 6 14 L 6 16 L 10 16 L 10 
14 L 6 14 z "
+     id="path983" />
+</svg>
diff --git a/src/meson.build b/src/meson.build
index 11d4100e..58f235dc 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -14,6 +14,7 @@ hdy_public_enum_headers = [
   'hdy-leaflet.h',
   'hdy-navigation-direction.h',
   'hdy-squeezer.h',
+  'hdy-tab-bar.h',
   'hdy-view-switcher.h',
 ]
 
@@ -93,6 +94,9 @@ src_headers = [
   'hdy-swipe-group.h',
   'hdy-swipe-tracker.h',
   'hdy-swipeable.h',
+  'hdy-tab-bar.h',
+  'hdy-tab-view.h',
+  'hdy-title-bar.h',
   'hdy-title-bar.h',
   'hdy-types.h',
   'hdy-value-object.h',
@@ -149,6 +153,10 @@ src_sources = [
   'hdy-swipe-group.c',
   'hdy-swipe-tracker.c',
   'hdy-swipeable.c',
+  'hdy-tab.c',
+  'hdy-tab-bar.c',
+  'hdy-tab-box.c',
+  'hdy-tab-view.c',
   'hdy-title-bar.c',
   'hdy-value-object.c',
   'hdy-view-switcher.c',
diff --git a/src/themes/Adwaita-dark.css b/src/themes/Adwaita-dark.css
index cd5441d7..fc06c643 100644
--- a/src/themes/Adwaita-dark.css
+++ b/src/themes/Adwaita-dark.css
@@ -168,11 +168,11 @@ list.content, list.content list { background-color: transparent; }
 
 list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > 
row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#353535, #2d2d2d, 0.5); }
 
-list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: 
mix(#eeeeec, #2d2d2d, 0.95); }
+list.content list.nested > row.activatable:not(:active):hover:not(:selected) { background-color: 
mix(#eeeeec, #2d2d2d, 0.95); }
 
 list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > 
row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander 
row.header:not(:active):not(:hover):not(:selected), list.content > row.expander 
row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #2d2d2d; }
 
-list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander 
row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#eeeeec, #2d2d2d, 0.95); }
+list.content > row.activatable:not(.expander):not(:active):hover:not(:selected), list.content > row.expander 
row.header.activatable:not(:active):hover:not(:selected) { background-color: mix(#eeeeec, #2d2d2d, 0.95); }
 
 list.content > row, list.content > row list > row { border-color: alpha(#1b1b1b, 0.7); border-style: solid; 
transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
 
@@ -182,7 +182,7 @@ list.content > row:first-child, list.content > row.expander:first-child row.head
 
 list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked { border-width: 1px; }
 
-list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander.checked-expander-row-previous-sibling:not(:checked) row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
 
 list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { 
margin-top: 6px; }
 
@@ -195,3 +195,69 @@ window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { b
 window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 
255, 255, 0.065); }
 
 
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized),
 
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration, 
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration-overlay { border-radius: 8px; }
+
+tabbar .box { min-height: 38px; background: #161616; border-color: #070707; border-style: solid; }
+
+tabbar .box:backdrop { background-color: #262626; border-color: #202020; }
+
+tabbar.top .box { border-bottom-width: 1px; }
+
+tabbar.bottom .box { border-top-width: 1px; }
+
+tabbar tabbox.pinned:dir(ltr) { padding-right: 1px; box-shadow: inset -1px 0 #070707; }
+
+tabbar tabbox.pinned:dir(ltr):backdrop { box-shadow: inset -1px 0 #202020; }
+
+tabbar tabbox.pinned:dir(rtl) { padding-left: 1px; box-shadow: inset 1px 0 #070707; }
+
+tabbar tabbox.pinned:dir(rtl):backdrop { box-shadow: inset 1px 0 #202020; }
+
+tabbar undershoot.left, tabbar undershoot.right { transition: all 200ms ease; }
+
+tabbar undershoot.left { background: linear-gradient(to right, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0) 20px); }
+
+tabbar undershoot.right { background: linear-gradient(to left, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0) 20px); }
+
+tabbar.needs-attention-left undershoot.left { background: linear-gradient(to right, rgba(21, 83, 158, 0.7), 
rgba(21, 83, 158, 0.5) 1px, rgba(21, 83, 158, 0) 20px); }
+
+tabbar.needs-attention-right undershoot.right { background: linear-gradient(to left, rgba(21, 83, 158, 0.7), 
rgba(21, 83, 158, 0.5) 1px, rgba(21, 83, 158, 0) 20px); }
+
+tabbar tab { min-width: 130px; border-style: solid; border-color: #070707; border-width: 0 1px 0 1px; 
background-color: #1e1e1e; transition: background 150ms ease-in-out; }
+
+tabbar tab.pinned { min-width: 40px; }
+
+tabbar tab:hover { background-color: #323232; }
+
+tabbar tab:checked { background-color: #262626; }
+
+tabbar tab:checked:hover { background-color: #3a3a3a; }
+
+tabbar tab:backdrop { border-color: #202020; background-color: #2d2d2d; }
+
+tabbar tab:backdrop:checked { background-color: #353535; }
+
+tabbar tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 255, 0.8), 
rgba(21, 83, 158, 0.2) 15%, rgba(21, 83, 158, 0) 15%); }
+
+tabbar tab .tab-contents { padding: 6px; }
+
+tabbar tab .tab-close-button { margin: 0; padding: 0; border-radius: 9999px; }
+
+tabbar tab .tab-close-button:not(:hover) { border-color: transparent; }
+
+tabbar .start-action, tabbar .end-action { background: #1e1e1e; border-color: #070707; border-style: solid; 
transition: background 150ms ease-in-out; }
+
+tabbar .start-action:backdrop, tabbar .end-action:backdrop { border-color: #202020; background-color: 
#2d2d2d; }
+
+tabbar .start-action button, tabbar .end-action button { border: none; border-radius: 0; }
+
+tabbar .start-action:dir(ltr), tabbar .end-action:dir(rtl) { border-right-width: 1px; }
+
+tabbar .start-action:dir(ltr) > *, tabbar .end-action:dir(rtl) > * { margin-right: 1px; }
+
+tabbar .start-action:dir(rtl), tabbar .end-action:dir(ltr) { border-left-width: 1px; }
+
+tabbar .start-action:dir(rtl) > *, tabbar .end-action:dir(ltr) > * { margin-left: 1px; }
+
+.tab-drag-icon tab { min-height: 38px; background-color: #3a3a3a; box-shadow: 0 3px 9px 1px rgba(0, 0, 0, 
0.5), 0 0 0 1px rgba(27, 27, 27, 0.9); margin: 25px; }
+
+.tab-drag-icon tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 
255, 0.8), rgba(21, 83, 158, 0.2) 15%, rgba(21, 83, 158, 0) 15%); }
diff --git a/src/themes/Adwaita.css b/src/themes/Adwaita.css
index b7f09037..618f3a7c 100644
--- a/src/themes/Adwaita.css
+++ b/src/themes/Adwaita.css
@@ -168,11 +168,11 @@ list.content, list.content list { background-color: transparent; }
 
 list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > 
row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#f6f5f4, #ffffff, 0.5); }
 
-list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: 
mix(#2e3436, #ffffff, 0.95); }
+list.content list.nested > row.activatable:not(:active):hover:not(:selected) { background-color: 
mix(#2e3436, #ffffff, 0.95); }
 
 list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > 
row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander 
row.header:not(:active):not(:hover):not(:selected), list.content > row.expander 
row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #ffffff; }
 
-list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander 
row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#2e3436, #ffffff, 0.95); }
+list.content > row.activatable:not(.expander):not(:active):hover:not(:selected), list.content > row.expander 
row.header.activatable:not(:active):hover:not(:selected) { background-color: mix(#2e3436, #ffffff, 0.95); }
 
 list.content > row, list.content > row list > row { border-color: alpha(#cdc7c2, 0.7); border-style: solid; 
transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
 
@@ -182,7 +182,7 @@ list.content > row:first-child, list.content > row.expander:first-child row.head
 
 list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked { border-width: 1px; }
 
-list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander.checked-expander-row-previous-sibling:not(:checked) row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
 
 list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { 
margin-top: 6px; }
 
@@ -195,3 +195,69 @@ window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { b
 window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 
255, 255, 0.34); }
 
 
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized),
 
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration, 
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration-overlay { border-radius: 8px; }
+
+tabbar .box { min-height: 38px; background: #c6bfb9; border-color: #bfb8b1; border-style: solid; }
+
+tabbar .box:backdrop { background-color: #e1dedb; border-color: #d5d0cc; }
+
+tabbar.top .box { border-bottom-width: 1px; }
+
+tabbar.bottom .box { border-top-width: 1px; }
+
+tabbar tabbox.pinned:dir(ltr) { padding-right: 1px; box-shadow: inset -1px 0 #bfb8b1; }
+
+tabbar tabbox.pinned:dir(ltr):backdrop { box-shadow: inset -1px 0 #d5d0cc; }
+
+tabbar tabbox.pinned:dir(rtl) { padding-left: 1px; box-shadow: inset 1px 0 #bfb8b1; }
+
+tabbar tabbox.pinned:dir(rtl):backdrop { box-shadow: inset 1px 0 #d5d0cc; }
+
+tabbar undershoot.left, tabbar undershoot.right { transition: all 200ms ease; }
+
+tabbar undershoot.left { background: linear-gradient(to right, rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0) 20px); }
+
+tabbar undershoot.right { background: linear-gradient(to left, rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0) 20px); }
+
+tabbar.needs-attention-left undershoot.left { background: linear-gradient(to right, rgba(53, 132, 228, 0.7), 
rgba(53, 132, 228, 0.5) 1px, rgba(53, 132, 228, 0) 20px); }
+
+tabbar.needs-attention-right undershoot.right { background: linear-gradient(to left, rgba(53, 132, 228, 
0.7), rgba(53, 132, 228, 0.5) 1px, rgba(53, 132, 228, 0) 20px); }
+
+tabbar tab { min-width: 130px; border-style: solid; border-color: #bfb8b1; border-width: 0 1px 0 1px; 
background-color: #cdc7c2; transition: background 150ms ease-in-out; }
+
+tabbar tab.pinned { min-width: 40px; }
+
+tabbar tab:hover { background-color: #d8d4d0; }
+
+tabbar tab:checked { background-color: #dad6d2; }
+
+tabbar tab:checked:hover { background-color: #e6e3e0; }
+
+tabbar tab:backdrop { border-color: #d5d0cc; background-color: #e8e6e3; }
+
+tabbar tab:backdrop:checked { background-color: #f6f5f4; }
+
+tabbar tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 255, 0.8), 
rgba(53, 132, 228, 0.2) 15%, rgba(53, 132, 228, 0) 15%); }
+
+tabbar tab .tab-contents { padding: 6px; }
+
+tabbar tab .tab-close-button { margin: 0; padding: 0; border-radius: 9999px; }
+
+tabbar tab .tab-close-button:not(:hover) { border-color: transparent; }
+
+tabbar .start-action, tabbar .end-action { background: #cdc7c2; border-color: #bfb8b1; border-style: solid; 
transition: background 150ms ease-in-out; }
+
+tabbar .start-action:backdrop, tabbar .end-action:backdrop { border-color: #d5d0cc; background-color: 
#e8e6e3; }
+
+tabbar .start-action button, tabbar .end-action button { border: none; border-radius: 0; }
+
+tabbar .start-action:dir(ltr), tabbar .end-action:dir(rtl) { border-right-width: 1px; }
+
+tabbar .start-action:dir(ltr) > *, tabbar .end-action:dir(rtl) > * { margin-right: 1px; }
+
+tabbar .start-action:dir(rtl), tabbar .end-action:dir(ltr) { border-left-width: 1px; }
+
+tabbar .start-action:dir(rtl) > *, tabbar .end-action:dir(ltr) > * { margin-left: 1px; }
+
+.tab-drag-icon tab { min-height: 38px; background-color: #e6e3e0; box-shadow: 0 3px 9px 1px rgba(0, 0, 0, 
0.5), 0 0 0 1px rgba(0, 0, 0, 0.23); margin: 25px; }
+
+.tab-drag-icon tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 
255, 0.8), rgba(53, 132, 228, 0.2) 15%, rgba(53, 132, 228, 0) 15%); }
diff --git a/src/themes/HighContrast.css b/src/themes/HighContrast.css
index 20bf2281..704593d7 100644
--- a/src/themes/HighContrast.css
+++ b/src/themes/HighContrast.css
@@ -168,11 +168,11 @@ list.content, list.content list { background-color: transparent; }
 
 list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > 
row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#fdfdfc, #ffffff, 0.5); }
 
-list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: 
mix(#272c2e, #ffffff, 0.95); }
+list.content list.nested > row.activatable:not(:active):hover:not(:selected) { background-color: 
mix(#272c2e, #ffffff, 0.95); }
 
 list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > 
row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander 
row.header:not(:active):not(:hover):not(:selected), list.content > row.expander 
row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #ffffff; }
 
-list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander 
row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#272c2e, #ffffff, 0.95); }
+list.content > row.activatable:not(.expander):not(:active):hover:not(:selected), list.content > row.expander 
row.header.activatable:not(:active):hover:not(:selected) { background-color: mix(#272c2e, #ffffff, 0.95); }
 
 list.content > row, list.content > row list > row { border-color: alpha(#877b6e, 0.7); border-style: solid; 
transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
 
@@ -182,7 +182,7 @@ list.content > row:first-child, list.content > row.expander:first-child row.head
 
 list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked { border-width: 1px; }
 
-list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander.checked-expander-row-previous-sibling:not(:checked) row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
 
 list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { 
margin-top: 6px; }
 
@@ -195,3 +195,69 @@ window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { b
 window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 
255, 255, 0.34); }
 
 
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized),
 
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration, 
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration-overlay { border-radius: 8px; }
+
+tabbar .box { min-height: 38px; background: #cdc7c2; border-color: #6e645a; border-style: solid; }
+
+tabbar .box:backdrop { background-color: #e8e6e3; border-color: #d5d0cc; }
+
+tabbar.top .box { border-bottom-width: 1px; }
+
+tabbar.bottom .box { border-top-width: 1px; }
+
+tabbar tabbox.pinned:dir(ltr) { padding-right: 1px; box-shadow: inset -1px 0 #6e645a; }
+
+tabbar tabbox.pinned:dir(ltr):backdrop { box-shadow: inset -1px 0 #d5d0cc; }
+
+tabbar tabbox.pinned:dir(rtl) { padding-left: 1px; box-shadow: inset 1px 0 #6e645a; }
+
+tabbar tabbox.pinned:dir(rtl):backdrop { box-shadow: inset 1px 0 #d5d0cc; }
+
+tabbar undershoot.left, tabbar undershoot.right { transition: all 200ms ease; }
+
+tabbar undershoot.left { background: linear-gradient(to right, rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0) 20px); }
+
+tabbar undershoot.right { background: linear-gradient(to left, rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0) 20px); }
+
+tabbar.needs-attention-left undershoot.left { background: linear-gradient(to right, rgba(27, 106, 203, 0.7), 
rgba(27, 106, 203, 0.5) 1px, rgba(27, 106, 203, 0) 20px); }
+
+tabbar.needs-attention-right undershoot.right { background: linear-gradient(to left, rgba(27, 106, 203, 
0.7), rgba(27, 106, 203, 0.5) 1px, rgba(27, 106, 203, 0) 20px); }
+
+tabbar tab { min-width: 130px; border-style: solid; border-color: #6e645a; border-width: 0 1px 0 1px; 
background-color: #d4cfca; transition: background 150ms ease-in-out; }
+
+tabbar tab.pinned { min-width: 40px; }
+
+tabbar tab:hover { background-color: #dfdcd8; }
+
+tabbar tab:checked { background-color: #e1dedb; }
+
+tabbar tab:checked:hover { background-color: #edebe9; }
+
+tabbar tab:backdrop { border-color: #d5d0cc; background-color: #efedec; }
+
+tabbar tab:backdrop:checked { background-color: #fdfdfc; }
+
+tabbar tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 255, 0.8), 
rgba(27, 106, 203, 0.2) 15%, rgba(27, 106, 203, 0) 15%); }
+
+tabbar tab .tab-contents { padding: 6px; }
+
+tabbar tab .tab-close-button { margin: 0; padding: 0; border-radius: 9999px; }
+
+tabbar tab .tab-close-button:not(:hover) { border-color: transparent; }
+
+tabbar .start-action, tabbar .end-action { background: #d4cfca; border-color: #6e645a; border-style: solid; 
transition: background 150ms ease-in-out; }
+
+tabbar .start-action:backdrop, tabbar .end-action:backdrop { border-color: #d5d0cc; background-color: 
#efedec; }
+
+tabbar .start-action button, tabbar .end-action button { border: none; border-radius: 0; }
+
+tabbar .start-action:dir(ltr), tabbar .end-action:dir(rtl) { border-right-width: 1px; }
+
+tabbar .start-action:dir(ltr) > *, tabbar .end-action:dir(rtl) > * { margin-right: 1px; }
+
+tabbar .start-action:dir(rtl), tabbar .end-action:dir(ltr) { border-left-width: 1px; }
+
+tabbar .start-action:dir(rtl) > *, tabbar .end-action:dir(ltr) > * { margin-left: 1px; }
+
+.tab-drag-icon tab { min-height: 38px; background-color: #edebe9; box-shadow: 0 3px 9px 1px rgba(0, 0, 0, 
0.5), 0 0 0 1px rgba(0, 0, 0, 0.23); margin: 25px; }
+
+.tab-drag-icon tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 
255, 0.8), rgba(27, 106, 203, 0.2) 15%, rgba(27, 106, 203, 0) 15%); }
diff --git a/src/themes/HighContrastInverse.css b/src/themes/HighContrastInverse.css
index ed846282..39ff160e 100644
--- a/src/themes/HighContrastInverse.css
+++ b/src/themes/HighContrastInverse.css
@@ -168,11 +168,11 @@ list.content, list.content list { background-color: transparent; }
 
 list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested > 
row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#303030, #2d2d2d, 0.5); }
 
-list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color: 
mix(#f3f3f1, #2d2d2d, 0.95); }
+list.content list.nested > row.activatable:not(:active):hover:not(:selected) { background-color: 
mix(#f3f3f1, #2d2d2d, 0.95); }
 
 list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content > 
row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander 
row.header:not(:active):not(:hover):not(:selected), list.content > row.expander 
row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #2d2d2d; }
 
-list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander 
row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#f3f3f1, #2d2d2d, 0.95); }
+list.content > row.activatable:not(.expander):not(:active):hover:not(:selected), list.content > row.expander 
row.header.activatable:not(:active):hover:not(:selected) { background-color: mix(#f3f3f1, #2d2d2d, 0.95); }
 
 list.content > row, list.content > row list > row { border-color: alpha(#686868, 0.7); border-style: solid; 
transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
 
@@ -182,7 +182,7 @@ list.content > row:first-child, list.content > row.expander:first-child row.head
 
 list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked { border-width: 1px; }
 
-list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content > 
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content > 
row.expander.checked-expander-row-previous-sibling:not(:checked) row.header, list.content > 
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child { 
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px; 
-gtk-outline-bottom-right-radius: 7px; }
 
 list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row { 
margin-top: 6px; }
 
@@ -195,3 +195,69 @@ window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { b
 window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255, 
255, 255, 0.065); }
 
 
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized),
 
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration, 
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration-overlay { border-radius: 8px; }
+
+tabbar .box { min-height: 38px; background: #111111; border-color: #4e4e4e; border-style: solid; }
+
+tabbar .box:backdrop { background-color: #202020; border-color: #202020; }
+
+tabbar.top .box { border-bottom-width: 1px; }
+
+tabbar.bottom .box { border-top-width: 1px; }
+
+tabbar tabbox.pinned:dir(ltr) { padding-right: 1px; box-shadow: inset -1px 0 #4e4e4e; }
+
+tabbar tabbox.pinned:dir(ltr):backdrop { box-shadow: inset -1px 0 #202020; }
+
+tabbar tabbox.pinned:dir(rtl) { padding-left: 1px; box-shadow: inset 1px 0 #4e4e4e; }
+
+tabbar tabbox.pinned:dir(rtl):backdrop { box-shadow: inset 1px 0 #202020; }
+
+tabbar undershoot.left, tabbar undershoot.right { transition: all 200ms ease; }
+
+tabbar undershoot.left { background: linear-gradient(to right, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0) 20px); }
+
+tabbar undershoot.right { background: linear-gradient(to left, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0) 20px); }
+
+tabbar.needs-attention-left undershoot.left { background: linear-gradient(to right, rgba(15, 59, 113, 0.7), 
rgba(15, 59, 113, 0.5) 1px, rgba(15, 59, 113, 0) 20px); }
+
+tabbar.needs-attention-right undershoot.right { background: linear-gradient(to left, rgba(15, 59, 113, 0.7), 
rgba(15, 59, 113, 0.5) 1px, rgba(15, 59, 113, 0) 20px); }
+
+tabbar tab { min-width: 130px; border-style: solid; border-color: #4e4e4e; border-width: 0 1px 0 1px; 
background-color: #191919; transition: background 150ms ease-in-out; }
+
+tabbar tab.pinned { min-width: 40px; }
+
+tabbar tab:hover { background-color: #2d2d2d; }
+
+tabbar tab:checked { background-color: #202020; }
+
+tabbar tab:checked:hover { background-color: #353535; }
+
+tabbar tab:backdrop { border-color: #202020; background-color: #282828; }
+
+tabbar tab:backdrop:checked { background-color: #303030; }
+
+tabbar tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 255, 0.8), 
rgba(15, 59, 113, 0.2) 15%, rgba(15, 59, 113, 0) 15%); }
+
+tabbar tab .tab-contents { padding: 6px; }
+
+tabbar tab .tab-close-button { margin: 0; padding: 0; border-radius: 9999px; }
+
+tabbar tab .tab-close-button:not(:hover) { border-color: transparent; }
+
+tabbar .start-action, tabbar .end-action { background: #191919; border-color: #4e4e4e; border-style: solid; 
transition: background 150ms ease-in-out; }
+
+tabbar .start-action:backdrop, tabbar .end-action:backdrop { border-color: #202020; background-color: 
#282828; }
+
+tabbar .start-action button, tabbar .end-action button { border: none; border-radius: 0; }
+
+tabbar .start-action:dir(ltr), tabbar .end-action:dir(rtl) { border-right-width: 1px; }
+
+tabbar .start-action:dir(ltr) > *, tabbar .end-action:dir(rtl) > * { margin-right: 1px; }
+
+tabbar .start-action:dir(rtl), tabbar .end-action:dir(ltr) { border-left-width: 1px; }
+
+tabbar .start-action:dir(rtl) > *, tabbar .end-action:dir(ltr) > * { margin-left: 1px; }
+
+.tab-drag-icon tab { min-height: 38px; background-color: #353535; box-shadow: 0 3px 9px 1px rgba(0, 0, 0, 
0.5), 0 0 0 1px rgba(104, 104, 104, 0.9); margin: 25px; }
+
+.tab-drag-icon tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 
255, 0.8), rgba(15, 59, 113, 0.2) 15%, rgba(15, 59, 113, 0) 15%); }
diff --git a/src/themes/_Adwaita-base.scss b/src/themes/_Adwaita-base.scss
index cc0b7542..5d6e3bdb 100644
--- a/src/themes/_Adwaita-base.scss
+++ b/src/themes/_Adwaita-base.scss
@@ -334,3 +334,234 @@ window.csd.unified:not(.solid-csd):not(.fullscreen) {
     }
   }
 }
+
+
+
+$tab_selected_bg:           darken(darken($bg_color, 10%), 2%);
+$tab_selected_bg_backdrop:  $bg_color;
+
+@if $variant == 'dark' {
+  $tab_selected_bg:         lighten(darken($bg_color, 10%), 4%);
+}
+
+$tab_bg:                    darken($tab_selected_bg, 6%);
+$tab_bg_backdrop:           darken($tab_selected_bg_backdrop, 6%);
+
+$tab_bar_bg:                darken($tab_selected_bg, 9%);
+$tab_bar_bg_backdrop:       darken($tab_selected_bg_backdrop, 9%);
+
+$tab_hover_bg:              lighten($tab_bg, 5%);
+$tab_selected_hover_bg:     lighten($tab_selected_bg, 5%);
+
+@if $variant == 'dark' {
+  $tab_bg:                  darken($tab_selected_bg, 3%);
+  $tab_bg_backdrop:         darken($tab_selected_bg_backdrop, 3%);
+
+  $tab_bar_bg:              darken($tab_selected_bg, 6%);
+  $tab_bar_bg_backdrop:     darken($tab_selected_bg_backdrop, 6%);
+
+  $tab_hover_bg:            lighten($tab_bg, 8%);
+  $tab_selected_hover_bg:   lighten($tab_selected_bg, 8%);
+}
+
+$tab_needs_attention_color: $selected_bg_color;
+
+@mixin undershoot-gradient($dir) {
+  $color: black;
+
+  @if $variant == 'dark' {
+    background: linear-gradient(to #{$dir},
+                                transparentize($color, .6),
+                                transparentize($color, 1) 20px);
+  }
+  @else {
+    background: linear-gradient(to #{$dir},
+                                transparentize($color, .93),
+                                transparentize($color, 1) 20px);
+  }
+}
+
+@mixin need-attention-gradient($dir) {
+  background: linear-gradient(to #{$dir},
+                              transparentize($tab_needs_attention_color, .3),
+                              transparentize($tab_needs_attention_color, .5) 1px,
+                              transparentize($tab_needs_attention_color, 1) 20px);
+}
+
+tabbar {
+  .box {
+    min-height: 38px;
+    background: $tab_bar_bg;
+    border-color: $alt_borders_color;
+    border-style: solid;
+
+    &:backdrop {
+      background-color: $tab_bar_bg_backdrop;
+      border-color: $backdrop_borders_color;
+    }
+  }
+
+  &.top .box {
+    border-bottom-width: 1px;
+  }
+
+  &.bottom .box {
+    border-top-width: 1px;
+  }
+
+  tabbox.pinned {
+    &:dir(ltr) {
+      padding-right: 1px;
+      box-shadow: inset -1px 0 $alt_borders_color;
+
+      &:backdrop {
+        box-shadow: inset -1px 0 $backdrop_borders_color;
+      }
+    }
+
+    &:dir(rtl) {
+      padding-left: 1px;
+      box-shadow: inset 1px 0 $alt_borders_color;
+
+      &:backdrop {
+        box-shadow: inset 1px 0 $backdrop_borders_color;
+      }
+    }
+  }
+
+  undershoot {
+    &.left, &.right {
+      transition: all 200ms ease;
+    }
+
+    &.left {
+      @include undershoot-gradient("right");
+    }
+
+    &.right {
+      @include undershoot-gradient("left");
+    }
+  }
+
+  &.needs-attention-left undershoot.left {
+    @include need-attention-gradient("right");
+  }
+
+  &.needs-attention-right undershoot.right {
+    @include need-attention-gradient("left");
+  }
+
+  tab {
+    min-width: 130px;
+    border-style: solid;
+    border-color: $alt_borders_color;
+    border-width: 0 1px 0 1px;
+    background-color: $tab_bg;
+    transition: background 150ms ease-in-out;
+
+    &.pinned {
+      min-width: 40px;
+    }
+
+    &:hover {
+      background-color: $tab_hover_bg;
+    }
+
+    &:checked {
+      background-color: $tab_selected_bg;
+
+      &:hover {
+        background-color: $tab_selected_hover_bg;
+      }
+    }
+
+    &:backdrop {
+      border-color: $backdrop_borders_color;
+      background-color: $tab_bg_backdrop;
+
+      &:checked {
+        background-color: $tab_selected_bg_backdrop;
+      }
+    }
+
+    &.needs-attention {
+      background-image:
+        radial-gradient(ellipse at bottom,
+                        transparentize(white, .2),
+                        transparentize($tab_needs_attention_color, .8) 15%,
+                        transparentize($tab_needs_attention_color, 1) 15%);
+    }
+
+    .tab-contents {
+      padding: 6px;
+    }
+
+    .tab-close-button {
+      margin: 0;
+      padding: 0;
+      border-radius: 9999px;
+
+      &:not(:hover) {
+        border-color: transparent;
+      }
+    }
+  }
+
+  .start-action,
+  .end-action {
+    background: $tab_bg;
+    border-color: $alt_borders_color;
+    border-style: solid;
+    transition: background 150ms ease-in-out;
+
+    &:backdrop {
+      border-color: $backdrop_borders_color;
+      background-color: $tab_bg_backdrop;
+    }
+
+    button {
+      border: none;
+      border-radius: 0;
+    }
+  }
+
+  .start-action:dir(ltr),
+  .end-action:dir(rtl) {
+    border-right-width: 1px;
+
+    > * {
+      margin-right: 1px;
+    }
+  }
+
+  .start-action:dir(rtl),
+  .end-action:dir(ltr) {
+    border-left-width: 1px;
+
+    > * {
+      margin-left: 1px;
+    }
+  }
+}
+
+.tab-drag-icon {
+  tab {
+    min-height: 38px;
+    background-color: $tab_selected_hover_bg;
+
+    $_wm_border: if($variant=='light', transparentize(black, 0.77), transparentize($borders_color, 0.1));
+
+    box-shadow: 0 3px 9px 1px transparentize(black, 0.5),
+                0 0 0 1px $_wm_border; //doing borders with box-shadow
+
+    margin: 25px;
+
+    &.needs-attention {
+      background-image:
+        radial-gradient(ellipse at bottom,
+                        transparentize(white, .2),
+                        transparentize($tab_needs_attention_color, .8) 15%,
+                        transparentize($tab_needs_attention_color, 1) 15%);
+    }
+  }
+}


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