[libadwaita/wip/exalm/tab-overview: 3/15] Extract AdwTabBox internals into AdwTabListBase




commit 66a6bf0591c1b41ee0880acb309b4596bbdd4704
Author: Alexander Mikhaylenko <alexm gnome org>
Date:   Fri Aug 20 22:55:00 2021 +0500

    Extract AdwTabBox internals into AdwTabListBase
    
    We're gonna have a grid overview, would be nice to reuse AdwTabBox there so
    we can share things like DnD.

 src/adw-tab-bar.c               |   42 +-
 src/adw-tab-box-private.h       |   32 +-
 src/adw-tab-box.c               | 3653 +------------------------------------
 src/adw-tab-list-base-private.h |   60 +
 src/adw-tab-list-base.c         | 3821 +++++++++++++++++++++++++++++++++++++++
 src/meson.build                 |    1 +
 6 files changed, 3909 insertions(+), 3700 deletions(-)
---
diff --git a/src/adw-tab-bar.c b/src/adw-tab-bar.c
index 7c9d5f44..6a67c2f4 100644
--- a/src/adw-tab-bar.c
+++ b/src/adw-tab-bar.c
@@ -44,10 +44,10 @@ struct _AdwTabBar
   AdwBin *start_action_bin;
   AdwBin *end_action_bin;
 
-  AdwTabBox *box;
+  AdwTabListBase *box;
   GtkScrolledWindow *scrolled_window;
 
-  AdwTabBox *pinned_box;
+  AdwTabListBase *pinned_box;
   GtkScrolledWindow *pinned_scrolled_window;
 
   AdwTabView *view;
@@ -131,11 +131,11 @@ notify_selected_page_cb (AdwTabBar *self)
     return;
 
   if (adw_tab_page_get_pinned (page)) {
-    adw_tab_box_select_page (self->pinned_box, page);
-    adw_tab_box_select_page (self->box, page);
+    adw_tab_list_base_select_page (self->pinned_box, page);
+    adw_tab_list_base_select_page (self->box, page);
   } else {
-    adw_tab_box_select_page (self->box, page);
-    adw_tab_box_select_page (self->pinned_box, page);
+    adw_tab_list_base_select_page (self->box, page);
+    adw_tab_list_base_select_page (self->pinned_box, page);
   }
 }
 
