[libhandy] Add HdyTabBox



commit 45b60a455d8ab606e112bdd09af596fc513229da
Author: Alexander Mikhaylenko <alexm gnome org>
Date:   Sun Sep 13 02:27:04 2020 +0500

    Add HdyTabBox

 doc/meson.build           |    1 +
 src/hdy-tab-box-private.h |   54 +
 src/hdy-tab-box.c         | 3882 +++++++++++++++++++++++++++++++++++++++++++++
 src/meson.build           |    1 +
 4 files changed, 3938 insertions(+)
---
diff --git a/doc/meson.build b/doc/meson.build
index af9d3dd4..d4f54043 100644
--- a/doc/meson.build
+++ b/doc/meson.build
@@ -22,6 +22,7 @@ private_headers = [
     'hdy-stackable-box-private.h',
     'hdy-swipe-tracker-private.h',
     'hdy-tab-private.h',
+    'hdy-tab-box-private.h',
     'hdy-tab-view-private.h',
     'hdy-types.h',
     'hdy-view-switcher-button-private.h',
diff --git a/src/hdy-tab-box-private.h b/src/hdy-tab-box-private.h
new file mode 100644
index 00000000..44fcb2c9
--- /dev/null
+++ b/src/hdy-tab-box-private.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#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_attach_page (HdyTabBox  *self,
+                              HdyTabPage *page,
+                              gint        position);
+void hdy_tab_box_detach_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);
+
+void hdy_tab_box_set_extra_drag_dest_targets (HdyTabBox     *self,
+                                              GtkTargetList *extra_drag_dest_targets);
+
+gboolean hdy_tab_box_get_expand_tabs (HdyTabBox *self);
+void     hdy_tab_box_set_expand_tabs (HdyTabBox *self,
+                                      gboolean   expand_tabs);
+
+gboolean hdy_tab_box_get_inverted (HdyTabBox *self);
+void     hdy_tab_box_set_inverted (HdyTabBox *self,
+                                   gboolean   inverted);
+
+G_END_DECLS
diff --git a/src/hdy-tab-box.c b/src/hdy-tab-box.c
new file mode 100644
index 00000000..cd076911
--- /dev/null
+++ b/src/hdy-tab-box.c
@@ -0,0 +1,3882 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#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 DROP_SWITCH_TIMEOUT 500
+
+#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
+
+#define MAX_TAB_WIDTH_NON_EXPAND 220
+
+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;
+  gboolean expand_tabs;
+  gboolean inverted;
+
+  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;
+
+  gboolean pressed;
+  TabInfo *pressed_tab;
+
+  TabInfo *reordered_tab;
+  HdyAnimation *reorder_animation;
+
+  gint reorder_x;
+  gint reorder_y;
+  gint 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;
+  gint detached_index;
+  TabInfo *reorder_placeholder;
+  HdyTabPage *placeholder_page;
+  gint placeholder_scroll_offset;
+  gboolean can_remove_placeholder;
+  DragIcon *drag_icon;
+  gboolean should_detach_into_new_window;
+  GtkTargetList *source_targets;
+
+  TabInfo *drop_target_tab;
+  guint drop_switch_timeout_id;
+  guint reset_drop_target_tab_id;
+  gboolean can_accept_drop;
+  int drop_target_x;
+
+  struct {
+    TabInfo *info;
+    gint pos;
+    gint64 duration;
+    gboolean keep_selected_visible;
+  } scheduled_scroll;
+
+  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,
+  PROP_RESIZE_FROZEN,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+  SIGNAL_STOP_KINETIC_SCROLLING,
+  SIGNAL_EXTRA_DRAG_DATA_RECEIVED,
+  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,
+                    guint      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) floor ((base_width - OVERLAP) * info->appear_progress);
+}
+
+static gint
+get_base_tab_width (HdyTabBox *self, gboolean target)
+{
+  gdouble max_progress = 0;
+  gdouble n = 0;
+  gdouble used_width;
+  GList *l;
+  gint ret;
+
+  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 - (target ? 0 : self->end_padding)) * max_progress;
+
+  ret = (gint) ceil (used_width / n);
+
+  if (!self->expand_tabs)
+    ret = MIN (ret, MAX_TAB_WIDTH_NON_EXPAND + OVERLAP);
+
+  return ret;
+}
+
+static gint
+predict_tab_width (HdyTabBox *self,
+                   TabInfo   *info,
+                   gboolean   assume_placeholder)
+{
+  gint n;
+  gint width = self->allocated_width;
+  gint min;
+
+  if (self->pinned)
+    n = hdy_tab_view_get_n_pinned_pages (self->view);
+  else
+    n = hdy_tab_view_get_n_pages (self->view) - hdy_tab_view_get_n_pinned_pages (self->view);
+
+  if (assume_placeholder)
+      n++;
+
+  width += OVERLAP * (n + 1) - self->end_padding;
+
+  /* Tabs have 0 minimum width, we need natural width instead */
+  gtk_widget_get_preferred_width (GTK_WIDGET (info->tab), NULL, &min);
+
+  if (self->expand_tabs)
+    return MAX ((gint) floor (width / (gdouble) n), min);
+  else
+    return CLAMP ((gint) floor (width / (gdouble) n), min, MAX_TAB_WIDTH_NON_EXPAND);
+}
+
+static gint
+calculate_tab_offset (HdyTabBox *self,
+                      TabInfo   *info,
+                      gboolean target)
+{
+  gint width;
+
+  if (!self->reordered_tab)
+      return 0;
+
+  width = (target ? hdy_tab_get_display_width (self->reordered_tab->tab) : self->reordered_tab->width) - 
OVERLAP;
+
+  if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+      width = -width;
+
+  return (gint) round (width * (target ? info->end_reorder_offset : 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);
+  gdouble target_end_padding = 0;
+
+  if (!self->expand_tabs) {
+    gint predicted_tab_width = get_base_tab_width (self, TRUE);
+    GList *l;
+
+    target_end_padding = self->allocated_width + OVERLAP;
+
+    for (l = self->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+
+      target_end_padding -= calculate_tab_width (info, predicted_tab_width) - OVERLAP;
+    }
+
+    target_end_padding = MAX (target_end_padding, 0);
+  }
+
+  self->end_padding = (gint) floor (hdy_lerp (self->initial_end_padding, target_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);
+
+  self->end_padding = 0;
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+
+  g_clear_pointer (&self->resize_animation, hdy_animation_unref);
+}
+
+static void
+set_tab_resize_mode (HdyTabBox     *self,
+                     TabResizeMode  mode)
+{
+  gboolean notify;
+
+  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), 0, 1,
+                         RESIZE_ANIMATION_DURATION,
+                         hdy_ease_out_cubic,
+                         resize_animation_value_cb,
+                         resize_animation_done_cb,
+                         self);
+
+    hdy_animation_start (self->resize_animation);
+  }
+
+  notify = (self->tab_resize_mode == TAB_RESIZE_NORMAL) !=
+           (mode == TAB_RESIZE_NORMAL);
+
+  self->tab_resize_mode = mode;
+
+  if (notify)
+    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_RESIZE_FROZEN]);
+}
+
+/* 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_SHIFT_MASK,
+                                "reorder-tab", 2,
+                                GTK_TYPE_DIRECTION_TYPE, direction,
+                                G_TYPE_BOOLEAN, last);
+  gtk_binding_entry_add_signal (binding_set, keypad_keysym, GDK_SHIFT_MASK,
+                                "reorder-tab", 2,
+                                GTK_TYPE_DIRECTION_TYPE, direction,
+                                G_TYPE_BOOLEAN, last);
+}
+
+static void
+activate_tab (HdyTabBox *self)
+{
+  GtkWidget *child;
+
+  if (!self->selected_tab || !self->selected_tab->page)
+    return;
+
+  child = hdy_tab_page_get_child (self->selected_tab->page);
+
+  gtk_widget_grab_focus (child);
+}
+
+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 gboolean
+drop_switch_timeout_cb (HdyTabBox *self)
+{
+  self->drop_switch_timeout_id = 0;
+  hdy_tab_view_set_selected_page (self->view,
+                                  self->drop_target_tab->page);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+set_drop_target_tab (HdyTabBox *self,
+                     TabInfo   *info,
+                     gboolean   highlight)
+{
+  if (self->drop_target_tab == info)
+    return;
+
+  if (self->drop_target_tab) {
+    g_clear_handle_id (&self->drop_switch_timeout_id, g_source_remove);
+
+    gtk_drag_unhighlight (GTK_WIDGET (self->drop_target_tab->tab));
+    hdy_tab_set_hovering (self->drop_target_tab->tab, FALSE);
+  }
+
+  self->drop_target_tab = info;
+
+  if (self->drop_target_tab) {
+    hdy_tab_set_hovering (info->tab, TRUE);
+
+    if (highlight)
+      gtk_drag_highlight (GTK_WIDGET (info->tab));
+
+    self->drop_switch_timeout_id =
+      g_timeout_add (DROP_SWITCH_TIMEOUT,
+                     (GSourceFunc) drop_switch_timeout_cb,
+                     self);
+  }
+}
+
+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);
+
+  if (self->drop_target_tab) {
+    self->drop_target_x += (value - self->adjustment_prev_value);
+    set_drop_target_tab (self, find_tab_info_at (self, self->drop_target_x), self->can_accept_drop);
+  }
+
+  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_pointer (&self->scroll_animation, hdy_animation_unref);
+  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,
+                       hdy_ease_out_cubic,
+                       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_full (HdyTabBox *self,
+                    TabInfo   *info,
+                    gint       pos,
+                    gint64     duration,
+                    gboolean   keep_selected_visible)
+{
+  gint tab_width;
+  gdouble padding, value, page_size;
+
+  if (!self->adjustment)
+    return;
+
+  tab_width = info->width;
+
+  if (tab_width < 0) {
+    self->scheduled_scroll.info = info;
+    self->scheduled_scroll.pos = pos;
+    self->scheduled_scroll.duration = duration;
+    self->scheduled_scroll.keep_selected_visible = keep_selected_visible;
+
+    gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+    return;
+  }
+
+  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 (tab_width, page_size - tab_width) / 2.0;
+
+  if (pos < 0)
+    pos = get_tab_position (self, info);
+
+  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_full (self, info, -1, duration, FALSE);
+}
+
+/* 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, FALSE);
+
+  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_pointer (&self->reorder_animation, hdy_animation_unref);
+  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,
+                       hdy_ease_out_cubic,
+                       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_pointer (&info->reorder_animation, hdy_animation_unref);
+  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,
+                       hdy_ease_out_cubic,
+                       reorder_offset_animation_value_cb,
+                       reorder_offset_animation_done_cb,
+                       info);
+
+  hdy_animation_start (info->reorder_animation);
+}
+
+static void
+reset_reorder_animations (HdyTabBox *self)
+{
+  gint 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
+page_reordered_cb (HdyTabBox  *self,
+                   HdyTabPage *page,
+                   gint        index)
+{
+  GList *link;
+  gint 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_full (self, self->selected_tab, dest_tab->pos, REORDER_ANIMATION_DURATION, FALSE);
+
+  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))) {
+    gint 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;
+  gint i = 0;
+  gint width;
+  GList *l;
+
+  if (!self->dragging)
+    return;
+
+  x = get_reorder_position (self);
+
+  width = hdy_tab_get_display_width (self->reordered_tab->tab);
+
+  gdk_window_move_resize (self->reorder_window,
+                          x, 0,
+                          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, FALSE) + info->width / 2;
+    gdouble offset = 0;
+
+    if (x + width > center && center > x &&
+        (!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 + 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;
+  gint tab_width = 0;
+  gint autoscroll_area = 0;
+
+  if (self->reordered_tab) {
+    gtk_widget_get_preferred_width (GTK_WIDGET (self->reordered_tab->tab),
+                                    NULL, &tab_width);
+    x = (gdouble) self->reorder_x;
+  } else if (self->drop_target_tab) {
+    gtk_widget_get_preferred_width (GTK_WIDGET (self->drop_target_tab->tab),
+                                    NULL, &tab_width);
+    x = (gdouble) self->drop_target_x - tab_width / 2;
+  } else {
+    return G_SOURCE_CONTINUE;
+  }
+
+  g_object_get (self->adjustment,
+                "value", &value,
+                "lower", &lower,
+                "upper", &upper,
+                "page-size", &page_size,
+                NULL);
+
+  autoscroll_area = (tab_width - OVERLAP) / 2;
+
+  x = CLAMP (x,
+             lower + autoscroll_area,
+             upper - tab_width - autoscroll_area);
+
+  time = gdk_frame_clock_get_frame_time (frame_clock);
+  delta_ms = (time - self->drag_autoscroll_prev_time) / 1000.0;
+
+  start_threshold = value + autoscroll_area;
+  end_threshold = value + page_size - tab_width - autoscroll_area;
+  autoscroll_factor = 0;
+
+  if (x < start_threshold)
+    autoscroll_factor = -(start_threshold - x) / autoscroll_area;
+  else if (x > end_threshold)
+    autoscroll_factor = (x - end_threshold) / autoscroll_area;
+
+  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_autoscroll (HdyTabBox *self)
+{
+  GdkFrameClock *frame_clock;
+
+  if (!self->adjustment)
+    return;
+
+  if (self->drag_autoscroll_cb_id)
+    return;
+
+  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);
+}
+
+static void
+end_autoscroll (HdyTabBox *self)
+{
+  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;
+  }
+}
+
+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);
+
+  start_autoscroll (self);
+  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;
+
+  end_autoscroll (self);
+
+  dest_tab = g_list_nth_data (self->tabs, self->reorder_index);
+
+  if (!self->indirect_reordering) {
+    gint 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, page_reordered_cb, self);
+
+    hdy_tab_view_reorder_page (self->view, self->reordered_tab->page, index);
+
+    g_signal_handlers_unblock_by_func (self->view, page_reordered_cb, self);
+  }
+
+  animate_reordering (self, dest_tab);
+
+  self->continue_reorder = FALSE;
+}
+
+/* FIXME: workaround for https://gitlab.gnome.org/GNOME/gtk/-/issues/3159 */
+static void
+check_stuck_dragging (HdyTabBox *self)
+{
+  if (self->drag_icon) {
+    gboolean ret;
+
+    g_signal_emit_by_name (self, "drag-failed", self->drag_icon->context, GTK_DRAG_RESULT_NO_TARGET, &ret);
+    g_signal_emit_by_name (self, "drag-end", self->drag_icon->context);
+  }
+}
+
+/* Selection */
+
+static void
+reset_focus (HdyTabBox *self)
+{
+  GtkWidget *toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
+
+  gtk_container_set_focus_child (GTK_CONTAINER (self), NULL);
+
+  if (toplevel && GTK_IS_WINDOW (toplevel))
+    gtk_window_set_focus (GTK_WINDOW (toplevel), NULL);
+}
+
+static void
+select_page (HdyTabBox  *self,
+             HdyTabPage *page)
+{
+  if (!page) {
+    self->selected_tab = NULL;
+
+    reset_focus (self);
+
+    return;
+  }
+
+  self->selected_tab = find_info_for_page (self, page);
+
+  if (!self->selected_tab) {
+    if (gtk_container_get_focus_child (GTK_CONTAINER (self)))
+      reset_focus (self);
+
+    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;
+
+  if (!GTK_IS_WIDGET (info->tab)) {
+    info->appear_progress = value;
+    return;
+  }
+
+  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_pointer (&info->appear_animation, hdy_animation_unref);
+}
+
+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);
+
+  hdy_tab_set_page (info->tab, page);
+  hdy_tab_set_inverted (info->tab, self->inverted);
+
+  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
+page_attached_cb (HdyTabBox  *self,
+                  HdyTabPage *page,
+                  gint        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);
+
+  set_tab_resize_mode (self, TAB_RESIZE_NORMAL);
+  force_end_reordering (self);
+
+  info = create_tab_info (self, page);
+
+  gtk_widget_show (GTK_WIDGET (info->tab));
+
+  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,
+                       hdy_ease_out_cubic,
+                       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);
+  else
+    scroll_to_tab_full (self, info, -1, FOCUS_ANIMATION_DURATION, TRUE);
+}
+
+/* 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_pointer (&info->appear_animation, hdy_animation_unref);
+
+  self->tabs = g_list_remove (self->tabs, info);
+
+  if (info->reorder_animation)
+    hdy_animation_stop (info->reorder_animation);
+
+  if (self->reorder_animation)
+    hdy_animation_stop (self->reorder_animation);
+
+  if (self->hovered_tab == info)
+    self->hovered_tab = NULL;
+
+  if (self->pressed_tab == info)
+    self->pressed_tab = NULL;
+
+  if (self->reordered_tab == info)
+    self->reordered_tab = NULL;
+
+  remove_and_free_tab_info (info);
+
+  self->n_tabs--;
+}
+
+static void
+page_detached_cb (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;
+  page_link = page_link->next;
+
+  force_end_reordering (self);
+
+  if (self->hovering && !self->pinned) {
+    gboolean is_last = TRUE;
+
+    while (page_link) {
+      TabInfo *i = page_link->data;
+      page_link = page_link->next;
+
+      if (i->page) {
+        is_last = FALSE;
+        break;
+      }
+    }
+
+    if (is_last)
+      set_tab_resize_mode (self, self->inverted ? TAB_RESIZE_NORMAL : TAB_RESIZE_FIXED_END_PADDING);
+    else
+      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;
+
+  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,
+                       hdy_ease_out_cubic,
+                       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, FALSE);
+
+    if ((x <= end && !is_rtl) || (x >= end && is_rtl))
+      break;
+
+    pos += tab_width + (is_rtl ? OVERLAP : -OVERLAP);
+    i++;
+  }
+
+  return i;
+}
+
+static void
+insert_animation_value_cb (gdouble  value,
+                           gpointer user_data)
+{
+  TabInfo *info = user_data;
+  HdyTabBox *self = HDY_TAB_BOX (gtk_widget_get_parent (GTK_WIDGET (info->tab)));
+
+  appear_animation_value_cb (value, info);
+
+  update_dragging (self);
+}
+
+static void
+insert_placeholder (HdyTabBox  *self,
+                    HdyTabPage *page,
+                    gint        pos)
+{
+  TabInfo *info = self->reorder_placeholder;
+  gdouble initial_progress = 0;
+
+  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);
+
+    hdy_tab_set_dragging (info->tab, TRUE);
+    hdy_tab_set_hovering (info->tab, TRUE);
+
+    info->reorder_ignore_bounds = TRUE;
+
+    if (self->adjustment) {
+      gdouble lower, upper, page_size;
+
+      g_object_get (self->adjustment,
+                    "lower", &lower,
+                    "upper", &upper,
+                    "page-size", &page_size,
+                    NULL);
+
+      if (upper - lower > page_size) {
+        gtk_widget_get_preferred_width (GTK_WIDGET (info->tab), NULL,
+                                        &self->placeholder_scroll_offset);
+
+        self->placeholder_scroll_offset /= 2;
+      } else {
+        self->placeholder_scroll_offset = 0;
+      }
+    }
+
+    index = calculate_placeholder_index (self, pos + self->placeholder_scroll_offset);
+
+    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);
+
+    animate_scroll_relative (self, self->placeholder_scroll_offset, OPEN_ANIMATION_DURATION);
+  }
+
+  info->appear_animation =
+    hdy_animation_new (GTK_WIDGET (self), initial_progress, 1,
+                       OPEN_ANIMATION_DURATION,
+                       hdy_ease_out_cubic,
+                       insert_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_pointer (&info->appear_animation, hdy_animation_unref);
+  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;
+
+  self->placeholder_scroll_offset = 0;
+  gtk_widget_show (GTK_WIDGET (self->reorder_placeholder->tab));
+  hdy_tab_set_dragging (info->tab, FALSE);
+
+  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,
+                       hdy_ease_out_cubic,
+                       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);
+
+  g_clear_pointer (&info->appear_animation, hdy_animation_unref);
+
+  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;
+  }
+
+  if (self->hovered_tab == info)
+    self->hovered_tab = NULL;
+
+  if (self->pressed_tab == info)
+    self->pressed_tab = NULL;
+
+  self->tabs = g_list_remove (self->tabs, info);
+
+  remove_and_free_tab_info (info);
+
+  self->n_tabs--;
+
+  self->reorder_placeholder = NULL;
+}
+
+static gboolean
+remove_placeholder_scroll_cb (HdyTabBox *self)
+{
+  animate_scroll_relative (self, -self->placeholder_scroll_offset, CLOSE_ANIMATION_DURATION);
+  self->placeholder_scroll_offset = 0;
+
+  return G_SOURCE_REMOVE;
+}
+
+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);
+
+  g_idle_add ((GSourceFunc) remove_placeholder_scroll_cb, self);
+
+  info->appear_animation =
+    hdy_animation_new (GTK_WIDGET (self), info->appear_progress, 0,
+                       CLOSE_ANIMATION_DURATION,
+                       hdy_ease_out_cubic,
+                       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 (!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;
+  gint offset;
+
+  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;
+  offset = (self->pinned ? 0 : hdy_tab_view_get_n_pinned_pages (self->view));
+
+  if (self->reorder_placeholder) {
+    replace_placeholder (self, page);
+    end_dragging (self);
+
+    g_signal_handlers_block_by_func (self->view, page_attached_cb, self);
+
+    hdy_tab_view_attach_page (self->view, page, self->reorder_index + offset);
+
+    g_signal_handlers_unblock_by_func (self->view, page_attached_cb, self);
+  } else {
+    hdy_tab_view_attach_page (self->view, page, self->reorder_index + offset);
+  }
+
+  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)
+{
+  HdyTabPage *page;
+  HdyTabView *new_view;
+
+  page = self->detached_page;
+
+  new_view = hdy_tab_view_create_window (self->view);
+
+  if (HDY_IS_TAB_VIEW (new_view))
+    hdy_tab_view_attach_page (new_view, page, 0);
+  else
+    hdy_tab_view_attach_page (self->view, page, self->detached_index);
+
+  self->should_detach_into_new_window = FALSE;
+}
+
+static gboolean
+is_view_in_the_same_group (HdyTabBox  *self,
+                           HdyTabView *other_view)
+{
+  /* TODO when we have groups, this should do the actual check */
+  return TRUE;
+}
+
+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;
+
+  gtk_window_set_screen (GTK_WINDOW (icon->window),
+                         gtk_widget_get_screen (GTK_WIDGET (self)));
+
+  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);
+  hdy_tab_set_page (icon->tab, self->reordered_tab->page);
+  hdy_tab_set_dragging (icon->tab, TRUE);
+  hdy_tab_set_inverted (icon->tab, self->inverted);
+  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);
+
+  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_pointer (&icon->resize_animation, hdy_animation_unref);
+}
+
+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,
+                       hdy_ease_out_cubic,
+                       icon_resize_animation_value_cb,
+                       icon_resize_animation_done_cb,
+                       icon);
+
+  hdy_animation_start (icon->resize_animation);
+}
+
+/* Context menu */
+
+static gboolean
+reset_setup_menu_cb (HdyTabBox *self)
+{
+  g_signal_emit_by_name (self->view, "setup-menu", NULL);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+touch_menu_notify_visible_cb (HdyTabBox *self)
+{
+  if (!self->touch_menu || gtk_widget_get_visible (GTK_WIDGET (self->touch_menu)))
+    return;
+
+  g_idle_add ((GSourceFunc) reset_setup_menu_cb, self);
+}
+
+static void
+destroy_cb (HdyTabBox *self)
+{
+  self->touch_menu = NULL;
+}
+
+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;
+
+  g_signal_emit_by_name (self->view, "setup-menu", info->page);
+
+  if (!self->touch_menu) {
+    self->touch_menu = GTK_POPOVER (gtk_popover_new_from_model (GTK_WIDGET (info->tab), model));
+
+    g_signal_connect_object (self->touch_menu, "notify::visible",
+                             G_CALLBACK (touch_menu_notify_visible_cb), self,
+                             G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+
+    g_signal_connect_object (self->touch_menu, "destroy",
+                             G_CALLBACK (destroy_cb), self,
+                             G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+  } 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)
+{
+  end_dragging (self);
+
+  if (self->pressed_tab && self->pressed_tab->page) {
+    do_touch_popup (self, self->pressed_tab);
+    gtk_gesture_set_state (self->touch_menu_gesture,
+                           GTK_EVENT_SEQUENCE_CLAIMED);
+  }
+
+  self->pressed = FALSE;
+  self->pressed_tab = NULL;
+}
+
+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);
+
+  g_idle_add ((GSourceFunc) reset_setup_menu_cb, 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;
+
+  g_signal_emit_by_name (self->view, "setup-menu", info->page);
+
+  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 {
+    GdkRectangle rect;
+
+    rect.x = info->pos;
+    rect.y = gtk_widget_get_allocated_height (GTK_WIDGET (info->tab));
+    rect.width = 0;
+    rect.height = 0;
+
+    if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+      rect.x += info->width;
+
+    gtk_menu_popup_at_rect (self->context_menu,
+                            gtk_widget_get_window (GTK_WIDGET (self)),
+                            &rect,
+                            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;
+
+      gtk_widget_get_preferred_width (GTK_WIDGET (info->tab), NULL,
+                                      &child_width);
+
+      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;
+  GList *l;
+  GtkAllocation child_allocation;
+  gint pos;
+
+  is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
+
+  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;
+      gint child_width;
+
+      gtk_widget_get_preferred_width (GTK_WIDGET (info->tab), NULL, &child_width);
+
+      info->width = calculate_tab_width (info, child_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, FALSE);
+    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 && info != self->reorder_placeholder)
+      hdy_tab_set_display_width (info->tab, predict_tab_width (self, info, FALSE));
+
+    info->pos = pos + calculate_tab_offset (self, info, FALSE);
+
+    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 (self->scheduled_scroll.info) {
+    scroll_to_tab_full (self,
+                        self->scheduled_scroll.info,
+                        self->scheduled_scroll.pos,
+                        self->scheduled_scroll.duration,
+                        self->scheduled_scroll.keep_selected_visible);
+    self->scheduled_scroll.info = NULL;
+  }
+
+  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_pointer (&self->scroll_animation, hdy_animation_unref);
+    }
+  }
+
+  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_upper (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;
+
+  /* enter-notify never happens on touch, so we don't need to check it */
+  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);
+  GdkDevice *source_device;
+  GdkInputSource input_source;
+
+  source_device = gdk_event_get_source_device ((GdkEvent *) event);
+  input_source = gdk_device_get_source (source_device);
+
+  if (input_source != GDK_SOURCE_TOUCHSCREEN)
+    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->pressed_tab &&
+      self->pressed_button == GDK_BUTTON_PRIMARY &&
+      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->pressed_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->pressed_tab &&
+      self->pressed_tab != self->reorder_placeholder &&
+      self->pressed_tab->page &&
+      input_source != GDK_SOURCE_TOUCHSCREEN &&
+      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;
+
+  check_stuck_dragging (self);
+
+  get_widget_coordinates (self, (GdkEvent *) event, &self->hover_x, &self->hover_y);
+
+  update_hover (self);
+
+  self->pressed_tab = find_tab_info_at (self, self->hover_x);
+  self->pressed = TRUE;
+
+  if (!self->pressed_tab || !self->pressed_tab->page)
+      return GDK_EVENT_PROPAGATE;
+
+  if (gdk_event_triggers_context_menu ((GdkEvent *) event)) {
+    do_popup (self, self->pressed_tab, (GdkEvent *) event);
+
+    return GDK_EVENT_STOP;
+  }
+
+  self->pressed_button = event->button;
+
+  if (self->pressed_button == GDK_BUTTON_MIDDLE) {
+    hdy_tab_view_close_page (self->view, self->pressed_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->pressed_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->pressed_tab->width - OVERLAP > value + page_size) {
+      scroll_to_tab (self, self->pressed_tab, SCROLL_ANIMATION_DURATION);
+
+      return GDK_EVENT_PROPAGATE;
+    }
+  }
+
+  can_grab_focus = hdy_tab_bar_tabs_have_visible_focus (self->tab_bar);
+
+  if (self->pressed_tab == self->selected_tab)
+    can_grab_focus = TRUE;
+  else
+    hdy_tab_view_set_selected_page (self->view, self->pressed_tab->page);
+
+  if (can_grab_focus)
+    gtk_widget_grab_focus (GTK_WIDGET (self->pressed_tab->tab));
+  else
+    activate_tab (self);
+
+  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->pressed_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_PROPAGATE;
+}
+
+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_info;
+  HdyTab *detached_tab;
+
+  if (self->pinned)
+    return;
+
+  create_drag_icon (self, context);
+
+  self->hovering = TRUE;
+  self->pressed = FALSE;
+  self->pressed_button = 0;
+  self->pressed_tab = NULL;
+
+  detached_info = self->reordered_tab;
+  detached_tab = g_object_ref (detached_info->tab);
+  self->detached_page = detached_info->page;
+
+  self->indirect_reordering = TRUE;
+
+  end_dragging (self);
+  update_hover (self);
+
+  gtk_widget_hide (GTK_WIDGET (detached_tab));
+  self->detached_index = hdy_tab_view_get_page_position (self->view, self->detached_page);
+
+  hdy_tab_view_detach_page (self->view, self->detached_page);
+
+  self->indirect_reordering = FALSE;
+
+  gtk_widget_get_preferred_width (GTK_WIDGET (detached_tab), NULL, &self->placeholder_scroll_offset);
+  self->placeholder_scroll_offset /= 2;
+
+  animate_scroll_relative (self, -self->placeholder_scroll_offset, CLOSE_ANIMATION_DURATION);
+
+  g_object_unref (detached_tab);
+}
+
+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);
+}
+
+static gboolean
+reset_drop_target_tab_cb (HdyTabBox *self)
+{
+  self->reset_drop_target_tab_id = 0;
+  set_drop_target_tab (self, NULL, FALSE);
+
+  return G_SOURCE_REMOVE;
+}
+
+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;
+
+  target = gtk_drag_dest_find_target (GTK_WIDGET (self), context, NULL);
+  tab_target = gdk_atom_intern_static_string ("HDY_TAB");
+
+  if (target != tab_target) {
+    GdkAtom none_target = gdk_atom_intern_static_string ("NONE");
+    TabInfo *info = find_tab_info_at (self, x);
+
+    if (!info) {
+      if (!self->reset_drop_target_tab_id)
+        self->reset_drop_target_tab_id =
+          g_idle_add ((GSourceFunc) reset_drop_target_tab_cb, self);
+
+      end_autoscroll (self);
+
+      gdk_drag_status (context, 0, time);
+
+      return GDK_EVENT_STOP;
+    }
+
+    self->drop_target_x = x;
+    self->can_accept_drop = target != none_target;
+    set_drop_target_tab (self, info, self->can_accept_drop);
+
+    start_autoscroll (self);
+
+    return GDK_EVENT_STOP;
+  }
+
+  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->can_remove_placeholder = FALSE;
+
+  if (!self->reorder_placeholder || !self->reorder_placeholder->page) {
+    HdyTabPage *page = source_tab_box->detached_page;
+    gdouble center = x - source_tab_box->drag_icon->hotspot_x + source_tab_box->drag_icon->width / 2;
+
+    insert_placeholder (self, page, center);
+
+    self->indirect_reordering = TRUE;
+
+    resize_drag_icon (source_tab_box, predict_tab_width (self, self->reorder_placeholder, TRUE));
+    hdy_tab_set_display_width (self->reorder_placeholder->tab, source_tab_box->drag_icon->target_width);
+    hdy_tab_set_inverted (source_tab_box->drag_icon->tab, self->inverted);
+
+    self->drag_offset_x = source_tab_box->drag_icon->hotspot_x;
+    self->drag_offset_y = source_tab_box->drag_icon->hotspot_y;
+
+    self->reorder_x = (gint) round (x - source_tab_box->drag_icon->hotspot_x);
+
+    start_dragging (self, gtk_get_current_event (), self->reorder_placeholder);
+
+    gdk_drag_status (context, GDK_ACTION_MOVE, time);
+
+    return GDK_EVENT_STOP;
+  }
+
+  self->reorder_x = (gint) round (x - source_tab_box->drag_icon->hotspot_x);
+
+  update_dragging (self);
+
+  gdk_drag_status (context, GDK_ACTION_MOVE, time);
+
+  return GDK_EVENT_STOP;
+}
+
+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;
+
+  target = gtk_drag_dest_find_target (GTK_WIDGET (self), context, NULL);
+  tab_target = gdk_atom_intern_static_string ("HDY_TAB");
+
+  if (target != tab_target) {
+    if (!self->reset_drop_target_tab_id)
+      self->reset_drop_target_tab_id =
+        g_idle_add ((GSourceFunc) reset_drop_target_tab_cb, self);
+
+    end_autoscroll (self);
+
+    return;
+  }
+
+  if (!self->indirect_reordering)
+    return;
+
+  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;
+
+  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;
+  GdkAtom target, tab_target;
+
+  target = gtk_drag_dest_find_target (GTK_WIDGET (self), context, NULL);
+  tab_target = gdk_atom_intern_static_string ("HDY_TAB");
+
+  if (target != tab_target) {
+    g_clear_handle_id (&self->reset_drop_target_tab_id, g_source_remove);
+
+    gtk_drag_get_data (widget, context, target, time);
+
+    return GDK_EVENT_STOP;
+  }
+
+  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_drag_data_received (GtkWidget        *widget,
+                                GdkDragContext   *context,
+                                int               x,
+                                int               y,
+                                GtkSelectionData *selection_data,
+                                guint             info,
+                                guint             time)
+{
+  HdyTabBox *self = HDY_TAB_BOX (widget);
+  TabInfo *tab_info = find_tab_info_at (self, x);
+
+  g_assert (tab_info);
+
+  g_signal_emit (self, signals[SIGNAL_EXTRA_DRAG_DATA_RECEIVED], 0,
+                 tab_info->page,
+                 context, selection_data, info, time);
+
+  set_drop_target_tab (self, NULL, FALSE);
+}
+
+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);
+
+  g_clear_handle_id (&self->drop_switch_timeout_id, g_source_remove);
+
+  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_clear_pointer (&self->source_targets, gtk_target_list_unref);
+
+  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;
+
+  case PROP_RESIZE_FROZEN:
+    g_value_set_boolean (value, self->tab_resize_mode != TAB_RESIZE_NORMAL);
+    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;
+  widget_class->drag_data_received = hdy_tab_box_drag_data_received;
+
+  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);
+
+  props[PROP_RESIZE_FROZEN] =
+    g_param_spec_boolean ("resize-frozen",
+                          _("Resize Frozen"),
+                          _("Resize Frozen"),
+                          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_EXTRA_DRAG_DATA_RECEIVED] =
+    g_signal_new ("extra-drag-data-received",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  5,
+                  HDY_TYPE_TAB_PAGE,
+                  GDK_TYPE_DRAG_CONTEXT,
+                  GTK_TYPE_SELECTION_DATA,
+                  G_TYPE_UINT,
+                  G_TYPE_UINT);
+
+  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);
+
+  g_signal_override_class_handler ("activate-tab",
+                                   G_TYPE_FROM_CLASS (klass),
+                                   G_CALLBACK (activate_tab));
+
+  g_signal_override_class_handler ("focus-tab",
+                                   G_TYPE_FROM_CLASS (klass),
+                                   G_CALLBACK (focus_tab_cb));
+
+  g_signal_override_class_handler ("reorder-tab",
+                                   G_TYPE_FROM_CLASS (klass),
+                                   G_CALLBACK (reorder_tab_cb));
+
+  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;
+  self->expand_tabs = 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_CAPTURE,
+                                           "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),
+                     0,
+                     dst_targets, G_N_ELEMENTS (dst_targets),
+                     GDK_ACTION_MOVE |
+                     GDK_ACTION_COPY |
+                     GDK_ACTION_LINK |
+                     GDK_ACTION_ASK |
+                     GDK_ACTION_PRIVATE);
+  gtk_drag_dest_set_track_motion (GTK_WIDGET (self), TRUE);
+
+  self->source_targets = gtk_target_list_new (src_targets,
+                                              G_N_ELEMENTS (src_targets));
+}
+
+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, page_attached_cb, self);
+    g_signal_handlers_disconnect_by_func (self->view, page_detached_cb, self);
+    g_signal_handlers_disconnect_by_func (self->view, page_reordered_cb, 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;
+  }
+
+  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--)
+      page_attached_cb (self, hdy_tab_view_get_nth_page (self->view, i), 0);
+
+    g_signal_connect_object (self->view, "page-attached", G_CALLBACK (page_attached_cb), self, 
G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->view, "page-detached", G_CALLBACK (page_detached_cb), self, 
G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->view, "page-reordered", G_CALLBACK (page_reordered_cb), 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);
+  }
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+  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)
+{
+  g_return_if_fail (HDY_IS_TAB_BOX (self));
+
+  self->block_scrolling = block_scrolling;
+}
+
+void
+hdy_tab_box_attach_page (HdyTabBox  *self,
+                         HdyTabPage *page,
+                         gint        position)
+{
+  g_return_if_fail (HDY_IS_TAB_BOX (self));
+  g_return_if_fail (HDY_IS_TAB_PAGE (page));
+
+  page_attached_cb (self, page, position);
+}
+
+void
+hdy_tab_box_detach_page (HdyTabBox  *self,
+                         HdyTabPage *page)
+{
+  g_return_if_fail (HDY_IS_TAB_BOX (self));
+  g_return_if_fail (HDY_IS_TAB_PAGE (page));
+
+  page_detached_cb (self, page);
+}
+
+void
+hdy_tab_box_select_page (HdyTabBox  *self,
+                         HdyTabPage *page)
+{
+  g_return_if_fail (HDY_IS_TAB_BOX (self));
+  g_return_if_fail (HDY_IS_TAB_PAGE (page) || page == NULL);
+
+  select_page (self, page);
+}
+
+void
+hdy_tab_box_try_focus_selected_tab (HdyTabBox *self)
+{
+  g_return_if_fail (HDY_IS_TAB_BOX (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;
+
+  g_return_val_if_fail (HDY_IS_TAB_BOX (self), FALSE);
+  g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+  info = find_info_for_page (self, page);
+
+  return info && gtk_widget_is_focus (GTK_WIDGET (info->tab));
+}
+
+void
+hdy_tab_box_set_extra_drag_dest_targets (HdyTabBox     *self,
+                                         GtkTargetList *extra_drag_dest_targets)
+{
+  GtkTargetList *list;
+  GtkTargetEntry *table;
+  gint n_targets;
+
+  g_return_if_fail (HDY_IS_TAB_BOX (self));
+
+  list = gtk_target_list_new (NULL, 0);
+  table = gtk_target_table_new_from_list (extra_drag_dest_targets, &n_targets);
+
+  gtk_target_list_add_table (list, dst_targets, G_N_ELEMENTS (dst_targets));
+  gtk_target_list_add_table (list, table, n_targets);
+
+  gtk_drag_dest_set_target_list (GTK_WIDGET (self), list);
+
+  gtk_target_list_unref (list);
+  gtk_target_table_free (table, n_targets);
+}
+
+gboolean
+hdy_tab_box_get_expand_tabs (HdyTabBox *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_BOX (self), FALSE);
+
+  return self->expand_tabs;
+}
+
+void
+hdy_tab_box_set_expand_tabs (HdyTabBox *self,
+                             gboolean   expand_tabs)
+{
+  g_return_if_fail (HDY_IS_TAB_BOX (self));
+
+  expand_tabs = !!expand_tabs;
+
+  if (expand_tabs == self->expand_tabs)
+    return;
+
+  self->expand_tabs = expand_tabs;
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+gboolean
+hdy_tab_box_get_inverted (HdyTabBox *self)
+{
+  g_return_val_if_fail (HDY_IS_TAB_BOX (self), FALSE);
+
+  return self->inverted;
+}
+
+void
+hdy_tab_box_set_inverted (HdyTabBox *self,
+                          gboolean   inverted)
+{
+  GList *l;
+
+  g_return_if_fail (HDY_IS_TAB_BOX (self));
+
+  inverted = !!inverted;
+
+  if (inverted == self->inverted)
+    return;
+
+  self->inverted = inverted;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    hdy_tab_set_inverted (info->tab, inverted);
+  }
+}
diff --git a/src/meson.build b/src/meson.build
index 7f916f98..5d149b77 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -157,6 +157,7 @@ src_sources = [
   'hdy-swipe-tracker.c',
   'hdy-swipeable.c',
   'hdy-tab.c',
+  'hdy-tab-box.c',
   'hdy-tab-view.c',
   'hdy-title-bar.c',
   'hdy-value-object.c',


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