@@ -144,7 +144,7 @@ notify_pinned_cb (AdwTabPage *page,
                   GParamSpec *pspec,
                   AdwTabBar  *self)
 {
-  AdwTabBox *from, *to;
+  AdwTabListBase *from, *to;
   gboolean should_focus;
 
   if (adw_tab_page_get_pinned (page)) {
@@ -155,13 +155,13 @@ notify_pinned_cb (AdwTabPage *page,
     to = self->box;
   }
 
-  should_focus = adw_tab_box_is_page_focused (from, page);
+  should_focus = adw_tab_list_base_is_page_focused (from, page);
 
-  adw_tab_box_detach_page (from, page);
-  adw_tab_box_attach_page (to, page, adw_tab_view_get_n_pinned_pages (self->view));
+  adw_tab_list_base_detach_page (from, page);
+  adw_tab_list_base_attach_page (to, page, adw_tab_view_get_n_pinned_pages (self->view));
 
   if (should_focus)
-    adw_tab_box_try_focus_selected_tab (to);
+    adw_tab_list_base_try_focus_selected_tab (to);
 }
 
 static void
@@ -747,8 +747,8 @@ adw_tab_bar_set_view (AdwTabBar  *self,
     for (i = 0; i < n; i++)
       page_detached_cb (self, adw_tab_view_get_nth_page (self->view, i), i);
 
-    adw_tab_box_set_view (self->pinned_box, NULL);
-    adw_tab_box_set_view (self->box, NULL);
+    adw_tab_list_base_set_view (self->pinned_box, NULL);
+    adw_tab_list_base_set_view (self->box, NULL);
   }
 
   g_set_object (&self->view, view);
@@ -756,8 +756,8 @@ adw_tab_bar_set_view (AdwTabBar  *self,
   if (self->view) {
     int i, n;
 
-    adw_tab_box_set_view (self->pinned_box, view);
-    adw_tab_box_set_view (self->box, view);
+    adw_tab_list_base_set_view (self->pinned_box, view);
+    adw_tab_list_base_set_view (self->box, view);
 
     g_signal_connect_object (self->view, "notify::is-transferring-page",
                              G_CALLBACK (update_autohide_cb), self,
@@ -964,7 +964,7 @@ adw_tab_bar_get_expand_tabs (AdwTabBar *self)
 {
   g_return_val_if_fail (ADW_IS_TAB_BAR (self), FALSE);
 
-  return adw_tab_box_get_expand_tabs (self->box);
+  return adw_tab_list_base_get_expand_tabs (self->box);
 }
 
 /**
@@ -987,7 +987,7 @@ adw_tab_bar_set_expand_tabs (AdwTabBar *self,
   if (adw_tab_bar_get_expand_tabs (self) == expand_tabs)
     return;
 
-  adw_tab_box_set_expand_tabs (self->box, expand_tabs);
+  adw_tab_list_base_set_expand_tabs (self->box, expand_tabs);
 
   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_EXPAND_TABS]);
 }
@@ -1007,7 +1007,7 @@ adw_tab_bar_get_inverted (AdwTabBar *self)
 {
   g_return_val_if_fail (ADW_IS_TAB_BAR (self), FALSE);
 
-  return adw_tab_box_get_inverted (self->box);
+  return adw_tab_list_base_get_inverted (self->box);
 }
 
 /**
@@ -1030,7 +1030,7 @@ adw_tab_bar_set_inverted (AdwTabBar *self,
   if (adw_tab_bar_get_inverted (self) == inverted)
     return;
 
-  adw_tab_box_set_inverted (self->box, inverted);
+  adw_tab_list_base_set_inverted (self->box, inverted);
 
   g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INVERTED]);
 }
@@ -1067,8 +1067,8 @@ adw_tab_bar_setup_extra_drop_target (AdwTabBar     *self,
   g_return_if_fail (ADW_IS_TAB_BAR (self));
   g_return_if_fail (n_types == 0 || types != NULL);
 
-  adw_tab_box_setup_extra_drop_target (self->box, actions, types, n_types);
-  adw_tab_box_setup_extra_drop_target (self->pinned_box, actions, types, n_types);
+  adw_tab_list_base_setup_extra_drop_target (self->box, actions, types, n_types);
+  adw_tab_list_base_setup_extra_drop_target (self->pinned_box, actions, types, n_types);
 }
 
 /**
diff --git a/src/adw-tab-box-private.h b/src/adw-tab-box-private.h
index 043d762b..8c852e44 100644
--- a/src/adw-tab-box-private.h
+++ b/src/adw-tab-box-private.h
@@ -13,40 +13,12 @@
 #endif
 
 #include <gtk/gtk.h>
-#include "adw-tab-view.h"
+#include "adw-tab-list-base-private.h"
 
 G_BEGIN_DECLS
 
 #define ADW_TYPE_TAB_BOX (adw_tab_box_get_type())
 
-G_DECLARE_FINAL_TYPE (AdwTabBox, adw_tab_box, ADW, TAB_BOX, GtkWidget)
-
-void adw_tab_box_set_view (AdwTabBox  *self,
-                           AdwTabView *view);
-
-void adw_tab_box_attach_page (AdwTabBox  *self,
-                              AdwTabPage *page,
-                              int         position);
-void adw_tab_box_detach_page (AdwTabBox  *self,
-                              AdwTabPage *page);
-void adw_tab_box_select_page (AdwTabBox  *self,
-                              AdwTabPage *page);
-
-void adw_tab_box_try_focus_selected_tab (AdwTabBox  *self);
-gboolean adw_tab_box_is_page_focused    (AdwTabBox  *self,
-                                         AdwTabPage *page);
-
-void adw_tab_box_setup_extra_drop_target (AdwTabBox     *self,
-                                          GdkDragAction  actions,
-                                          GType         *types,
-                                          gsize          n_types);
-
-gboolean adw_tab_box_get_expand_tabs (AdwTabBox *self);
-void     adw_tab_box_set_expand_tabs (AdwTabBox *self,
-                                      gboolean   expand_tabs);
-
-gboolean adw_tab_box_get_inverted (AdwTabBox *self);
-void     adw_tab_box_set_inverted (AdwTabBox *self,
-                                   gboolean   inverted);
+G_DECLARE_FINAL_TYPE (AdwTabBox, adw_tab_box, ADW, TAB_BOX, AdwTabListBase)
 
 G_END_DECLS
diff --git a/src/adw-tab-box.c b/src/adw-tab-box.c
index 4f070de1..32a86f42 100644
--- a/src/adw-tab-box.c
+++ b/src/adw-tab-box.c
@@ -9,3667 +9,22 @@
 #include "config.h"
 
 #include "adw-tab-box-private.h"
-#include "adw-animation-util-private.h"
-#include "adw-animation-private.h"
-#include "adw-gizmo-private.h"
-#include "adw-tab-private.h"
-#include "adw-tab-bar-private.h"
-#include "adw-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;
-
-typedef struct {
-  GdkDrag *drag;
-
-  AdwTab *tab;
-  GtkBorder tab_margin;
-
-  int hotspot_x;
-  int hotspot_y;
-
-  int width;
-  int target_width;
-  AdwAnimation *resize_animation;
-} DragIcon;
-
-typedef struct {
-  AdwTabPage *page;
-  AdwTab *tab;
-
-  int pos;
-  int width;
-  int last_width;
-
-  double end_reorder_offset;
-  double reorder_offset;
-
-  AdwAnimation *reorder_animation;
-  gboolean reorder_ignore_bounds;
-
-  double appear_progress;
-  AdwAnimation *appear_animation;
-
-  gulong notify_needs_attention_id;
-} TabInfo;
-
-struct _AdwTabBox
-{
-  GtkWidget parent_instance;
-
-  gboolean pinned;
-  AdwTabBar *tab_bar;
-  AdwTabView *view;
-  GtkAdjustment *adjustment;
-  gboolean needs_attention_left;
-  gboolean needs_attention_right;
-  gboolean expand_tabs;
-  gboolean inverted;
-
-  GtkEventController *view_drop_target;
-  GtkGesture *drag_gesture;
-
-  GList *tabs;
-  int n_tabs;
-
-  GtkPopover *context_menu;
-  GtkWidget *background;
-
-  int allocated_width;
-  int last_width;
-  int end_padding;
-  int initial_end_padding;
-  TabResizeMode tab_resize_mode;
-  AdwAnimation *resize_animation;
-
-  TabInfo *selected_tab;
-
-  gboolean hovering;
-  TabInfo *pressed_tab;
-  TabInfo *reordered_tab;
-  AdwAnimation *reorder_animation;
-
-  int reorder_start_pos;
-  int reorder_x;
-  int reorder_y;
-  int reorder_index;
-  int reorder_window_x;
-  gboolean continue_reorder;
-  gboolean indirect_reordering;
-
-  gboolean dragging;
-  double drag_offset_x;
-  double drag_offset_y;
-
-  guint drag_autoscroll_cb_id;
-  gint64 drag_autoscroll_prev_time;
-
-  AdwTabPage *detached_page;
-  int detached_index;
-  TabInfo *reorder_placeholder;
-  AdwTabPage *placeholder_page;
-  int placeholder_scroll_offset;
-  gboolean can_remove_placeholder;
-  DragIcon *drag_icon;
-  gboolean should_detach_into_new_window;
-
-  TabInfo *drop_target_tab;
-  guint drop_switch_timeout_id;
-  guint reset_drop_target_tab_id;
-  double drop_target_x;
-
-  struct {
-    TabInfo *info;
-    int pos;
-    gint64 duration;
-    gboolean keep_selected_visible;
-  } scheduled_scroll;
-
-  AdwAnimation *scroll_animation;
-  gboolean scroll_animation_done;
-  double scroll_animation_from;
-  double scroll_animation_offset;
-  TabInfo *scroll_animation_tab;
-  gboolean block_scrolling;
-  double adjustment_prev_value;
-
-  GdkDragAction extra_drag_actions;
-  GType *extra_drag_types;
-  gsize extra_drag_n_types;
-};
-
-G_DEFINE_TYPE_WITH_CODE (AdwTabBox, adw_tab_box, GTK_TYPE_WIDGET,
-                         G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL))
-
-enum {
-  PROP_0,
-  PROP_PINNED,
-  PROP_TAB_BAR,
-  PROP_VIEW,
-  PROP_NEEDS_ATTENTION_LEFT,
-  PROP_NEEDS_ATTENTION_RIGHT,
-  PROP_RESIZE_FROZEN,
-  PROP_HADJUSTMENT,
-  PROP_VADJUSTMENT,
-  PROP_HSCROLL_POLICY,
-  PROP_VSCROLL_POLICY,
-  LAST_PROP = PROP_HADJUSTMENT
-};
-
-static GParamSpec *props[LAST_PROP];
-
-enum {
-  SIGNAL_STOP_KINETIC_SCROLLING,
-  SIGNAL_EXTRA_DRAG_DROP,
-  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 int
-get_tab_position (AdwTabBox *self,
-                  TabInfo   *info)
-{
-  if (info == self->reordered_tab)
-    return self->reorder_window_x;
-
-  return info->pos;
-}
-
-static inline TabInfo *
-find_tab_info_at (AdwTabBox *self,
-                  double     x)
-{
-  GList *l;
-
-  if (self->reordered_tab) {
-    int 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 (AdwTabBox  *self,
-                    AdwTabPage *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 (AdwTabBox  *self,
-                    AdwTabPage *page)
-{
-  GList *l = find_link_for_page (self, page);
-
-  return l ? l->data : NULL;
-}
-
-static GList *
-find_nth_alive_tab (AdwTabBox *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 int
-calculate_tab_width (TabInfo *info,
-                     int      base_width)
-{
-  return OVERLAP + (int) floor ((base_width - OVERLAP) * info->appear_progress);
-}
-
-static int
-get_base_tab_width (AdwTabBox *self,
-                    gboolean   target)
-{
-  double max_progress = 0;
-  double n = 0;
-  double used_width;
-  GList *l;
-  int 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 = (int) ceil (used_width / n);
-
-  if (!self->expand_tabs)
-    ret = MIN (ret, MAX_TAB_WIDTH_NON_EXPAND + OVERLAP);
-
-  return ret;
-}
-
-static int
-predict_tab_width (AdwTabBox *self,
-                   TabInfo   *info,
-                   gboolean   assume_placeholder)
-{
-  int n;
-  int width = self->allocated_width;
-  int min;
-
-  if (self->pinned)
-    n = adw_tab_view_get_n_pinned_pages (self->view);
-  else
-    n = adw_tab_view_get_n_pages (self->view) - adw_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_measure (GTK_WIDGET (info->tab), GTK_ORIENTATION_HORIZONTAL, -1,
-                      NULL, &min, NULL, NULL);
-
-  if (self->expand_tabs)
-    return MAX ((int) floor (width / (double) n), min);
-  else
-    return CLAMP ((int) floor (width / (double) n), min, MAX_TAB_WIDTH_NON_EXPAND);
-}
-
-static int
-calculate_tab_offset (AdwTabBox *self,
-                      TabInfo   *info,
-                      gboolean target)
-{
-  int width;
-
-  if (!self->reordered_tab)
-      return 0;
-
-  width = (target ? adw_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 (int) round (width * (target ? info->end_reorder_offset : info->reorder_offset));
-}
-
-static void
-get_visible_range (AdwTabBox *self,
-                   int       *lower,
-                   int       *upper)
-{
-  int min = -OVERLAP;
-  int max = self->allocated_width + OVERLAP;
-
-  if (self->pinned) {
-    if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
-      min += OVERLAP;
-    else
-      max -= OVERLAP;
-  }
-
-  if (self->adjustment) {
-    double value = gtk_adjustment_get_value (self->adjustment);
-    double page_size = gtk_adjustment_get_page_size (self->adjustment);
-
-    min = MAX (min, (int) floor (value) - OVERLAP);
-    max = MIN (max, (int) ceil (value + page_size) + OVERLAP);
-  }
-
-  if (lower)
-    *lower = min;
-
-  if (upper)
-    *upper = max;
-}
-
-static inline gboolean
-is_touchscreen (GtkGesture *gesture)
-{
-  GtkEventController *controller = GTK_EVENT_CONTROLLER (gesture);
-  GdkDevice *device = gtk_event_controller_get_current_event_device (controller);
-  GdkInputSource input_source = gdk_device_get_source (device);
-
-  return input_source == GDK_SOURCE_TOUCHSCREEN;
-}
-
-/* Tab resize delay */
-
-static void
-resize_animation_value_cb (double   value,
-                           gpointer user_data)
-{
-  AdwTabBox *self = ADW_TAB_BOX (user_data);
-  double target_end_padding = 0;
-
-  if (!self->expand_tabs) {
-    int 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 = (int) floor (adw_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)
-{
-  AdwTabBox *self = ADW_TAB_BOX (user_data);
-
-  self->end_padding = 0;
-  gtk_widget_queue_resize (GTK_WIDGET (self));
-
-  g_clear_object (&self->resize_animation);
-}
-
-static void
-set_tab_resize_mode (AdwTabBox     *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 = adw_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 =
-      adw_animation_new (GTK_WIDGET (self), 0, 1,
-                         RESIZE_ANIMATION_DURATION,
-                         resize_animation_value_cb,
-                         self);
-
-    g_signal_connect_swapped (self->resize_animation, "done", G_CALLBACK (resize_animation_done_cb), self);
-
-    adw_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 (AdwTabBox *self)
-{
-  if (!self->dragging && !self->hovering)
-    set_tab_resize_mode (self, TAB_RESIZE_NORMAL);
-}
-
-static void
-motion_cb (AdwTabBox          *self,
-           double              x,
-           double              y,
-           GtkEventController *controller)
-{
-  GdkDevice *device = gtk_event_controller_get_current_event_device (controller);
-  GdkInputSource input_source = gdk_device_get_source (device);
-
-  if (input_source == GDK_SOURCE_TOUCHSCREEN)
-    return;
-
-  if (self->hovering)
-    return;
-
-  self->hovering = TRUE;
-
-  update_hover (self);
-}
-
-static void
-leave_cb (AdwTabBox          *self,
-          GtkEventController *controller)
-{
-  self->hovering = FALSE;
-
-  update_hover (self);
-}
-
-/* Keybindings */
-
-static void
-focus_tab_cb (AdwTabBox *self,
-              GVariant  *args)
-{
-  GtkDirectionType direction;
-  gboolean last, is_rtl, success;
-
-  if (!self->view || !self->selected_tab)
-    return;
-
-  g_variant_get (args, "(hb)", &direction, &last);
-
-  is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
-  success = last;
-
-  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 = adw_tab_view_select_first_page (self->view);
-    else
-      success = adw_tab_view_select_previous_page (self->view);
-  } else if (direction == GTK_DIR_TAB_FORWARD) {
-    if (last)
-      success = adw_tab_view_select_last_page (self->view);
-    else
-      success = adw_tab_view_select_next_page (self->view);
-  }
-
-  if (!success)
-    gtk_widget_error_bell (GTK_WIDGET (self));
-}
-
-static void
-reorder_tab_cb (AdwTabBox *self,
-                GVariant  *args)
-{
-  GtkDirectionType direction;
-  gboolean last, is_rtl, success;
-
-  if (!self->view || !self->selected_tab || !self->selected_tab->page)
-    return;
-
-  g_variant_get (args, "(hb)", &direction, &last);
-
-  is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
-  success = last;
-
-  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 = adw_tab_view_reorder_first (self->view, self->selected_tab->page);
-    else
-      success = adw_tab_view_reorder_backward (self->view, self->selected_tab->page);
-  } else if (direction == GTK_DIR_TAB_FORWARD) {
-    if (last)
-      success = adw_tab_view_reorder_last (self->view, self->selected_tab->page);
-    else
-      success = adw_tab_view_reorder_forward (self->view, self->selected_tab->page);
-  }
-
-  if (!success)
-    gtk_widget_error_bell (GTK_WIDGET (self));
-}
-
-static void
-add_focus_bindings (GtkWidgetClass   *widget_class,
-                    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_widget_class_add_binding (widget_class, keysym, 0,
-                                (GtkShortcutFunc) focus_tab_cb,
-                                "(hb)", direction, last);
-  gtk_widget_class_add_binding (widget_class, keypad_keysym, 0,
-                                (GtkShortcutFunc) focus_tab_cb,
-                                "(hb)", direction, last);
-}
-
-static void
-add_reorder_bindings (GtkWidgetClass   *widget_class,
-                      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_widget_class_add_binding (widget_class, keysym, GDK_SHIFT_MASK,
-                                (GtkShortcutFunc) reorder_tab_cb,
-                                "(hb)", direction, last);
-  gtk_widget_class_add_binding (widget_class, keypad_keysym, GDK_SHIFT_MASK,
-                                (GtkShortcutFunc) reorder_tab_cb,
-                                "(hb)", direction, last);
-}
-
-static void
-activate_tab (AdwTabBox *self)
-{
-  GtkWidget *child;
-
-  if (!self->selected_tab || !self->selected_tab->page)
-    return;
-
-  child = adw_tab_page_get_child (self->selected_tab->page);
-
-  gtk_widget_grab_focus (child);
-}
-
-/* Scrolling */
-
-static void
-update_visible (AdwTabBox *self)
-{
-  gboolean left = FALSE, right = FALSE;
-  GList *l;
-  double 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;
-    int pos;
-
-    if (!info->page)
-      continue;
-
-    pos = get_tab_position (self, info);
-
-    adw_tab_set_fully_visible (info->tab,
-                               pos + OVERLAP >= value &&
-                               pos + info->width - OVERLAP <= value + page_size);
-
-    if (!adw_tab_page_get_needs_attention (info->page))
-      continue;
-
-    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 double
-get_scroll_animation_value (AdwTabBox *self)
-{
-  double to, value;
-
-  g_assert (self->scroll_animation);
-
-  to = self->scroll_animation_offset;
-
-  if (self->scroll_animation_tab) {
-    double page_size = gtk_adjustment_get_page_size (self->adjustment);
-
-    to += get_tab_position (self, self->scroll_animation_tab);
-    to = CLAMP (to, 0, self->allocated_width - page_size);
-  }
-
-  value = adw_animation_get_value (self->scroll_animation);
-
-  return round (adw_lerp (self->scroll_animation_from, to, value));
-}
-
-static gboolean
-drop_switch_timeout_cb (AdwTabBox *self)
-{
-  self->drop_switch_timeout_id = 0;
-  adw_tab_view_set_selected_page (self->view,
-                                  self->drop_target_tab->page);
-
-  return G_SOURCE_REMOVE;
-}
-
-static void
-set_drop_target_tab (AdwTabBox *self,
-                     TabInfo   *info)
-{
-  if (self->drop_target_tab == info)
-    return;
-
-  if (self->drop_target_tab)
-    g_clear_handle_id (&self->drop_switch_timeout_id, g_source_remove);
-
-  self->drop_target_tab = info;
-
-  if (self->drop_target_tab) {
-    self->drop_switch_timeout_id =
-      g_timeout_add (DROP_SWITCH_TIMEOUT,
-                     (GSourceFunc) drop_switch_timeout_cb,
-                     self);
-  }
-}
-
-static void
-adjustment_value_changed_cb (AdwTabBox *self)
-{
-  double value = gtk_adjustment_get_value (self->adjustment);
-
-  update_visible (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->adjustment_prev_value = value;
-
-  if (self->block_scrolling)
-      return;
-
-  if (self->scroll_animation)
-    adw_animation_stop (self->scroll_animation);
-
-  gtk_widget_queue_allocate (GTK_WIDGET (self));
-}
-
-static void
-scroll_animation_value_cb (double   value,
-                           gpointer user_data)
-{
-  gtk_widget_queue_resize (GTK_WIDGET (user_data));
-}
-
-static void
-scroll_animation_done_cb (gpointer user_data)
-{
-  AdwTabBox *self = ADW_TAB_BOX (user_data);
-
-  self->scroll_animation_done = TRUE;
-  gtk_widget_queue_resize (GTK_WIDGET (self));
-}
-
-static void
-animate_scroll (AdwTabBox *self,
-                TabInfo   *info,
-                double     offset,
-                gint64     duration)
-{
-  if (!self->adjustment)
-    return;
-
-  g_signal_emit (self, signals[SIGNAL_STOP_KINETIC_SCROLLING], 0);
-
-  if (self->scroll_animation)
-    adw_animation_stop (self->scroll_animation);
-
-  g_clear_object (&self->scroll_animation);
-  self->scroll_animation_done = FALSE;
-  self->scroll_animation_from = gtk_adjustment_get_value (self->adjustment);
-  self->scroll_animation_tab = info;
-  self->scroll_animation_offset = offset;
-
-  /* The actual update will be done in size_allocate(). After the animation
-   * finishes, don't remove it right away, it will be done in size-allocate as
-   * well after one last update, so that we don't miss the last frame.
-   */
-
-  self->scroll_animation =
-    adw_animation_new (GTK_WIDGET (self), 0, 1, duration,
-                       scroll_animation_value_cb,
-                       self);
-
-  g_signal_connect_swapped (self->scroll_animation, "done", G_CALLBACK (scroll_animation_done_cb), self);
-
-  adw_animation_start (self->scroll_animation);
-}
-
-static void
-animate_scroll_relative (AdwTabBox *self,
-                         double     delta,
-                         gint64     duration)
-{
-  double 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 (AdwTabBox *self,
-                    TabInfo   *info,
-                    int        pos,
-                    gint64     duration,
-                    gboolean   keep_selected_visible)
-{
-  int tab_width;
-  double 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 = adw_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 (AdwTabBox *self,
-               TabInfo   *info,
-               gint64     duration)
-{
-  scroll_to_tab_full (self, info, -1, duration, FALSE);
-}
-
-static gboolean
-scroll_cb (AdwTabBox          *self,
-           double              dx,
-           double              dy,
-           GtkEventController *controller)
-{
-  double page_size, pow_unit, scroll_unit;
-  GdkDevice *source_device;
-  GdkInputSource input_source;
-
-  if (!self->adjustment)
-    return GDK_EVENT_PROPAGATE;
-
-  source_device = gtk_event_controller_get_current_event_device (controller);
-  input_source = gdk_device_get_source (source_device);
-
-  if (input_source != GDK_SOURCE_MOUSE)
-    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 (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
-    dy = -dy;
-
-  animate_scroll_relative (self, dy * scroll_unit, SCROLL_ANIMATION_DURATION);
-
-  return GDK_EVENT_STOP;
-}
-
-static void
-set_hadjustment (AdwTabBox     *self,
-                 GtkAdjustment *adjustment)
-{
-  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_visible, 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_visible), self, 
G_CONNECT_SWAPPED);
-  }
-
-  g_object_notify (G_OBJECT (self), "hadjustment");
-}
-
-/* Reordering */
-
-static void
-force_end_reordering (AdwTabBox *self)
-{
-  GList *l;
-
-  if (self->dragging || !self->reordered_tab)
-    return;
-
-  if (self->reorder_animation)
-    adw_animation_stop (self->reorder_animation);
-
-  for (l = self->tabs; l; l = l->next) {
-    TabInfo *info = l->data;
-
-    if (info->reorder_animation)
-      adw_animation_stop (info->reorder_animation);
-  }
-}
-
-static void
-check_end_reordering (AdwTabBox *self)
-{
-  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;
-  }
-
-  self->reordered_tab->reorder_ignore_bounds = FALSE;
-
-  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 (AdwTabBox *self,
-                  TabInfo   *info)
-{
-  self->reordered_tab = info;
-
-  /* The reordered tab should be displayed above everything else */
-  gtk_widget_insert_before (GTK_WIDGET (self->reordered_tab->tab),
-                            GTK_WIDGET (self), NULL);
-
-  gtk_widget_queue_allocate (GTK_WIDGET (self));
-}
-
-static int
-get_reorder_position (AdwTabBox *self)
-{
-  int 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 (double   value,
-                            gpointer user_data)
-{
-  TabInfo *dest_tab = user_data;
-  GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (dest_tab->tab));
-  AdwTabBox *self = ADW_TAB_BOX (parent);
-  gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
-  double 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 = (int) round (adw_lerp (x1, x2, value));
-
-  gtk_widget_queue_allocate (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));
-  AdwTabBox *self = ADW_TAB_BOX (parent);
-
-  g_clear_object (&self->reorder_animation);
-  check_end_reordering (self);
-}
-
-static void
-animate_reordering (AdwTabBox *self,
-                    TabInfo   *dest_tab)
-{
-  if (self->reorder_animation)
-    adw_animation_stop (self->reorder_animation);
-
-  self->reorder_animation =
-    adw_animation_new (GTK_WIDGET (self), 0, 1,
-                       REORDER_ANIMATION_DURATION,
-                       reorder_animation_value_cb,
-                       dest_tab);
-
-  g_signal_connect_swapped (self->reorder_animation, "done", G_CALLBACK (reorder_animation_done_cb), 
dest_tab);
-
-  adw_animation_start (self->reorder_animation);
-
-  check_end_reordering (self);
-}
-
-static void
-reorder_offset_animation_value_cb (double   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));
-  AdwTabBox *self = ADW_TAB_BOX (parent);
-
-  g_clear_object (&info->reorder_animation);
-  check_end_reordering (self);
-}
-
-static void
-animate_reorder_offset (AdwTabBox *self,
-                        TabInfo   *info,
-                        double     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)
-    adw_animation_stop (info->reorder_animation);
-
-  info->reorder_animation =
-    adw_animation_new (GTK_WIDGET (self), info->reorder_offset, offset,
-                       REORDER_ANIMATION_DURATION,
-                       reorder_offset_animation_value_cb,
-                       info);
-
-  g_signal_connect_swapped (info->reorder_animation, "done", G_CALLBACK (reorder_offset_animation_done_cb), 
info);
-
-  adw_animation_start (info->reorder_animation);
-}
-
-static void
-reset_reorder_animations (AdwTabBox *self)
-{
-  int i, original_index;
-  GList *l;
-
-  if (!adw_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 (AdwTabBox  *self,
-                   AdwTabPage *page,
-                   int         index)
-{
-  GList *link;
-  int original_index;
-  TabInfo *info, *dest_tab;
-  gboolean is_rtl;
-
-  if (adw_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);
-
-  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 -= adw_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 (adw_get_enable_animations (GTK_WIDGET (self)) &&
-      gtk_widget_get_mapped (GTK_WIDGET (self))) {
-    int 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
-update_drag_reodering (AdwTabBox *self)
-{
-  gboolean is_rtl, after_selected, found_index;
-  int x;
-  int i = 0;
-  int width;
-  GList *l;
-
-  if (!self->dragging)
-    return;
-
-  x = get_reorder_position (self);
-
-  width = adw_tab_get_display_width (self->reordered_tab->tab);
-
-  self->reorder_window_x = x;
-
-  gtk_widget_queue_allocate (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;
-    int center = info->pos - calculate_tab_offset (self, info, FALSE) + info->width / 2;
-    double 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,
-                    AdwTabBox     *self)
-{
-  double value, page_size;
-  double x, delta_ms, start_threshold, end_threshold, autoscroll_factor;
-  gint64 time;
-  int offset = 0;
-  int tab_width = 0;
-  int autoscroll_area = 0;
-
-  if (self->reordered_tab) {
-    gtk_widget_measure (GTK_WIDGET (self->reordered_tab->tab),
-                        GTK_ORIENTATION_HORIZONTAL, -1,
-                        NULL, &tab_width, NULL, NULL);
-    tab_width -= 2 * OVERLAP;
-    x = (double) self->reorder_x + OVERLAP;
-  } else if (self->drop_target_tab) {
-    gtk_widget_measure (GTK_WIDGET (self->drop_target_tab->tab),
-                        GTK_ORIENTATION_HORIZONTAL, -1,
-                        NULL, &tab_width, NULL, NULL);
-    tab_width -= 2 * OVERLAP;
-    x = (double) self->drop_target_x + OVERLAP - tab_width / 2;
-  } else {
-    return G_SOURCE_CONTINUE;
-  }
-
-  value = gtk_adjustment_get_value (self->adjustment);
-  page_size = gtk_adjustment_get_page_size (self->adjustment);
-  autoscroll_area = tab_width / 2;
-
-  x = CLAMP (x,
-             autoscroll_area,
-             self->allocated_width - 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 = adw_ease_in_cubic (autoscroll_factor);
-  self->drag_autoscroll_prev_time = time;
-
-  if (autoscroll_factor == 0)
-    return G_SOURCE_CONTINUE;
-
-  if (autoscroll_factor > 0)
-    offset = (int) ceil (autoscroll_factor * delta_ms * AUTOSCROLL_SPEED);
-  else
-    offset = (int) floor (autoscroll_factor * delta_ms * AUTOSCROLL_SPEED);
-
-  self->reorder_x += offset;
-  gtk_adjustment_set_value (self->adjustment, value + offset);
-  update_drag_reodering (self);
-
-  return G_SOURCE_CONTINUE;
-}
-
-static void
-start_autoscroll (AdwTabBox *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 (AdwTabBox *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_drag_reodering (AdwTabBox *self,
-                      TabInfo   *info,
-                      double     x,
-                      double     y)
-{
-  if (self->dragging)
-    return;
-
-  if (!info)
-    return;
-
-  self->continue_reorder = info == self->reordered_tab;
-
-  if (self->continue_reorder) {
-    if (self->reorder_animation)
-      adw_animation_stop (self->reorder_animation);
-
-    reset_reorder_animations (self);
-
-    self->reorder_x = (int) round (x - self->drag_offset_x);
-    self->reorder_y = (int) round (y - self->drag_offset_y);
-  } else
-    force_end_reordering (self);
-
-  start_autoscroll (self);
-  self->dragging = TRUE;
-
-  if (!self->continue_reorder)
-    start_reordering (self, info);
-}
-
-static void
-end_drag_reodering (AdwTabBox *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) {
-    int index = self->reorder_index;
-
-    if (!self->pinned)
-      index += adw_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);
-
-    adw_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;
-}
-
-static void
-reorder_begin_cb (AdwTabBox  *self,
-                  double      start_x,
-                  double      start_y,
-                  GtkGesture *gesture)
-{
-  self->reorder_start_pos = gtk_adjustment_get_value (self->adjustment);
-
-  start_x += self->reorder_start_pos;
-
-  self->pressed_tab = find_tab_info_at (self, start_x);
-
-  self->drag_offset_x = start_x - get_tab_position (self, self->pressed_tab);
-  self->drag_offset_y = start_y;
-
-  if (!self->reorder_animation) {
-    self->reorder_x = (int) round (start_x - self->drag_offset_x);
-    self->reorder_y = (int) round (start_y - self->drag_offset_y);
-  }
-}
-
-/* Copied from gtkdragsource.c */
-static gboolean
-gtk_drag_check_threshold_double (GtkWidget *widget,
-                                 double     start_x,
-                                 double     start_y,
-                                 double     current_x,
-                                 double     current_y)
-{
-  int drag_threshold;
-
-  g_object_get (gtk_widget_get_settings (widget),
-                "gtk-dnd-drag-threshold", &drag_threshold,
-                NULL);
-
-  return (ABS (current_x - start_x) > drag_threshold ||
-          ABS (current_y - start_y) > drag_threshold);
-}
-
-static gboolean
-check_dnd_threshold (AdwTabBox *self,
-                     double     x,
-                     double     y)
-{
-  int threshold;
-  graphene_rect_t rect;
-
-  g_object_get (gtk_widget_get_settings (GTK_WIDGET (self)),
-                "gtk-dnd-drag-threshold", &threshold,
-                NULL);
-
-  threshold *= DND_THRESHOLD_MULTIPLIER;
-
-  graphene_rect_init (&rect, 0, 0,
-                      self->allocated_width,
-                      gtk_widget_get_height (GTK_WIDGET (self)));
-  graphene_rect_inset (&rect, -threshold, -threshold);
-
-  return !graphene_rect_contains_point (&rect, &GRAPHENE_POINT_INIT (x, y));
-}
-
-static void begin_drag (AdwTabBox *self,
-                        GdkDevice *device);
-
-static void
-reorder_update_cb (AdwTabBox  *self,
-                   double      offset_x,
-                   double      offset_y,
-                   GtkGesture *gesture)
-{
-  double start_x, start_y, x, y;
-  GdkDevice *device;
-
-  if (!self->pressed_tab) {
-    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
-    return;
-  }
-
-  if (!self->dragging &&
-      !gtk_drag_check_threshold_double (GTK_WIDGET (self), 0, 0,
-                                        offset_x, offset_y))
-    return;
-
-  gtk_gesture_drag_get_start_point (GTK_GESTURE_DRAG (gesture),
-                                    &start_x, &start_y);
-
-  x = start_x + gtk_adjustment_get_value (self->adjustment) + offset_x;
-  y = start_y + offset_y;
-
-  start_drag_reodering (self, self->pressed_tab, x, y);
-
-  if (self->dragging) {
-    adw_tab_view_set_selected_page (self->view, self->pressed_tab->page);
-    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
-  } else {
-    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
-    return;
-  }
-
-  self->reorder_x = (int) round (x - self->drag_offset_x);
-  self->reorder_y = (int) round (y - self->drag_offset_y);
-
-  device = gtk_event_controller_get_current_event_device (GTK_EVENT_CONTROLLER (gesture));
-
-  if (!self->pinned &&
-      self->pressed_tab &&
-      self->pressed_tab != self->reorder_placeholder &&
-      self->pressed_tab->page &&
-      !is_touchscreen (gesture) &&
-      adw_tab_view_get_n_pages (self->view) > 1 &&
-      check_dnd_threshold (self, x, y)) {
-    begin_drag (self, device);
-
-    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
-
-    return;
-  }
-
-  update_drag_reodering (self);
-}
-
-static void
-reorder_end_cb (AdwTabBox  *self,
-                double      offset_x,
-                double      offset_y,
-                GtkGesture *gesture)
-{
-  end_drag_reodering (self);
-}
-
-/* Selection */
-
-static void
-reset_focus (AdwTabBox *self)
-{
-  GtkRoot *root = gtk_widget_get_root (GTK_WIDGET (self));
-
-  gtk_widget_set_focus_child (GTK_WIDGET (self), NULL);
-
-  if (root)
-    gtk_root_set_focus (root, NULL);
-}
-
-static void
-select_page (AdwTabBox  *self,
-             AdwTabPage *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_widget_get_focus_child (GTK_WIDGET (self)))
-      reset_focus (self);
-
-    return;
-  }
-
-  if (adw_tab_bar_tabs_have_visible_focus (self->tab_bar))
-    gtk_widget_grab_focus (GTK_WIDGET (self->selected_tab->tab));
-
-  gtk_widget_set_focus_child (GTK_WIDGET (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 gboolean
-extra_drag_drop_cb (AdwTab    *tab,
-                    GValue    *value,
-                    AdwTabBox *self)
-{
-  gboolean ret = GDK_EVENT_PROPAGATE;
-  AdwTabPage *page = adw_tab_get_page (tab);
-
-  g_signal_emit (self, signals[SIGNAL_EXTRA_DRAG_DROP], 0, page, value, &ret);
-
-  return ret;
-}
-
-static void
-appear_animation_value_cb (double   value,
-                           gpointer user_data)
-{
-  TabInfo *info = user_data;
-
-  info->appear_progress = value;
-
-  if (GTK_IS_WIDGET (info->tab))
-    gtk_widget_queue_resize (GTK_WIDGET (info->tab));
-}
-
-static void
-open_animation_done_cb (gpointer user_data)
-{
-  TabInfo *info = user_data;
-
-  g_clear_object (&info->appear_animation);
-}
-
-static TabInfo *
-create_tab_info (AdwTabBox  *self,
-                 AdwTabPage *page)
-{
-  TabInfo *info;
-
-  info = g_new0 (TabInfo, 1);
-  info->page = page;
-  info->pos = -1;
-  info->width = -1;
-  info->tab = adw_tab_new (self->view, self->pinned);
-
-  adw_tab_set_page (info->tab, page);
-  adw_tab_set_inverted (info->tab, self->inverted);
-  adw_tab_setup_extra_drop_target (info->tab,
-                                   self->extra_drag_actions,
-                                   self->extra_drag_types,
-                                   self->extra_drag_n_types);
-
-  gtk_widget_set_parent (GTK_WIDGET (info->tab), GTK_WIDGET (self));
-
-  g_signal_connect_object (info->tab, "extra-drag-drop", G_CALLBACK (extra_drag_drop_cb), self, 0);
-
-  return info;
-}
-
-static void
-page_attached_cb (AdwTabBox  *self,
-                  AdwTabPage *page,
-                  int         position)
-{
-  TabInfo *info;
-  GList *l;
-
-  if (adw_tab_page_get_pinned (page) != self->pinned)
-    return;
-
-  if (!self->pinned)
-    position -= adw_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);
-
-  info->notify_needs_attention_id =
-    g_signal_connect_object (page,
-                             "notify::needs-attention",
-                             G_CALLBACK (update_visible),
-                             self,
-                             G_CONNECT_SWAPPED);
-
-  info->appear_animation =
-    adw_animation_new (GTK_WIDGET (self), 0, 1,
-                       OPEN_ANIMATION_DURATION,
-                       appear_animation_value_cb,
-                       info);
-
-  g_signal_connect_swapped (info->appear_animation, "done", G_CALLBACK (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++;
-
-  adw_animation_start (info->appear_animation);
-
-  if (page == adw_tab_view_get_selected_page (self->view))
-    adw_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));
-  AdwTabBox *self = ADW_TAB_BOX (parent);
-
-  g_clear_object (&info->appear_animation);
-
-  self->tabs = g_list_remove (self->tabs, info);
-
-  if (info->reorder_animation)
-    adw_animation_stop (info->reorder_animation);
-
-  if (self->reorder_animation)
-    adw_animation_stop (self->reorder_animation);
-
-  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 (AdwTabBox  *self,
-                  AdwTabPage *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)))
-    adw_tab_box_try_focus_selected_tab (self);
-
-  if (info == self->selected_tab)
-    adw_tab_box_select_page (self, NULL);
-
-  adw_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)
-    adw_animation_stop (info->appear_animation);
-
-  info->appear_animation =
-    adw_animation_new (GTK_WIDGET (self), info->appear_progress, 0,
-                       CLOSE_ANIMATION_DURATION,
-                       appear_animation_value_cb,
-                       info);
-
-  g_signal_connect_swapped (info->appear_animation, "done", G_CALLBACK (close_animation_done_cb), info);
-
-  adw_animation_start (info->appear_animation);
-}
-
-/* Tab DND */
-
-#define ADW_TYPE_TAB_BOX_ROOT_CONTENT (adw_tab_box_root_content_get_type ())
-
-G_DECLARE_FINAL_TYPE (AdwTabBoxRootContent, adw_tab_box_root_content, ADW, TAB_BOX_ROOT_CONTENT, 
GdkContentProvider)
-
-struct _AdwTabBoxRootContent
-{
-  GdkContentProvider parent_instance;
-
-  AdwTabBox *tab_box;
-};
-
-G_DEFINE_TYPE (AdwTabBoxRootContent, adw_tab_box_root_content, GDK_TYPE_CONTENT_PROVIDER)
-
-static GdkContentFormats *
-adw_tab_box_root_content_ref_formats (GdkContentProvider *provider)
-{
-  return gdk_content_formats_new ((const char *[1]) { "application/x-rootwindow-drop" }, 1);
-}
-
-static void
-adw_tab_box_root_content_write_mime_type_async (GdkContentProvider  *provider,
-                                                const char          *mime_type,
-                                                GOutputStream       *stream,
-                                                int                  io_priority,
-                                                GCancellable        *cancellable,
-                                                GAsyncReadyCallback  callback,
-                                                gpointer             user_data)
-{
-  AdwTabBoxRootContent *self = ADW_TAB_BOX_ROOT_CONTENT (provider);
-  g_autoptr (GTask) task = NULL;
-
-  self->tab_box->should_detach_into_new_window = TRUE;
-
-  task = g_task_new (self, cancellable, callback, user_data);
-  g_task_set_priority (task, io_priority);
-  g_task_set_source_tag (task, adw_tab_box_root_content_write_mime_type_async);
-  g_task_return_boolean (task, TRUE);
-}
-
-static gboolean
-adw_tab_box_root_content_write_mime_type_finish (GdkContentProvider  *provider,
-                                                 GAsyncResult        *result,
-                                                 GError             **error)
-{
-  return g_task_propagate_boolean (G_TASK (result), error);
-}
-
-static void
-adw_tab_box_root_content_finalize (GObject *object)
-{
-  AdwTabBoxRootContent *self = ADW_TAB_BOX_ROOT_CONTENT (object);
-
-  g_clear_object (&self->tab_box);
-
-  G_OBJECT_CLASS (adw_tab_box_root_content_parent_class)->finalize (object);
-}
-
-static void
-adw_tab_box_root_content_class_init (AdwTabBoxRootContentClass *klass)
-{
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
-  GdkContentProviderClass *provider_class = GDK_CONTENT_PROVIDER_CLASS (klass);
-
-  object_class->finalize = adw_tab_box_root_content_finalize;
-
-  provider_class->ref_formats = adw_tab_box_root_content_ref_formats;
-  provider_class->write_mime_type_async = adw_tab_box_root_content_write_mime_type_async;
-  provider_class->write_mime_type_finish = adw_tab_box_root_content_write_mime_type_finish;
-}
-
-static void
-adw_tab_box_root_content_init (AdwTabBoxRootContent *self)
-{
-}
-
-static GdkContentProvider *
-adw_tab_box_root_content_new (AdwTabBox *tab_box)
-{
-  AdwTabBoxRootContent *self = g_object_new (ADW_TYPE_TAB_BOX_ROOT_CONTENT, NULL);
-
-  self->tab_box = g_object_ref (tab_box);
-
-  return GDK_CONTENT_PROVIDER (self);
-}
-
-static int
-calculate_placeholder_index (AdwTabBox *self,
-                             int        x)
-{
-  int 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 (double   value,
-                           gpointer user_data)
-{
-  TabInfo *info = user_data;
-  AdwTabBox *self = ADW_TAB_BOX (gtk_widget_get_parent (GTK_WIDGET (info->tab)));
-
-  appear_animation_value_cb (value, info);
-
-  update_drag_reodering (self);
-}
-
-static void
-insert_placeholder (AdwTabBox  *self,
-                    AdwTabPage *page,
-                    int         pos)
-{
-  TabInfo *info = self->reorder_placeholder;
-  double initial_progress = 0;
-
-  if (info) {
-    initial_progress = info->appear_progress;
-
-    if (info->appear_animation)
-      adw_animation_stop (info->appear_animation);
-  } else {
-    int index;
-
-    self->placeholder_page = page;
-
-    info = create_tab_info (self, page);
-
-    gtk_widget_set_opacity (GTK_WIDGET (info->tab), 0);
-
-    adw_tab_set_dragging (info->tab, TRUE);
-
-    info->reorder_ignore_bounds = TRUE;
-
-    if (self->adjustment) {
-      double page_size = gtk_adjustment_get_page_size (self->adjustment);
-
-      if (self->allocated_width > page_size) {
-        gtk_widget_measure (GTK_WIDGET (info->tab), GTK_ORIENTATION_HORIZONTAL, -1,
-                            NULL, &self->placeholder_scroll_offset, NULL, NULL);
-
-        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 =
-    adw_animation_new (GTK_WIDGET (self), initial_progress, 1,
-                       OPEN_ANIMATION_DURATION,
-                       insert_animation_value_cb,
-                       info);
-
-  g_signal_connect_swapped (info->appear_animation, "done", G_CALLBACK (open_animation_done_cb), info);
-
-  adw_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));
-  AdwTabBox *self = ADW_TAB_BOX (parent);
-
-  g_clear_object (&info->appear_animation);
-  self->reorder_placeholder = NULL;
-  self->can_remove_placeholder = TRUE;
-}
-
-static void
-replace_placeholder (AdwTabBox  *self,
-                     AdwTabPage *page)
-{
-  TabInfo *info = self->reorder_placeholder;
-  double initial_progress;
-
-  self->placeholder_scroll_offset = 0;
-  gtk_widget_set_opacity (GTK_WIDGET (self->reorder_placeholder->tab), 1);
-  adw_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;
-
-  adw_tab_set_page (info->tab, page);
-  info->page = page;
-
-  adw_animation_stop (info->appear_animation);
-
-  info->appear_animation =
-    adw_animation_new (GTK_WIDGET (self), initial_progress, 1,
-                       OPEN_ANIMATION_DURATION,
-                       appear_animation_value_cb,
-                       info);
-
-  g_signal_connect_swapped (info->appear_animation, "done", G_CALLBACK (replace_animation_done_cb), info);
-
-  adw_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));
-  AdwTabBox *self = ADW_TAB_BOX (parent);
-
-  g_clear_object (&info->appear_animation);
-
-  if (!self->can_remove_placeholder) {
-    adw_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)
-      adw_animation_stop (info->reorder_animation);
-
-    self->reordered_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 (AdwTabBox *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 (AdwTabBox *self)
-{
-  TabInfo *info = self->reorder_placeholder;
-
-  if (!info || !info->page)
-    return;
-
-  adw_tab_set_page (info->tab, NULL);
-  info->page = NULL;
-
-  if (info->appear_animation)
-    adw_animation_stop (info->appear_animation);
-
-  g_idle_add ((GSourceFunc) remove_placeholder_scroll_cb, self);
-
-  info->appear_animation =
-    adw_animation_new (GTK_WIDGET (self), info->appear_progress, 0,
-                       CLOSE_ANIMATION_DURATION,
-                       appear_animation_value_cb,
-                       info);
-
-  g_signal_connect_swapped (info->appear_animation, "done", G_CALLBACK (remove_animation_done_cb), info);
-
-  adw_animation_start (info->appear_animation);
-}
-
-static inline AdwTabBox *
-get_source_tab_box (GtkDropTarget *target)
-{
-  GdkDrop *drop = gtk_drop_target_get_current_drop (target);
-  GdkDrag *drag = gdk_drop_get_drag (drop);
-
-  if (!drag)
-    return NULL;
-
-  return ADW_TAB_BOX (g_object_get_data (G_OBJECT (drag),
-                      "adw-tab-bar-drag-origin"));
-}
-
-static void
-do_drag_drop (AdwTabBox *self,
-              AdwTabBox *source_tab_box)
-{
-  AdwTabPage *page = source_tab_box->detached_page;
-  int offset = (self->pinned ? 0 : adw_tab_view_get_n_pinned_pages (self->view));
-
-  if (self->reorder_placeholder) {
-    replace_placeholder (self, page);
-    end_drag_reodering (self);
-
-    g_signal_handlers_block_by_func (self->view, page_attached_cb, self);
-
-    adw_tab_view_attach_page (self->view, page, self->reorder_index + offset);
-
-    g_signal_handlers_unblock_by_func (self->view, page_attached_cb, self);
-  } else {
-    adw_tab_view_attach_page (self->view, page, self->reorder_index + offset);
-  }
-
-  source_tab_box->should_detach_into_new_window = FALSE;
-  source_tab_box->detached_page = NULL;
-
-  self->indirect_reordering = FALSE;
-}
-
-static void
-detach_into_new_window (AdwTabBox *self)
-{
-  AdwTabPage *page;
-  AdwTabView *new_view;
-
-  page = self->detached_page;
-
-  new_view = adw_tab_view_create_window (self->view);
-
-  if (ADW_IS_TAB_VIEW (new_view))
-    adw_tab_view_attach_page (new_view, page, 0);
-  else
-    adw_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 (AdwTabBox  *self,
-                           AdwTabView *other_view)
-{
-  /* TODO when we have groups, this should do the actual check */
-  return TRUE;
-}
-
-static void
-drag_end (AdwTabBox *self,
-          GdkDrag   *drag,
-          gboolean   success)
-{
-  g_signal_handlers_disconnect_by_data (drag, self);
-
-  gdk_drag_drop_done (drag, success);
-
-  if (!success) {
-    adw_tab_view_attach_page (self->view,
-                              self->detached_page,
-                              self->detached_index);
-
-    self->indirect_reordering = FALSE;
-  }
-
-  self->detached_page = NULL;
-
-  if (self->drag_icon)
-    g_clear_pointer (&self->drag_icon, g_free);
-
-  g_object_unref (drag);
-}
-
-static void
-tab_drop_performed_cb (AdwTabBox *self,
-                       GdkDrag   *drag)
-{
-  /* Catch drops into our windows, but outside of tab views. If this is a false
-   * positive, it will be set to FALSE in do_drag_drop(). */
-  self->should_detach_into_new_window = TRUE;
-}
-
-static void
-tab_dnd_finished_cb (AdwTabBox *self,
-                     GdkDrag   *drag)
-{
-  if (self->should_detach_into_new_window)
-    detach_into_new_window (self);
-
-  drag_end (self, drag, TRUE);
-}
-
-static void
-tab_drag_cancel_cb (AdwTabBox           *self,
-                    GdkDragCancelReason  reason,
-                    GdkDrag             *drag)
-{
-  if (reason == GDK_DRAG_CANCEL_NO_TARGET) {
-    detach_into_new_window (self);
-    drag_end (self, drag, TRUE);
-
-    return;
-  }
-
-  self->should_detach_into_new_window = FALSE;
-  drag_end (self, drag, FALSE);
-}
-
-static void
-create_drag_icon (AdwTabBox *self,
-                  GdkDrag   *drag)
-{
-  DragIcon *icon;
-
-  icon = g_new0 (DragIcon, 1);
-
-  icon->drag = drag;
-
-  icon->width = predict_tab_width (self, self->reordered_tab, FALSE);
-  icon->target_width = icon->width;
-
-  icon->tab = adw_tab_new (self->view, FALSE);
-  adw_tab_set_page (icon->tab, self->reordered_tab->page);
-  adw_tab_set_dragging (icon->tab, TRUE);
-  adw_tab_set_inverted (icon->tab, self->inverted);
-  adw_tab_set_display_width (icon->tab, icon->width);
-  gtk_widget_set_halign (GTK_WIDGET (icon->tab), GTK_ALIGN_START);
-
-  gtk_drag_icon_set_child (GTK_DRAG_ICON (gtk_drag_icon_get_for_drag (drag)),
-                           GTK_WIDGET (icon->tab));
-
-  gtk_style_context_get_margin (gtk_widget_get_style_context (GTK_WIDGET (icon->tab)),
-                                &icon->tab_margin);
-
-  gtk_widget_set_size_request (GTK_WIDGET (icon->tab),
-                               icon->width + icon->tab_margin.left + icon->tab_margin.right,
-                               -1);
-
-  icon->hotspot_x = (int) self->drag_offset_x;
-  icon->hotspot_y = (int) self->drag_offset_y;
-
-  gdk_drag_set_hotspot (drag,
-                        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 (double   value,
-                                gpointer user_data)
-{
-  DragIcon *icon = user_data;
-  double relative_pos;
-
-  relative_pos = (double) icon->hotspot_x / icon->width;
-
-  icon->width = (int) round (value);
-
-  adw_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 = (int) round (icon->width * relative_pos);
-
-  gdk_drag_set_hotspot (icon->drag,
-                        icon->hotspot_x + icon->tab_margin.left,
-                        icon->hotspot_y + icon->tab_margin.top);
-
-  gtk_widget_queue_resize (GTK_WIDGET (icon->tab));
-}
-
-static void
-icon_resize_animation_done_cb (gpointer user_data)
-{
-  DragIcon *icon = user_data;
-
-  g_clear_object (&icon->resize_animation);
-}
-
-static void
-resize_drag_icon (AdwTabBox *self,
-                  int        width)
-{
-  DragIcon *icon = self->drag_icon;
-
-  if (width == icon->target_width)
-    return;
-
-  if (icon->resize_animation)
-    adw_animation_stop (icon->resize_animation);
-
-  icon->target_width = width;
-
-  icon->resize_animation =
-    adw_animation_new (GTK_WIDGET (icon->tab), icon->width, width,
-                       ICON_RESIZE_ANIMATION_DURATION,
-                       icon_resize_animation_value_cb,
-                       icon);
-
-  g_signal_connect_swapped (icon->resize_animation, "done", G_CALLBACK (icon_resize_animation_done_cb), 
icon);
-
-  adw_animation_start (icon->resize_animation);
-}
-
-static void
-begin_drag (AdwTabBox *self,
-            GdkDevice *device)
-{
-  GtkNative *native;
-  GdkSurface *surface;
-  GdkContentProvider *content;
-  GdkDrag *drag;
-  TabInfo *detached_info;
-  AdwTab *detached_tab;
-
-  native = gtk_widget_get_native (GTK_WIDGET (self));
-  surface = gtk_native_get_surface (native);
-
-  self->hovering = TRUE;
-  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;
-
-  content = gdk_content_provider_new_union ((GdkContentProvider *[2]) {
-                                              adw_tab_box_root_content_new (self),
-                                              gdk_content_provider_new_typed (ADW_TYPE_TAB_PAGE, 
detached_info->page)
-                                            }, 2);
-
-  drag = gdk_drag_begin (surface, device, content, GDK_ACTION_MOVE,
-                         self->reorder_x, self->reorder_y);
-
-  g_object_set_data (G_OBJECT (drag), "adw-tab-bar-drag-origin", self);
-
-  g_signal_connect_swapped (drag, "drop-performed",
-                            G_CALLBACK (tab_drop_performed_cb), self);
-  g_signal_connect_swapped (drag, "dnd-finished",
-                            G_CALLBACK (tab_dnd_finished_cb), self);
-  g_signal_connect_swapped (drag, "cancel",
-                            G_CALLBACK (tab_drag_cancel_cb), self);
-
-  create_drag_icon (self, drag);
-
-  end_drag_reodering (self);
-  update_hover (self);
-
-  gtk_widget_set_opacity (GTK_WIDGET (detached_tab), 0);
-  self->detached_index = adw_tab_view_get_page_position (self->view, self->detached_page);
-
-  adw_tab_view_detach_page (self->view, self->detached_page);
-
-  self->indirect_reordering = FALSE;
-
-  gtk_widget_measure (GTK_WIDGET (detached_tab),
-                      GTK_ORIENTATION_HORIZONTAL, -1,
-                      NULL, &self->placeholder_scroll_offset, NULL, NULL);
-  self->placeholder_scroll_offset /= 2;
-
-  animate_scroll_relative (self, -self->placeholder_scroll_offset, CLOSE_ANIMATION_DURATION);
-
-  g_object_unref (detached_tab);
-}
-
-static GdkDragAction
-tab_drag_enter_motion_cb (AdwTabBox     *self,
-                          double         x,
-                          double         y,
-                          GtkDropTarget *target)
-{
-  AdwTabBox *source_tab_box;
-
-  if (self->pinned)
-    return 0;
-
-  source_tab_box = get_source_tab_box (target);
-
-  if (!source_tab_box)
-    return 0;
-
-  if (!self->view || !is_view_in_the_same_group (self, source_tab_box->view))
-    return 0;
-
-  x += gtk_adjustment_get_value (self->adjustment);
-
-  self->can_remove_placeholder = FALSE;
-
-  if (!self->reorder_placeholder || !self->reorder_placeholder->page) {
-    AdwTabPage *page = source_tab_box->detached_page;
-    double 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));
-    adw_tab_set_display_width (self->reorder_placeholder->tab, source_tab_box->drag_icon->target_width);
-    adw_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 = (int) round (x - source_tab_box->drag_icon->hotspot_x);
-
-    start_drag_reodering (self, self->reorder_placeholder, x, y);
-
-    return GDK_ACTION_MOVE;
-  }
-
-  self->reorder_x = (int) round (x - source_tab_box->drag_icon->hotspot_x);
-
-  update_drag_reodering (self);
-
-  return GDK_ACTION_MOVE;
-}
-
-static void
-tab_drag_leave_cb (AdwTabBox     *self,
-                   GtkDropTarget *target)
-{
-  AdwTabBox *source_tab_box;
-
-  if (!self->indirect_reordering)
-    return;
-
-  if (self->pinned)
-    return;
-
-  source_tab_box = get_source_tab_box (target);
-
-  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_drag_reodering (self);
-  remove_placeholder (self);
-
-  self->indirect_reordering = FALSE;
-}
-
-static gboolean
-tab_drag_drop_cb (AdwTabBox     *self,
-                  const GValue  *value,
-                  double         x,
-                  double         y,
-                  GtkDropTarget *target)
-{
-  AdwTabBox *source_tab_box;
-
-  if (self->pinned)
-    return GDK_EVENT_PROPAGATE;
-
-  source_tab_box = get_source_tab_box (target);
-
-  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;
-
-  do_drag_drop (self, source_tab_box);
-
-  return GDK_EVENT_STOP;
-}
-
-static gboolean
-view_drag_drop_cb (AdwTabBox      *self,
-                   const GValue  *value,
-                   double         x,
-                   double         y,
-                   GtkDropTarget *target)
-{
-  AdwTabBox *source_tab_box;
-
-  if (self->pinned)
-    return GDK_EVENT_PROPAGATE;
-
-  source_tab_box = get_source_tab_box (target);
-
-  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 = adw_tab_view_get_n_pages (self->view) -
-                        adw_tab_view_get_n_pinned_pages (self->view);
-
-  do_drag_drop (self, source_tab_box);
-
-  return GDK_EVENT_STOP;
-}
-
-/* DND autoscrolling */
-
-static gboolean
-reset_drop_target_tab_cb (AdwTabBox *self)
-{
-  self->reset_drop_target_tab_id = 0;
-  set_drop_target_tab (self, NULL);
-
-  return G_SOURCE_REMOVE;
-}
-
-static void
-drag_leave_cb (AdwTabBox               *self,
-               GtkDropControllerMotion *controller)
-{
-  GdkDrop *drop = gtk_drop_controller_motion_get_drop (controller);
-  GdkDrag *drag = gdk_drop_get_drag (drop);
-  AdwTabBox *source = ADW_TAB_BOX (g_object_get_data (G_OBJECT (drag),
-                                                      "adw-tab-bar-drag-origin"));
-
-  if (source)
-    return;
-
-  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);
-}
-
-static void
-drag_enter_motion_cb (AdwTabBox               *self,
-                      double                   x,
-                      double                   y,
-                      GtkDropControllerMotion *controller)
-{
-  TabInfo *info;
-  GdkDrop *drop = gtk_drop_controller_motion_get_drop (controller);
-  GdkDrag *drag = gdk_drop_get_drag (drop);
-  AdwTabBox *source = ADW_TAB_BOX (g_object_get_data (G_OBJECT (drag),
-                                                      "adw-tab-bar-drag-origin"));
-
-  if (source)
-    return;
-
-  x += gtk_adjustment_get_value (self->adjustment);
-
-  info = find_tab_info_at (self, x);
-
-  if (!info) {
-    drag_leave_cb (self, controller);
-
-    return;
-  }
-
-  self->drop_target_x = x;
-  set_drop_target_tab (self, info);
-
-  start_autoscroll (self);
-}
-
-/* Context menu */
-
-static gboolean
-reset_setup_menu_cb (AdwTabBox *self)
-{
-  g_signal_emit_by_name (self->view, "setup-menu", NULL);
-
-  return G_SOURCE_REMOVE;
-}
-
-static void
-touch_menu_notify_visible_cb (AdwTabBox *self)
-{
-  if (!self->context_menu || gtk_widget_get_visible (GTK_WIDGET (self->context_menu)))
-    return;
-
-  self->hovering = FALSE;
-  update_hover (self);
-
-  g_idle_add ((GSourceFunc) reset_setup_menu_cb, self);
-}
-
-static void
-do_popup (AdwTabBox *self,
-          TabInfo   *info,
-          double     x,
-          double     y)
-{
-  GMenuModel *model = adw_tab_view_get_menu_model (self->view);
-  GdkRectangle rect;
-
-  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_POPOVER (gtk_popover_menu_new_from_model (model));
-    gtk_widget_set_parent (GTK_WIDGET (self->context_menu), GTK_WIDGET (self));
-    gtk_popover_set_position (self->context_menu, GTK_POS_BOTTOM);
-    gtk_popover_set_has_arrow (self->context_menu, FALSE);
-
-    if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
-      gtk_widget_set_halign (GTK_WIDGET (self->context_menu), GTK_ALIGN_END);
-    else
-      gtk_widget_set_halign (GTK_WIDGET (self->context_menu), GTK_ALIGN_START);
-
-    g_signal_connect_object (self->context_menu, "notify::visible",
-                             G_CALLBACK (touch_menu_notify_visible_cb), self,
-                             G_CONNECT_AFTER | G_CONNECT_SWAPPED);
-  }
-
-  if (x >= 0 && y >= 0) {
-    rect.x = x;
-    rect.y = y;
-  } else {
-    rect.x = info->pos;
-    rect.y = gtk_widget_get_allocated_height (GTK_WIDGET (info->tab));
-
-    if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
-      rect.x += info->width;
-  }
-
-  rect.x -= gtk_adjustment_get_value (self->adjustment);
-  rect.width = 0;
-  rect.height = 0;
-
-  gtk_popover_set_pointing_to (self->context_menu, &rect);
-
-  gtk_popover_popup (self->context_menu);
-}
-
-static void
-long_pressed_cb (AdwTabBox  *self,
-                 double      x,
-                 double      y,
-                 GtkGesture *gesture)
-{
-  TabInfo *info = find_tab_info_at (self, x);
-
-  gtk_gesture_set_state (self->drag_gesture, GTK_EVENT_SEQUENCE_DENIED);
-
-  if (!info || !info->page) {
-    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
-    return;
-  }
-
-  x += gtk_adjustment_get_value (self->adjustment);
-
-  gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
-  do_popup (self, self->pressed_tab, x, y);
-}
-
-static void
-popup_menu_cb (GtkWidget  *widget,
-               const char *action_name,
-               GVariant   *parameter)
-{
-  AdwTabBox *self = ADW_TAB_BOX (widget);
-
-  if (self->selected_tab && self->selected_tab->page)
-    do_popup (self, self->selected_tab, -1, -1);
-}
-
-/* Clicking */
-
-static void
-handle_click (AdwTabBox  *self,
-              TabInfo    *info,
-              GtkGesture *gesture)
-{
-  gboolean can_grab_focus;
-
-  if (self->adjustment) {
-    int pos = get_tab_position (self, info);
-    double value = gtk_adjustment_get_value (self->adjustment);
-    double page_size = gtk_adjustment_get_page_size (self->adjustment);
-
-    if (pos + OVERLAP < value ||
-        pos + info->width - OVERLAP > value + page_size) {
-      gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
-
-      scroll_to_tab (self, info, SCROLL_ANIMATION_DURATION);
-
-      return;
-    }
-  }
-
-  can_grab_focus = adw_tab_bar_tabs_have_visible_focus (self->tab_bar);
-
-  if (info == self->selected_tab)
-    can_grab_focus = TRUE;
-  else
-    adw_tab_view_set_selected_page (self->view, info->page);
-
-  if (can_grab_focus)
-    gtk_widget_grab_focus (GTK_WIDGET (info->tab));
-  else
-    activate_tab (self);
-}
-
-static void
-pressed_cb (AdwTabBox  *self,
-            int         n_press,
-            double      x,
-            double      y,
-            GtkGesture *gesture)
-{
-  TabInfo *info;
-  GdkEvent *event;
-  GdkEventSequence *current;
-  guint button;
-
-  if (is_touchscreen (gesture))
-    return;
-
-  x += gtk_adjustment_get_value (self->adjustment);
-
-  info = find_tab_info_at (self, x);
-
-  if (!info || !info->page) {
-    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
-
-    return;
-  }
-
-  current = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
-  event = gtk_gesture_get_last_event (gesture, current);
-
-   if (gdk_event_triggers_context_menu (event)) {
-    do_popup (self, info, x, y);
-    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
-    gtk_event_controller_reset (GTK_EVENT_CONTROLLER (gesture));
-
-    return;
-  }
-
-  button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
-
-  if (button == GDK_BUTTON_MIDDLE) {
-    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
-    adw_tab_view_close_page (self->view, info->page);
-
-    return;
-  }
-
-  if (button != GDK_BUTTON_PRIMARY) {
-    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
-
-    return;
-  }
-
-  handle_click (self, info, gesture);
-}
-
-static void
-released_cb (AdwTabBox  *self,
-             int         n_press,
-             double      x,
-             double      y,
-             GtkGesture *gesture)
-{
-  TabInfo *info;
-
-  if (!is_touchscreen (gesture))
-    return;
-
-  x += gtk_adjustment_get_value (self->adjustment);
-
-  info = find_tab_info_at (self, x);
-
-  if (!info || !info->page) {
-    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
-
-    return;
-  }
-
-  handle_click (self, info, gesture);
-}
-
-/* Overrides */
-
-static void
-adw_tab_box_measure (GtkWidget      *widget,
-                     GtkOrientation  orientation,
-                     int             for_size,
-                     int            *minimum,
-                     int            *natural,
-                     int            *minimum_baseline,
-                     int            *natural_baseline)
-{
-  AdwTabBox *self = ADW_TAB_BOX (widget);
-  int 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) {
-    int width = self->end_padding;
-    GList *l;
-
-    for (l = self->tabs; l; l = l->next) {
-      TabInfo *info = l->data;
-      int child_width;
-
-      gtk_widget_measure (GTK_WIDGET (info->tab), orientation, -1,
-                          NULL, &child_width, NULL, NULL);
-
-      width += calculate_tab_width (info, child_width) - OVERLAP;
-    }
-
-    if (!self->pinned)
-      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;
-      int child_min, child_nat;
-
-      gtk_widget_measure (GTK_WIDGET (info->tab), orientation, -1,
-                          &child_min, &child_nat, NULL, NULL);
-
-      if (child_min > min)
-        min = child_min;
-
-      if (child_nat > nat)
-        nat = child_nat;
-    }
-  }
-
-  if (minimum)
-    *minimum = min;
-
-  if (natural)
-    *natural = nat;
-
-  if (minimum_baseline)
-    *minimum_baseline = -1;
-
-  if (natural_baseline)
-    *natural_baseline = -1;
-}
-
-static void
-adw_tab_box_size_allocate (GtkWidget *widget,
-                           int        width,
-                           int        height,
-                           int        baseline)
-{
-  AdwTabBox *self = ADW_TAB_BOX (widget);
-  gboolean is_rtl;
-  GList *l;
-  GtkAllocation child_allocation;
-  int pos;
-  double value;
-
-  gtk_widget_measure (self->background, GTK_ORIENTATION_HORIZONTAL, -1,
-                      NULL, NULL, NULL, NULL);
-  gtk_widget_allocate (self->background, width, height, baseline, NULL);
-
-  adw_tab_box_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
-                       &self->allocated_width, NULL, NULL, NULL);
-  self->allocated_width = MAX (self->allocated_width, width);
-
-  value = gtk_adjustment_get_value (self->adjustment);
-
-  gtk_adjustment_configure (self->adjustment,
-                            value,
-                            0,
-                            self->allocated_width,
-                            width * 0.1,
-                            width * 0.9,
-                            width);
-
-  if (self->context_menu)
-    gtk_popover_present (self->context_menu);
-
-  if (!self->n_tabs)
-    return;
-
-  is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
-
-  if (self->pinned) {
-    for (l = self->tabs; l; l = l->next) {
-      TabInfo *info = l->data;
-      int child_width;
-
-      gtk_widget_measure (GTK_WIDGET (info->tab), GTK_ORIENTATION_HORIZONTAL, -1,
-                          NULL, &child_width, NULL, NULL);
-
-      info->width = calculate_tab_width (info, child_width);
-    }
-  } else if (self->tab_resize_mode == TAB_RESIZE_FIXED_TAB_WIDTH) {
-    self->end_padding = self->allocated_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 {
-    int tab_width = get_base_tab_width (self, FALSE);
-    int excess = self->allocated_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 = is_rtl ? self->allocated_width + OVERLAP : -OVERLAP;
-
-  for (l = self->tabs; l; l = l->next) {
-    TabInfo *info = l->data;
-
-    if (!info->appear_animation)
-      adw_tab_set_display_width (info->tab, info->width);
-    else if (info->page && info != self->reorder_placeholder)
-      adw_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) ? self->reorder_window_x : info->pos) - (int) floor 
(value);
-    child_allocation.y = 0;
-    child_allocation.width = info->width;
-    child_allocation.height = height;
-
-    gtk_widget_size_allocate (GTK_WIDGET (info->tab), &child_allocation, baseline);
-
-    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) {
-    self->block_scrolling = TRUE;
-    gtk_adjustment_set_value (self->adjustment,
-                              get_scroll_animation_value (self));
-    self->block_scrolling = FALSE;
-
-    if (self->scroll_animation_done) {
-        self->scroll_animation_done = FALSE;
-        self->scroll_animation_tab = NULL;
-        g_clear_object (&self->scroll_animation);
-    }
-  }
-
-  update_visible (self);
-}
-
-static void
-snapshot_tab (AdwTabBox      *self,
-              GtkSnapshot    *snapshot,
-              TabInfo        *info,
-              cairo_region_t *clip_region)
-{
-  cairo_rectangle_int_t rect = { 0, 0, 0, 0 };
-  gboolean clip = FALSE;
-  int pos, width, scroll_pos;
-  int i, n;
-
-  if (gtk_widget_get_opacity (GTK_WIDGET (info->tab)) <= 0)
-    return;
-
-  rect.height = gtk_widget_get_height (GTK_WIDGET (self));
-  scroll_pos = (int) floor (gtk_adjustment_get_value (self->adjustment));
-
-  pos = get_tab_position (self, info);
-  width = info->width;
-
-  n = cairo_region_num_rectangles (clip_region);
-  for (i = 0; i < n; i++) {
-    cairo_rectangle_int_t clip_rect;
-    int x1, x2;
-
-    cairo_region_get_rectangle (clip_region, i, &clip_rect);
-    x1 = clip_rect.x + scroll_pos;
-    x2 = x1 + clip_rect.width;
-
-    if (x1 < pos && x2 > pos + width) {
-      clip = FALSE;
-      break;
-    }
-
-    if (x2 < pos || x1 > pos + width)
-      continue;
-
-    gtk_snapshot_push_clip (snapshot, &GRAPHENE_RECT_INIT (clip_rect.x, clip_rect.y, clip_rect.width, 
clip_rect.height));
-    gtk_widget_snapshot_child (GTK_WIDGET (self), GTK_WIDGET (info->tab), snapshot);
-    gtk_snapshot_pop (snapshot);
-    clip = TRUE;
-  }
-
-  if (!clip)
-    gtk_widget_snapshot_child (GTK_WIDGET (self), GTK_WIDGET (info->tab), snapshot);
-
-  rect.x = pos - scroll_pos;
-  rect.width = width;
-  cairo_region_subtract_rectangle (clip_region, &rect);
-}
-
-static void
-adw_tab_box_snapshot (GtkWidget   *widget,
-                      GtkSnapshot *snapshot)
-{
-  AdwTabBox *self = ADW_TAB_BOX (widget);
-  int w = gtk_widget_get_width (widget);
-  int h = gtk_widget_get_height (widget);
-  cairo_rectangle_int_t rect = { 0, 0, 0, 0 };
-  cairo_region_t *region;
-  int i, n;
-  GList *l;
-
-  rect.width = w;
-  rect.height = h;
-  region = cairo_region_create_rectangle (&rect);
-
-  if (self->reordered_tab)
-    snapshot_tab (self, snapshot, self->reordered_tab, region);
-
-  if (self->selected_tab)
-    snapshot_tab (self, snapshot, self->selected_tab, region);
-
-  for (l = self->tabs; l; l = l->next) {
-    TabInfo *info = l->data;
-
-    if (info == self->reordered_tab || info == self->selected_tab)
-      continue;
-
-    snapshot_tab (self, snapshot, info, region);
-  }
-
-  n = cairo_region_num_rectangles (region);
-  for (i = 0; i < n; i++) {
-    cairo_region_get_rectangle (region, i, &rect);
-
-    gtk_snapshot_push_clip (snapshot, &GRAPHENE_RECT_INIT (rect.x, rect.y, rect.width, rect.height));
-    gtk_widget_snapshot_child (widget, self->background, snapshot);
-    gtk_snapshot_pop (snapshot);
-  }
-
-  cairo_region_destroy (region);
-}
-
-static gboolean
-adw_tab_box_focus (GtkWidget        *widget,
-                   GtkDirectionType  direction)
-{
-  AdwTabBox *self = ADW_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
-adw_tab_box_unrealize (GtkWidget *widget)
-{
-  AdwTabBox *self = ADW_TAB_BOX (widget);
-
-  g_clear_pointer ((GtkWidget **) &self->context_menu, gtk_widget_unparent);
-
-  GTK_WIDGET_CLASS (adw_tab_box_parent_class)->unrealize (widget);
-}
-
-static void
-adw_tab_box_unmap (GtkWidget *widget)
-{
-  AdwTabBox *self = ADW_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;
-  }
-
-  self->hovering = FALSE;
-  update_hover (self);
-
-  GTK_WIDGET_CLASS (adw_tab_box_parent_class)->unmap (widget);
-}
-
-static void
-adw_tab_box_direction_changed (GtkWidget        *widget,
-                               GtkTextDirection  previous_direction)
-{
-  AdwTabBox *self = ADW_TAB_BOX (widget);
-  double 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);
-
-  if (self->context_menu) {
-    if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
-      gtk_widget_set_halign (GTK_WIDGET (self->context_menu), GTK_ALIGN_END);
-    else
-      gtk_widget_set_halign (GTK_WIDGET (self->context_menu), GTK_ALIGN_START);
-  }
-}
-
-static void
-adw_tab_box_dispose (GObject *object)
-{
-  AdwTabBox *self = ADW_TAB_BOX (object);
-
-  g_clear_handle_id (&self->drop_switch_timeout_id, g_source_remove);
-
-  g_clear_pointer (&self->background, gtk_widget_unparent);
-
-  self->drag_gesture = NULL;
-  self->tab_bar = NULL;
-  adw_tab_box_set_view (self, NULL);
-  set_hadjustment (self, NULL);
-
-  G_OBJECT_CLASS (adw_tab_box_parent_class)->dispose (object);
-}
-
-static void
-adw_tab_box_finalize (GObject *object)
-{
-  AdwTabBox *self = (AdwTabBox *) object;
-
-  g_clear_pointer (&self->extra_drag_types, g_free);
-
-  G_OBJECT_CLASS (adw_tab_box_parent_class)->finalize (object);
-}
-
-static void
-adw_tab_box_get_property (GObject    *object,
-                          guint       prop_id,
-                          GValue     *value,
-                          GParamSpec *pspec)
-{
-  AdwTabBox *self = ADW_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_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;
-
-  case PROP_HADJUSTMENT:
-    g_value_set_object (value, self->adjustment);
-    break;
-
-  case PROP_VADJUSTMENT:
-  case PROP_HSCROLL_POLICY:
-  case PROP_VSCROLL_POLICY:
-    break;
-
-  default:
-    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-  }
-}
-
-static void
-adw_tab_box_set_property (GObject      *object,
-                          guint         prop_id,
-                          const GValue *value,
-                          GParamSpec   *pspec)
+struct _AdwTabBox
 {
-  AdwTabBox *self = ADW_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:
-    adw_tab_box_set_view (self, g_value_get_object (value));
-    break;
-
-  case PROP_HADJUSTMENT:
-    set_hadjustment (self, g_value_get_object (value));
-    break;
-
-  case PROP_VADJUSTMENT:
-  case PROP_HSCROLL_POLICY:
-  case PROP_VSCROLL_POLICY:
-    break;
+  AdwTabListBase parent_instance;
+};
 
-  default:
-    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
-  }
-}
+G_DEFINE_TYPE (AdwTabBox, adw_tab_box, ADW_TYPE_TAB_LIST_BASE)
 
 static void
 adw_tab_box_class_init (AdwTabBoxClass *klass)
 {
-  GObjectClass *object_class = G_OBJECT_CLASS (klass);
   GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
 
-  object_class->dispose = adw_tab_box_dispose;
-  object_class->finalize = adw_tab_box_finalize;
-  object_class->get_property = adw_tab_box_get_property;
-  object_class->set_property = adw_tab_box_set_property;
-
-  widget_class->measure = adw_tab_box_measure;
-  widget_class->size_allocate = adw_tab_box_size_allocate;
-  widget_class->snapshot = adw_tab_box_snapshot;
-  widget_class->focus = adw_tab_box_focus;
-  widget_class->unrealize = adw_tab_box_unrealize;
-  widget_class->unmap = adw_tab_box_unmap;
-  widget_class->direction_changed = adw_tab_box_direction_changed;
-
-  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",
-                         ADW_TYPE_TAB_BAR,
-                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
-
-  props[PROP_VIEW] =
-    g_param_spec_object ("view",
-                         "View",
-                         "View",
-                         ADW_TYPE_TAB_VIEW,
-                         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);
-
-  g_object_class_override_property (object_class, PROP_HADJUSTMENT, "hadjustment");
-  g_object_class_override_property (object_class, PROP_VADJUSTMENT, "vadjustment");
-  g_object_class_override_property (object_class, PROP_HSCROLL_POLICY, "hscroll-policy");
-  g_object_class_override_property (object_class, PROP_VSCROLL_POLICY, "vscroll-policy");
-
-  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_DROP] =
-    g_signal_new ("extra-drag-drop",
-                  G_TYPE_FROM_CLASS (klass),
-                  G_SIGNAL_RUN_LAST,
-                  0,
-                  g_signal_accumulator_first_wins, NULL, NULL,
-                  G_TYPE_BOOLEAN,
-                  2,
-                  ADW_TYPE_TAB_PAGE,
-                  G_TYPE_VALUE);
-
-  gtk_widget_class_install_action (widget_class, "menu.popup", NULL, popup_menu_cb);
-
-  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_F10, GDK_SHIFT_MASK, "menu.popup", NULL);
-  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_Menu, 0, "menu.popup", NULL);
-
-  add_focus_bindings (widget_class, GDK_KEY_Page_Up,   GTK_DIR_TAB_BACKWARD, FALSE);
-  add_focus_bindings (widget_class, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD,  FALSE);
-  add_focus_bindings (widget_class, GDK_KEY_Home,      GTK_DIR_TAB_BACKWARD, TRUE);
-  add_focus_bindings (widget_class, GDK_KEY_End,       GTK_DIR_TAB_FORWARD,  TRUE);
-
-  add_reorder_bindings (widget_class, GDK_KEY_Left,      GTK_DIR_LEFT,         FALSE);
-  add_reorder_bindings (widget_class, GDK_KEY_Right,     GTK_DIR_RIGHT,        FALSE);
-  add_reorder_bindings (widget_class, GDK_KEY_Page_Up,   GTK_DIR_TAB_BACKWARD, FALSE);
-  add_reorder_bindings (widget_class, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD,  FALSE);
-  add_reorder_bindings (widget_class, GDK_KEY_Home,      GTK_DIR_TAB_BACKWARD, TRUE);
-  add_reorder_bindings (widget_class, GDK_KEY_End,       GTK_DIR_TAB_FORWARD,  TRUE);
-
   gtk_widget_class_set_css_name (widget_class, "tabbox");
 }
 
 static void
 adw_tab_box_init (AdwTabBox *self)
 {
-  GtkEventController *controller;
-
-  self->can_remove_placeholder = TRUE;
-  self->expand_tabs = TRUE;
-
-  gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN);
-
-  self->background = adw_gizmo_new ("background", NULL, NULL, NULL, NULL, NULL, NULL);
-  gtk_widget_set_can_target (self->background, FALSE);
-  gtk_widget_set_can_focus (self->background, FALSE);
-  gtk_widget_set_parent (self->background, GTK_WIDGET (self));
-
-  controller = gtk_event_controller_motion_new ();
-  g_signal_connect_swapped (controller, "motion", G_CALLBACK (motion_cb), self);
-  g_signal_connect_swapped (controller, "leave", G_CALLBACK (leave_cb), self);
-  gtk_widget_add_controller (GTK_WIDGET (self), controller);
-
-  controller = gtk_event_controller_scroll_new (GTK_EVENT_CONTROLLER_SCROLL_VERTICAL);
-  g_signal_connect_swapped (controller, "scroll", G_CALLBACK (scroll_cb), self);
-  gtk_widget_add_controller (GTK_WIDGET (self), controller);
-
-  controller = GTK_EVENT_CONTROLLER (gtk_gesture_click_new ());
-  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (controller), 0);
-  gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (controller), TRUE);
-  g_signal_connect_swapped (controller, "pressed", G_CALLBACK (pressed_cb), self);
-  g_signal_connect_swapped (controller, "released", G_CALLBACK (released_cb), self);
-  gtk_widget_add_controller (GTK_WIDGET (self), controller);
-
-  controller = GTK_EVENT_CONTROLLER (gtk_gesture_long_press_new ());
-  gtk_gesture_long_press_set_delay_factor (GTK_GESTURE_LONG_PRESS (controller), 2);
-  gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (controller), TRUE);
-  gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (controller), TRUE);
-  g_signal_connect_swapped (controller, "pressed", G_CALLBACK (long_pressed_cb), self);
-  gtk_widget_add_controller (GTK_WIDGET (self), controller);
-
-  controller = GTK_EVENT_CONTROLLER (gtk_gesture_drag_new ());
-  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (controller), GDK_BUTTON_PRIMARY);
-  gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (controller), TRUE);
-  g_signal_connect_swapped (controller, "drag-begin", G_CALLBACK (reorder_begin_cb), self);
-  g_signal_connect_swapped (controller, "drag-update", G_CALLBACK (reorder_update_cb), self);
-  g_signal_connect_swapped (controller, "drag-end", G_CALLBACK (reorder_end_cb), self);
-  gtk_widget_add_controller (GTK_WIDGET (self), controller);
-  self->drag_gesture = GTK_GESTURE (controller);
-
-  controller = gtk_drop_controller_motion_new ();
-  g_signal_connect_swapped (controller, "enter", G_CALLBACK (drag_enter_motion_cb), self);
-  g_signal_connect_swapped (controller, "motion", G_CALLBACK (drag_enter_motion_cb), self);
-  g_signal_connect_swapped (controller, "leave", G_CALLBACK (drag_leave_cb), self);
-  gtk_widget_add_controller (GTK_WIDGET (self), controller);
-
-  controller = GTK_EVENT_CONTROLLER (gtk_drop_target_new (ADW_TYPE_TAB_PAGE, GDK_ACTION_MOVE));
-  gtk_drop_target_set_preload (GTK_DROP_TARGET (controller), TRUE);
-  g_signal_connect_swapped (controller, "enter", G_CALLBACK (tab_drag_enter_motion_cb), self);
-  g_signal_connect_swapped (controller, "motion", G_CALLBACK (tab_drag_enter_motion_cb), self);
-  g_signal_connect_swapped (controller, "leave", G_CALLBACK (tab_drag_leave_cb), self);
-  g_signal_connect_swapped (controller, "drop", G_CALLBACK (tab_drag_drop_cb), self);
-  gtk_widget_add_controller (GTK_WIDGET (self), controller);
-}
-
-void
-adw_tab_box_set_view (AdwTabBox  *self,
-                      AdwTabView *view)
-{
-  g_return_if_fail (ADW_IS_TAB_BOX (self));
-  g_return_if_fail (view == NULL || ADW_IS_TAB_VIEW (view));
-
-  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) {
-      gtk_widget_remove_controller (GTK_WIDGET (self->view), self->view_drop_target);
-      self->view_drop_target = NULL;
-    }
-
-    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 = adw_tab_view_get_n_pages (self->view);
-
-    for (i = n_pages - 1; i >= 0; i--)
-      page_attached_cb (self, adw_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) {
-      self->view_drop_target = GTK_EVENT_CONTROLLER (gtk_drop_target_new (ADW_TYPE_TAB_PAGE, 
GDK_ACTION_MOVE));
-
-      g_signal_connect_object (self->view_drop_target, "drop", G_CALLBACK (view_drag_drop_cb), self, 
G_CONNECT_SWAPPED);
-
-      gtk_widget_add_controller (GTK_WIDGET (self->view), self->view_drop_target);
-    }
-  }
-
-  gtk_widget_queue_allocate (GTK_WIDGET (self));
-
-  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW]);
-}
-
-void
-adw_tab_box_attach_page (AdwTabBox  *self,
-                         AdwTabPage *page,
-                         int         position)
-{
-  g_return_if_fail (ADW_IS_TAB_BOX (self));
-  g_return_if_fail (ADW_IS_TAB_PAGE (page));
-
-  page_attached_cb (self, page, position);
-}
-
-void
-adw_tab_box_detach_page (AdwTabBox  *self,
-                         AdwTabPage *page)
-{
-  g_return_if_fail (ADW_IS_TAB_BOX (self));
-  g_return_if_fail (ADW_IS_TAB_PAGE (page));
-
-  page_detached_cb (self, page);
-}
-
-void
-adw_tab_box_select_page (AdwTabBox  *self,
-                         AdwTabPage *page)
-{
-  g_return_if_fail (ADW_IS_TAB_BOX (self));
-  g_return_if_fail (page == NULL || ADW_IS_TAB_PAGE (page));
-
-  select_page (self, page);
-}
-
-void
-adw_tab_box_try_focus_selected_tab (AdwTabBox *self)
-{
-  g_return_if_fail (ADW_IS_TAB_BOX (self));
-
-  if (self->selected_tab)
-    gtk_widget_grab_focus (GTK_WIDGET (self->selected_tab->tab));
-}
-
-gboolean
-adw_tab_box_is_page_focused (AdwTabBox  *self,
-                             AdwTabPage *page)
-{
-  TabInfo *info;
-
-  g_return_val_if_fail (ADW_IS_TAB_BOX (self), FALSE);
-  g_return_val_if_fail (ADW_IS_TAB_PAGE (page), FALSE);
-
-  info = find_info_for_page (self, page);
-
-  return info && gtk_widget_is_focus (GTK_WIDGET (info->tab));
-}
-
-void
-adw_tab_box_setup_extra_drop_target (AdwTabBox     *self,
-                                     GdkDragAction  actions,
-                                     GType         *types,
-                                     gsize          n_types)
-{
-  GList *l;
-
-  g_return_if_fail (ADW_IS_TAB_BOX (self));
-  g_return_if_fail (n_types == 0 || types != NULL);
-
-  g_clear_pointer (&self->extra_drag_types, g_free);
-
-  self->extra_drag_actions = actions;
-#if GLIB_CHECK_VERSION(2, 67, 3)
-  self->extra_drag_types = g_memdup2 (types, sizeof (GType) * n_types);
-#else
-  self->extra_drag_types = g_memdup (types, sizeof (GType) * n_types);
-#endif
-  self->extra_drag_n_types = n_types;
-
-  for (l = self->tabs; l; l = l->next) {
-    TabInfo *info = l->data;
-
-    adw_tab_setup_extra_drop_target (info->tab,
-                                     self->extra_drag_actions,
-                                     self->extra_drag_types,
-                                     self->extra_drag_n_types);
-  }
-}
-
-gboolean
-adw_tab_box_get_expand_tabs (AdwTabBox *self)
-{
-  g_return_val_if_fail (ADW_IS_TAB_BOX (self), FALSE);
-
-  return self->expand_tabs;
-}
-
-void
-adw_tab_box_set_expand_tabs (AdwTabBox *self,
-                             gboolean   expand_tabs)
-{
-  g_return_if_fail (ADW_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
-adw_tab_box_get_inverted (AdwTabBox *self)
-{
-  g_return_val_if_fail (ADW_IS_TAB_BOX (self), FALSE);
-
-  return self->inverted;
-}
-
-void
-adw_tab_box_set_inverted (AdwTabBox *self,
-                          gboolean   inverted)
-{
-  GList *l;
-
-  g_return_if_fail (ADW_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;
-
-    adw_tab_set_inverted (info->tab, inverted);
-  }
 }
diff --git a/src/adw-tab-list-base-private.h b/src/adw-tab-list-base-private.h
new file mode 100644
index 00000000..84ea6999
--- /dev/null
+++ b/src/adw-tab-list-base-private.h
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include "adw-tab-view.h"
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_TAB_LIST_BASE (adw_tab_list_base_get_type())
+
+G_DECLARE_DERIVABLE_TYPE (AdwTabListBase, adw_tab_list_base, ADW, TAB_LIST_BASE, GtkWidget)
+
+struct _AdwTabListBaseClass
+{
+  GtkWidgetClass parent_class;
+};
+
+void adw_tab_list_base_set_view (AdwTabListBase *self,
+                                 AdwTabView     *view);
+
+void adw_tab_list_base_set_adjustment (AdwTabListBase *self,
+                                       GtkAdjustment  *adjustment);
+
+void adw_tab_list_base_attach_page (AdwTabListBase *self,
+                                    AdwTabPage     *page,
+                                    int             position);
+void adw_tab_list_base_detach_page (AdwTabListBase *self,
+                                    AdwTabPage     *page);
+void adw_tab_list_base_select_page (AdwTabListBase *self,
+                                    AdwTabPage     *page);
+
+void adw_tab_list_base_try_focus_selected_tab (AdwTabListBase  *self);
+gboolean adw_tab_list_base_is_page_focused    (AdwTabListBase *self,
+                                               AdwTabPage     *page);
+
+void adw_tab_list_base_setup_extra_drop_target (AdwTabListBase *self,
+                                                GdkDragAction   actions,
+                                                GType          *types,
+                                                gsize           n_types);
+
+gboolean adw_tab_list_base_get_expand_tabs (AdwTabListBase *self);
+void     adw_tab_list_base_set_expand_tabs (AdwTabListBase *self,
+                                            gboolean        expand_tabs);
+
+gboolean adw_tab_list_base_get_inverted (AdwTabListBase *self);
+void     adw_tab_list_base_set_inverted (AdwTabListBase *self,
+                                         gboolean        inverted);
+
+G_END_DECLS
diff --git a/src/adw-tab-list-base.c b/src/adw-tab-list-base.c
new file mode 100644
index 00000000..92c324f5
--- /dev/null
+++ b/src/adw-tab-list-base.c
@@ -0,0 +1,3821 @@
+/*
+ * Copyright (C) 2020-2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#include "config.h"
+
+#include "adw-tab-list-base-private.h"
+
+#include "adw-animation-util-private.h"
+#include "adw-animation-private.h"
+#include "adw-gizmo-private.h"
+#include "adw-tab-private.h"
+#include "adw-tab-bar-private.h"
+#include "adw-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;
+
+typedef struct {
+  GdkDrag *drag;
+
+  AdwTab *tab;
+  GtkBorder tab_margin;
+
+  int hotspot_x;
+  int hotspot_y;
+
+  int width;
+  int target_width;
+  AdwAnimation *resize_animation;
+} DragIcon;
+
+typedef struct {
+  AdwTabPage *page;
+  AdwTab *tab;
+
+  int pos;
+  int width;
+  int last_width;
+
+  double end_reorder_offset;
+  double reorder_offset;
+
+  AdwAnimation *reorder_animation;
+  gboolean reorder_ignore_bounds;
+
+  double appear_progress;
+  AdwAnimation *appear_animation;
+
+  gulong notify_needs_attention_id;
+} TabInfo;
+
+typedef struct {
+  gboolean pinned;
+  AdwTabBar *tab_bar;
+  AdwTabView *view;
+  GtkAdjustment *adjustment;
+  gboolean needs_attention_left;
+  gboolean needs_attention_right;
+  gboolean expand_tabs;
+  gboolean inverted;
+
+  GtkEventController *view_drop_target;
+  GtkGesture *drag_gesture;
+
+  GList *tabs;
+  int n_tabs;
+
+  GtkPopover *context_menu;
+  GtkWidget *background;
+
+  int allocated_width;
+  int last_width;
+  int end_padding;
+  int initial_end_padding;
+  TabResizeMode tab_resize_mode;
+  AdwAnimation *resize_animation;
+
+  TabInfo *selected_tab;
+
+  gboolean hovering;
+  TabInfo *pressed_tab;
+  TabInfo *reordered_tab;
+  AdwAnimation *reorder_animation;
+
+  int reorder_start_pos;
+  int reorder_x;
+  int reorder_y;
+  int reorder_index;
+  int reorder_window_x;
+  gboolean continue_reorder;
+  gboolean indirect_reordering;
+
+  gboolean dragging;
+  double drag_offset_x;
+  double drag_offset_y;
+
+  guint drag_autoscroll_cb_id;
+  gint64 drag_autoscroll_prev_time;
+
+  AdwTabPage *detached_page;
+  int detached_index;
+  TabInfo *reorder_placeholder;
+  AdwTabPage *placeholder_page;
+  int placeholder_scroll_offset;
+  gboolean can_remove_placeholder;
+  DragIcon *drag_icon;
+  gboolean should_detach_into_new_window;
+
+  TabInfo *drop_target_tab;
+  guint drop_switch_timeout_id;
+  guint reset_drop_target_tab_id;
+  double drop_target_x;
+
+  struct {
+    TabInfo *info;
+    int pos;
+    gint64 duration;
+    gboolean keep_selected_visible;
+  } scheduled_scroll;
+
+  AdwAnimation *scroll_animation;
+  gboolean scroll_animation_done;
+  double scroll_animation_from;
+  double scroll_animation_offset;
+  TabInfo *scroll_animation_tab;
+  gboolean block_scrolling;
+  double adjustment_prev_value;
+
+  GdkDragAction extra_drag_actions;
+  GType *extra_drag_types;
+  gsize extra_drag_n_types;
+} AdwTabListBasePrivate;
+
+G_DEFINE_ABSTRACT_TYPE_WITH_CODE (AdwTabListBase, adw_tab_list_base, GTK_TYPE_WIDGET,
+                                  G_ADD_PRIVATE (AdwTabListBase)
+                                  G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL))
+
+enum {
+  PROP_0,
+  PROP_PINNED,
+  PROP_TAB_BAR,
+  PROP_VIEW,
+  PROP_NEEDS_ATTENTION_LEFT,
+  PROP_NEEDS_ATTENTION_RIGHT,
+  PROP_RESIZE_FROZEN,
+  PROP_HADJUSTMENT,
+  PROP_VADJUSTMENT,
+  PROP_HSCROLL_POLICY,
+  PROP_VSCROLL_POLICY,
+  LAST_PROP = PROP_HADJUSTMENT
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+  SIGNAL_STOP_KINETIC_SCROLLING,
+  SIGNAL_EXTRA_DRAG_DROP,
+  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 int
+get_tab_position (AdwTabListBase *self,
+                  TabInfo        *info)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  if (info == priv->reordered_tab)
+    return priv->reorder_window_x;
+
+  return info->pos;
+}
+
+static inline TabInfo *
+find_tab_info_at (AdwTabListBase *self,
+                  double          x)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  GList *l;
+
+  if (priv->reordered_tab) {
+    int pos = get_tab_position (self, priv->reordered_tab);
+
+    if (pos <= x && x < pos + priv->reordered_tab->width)
+      return priv->reordered_tab;
+  }
+
+  for (l = priv->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (info != priv->reordered_tab &&
+        info->pos <= x && x < info->pos + info->width)
+      return info;
+  }
+
+  return NULL;
+}
+
+static inline GList *
+find_link_for_page (AdwTabListBase *self,
+                    AdwTabPage     *page)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  GList *l;
+
+  for (l = priv->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (info->page == page)
+      return l;
+  }
+
+  return NULL;
+}
+
+static inline TabInfo *
+find_info_for_page (AdwTabListBase *self,
+                    AdwTabPage     *page)
+{
+  GList *l = find_link_for_page (self, page);
+
+  return l ? l->data : NULL;
+}
+
+static GList *
+find_nth_alive_tab (AdwTabListBase *self,
+                    guint      position)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  GList *l;
+
+  for (l = priv->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (!info->page)
+        continue;
+
+    if (!position--)
+        return l;
+  }
+
+  return NULL;
+}
+
+static inline int
+calculate_tab_width (TabInfo *info,
+                     int      base_width)
+{
+  return OVERLAP + (int) floor ((base_width - OVERLAP) * info->appear_progress);
+}
+
+static int
+get_base_tab_width (AdwTabListBase *self,
+                    gboolean   target)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  double max_progress = 0;
+  double n = 0;
+  double used_width;
+  GList *l;
+  int ret;
+
+  for (l = priv->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    max_progress = MAX (max_progress, info->appear_progress);
+    n += info->appear_progress;
+  }
+
+  used_width = (priv->allocated_width + (n + 1) * OVERLAP - (target ? 0 : priv->end_padding)) * max_progress;
+
+  ret = (int) ceil (used_width / n);
+
+  if (!priv->expand_tabs)
+    ret = MIN (ret, MAX_TAB_WIDTH_NON_EXPAND + OVERLAP);
+
+  return ret;
+}
+
+static int
+predict_tab_width (AdwTabListBase *self,
+                   TabInfo        *info,
+                   gboolean        assume_placeholder)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  int n;
+  int width = priv->allocated_width;
+  int min;
+
+  if (priv->pinned)
+    n = adw_tab_view_get_n_pinned_pages (priv->view);
+  else
+    n = adw_tab_view_get_n_pages (priv->view) - adw_tab_view_get_n_pinned_pages (priv->view);
+
+  if (assume_placeholder)
+      n++;
+
+  width += OVERLAP * (n + 1) - priv->end_padding;
+
+  /* Tabs have 0 minimum width, we need natural width instead */
+  gtk_widget_measure (GTK_WIDGET (info->tab), GTK_ORIENTATION_HORIZONTAL, -1,
+                      NULL, &min, NULL, NULL);
+
+  if (priv->expand_tabs)
+    return MAX ((int) floor (width / (double) n), min);
+  else
+    return CLAMP ((int) floor (width / (double) n), min, MAX_TAB_WIDTH_NON_EXPAND);
+}
+
+static int
+calculate_tab_offset (AdwTabListBase *self,
+                      TabInfo        *info,
+                      gboolean        target)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  int width;
+
+  if (!priv->reordered_tab)
+      return 0;
+
+  width = (target ? adw_tab_get_display_width (priv->reordered_tab->tab) : priv->reordered_tab->width) - 
OVERLAP;
+
+  if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+      width = -width;
+
+  return (int) round (width * (target ? info->end_reorder_offset : info->reorder_offset));
+}
+
+static void
+get_visible_range (AdwTabListBase *self,
+                   int            *lower,
+                   int            *upper)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  int min = -OVERLAP;
+  int max = priv->allocated_width + OVERLAP;
+
+  if (priv->pinned) {
+    if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+      min += OVERLAP;
+    else
+      max -= OVERLAP;
+  }
+
+  if (priv->adjustment) {
+    double value = gtk_adjustment_get_value (priv->adjustment);
+    double page_size = gtk_adjustment_get_page_size (priv->adjustment);
+
+    min = MAX (min, (int) floor (value) - OVERLAP);
+    max = MIN (max, (int) ceil (value + page_size) + OVERLAP);
+  }
+
+  if (lower)
+    *lower = min;
+
+  if (upper)
+    *upper = max;
+}
+
+static inline gboolean
+is_touchscreen (GtkGesture *gesture)
+{
+  GtkEventController *controller = GTK_EVENT_CONTROLLER (gesture);
+  GdkDevice *device = gtk_event_controller_get_current_event_device (controller);
+  GdkInputSource input_source = gdk_device_get_source (device);
+
+  return input_source == GDK_SOURCE_TOUCHSCREEN;
+}
+
+/* Tab resize delay */
+
+static void
+resize_animation_value_cb (double   value,
+                           gpointer user_data)
+{
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (user_data);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  double target_end_padding = 0;
+
+  if (!priv->expand_tabs) {
+    int predicted_tab_width = get_base_tab_width (self, TRUE);
+    GList *l;
+
+    target_end_padding = priv->allocated_width + OVERLAP;
+
+    for (l = priv->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);
+  }
+
+  priv->end_padding = (int) floor (adw_lerp (priv->initial_end_padding, target_end_padding, value));
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+resize_animation_done_cb (gpointer user_data)
+{
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (user_data);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  priv->end_padding = 0;
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+
+  g_clear_object (&priv->resize_animation);
+}
+
+static void
+set_tab_resize_mode (AdwTabListBase *self,
+                     TabResizeMode   mode)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  gboolean notify;
+
+  if (priv->tab_resize_mode == mode)
+    return;
+
+  if (mode == TAB_RESIZE_FIXED_TAB_WIDTH) {
+    GList *l;
+
+    priv->last_width = priv->allocated_width;
+
+    for (l = priv->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+
+      if (info->appear_animation)
+        info->last_width = adw_tab_get_display_width (info->tab);
+      else
+        info->last_width = info->width;
+    }
+  } else {
+    priv->last_width = 0;
+  }
+
+  if (mode == TAB_RESIZE_NORMAL) {
+    priv->initial_end_padding = priv->end_padding;
+
+    priv->resize_animation =
+      adw_animation_new (GTK_WIDGET (self), 0, 1,
+                         RESIZE_ANIMATION_DURATION,
+                         resize_animation_value_cb,
+                         self);
+
+    g_signal_connect_swapped (priv->resize_animation, "done", G_CALLBACK (resize_animation_done_cb), self);
+
+    adw_animation_start (priv->resize_animation);
+  }
+
+  notify = (priv->tab_resize_mode == TAB_RESIZE_NORMAL) !=
+           (mode == TAB_RESIZE_NORMAL);
+
+  priv->tab_resize_mode = mode;
+
+  if (notify)
+    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_RESIZE_FROZEN]);
+}
+
+/* Hover */
+
+static void
+update_hover (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  if (!priv->dragging && !priv->hovering)
+    set_tab_resize_mode (self, TAB_RESIZE_NORMAL);
+}
+
+static void
+motion_cb (AdwTabListBase     *self,
+           double              x,
+           double              y,
+           GtkEventController *controller)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  GdkDevice *device = gtk_event_controller_get_current_event_device (controller);
+  GdkInputSource input_source = gdk_device_get_source (device);
+
+  if (input_source == GDK_SOURCE_TOUCHSCREEN)
+    return;
+
+  if (priv->hovering)
+    return;
+
+  priv->hovering = TRUE;
+
+  update_hover (self);
+}
+
+static void
+leave_cb (AdwTabListBase     *self,
+          GtkEventController *controller)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  priv->hovering = FALSE;
+
+  update_hover (self);
+}
+
+/* Keybindings */
+
+static void
+focus_tab_cb (AdwTabListBase *self,
+              GVariant       *args)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  GtkDirectionType direction;
+  gboolean last, is_rtl, success;
+
+  if (!priv->view || !priv->selected_tab)
+    return;
+
+  g_variant_get (args, "(hb)", &direction, &last);
+
+  is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+  success = last;
+
+  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 = adw_tab_view_select_first_page (priv->view);
+    else
+      success = adw_tab_view_select_previous_page (priv->view);
+  } else if (direction == GTK_DIR_TAB_FORWARD) {
+    if (last)
+      success = adw_tab_view_select_last_page (priv->view);
+    else
+      success = adw_tab_view_select_next_page (priv->view);
+  }
+
+  if (!success)
+    gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+static void
+reorder_tab_cb (AdwTabListBase *self,
+                GVariant       *args)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  GtkDirectionType direction;
+  gboolean last, is_rtl, success;
+
+  if (!priv->view || !priv->selected_tab || !priv->selected_tab->page)
+    return;
+
+  g_variant_get (args, "(hb)", &direction, &last);
+
+  is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+  success = last;
+
+  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 = adw_tab_view_reorder_first (priv->view, priv->selected_tab->page);
+    else
+      success = adw_tab_view_reorder_backward (priv->view, priv->selected_tab->page);
+  } else if (direction == GTK_DIR_TAB_FORWARD) {
+    if (last)
+      success = adw_tab_view_reorder_last (priv->view, priv->selected_tab->page);
+    else
+      success = adw_tab_view_reorder_forward (priv->view, priv->selected_tab->page);
+  }
+
+  if (!success)
+    gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+static void
+add_focus_bindings (GtkWidgetClass   *widget_class,
+                    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_widget_class_add_binding (widget_class, keysym, 0,
+                                (GtkShortcutFunc) focus_tab_cb,
+                                "(hb)", direction, last);
+  gtk_widget_class_add_binding (widget_class, keypad_keysym, 0,
+                                (GtkShortcutFunc) focus_tab_cb,
+                                "(hb)", direction, last);
+}
+
+static void
+add_reorder_bindings (GtkWidgetClass   *widget_class,
+                      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_widget_class_add_binding (widget_class, keysym, GDK_SHIFT_MASK,
+                                (GtkShortcutFunc) reorder_tab_cb,
+                                "(hb)", direction, last);
+  gtk_widget_class_add_binding (widget_class, keypad_keysym, GDK_SHIFT_MASK,
+                                (GtkShortcutFunc) reorder_tab_cb,
+                                "(hb)", direction, last);
+}
+
+static void
+activate_tab (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  GtkWidget *child;
+
+  if (!priv->selected_tab || !priv->selected_tab->page)
+    return;
+
+  child = adw_tab_page_get_child (priv->selected_tab->page);
+
+  gtk_widget_grab_focus (child);
+}
+
+/* Scrolling */
+
+static void
+update_visible (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  gboolean left = FALSE, right = FALSE;
+  GList *l;
+  double value, page_size;
+
+  if (!priv->adjustment)
+    return;
+
+  value = gtk_adjustment_get_value (priv->adjustment);
+  page_size = gtk_adjustment_get_page_size (priv->adjustment);
+
+  if (!priv->adjustment)
+      return;
+
+  for (l = priv->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+    int pos;
+
+    if (!info->page)
+      continue;
+
+    pos = get_tab_position (self, info);
+
+    adw_tab_set_fully_visible (info->tab,
+                               pos + OVERLAP >= value &&
+                               pos + info->width - OVERLAP <= value + page_size);
+
+    if (!adw_tab_page_get_needs_attention (info->page))
+      continue;
+
+    if (pos + info->width / 2.0 <= value)
+      left = TRUE;
+
+    if (pos + info->width / 2.0 >= value + page_size)
+      right = TRUE;
+  }
+
+  if (priv->needs_attention_left != left) {
+    priv->needs_attention_left = left;
+    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NEEDS_ATTENTION_LEFT]);
+  }
+
+  if (priv->needs_attention_right != right) {
+    priv->needs_attention_right = right;
+    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NEEDS_ATTENTION_RIGHT]);
+  }
+}
+
+static double
+get_scroll_animation_value (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  double to, value;
+
+  g_assert (priv->scroll_animation);
+
+  to = priv->scroll_animation_offset;
+
+  if (priv->scroll_animation_tab) {
+    double page_size = gtk_adjustment_get_page_size (priv->adjustment);
+
+    to += get_tab_position (self, priv->scroll_animation_tab);
+    to = CLAMP (to, 0, priv->allocated_width - page_size);
+  }
+
+  value = adw_animation_get_value (priv->scroll_animation);
+
+  return round (adw_lerp (priv->scroll_animation_from, to, value));
+}
+
+static gboolean
+drop_switch_timeout_cb (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  priv->drop_switch_timeout_id = 0;
+  adw_tab_view_set_selected_page (priv->view,
+                                  priv->drop_target_tab->page);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+set_drop_target_tab (AdwTabListBase *self,
+                     TabInfo        *info)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  if (priv->drop_target_tab == info)
+    return;
+
+  if (priv->drop_target_tab)
+    g_clear_handle_id (&priv->drop_switch_timeout_id, g_source_remove);
+
+  priv->drop_target_tab = info;
+
+  if (priv->drop_target_tab) {
+    priv->drop_switch_timeout_id =
+      g_timeout_add (DROP_SWITCH_TIMEOUT,
+                     (GSourceFunc) drop_switch_timeout_cb,
+                     self);
+  }
+}
+
+static void
+adjustment_value_changed_cb (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  double value = gtk_adjustment_get_value (priv->adjustment);
+
+  update_visible (self);
+
+  if (priv->drop_target_tab) {
+    priv->drop_target_x += (value - priv->adjustment_prev_value);
+    set_drop_target_tab (self, find_tab_info_at (self, priv->drop_target_x));
+  }
+
+  priv->adjustment_prev_value = value;
+
+  if (priv->block_scrolling)
+      return;
+
+  if (priv->scroll_animation)
+    adw_animation_stop (priv->scroll_animation);
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static void
+scroll_animation_value_cb (double   value,
+                           gpointer user_data)
+{
+  gtk_widget_queue_resize (GTK_WIDGET (user_data));
+}
+
+static void
+scroll_animation_done_cb (gpointer user_data)
+{
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (user_data);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  priv->scroll_animation_done = TRUE;
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+animate_scroll (AdwTabListBase *self,
+                TabInfo        *info,
+                double          offset,
+                gint64          duration)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  if (!priv->adjustment)
+    return;
+
+  g_signal_emit (self, signals[SIGNAL_STOP_KINETIC_SCROLLING], 0);
+
+  if (priv->scroll_animation)
+    adw_animation_stop (priv->scroll_animation);
+
+  g_clear_object (&priv->scroll_animation);
+  priv->scroll_animation_done = FALSE;
+  priv->scroll_animation_from = gtk_adjustment_get_value (priv->adjustment);
+  priv->scroll_animation_tab = info;
+  priv->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.
+   */
+
+  priv->scroll_animation =
+    adw_animation_new (GTK_WIDGET (self), 0, 1, duration,
+                       scroll_animation_value_cb,
+                       self);
+
+  g_signal_connect_swapped (priv->scroll_animation, "done", G_CALLBACK (scroll_animation_done_cb), self);
+
+  adw_animation_start (priv->scroll_animation);
+}
+
+static void
+animate_scroll_relative (AdwTabListBase *self,
+                         double          delta,
+                         gint64          duration)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  double current_value = gtk_adjustment_get_value (priv->adjustment);
+
+  if (priv->scroll_animation) {
+    current_value = priv->scroll_animation_offset;
+
+    if (priv->scroll_animation_tab)
+      current_value += get_tab_position (self, priv->scroll_animation_tab);
+  }
+
+  animate_scroll (self, NULL, current_value + delta, duration);
+}
+
+static void
+scroll_to_tab_full (AdwTabListBase *self,
+                    TabInfo        *info,
+                    int             pos,
+                    gint64          duration,
+                    gboolean        keep_selected_visible)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  int tab_width;
+  double padding, value, page_size;
+
+  if (!priv->adjustment)
+    return;
+
+  tab_width = info->width;
+
+  if (tab_width < 0) {
+    priv->scheduled_scroll.info = info;
+    priv->scheduled_scroll.pos = pos;
+    priv->scheduled_scroll.duration = duration;
+    priv->scheduled_scroll.keep_selected_visible = keep_selected_visible;
+
+    gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+    return;
+  }
+
+  if (info->appear_animation)
+    tab_width = adw_tab_get_display_width (info->tab);
+
+  value = gtk_adjustment_get_value (priv->adjustment);
+  page_size = gtk_adjustment_get_page_size (priv->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 (AdwTabListBase *self,
+               TabInfo        *info,
+               gint64          duration)
+{
+  scroll_to_tab_full (self, info, -1, duration, FALSE);
+}
+
+static gboolean
+scroll_cb (AdwTabListBase     *self,
+           double              dx,
+           double              dy,
+           GtkEventController *controller)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  double page_size, pow_unit, scroll_unit;
+  GdkDevice *source_device;
+  GdkInputSource input_source;
+
+  if (!priv->adjustment)
+    return GDK_EVENT_PROPAGATE;
+
+  source_device = gtk_event_controller_get_current_event_device (controller);
+  input_source = gdk_device_get_source (source_device);
+
+  if (input_source != GDK_SOURCE_MOUSE)
+    return GDK_EVENT_PROPAGATE;
+
+  page_size = gtk_adjustment_get_page_size (priv->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 (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+    dy = -dy;
+
+  animate_scroll_relative (self, dy * scroll_unit, SCROLL_ANIMATION_DURATION);
+
+  return GDK_EVENT_STOP;
+}
+
+static void
+set_hadjustment (AdwTabListBase *self,
+                 GtkAdjustment  *adjustment)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  if (adjustment == priv->adjustment)
+    return;
+
+  if (priv->adjustment) {
+    g_signal_handlers_disconnect_by_func (priv->adjustment, adjustment_value_changed_cb, self);
+    g_signal_handlers_disconnect_by_func (priv->adjustment, update_visible, self);
+  }
+
+  g_set_object (&priv->adjustment, adjustment);
+
+  if (priv->adjustment) {
+    g_signal_connect_object (priv->adjustment, "value-changed", G_CALLBACK (adjustment_value_changed_cb), 
self, G_CONNECT_SWAPPED);
+    g_signal_connect_object (priv->adjustment, "notify::page-size", G_CALLBACK (update_visible), self, 
G_CONNECT_SWAPPED);
+  }
+
+  g_object_notify (G_OBJECT (self), "hadjustment");
+}
+
+/* Reordering */
+
+static void
+force_end_reordering (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  GList *l;
+
+  if (priv->dragging || !priv->reordered_tab)
+    return;
+
+  if (priv->reorder_animation)
+    adw_animation_stop (priv->reorder_animation);
+
+  for (l = priv->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (info->reorder_animation)
+      adw_animation_stop (info->reorder_animation);
+  }
+}
+
+static void
+check_end_reordering (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  GList *l;
+
+  if (priv->dragging || !priv->reordered_tab || priv->continue_reorder)
+    return;
+
+  if (priv->reorder_animation)
+    return;
+
+  for (l = priv->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (info->reorder_animation)
+      return;
+  }
+
+  for (l = priv->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    info->end_reorder_offset = 0;
+    info->reorder_offset = 0;
+  }
+
+  priv->reordered_tab->reorder_ignore_bounds = FALSE;
+
+  priv->tabs = g_list_remove (priv->tabs, priv->reordered_tab);
+  priv->tabs = g_list_insert (priv->tabs, priv->reordered_tab, priv->reorder_index);
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+  priv->reordered_tab = NULL;
+}
+
+static void
+start_reordering (AdwTabListBase *self,
+                  TabInfo        *info)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  priv->reordered_tab = info;
+
+  /* The reordered tab should be displayed above everything else */
+  gtk_widget_insert_before (GTK_WIDGET (priv->reordered_tab->tab),
+                            GTK_WIDGET (self), NULL);
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static int
+get_reorder_position (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  int lower, upper;
+
+  if (priv->reordered_tab->reorder_ignore_bounds)
+    return priv->reorder_x;
+
+  get_visible_range (self, &lower, &upper);
+
+  return CLAMP (priv->reorder_x, lower, upper - priv->reordered_tab->width);
+}
+
+static void
+reorder_animation_value_cb (double   value,
+                            gpointer user_data)
+{
+  TabInfo *dest_tab = user_data;
+  GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (dest_tab->tab));
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (parent);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+  double 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 - priv->reordered_tab->width;
+
+  priv->reorder_window_x = (int) round (adw_lerp (x1, x2, value));
+
+  gtk_widget_queue_allocate (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));
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (parent);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  g_clear_object (&priv->reorder_animation);
+  check_end_reordering (self);
+}
+
+static void
+animate_reordering (AdwTabListBase *self,
+                    TabInfo        *dest_tab)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  if (priv->reorder_animation)
+    adw_animation_stop (priv->reorder_animation);
+
+  priv->reorder_animation =
+    adw_animation_new (GTK_WIDGET (self), 0, 1,
+                       REORDER_ANIMATION_DURATION,
+                       reorder_animation_value_cb,
+                       dest_tab);
+
+  g_signal_connect_swapped (priv->reorder_animation, "done", G_CALLBACK (reorder_animation_done_cb), 
dest_tab);
+
+  adw_animation_start (priv->reorder_animation);
+
+  check_end_reordering (self);
+}
+
+static void
+reorder_offset_animation_value_cb (double   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));
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (parent);
+
+  g_clear_object (&info->reorder_animation);
+  check_end_reordering (self);
+}
+
+static void
+animate_reorder_offset (AdwTabListBase *self,
+                        TabInfo        *info,
+                        double          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)
+    adw_animation_stop (info->reorder_animation);
+
+  info->reorder_animation =
+    adw_animation_new (GTK_WIDGET (self), info->reorder_offset, offset,
+                       REORDER_ANIMATION_DURATION,
+                       reorder_offset_animation_value_cb,
+                       info);
+
+  g_signal_connect_swapped (info->reorder_animation, "done", G_CALLBACK (reorder_offset_animation_done_cb), 
info);
+
+  adw_animation_start (info->reorder_animation);
+}
+
+static void
+reset_reorder_animations (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  int i, original_index;
+  GList *l;
+
+  if (!adw_get_enable_animations (GTK_WIDGET (self)))
+      return;
+
+  l = find_link_for_page (self, priv->reordered_tab->page);
+  original_index = g_list_position (priv->tabs, l);
+
+  if (priv->reorder_index > original_index)
+    for (i = 0; i < priv->reorder_index - original_index; i++) {
+      l = l->next;
+      animate_reorder_offset (self, l->data, 0);
+    }
+
+  if (priv->reorder_index < original_index)
+    for (i = 0; i < original_index - priv->reorder_index; i++) {
+      l = l->prev;
+      animate_reorder_offset (self, l->data, 0);
+    }
+}
+
+static void
+page_reordered_cb (AdwTabListBase *self,
+                   AdwTabPage     *page,
+                   int             index)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  GList *link;
+  int original_index;
+  TabInfo *info, *dest_tab;
+  gboolean is_rtl;
+
+  if (adw_tab_page_get_pinned (page) != priv->pinned)
+    return;
+
+  priv->continue_reorder = priv->reordered_tab && page == priv->reordered_tab->page;
+
+  if (priv->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 (priv->tabs, link);
+
+  if (!priv->continue_reorder)
+    start_reordering (self, info);
+
+  if (priv->continue_reorder)
+    priv->reorder_x = priv->reorder_window_x;
+  else
+    priv->reorder_x = info->pos;
+
+  priv->reorder_index = index;
+
+  if (!priv->pinned)
+    priv->reorder_index -= adw_tab_view_get_n_pinned_pages (priv->view);
+
+  dest_tab = g_list_nth_data (priv->tabs, priv->reorder_index);
+
+  if (info == priv->selected_tab)
+    scroll_to_tab_full (self, priv->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 (adw_get_enable_animations (GTK_WIDGET (self)) &&
+      gtk_widget_get_mapped (GTK_WIDGET (self))) {
+    int i;
+
+    if (priv->reorder_index > original_index)
+      for (i = 0; i < priv->reorder_index - original_index; i++) {
+        link = link->next;
+        animate_reorder_offset (self, link->data, is_rtl ? 1 : -1);
+      }
+
+    if (priv->reorder_index < original_index)
+      for (i = 0; i < original_index - priv->reorder_index; i++) {
+        link = link->prev;
+        animate_reorder_offset (self, link->data, is_rtl ? -1 : 1);
+      }
+  }
+
+  priv->continue_reorder = FALSE;
+}
+
+static void
+update_drag_reodering (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  gboolean is_rtl, after_selected, found_index;
+  int x;
+  int i = 0;
+  int width;
+  GList *l;
+
+  if (!priv->dragging)
+    return;
+
+  x = get_reorder_position (self);
+
+  width = adw_tab_get_display_width (priv->reordered_tab->tab);
+
+  priv->reorder_window_x = x;
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+  is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+  after_selected = FALSE;
+  found_index = FALSE;
+
+  for (l = priv->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+    int center = info->pos - calculate_tab_offset (self, info, FALSE) + info->width / 2;
+    double offset = 0;
+
+    if (x + width > center && center > x &&
+        (!found_index || after_selected)) {
+      priv->reorder_index = i;
+      found_index = TRUE;
+    }
+
+    i++;
+
+    if (info == priv->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,
+                    AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  double value, page_size;
+  double x, delta_ms, start_threshold, end_threshold, autoscroll_factor;
+  gint64 time;
+  int offset = 0;
+  int tab_width = 0;
+  int autoscroll_area = 0;
+
+  if (priv->reordered_tab) {
+    gtk_widget_measure (GTK_WIDGET (priv->reordered_tab->tab),
+                        GTK_ORIENTATION_HORIZONTAL, -1,
+                        NULL, &tab_width, NULL, NULL);
+    tab_width -= 2 * OVERLAP;
+    x = (double) priv->reorder_x + OVERLAP;
+  } else if (priv->drop_target_tab) {
+    gtk_widget_measure (GTK_WIDGET (priv->drop_target_tab->tab),
+                        GTK_ORIENTATION_HORIZONTAL, -1,
+                        NULL, &tab_width, NULL, NULL);
+    tab_width -= 2 * OVERLAP;
+    x = (double) priv->drop_target_x + OVERLAP - tab_width / 2;
+  } else {
+    return G_SOURCE_CONTINUE;
+  }
+
+  value = gtk_adjustment_get_value (priv->adjustment);
+  page_size = gtk_adjustment_get_page_size (priv->adjustment);
+  autoscroll_area = tab_width / 2;
+
+  x = CLAMP (x,
+             autoscroll_area,
+             priv->allocated_width - tab_width - autoscroll_area);
+
+  time = gdk_frame_clock_get_frame_time (frame_clock);
+  delta_ms = (time - priv->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 = adw_ease_in_cubic (autoscroll_factor);
+  priv->drag_autoscroll_prev_time = time;
+
+  if (autoscroll_factor == 0)
+    return G_SOURCE_CONTINUE;
+
+  if (autoscroll_factor > 0)
+    offset = (int) ceil (autoscroll_factor * delta_ms * AUTOSCROLL_SPEED);
+  else
+    offset = (int) floor (autoscroll_factor * delta_ms * AUTOSCROLL_SPEED);
+
+  priv->reorder_x += offset;
+  gtk_adjustment_set_value (priv->adjustment, value + offset);
+  update_drag_reodering (self);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+start_autoscroll (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  GdkFrameClock *frame_clock;
+
+  if (!priv->adjustment)
+    return;
+
+  if (priv->drag_autoscroll_cb_id)
+    return;
+
+  frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
+
+  priv->drag_autoscroll_prev_time = gdk_frame_clock_get_frame_time (frame_clock);
+  priv->drag_autoscroll_cb_id =
+    gtk_widget_add_tick_callback (GTK_WIDGET (self),
+                                  (GtkTickCallback) drag_autoscroll_cb,
+                                  self, NULL);
+}
+
+static void
+end_autoscroll (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  if (priv->drag_autoscroll_cb_id) {
+    gtk_widget_remove_tick_callback (GTK_WIDGET (self),
+                                     priv->drag_autoscroll_cb_id);
+    priv->drag_autoscroll_cb_id = 0;
+  }
+}
+
+static void
+start_drag_reodering (AdwTabListBase *self,
+                      TabInfo        *info,
+                      double          x,
+                      double          y)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  if (priv->dragging)
+    return;
+
+  if (!info)
+    return;
+
+  priv->continue_reorder = info == priv->reordered_tab;
+
+  if (priv->continue_reorder) {
+    if (priv->reorder_animation)
+      adw_animation_stop (priv->reorder_animation);
+
+    reset_reorder_animations (self);
+
+    priv->reorder_x = (int) round (x - priv->drag_offset_x);
+    priv->reorder_y = (int) round (y - priv->drag_offset_y);
+  } else
+    force_end_reordering (self);
+
+  start_autoscroll (self);
+  priv->dragging = TRUE;
+
+  if (!priv->continue_reorder)
+    start_reordering (self, info);
+}
+
+static void
+end_drag_reodering (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  TabInfo *dest_tab;
+
+  if (!priv->dragging)
+    return;
+
+  priv->dragging = FALSE;
+
+  end_autoscroll (self);
+
+  dest_tab = g_list_nth_data (priv->tabs, priv->reorder_index);
+
+  if (!priv->indirect_reordering) {
+    int index = priv->reorder_index;
+
+    if (!priv->pinned)
+      index += adw_tab_view_get_n_pinned_pages (priv->view);
+
+    /* We've already reordered the tab here, no need to do it again */
+    g_signal_handlers_block_by_func (priv->view, page_reordered_cb, self);
+
+    adw_tab_view_reorder_page (priv->view, priv->reordered_tab->page, index);
+
+    g_signal_handlers_unblock_by_func (priv->view, page_reordered_cb, self);
+  }
+
+  animate_reordering (self, dest_tab);
+
+  priv->continue_reorder = FALSE;
+}
+
+static void
+reorder_begin_cb (AdwTabListBase *self,
+                  double          start_x,
+                  double          start_y,
+                  GtkGesture     *gesture)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  priv->reorder_start_pos = gtk_adjustment_get_value (priv->adjustment);
+
+  start_x += priv->reorder_start_pos;
+
+  priv->pressed_tab = find_tab_info_at (self, start_x);
+
+  priv->drag_offset_x = start_x - get_tab_position (self, priv->pressed_tab);
+  priv->drag_offset_y = start_y;
+
+  if (!priv->reorder_animation) {
+    priv->reorder_x = (int) round (start_x - priv->drag_offset_x);
+    priv->reorder_y = (int) round (start_y - priv->drag_offset_y);
+  }
+}
+
+/* Copied from gtkdragsource.c */
+static gboolean
+gtk_drag_check_threshold_double (GtkWidget *widget,
+                                 double     start_x,
+                                 double     start_y,
+                                 double     current_x,
+                                 double     current_y)
+{
+  int drag_threshold;
+
+  g_object_get (gtk_widget_get_settings (widget),
+                "gtk-dnd-drag-threshold", &drag_threshold,
+                NULL);
+
+  return (ABS (current_x - start_x) > drag_threshold ||
+          ABS (current_y - start_y) > drag_threshold);
+}
+
+static gboolean
+check_dnd_threshold (AdwTabListBase *self,
+                     double          x,
+                     double          y)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  int threshold;
+  graphene_rect_t rect;
+
+  g_object_get (gtk_widget_get_settings (GTK_WIDGET (self)),
+                "gtk-dnd-drag-threshold", &threshold,
+                NULL);
+
+  threshold *= DND_THRESHOLD_MULTIPLIER;
+
+  graphene_rect_init (&rect, 0, 0,
+                      priv->allocated_width,
+                      gtk_widget_get_height (GTK_WIDGET (self)));
+  graphene_rect_inset (&rect, -threshold, -threshold);
+
+  return !graphene_rect_contains_point (&rect, &GRAPHENE_POINT_INIT (x, y));
+}
+
+static void begin_drag (AdwTabListBase *self,
+                        GdkDevice *device);
+
+static void
+reorder_update_cb (AdwTabListBase *self,
+                   double          offset_x,
+                   double          offset_y,
+                   GtkGesture     *gesture)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  double start_x, start_y, x, y;
+  GdkDevice *device;
+
+  if (!priv->pressed_tab) {
+    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+    return;
+  }
+
+  if (!priv->dragging &&
+      !gtk_drag_check_threshold_double (GTK_WIDGET (self), 0, 0,
+                                        offset_x, offset_y))
+    return;
+
+  gtk_gesture_drag_get_start_point (GTK_GESTURE_DRAG (gesture),
+                                    &start_x, &start_y);
+
+  x = start_x + gtk_adjustment_get_value (priv->adjustment) + offset_x;
+  y = start_y + offset_y;
+
+  start_drag_reodering (self, priv->pressed_tab, x, y);
+
+  if (priv->dragging) {
+    adw_tab_view_set_selected_page (priv->view, priv->pressed_tab->page);
+    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+  } else {
+    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+    return;
+  }
+
+  priv->reorder_x = (int) round (x - priv->drag_offset_x);
+  priv->reorder_y = (int) round (y - priv->drag_offset_y);
+
+  device = gtk_event_controller_get_current_event_device (GTK_EVENT_CONTROLLER (gesture));
+
+  if (!priv->pinned &&
+      priv->pressed_tab &&
+      priv->pressed_tab != priv->reorder_placeholder &&
+      priv->pressed_tab->page &&
+      !is_touchscreen (gesture) &&
+      adw_tab_view_get_n_pages (priv->view) > 1 &&
+      check_dnd_threshold (self, x, y)) {
+    begin_drag (self, device);
+
+    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+    return;
+  }
+
+  update_drag_reodering (self);
+}
+
+static void
+reorder_end_cb (AdwTabListBase *self,
+                double          offset_x,
+                double          offset_y,
+                GtkGesture     *gesture)
+{
+  end_drag_reodering (self);
+}
+
+/* Selection */
+
+static void
+reset_focus (AdwTabListBase *self)
+{
+  GtkRoot *root = gtk_widget_get_root (GTK_WIDGET (self));
+
+  gtk_widget_set_focus_child (GTK_WIDGET (self), NULL);
+
+  if (root)
+    gtk_root_set_focus (root, NULL);
+}
+
+static void
+select_page (AdwTabListBase *self,
+             AdwTabPage     *page)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  if (!page) {
+    priv->selected_tab = NULL;
+
+    reset_focus (self);
+
+    return;
+  }
+
+  priv->selected_tab = find_info_for_page (self, page);
+
+  if (!priv->selected_tab) {
+    if (gtk_widget_get_focus_child (GTK_WIDGET (self)))
+      reset_focus (self);
+
+    return;
+  }
+
+  if (adw_tab_bar_tabs_have_visible_focus (priv->tab_bar))
+    gtk_widget_grab_focus (GTK_WIDGET (priv->selected_tab->tab));
+
+  gtk_widget_set_focus_child (GTK_WIDGET (self),
+                              GTK_WIDGET (priv->selected_tab->tab));
+
+  if (priv->selected_tab->width >= 0)
+    scroll_to_tab (self, priv->selected_tab, FOCUS_ANIMATION_DURATION);
+}
+
+/* Opening */
+
+static gboolean
+extra_drag_drop_cb (AdwTab         *tab,
+                    GValue         *value,
+                    AdwTabListBase *self)
+{
+  gboolean ret = GDK_EVENT_PROPAGATE;
+  AdwTabPage *page = adw_tab_get_page (tab);
+
+  g_signal_emit (self, signals[SIGNAL_EXTRA_DRAG_DROP], 0, page, value, &ret);
+
+  return ret;
+}
+
+static void
+appear_animation_value_cb (double   value,
+                           gpointer user_data)
+{
+  TabInfo *info = user_data;
+
+  info->appear_progress = value;
+
+  if (GTK_IS_WIDGET (info->tab))
+    gtk_widget_queue_resize (GTK_WIDGET (info->tab));
+}
+
+static void
+open_animation_done_cb (gpointer user_data)
+{
+  TabInfo *info = user_data;
+
+  g_clear_object (&info->appear_animation);
+}
+
+static TabInfo *
+create_tab_info (AdwTabListBase *self,
+                 AdwTabPage     *page)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  TabInfo *info;
+
+  info = g_new0 (TabInfo, 1);
+  info->page = page;
+  info->pos = -1;
+  info->width = -1;
+  info->tab = adw_tab_new (priv->view, priv->pinned);
+
+  adw_tab_set_page (info->tab, page);
+  adw_tab_set_inverted (info->tab, priv->inverted);
+  adw_tab_setup_extra_drop_target (info->tab,
+                                   priv->extra_drag_actions,
+                                   priv->extra_drag_types,
+                                   priv->extra_drag_n_types);
+
+  gtk_widget_set_parent (GTK_WIDGET (info->tab), GTK_WIDGET (self));
+
+  g_signal_connect_object (info->tab, "extra-drag-drop", G_CALLBACK (extra_drag_drop_cb), self, 0);
+
+  return info;
+}
+
+static void
+page_attached_cb (AdwTabListBase *self,
+                  AdwTabPage     *page,
+                  int             position)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  TabInfo *info;
+  GList *l;
+
+  if (adw_tab_page_get_pinned (page) != priv->pinned)
+    return;
+
+  if (!priv->pinned)
+    position -= adw_tab_view_get_n_pinned_pages (priv->view);
+
+  set_tab_resize_mode (self, TAB_RESIZE_NORMAL);
+  force_end_reordering (self);
+
+  info = create_tab_info (self, page);
+
+  info->notify_needs_attention_id =
+    g_signal_connect_object (page,
+                             "notify::needs-attention",
+                             G_CALLBACK (update_visible),
+                             self,
+                             G_CONNECT_SWAPPED);
+
+  info->appear_animation =
+    adw_animation_new (GTK_WIDGET (self), 0, 1,
+                       OPEN_ANIMATION_DURATION,
+                       appear_animation_value_cb,
+                       info);
+
+  g_signal_connect_swapped (info->appear_animation, "done", G_CALLBACK (open_animation_done_cb), info);
+
+  l = find_nth_alive_tab (self, position);
+  priv->tabs = g_list_insert_before (priv->tabs, l, info);
+
+  priv->n_tabs++;
+
+  adw_animation_start (info->appear_animation);
+
+  if (page == adw_tab_view_get_selected_page (priv->view))
+    adw_tab_list_base_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));
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (parent);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  g_clear_object (&info->appear_animation);
+
+  priv->tabs = g_list_remove (priv->tabs, info);
+
+  if (info->reorder_animation)
+    adw_animation_stop (info->reorder_animation);
+
+  if (priv->reorder_animation)
+    adw_animation_stop (priv->reorder_animation);
+
+  if (priv->pressed_tab == info)
+    priv->pressed_tab = NULL;
+
+  if (priv->reordered_tab == info)
+    priv->reordered_tab = NULL;
+
+  remove_and_free_tab_info (info);
+
+  priv->n_tabs--;
+}
+
+static void
+page_detached_cb (AdwTabListBase *self,
+                  AdwTabPage     *page)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  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 (priv->hovering && !priv->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, priv->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)))
+    adw_tab_list_base_try_focus_selected_tab (self);
+
+  if (info == priv->selected_tab)
+    adw_tab_list_base_select_page (self, NULL);
+
+  adw_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)
+    adw_animation_stop (info->appear_animation);
+
+  info->appear_animation =
+    adw_animation_new (GTK_WIDGET (self), info->appear_progress, 0,
+                       CLOSE_ANIMATION_DURATION,
+                       appear_animation_value_cb,
+                       info);
+
+  g_signal_connect_swapped (info->appear_animation, "done", G_CALLBACK (close_animation_done_cb), info);
+
+  adw_animation_start (info->appear_animation);
+}
+
+/* Tab DND */
+
+#define ADW_TYPE_TAB_LIST_BASE_ROOT_CONTENT (adw_tab_list_base_root_content_get_type ())
+
+G_DECLARE_FINAL_TYPE (AdwTabListBaseRootContent, adw_tab_list_base_root_content, ADW, 
TAB_LIST_BASE_ROOT_CONTENT, GdkContentProvider)
+
+struct _AdwTabListBaseRootContent
+{
+  GdkContentProvider parent_instance;
+
+  AdwTabListBase *tab_list_base;
+};
+
+G_DEFINE_TYPE (AdwTabListBaseRootContent, adw_tab_list_base_root_content, GDK_TYPE_CONTENT_PROVIDER)
+
+static GdkContentFormats *
+adw_tab_list_base_root_content_ref_formats (GdkContentProvider *provider)
+{
+  return gdk_content_formats_new ((const char *[1]) { "application/x-rootwindow-drop" }, 1);
+}
+
+static void
+adw_tab_list_base_root_content_write_mime_type_async (GdkContentProvider  *provider,
+                                                      const char          *mime_type,
+                                                      GOutputStream       *stream,
+                                                      int                  io_priority,
+                                                      GCancellable        *cancellable,
+                                                      GAsyncReadyCallback  callback,
+                                                      gpointer             user_data)
+{
+  AdwTabListBaseRootContent *self = ADW_TAB_LIST_BASE_ROOT_CONTENT (provider);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self->tab_list_base);
+  g_autoptr (GTask) task = NULL;
+
+  priv->should_detach_into_new_window = TRUE;
+
+  task = g_task_new (self, cancellable, callback, user_data);
+  g_task_set_priority (task, io_priority);
+  g_task_set_source_tag (task, adw_tab_list_base_root_content_write_mime_type_async);
+  g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+adw_tab_list_base_root_content_write_mime_type_finish (GdkContentProvider  *provider,
+                                                       GAsyncResult        *result,
+                                                       GError             **error)
+{
+  return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+adw_tab_list_base_root_content_finalize (GObject *object)
+{
+  AdwTabListBaseRootContent *self = ADW_TAB_LIST_BASE_ROOT_CONTENT (object);
+
+  g_clear_object (&self->tab_list_base);
+
+  G_OBJECT_CLASS (adw_tab_list_base_root_content_parent_class)->finalize (object);
+}
+
+static void
+adw_tab_list_base_root_content_class_init (AdwTabListBaseRootContentClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GdkContentProviderClass *provider_class = GDK_CONTENT_PROVIDER_CLASS (klass);
+
+  object_class->finalize = adw_tab_list_base_root_content_finalize;
+
+  provider_class->ref_formats = adw_tab_list_base_root_content_ref_formats;
+  provider_class->write_mime_type_async = adw_tab_list_base_root_content_write_mime_type_async;
+  provider_class->write_mime_type_finish = adw_tab_list_base_root_content_write_mime_type_finish;
+}
+
+static void
+adw_tab_list_base_root_content_init (AdwTabListBaseRootContent *self)
+{
+}
+
+static GdkContentProvider *
+adw_tab_list_base_root_content_new (AdwTabListBase *tab_list_base)
+{
+  AdwTabListBaseRootContent *self = g_object_new (ADW_TYPE_TAB_LIST_BASE_ROOT_CONTENT, NULL);
+
+  self->tab_list_base = g_object_ref (tab_list_base);
+
+  return GDK_CONTENT_PROVIDER (self);
+}
+
+static int
+calculate_placeholder_index (AdwTabListBase *self,
+                             int             x)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  int 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 ? priv->allocated_width + OVERLAP : -OVERLAP);
+  i = 0;
+
+  for (l = priv->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 (double   value,
+                           gpointer user_data)
+{
+  TabInfo *info = user_data;
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (gtk_widget_get_parent (GTK_WIDGET (info->tab)));
+
+  appear_animation_value_cb (value, info);
+
+  update_drag_reodering (self);
+}
+
+static void
+insert_placeholder (AdwTabListBase *self,
+                    AdwTabPage     *page,
+                    int             pos)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  TabInfo *info = priv->reorder_placeholder;
+  double initial_progress = 0;
+
+  if (info) {
+    initial_progress = info->appear_progress;
+
+    if (info->appear_animation)
+      adw_animation_stop (info->appear_animation);
+  } else {
+    int index;
+
+    priv->placeholder_page = page;
+
+    info = create_tab_info (self, page);
+
+    gtk_widget_set_opacity (GTK_WIDGET (info->tab), 0);
+
+    adw_tab_set_dragging (info->tab, TRUE);
+
+    info->reorder_ignore_bounds = TRUE;
+
+    if (priv->adjustment) {
+      double page_size = gtk_adjustment_get_page_size (priv->adjustment);
+
+      if (priv->allocated_width > page_size) {
+        gtk_widget_measure (GTK_WIDGET (info->tab), GTK_ORIENTATION_HORIZONTAL, -1,
+                            NULL, &priv->placeholder_scroll_offset, NULL, NULL);
+
+        priv->placeholder_scroll_offset /= 2;
+      } else {
+        priv->placeholder_scroll_offset = 0;
+      }
+    }
+
+    index = calculate_placeholder_index (self, pos + priv->placeholder_scroll_offset);
+
+    priv->tabs = g_list_insert (priv->tabs, info, index);
+    priv->n_tabs++;
+
+    priv->reorder_placeholder = info;
+    priv->reorder_index = g_list_index (priv->tabs, info);
+
+    animate_scroll_relative (self, priv->placeholder_scroll_offset, OPEN_ANIMATION_DURATION);
+  }
+
+  info->appear_animation =
+    adw_animation_new (GTK_WIDGET (self), initial_progress, 1,
+                       OPEN_ANIMATION_DURATION,
+                       insert_animation_value_cb,
+                       info);
+
+  g_signal_connect_swapped (info->appear_animation, "done", G_CALLBACK (open_animation_done_cb), info);
+
+  adw_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));
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (parent);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  g_clear_object (&info->appear_animation);
+  priv->reorder_placeholder = NULL;
+  priv->can_remove_placeholder = TRUE;
+}
+
+static void
+replace_placeholder (AdwTabListBase *self,
+                     AdwTabPage     *page)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  TabInfo *info = priv->reorder_placeholder;
+  double initial_progress;
+
+  priv->placeholder_scroll_offset = 0;
+  gtk_widget_set_opacity (GTK_WIDGET (priv->reorder_placeholder->tab), 1);
+  adw_tab_set_dragging (info->tab, FALSE);
+
+  if (!info->appear_animation) {
+    priv->reorder_placeholder = NULL;
+
+    return;
+  }
+
+  initial_progress = info->appear_progress;
+
+  priv->can_remove_placeholder = FALSE;
+
+  adw_tab_set_page (info->tab, page);
+  info->page = page;
+
+  adw_animation_stop (info->appear_animation);
+
+  info->appear_animation =
+    adw_animation_new (GTK_WIDGET (self), initial_progress, 1,
+                       OPEN_ANIMATION_DURATION,
+                       appear_animation_value_cb,
+                       info);
+
+  g_signal_connect_swapped (info->appear_animation, "done", G_CALLBACK (replace_animation_done_cb), info);
+
+  adw_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));
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (parent);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  g_clear_object (&info->appear_animation);
+
+  if (!priv->can_remove_placeholder) {
+    adw_tab_set_page (info->tab, priv->placeholder_page);
+    info->page = priv->placeholder_page;
+
+    return;
+  }
+
+  if (priv->reordered_tab == info) {
+    force_end_reordering (self);
+
+    if (priv->reorder_animation)
+      adw_animation_stop (info->reorder_animation);
+
+    priv->reordered_tab = NULL;
+  }
+
+  if (priv->pressed_tab == info)
+    priv->pressed_tab = NULL;
+
+  priv->tabs = g_list_remove (priv->tabs, info);
+
+  remove_and_free_tab_info (info);
+
+  priv->n_tabs--;
+
+  priv->reorder_placeholder = NULL;
+}
+
+static gboolean
+remove_placeholder_scroll_cb (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  animate_scroll_relative (self, -priv->placeholder_scroll_offset, CLOSE_ANIMATION_DURATION);
+  priv->placeholder_scroll_offset = 0;
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+remove_placeholder (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  TabInfo *info = priv->reorder_placeholder;
+
+  if (!info || !info->page)
+    return;
+
+  adw_tab_set_page (info->tab, NULL);
+  info->page = NULL;
+
+  if (info->appear_animation)
+    adw_animation_stop (info->appear_animation);
+
+  g_idle_add ((GSourceFunc) remove_placeholder_scroll_cb, self);
+
+  info->appear_animation =
+    adw_animation_new (GTK_WIDGET (self), info->appear_progress, 0,
+                       CLOSE_ANIMATION_DURATION,
+                       appear_animation_value_cb,
+                       info);
+
+  g_signal_connect_swapped (info->appear_animation, "done", G_CALLBACK (remove_animation_done_cb), info);
+
+  adw_animation_start (info->appear_animation);
+}
+
+static inline AdwTabListBase *
+get_source_list (GtkDropTarget *target)
+{
+  GdkDrop *drop = gtk_drop_target_get_current_drop (target);
+  GdkDrag *drag = gdk_drop_get_drag (drop);
+
+  if (!drag)
+    return NULL;
+
+  return ADW_TAB_LIST_BASE (g_object_get_data (G_OBJECT (drag), "adw-tab-bar-drag-origin"));
+}
+
+static void
+do_drag_drop (AdwTabListBase *self,
+              AdwTabListBase *source_list)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  AdwTabListBasePrivate *source_priv = adw_tab_list_base_get_instance_private (source_list);
+  AdwTabPage *page = source_priv->detached_page;
+  int offset = (priv->pinned ? 0 : adw_tab_view_get_n_pinned_pages (priv->view));
+
+  if (priv->reorder_placeholder) {
+    replace_placeholder (self, page);
+    end_drag_reodering (self);
+
+    g_signal_handlers_block_by_func (priv->view, page_attached_cb, self);
+
+    adw_tab_view_attach_page (priv->view, page, priv->reorder_index + offset);
+
+    g_signal_handlers_unblock_by_func (priv->view, page_attached_cb, self);
+  } else {
+    adw_tab_view_attach_page (priv->view, page, priv->reorder_index + offset);
+  }
+
+  source_priv->should_detach_into_new_window = FALSE;
+  source_priv->detached_page = NULL;
+
+  priv->indirect_reordering = FALSE;
+}
+
+static void
+detach_into_new_window (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  AdwTabPage *page;
+  AdwTabView *new_view;
+
+  page = priv->detached_page;
+
+  new_view = adw_tab_view_create_window (priv->view);
+
+  if (ADW_IS_TAB_VIEW (new_view))
+    adw_tab_view_attach_page (new_view, page, 0);
+  else
+    adw_tab_view_attach_page (priv->view, page, priv->detached_index);
+
+  priv->should_detach_into_new_window = FALSE;
+}
+
+static gboolean
+is_in_the_same_group (AdwTabListBase *self,
+                      AdwTabListBase *other_list)
+{
+  /* TODO when we have groups, this should do the actual check */
+  return TRUE;
+}
+
+static void
+drag_end (AdwTabListBase *self,
+          GdkDrag        *drag,
+          gboolean        success)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  g_signal_handlers_disconnect_by_data (drag, self);
+
+  gdk_drag_drop_done (drag, success);
+
+  if (!success) {
+    adw_tab_view_attach_page (priv->view,
+                              priv->detached_page,
+                              priv->detached_index);
+
+    priv->indirect_reordering = FALSE;
+  }
+
+  priv->detached_page = NULL;
+
+  if (priv->drag_icon)
+    g_clear_pointer (&priv->drag_icon, g_free);
+
+  g_object_unref (drag);
+}
+
+static void
+tab_drop_performed_cb (AdwTabListBase *self,
+                       GdkDrag        *drag)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  /* Catch drops into our windows, but outside of tab views. If this is a false
+   * positive, it will be set to FALSE in do_drag_drop(). */
+  priv->should_detach_into_new_window = TRUE;
+}
+
+static void
+tab_dnd_finished_cb (AdwTabListBase *self,
+                     GdkDrag        *drag)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  if (priv->should_detach_into_new_window)
+    detach_into_new_window (self);
+
+  drag_end (self, drag, TRUE);
+}
+
+static void
+tab_drag_cancel_cb (AdwTabListBase      *self,
+                    GdkDragCancelReason  reason,
+                    GdkDrag             *drag)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  if (reason == GDK_DRAG_CANCEL_NO_TARGET) {
+    detach_into_new_window (self);
+    drag_end (self, drag, TRUE);
+
+    return;
+  }
+
+  priv->should_detach_into_new_window = FALSE;
+  drag_end (self, drag, FALSE);
+}
+
+static void
+create_drag_icon (AdwTabListBase *self,
+                  GdkDrag        *drag)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  DragIcon *icon;
+
+  icon = g_new0 (DragIcon, 1);
+
+  icon->drag = drag;
+
+  icon->width = predict_tab_width (self, priv->reordered_tab, FALSE);
+  icon->target_width = icon->width;
+
+  icon->tab = adw_tab_new (priv->view, FALSE);
+  adw_tab_set_page (icon->tab, priv->reordered_tab->page);
+  adw_tab_set_dragging (icon->tab, TRUE);
+  adw_tab_set_inverted (icon->tab, priv->inverted);
+  adw_tab_set_display_width (icon->tab, icon->width);
+  gtk_widget_set_halign (GTK_WIDGET (icon->tab), GTK_ALIGN_START);
+
+  gtk_drag_icon_set_child (GTK_DRAG_ICON (gtk_drag_icon_get_for_drag (drag)),
+                           GTK_WIDGET (icon->tab));
+
+  gtk_style_context_get_margin (gtk_widget_get_style_context (GTK_WIDGET (icon->tab)),
+                                &icon->tab_margin);
+
+  gtk_widget_set_size_request (GTK_WIDGET (icon->tab),
+                               icon->width + icon->tab_margin.left + icon->tab_margin.right,
+                               -1);
+
+  icon->hotspot_x = (int) priv->drag_offset_x;
+  icon->hotspot_y = (int) priv->drag_offset_y;
+
+  gdk_drag_set_hotspot (drag,
+                        icon->hotspot_x + icon->tab_margin.left,
+                        icon->hotspot_y + icon->tab_margin.top);
+
+  priv->drag_icon = icon;
+}
+
+static void
+icon_resize_animation_value_cb (double   value,
+                                gpointer user_data)
+{
+  DragIcon *icon = user_data;
+  double relative_pos;
+
+  relative_pos = (double) icon->hotspot_x / icon->width;
+
+  icon->width = (int) round (value);
+
+  adw_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 = (int) round (icon->width * relative_pos);
+
+  gdk_drag_set_hotspot (icon->drag,
+                        icon->hotspot_x + icon->tab_margin.left,
+                        icon->hotspot_y + icon->tab_margin.top);
+
+  gtk_widget_queue_resize (GTK_WIDGET (icon->tab));
+}
+
+static void
+icon_resize_animation_done_cb (gpointer user_data)
+{
+  DragIcon *icon = user_data;
+
+  g_clear_object (&icon->resize_animation);
+}
+
+static void
+resize_drag_icon (AdwTabListBase *self,
+                  int             width)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  DragIcon *icon = priv->drag_icon;
+
+  if (width == icon->target_width)
+    return;
+
+  if (icon->resize_animation)
+    adw_animation_stop (icon->resize_animation);
+
+  icon->target_width = width;
+
+  icon->resize_animation =
+    adw_animation_new (GTK_WIDGET (icon->tab), icon->width, width,
+                       ICON_RESIZE_ANIMATION_DURATION,
+                       icon_resize_animation_value_cb,
+                       icon);
+
+  g_signal_connect_swapped (icon->resize_animation, "done", G_CALLBACK (icon_resize_animation_done_cb), 
icon);
+
+  adw_animation_start (icon->resize_animation);
+}
+
+static void
+begin_drag (AdwTabListBase *self,
+            GdkDevice      *device)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  GtkNative *native;
+  GdkSurface *surface;
+  GdkContentProvider *content;
+  GdkDrag *drag;
+  TabInfo *detached_info;
+  AdwTab *detached_tab;
+
+  native = gtk_widget_get_native (GTK_WIDGET (self));
+  surface = gtk_native_get_surface (native);
+
+  priv->hovering = TRUE;
+  priv->pressed_tab = NULL;
+
+  detached_info = priv->reordered_tab;
+  detached_tab = g_object_ref (detached_info->tab);
+  priv->detached_page = detached_info->page;
+
+  priv->indirect_reordering = TRUE;
+
+  content = gdk_content_provider_new_union ((GdkContentProvider *[2]) {
+                                              adw_tab_list_base_root_content_new (self),
+                                              gdk_content_provider_new_typed (ADW_TYPE_TAB_PAGE, 
detached_info->page)
+                                            }, 2);
+
+  drag = gdk_drag_begin (surface, device, content, GDK_ACTION_MOVE,
+                         priv->reorder_x, priv->reorder_y);
+
+  g_object_set_data (G_OBJECT (drag), "adw-tab-bar-drag-origin", self);
+
+  g_signal_connect_swapped (drag, "drop-performed",
+                            G_CALLBACK (tab_drop_performed_cb), self);
+  g_signal_connect_swapped (drag, "dnd-finished",
+                            G_CALLBACK (tab_dnd_finished_cb), self);
+  g_signal_connect_swapped (drag, "cancel",
+                            G_CALLBACK (tab_drag_cancel_cb), self);
+
+  create_drag_icon (self, drag);
+
+  end_drag_reodering (self);
+  update_hover (self);
+
+  gtk_widget_set_opacity (GTK_WIDGET (detached_tab), 0);
+  priv->detached_index = adw_tab_view_get_page_position (priv->view, priv->detached_page);
+
+  adw_tab_view_detach_page (priv->view, priv->detached_page);
+
+  priv->indirect_reordering = FALSE;
+
+  gtk_widget_measure (GTK_WIDGET (detached_tab),
+                      GTK_ORIENTATION_HORIZONTAL, -1,
+                      NULL, &priv->placeholder_scroll_offset, NULL, NULL);
+  priv->placeholder_scroll_offset /= 2;
+
+  animate_scroll_relative (self, -priv->placeholder_scroll_offset, CLOSE_ANIMATION_DURATION);
+
+  g_object_unref (detached_tab);
+}
+
+static GdkDragAction
+tab_drag_enter_motion_cb (AdwTabListBase *self,
+                          double          x,
+                          double          y,
+                          GtkDropTarget  *target)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  AdwTabListBasePrivate *source_priv;
+  AdwTabListBase *source_list;
+
+  if (priv->pinned)
+    return 0;
+
+  source_list = get_source_list (target);
+
+  if (!source_list)
+    return 0;
+
+  source_priv = adw_tab_list_base_get_instance_private (source_list);
+
+  if (!priv->view || !is_in_the_same_group (self, source_list))
+    return 0;
+
+  x += gtk_adjustment_get_value (priv->adjustment);
+
+  priv->can_remove_placeholder = FALSE;
+
+  if (!priv->reorder_placeholder || !priv->reorder_placeholder->page) {
+    AdwTabPage *page = source_priv->detached_page;
+    double center = x - source_priv->drag_icon->hotspot_x + source_priv->drag_icon->width / 2;
+
+    insert_placeholder (self, page, center);
+
+    priv->indirect_reordering = TRUE;
+
+    resize_drag_icon (source_list, predict_tab_width (self, priv->reorder_placeholder, TRUE));
+    adw_tab_set_display_width (priv->reorder_placeholder->tab, source_priv->drag_icon->target_width);
+    adw_tab_set_inverted (source_priv->drag_icon->tab, priv->inverted);
+
+    priv->drag_offset_x = source_priv->drag_icon->hotspot_x;
+    priv->drag_offset_y = source_priv->drag_icon->hotspot_y;
+
+    priv->reorder_x = (int) round (x - source_priv->drag_icon->hotspot_x);
+
+    start_drag_reodering (self, priv->reorder_placeholder, x, y);
+
+    return GDK_ACTION_MOVE;
+  }
+
+  priv->reorder_x = (int) round (x - source_priv->drag_icon->hotspot_x);
+
+  update_drag_reodering (self);
+
+  return GDK_ACTION_MOVE;
+}
+
+static void
+tab_drag_leave_cb (AdwTabListBase *self,
+                   GtkDropTarget  *target)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  AdwTabListBase *source_list;
+
+  if (!priv->indirect_reordering)
+    return;
+
+  if (priv->pinned)
+    return;
+
+  source_list = get_source_list (target);
+
+  if (!source_list)
+    return;
+
+  if (!priv->view || !is_in_the_same_group (self, source_list))
+    return;
+
+  priv->can_remove_placeholder = TRUE;
+
+  end_drag_reodering (self);
+  remove_placeholder (self);
+
+  priv->indirect_reordering = FALSE;
+}
+
+static gboolean
+tab_drag_drop_cb (AdwTabListBase *self,
+                  const GValue   *value,
+                  double          x,
+                  double          y,
+                  GtkDropTarget  *target)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  AdwTabListBase *source_list;
+
+  if (priv->pinned)
+    return GDK_EVENT_PROPAGATE;
+
+  source_list = get_source_list (target);
+
+  if (!source_list)
+    return GDK_EVENT_PROPAGATE;
+
+  if (!priv->view || !is_in_the_same_group (self, source_list))
+    return GDK_EVENT_PROPAGATE;
+
+  do_drag_drop (self, source_list);
+
+  return GDK_EVENT_STOP;
+}
+
+static gboolean
+view_drag_drop_cb (AdwTabListBase *self,
+                   const GValue   *value,
+                   double          x,
+                   double          y,
+                   GtkDropTarget  *target)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  AdwTabListBase *source_list;
+
+  if (priv->pinned)
+    return GDK_EVENT_PROPAGATE;
+
+  source_list = get_source_list (target);
+
+  if (!source_list)
+    return GDK_EVENT_PROPAGATE;
+
+  if (!priv->view || !is_in_the_same_group (self, source_list))
+    return GDK_EVENT_PROPAGATE;
+
+  priv->reorder_index = adw_tab_view_get_n_pages (priv->view) -
+                        adw_tab_view_get_n_pinned_pages (priv->view);
+
+  do_drag_drop (self, source_list);
+
+  return GDK_EVENT_STOP;
+}
+
+/* DND autoscrolling */
+
+static gboolean
+reset_drop_target_tab_cb (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  priv->reset_drop_target_tab_id = 0;
+  set_drop_target_tab (self, NULL);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+drag_leave_cb (AdwTabListBase          *self,
+               GtkDropControllerMotion *controller)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  GdkDrop *drop = gtk_drop_controller_motion_get_drop (controller);
+  GdkDrag *drag = gdk_drop_get_drag (drop);
+  AdwTabListBase *source = ADW_TAB_LIST_BASE (g_object_get_data (G_OBJECT (drag),
+                                                      "adw-tab-bar-drag-origin"));
+
+  if (source)
+    return;
+
+  if (!priv->reset_drop_target_tab_id)
+    priv->reset_drop_target_tab_id =
+      g_idle_add ((GSourceFunc) reset_drop_target_tab_cb, self);
+
+  end_autoscroll (self);
+}
+
+static void
+drag_enter_motion_cb (AdwTabListBase          *self,
+                      double                   x,
+                      double                   y,
+                      GtkDropControllerMotion *controller)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  TabInfo *info;
+  GdkDrop *drop = gtk_drop_controller_motion_get_drop (controller);
+  GdkDrag *drag = gdk_drop_get_drag (drop);
+  AdwTabListBase *source = ADW_TAB_LIST_BASE (g_object_get_data (G_OBJECT (drag),
+                                                      "adw-tab-bar-drag-origin"));
+
+  if (source)
+    return;
+
+  x += gtk_adjustment_get_value (priv->adjustment);
+
+  info = find_tab_info_at (self, x);
+
+  if (!info) {
+    drag_leave_cb (self, controller);
+
+    return;
+  }
+
+  priv->drop_target_x = x;
+  set_drop_target_tab (self, info);
+
+  start_autoscroll (self);
+}
+
+/* Context menu */
+
+static gboolean
+reset_setup_menu_cb (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  g_signal_emit_by_name (priv->view, "setup-menu", NULL);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+touch_menu_notify_visible_cb (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  if (!priv->context_menu || gtk_widget_get_visible (GTK_WIDGET (priv->context_menu)))
+    return;
+
+  priv->hovering = FALSE;
+  update_hover (self);
+
+  g_idle_add ((GSourceFunc) reset_setup_menu_cb, self);
+}
+
+static void
+do_popup (AdwTabListBase *self,
+          TabInfo        *info,
+          double          x,
+          double          y)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  GMenuModel *model = adw_tab_view_get_menu_model (priv->view);
+  GdkRectangle rect;
+
+  if (!G_IS_MENU_MODEL (model))
+    return;
+
+  g_signal_emit_by_name (priv->view, "setup-menu", info->page);
+
+  if (!priv->context_menu) {
+    priv->context_menu = GTK_POPOVER (gtk_popover_menu_new_from_model (model));
+    gtk_widget_set_parent (GTK_WIDGET (priv->context_menu), GTK_WIDGET (self));
+    gtk_popover_set_position (priv->context_menu, GTK_POS_BOTTOM);
+    gtk_popover_set_has_arrow (priv->context_menu, FALSE);
+
+    if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+      gtk_widget_set_halign (GTK_WIDGET (priv->context_menu), GTK_ALIGN_END);
+    else
+      gtk_widget_set_halign (GTK_WIDGET (priv->context_menu), GTK_ALIGN_START);
+
+    g_signal_connect_object (priv->context_menu, "notify::visible",
+                             G_CALLBACK (touch_menu_notify_visible_cb), self,
+                             G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+  }
+
+  if (x >= 0 && y >= 0) {
+    rect.x = x;
+    rect.y = y;
+  } else {
+    rect.x = info->pos;
+    rect.y = gtk_widget_get_allocated_height (GTK_WIDGET (info->tab));
+
+    if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+      rect.x += info->width;
+  }
+
+  rect.x -= gtk_adjustment_get_value (priv->adjustment);
+  rect.width = 0;
+  rect.height = 0;
+
+  gtk_popover_set_pointing_to (priv->context_menu, &rect);
+
+  gtk_popover_popup (priv->context_menu);
+}
+
+static void
+long_pressed_cb (AdwTabListBase *self,
+                 double          x,
+                 double          y,
+                 GtkGesture     *gesture)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  TabInfo *info = find_tab_info_at (self, x);
+
+  gtk_gesture_set_state (priv->drag_gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+  if (!info || !info->page) {
+    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+    return;
+  }
+
+  x += gtk_adjustment_get_value (priv->adjustment);
+
+  gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+  do_popup (self, priv->pressed_tab, x, y);
+}
+
+static void
+popup_menu_cb (GtkWidget  *widget,
+               const char *action_name,
+               GVariant   *parameter)
+{
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (widget);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  if (priv->selected_tab && priv->selected_tab->page)
+    do_popup (self, priv->selected_tab, -1, -1);
+}
+
+/* Clicking */
+
+static void
+handle_click (AdwTabListBase *self,
+              TabInfo        *info,
+              GtkGesture     *gesture)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  gboolean can_grab_focus;
+
+  if (priv->adjustment) {
+    int pos = get_tab_position (self, info);
+    double value = gtk_adjustment_get_value (priv->adjustment);
+    double page_size = gtk_adjustment_get_page_size (priv->adjustment);
+
+    if (pos + OVERLAP < value ||
+        pos + info->width - OVERLAP > value + page_size) {
+      gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+
+      scroll_to_tab (self, info, SCROLL_ANIMATION_DURATION);
+
+      return;
+    }
+  }
+
+  can_grab_focus = adw_tab_bar_tabs_have_visible_focus (priv->tab_bar);
+
+  if (info == priv->selected_tab)
+    can_grab_focus = TRUE;
+  else
+    adw_tab_view_set_selected_page (priv->view, info->page);
+
+  if (can_grab_focus)
+    gtk_widget_grab_focus (GTK_WIDGET (info->tab));
+  else
+    activate_tab (self);
+}
+
+static void
+pressed_cb (AdwTabListBase *self,
+            int             n_press,
+            double          x,
+            double          y,
+            GtkGesture     *gesture)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  TabInfo *info;
+  GdkEvent *event;
+  GdkEventSequence *current;
+  guint button;
+
+  if (is_touchscreen (gesture))
+    return;
+
+  x += gtk_adjustment_get_value (priv->adjustment);
+
+  info = find_tab_info_at (self, x);
+
+  if (!info || !info->page) {
+    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+    return;
+  }
+
+  current = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
+  event = gtk_gesture_get_last_event (gesture, current);
+
+   if (gdk_event_triggers_context_menu (event)) {
+    do_popup (self, info, x, y);
+    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+    gtk_event_controller_reset (GTK_EVENT_CONTROLLER (gesture));
+
+    return;
+  }
+
+  button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
+
+  if (button == GDK_BUTTON_MIDDLE) {
+    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+    adw_tab_view_close_page (priv->view, info->page);
+
+    return;
+  }
+
+  if (button != GDK_BUTTON_PRIMARY) {
+    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+    return;
+  }
+
+  handle_click (self, info, gesture);
+}
+
+static void
+released_cb (AdwTabListBase *self,
+             int             n_press,
+             double          x,
+             double          y,
+             GtkGesture     *gesture)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  TabInfo *info;
+
+  if (!is_touchscreen (gesture))
+    return;
+
+  x += gtk_adjustment_get_value (priv->adjustment);
+
+  info = find_tab_info_at (self, x);
+
+  if (!info || !info->page) {
+    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+    return;
+  }
+
+  handle_click (self, info, gesture);
+}
+
+/* Overrides */
+
+static void
+adw_tab_list_base_measure (GtkWidget      *widget,
+                           GtkOrientation  orientation,
+                           int             for_size,
+                           int            *minimum,
+                           int            *natural,
+                           int            *minimum_baseline,
+                           int            *natural_baseline)
+{
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (widget);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  int min, nat;
+
+  if (priv->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) {
+    int width = priv->end_padding;
+    GList *l;
+
+    for (l = priv->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+      int child_width;
+
+      gtk_widget_measure (GTK_WIDGET (info->tab), orientation, -1,
+                          NULL, &child_width, NULL, NULL);
+
+      width += calculate_tab_width (info, child_width) - OVERLAP;
+    }
+
+    if (!priv->pinned)
+      width -= OVERLAP;
+
+    min = nat = MAX (priv->last_width, width);
+  } else {
+    GList *l;
+
+    min = nat = 0;
+
+    for (l = priv->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+      int child_min, child_nat;
+
+      gtk_widget_measure (GTK_WIDGET (info->tab), orientation, -1,
+                          &child_min, &child_nat, NULL, NULL);
+
+      if (child_min > min)
+        min = child_min;
+
+      if (child_nat > nat)
+        nat = child_nat;
+    }
+  }
+
+  if (minimum)
+    *minimum = min;
+
+  if (natural)
+    *natural = nat;
+
+  if (minimum_baseline)
+    *minimum_baseline = -1;
+
+  if (natural_baseline)
+    *natural_baseline = -1;
+}
+
+static void
+adw_tab_list_base_size_allocate (GtkWidget *widget,
+                                 int        width,
+                                 int        height,
+                                 int        baseline)
+{
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (widget);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  gboolean is_rtl;
+  GList *l;
+  GtkAllocation child_allocation;
+  int pos;
+  double value;
+
+  gtk_widget_measure (priv->background, GTK_ORIENTATION_HORIZONTAL, -1,
+                      NULL, NULL, NULL, NULL);
+  gtk_widget_allocate (priv->background, width, height, baseline, NULL);
+
+  adw_tab_list_base_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+                       &priv->allocated_width, NULL, NULL, NULL);
+  priv->allocated_width = MAX (priv->allocated_width, width);
+
+  value = gtk_adjustment_get_value (priv->adjustment);
+
+  gtk_adjustment_configure (priv->adjustment,
+                            value,
+                            0,
+                            priv->allocated_width,
+                            width * 0.1,
+                            width * 0.9,
+                            width);
+
+  if (priv->context_menu)
+    gtk_popover_present (priv->context_menu);
+
+  if (!priv->n_tabs)
+    return;
+
+  is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
+
+  if (priv->pinned) {
+    for (l = priv->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+      int child_width;
+
+      gtk_widget_measure (GTK_WIDGET (info->tab), GTK_ORIENTATION_HORIZONTAL, -1,
+                          NULL, &child_width, NULL, NULL);
+
+      info->width = calculate_tab_width (info, child_width);
+    }
+  } else if (priv->tab_resize_mode == TAB_RESIZE_FIXED_TAB_WIDTH) {
+    priv->end_padding = priv->allocated_width + OVERLAP;
+
+    for (l = priv->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+
+      info->width = calculate_tab_width (info, info->last_width);
+      priv->end_padding -= info->width - OVERLAP;
+    }
+  } else {
+    int tab_width = get_base_tab_width (self, FALSE);
+    int excess = priv->allocated_width + OVERLAP - priv->end_padding;
+
+    for (l = priv->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 = priv->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+
+      if (excess >= 0)
+          break;
+
+      info->width--;
+      excess++;
+    }
+  }
+
+  pos = is_rtl ? priv->allocated_width + OVERLAP : -OVERLAP;
+
+  for (l = priv->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (!info->appear_animation)
+      adw_tab_set_display_width (info->tab, info->width);
+    else if (info->page && info != priv->reorder_placeholder)
+      adw_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 == priv->reordered_tab) ? priv->reorder_window_x : info->pos) - (int) floor 
(value);
+    child_allocation.y = 0;
+    child_allocation.width = info->width;
+    child_allocation.height = height;
+
+    gtk_widget_size_allocate (GTK_WIDGET (info->tab), &child_allocation, baseline);
+
+    pos += (is_rtl ? -1 : 1) * (info->width - OVERLAP);
+  }
+
+  if (priv->scheduled_scroll.info) {
+    scroll_to_tab_full (self,
+                        priv->scheduled_scroll.info,
+                        priv->scheduled_scroll.pos,
+                        priv->scheduled_scroll.duration,
+                        priv->scheduled_scroll.keep_selected_visible);
+    priv->scheduled_scroll.info = NULL;
+  }
+
+  if (priv->scroll_animation) {
+    priv->block_scrolling = TRUE;
+    gtk_adjustment_set_value (priv->adjustment,
+                              get_scroll_animation_value (self));
+    priv->block_scrolling = FALSE;
+
+    if (priv->scroll_animation_done) {
+        priv->scroll_animation_done = FALSE;
+        priv->scroll_animation_tab = NULL;
+        g_clear_object (&priv->scroll_animation);
+    }
+  }
+
+  update_visible (self);
+}
+
+static void
+snapshot_tab (AdwTabListBase *self,
+              GtkSnapshot    *snapshot,
+              TabInfo        *info,
+              cairo_region_t *clip_region)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  cairo_rectangle_int_t rect = { 0, 0, 0, 0 };
+  gboolean clip = FALSE;
+  int pos, width, scroll_pos;
+  int i, n;
+
+  if (gtk_widget_get_opacity (GTK_WIDGET (info->tab)) <= 0)
+    return;
+
+  rect.height = gtk_widget_get_height (GTK_WIDGET (self));
+  scroll_pos = (int) floor (gtk_adjustment_get_value (priv->adjustment));
+
+  pos = get_tab_position (self, info);
+  width = info->width;
+
+  n = cairo_region_num_rectangles (clip_region);
+  for (i = 0; i < n; i++) {
+    cairo_rectangle_int_t clip_rect;
+    int x1, x2;
+
+    cairo_region_get_rectangle (clip_region, i, &clip_rect);
+    x1 = clip_rect.x + scroll_pos;
+    x2 = x1 + clip_rect.width;
+
+    if (x1 < pos && x2 > pos + width) {
+      clip = FALSE;
+      break;
+    }
+
+    if (x2 < pos || x1 > pos + width)
+      continue;
+
+    gtk_snapshot_push_clip (snapshot, &GRAPHENE_RECT_INIT (clip_rect.x, clip_rect.y, clip_rect.width, 
clip_rect.height));
+    gtk_widget_snapshot_child (GTK_WIDGET (self), GTK_WIDGET (info->tab), snapshot);
+    gtk_snapshot_pop (snapshot);
+    clip = TRUE;
+  }
+
+  if (!clip)
+    gtk_widget_snapshot_child (GTK_WIDGET (self), GTK_WIDGET (info->tab), snapshot);
+
+  rect.x = pos - scroll_pos;
+  rect.width = width;
+  cairo_region_subtract_rectangle (clip_region, &rect);
+}
+
+static void
+adw_tab_list_base_snapshot (GtkWidget   *widget,
+                            GtkSnapshot *snapshot)
+{
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (widget);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  int w = gtk_widget_get_width (widget);
+  int h = gtk_widget_get_height (widget);
+  cairo_rectangle_int_t rect = { 0, 0, 0, 0 };
+  cairo_region_t *region;
+  int i, n;
+  GList *l;
+
+  rect.width = w;
+  rect.height = h;
+  region = cairo_region_create_rectangle (&rect);
+
+  if (priv->reordered_tab)
+    snapshot_tab (self, snapshot, priv->reordered_tab, region);
+
+  if (priv->selected_tab)
+    snapshot_tab (self, snapshot, priv->selected_tab, region);
+
+  for (l = priv->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (info == priv->reordered_tab || info == priv->selected_tab)
+      continue;
+
+    snapshot_tab (self, snapshot, info, region);
+  }
+
+  n = cairo_region_num_rectangles (region);
+  for (i = 0; i < n; i++) {
+    cairo_region_get_rectangle (region, i, &rect);
+
+    gtk_snapshot_push_clip (snapshot, &GRAPHENE_RECT_INIT (rect.x, rect.y, rect.width, rect.height));
+    gtk_widget_snapshot_child (widget, priv->background, snapshot);
+    gtk_snapshot_pop (snapshot);
+  }
+
+  cairo_region_destroy (region);
+}
+
+static gboolean
+adw_tab_list_base_focus (GtkWidget        *widget,
+                         GtkDirectionType  direction)
+{
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (widget);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  if (!priv->selected_tab)
+    return GDK_EVENT_PROPAGATE;
+
+  return gtk_widget_child_focus (GTK_WIDGET (priv->selected_tab->tab), direction);
+}
+
+static void
+adw_tab_list_base_unrealize (GtkWidget *widget)
+{
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (widget);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  g_clear_pointer ((GtkWidget **) &priv->context_menu, gtk_widget_unparent);
+
+  GTK_WIDGET_CLASS (adw_tab_list_base_parent_class)->unrealize (widget);
+}
+
+static void
+adw_tab_list_base_unmap (GtkWidget *widget)
+{
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (widget);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  force_end_reordering (self);
+
+  if (priv->drag_autoscroll_cb_id) {
+    gtk_widget_remove_tick_callback (widget, priv->drag_autoscroll_cb_id);
+    priv->drag_autoscroll_cb_id = 0;
+  }
+
+  priv->hovering = FALSE;
+  update_hover (self);
+
+  GTK_WIDGET_CLASS (adw_tab_list_base_parent_class)->unmap (widget);
+}
+
+static void
+adw_tab_list_base_direction_changed (GtkWidget        *widget,
+                                     GtkTextDirection  previous_direction)
+{
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (widget);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  double upper, page_size;
+
+  if (!priv->adjustment)
+    return;
+
+  if (gtk_widget_get_direction (widget) == previous_direction)
+    return;
+
+  upper = gtk_adjustment_get_upper (priv->adjustment);
+  page_size = gtk_adjustment_get_page_size (priv->adjustment);
+
+  gtk_adjustment_set_value (priv->adjustment,
+                            upper - page_size - priv->adjustment_prev_value);
+
+  if (priv->context_menu) {
+    if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+      gtk_widget_set_halign (GTK_WIDGET (priv->context_menu), GTK_ALIGN_END);
+    else
+      gtk_widget_set_halign (GTK_WIDGET (priv->context_menu), GTK_ALIGN_START);
+  }
+}
+
+static void
+adw_tab_list_base_dispose (GObject *object)
+{
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (object);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  g_clear_handle_id (&priv->drop_switch_timeout_id, g_source_remove);
+
+  g_clear_pointer (&priv->background, gtk_widget_unparent);
+
+  priv->drag_gesture = NULL;
+  priv->tab_bar = NULL;
+  adw_tab_list_base_set_view (self, NULL);
+  set_hadjustment (self, NULL);
+
+  G_OBJECT_CLASS (adw_tab_list_base_parent_class)->dispose (object);
+}
+
+static void
+adw_tab_list_base_finalize (GObject *object)
+{
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (object);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  g_clear_pointer (&priv->extra_drag_types, g_free);
+
+  G_OBJECT_CLASS (adw_tab_list_base_parent_class)->finalize (object);
+}
+
+static void
+adw_tab_list_base_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (object);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  switch (prop_id) {
+  case PROP_PINNED:
+    g_value_set_boolean (value, priv->pinned);
+    break;
+
+  case PROP_TAB_BAR:
+    g_value_set_object (value, priv->tab_bar);
+    break;
+
+  case PROP_VIEW:
+    g_value_set_object (value, priv->view);
+    break;
+
+  case PROP_NEEDS_ATTENTION_LEFT:
+    g_value_set_boolean (value, priv->needs_attention_left);
+    break;
+
+  case PROP_NEEDS_ATTENTION_RIGHT:
+    g_value_set_boolean (value, priv->needs_attention_right);
+    break;
+
+  case PROP_RESIZE_FROZEN:
+    g_value_set_boolean (value, priv->tab_resize_mode != TAB_RESIZE_NORMAL);
+    break;
+
+  case PROP_HADJUSTMENT:
+    g_value_set_object (value, priv->adjustment);
+    break;
+
+  case PROP_VADJUSTMENT:
+  case PROP_HSCROLL_POLICY:
+  case PROP_VSCROLL_POLICY:
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_tab_list_base_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  AdwTabListBase *self = ADW_TAB_LIST_BASE (object);
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+
+  switch (prop_id) {
+  case PROP_PINNED:
+    priv->pinned = g_value_get_boolean (value);
+    break;
+
+  case PROP_TAB_BAR:
+    priv->tab_bar = g_value_get_object (value);
+    break;
+
+  case PROP_VIEW:
+    adw_tab_list_base_set_view (self, g_value_get_object (value));
+    break;
+
+  case PROP_HADJUSTMENT:
+    set_hadjustment (self, g_value_get_object (value));
+    break;
+
+  case PROP_VADJUSTMENT:
+  case PROP_HSCROLL_POLICY:
+  case PROP_VSCROLL_POLICY:
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_tab_list_base_class_init (AdwTabListBaseClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = adw_tab_list_base_dispose;
+  object_class->finalize = adw_tab_list_base_finalize;
+  object_class->get_property = adw_tab_list_base_get_property;
+  object_class->set_property = adw_tab_list_base_set_property;
+
+  widget_class->measure = adw_tab_list_base_measure;
+  widget_class->size_allocate = adw_tab_list_base_size_allocate;
+  widget_class->snapshot = adw_tab_list_base_snapshot;
+  widget_class->focus = adw_tab_list_base_focus;
+  widget_class->unrealize = adw_tab_list_base_unrealize;
+  widget_class->unmap = adw_tab_list_base_unmap;
+  widget_class->direction_changed = adw_tab_list_base_direction_changed;
+
+  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",
+                         ADW_TYPE_TAB_BAR,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  props[PROP_VIEW] =
+    g_param_spec_object ("view",
+                         "View",
+                         "View",
+                         ADW_TYPE_TAB_VIEW,
+                         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);
+
+  g_object_class_override_property (object_class, PROP_HADJUSTMENT, "hadjustment");
+  g_object_class_override_property (object_class, PROP_VADJUSTMENT, "vadjustment");
+  g_object_class_override_property (object_class, PROP_HSCROLL_POLICY, "hscroll-policy");
+  g_object_class_override_property (object_class, PROP_VSCROLL_POLICY, "vscroll-policy");
+
+  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_DROP] =
+    g_signal_new ("extra-drag-drop",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  g_signal_accumulator_first_wins, NULL, NULL,
+                  G_TYPE_BOOLEAN,
+                  2,
+                  ADW_TYPE_TAB_PAGE,
+                  G_TYPE_VALUE);
+
+  gtk_widget_class_install_action (widget_class, "menu.popup", NULL, popup_menu_cb);
+
+  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_F10, GDK_SHIFT_MASK, "menu.popup", NULL);
+  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_Menu, 0, "menu.popup", NULL);
+
+  add_focus_bindings (widget_class, GDK_KEY_Page_Up,   GTK_DIR_TAB_BACKWARD, FALSE);
+  add_focus_bindings (widget_class, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD,  FALSE);
+  add_focus_bindings (widget_class, GDK_KEY_Home,      GTK_DIR_TAB_BACKWARD, TRUE);
+  add_focus_bindings (widget_class, GDK_KEY_End,       GTK_DIR_TAB_FORWARD,  TRUE);
+
+  add_reorder_bindings (widget_class, GDK_KEY_Left,      GTK_DIR_LEFT,         FALSE);
+  add_reorder_bindings (widget_class, GDK_KEY_Right,     GTK_DIR_RIGHT,        FALSE);
+  add_reorder_bindings (widget_class, GDK_KEY_Page_Up,   GTK_DIR_TAB_BACKWARD, FALSE);
+  add_reorder_bindings (widget_class, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD,  FALSE);
+  add_reorder_bindings (widget_class, GDK_KEY_Home,      GTK_DIR_TAB_BACKWARD, TRUE);
+  add_reorder_bindings (widget_class, GDK_KEY_End,       GTK_DIR_TAB_FORWARD,  TRUE);
+
+  gtk_widget_class_set_css_name (widget_class, "tabbox");
+}
+
+static void
+adw_tab_list_base_init (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv = adw_tab_list_base_get_instance_private (self);
+  GtkEventController *controller;
+
+  priv->can_remove_placeholder = TRUE;
+  priv->expand_tabs = TRUE;
+
+  gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN);
+
+  priv->background = adw_gizmo_new ("background", NULL, NULL, NULL, NULL, NULL, NULL);
+  gtk_widget_set_can_target (priv->background, FALSE);
+  gtk_widget_set_can_focus (priv->background, FALSE);
+  gtk_widget_set_parent (priv->background, GTK_WIDGET (self));
+
+  controller = gtk_event_controller_motion_new ();
+  g_signal_connect_swapped (controller, "motion", G_CALLBACK (motion_cb), self);
+  g_signal_connect_swapped (controller, "leave", G_CALLBACK (leave_cb), self);
+  gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+  controller = gtk_event_controller_scroll_new (GTK_EVENT_CONTROLLER_SCROLL_VERTICAL);
+  g_signal_connect_swapped (controller, "scroll", G_CALLBACK (scroll_cb), self);
+  gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+  controller = GTK_EVENT_CONTROLLER (gtk_gesture_click_new ());
+  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (controller), 0);
+  gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (controller), TRUE);
+  g_signal_connect_swapped (controller, "pressed", G_CALLBACK (pressed_cb), self);
+  g_signal_connect_swapped (controller, "released", G_CALLBACK (released_cb), self);
+  gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+  controller = GTK_EVENT_CONTROLLER (gtk_gesture_long_press_new ());
+  gtk_gesture_long_press_set_delay_factor (GTK_GESTURE_LONG_PRESS (controller), 2);
+  gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (controller), TRUE);
+  gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (controller), TRUE);
+  g_signal_connect_swapped (controller, "pressed", G_CALLBACK (long_pressed_cb), self);
+  gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+  controller = GTK_EVENT_CONTROLLER (gtk_gesture_drag_new ());
+  gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (controller), GDK_BUTTON_PRIMARY);
+  gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (controller), TRUE);
+  g_signal_connect_swapped (controller, "drag-begin", G_CALLBACK (reorder_begin_cb), self);
+  g_signal_connect_swapped (controller, "drag-update", G_CALLBACK (reorder_update_cb), self);
+  g_signal_connect_swapped (controller, "drag-end", G_CALLBACK (reorder_end_cb), self);
+  gtk_widget_add_controller (GTK_WIDGET (self), controller);
+  priv->drag_gesture = GTK_GESTURE (controller);
+
+  controller = gtk_drop_controller_motion_new ();
+  g_signal_connect_swapped (controller, "enter", G_CALLBACK (drag_enter_motion_cb), self);
+  g_signal_connect_swapped (controller, "motion", G_CALLBACK (drag_enter_motion_cb), self);
+  g_signal_connect_swapped (controller, "leave", G_CALLBACK (drag_leave_cb), self);
+  gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+  controller = GTK_EVENT_CONTROLLER (gtk_drop_target_new (ADW_TYPE_TAB_PAGE, GDK_ACTION_MOVE));
+  gtk_drop_target_set_preload (GTK_DROP_TARGET (controller), TRUE);
+  g_signal_connect_swapped (controller, "enter", G_CALLBACK (tab_drag_enter_motion_cb), self);
+  g_signal_connect_swapped (controller, "motion", G_CALLBACK (tab_drag_enter_motion_cb), self);
+  g_signal_connect_swapped (controller, "leave", G_CALLBACK (tab_drag_leave_cb), self);
+  g_signal_connect_swapped (controller, "drop", G_CALLBACK (tab_drag_drop_cb), self);
+  gtk_widget_add_controller (GTK_WIDGET (self), controller);
+}
+
+void
+adw_tab_list_base_set_view (AdwTabListBase *self,
+                            AdwTabView     *view)
+{
+  AdwTabListBasePrivate *priv;
+
+  g_return_if_fail (ADW_IS_TAB_LIST_BASE (self));
+  g_return_if_fail (view == NULL || ADW_IS_TAB_VIEW (view));
+
+  priv = adw_tab_list_base_get_instance_private (self);
+
+  if (view == priv->view)
+    return;
+
+  if (priv->view) {
+    force_end_reordering (self);
+    g_signal_handlers_disconnect_by_func (priv->view, page_attached_cb, self);
+    g_signal_handlers_disconnect_by_func (priv->view, page_detached_cb, self);
+    g_signal_handlers_disconnect_by_func (priv->view, page_reordered_cb, self);
+
+    if (!priv->pinned) {
+      gtk_widget_remove_controller (GTK_WIDGET (priv->view), priv->view_drop_target);
+      priv->view_drop_target = NULL;
+    }
+
+    g_list_free_full (priv->tabs, (GDestroyNotify) remove_and_free_tab_info);
+
+    priv->tabs = NULL;
+    priv->n_tabs = 0;
+  }
+
+  priv->view = view;
+
+  if (priv->view) {
+    int i, n_pages = adw_tab_view_get_n_pages (priv->view);
+
+    for (i = n_pages - 1; i >= 0; i--)
+      page_attached_cb (self, adw_tab_view_get_nth_page (priv->view, i), 0);
+
+    g_signal_connect_object (priv->view, "page-attached", G_CALLBACK (page_attached_cb), self, 
G_CONNECT_SWAPPED);
+    g_signal_connect_object (priv->view, "page-detached", G_CALLBACK (page_detached_cb), self, 
G_CONNECT_SWAPPED);
+    g_signal_connect_object (priv->view, "page-reordered", G_CALLBACK (page_reordered_cb), self, 
G_CONNECT_SWAPPED);
+
+    if (!priv->pinned) {
+      priv->view_drop_target = GTK_EVENT_CONTROLLER (gtk_drop_target_new (ADW_TYPE_TAB_PAGE, 
GDK_ACTION_MOVE));
+
+      g_signal_connect_object (priv->view_drop_target, "drop", G_CALLBACK (view_drag_drop_cb), self, 
G_CONNECT_SWAPPED);
+
+      gtk_widget_add_controller (GTK_WIDGET (priv->view), priv->view_drop_target);
+    }
+  }
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW]);
+}
+
+void
+adw_tab_list_base_attach_page (AdwTabListBase *self,
+                               AdwTabPage     *page,
+                               int             position)
+{
+  g_return_if_fail (ADW_IS_TAB_LIST_BASE (self));
+  g_return_if_fail (ADW_IS_TAB_PAGE (page));
+
+  page_attached_cb (self, page, position);
+}
+
+void
+adw_tab_list_base_detach_page (AdwTabListBase *self,
+                               AdwTabPage     *page)
+{
+  g_return_if_fail (ADW_IS_TAB_LIST_BASE (self));
+  g_return_if_fail (ADW_IS_TAB_PAGE (page));
+
+  page_detached_cb (self, page);
+}
+
+void
+adw_tab_list_base_select_page (AdwTabListBase *self,
+                               AdwTabPage     *page)
+{
+  g_return_if_fail (ADW_IS_TAB_LIST_BASE (self));
+  g_return_if_fail (page == NULL || ADW_IS_TAB_PAGE (page));
+
+  select_page (self, page);
+}
+
+void
+adw_tab_list_base_try_focus_selected_tab (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv;
+
+  g_return_if_fail (ADW_IS_TAB_LIST_BASE (self));
+
+  priv = adw_tab_list_base_get_instance_private (self);
+
+  if (priv->selected_tab)
+    gtk_widget_grab_focus (GTK_WIDGET (priv->selected_tab->tab));
+}
+
+gboolean
+adw_tab_list_base_is_page_focused (AdwTabListBase *self,
+                                   AdwTabPage     *page)
+{
+  TabInfo *info;
+
+  g_return_val_if_fail (ADW_IS_TAB_LIST_BASE (self), FALSE);
+  g_return_val_if_fail (ADW_IS_TAB_PAGE (page), FALSE);
+
+  info = find_info_for_page (self, page);
+
+  return info && gtk_widget_is_focus (GTK_WIDGET (info->tab));
+}
+
+void
+adw_tab_list_base_setup_extra_drop_target (AdwTabListBase *self,
+                                           GdkDragAction   actions,
+                                           GType          *types,
+                                           gsize           n_types)
+{
+  AdwTabListBasePrivate *priv;
+  GList *l;
+
+  g_return_if_fail (ADW_IS_TAB_LIST_BASE (self));
+  g_return_if_fail (n_types == 0 || types != NULL);
+
+  priv = adw_tab_list_base_get_instance_private (self);
+
+  g_clear_pointer (&priv->extra_drag_types, g_free);
+
+  priv->extra_drag_actions = actions;
+#if GLIB_CHECK_VERSION(2, 67, 3)
+  priv->extra_drag_types = g_memdup2 (types, sizeof (GType) * n_types);
+#else
+  priv->extra_drag_types = g_memdup (types, sizeof (GType) * n_types);
+#endif
+  priv->extra_drag_n_types = n_types;
+
+  for (l = priv->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    adw_tab_setup_extra_drop_target (info->tab,
+                                     priv->extra_drag_actions,
+                                     priv->extra_drag_types,
+                                     priv->extra_drag_n_types);
+  }
+}
+
+gboolean
+adw_tab_list_base_get_expand_tabs (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv;
+
+  g_return_val_if_fail (ADW_IS_TAB_LIST_BASE (self), FALSE);
+
+  priv = adw_tab_list_base_get_instance_private (self);
+
+  return priv->expand_tabs;
+}
+
+void
+adw_tab_list_base_set_expand_tabs (AdwTabListBase *self,
+                                   gboolean        expand_tabs)
+{
+  AdwTabListBasePrivate *priv;
+
+  g_return_if_fail (ADW_IS_TAB_LIST_BASE (self));
+
+  priv = adw_tab_list_base_get_instance_private (self);
+
+  expand_tabs = !!expand_tabs;
+
+  if (expand_tabs == priv->expand_tabs)
+    return;
+
+  priv->expand_tabs = expand_tabs;
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+gboolean
+adw_tab_list_base_get_inverted (AdwTabListBase *self)
+{
+  AdwTabListBasePrivate *priv;
+
+  g_return_val_if_fail (ADW_IS_TAB_LIST_BASE (self), FALSE);
+
+  priv = adw_tab_list_base_get_instance_private (self);
+
+  return priv->inverted;
+}
+
+void
+adw_tab_list_base_set_inverted (AdwTabListBase *self,
+                                gboolean        inverted)
+{
+  AdwTabListBasePrivate *priv;
+  GList *l;
+
+  g_return_if_fail (ADW_IS_TAB_LIST_BASE (self));
+
+  priv = adw_tab_list_base_get_instance_private (self);
+
+  inverted = !!inverted;
+
+  if (inverted == priv->inverted)
+    return;
+
+  priv->inverted = inverted;
+
+  for (l = priv->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    adw_tab_set_inverted (info->tab, inverted);
+  }
+}
diff --git a/src/meson.build b/src/meson.build
index 3bcd2db9..7e928669 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -169,6 +169,7 @@ src_sources = [
   'adw-tab-bar.c',
   'adw-tab-box.c',
   'adw-tab-button.c',
+  'adw-tab-list-base.c',
   'adw-tab-view.c',
   'adw-view-stack.c',
   'adw-view-switcher.c',


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