[libadwaita/wip/exalm/tab-overview: 806/807] Add AdwTabOverview




commit b26e6f1c0d73cce812bd48e691dde634955d8d52
Author: Alexander Mikhaylenko <alexm gnome org>
Date:   Fri Aug 12 03:04:59 2022 +0400

    Add AdwTabOverview

 src/adw-tab-grid-private.h            |   54 +
 src/adw-tab-grid.c                    | 3734 +++++++++++++++++++++++++++++++++
 src/adw-tab-overview-private.h        |   21 +
 src/adw-tab-overview.c                |  808 +++++++
 src/adw-tab-overview.h                |   53 +
 src/adw-tab-overview.ui               |   85 +
 src/adw-tab-thumbnail-private.h       |   50 +
 src/adw-tab-thumbnail.c               |  778 +++++++
 src/adw-tab-thumbnail.ui              |  110 +
 src/adwaita.gresources.xml            |    3 +
 src/adwaita.h                         |    1 +
 src/glsl/tab-overview.glsl            |   18 +
 src/meson.build                       |    4 +
 src/stylesheet/widgets/_tab-view.scss |   35 +-
 14 files changed, 5753 insertions(+), 1 deletion(-)
---
diff --git a/src/adw-tab-grid-private.h b/src/adw-tab-grid-private.h
new file mode 100644
index 00000000..fee08f84
--- /dev/null
+++ b/src/adw-tab-grid-private.h
@@ -0,0 +1,54 @@
+/*
+ * 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_GRID (adw_tab_grid_get_type())
+
+G_DECLARE_FINAL_TYPE (AdwTabGrid, adw_tab_grid, ADW, TAB_GRID, GtkWidget)
+
+void adw_tab_grid_set_view (AdwTabGrid *self,
+                            AdwTabView *view);
+
+void adw_tab_grid_attach_page (AdwTabGrid *self,
+                               AdwTabPage *page,
+                               int         position);
+void adw_tab_grid_detach_page (AdwTabGrid *self,
+                               AdwTabPage *page);
+void adw_tab_grid_select_page (AdwTabGrid *self,
+                               AdwTabPage *page);
+
+void adw_tab_grid_try_focus_selected_tab (AdwTabGrid *self);
+gboolean adw_tab_grid_is_page_focused    (AdwTabGrid *self,
+                                          AdwTabPage *page);
+
+void adw_tab_grid_setup_extra_drop_target (AdwTabGrid    *self,
+                                           GdkDragAction  actions,
+                                           GType         *types,
+                                           gsize          n_types);
+
+gboolean adw_tab_grid_get_expand_tabs (AdwTabGrid *self);
+void     adw_tab_grid_set_expand_tabs (AdwTabGrid *self,
+                                       gboolean    expand_tabs);
+
+gboolean adw_tab_grid_get_inverted (AdwTabGrid *self);
+void     adw_tab_grid_set_inverted (AdwTabGrid *self,
+                                    gboolean    inverted);
+
+GtkPicture *adw_tab_grid_get_transition_picture (AdwTabGrid *self);
+
+G_END_DECLS
diff --git a/src/adw-tab-grid.c b/src/adw-tab-grid.c
new file mode 100644
index 00000000..6564951c
--- /dev/null
+++ b/src/adw-tab-grid.c
@@ -0,0 +1,3734 @@
+/*
+ * 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-grid-private.h"
+
+#include "adw-animation-util.h"
+#include "adw-easing.h"
+#include "adw-gizmo-private.h"
+#include "adw-macros-private.h"
+#include "adw-tab-overview-private.h"
+#include "adw-tab-thumbnail-private.h"
+#include "adw-tab-view-private.h"
+#include "adw-timed-animation.h"
+#include "adw-widget-utils-private.h"
+#include <math.h>
+
+#define SPACING 5
+#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
+
+#define FADE_OFFSET 6.0f
+#define FADE_WIDTH 36.0f
+
+typedef enum {
+  TAB_RESIZE_NORMAL,
+  TAB_RESIZE_FIXED_TAB_WIDTH,
+  TAB_RESIZE_FIXED_END_PADDING
+} TabResizeMode;
+
+typedef struct {
+  GdkDrag *drag;
+
+  AdwTabThumbnail *tab;
+  GtkBorder tab_margin;
+
+  int hotspot_x;
+  int hotspot_y;
+
+  int width;
+  int height;
+
+  int initial_width;
+  int initial_height;
+
+  int target_width;
+  int target_height;
+  AdwAnimation *resize_animation;
+} DragIcon;
+
+typedef struct {
+  AdwTabGrid *box;
+  AdwTabPage *page;
+  AdwTabThumbnail *tab;
+  GtkWidget *container;
+
+  int final_x;
+  int final_y;
+  int final_width;
+  int final_height;
+
+  int unshifted_x;
+  int unshifted_y;
+  int pos_x;
+  int pos_y;
+  int width;
+  int height;
+  int last_width;
+  int last_height;
+
+  double index;
+  double final_index;
+
+  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 _AdwTabGrid
+{
+  GtkWidget parent_instance;
+
+  gboolean pinned;
+  AdwTabOverview *tab_overview;
+  AdwTabView *view;
+  GtkAdjustment *adjustment;
+  gboolean expand_tabs;
+  gboolean inverted;
+
+  GtkEventController *view_drop_target;
+  GtkGesture *drag_gesture;
+
+  GList *tabs;
+  int n_tabs;
+
+  GtkWidget *context_menu;
+
+  int allocated_width;
+  int allocated_height;
+  int last_width;
+  int end_padding;
+  int initial_end_padding;
+  int final_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_x;
+  int reorder_y;
+  int reorder_index;
+  int reorder_window_x;
+  int reorder_window_y;
+  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;
+  double drop_target_y;
+
+  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;
+
+  GtkWidget *needs_attention_top;
+  GtkWidget *needs_attention_bottom;
+
+
+
+
+  double n_columns;
+
+  int tab_width;
+  int tab_height;
+};
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (AdwTabGrid, adw_tab_grid, GTK_TYPE_WIDGET,
+                               G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL))
+
+enum {
+  PROP_0,
+  PROP_PINNED,
+  PROP_TAB_OVERVIEW,
+  PROP_VIEW,
+  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->container));
+
+  g_free (info);
+}
+
+static inline int
+get_tab_x (AdwTabGrid *self,
+           TabInfo    *info,
+           gboolean    final)
+{
+  if (info == self->reordered_tab)
+    return self->reorder_window_x;
+
+  return final ? info->final_x : info->pos_x;
+}
+
+static inline int
+get_tab_y (AdwTabGrid *self,
+           TabInfo    *info,
+           gboolean    final)
+{
+  if (info == self->reordered_tab)
+    return self->reorder_window_y;
+
+  return final ? info->final_y : info->pos_y;
+}
+
+static inline TabInfo *
+find_tab_info_at (AdwTabGrid *self,
+                  double      x,
+                  double      y)
+{
+  GList *l;
+
+  if (self->reordered_tab) {
+    int pos_x = get_tab_x (self, self->reordered_tab, FALSE);
+    int pos_y = get_tab_y (self, self->reordered_tab, FALSE);
+
+    if (pos_x <= x && x < pos_x + self->reordered_tab->width &&
+        pos_y <= y && y < pos_y + self->reordered_tab->height)
+      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 && x < info->pos_x + info->width &&
+        info->pos_y <= y && y < info->pos_y + info->height)
+      return info;
+  }
+
+  return NULL;
+}
+
+static inline GList *
+find_link_for_page (AdwTabGrid *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 (AdwTabGrid *self,
+                    AdwTabPage *page)
+{
+  GList *l = find_link_for_page (self, page);
+
+  return l ? l->data : NULL;
+}
+
+static GList *
+find_nth_alive_tab (AdwTabGrid *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;
+}
+
+/* Layout */
+
+static int
+get_n_columns (AdwTabGrid *self,
+               int         for_width)
+{
+  int max_columns;
+
+  if (for_width < 0)
+    return self->n_tabs;
+
+  max_columns = CLAMP ((int) ceil ((double) for_width / 300.0), 1, 8);
+
+  return max_columns;
+}
+
+static int
+get_tab_width (AdwTabGrid *self,
+               int         for_width)
+{
+  int n = get_n_columns (self, for_width);
+
+  return (int) ceil ((double) (for_width - SPACING * (n + 1)) / n);
+}
+
+static int
+get_tab_height (AdwTabGrid *self,
+                int         tab_width)
+{
+  int height = 0;
+  GList *l;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+    int tab_height;
+
+    gtk_widget_measure (GTK_WIDGET (info->tab), GTK_ORIENTATION_VERTICAL,
+                        tab_width, NULL, &tab_height, NULL, NULL);
+
+    height = MAX (height, tab_height);
+  }
+
+  return height;
+}
+
+static void
+get_position_for_index (AdwTabGrid *self,
+                        double      index,
+                        gboolean    is_rtl,
+                        int        *pos_x,
+                        int        *pos_y)
+{
+  double col = fmod (index, self->n_columns);
+  double row = (index - col) / self->n_columns;
+  int x, y;
+
+  if (col > self->n_columns - 1) {
+    double start, end, t;
+
+    if (is_rtl) {
+      start = self->allocated_width - SPACING - self->tab_width;
+      end = SPACING;
+    } else {
+      start = SPACING;
+      end = self->allocated_width - SPACING - self->tab_width;
+    }
+
+    t = self->n_columns - col;
+    x = adw_lerp (start, end, t);
+    y = SPACING + (row + 1 - t) * (self->tab_height + SPACING);
+
+  } else {
+    x = is_rtl ? self->allocated_width - SPACING - self->tab_width : SPACING;
+
+    if (is_rtl)
+      x -= col * (self->tab_width + SPACING);
+    else
+      x += col * (self->tab_width + SPACING);
+
+    y = SPACING + row * (self->tab_height + SPACING);
+  }
+
+  if (pos_x)
+    *pos_x = x;
+
+  if (pos_y)
+    *pos_y = y;
+}
+
+static inline int
+calculate_tab_width (TabInfo *info,
+                     int      base_width)
+{
+  return (int) floor ((base_width + SPACING) * info->appear_progress) - SPACING;
+}
+
+static int
+get_base_tab_width (AdwTabGrid *self,
+                    gboolean    target_end_padding,
+                    gboolean    target_animations)
+{
+  double max_progress = 0;
+  double n = 0;
+  double used_width;
+  GList *l;
+  int ret;
+  int end_padding = 0;
+
+  if (target_animations) {
+    max_progress = 1;
+    n = self->n_tabs;
+
+    if (!target_end_padding)
+      end_padding = self->final_end_padding;
+  } else {
+    for (l = self->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+
+      max_progress = MAX (max_progress, info->appear_progress);
+      n += info->appear_progress;
+    }
+
+    if (!target_end_padding)
+      end_padding = self->end_padding;
+  }
+
+  used_width = (self->allocated_width - (n + 1) * SPACING - end_padding) * max_progress;
+
+  ret = (int) ceil (used_width / n);
+
+  if (!self->expand_tabs)
+    ret = MIN (ret, MAX_TAB_WIDTH_NON_EXPAND - SPACING);
+
+  return ret;
+}
+
+static void
+get_visible_range (AdwTabGrid *self,
+                   int        *lower,
+                   int        *upper)
+{
+  int min = SPACING;
+  int max = self->allocated_height - SPACING;
+
+  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) + SPACING);
+    max = MIN (max, (int) ceil (value + page_size) - SPACING);
+  }
+
+  if (self->pinned)
+    max += SPACING;
+
+  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;
+}
+
+/* Single tab style */
+
+static void
+update_single_tab_style (AdwTabGrid *self)
+{
+  if (self->pinned)
+    return;
+
+  if (self->view &&
+      adw_tab_view_get_n_pages (self->view) <= 1 &&
+      adw_tab_view_get_n_pinned_pages (self->view) == 0 &&
+      self->expand_tabs &&
+      self->tab_resize_mode == TAB_RESIZE_NORMAL)
+    gtk_widget_add_css_class (GTK_WIDGET (self), "single-tab");
+  else
+    gtk_widget_remove_css_class (GTK_WIDGET (self), "single-tab");
+}
+
+/* Tab resize delay */
+
+static void
+resize_animation_value_cb (double      value,
+                           AdwTabGrid *self)
+{
+  double target_end_padding = 0;
+
+  if (!self->expand_tabs) {
+    int predicted_tab_width = get_base_tab_width (self, TRUE, FALSE);
+    GList *l;
+
+    target_end_padding = self->allocated_width - SPACING;
+
+    for (l = self->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+
+      target_end_padding -= calculate_tab_width (info, predicted_tab_width) + SPACING;
+    }
+
+    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 (AdwTabGrid *self)
+{
+  self->end_padding = 0;
+  self->final_end_padding = 0;
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+set_tab_resize_mode (AdwTabGrid    *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 = info->final_width;
+      else
+        info->last_width = info->width;
+    }
+  } else {
+    self->last_width = 0;
+  }
+
+  if (mode == TAB_RESIZE_NORMAL) {
+    self->initial_end_padding = self->end_padding;
+
+    adw_animation_play (self->resize_animation);
+  }
+
+  notify = (self->tab_resize_mode == TAB_RESIZE_NORMAL) !=
+           (mode == TAB_RESIZE_NORMAL);
+
+  self->tab_resize_mode = mode;
+
+  update_single_tab_style (self);
+
+  if (notify)
+    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_RESIZE_FROZEN]);
+}
+
+/* Hover */
+
+static void
+update_hover (AdwTabGrid *self)
+{
+  if (!self->dragging && !self->hovering)
+    set_tab_resize_mode (self, TAB_RESIZE_NORMAL);
+}
+
+static void
+motion_cb (AdwTabGrid         *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 (AdwTabGrid         *self,
+          GtkEventController *controller)
+{
+  self->hovering = FALSE;
+
+  update_hover (self);
+}
+
+/* Keybindings */
+
+static void
+focus_tab_cb (AdwTabGrid *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 (AdwTabGrid *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 (AdwTabGrid *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 (AdwTabGrid *self)
+{
+  gboolean top = FALSE, bottom = 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_y (self, info, FALSE);
+
+    adw_tab_thumbnail_set_fully_visible (info->tab,
+                                         pos - SPACING >= value &&
+                                         pos + info->height + SPACING <= value + page_size);
+
+    if (!adw_tab_page_get_needs_attention (info->page))
+      continue;
+
+    if (pos + info->height / 2.0 <= value)
+      top = TRUE;
+
+    if (pos + info->height / 2.0 >= value + page_size)
+      bottom = TRUE;
+  }
+
+  gtk_revealer_set_reveal_child (GTK_REVEALER (self->needs_attention_top), top);
+  gtk_revealer_set_reveal_child (GTK_REVEALER (self->needs_attention_bottom), bottom);
+}
+
+static double
+get_scroll_animation_value (AdwTabGrid *self)
+{
+  double to, value;
+
+  g_assert (self->scroll_animation);
+
+  if (adw_animation_get_state (self->scroll_animation) != ADW_ANIMATION_PLAYING &&
+      adw_animation_get_state (self->scroll_animation) != ADW_ANIMATION_FINISHED)
+    return gtk_adjustment_get_value (self->adjustment);
+
+  to = self->scroll_animation_offset;
+
+  if (self->scroll_animation_tab) {
+    double page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+    to += get_tab_y (self, self->scroll_animation_tab, TRUE);
+    to = CLAMP (to, 0, self->allocated_height - 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 (AdwTabGrid *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 (AdwTabGrid *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 (AdwTabGrid *self)
+{
+  double value = gtk_adjustment_get_value (self->adjustment);
+
+  update_visible (self);
+
+  if (self->drop_target_tab) {
+    self->drop_target_y += (value - self->adjustment_prev_value);
+    set_drop_target_tab (self, find_tab_info_at (self,
+                                                 self->drop_target_x,
+                                                 self->drop_target_y));
+  }
+
+  self->adjustment_prev_value = value;
+
+  if (self->block_scrolling)
+      return;
+
+  adw_animation_pause (self->scroll_animation);
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static void
+animate_scroll (AdwTabGrid *self,
+                TabInfo    *info,
+                double      offset,
+                guint       duration)
+{
+  if (!self->adjustment)
+    return;
+
+  g_signal_emit (self, signals[SIGNAL_STOP_KINETIC_SCROLLING], 0);
+
+  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;
+
+  adw_timed_animation_set_duration (ADW_TIMED_ANIMATION (self->scroll_animation),
+                                    duration);
+  adw_animation_play (self->scroll_animation);
+}
+
+static void
+animate_scroll_relative (AdwTabGrid *self,
+                         double      delta,
+                         guint       duration)
+{
+  double current_value = gtk_adjustment_get_value (self->adjustment);
+
+  if (adw_animation_get_state (self->scroll_animation) == ADW_ANIMATION_PLAYING) {
+    current_value = self->scroll_animation_offset;
+
+    if (self->scroll_animation_tab)
+      current_value += get_tab_y (self, self->scroll_animation_tab, TRUE);
+  }
+
+  animate_scroll (self, NULL, current_value + delta, duration);
+}
+
+static gboolean
+scroll_to_tab_full (AdwTabGrid *self,
+                    TabInfo    *info,
+                    int         pos,
+                    guint       duration,
+                    gboolean    keep_selected_visible)
+{
+  int tab_height;
+  double padding, value, page_size;
+
+  if (!self->adjustment)
+    return FALSE;
+
+  tab_height = info->height;
+
+  if (info->appear_animation)
+    tab_height = info->final_height;
+
+  value = gtk_adjustment_get_value (self->adjustment);
+  page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+  padding = MIN (tab_height, page_size - tab_height) / 2.0;
+
+  if (pos < 0)
+    pos = get_tab_y (self, info, TRUE);
+
+  if (pos - SPACING < value)
+    animate_scroll (self, info, -padding, duration);
+  else if (pos + tab_height + SPACING > value + page_size)
+    animate_scroll (self, info, tab_height + padding - page_size, duration);
+
+  return TRUE;
+}
+
+static gboolean
+scroll_to_tab (AdwTabGrid *self,
+               TabInfo    *info,
+               guint       duration)
+{
+  return scroll_to_tab_full (self, info, -1, duration, FALSE);
+}
+
+static void
+scroll_animation_cb (double      value,
+                     AdwTabGrid *self)
+{
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+scroll_animation_done_cb (AdwTabGrid *self)
+{
+  self->scroll_animation_done = TRUE;
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+set_vadjustment (AdwTabGrid    *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), "vadjustment");
+}
+
+/* Reordering */
+
+static void
+force_end_reordering (AdwTabGrid *self)
+{
+  GList *l;
+
+  if (self->dragging || !self->reordered_tab)
+    return;
+
+  if (self->reorder_animation)
+    adw_animation_skip (self->reorder_animation);
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (info->reorder_animation)
+      adw_animation_skip (info->reorder_animation);
+  }
+}
+
+static void
+check_end_reordering (AdwTabGrid *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 (AdwTabGrid *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->container),
+                            GTK_WIDGET (self), self->needs_attention_top);
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static void
+get_reorder_position (AdwTabGrid *self,
+                      int        *x,
+                      int        *y)
+{
+  int lower, upper;
+  int width;
+
+  if (self->reordered_tab->reorder_ignore_bounds) {
+    *x = self->reorder_x;
+    *y = self->reorder_y;
+    return;
+  }
+
+  get_visible_range (self, &lower, &upper);
+
+  width = gtk_widget_get_width (GTK_WIDGET (self));
+
+  *x = CLAMP (self->reorder_x, SPACING, width - self->reordered_tab->width - SPACING);
+  *y = CLAMP (self->reorder_y, lower, upper - self->reordered_tab->height);
+}
+
+static void
+reorder_animation_value_cb (double   value,
+                            TabInfo *dest_tab)
+{
+  AdwTabGrid *self = dest_tab->box;
+  gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+  int x1, y1, x2, y2;
+
+  get_reorder_position (self, &x1, &y1);
+  get_position_for_index (self, dest_tab->index, is_rtl, &x2, &y2);
+
+  self->reorder_window_x = (int) round (adw_lerp (x1, x2, value));
+  self->reorder_window_y = (int) round (adw_lerp (y1, y2, value));
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static void
+reorder_animation_done_cb (AdwTabGrid *self)
+{
+  g_clear_object (&self->reorder_animation);
+  check_end_reordering (self);
+}
+
+static void
+animate_reordering (AdwTabGrid *self,
+                    TabInfo    *dest_tab)
+{
+  AdwAnimationTarget *target;
+
+  if (self->reorder_animation)
+    adw_animation_skip (self->reorder_animation);
+
+  target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+                                              reorder_animation_value_cb,
+                                              dest_tab, NULL);
+  self->reorder_animation =
+    adw_timed_animation_new (GTK_WIDGET (self), 0, 1,
+                             REORDER_ANIMATION_DURATION, target);
+
+  g_signal_connect_swapped (self->reorder_animation, "done",
+                            G_CALLBACK (reorder_animation_done_cb), self);
+
+  adw_animation_play (self->reorder_animation);
+
+  check_end_reordering (self);
+}
+
+static void
+reorder_offset_animation_value_cb (double   value,
+                                   TabInfo *info)
+{
+  AdwTabGrid *self = info->box;
+
+  info->reorder_offset = value;
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static void
+reorder_offset_animation_done_cb (TabInfo *info)
+{
+  AdwTabGrid *self = info->box;
+
+  g_clear_object (&info->reorder_animation);
+  check_end_reordering (self);
+}
+
+static void
+animate_reorder_offset (AdwTabGrid *self,
+                        TabInfo    *info,
+                        double      offset)
+{
+  gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+  AdwAnimationTarget *target;
+  double start_offset;
+
+  offset *= (is_rtl ? -1 : 1);
+
+  if (info->end_reorder_offset == offset)
+    return;
+
+  info->end_reorder_offset = offset;
+  start_offset = info->reorder_offset;
+
+  if (info->reorder_animation)
+    adw_animation_skip (info->reorder_animation);
+
+  target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+                                              reorder_offset_animation_value_cb,
+                                              info, NULL);
+  info->reorder_animation =
+    adw_timed_animation_new (GTK_WIDGET (self), start_offset, offset,
+                             REORDER_ANIMATION_DURATION, target);
+
+  g_signal_connect_swapped (info->reorder_animation, "done",
+                            G_CALLBACK (reorder_offset_animation_done_cb), info);
+
+  adw_animation_play (info->reorder_animation);
+}
+
+static void
+reset_reorder_animations (AdwTabGrid *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 (AdwTabGrid *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;
+    self->reorder_y = self->reorder_window_y;
+  } else {
+    self->reorder_x = info->pos_x;
+    self->reorder_y = info->pos_y;
+  }
+
+  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->final_y, 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 (AdwTabGrid *self)
+{
+  gboolean is_rtl;
+  int old_index = -1, new_index = -1;
+  int x, y;
+  int i = 0;
+  int width, height;
+  GList *l;
+
+  if (!self->dragging)
+    return;
+
+  get_reorder_position (self, &x, &y);
+
+  width = self->reordered_tab->final_width;
+  height = self->reordered_tab->final_height;
+
+  self->reorder_window_x = x;
+  self->reorder_window_y = y;
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+  is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+    int center_x, center_y;
+
+    center_x = info->unshifted_x + info->final_width / 2;
+    center_y = info->unshifted_y + info->final_height / 2;
+
+    if (is_rtl)
+      center_x -= info->final_width;
+
+    if (info == self->reordered_tab)
+      old_index = i;
+
+    if (x + width  + SPACING > center_x && center_x > x - SPACING &&
+        y + height + SPACING > center_y && center_y > y - SPACING &&
+        new_index < 0)
+      new_index = i;
+
+    if (old_index >= 0 && new_index >= 0)
+      break;
+
+    i++;
+  }
+
+  if (new_index < 0)
+    new_index = g_list_length (self->tabs) - 1;
+
+  i = 0;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+    double offset = 0;
+
+    if (i > old_index && i <= new_index)
+      offset = is_rtl ? 1 : -1;
+
+    if (i < old_index && i >= new_index)
+      offset = is_rtl ? -1 : 1;
+
+    i++;
+
+    animate_reorder_offset (self, info, offset);
+  }
+
+  self->reorder_index = new_index;
+}
+
+static gboolean
+drag_autoscroll_cb (GtkWidget     *widget,
+                    GdkFrameClock *frame_clock,
+                    AdwTabGrid    *self)
+{
+  double value, page_size;
+  double y, delta_ms, start_threshold, end_threshold, autoscroll_factor;
+  gint64 time;
+  int offset = 0;
+  int tab_height = 0;
+  int autoscroll_area = 0;
+
+  if (self->reordered_tab) {
+    tab_height = gtk_widget_get_height (self->reordered_tab->container);
+    y = (double) self->reorder_y - SPACING;
+  } else if (self->drop_target_tab) {
+    tab_height = gtk_widget_get_height (self->drop_target_tab->container);
+    y = (double) self->drop_target_y - tab_height / 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_height / 2;
+
+  y = CLAMP (y,
+             autoscroll_area,
+             self->allocated_height - tab_height - 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_height - autoscroll_area;
+  autoscroll_factor = 0;
+
+  if (y < start_threshold)
+    autoscroll_factor = -(start_threshold - y) / autoscroll_area;
+  else if (y > end_threshold)
+    autoscroll_factor = (y - end_threshold) / autoscroll_area;
+
+  autoscroll_factor = CLAMP (autoscroll_factor, -1, 1);
+  autoscroll_factor = adw_easing_ease (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_y += offset;
+  gtk_adjustment_set_value (self->adjustment, value + offset);
+  update_drag_reodering (self);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+start_autoscroll (AdwTabGrid *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 (AdwTabGrid *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 (AdwTabGrid *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_skip (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 (AdwTabGrid *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 (AdwTabGrid *self,
+                  double      start_x,
+                  double      start_y,
+                  GtkGesture *gesture)
+{
+  start_y += gtk_adjustment_get_value (self->adjustment);
+
+  self->pressed_tab = find_tab_info_at (self, start_x, start_y);
+
+  self->drag_offset_x = start_x - get_tab_x (self, self->pressed_tab, FALSE);
+  self->drag_offset_y = start_y - get_tab_y (self, self->pressed_tab, FALSE);
+
+  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 (AdwTabGrid *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,
+                      gtk_widget_get_width (GTK_WIDGET (self)),
+                      self->allocated_height);
+  graphene_rect_inset (&rect, -threshold, -threshold);
+
+  return !graphene_rect_contains_point (&rect, &GRAPHENE_POINT_INIT (x, y));
+}
+
+static void begin_drag (AdwTabGrid *self,
+                        GdkDevice  *device);
+
+static void
+reorder_update_cb (AdwTabGrid *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 + offset_x;
+  y = start_y + gtk_adjustment_get_value (self->adjustment) + 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 (AdwTabGrid *self,
+                double      offset_x,
+                double      offset_y,
+                GtkGesture *gesture)
+{
+  end_drag_reodering (self);
+}
+
+/* Selection */
+
+static void
+reset_focus (AdwTabGrid *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 (AdwTabGrid *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_overview_tabs_have_visible_focus (self->tab_overview))
+    gtk_widget_grab_focus (self->selected_tab->container);
+
+  gtk_widget_set_focus_child (GTK_WIDGET (self),
+                              self->selected_tab->container);
+
+  if (self->selected_tab->width >= 0)
+    scroll_to_tab (self, self->selected_tab, FOCUS_ANIMATION_DURATION);
+}
+
+/* Opening */
+
+static gboolean
+extra_drag_drop_cb (AdwTabThumbnail *tab,
+                    GValue          *value,
+                    AdwTabGrid      *self)
+{
+  gboolean ret = GDK_EVENT_PROPAGATE;
+  AdwTabPage *page = adw_tab_thumbnail_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,
+                           TabInfo *info)
+{
+  info->appear_progress = value;
+
+  if (GTK_IS_WIDGET (info->container))
+    gtk_widget_queue_resize (info->container);
+}
+
+static void
+open_animation_done_cb (TabInfo *info)
+{
+  g_clear_object (&info->appear_animation);
+}
+
+static void
+measure_tab (AdwGizmo       *widget,
+             GtkOrientation  orientation,
+             int             for_size,
+             int            *minimum,
+             int            *natural,
+             int            *minimum_baseline,
+             int            *natural_baseline)
+{
+  GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (widget));
+
+  gtk_widget_measure (child, orientation, for_size,
+                      minimum, natural,
+                      minimum_baseline,  natural_baseline);
+
+  if (orientation == GTK_ORIENTATION_HORIZONTAL && minimum)
+    *minimum = 0;
+}
+
+static void
+allocate_tab (AdwGizmo *widget,
+              int       width,
+              int       height,
+              int       baseline)
+{
+  TabInfo *info = g_object_get_data (G_OBJECT (widget), "info");
+  GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (widget));
+  int allocated_width = gtk_widget_get_allocated_width (GTK_WIDGET (widget));
+  int width_diff = MAX (0, info->final_width - allocated_width);
+
+  gtk_widget_allocate (child, width + width_diff, height, baseline,
+                       gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (-width_diff / 2, 0)));
+}
+
+static TabInfo *
+create_tab_info (AdwTabGrid  *self,
+                 AdwTabPage *page)
+{
+  TabInfo *info;
+
+  info = g_new0 (TabInfo, 1);
+  info->box = self;
+  info->page = page;
+  info->unshifted_x = -1;
+  info->unshifted_y = -1;
+  info->pos_x = -1;
+  info->pos_y = -1;
+  info->width = -1;
+  info->height = -1;
+  info->container = adw_gizmo_new ("tabgridchild", measure_tab, allocate_tab,
+                                   NULL, NULL,
+                                   (AdwGizmoFocusFunc) adw_widget_focus_child,
+                                   (AdwGizmoGrabFocusFunc) adw_widget_grab_focus_self);
+  info->tab = adw_tab_thumbnail_new (self->view, self->pinned);
+
+  g_object_set_data (G_OBJECT (info->container), "info", info);
+  gtk_widget_set_overflow (info->container, GTK_OVERFLOW_HIDDEN);
+  gtk_widget_set_focusable (info->container, TRUE);
+
+  adw_tab_thumbnail_set_page (info->tab, page);
+  adw_tab_thumbnail_set_inverted (info->tab, self->inverted);
+  adw_tab_thumbnail_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), info->container);
+  gtk_widget_insert_before (info->container, GTK_WIDGET (self), self->needs_attention_top);
+
+  g_signal_connect_object (info->tab, "extra-drag-drop", G_CALLBACK (extra_drag_drop_cb), self, 0);
+
+  return info;
+}
+
+static void
+page_attached_cb (AdwTabGrid *self,
+                  AdwTabPage *page,
+                  int         position)
+{
+  AdwAnimationTarget *target;
+  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);
+
+  target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+                                              appear_animation_value_cb,
+                                              info, NULL);
+  info->appear_animation =
+    adw_timed_animation_new (GTK_WIDGET (self), 0, 1,
+                             OPEN_ANIMATION_DURATION, target);
+
+  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_play (info->appear_animation);
+
+  if (page == adw_tab_view_get_selected_page (self->view))
+    adw_tab_grid_select_page (self, page);
+  else
+    scroll_to_tab_full (self, info, -1, OPEN_ANIMATION_DURATION, TRUE);
+}
+
+/* Closing */
+
+static void
+close_animation_done_cb (TabInfo *info)
+{
+  AdwTabGrid *self = info->box;
+
+  g_clear_object (&info->appear_animation);
+
+  self->tabs = g_list_remove (self->tabs, info);
+
+  if (info->reorder_animation)
+    adw_animation_skip (info->reorder_animation);
+
+  if (self->reorder_animation)
+    adw_animation_skip (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 (AdwTabGrid *self,
+                  AdwTabPage *page)
+{
+  AdwAnimationTarget *target;
+  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 (info->container))
+    adw_tab_grid_try_focus_selected_tab (self);
+
+  if (info == self->selected_tab)
+    adw_tab_grid_select_page (self, NULL);
+
+  adw_tab_thumbnail_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_skip (info->appear_animation);
+
+  target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+                                              appear_animation_value_cb,
+                                              info, NULL);
+  info->appear_animation =
+    adw_timed_animation_new (GTK_WIDGET (self), info->appear_progress, 0,
+                             CLOSE_ANIMATION_DURATION, target);
+
+  g_signal_connect_swapped (info->appear_animation, "done",
+                            G_CALLBACK (close_animation_done_cb), info);
+
+  adw_animation_play (info->appear_animation);
+}
+
+/* Tab DND */
+
+#define ADW_TYPE_TAB_GRID_ROOT_CONTENT (adw_tab_grid_root_content_get_type ())
+
+G_DECLARE_FINAL_TYPE (AdwTabGridRootContent, adw_tab_grid_root_content, ADW, TAB_GRID_ROOT_CONTENT, 
GdkContentProvider)
+
+struct _AdwTabGridRootContent
+{
+  GdkContentProvider parent_instance;
+
+  AdwTabGrid *tab_grid;
+};
+
+G_DEFINE_FINAL_TYPE (AdwTabGridRootContent, adw_tab_grid_root_content, GDK_TYPE_CONTENT_PROVIDER)
+
+static GdkContentFormats *
+adw_tab_grid_root_content_ref_formats (GdkContentProvider *provider)
+{
+  return gdk_content_formats_new ((const char *[1]) { "application/x-rootwindow-drop" }, 1);
+}
+
+static void
+adw_tab_grid_root_content_write_mime_type_async (GdkContentProvider  *provider,
+                                                 const char          *mime_type,
+                                                 GOutputStream       *stream,
+                                                 int                  io_priority,
+                                                 GCancellable        *cancellable,
+                                                 GAsyncReadyCallback  callback,
+                                                 gpointer             user_data)
+{
+  AdwTabGridRootContent *self = ADW_TAB_GRID_ROOT_CONTENT (provider);
+  GTask *task;
+
+  self->tab_grid->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_grid_root_content_write_mime_type_async);
+  g_task_return_boolean (task, TRUE);
+
+  g_object_unref (task);
+}
+
+static gboolean
+adw_tab_grid_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_grid_root_content_finalize (GObject *object)
+{
+  AdwTabGridRootContent *self = ADW_TAB_GRID_ROOT_CONTENT (object);
+
+  g_clear_object (&self->tab_grid);
+
+  G_OBJECT_CLASS (adw_tab_grid_root_content_parent_class)->finalize (object);
+}
+
+static void
+adw_tab_grid_root_content_class_init (AdwTabGridRootContentClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GdkContentProviderClass *provider_class = GDK_CONTENT_PROVIDER_CLASS (klass);
+
+  object_class->finalize = adw_tab_grid_root_content_finalize;
+
+  provider_class->ref_formats = adw_tab_grid_root_content_ref_formats;
+  provider_class->write_mime_type_async = adw_tab_grid_root_content_write_mime_type_async;
+  provider_class->write_mime_type_finish = adw_tab_grid_root_content_write_mime_type_finish;
+}
+
+static void
+adw_tab_grid_root_content_init (AdwTabGridRootContent *self)
+{
+}
+
+static GdkContentProvider *
+adw_tab_grid_root_content_new (AdwTabGrid *tab_grid)
+{
+  AdwTabGridRootContent *self = g_object_new (ADW_TYPE_TAB_GRID_ROOT_CONTENT, NULL);
+
+  self->tab_grid = g_object_ref (tab_grid);
+
+  return GDK_CONTENT_PROVIDER (self);
+}
+
+static int
+calculate_placeholder_index (AdwTabGrid *self,
+                             int         x,
+                             int         y)
+{
+  int lower, upper, i;
+  gboolean is_rtl;
+
+  get_visible_range (self, &lower, &upper);
+
+  // FIXME x
+  x = CLAMP (x, 0, self->allocated_width);
+  y = CLAMP (y, lower, upper);
+
+  is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+  for (i = 0; i < self->n_tabs; i++) {
+    int tab_x, tab_y;
+
+    get_position_for_index (self, i, is_rtl, &tab_x, &tab_y);
+
+    if (x <= tab_x + self->tab_height + SPACING / 2 &&
+        y <= tab_y + self->tab_width + SPACING / 2)
+      return i;
+  }
+
+  return i;
+}
+
+static void
+insert_animation_value_cb (double   value,
+                           TabInfo *info)
+{
+  AdwTabGrid *self = info->box;
+
+  appear_animation_value_cb (value, info);
+
+  update_drag_reodering (self);
+}
+
+static void
+insert_placeholder (AdwTabGrid *self,
+                    AdwTabPage *page,
+                    int         x,
+                    int         y)
+{
+  TabInfo *info = self->reorder_placeholder;
+  double initial_progress = 0;
+  AdwAnimationTarget *target;
+
+  if (info) {
+    initial_progress = info->appear_progress;
+
+    if (info->appear_animation)
+      adw_animation_skip (info->appear_animation);
+  } else {
+    int index;
+
+    self->placeholder_page = page;
+
+    info = create_tab_info (self, page);
+
+    gtk_widget_set_opacity (info->container, 0);
+
+    adw_tab_thumbnail_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_height > page_size) {
+        gtk_widget_measure (info->container, GTK_ORIENTATION_VERTICAL, -1,
+                            NULL, &self->placeholder_scroll_offset, NULL, NULL);
+
+        self->placeholder_scroll_offset /= 2;
+      } else {
+        self->placeholder_scroll_offset = 0;
+      }
+    }
+
+    index = calculate_placeholder_index (self, x, y + 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);
+  }
+
+  target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+                                              insert_animation_value_cb,
+                                              info, NULL);
+  info->appear_animation =
+    adw_timed_animation_new (GTK_WIDGET (self), initial_progress, 1,
+                             OPEN_ANIMATION_DURATION, target);
+
+  g_signal_connect_swapped (info->appear_animation, "done",
+                            G_CALLBACK (open_animation_done_cb), info);
+
+  adw_animation_play (info->appear_animation);
+}
+
+static void
+replace_animation_done_cb (TabInfo *info)
+{
+  AdwTabGrid *self = info->box;
+
+  g_clear_object (&info->appear_animation);
+  self->reorder_placeholder = NULL;
+  self->can_remove_placeholder = TRUE;
+}
+
+static void
+replace_placeholder (AdwTabGrid *self,
+                     AdwTabPage *page)
+{
+  TabInfo *info = self->reorder_placeholder;
+  double initial_progress;
+  AdwAnimationTarget *target;
+
+  self->placeholder_scroll_offset = 0;
+  gtk_widget_set_opacity (self->reorder_placeholder->container, 1);
+  adw_tab_thumbnail_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_thumbnail_set_page (info->tab, page);
+  info->page = page;
+
+  adw_animation_skip (info->appear_animation);
+
+  target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+                                              appear_animation_value_cb,
+                                              info, NULL);
+  info->appear_animation =
+    adw_timed_animation_new (GTK_WIDGET (self), initial_progress, 1,
+                             OPEN_ANIMATION_DURATION, target);
+
+  g_signal_connect_swapped (info->appear_animation, "done",
+                            G_CALLBACK (replace_animation_done_cb), info);
+
+  adw_animation_play (info->appear_animation);
+}
+
+static void
+remove_animation_done_cb (TabInfo *info)
+{
+  AdwTabGrid *self = info->box;
+
+  g_clear_object (&info->appear_animation);
+
+  if (!self->can_remove_placeholder) {
+    adw_tab_thumbnail_set_page (info->tab, self->placeholder_page);
+    info->page = self->placeholder_page;
+
+    return;
+  }
+
+  if (self->reordered_tab == info) {
+    force_end_reordering (self);
+
+    if (info->reorder_animation)
+      adw_animation_skip (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 (AdwTabGrid *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 (AdwTabGrid *self)
+{
+  TabInfo *info = self->reorder_placeholder;
+  AdwAnimationTarget *target;
+
+  if (!info || !info->page)
+    return;
+
+  adw_tab_thumbnail_set_page (info->tab, NULL);
+  info->page = NULL;
+
+  if (info->appear_animation)
+    adw_animation_skip (info->appear_animation);
+
+  g_idle_add ((GSourceFunc) remove_placeholder_scroll_cb, self);
+
+  target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+                                              appear_animation_value_cb,
+                                              info, NULL);
+  info->appear_animation =
+    adw_timed_animation_new (GTK_WIDGET (self), info->appear_progress, 0,
+                             CLOSE_ANIMATION_DURATION, target);
+
+  g_signal_connect_swapped (info->appear_animation, "done",
+                            G_CALLBACK (remove_animation_done_cb), info);
+
+  adw_animation_play (info->appear_animation);
+}
+
+static inline AdwTabGrid *
+get_source_tab_grid (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_GRID (g_object_get_data (G_OBJECT (drag),
+                      "adw-tab-overview-drag-origin"));
+}
+
+static void
+do_drag_drop (AdwTabGrid *self,
+              AdwTabGrid *source_tab_grid)
+{
+  AdwTabPage *page = source_tab_grid->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_grid->should_detach_into_new_window = FALSE;
+  source_tab_grid->detached_page = NULL;
+
+  self->indirect_reordering = FALSE;
+}
+
+static void
+detach_into_new_window (AdwTabGrid *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 (AdwTabGrid  *self,
+                           AdwTabView *other_view)
+{
+  /* TODO when we have groups, this should do the actual check */
+  return TRUE;
+}
+
+static void
+drag_end (AdwTabGrid *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_object (&self->drag_icon->resize_animation);
+    g_clear_pointer (&self->drag_icon, g_free);
+  }
+
+  g_object_unref (drag);
+}
+
+static void
+tab_drop_performed_cb (AdwTabGrid *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 (AdwTabGrid *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 (AdwTabGrid           *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
+icon_resize_animation_value_cb (double    value,
+                                DragIcon *icon)
+{
+  double relative_x, relative_y;
+
+  relative_x = (double) icon->hotspot_x / icon->width;
+  relative_y = (double) icon->hotspot_y / icon->height;
+
+  icon->width = (int) round (adw_lerp (icon->initial_width, icon->target_width, value));
+  icon->height = (int) round (adw_lerp (icon->initial_height, icon->target_height, value));
+
+  gtk_widget_set_size_request (GTK_WIDGET (icon->tab),
+                               icon->width + icon->tab_margin.left + icon->tab_margin.right,
+                               icon->height + icon->tab_margin.top + icon->tab_margin.bottom);
+
+  icon->hotspot_x = (int) round (icon->width * relative_x);
+  icon->hotspot_y = (int) round (icon->height * relative_y);
+
+  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
+create_drag_icon (AdwTabGrid *self,
+                  GdkDrag    *drag)
+{
+  DragIcon *icon;
+  AdwAnimationTarget *target;
+
+  icon = g_new0 (DragIcon, 1);
+
+  icon->drag = drag;
+
+  icon->width = self->tab_width;
+  icon->initial_width = icon->width;
+  icon->target_width = icon->width;
+
+  icon->height = self->tab_height;
+  icon->initial_width = icon->height;
+  icon->target_width = icon->height;
+
+  icon->tab = adw_tab_thumbnail_new (self->view, FALSE);
+  adw_tab_thumbnail_set_page (icon->tab, self->reordered_tab->page);
+  adw_tab_thumbnail_set_dragging (icon->tab, TRUE);
+  adw_tab_thumbnail_set_inverted (icon->tab, self->inverted);
+  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,
+                               icon->height + icon->tab_margin.top + icon->tab_margin.bottom);
+
+  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);
+
+  target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+                                              icon_resize_animation_value_cb,
+                                              icon, NULL);
+  icon->resize_animation =
+    adw_timed_animation_new (GTK_WIDGET (icon->tab), 0, 1,
+                             ICON_RESIZE_ANIMATION_DURATION, target);
+
+  self->drag_icon = icon;
+}
+
+static void
+resize_drag_icon (AdwTabGrid *self,
+                  int         width,
+                  int         height)
+{
+  DragIcon *icon = self->drag_icon;
+
+  if (width == icon->target_width && height == icon->target_height)
+    return;
+
+  icon->initial_width = icon->width;
+  icon->initial_height = icon->height;
+
+  icon->target_width = width;
+  icon->target_height = height;
+
+  adw_animation_play (icon->resize_animation);
+}
+
+static void
+begin_drag (AdwTabGrid *self,
+            GdkDevice  *device)
+{
+  GdkContentProvider *content;
+  GtkNative *native;
+  GdkSurface *surface;
+  GdkDrag *drag;
+  TabInfo *detached_info;
+  GtkWidget *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->container);
+  self->detached_page = detached_info->page;
+
+  self->indirect_reordering = TRUE;
+
+  content = gdk_content_provider_new_union ((GdkContentProvider *[2]) {
+                                              adw_tab_grid_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-overview-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 (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 (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 (content);
+  g_object_unref (detached_tab);
+}
+
+static GdkDragAction
+tab_drag_enter_motion_cb (AdwTabGrid    *self,
+                          double         x,
+                          double         y,
+                          GtkDropTarget *target)
+{
+  AdwTabGrid *source_tab_grid;
+
+  if (self->pinned)
+    return 0;
+
+  source_tab_grid = get_source_tab_grid (target);
+
+  if (!source_tab_grid)
+    return 0;
+
+  if (!self->view || !is_view_in_the_same_group (self, source_tab_grid->view))
+    return 0;
+
+  y += gtk_adjustment_get_value (self->adjustment);
+
+  self->can_remove_placeholder = FALSE;
+
+  if (!self->reorder_placeholder || !self->reorder_placeholder->page) {
+    AdwTabPage *page = source_tab_grid->detached_page;
+    double center_x = x - source_tab_grid->drag_icon->hotspot_x + source_tab_grid->drag_icon->width / 2;
+    double center_y = y - source_tab_grid->drag_icon->hotspot_y + source_tab_grid->drag_icon->height / 2;
+
+    insert_placeholder (self, page, center_x, center_y);
+
+    self->indirect_reordering = TRUE;
+
+    resize_drag_icon (source_tab_grid, self->tab_width, self->tab_height);
+    adw_tab_thumbnail_set_inverted (source_tab_grid->drag_icon->tab, self->inverted);
+
+    self->drag_offset_x = source_tab_grid->drag_icon->hotspot_x;
+    self->drag_offset_y = source_tab_grid->drag_icon->hotspot_y;
+
+    self->reorder_x = (int) round (x - source_tab_grid->drag_icon->hotspot_x);
+    self->reorder_y = (int) round (y - source_tab_grid->drag_icon->hotspot_y);
+
+    start_drag_reodering (self, self->reorder_placeholder, x, y);
+
+    return GDK_ACTION_MOVE;
+  }
+
+  self->reorder_x = (int) round (x - source_tab_grid->drag_icon->hotspot_x);
+  self->reorder_y = (int) round (y - source_tab_grid->drag_icon->hotspot_y);
+
+  update_drag_reodering (self);
+
+  return GDK_ACTION_MOVE;
+}
+
+static void
+tab_drag_leave_cb (AdwTabGrid    *self,
+                   GtkDropTarget *target)
+{
+  AdwTabGrid *source_tab_grid;
+
+  if (!self->indirect_reordering)
+    return;
+
+  if (self->pinned)
+    return;
+
+  source_tab_grid = get_source_tab_grid (target);
+
+  if (!source_tab_grid)
+    return;
+
+  if (!self->view || !is_view_in_the_same_group (self, source_tab_grid->view))
+    return;
+
+  self->can_remove_placeholder = TRUE;
+
+  end_drag_reodering (self);
+  remove_placeholder (self);
+
+  self->indirect_reordering = FALSE;
+}
+
+static gboolean
+tab_drag_drop_cb (AdwTabGrid    *self,
+                  const GValue  *value,
+                  double         x,
+                  double         y,
+                  GtkDropTarget *target)
+{
+  AdwTabGrid *source_tab_grid;
+
+  if (self->pinned)
+    return GDK_EVENT_PROPAGATE;
+
+  source_tab_grid = get_source_tab_grid (target);
+
+  if (!source_tab_grid)
+    return GDK_EVENT_PROPAGATE;
+
+  if (!self->view || !is_view_in_the_same_group (self, source_tab_grid->view))
+    return GDK_EVENT_PROPAGATE;
+
+  do_drag_drop (self, source_tab_grid);
+
+  return GDK_EVENT_STOP;
+}
+
+static gboolean
+view_drag_drop_cb (AdwTabGrid    *self,
+                   const GValue  *value,
+                   double         x,
+                   double         y,
+                   GtkDropTarget *target)
+{
+  AdwTabGrid *source_tab_grid;
+
+  if (self->pinned)
+    return GDK_EVENT_PROPAGATE;
+
+  source_tab_grid = get_source_tab_grid (target);
+
+  if (!source_tab_grid)
+    return GDK_EVENT_PROPAGATE;
+
+  if (!self->view || !is_view_in_the_same_group (self, source_tab_grid->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_grid);
+
+  return GDK_EVENT_STOP;
+}
+
+/* DND autoscrolling */
+
+static gboolean
+reset_drop_target_tab_cb (AdwTabGrid *self)
+{
+  self->reset_drop_target_tab_id = 0;
+  set_drop_target_tab (self, NULL);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+drag_leave_cb (AdwTabGrid              *self,
+               GtkDropControllerMotion *controller)
+{
+  GdkDrop *drop = gtk_drop_controller_motion_get_drop (controller);
+  GdkDrag *drag = gdk_drop_get_drag (drop);
+  AdwTabGrid *source = ADW_TAB_GRID (g_object_get_data (G_OBJECT (drag),
+                                                      "adw-tab-overview-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 (AdwTabGrid              *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);
+  AdwTabGrid *source = ADW_TAB_GRID (g_object_get_data (G_OBJECT (drag),
+                                                      "adw-tab-overview-drag-origin"));
+
+  if (source)
+    return;
+
+  y += gtk_adjustment_get_value (self->adjustment);
+
+  info = find_tab_info_at (self, x, y);
+
+  if (!info) {
+    drag_leave_cb (self, controller);
+
+    return;
+  }
+
+  self->drop_target_x = x;
+  self->drop_target_y = y;
+  set_drop_target_tab (self, info);
+
+  start_autoscroll (self);
+}
+
+/* Context menu */
+
+static gboolean
+reset_setup_menu_cb (AdwTabGrid *self)
+{
+  g_signal_emit_by_name (self->view, "setup-menu", NULL);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+touch_menu_notify_visible_cb (AdwTabGrid *self)
+{
+  if (!self->context_menu || gtk_widget_get_visible (self->context_menu))
+    return;
+
+  self->hovering = FALSE;
+  update_hover (self);
+
+  g_idle_add ((GSourceFunc) reset_setup_menu_cb, self);
+}
+
+static void
+do_popup (AdwTabGrid *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_menu_new_from_model (model);
+    gtk_widget_set_parent (self->context_menu, GTK_WIDGET (self));
+    gtk_popover_set_position (GTK_POPOVER (self->context_menu), GTK_POS_BOTTOM);
+    gtk_popover_set_has_arrow (GTK_POPOVER (self->context_menu), FALSE);
+
+    if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+      gtk_widget_set_halign (self->context_menu, GTK_ALIGN_END);
+    else
+      gtk_widget_set_halign (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_x;
+    rect.y = info->pos_y + gtk_widget_get_allocated_height (GTK_WIDGET (info->container));
+
+    if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+      rect.x += info->width;
+  }
+
+  rect.y -= gtk_adjustment_get_value (self->adjustment);
+  rect.width = 0;
+  rect.height = 0;
+
+  gtk_popover_set_pointing_to (GTK_POPOVER (self->context_menu), &rect);
+
+  gtk_popover_popup (GTK_POPOVER (self->context_menu));
+}
+
+static void
+long_pressed_cb (AdwTabGrid *self,
+                 double      x,
+                 double      y,
+                 GtkGesture *gesture)
+{
+  TabInfo *info = find_tab_info_at (self, x, y);
+
+  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;
+  }
+
+  y += 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)
+{
+  AdwTabGrid *self = ADW_TAB_GRID (widget);
+
+  if (self->selected_tab && self->selected_tab->page)
+    do_popup (self, self->selected_tab, -1, -1);
+}
+
+/* Clicking */
+
+static void
+handle_click (AdwTabGrid *self,
+              TabInfo    *info,
+              GtkGesture *gesture)
+{
+  gboolean can_grab_focus;
+
+  can_grab_focus = adw_tab_overview_tabs_have_visible_focus (self->tab_overview);
+
+  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 (info->container);
+  else
+    activate_tab (self);
+}
+
+static void
+pressed_cb (AdwTabGrid *self,
+            int         n_press,
+            double      x,
+            double      y,
+            GtkGesture *gesture)
+{
+  TabInfo *info;
+  GdkEvent *event;
+  GdkEventSequence *current;
+  guint button;
+
+  if (is_touchscreen (gesture))
+    return;
+
+  y += gtk_adjustment_get_value (self->adjustment);
+
+  info = find_tab_info_at (self, x, y);
+
+  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);
+
+    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 (AdwTabGrid *self,
+             int         n_press,
+             double      x,
+             double      y,
+             GtkGesture *gesture)
+{
+  TabInfo *info;
+  guint button;
+
+  y += gtk_adjustment_get_value (self->adjustment);
+
+  if (x < 0 || x > gtk_widget_get_width (GTK_WIDGET (self))) {
+    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+    return;
+  }
+
+  info = find_tab_info_at (self, x, y);
+
+  if (!info || !info->page) {
+    gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+    return;
+  }
+
+  button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
+
+  if (button == GDK_BUTTON_MIDDLE) {
+    adw_tab_view_close_page (self->view, info->page);
+
+    return;
+  }
+
+  if (is_touchscreen (gesture))
+    handle_click (self, info, gesture);
+
+  adw_tab_overview_close (self->tab_overview);
+}
+
+/* Overrides */
+
+static void
+measure_tab_grid (AdwTabGrid     *self,
+                  GtkOrientation  orientation,
+                  int             for_size,
+                  int            *minimum,
+                  int            *natural,
+                  gboolean        animated)
+{
+  int min, nat;
+
+  if (self->n_tabs == 0) {
+    if (minimum)
+      *minimum = 0;
+
+    if (natural)
+      *natural = 0;
+
+    return;
+  }
+
+  if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+    GList *l;
+    int indicator_min;
+    min = nat = 0;
+
+    for (l = self->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+      int child_width;
+
+      gtk_widget_measure (info->container, orientation, -1,
+                          NULL, &child_width, NULL, NULL);
+
+      if (animated)
+        min = MAX (min, calculate_tab_width (info, child_width));
+      else
+        min = MAX (min, child_width);
+
+      nat += child_width + SPACING;
+    }
+
+    nat += SPACING;
+    min += SPACING * 2;
+
+    gtk_widget_measure (self->needs_attention_top, orientation, -1,
+                        &indicator_min, NULL, NULL, NULL);
+
+    min = MAX (min, indicator_min);
+
+    gtk_widget_measure (self->needs_attention_bottom, orientation, -1,
+                        &indicator_min, NULL, NULL, NULL);
+
+    min = MAX (min, indicator_min);
+  } else {
+    double n_columns, n_rows;
+    int child_width = -1, child_height;
+    double index = 0;
+    GList *l;
+
+    if (for_size >= 0)
+      child_width = get_tab_width (self, for_size);
+
+    child_height = get_tab_height (self, child_width);
+
+    for (l = self->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+
+      if (animated) {
+        index += info->appear_progress;
+      } else {
+        if (info->page)
+          index++;
+      }
+    }
+
+    n_columns = get_n_columns (self, for_size);
+    n_rows = ceil (index / n_columns);
+
+    if (animated) {
+      double col = fmod (index, n_columns);
+
+      if (col > 0 && col < 1)
+        n_rows = n_rows + adw_easing_ease (ADW_EASE_OUT_CUBIC, col) - 1;
+    }
+
+    min = nat = (child_height + SPACING) * n_rows + SPACING;
+  }
+
+  if (minimum)
+    *minimum = min;
+
+  if (natural)
+    *natural = nat;
+}
+
+static void
+adw_tab_grid_measure (GtkWidget      *widget,
+                      GtkOrientation  orientation,
+                      int             for_size,
+                      int            *minimum,
+                      int            *natural,
+                      int            *minimum_baseline,
+                      int            *natural_baseline)
+{
+  AdwTabGrid *self = ADW_TAB_GRID (widget);
+
+  measure_tab_grid (self, orientation, for_size, minimum, natural, TRUE);
+
+  if (minimum_baseline)
+    *minimum_baseline = -1;
+
+  if (natural_baseline)
+    *natural_baseline = -1;
+}
+
+static void
+adw_tab_grid_size_allocate (GtkWidget *widget,
+                            int        width,
+                            int        height,
+                            int        baseline)
+{
+  AdwTabGrid *self = ADW_TAB_GRID (widget);
+  gboolean is_rtl;
+  GList *l;
+  GtkAllocation child_allocation;
+  double value;
+  int indicator_size;
+  GskTransform *transform;
+  double index = 0, final_index = 0;
+
+  self->n_columns = get_n_columns (self, width);
+
+  adw_tab_grid_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+                       &self->allocated_width, NULL, NULL, NULL);
+  self->allocated_width = MAX (self->allocated_width, width);
+
+  adw_tab_grid_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+                       &self->allocated_height, NULL, NULL, NULL);
+  self->allocated_height = MAX (self->allocated_height, height);
+
+  if (self->context_menu)
+    gtk_popover_present (GTK_POPOVER (self->context_menu));
+
+  if (!self->n_tabs)
+    return;
+
+  is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
+
+  self->tab_width = get_tab_width (self, width);
+  self->tab_height = get_tab_height (self, self->tab_width);
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    get_position_for_index (self, final_index, is_rtl,
+                            &info->unshifted_x, &info->unshifted_y);
+    get_position_for_index (self, index + info->reorder_offset, is_rtl,
+                            &info->pos_x, &info->pos_y);
+    get_position_for_index (self, final_index + info->end_reorder_offset, is_rtl,
+                            &info->final_x, &info->final_y);
+
+    info->width = calculate_tab_width (info, self->tab_width);
+    info->final_width = self->tab_width;
+
+    info->height = self->tab_height;
+    info->final_height = self->tab_height;
+
+    info->index = index;
+    info->final_index = final_index;
+
+    index += info->appear_progress;
+    final_index++;
+  }
+
+  value = get_scroll_animation_value (self);
+
+  self->block_scrolling = TRUE;
+  gtk_adjustment_configure (self->adjustment,
+                            value,
+                            0,
+                            self->allocated_height,
+                            height * 0.1,
+                            height * 0.9,
+                            height);
+  self->block_scrolling = FALSE;
+
+  /* The value may have been changed during gtk_adjustment_configure() */
+  value = gtk_adjustment_get_value (self->adjustment);
+
+  if (self->scroll_animation_done) {
+    self->scroll_animation_tab = NULL;
+    self->scroll_animation_done = FALSE;
+    adw_animation_reset (self->scroll_animation);
+  }
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    child_allocation.x = ((info == self->reordered_tab) ? self->reorder_window_x : info->pos_x);
+    child_allocation.y = ((info == self->reordered_tab) ? self->reorder_window_y : info->pos_y) - (int) 
floor (value);
+    child_allocation.width = MAX (0, info->width);
+    child_allocation.height = MAX (0, info->height);
+
+    gtk_widget_size_allocate (info->container, &child_allocation, baseline);
+  }
+
+  gtk_widget_measure (self->needs_attention_top, GTK_ORIENTATION_VERTICAL, -1,
+                      &indicator_size, NULL, NULL, NULL);
+  gtk_widget_allocate (self->needs_attention_top, width, indicator_size, baseline, NULL);
+
+  gtk_widget_measure (self->needs_attention_bottom, GTK_ORIENTATION_VERTICAL, -1,
+                      &indicator_size, NULL, NULL, NULL);
+  transform = gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (0, height - indicator_size));
+  gtk_widget_allocate (self->needs_attention_bottom, width, indicator_size, baseline, transform);
+
+  update_visible (self);
+}
+
+static GtkSizeRequestMode
+adw_tab_grid_get_request_mode (GtkWidget *widget)
+{
+  return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static gboolean
+adw_tab_grid_focus (GtkWidget        *widget,
+                    GtkDirectionType  direction)
+{
+  AdwTabGrid *self = ADW_TAB_GRID (widget);
+
+  if (!self->selected_tab)
+    return GDK_EVENT_PROPAGATE;
+
+  return gtk_widget_grab_focus (self->selected_tab->container);
+}
+
+static void
+adw_tab_grid_unrealize (GtkWidget *widget)
+{
+  AdwTabGrid *self = ADW_TAB_GRID (widget);
+
+  g_clear_pointer (&self->context_menu, gtk_widget_unparent);
+
+  GTK_WIDGET_CLASS (adw_tab_grid_parent_class)->unrealize (widget);
+}
+
+static void
+adw_tab_grid_unmap (GtkWidget *widget)
+{
+  AdwTabGrid *self = ADW_TAB_GRID (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_grid_parent_class)->unmap (widget);
+}
+
+static void
+adw_tab_grid_direction_changed (GtkWidget        *widget,
+                                GtkTextDirection  previous_direction)
+{
+  AdwTabGrid *self = ADW_TAB_GRID (widget);
+
+  if (gtk_widget_get_direction (widget) == previous_direction)
+    return;
+
+  if (self->context_menu) {
+    if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+      gtk_widget_set_halign (self->context_menu, GTK_ALIGN_END);
+    else
+      gtk_widget_set_halign (self->context_menu, GTK_ALIGN_START);
+  }
+}
+
+static void
+adw_tab_grid_dispose (GObject *object)
+{
+  AdwTabGrid *self = ADW_TAB_GRID (object);
+
+  g_clear_handle_id (&self->drop_switch_timeout_id, g_source_remove);
+
+  self->drag_gesture = NULL;
+  self->tab_overview = NULL;
+  adw_tab_grid_set_view (self, NULL);
+  set_vadjustment (self, NULL);
+
+  g_clear_object (&self->resize_animation);
+  g_clear_object (&self->scroll_animation);
+
+  g_clear_pointer (&self->needs_attention_top, gtk_widget_unparent);
+  g_clear_pointer (&self->needs_attention_bottom, gtk_widget_unparent);
+
+  G_OBJECT_CLASS (adw_tab_grid_parent_class)->dispose (object);
+}
+
+static void
+adw_tab_grid_finalize (GObject *object)
+{
+  AdwTabGrid *self = (AdwTabGrid *) object;
+
+  g_clear_pointer (&self->extra_drag_types, g_free);
+
+  G_OBJECT_CLASS (adw_tab_grid_parent_class)->finalize (object);
+}
+
+static void
+adw_tab_grid_get_property (GObject    *object,
+                           guint       prop_id,
+                           GValue     *value,
+                           GParamSpec *pspec)
+{
+  AdwTabGrid *self = ADW_TAB_GRID (object);
+
+  switch (prop_id) {
+  case PROP_PINNED:
+    g_value_set_boolean (value, self->pinned);
+    break;
+
+  case PROP_TAB_OVERVIEW:
+    g_value_set_object (value, self->tab_overview);
+    break;
+
+  case PROP_VIEW:
+    g_value_set_object (value, self->view);
+    break;
+
+  case PROP_RESIZE_FROZEN:
+    g_value_set_boolean (value, self->tab_resize_mode != TAB_RESIZE_NORMAL);
+    break;
+
+  case PROP_VADJUSTMENT:
+    g_value_set_object (value, self->adjustment);
+    break;
+
+  case PROP_HADJUSTMENT:
+  case PROP_HSCROLL_POLICY:
+  case PROP_VSCROLL_POLICY:
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_tab_grid_set_property (GObject      *object,
+                           guint         prop_id,
+                           const GValue *value,
+                           GParamSpec   *pspec)
+{
+  AdwTabGrid *self = ADW_TAB_GRID (object);
+
+  switch (prop_id) {
+  case PROP_PINNED:
+    self->pinned = g_value_get_boolean (value);
+    break;
+
+  case PROP_TAB_OVERVIEW:
+    self->tab_overview = g_value_get_object (value);
+    break;
+
+  case PROP_VIEW:
+    adw_tab_grid_set_view (self, g_value_get_object (value));
+    break;
+
+  case PROP_VADJUSTMENT:
+    set_vadjustment (self, g_value_get_object (value));
+    break;
+
+  case PROP_HADJUSTMENT:
+  case PROP_HSCROLL_POLICY:
+  case PROP_VSCROLL_POLICY:
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_tab_grid_class_init (AdwTabGridClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = adw_tab_grid_dispose;
+  object_class->finalize = adw_tab_grid_finalize;
+  object_class->get_property = adw_tab_grid_get_property;
+  object_class->set_property = adw_tab_grid_set_property;
+
+  widget_class->measure = adw_tab_grid_measure;
+  widget_class->size_allocate = adw_tab_grid_size_allocate;
+  widget_class->get_request_mode = adw_tab_grid_get_request_mode;
+  widget_class->focus = adw_tab_grid_focus;
+  widget_class->unrealize = adw_tab_grid_unrealize;
+  widget_class->unmap = adw_tab_grid_unmap;
+  widget_class->direction_changed = adw_tab_grid_direction_changed;
+
+  props[PROP_PINNED] =
+    g_param_spec_boolean ("pinned", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  props[PROP_TAB_OVERVIEW] =
+    g_param_spec_object ("tab-overview", NULL, NULL,
+                         ADW_TYPE_TAB_OVERVIEW,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  props[PROP_VIEW] =
+    g_param_spec_object ("view", NULL, NULL,
+                         ADW_TYPE_TAB_VIEW,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_RESIZE_FROZEN] =
+    g_param_spec_boolean ("resize-frozen", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  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, "tabgrid");
+}
+
+static void
+adw_tab_grid_init (AdwTabGrid *self)
+{
+  GtkEventController *controller;
+  AdwAnimationTarget *target;
+  GtkWidget *widget;
+
+  self->can_remove_placeholder = TRUE;
+  self->expand_tabs = TRUE;
+
+  gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN);
+
+  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 (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);
+
+  target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+                                              resize_animation_value_cb,
+                                              self, NULL);
+  self->resize_animation =
+    adw_timed_animation_new (GTK_WIDGET (self), 0, 1,
+                             RESIZE_ANIMATION_DURATION, target);
+
+  g_signal_connect_swapped (self->resize_animation, "done",
+                            G_CALLBACK (resize_animation_done_cb), self);
+
+  /* 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.
+   */
+  target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+                                              scroll_animation_cb,
+                                              self, NULL);
+  self->scroll_animation =
+    adw_timed_animation_new (GTK_WIDGET (self), 0, 1,
+                             SCROLL_ANIMATION_DURATION, target);
+  g_signal_connect_swapped (self->scroll_animation, "done",
+                            G_CALLBACK (scroll_animation_done_cb), self);
+
+  self->needs_attention_top = gtk_revealer_new ();
+  gtk_revealer_set_transition_type (GTK_REVEALER (self->needs_attention_top),
+                                    GTK_REVEALER_TRANSITION_TYPE_CROSSFADE);
+  gtk_widget_set_can_target (self->needs_attention_top, FALSE);
+  gtk_widget_set_can_focus (self->needs_attention_top, FALSE);
+  gtk_widget_set_parent (self->needs_attention_top, GTK_WIDGET (self));
+
+  widget = adw_gizmo_new ("indicator", NULL, NULL, NULL, NULL, NULL, NULL);
+  gtk_widget_add_css_class (widget, "top");
+  gtk_revealer_set_child (GTK_REVEALER (self->needs_attention_top), widget);
+
+  self->needs_attention_bottom = gtk_revealer_new ();
+  gtk_revealer_set_transition_type (GTK_REVEALER (self->needs_attention_bottom),
+                                    GTK_REVEALER_TRANSITION_TYPE_CROSSFADE);
+  gtk_widget_set_can_target (self->needs_attention_bottom, FALSE);
+  gtk_widget_set_can_focus (self->needs_attention_bottom, FALSE);
+  gtk_widget_set_parent (self->needs_attention_bottom, GTK_WIDGET (self));
+
+  widget = adw_gizmo_new ("indicator", NULL, NULL, NULL, NULL, NULL, NULL);
+  gtk_widget_add_css_class (widget, "bottom");
+  gtk_revealer_set_child (GTK_REVEALER (self->needs_attention_bottom), widget);
+}
+
+void
+adw_tab_grid_set_view (AdwTabGrid *self,
+                       AdwTabView *view)
+{
+  g_return_if_fail (ADW_IS_TAB_GRID (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_clear_list (&self->tabs, (GDestroyNotify) remove_and_free_tab_info);
+    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) {
+      g_signal_connect_object (self->view, "notify::n-pages", G_CALLBACK (update_single_tab_style), self, 
G_CONNECT_SWAPPED);
+      g_signal_connect_object (self->view, "notify::n-pinned-pages", G_CALLBACK (update_single_tab_style), 
self, G_CONNECT_SWAPPED);
+
+      update_single_tab_style (self);
+
+      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_grid_attach_page (AdwTabGrid *self,
+                          AdwTabPage *page,
+                          int         position)
+{
+  g_return_if_fail (ADW_IS_TAB_GRID (self));
+  g_return_if_fail (ADW_IS_TAB_PAGE (page));
+
+  page_attached_cb (self, page, position);
+}
+
+void
+adw_tab_grid_detach_page (AdwTabGrid *self,
+                          AdwTabPage *page)
+{
+  g_return_if_fail (ADW_IS_TAB_GRID (self));
+  g_return_if_fail (ADW_IS_TAB_PAGE (page));
+
+  page_detached_cb (self, page);
+}
+
+void
+adw_tab_grid_select_page (AdwTabGrid *self,
+                          AdwTabPage *page)
+{
+  g_return_if_fail (ADW_IS_TAB_GRID (self));
+  g_return_if_fail (page == NULL || ADW_IS_TAB_PAGE (page));
+
+  select_page (self, page);
+}
+
+void
+adw_tab_grid_try_focus_selected_tab (AdwTabGrid *self)
+{
+  g_return_if_fail (ADW_IS_TAB_GRID (self));
+
+  if (self->selected_tab)
+    gtk_widget_grab_focus (self->selected_tab->container);
+}
+
+gboolean
+adw_tab_grid_is_page_focused (AdwTabGrid *self,
+                              AdwTabPage *page)
+{
+  TabInfo *info;
+
+  g_return_val_if_fail (ADW_IS_TAB_GRID (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 (info->container);
+}
+
+void
+adw_tab_grid_setup_extra_drop_target (AdwTabGrid    *self,
+                                      GdkDragAction  actions,
+                                      GType         *types,
+                                      gsize          n_types)
+{
+  GList *l;
+
+  g_return_if_fail (ADW_IS_TAB_GRID (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_thumbnail_setup_extra_drop_target (info->tab,
+                                               self->extra_drag_actions,
+                                               self->extra_drag_types,
+                                               self->extra_drag_n_types);
+  }
+}
+
+gboolean
+adw_tab_grid_get_expand_tabs (AdwTabGrid *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_GRID (self), FALSE);
+
+  return self->expand_tabs;
+}
+
+void
+adw_tab_grid_set_expand_tabs (AdwTabGrid *self,
+                              gboolean    expand_tabs)
+{
+  g_return_if_fail (ADW_IS_TAB_GRID (self));
+
+  expand_tabs = !!expand_tabs;
+
+  if (expand_tabs == self->expand_tabs)
+    return;
+
+  self->expand_tabs = expand_tabs;
+
+  update_single_tab_style (self);
+
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+gboolean
+adw_tab_grid_get_inverted (AdwTabGrid *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_GRID (self), FALSE);
+
+  return self->inverted;
+}
+
+void
+adw_tab_grid_set_inverted (AdwTabGrid *self,
+                           gboolean    inverted)
+{
+  GList *l;
+
+  g_return_if_fail (ADW_IS_TAB_GRID (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_thumbnail_set_inverted (info->tab, inverted);
+  }
+}
+
+GtkPicture *
+adw_tab_grid_get_transition_picture (AdwTabGrid *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_GRID (self), NULL);
+
+  if (self->selected_tab)
+    return adw_tab_thumbnail_get_picture (self->selected_tab->tab);
+
+  return NULL;
+}
diff --git a/src/adw-tab-overview-private.h b/src/adw-tab-overview-private.h
new file mode 100644
index 00000000..c333281b
--- /dev/null
+++ b/src/adw-tab-overview-private.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 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 "adw-tab-overview.h"
+
+G_BEGIN_DECLS
+
+gboolean adw_tab_overview_tabs_have_visible_focus (AdwTabOverview *self);
+
+G_END_DECLS
diff --git a/src/adw-tab-overview.c b/src/adw-tab-overview.c
new file mode 100644
index 00000000..4fa188a4
--- /dev/null
+++ b/src/adw-tab-overview.c
@@ -0,0 +1,808 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "adw-tab-overview-private.h"
+
+#include "adw-macros-private.h"
+#include "adw-tab-grid-private.h"
+#include "adw-timed-animation.h"
+#include "adw-widget-utils-private.h"
+
+#define TRANSITION_DURATION 400
+#define OFFSET_FACTOR 0.1f
+#define THUMBNAIL_BORDER_RADIUS 12
+
+/**
+ * AdwTabOverview:
+ *
+ * Since: 1.3
+ */
+
+struct _AdwTabOverview
+{
+  GtkWidget parent_instance;
+
+  GtkWidget *child;
+  GtkWidget *overview;
+  AdwTabView *view;
+
+  AdwTabGrid *grid;
+  AdwTabGrid *pinned_grid;
+
+  gboolean is_open;
+  AdwAnimation *open_animation;
+  double progress;
+
+  GtkWidget *hidden_thumbnail;
+  GdkPaintable *transition_paintable;
+
+  GskGLShader *shader;
+  gboolean shader_compiled;
+};
+
+static void adw_tab_overview_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (AdwTabOverview, adw_tab_overview, GTK_TYPE_WIDGET,
+                               G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, adw_tab_overview_buildable_init))
+
+static GtkBuildableIface *parent_buildable_iface;
+
+enum {
+  PROP_0,
+  PROP_VIEW,
+  PROP_CHILD,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+  SIGNAL_EXTRA_DRAG_DROP,
+  SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static gboolean
+extra_drag_drop_cb (AdwTabOverview *self,
+                    AdwTabPage     *page,
+                    GValue         *value)
+{
+  gboolean ret = GDK_EVENT_PROPAGATE;
+
+  g_signal_emit (self, signals[SIGNAL_EXTRA_DRAG_DROP], 0, page, value, &ret);
+
+  return ret;
+}
+
+static void
+view_destroy_cb (AdwTabOverview *self)
+{
+  adw_tab_overview_set_view (self, NULL);
+}
+
+static void
+notify_selected_page_cb (AdwTabOverview *self)
+{
+  AdwTabPage *page = adw_tab_view_get_selected_page (self->view);
+
+  if (!page)
+    return;
+
+  if (adw_tab_page_get_pinned (page)) {
+    adw_tab_grid_select_page (self->pinned_grid, page);
+    adw_tab_grid_select_page (self->grid, page);
+  } else {
+    adw_tab_grid_select_page (self->grid, page);
+    adw_tab_grid_select_page (self->pinned_grid, page);
+  }
+}
+
+static void
+notify_pinned_cb (AdwTabPage     *page,
+                  GParamSpec     *pspec,
+                  AdwTabOverview *self)
+{
+  AdwTabGrid *from, *to;
+  gboolean should_focus;
+
+  if (adw_tab_page_get_pinned (page)) {
+    from = self->grid;
+    to = self->pinned_grid;
+  } else {
+    from = self->pinned_grid;
+    to = self->grid;
+  }
+
+  should_focus = adw_tab_grid_is_page_focused (from, page);
+
+  adw_tab_grid_detach_page (from, page);
+  adw_tab_grid_attach_page (to, page, adw_tab_view_get_n_pinned_pages (self->view));
+
+  if (should_focus)
+    adw_tab_grid_try_focus_selected_tab (to);
+}
+
+static void
+page_attached_cb (AdwTabOverview *self,
+                  AdwTabPage     *page,
+                  int             position)
+{
+  g_signal_connect_object (page, "notify::pinned",
+                           G_CALLBACK (notify_pinned_cb), self,
+                           0);
+}
+
+static void
+page_detached_cb (AdwTabOverview *self,
+                  AdwTabPage     *page,
+                  int             position)
+{
+  g_signal_handlers_disconnect_by_func (page, notify_pinned_cb, self);
+}
+
+static void
+open_animation_value_cb (double          value,
+                         AdwTabOverview *self)
+{
+  self->progress = value;
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+open_animation_done_cb (AdwTabOverview *self)
+{
+  gtk_widget_set_opacity (self->hidden_thumbnail, 1);
+  g_clear_object (&self->hidden_thumbnail);
+  g_clear_object (&self->transition_paintable);
+
+  if (!self->is_open) {
+//    grid.did_close ();
+    gtk_widget_hide (self->overview);
+    gtk_widget_set_can_target (self->overview, FALSE);
+  }
+}
+
+static void
+set_open (AdwTabOverview *self,
+          gboolean        is_open)
+{
+  AdwTabPage *selected_page;
+  GtkPicture *picture;
+
+  self->is_open = is_open;
+
+  if (is_open) {
+    adw_animation_skip (self->open_animation);
+
+    gtk_widget_show (self->overview);
+    gtk_widget_set_can_target (self->overview, TRUE);
+  }
+
+//  if (is_open)
+//    grid.will_open ();
+
+  selected_page = adw_tab_view_get_selected_page (self->view);
+
+  if (adw_tab_page_get_pinned (selected_page))
+    picture = adw_tab_grid_get_transition_picture (ADW_TAB_GRID (self->pinned_grid));
+  else
+    picture = adw_tab_grid_get_transition_picture (ADW_TAB_GRID (self->grid));
+
+  self->hidden_thumbnail = g_object_ref (GTK_WIDGET (picture));
+  gtk_widget_set_opacity (self->hidden_thumbnail, 0);
+
+  self->transition_paintable = g_object_ref (gtk_picture_get_paintable (picture));
+
+  adw_timed_animation_set_value_from (ADW_TIMED_ANIMATION (self->open_animation),
+                                      self->progress);
+  adw_timed_animation_set_value_to (ADW_TIMED_ANIMATION (self->open_animation),
+                                    is_open ? 1 : 0);
+
+  adw_animation_play (self->open_animation);
+}
+
+static void
+ensure_shader (AdwTabOverview *self)
+{
+  GtkNative *native;
+  GskRenderer *renderer;
+  GError *error = NULL;
+
+  if (self->shader)
+    return;
+
+  self->shader = gsk_gl_shader_new_from_resource ("/org/gnome/Adwaita/glsl/tab-overview.glsl");
+
+  native = gtk_widget_get_native (GTK_WIDGET (self));
+  renderer = gtk_native_get_renderer (native);
+
+  self->shader_compiled = gsk_gl_shader_compile (self->shader, renderer, &error);
+
+  if (error) {
+    /* If shaders aren't supported, the error doesn't matter and we just
+     * silently fall back */
+    if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
+      g_warning ("Couldn't compile shader: %s\n", error->message);
+  }
+
+  g_clear_error (&error);
+}
+
+static void
+adw_tab_overview_snapshot (GtkWidget   *widget,
+                           GtkSnapshot *snapshot)
+{
+  AdwTabOverview *self = ADW_TAB_OVERVIEW (widget);
+
+  if (self->progress > 0 && self->progress < 1) {
+    graphene_rect_t view_bounds, thumbnail_bounds, transition_bounds;
+    graphene_point_t view_center;
+    int width, height;
+    float scale;
+    GskRoundedRect transition_rect;
+    GdkRGBA borders_color;
+    GtkSnapshot *child_snapshot;
+    GskRenderNode *child_node = NULL;
+
+    ensure_shader (self);
+
+    width = gtk_widget_get_width (widget);
+    height = gtk_widget_get_height (widget);
+    scale = 1 - OFFSET_FACTOR * (float) (1 - CLAMP (self->progress, 0, 1));
+
+    if (!gtk_widget_compute_bounds (GTK_WIDGET (self->view), widget, &view_bounds))
+      g_critical ("View must be inside the overview"); // TODO
+
+    if (!gtk_widget_compute_bounds (self->hidden_thumbnail, widget, &thumbnail_bounds))
+      graphene_rect_init (&thumbnail_bounds, 0, 0, 0, 0);
+
+    graphene_rect_get_center (&view_bounds, &view_center);
+
+    graphene_rect_interpolate (&view_bounds, &thumbnail_bounds,
+                               self->progress, &transition_bounds);
+
+    gsk_rounded_rect_init_from_rect (&transition_rect,
+                                     &GRAPHENE_RECT_INIT (0, 0,
+                                                          transition_bounds.size.width,
+                                                          transition_bounds.size.height),
+                                     (float) (THUMBNAIL_BORDER_RADIUS * self->progress));
+
+    if (!gtk_style_context_lookup_color (gtk_widget_get_style_context (widget),
+                                         "borders", &borders_color))
+      borders_color.alpha = 0;
+
+    /* Draw overview */
+    gtk_snapshot_save (snapshot);
+    gtk_snapshot_push_opacity (snapshot, self->progress);
+    gtk_snapshot_translate (snapshot, &view_center);
+    gtk_snapshot_scale (snapshot, scale, scale);
+    gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (-view_center.x, -view_center.y));
+    gtk_widget_snapshot_child (widget, self->overview, snapshot);
+    gtk_snapshot_pop (snapshot);
+    gtk_snapshot_restore (snapshot);
+
+    /* Draw the transition thumbnail. Unfortunately, since GTK widgets have
+     * integer sizes, we can't use a real widget for this and have to custom
+     * draw it instead. We also want to interpolate border-radius. */
+    if (self->transition_paintable) {
+      gtk_snapshot_save (snapshot);
+      gtk_snapshot_translate (snapshot, &transition_bounds.origin);
+      gtk_snapshot_append_outset_shadow (snapshot, &transition_rect,
+                                         &borders_color, 0, 0, 1, 0);
+      gtk_snapshot_push_rounded_clip (snapshot, &transition_rect);
+      gdk_paintable_snapshot (self->transition_paintable,
+                              GDK_SNAPSHOT (snapshot),
+                              transition_rect.bounds.size.width,
+                              transition_rect.bounds.size.height);
+      gtk_snapshot_pop (snapshot);
+      gtk_snapshot_restore (snapshot);
+    }
+
+    /* Draw the child */
+    scale += OFFSET_FACTOR;
+    gtk_snapshot_save (snapshot);
+    gtk_snapshot_translate (snapshot, &view_center);
+    gtk_snapshot_scale (snapshot, scale, scale);
+    gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (-view_center.x, -view_center.y));
+
+    child_snapshot = gtk_snapshot_new ();
+
+    if (self->shader_compiled) {
+      graphene_vec2_t origin, size;
+      double opacity;
+
+      opacity = 1 - CLAMP (self->progress, 0, 1);
+      graphene_point_to_vec2 (&view_bounds.origin, &origin);
+      graphene_vec2_init (&size, view_bounds.size.width, view_bounds.size.height);
+
+      gtk_snapshot_push_gl_shader (child_snapshot,
+                                   self->shader,
+                                   &GRAPHENE_RECT_INIT (0, 0, width, height),
+                                   gsk_gl_shader_format_args (self->shader,
+                                                              "opacity", opacity,
+                                                              "origin", &origin,
+                                                              "size", &size,
+                                                              NULL));
+    } else {
+      gtk_snapshot_push_opacity (child_snapshot, 1 - self->progress);
+    }
+
+    gtk_widget_snapshot_child (widget, self->child, child_snapshot);
+
+    if (self->shader_compiled)
+      gtk_snapshot_gl_shader_pop_texture (child_snapshot);
+
+    gtk_snapshot_pop (child_snapshot);
+
+    child_node = gtk_snapshot_free_to_node (child_snapshot);
+    gtk_snapshot_append_node (snapshot, child_node);
+
+    gtk_snapshot_restore (snapshot);
+    gsk_render_node_unref (child_node);
+
+    return;
+  }
+
+  if (self->progress > 0.5)
+    gtk_widget_snapshot_child (widget, self->overview, snapshot);
+
+  if (!self->child)
+    return;
+
+  /* We don't want to actually draw the child, but we do need it
+   * to redraw so that it can be displayed by the paintables */
+  if (self->progress > 0.5) {
+    GtkSnapshot *child_snapshot = gtk_snapshot_new ();
+
+    gtk_widget_snapshot_child (widget, self->child, child_snapshot);
+
+    g_object_unref (child_snapshot);
+  } else {
+    gtk_widget_snapshot_child (widget, self->child, snapshot);
+  }
+}
+
+static void
+adw_tab_overview_unrealize (GtkWidget *widget)
+{
+  AdwTabOverview *self = ADW_TAB_OVERVIEW (widget);
+
+  GTK_WIDGET_CLASS (adw_tab_overview_parent_class)->unrealize (widget);
+
+  g_clear_object (&self->shader);
+}
+
+static void
+adw_tab_overview_dispose (GObject *object)
+{
+  AdwTabOverview *self = ADW_TAB_OVERVIEW (object);
+
+  adw_tab_overview_set_view (self, NULL);
+  adw_tab_overview_set_child (self, NULL);
+
+  gtk_widget_unparent (GTK_WIDGET (self->overview));
+
+  g_clear_object (&self->open_animation);
+
+  G_OBJECT_CLASS (adw_tab_overview_parent_class)->dispose (object);
+}
+
+static void
+adw_tab_overview_get_property (GObject    *object,
+                               guint       prop_id,
+                               GValue     *value,
+                               GParamSpec *pspec)
+{
+  AdwTabOverview *self = ADW_TAB_OVERVIEW (object);
+
+  switch (prop_id) {
+  case PROP_VIEW:
+    g_value_set_object (value, adw_tab_overview_get_view (self));
+    break;
+  case PROP_CHILD:
+    g_value_set_object (value, adw_tab_overview_get_child (self));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_tab_overview_set_property (GObject      *object,
+                               guint         prop_id,
+                               const GValue *value,
+                               GParamSpec   *pspec)
+{
+  AdwTabOverview *self = ADW_TAB_OVERVIEW (object);
+
+  switch (prop_id) {
+  case PROP_VIEW:
+    adw_tab_overview_set_view (self, g_value_get_object (value));
+    break;
+  case PROP_CHILD:
+    adw_tab_overview_set_child (self, g_value_get_object (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+overview_open_cb (GtkWidget  *widget,
+                  const char *action_name,
+                  GVariant   *param)
+{
+  AdwTabOverview *self = ADW_TAB_OVERVIEW (widget);
+
+  adw_tab_overview_open (self);
+}
+
+static void
+adw_tab_overview_class_init (AdwTabOverviewClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = adw_tab_overview_dispose;
+  object_class->get_property = adw_tab_overview_get_property;
+  object_class->set_property = adw_tab_overview_set_property;
+
+  widget_class->snapshot = adw_tab_overview_snapshot;
+  widget_class->unrealize = adw_tab_overview_unrealize;
+  widget_class->compute_expand = adw_widget_compute_expand;
+
+  /**
+   * AdwTabOverview:view: (attributes org.gtk.Property.get=adw_tab_overview_get_view 
org.gtk.Property.set=adw_tab_overview_set_view)
+   *
+   * The tab view the overview controls.
+   *
+   * Since: 1.3
+   */
+  props[PROP_VIEW] =
+    g_param_spec_object ("view", NULL, NULL,
+                         ADW_TYPE_TAB_VIEW,
+                         G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabOverview:child: (attributes org.gtk.Property.get=adw_tab_overview_get_child 
org.gtk.Property.set=adw_tab_overview_set_child)
+   *
+   * The child widget.
+   *
+   * Since: 1.3
+   */
+  props[PROP_CHILD] =
+      g_param_spec_object ("child", NULL, NULL,
+                           GTK_TYPE_WIDGET,
+                           G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  /**
+   * AdwTabOverview::extra-drag-drop:
+   * @self: a tab overview
+   * @page: the page matching the tab the content was dropped onto
+   * @value: the `GValue` being dropped
+   *
+   * This signal is emitted when content is dropped onto a tab.
+   *
+   * The content must be of one of the types set up via
+   * [method@TabOverview.setup_extra_drop_target].
+   *
+   * See [signal@Gtk.DropTarget::drop].
+   *
+   * Returns: whether the drop was accepted for @page
+   *
+   * Since: 1.3
+   */
+  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, "overview.open", NULL, overview_open_cb);
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/org/gnome/Adwaita/ui/adw-tab-overview.ui");
+
+  gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, overview);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, grid);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, pinned_grid);
+  gtk_widget_class_bind_template_callback (widget_class, extra_drag_drop_cb);
+
+  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+  gtk_widget_class_set_css_name (widget_class, "taboverview");
+
+  g_type_ensure (ADW_TYPE_TAB_GRID);
+}
+
+static void
+adw_tab_overview_init (AdwTabOverview *self)
+{
+  AdwAnimationTarget *target;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  target = adw_callback_animation_target_new ((AdwAnimationTargetFunc) open_animation_value_cb,
+                                              self, NULL);
+
+  self->open_animation =
+    adw_timed_animation_new (GTK_WIDGET (self),
+                             0, 0,
+                             TRANSITION_DURATION,
+                             target);
+
+  g_signal_connect_swapped (self->open_animation, "done",
+                            G_CALLBACK (open_animation_done_cb), self);
+}
+
+static void
+adw_tab_overview_buildable_add_child (GtkBuildable *buildable,
+                                      GtkBuilder   *builder,
+                                      GObject      *child,
+                                      const char   *type)
+{
+  AdwTabOverview *self = ADW_TAB_OVERVIEW (buildable);
+
+  if (!self->overview)
+    parent_buildable_iface->add_child (buildable, builder, child, type);
+  else if (GTK_IS_WIDGET (child))
+    adw_tab_overview_set_child (self, GTK_WIDGET (child));
+  else
+    parent_buildable_iface->add_child (buildable, builder, child, type);
+}
+
+static void
+adw_tab_overview_buildable_init (GtkBuildableIface *iface)
+{
+  parent_buildable_iface = g_type_interface_peek_parent (iface);
+
+  iface->add_child = adw_tab_overview_buildable_add_child;
+}
+
+/**
+ * adw_tab_overview_new:
+ *
+ * Creates a new `AdwTabOverview`.
+ *
+ * Returns: the newly created `AdwTabOverview`
+ *
+ * Since: 1.3
+ */
+GtkWidget *
+adw_tab_overview_new (void)
+{
+  return g_object_new (ADW_TYPE_TAB_OVERVIEW, NULL);
+}
+
+/**
+ * adw_tab_overview_get_view: (attributes org.gtk.Method.get_property=view)
+ * @self: a tab overview
+ *
+ * Gets the tab view @self controls.
+ *
+ * Returns: (transfer none) (nullable): the tab view
+ *
+ * Since: 1.3
+ */
+AdwTabView *
+adw_tab_overview_get_view (AdwTabOverview *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), NULL);
+
+  return self->view;
+}
+
+/**
+ * adw_tab_overview_set_view: (attributes org.gtk.Method.set_property=view)
+ * @self: a tab overview
+ * @view: (nullable): a tab view
+ *
+ * Sets the tab view to control.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_view (AdwTabOverview *self,
+                           AdwTabView     *view)
+{
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+  g_return_if_fail (view == NULL || ADW_IS_TAB_VIEW (view));
+
+  if (self->view == view)
+    return;
+
+  if (self->view) {
+    int i, n;
+
+    g_signal_handlers_disconnect_by_func (self->view, notify_selected_page_cb, self);
+    g_signal_handlers_disconnect_by_func (self->view, page_attached_cb, self);
+    g_signal_handlers_disconnect_by_func (self->view, page_detached_cb, self);
+    g_signal_handlers_disconnect_by_func (self->view, view_destroy_cb, self);
+
+    n = adw_tab_view_get_n_pages (self->view);
+
+    for (i = 0; i < n; i++)
+      page_detached_cb (self, adw_tab_view_get_nth_page (self->view, i), i);
+
+    adw_tab_grid_set_view (self->grid, NULL);
+    adw_tab_grid_set_view (self->pinned_grid, NULL);
+  }
+
+  g_set_object (&self->view, view);
+
+  if (self->view) {
+    int i, n;
+
+    adw_tab_grid_set_view (self->grid, view);
+    adw_tab_grid_set_view (self->pinned_grid, view);
+
+    g_signal_connect_object (self->view, "notify::selected-page",
+                             G_CALLBACK (notify_selected_page_cb), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->view, "page-attached",
+                             G_CALLBACK (page_attached_cb), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->view, "page-detached",
+                             G_CALLBACK (page_detached_cb), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->view, "destroy",
+                             G_CALLBACK (view_destroy_cb), self,
+                             G_CONNECT_SWAPPED);
+
+    n = adw_tab_view_get_n_pages (self->view);
+
+    for (i = 0; i < n; i++)
+      page_attached_cb (self, adw_tab_view_get_nth_page (self->view, i), i);
+  }
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW]);
+}
+
+/**
+ * adw_tab_overview_get_child: (attributes org.gtk.Method.get_property=child)
+ * @self: a `AdwTabOveview`
+ *
+ * Gets the child widget of @self.
+ *
+ * Returns: (nullable) (transfer none): the child widget of @self
+ *
+ * Since: 1.3
+ */
+GtkWidget *
+adw_tab_overview_get_child (AdwTabOverview *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), NULL);
+
+  return self->child;
+}
+
+/**
+ * adw_tab_overview_set_child: (attributes org.gtk.Method.set_property=child)
+ * @self: a tab overview
+ * @child: (nullable): the child widget
+ *
+ * Sets the child widget of @self.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_child (AdwTabOverview *self,
+                            GtkWidget      *child)
+{
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+  g_return_if_fail (child == NULL || GTK_IS_WIDGET (child));
+
+  if (self->child == child)
+    return;
+
+  if (self->child)
+    gtk_widget_unparent (self->child);
+
+  self->child = child;
+
+  if (self->child)
+    gtk_widget_insert_after (self->child, GTK_WIDGET (self), NULL);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD]);
+}
+
+/**
+ * adw_tab_overview_open:
+ * @self: a tab overview
+ *
+ * Opens the overview.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_open (AdwTabOverview *self)
+{
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+  if (self->is_open)
+    return;
+
+  set_open (self, TRUE);
+}
+
+// TODO
+void
+adw_tab_overview_close (AdwTabOverview *self)
+{
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+  if (!self->is_open)
+    return;
+
+  set_open (self, FALSE);
+}
+
+/**
+ * adw_tab_overview_setup_extra_drop_target:
+ * @self: a tab overview
+ * @actions: the supported actions
+ * @types: (nullable) (transfer none) (array length=n_types):
+ *   all supported `GType`s that can be dropped
+ * @n_types: number of @types
+ *
+ * Sets the supported types for this drop target.
+ *
+ * Sets up an extra drop target on tabs.
+ *
+ * This allows to drag arbitrary content onto tabs, for example URLs in a web
+ * browser.
+ *
+ * If a tab is hovered for a certain period of time while dragging the content,
+ * it will be automatically selected.
+ *
+ * The [signal@TabOverview::extra-drag-drop] signal can be used to handle the
+ * drop.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_setup_extra_drop_target (AdwTabOverview *self,
+                                          GdkDragAction   actions,
+                                          GType          *types,
+                                          gsize           n_types)
+{
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+  g_return_if_fail (n_types == 0 || types != NULL);
+
+  adw_tab_grid_setup_extra_drop_target (self->grid, actions, types, n_types);
+  adw_tab_grid_setup_extra_drop_target (self->pinned_grid, actions, types, n_types);
+}
+
+gboolean
+adw_tab_overview_tabs_have_visible_focus (AdwTabOverview *self)
+{
+  GtkWidget *pinned_focus_child, *scroll_focus_child;
+
+  g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), FALSE);
+
+  pinned_focus_child = gtk_widget_get_focus_child (GTK_WIDGET (self->pinned_grid));
+  scroll_focus_child = gtk_widget_get_focus_child (GTK_WIDGET (self->grid));
+
+  if (pinned_focus_child && gtk_widget_has_visible_focus (pinned_focus_child))
+    return TRUE;
+
+  if (scroll_focus_child && gtk_widget_has_visible_focus (scroll_focus_child))
+    return TRUE;
+
+  return FALSE;
+}
diff --git a/src/adw-tab-overview.h b/src/adw-tab-overview.h
new file mode 100644
index 00000000..2869b1bd
--- /dev/null
+++ b/src/adw-tab-overview.h
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include "adw-version.h"
+
+#include <gtk/gtk.h>
+#include "adw-tab-view.h"
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_TAB_OVERVIEW (adw_tab_overview_get_type())
+
+ADW_AVAILABLE_IN_1_2
+G_DECLARE_FINAL_TYPE (AdwTabOverview, adw_tab_overview, ADW, TAB_OVERVIEW, GtkWidget)
+
+ADW_AVAILABLE_IN_1_2
+GtkWidget *adw_tab_overview_new (void) G_GNUC_WARN_UNUSED_RESULT;
+
+ADW_AVAILABLE_IN_1_2
+AdwTabView *adw_tab_overview_get_view (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void        adw_tab_overview_set_view (AdwTabOverview *self,
+                                       AdwTabView     *view);
+
+ADW_AVAILABLE_IN_1_2
+GtkWidget *adw_tab_overview_get_child (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void       adw_tab_overview_set_child (AdwTabOverview *self,
+                                       GtkWidget      *child);
+
+ADW_AVAILABLE_IN_1_2
+void adw_tab_overview_open  (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void adw_tab_overview_close (AdwTabOverview *self);
+
+ADW_AVAILABLE_IN_1_2
+void adw_tab_overview_setup_extra_drop_target (AdwTabOverview *self,
+                                               GdkDragAction   actions,
+                                               GType          *types,
+                                               gsize           n_types);
+
+G_END_DECLS
diff --git a/src/adw-tab-overview.ui b/src/adw-tab-overview.ui
new file mode 100644
index 00000000..a4bfdf7a
--- /dev/null
+++ b/src/adw-tab-overview.ui
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <template class="AdwTabOverview" parent="GtkWidget">
+    <child>
+      <object class="GtkOverlay" id="overview">
+        <property name="visible">False</property>
+        <property name="can-target">False</property>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="GtkScrolledWindow" id="pinned_scrolled_window">
+                <property name="hscrollbar-policy">never</property>
+                <property name="propagate-natural-height">True</property>
+                <property name="vexpand">False</property>
+                <property name="margin-top">47</property>
+                <style>
+                  <class name="pinned"/>
+                </style>
+                <property name="child">
+                  <object class="AdwTabGrid" id="pinned_grid">
+                    <property name="pinned">True</property>
+                    <property name="tab-overview">AdwTabOverview</property>
+    <!--
+                    <signal name="stop-kinetic-scrolling" handler="stop_kinetic_scrolling_cb" 
object="pinned_scrolled_window" swapped="true"/>-->
+                    <signal name="extra-drag-drop" handler="extra_drag_drop_cb" swapped="true"/>
+                  </object>
+                </property>
+              </object>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow" id="scrolled_window">
+                <property name="hscrollbar-policy">never</property>
+                <property name="vexpand">True</property>
+                <property name="min-content-height">100</property>
+                <property name="child">
+                  <object class="AdwTabGrid" id="grid">
+                    <property name="tab-overview">AdwTabOverview</property>
+    <!--
+                    <signal name="stop-kinetic-scrolling" handler="stop_kinetic_scrolling_cb" 
object="scrolled_window" swapped="true"/>-->
+                    <signal name="extra-drag-drop" handler="extra_drag_drop_cb" swapped="true"/>
+                  </object>
+                </property>
+              </object>
+            </child>
+<!--                <property name="child">
+                  <object class="AdwClamp">
+                    <property name="margin-top">50</property>
+                    <property name="margin-bottom">24</property>
+                    <property name="margin-start">24</property>
+                    <property name="margin-end">24</property>
+                    <property name="maximum-size">4000</property
+                      <object class="GtkFlowBox" id="flowbox">
+                        <property name="halign">fill</property>
+                        <property name="valign">center</property>
+                        <property name="column-spacing">24</property>
+                        <property name="row-spacing">24</property>
+                        <property name="selection-mode">none</property>
+                        <property name="activate-on-single-click">True</property>
+                        <property name="homogeneous">True</property>
+                        <property name="min-children-per-line">2</property>
+                      </object>
+                    </property>
+                  </object>-->
+          </object>
+        </child>
+        <child type="overlay">
+          <object class="GtkHeaderBar" id="headerbar">
+            <property name="valign">start</property>
+            <property name="title-widget">
+              <object class="AdwWindowTitle"/>
+            </property>
+            <layout>
+              <property name="measure">True</property>
+            </layout>
+            <style>
+              <class name="flat"/>
+            </style>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/adw-tab-thumbnail-private.h b/src/adw-tab-thumbnail-private.h
new file mode 100644
index 00000000..12d1bc14
--- /dev/null
+++ b/src/adw-tab-thumbnail-private.h
@@ -0,0 +1,50 @@
+/*
+ * 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_THUMBNAIL (adw_tab_thumbnail_get_type())
+
+G_DECLARE_FINAL_TYPE (AdwTabThumbnail, adw_tab_thumbnail, ADW, TAB_THUMBNAIL, GtkWidget)
+
+AdwTabThumbnail *adw_tab_thumbnail_new (AdwTabView *view,
+                                        gboolean    pinned) G_GNUC_WARN_UNUSED_RESULT;
+
+AdwTabPage *adw_tab_thumbnail_get_page (AdwTabThumbnail *self);
+void        adw_tab_thumbnail_set_page (AdwTabThumbnail *self,
+                                        AdwTabPage      *page);
+
+gboolean adw_tab_thumbnail_get_dragging (AdwTabThumbnail *self);
+void     adw_tab_thumbnail_set_dragging (AdwTabThumbnail *self,
+                                         gboolean         dragging);
+
+gboolean adw_tab_thumbnail_get_inverted (AdwTabThumbnail *self);
+void     adw_tab_thumbnail_set_inverted (AdwTabThumbnail *self,
+                                         gboolean         inverted);
+
+void adw_tab_thumbnail_set_fully_visible (AdwTabThumbnail *self,
+                                          gboolean         fully_visible);
+
+void adw_tab_thumbnail_setup_extra_drop_target (AdwTabThumbnail *self,
+                                                GdkDragAction    actions,
+                                                GType           *types,
+                                                gsize            n_types);
+
+GtkPicture *adw_tab_thumbnail_get_picture (AdwTabThumbnail *self);
+
+G_END_DECLS
diff --git a/src/adw-tab-thumbnail.c b/src/adw-tab-thumbnail.c
new file mode 100644
index 00000000..da8cff4e
--- /dev/null
+++ b/src/adw-tab-thumbnail.c
@@ -0,0 +1,778 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#include "config.h"
+#include "adw-tab-thumbnail-private.h"
+
+#include "adw-fading-label-private.h"
+#include "adw-macros-private.h"
+
+#define MIN_ASPECT_RATIO 0.8
+#define MAX_ASPECT_RATIO 1.6
+#define XALIGN 0.5
+#define YALIGN 0.5
+
+#define ADW_TYPE_TAB_PAINTABLE (adw_tab_paintable_get_type ())
+
+G_DECLARE_FINAL_TYPE (AdwTabPaintable, adw_tab_paintable, ADW, TAB_PAINTABLE, GObject)
+
+struct _AdwTabPaintable
+{
+  GObject parent_instance;
+
+  GtkWidget *view;
+  GtkWidget *child;
+
+  GdkPaintable *paintable;
+  GdkPaintable *view_paintable;
+  GdkPaintable *cached_paintable;
+
+  GdkRGBA cached_bg;
+  int cached_width;
+  int cached_height;
+  bool schedule_clear_cache;
+};
+
+static double
+adw_tab_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable)
+{
+  AdwTabPaintable *self = ADW_TAB_PAINTABLE (paintable);
+  int width = gtk_widget_get_width (self->view);
+  int height = gtk_widget_get_height (self->view);
+  double ratio = (double) width / height;
+
+  return CLAMP (ratio, MIN_ASPECT_RATIO, MAX_ASPECT_RATIO);
+}
+
+static GdkPaintable *
+adw_tab_paintable_get_current_image (GdkPaintable *paintable)
+{
+  AdwTabPaintable *self = ADW_TAB_PAINTABLE (paintable);
+
+  if (self->cached_paintable)
+    return g_object_ref (self->cached_paintable);
+
+  return gdk_paintable_get_current_image (self->paintable);
+}
+
+static void
+snapshot_paintable (GdkSnapshot  *snapshot,
+                    double        width,
+                    double        height,
+                    GdkPaintable *paintable)
+{
+  double snapshot_ratio = width / height;
+  double paintable_ratio = gdk_paintable_get_intrinsic_aspect_ratio (paintable);
+
+  if (paintable_ratio > snapshot_ratio) {
+    double new_width = width * paintable_ratio / snapshot_ratio;
+
+    gtk_snapshot_translate (GTK_SNAPSHOT (snapshot),
+                            &GRAPHENE_POINT_INIT ((float) (width - new_width) * XALIGN, 0));
+
+    width = new_width;
+  } if (paintable_ratio < snapshot_ratio) {
+    double new_height = height * snapshot_ratio / paintable_ratio;
+
+    gtk_snapshot_translate (GTK_SNAPSHOT (snapshot),
+                            &GRAPHENE_POINT_INIT (0, (float) (height - new_height) * YALIGN));
+
+    height = new_height;
+  }
+
+  gdk_paintable_snapshot (paintable, snapshot, width, height);
+}
+
+static void
+get_background_color (AdwTabPaintable *self,
+                      GdkRGBA         *rgba)
+{
+  GtkStyleContext *context = gtk_widget_get_style_context (self->view);
+
+  if (gtk_style_context_lookup_color (context, "theme_bg_color", rgba))
+    return;
+
+  rgba->red = 1;
+  rgba->green = 1;
+  rgba->blue = 1;
+  rgba->alpha = 1;
+}
+
+static void
+adw_tab_paintable_snapshot (GdkPaintable *paintable,
+                            GdkSnapshot  *snapshot,
+                            double        width,
+                            double        height)
+{
+  AdwTabPaintable *self = ADW_TAB_PAINTABLE (paintable);
+  GdkRGBA bg;
+
+  if (self->cached_paintable) {
+    gtk_snapshot_append_color (GTK_SNAPSHOT (snapshot), &self->cached_bg,
+                               &GRAPHENE_RECT_INIT (0, 0, width, height));
+
+    snapshot_paintable (snapshot, width, height, self->cached_paintable);
+
+    return;
+  }
+
+  if (!gtk_widget_get_mapped (self->child))
+    return;
+
+  get_background_color (self, &bg);
+  gtk_snapshot_append_color (GTK_SNAPSHOT (snapshot), &bg,
+                             &GRAPHENE_RECT_INIT (0, 0, width, height));
+
+  snapshot_paintable (snapshot, width, height, self->paintable);
+}
+
+static void
+adw_tab_paintable_iface_init (GdkPaintableInterface *iface)
+{
+  iface->get_intrinsic_aspect_ratio = adw_tab_paintable_get_intrinsic_aspect_ratio;
+  iface->get_current_image = adw_tab_paintable_get_current_image;
+  iface->snapshot = adw_tab_paintable_snapshot;
+}
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (AdwTabPaintable, adw_tab_paintable, G_TYPE_OBJECT,
+                               G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, adw_tab_paintable_iface_init))
+
+static void
+adw_tab_paintable_dispose (GObject *object)
+{
+  AdwTabPaintable *self = ADW_TAB_PAINTABLE (object);
+
+  g_clear_object (&self->paintable);
+  g_clear_object (&self->view_paintable);
+  g_clear_object (&self->cached_paintable);
+
+  G_OBJECT_CLASS (adw_tab_paintable_parent_class)->dispose (object);
+}
+
+static void
+adw_tab_paintable_class_init (AdwTabPaintableClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->dispose = adw_tab_paintable_dispose;
+}
+
+static void
+adw_tab_paintable_init (AdwTabPaintable *self)
+{
+}
+
+static void
+invalidate_contents_cb (AdwTabPaintable *self)
+{
+  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
+
+  if (self->schedule_clear_cache) {
+    g_clear_object (&self->cached_paintable);
+    self->schedule_clear_cache = FALSE;
+  }
+}
+
+static void
+child_map_cb (AdwTabPaintable *self)
+{
+  if (self->cached_paintable)
+    self->schedule_clear_cache = TRUE;
+}
+
+static void
+child_unmap_cb (AdwTabPaintable *self)
+{
+  self->cached_paintable = gdk_paintable_get_current_image (self->paintable);
+  self->cached_width = gtk_widget_get_width (self->view);
+  self->cached_height = gtk_widget_get_height (self->view);
+  get_background_color (self, &self->cached_bg);
+}
+
+static GdkPaintable *
+adw_tab_paintable_new (AdwTabView *view,
+                       AdwTabPage *page)
+{
+  AdwTabPaintable *self = g_object_new (ADW_TYPE_TAB_PAINTABLE, NULL);
+
+  self->view = GTK_WIDGET (view);
+  self->child = adw_tab_page_get_child (page);
+
+  self->paintable = gtk_widget_paintable_new (self->child);
+  self->view_paintable = gtk_widget_paintable_new (self->view);
+
+  g_signal_connect_swapped (self->paintable, "invalidate-contents", G_CALLBACK (invalidate_contents_cb), 
self);
+  g_signal_connect_swapped (self->view_paintable, "invalidate-size", G_CALLBACK 
(gdk_paintable_invalidate_size), self);
+
+  g_signal_connect_swapped (self->child, "map", G_CALLBACK (child_map_cb), self);
+  g_signal_connect_swapped (self->child, "unmap", G_CALLBACK (child_unmap_cb), self);
+
+  return GDK_PAINTABLE (self);
+}
+
+
+
+
+
+
+
+
+
+
+
+struct _AdwTabThumbnail
+{
+  GtkWidget parent_instance;
+
+  GtkPicture *picture;
+  GtkWidget *contents;
+  GtkWidget *icon_stack;
+  GtkImage *icon;
+  GtkSpinner *spinner;
+  GtkImage *indicator_icon;
+  GtkWidget *indicator_btn;
+  GtkDropTarget *drop_target;
+
+  AdwTabView *view;
+  AdwTabPage *page;
+  gboolean pinned;
+  gboolean dragging;
+
+  gboolean inverted;
+};
+
+G_DEFINE_FINAL_TYPE (AdwTabThumbnail, adw_tab_thumbnail, GTK_TYPE_WIDGET)
+
+enum {
+  PROP_0,
+  PROP_VIEW,
+  PROP_PINNED,
+  PROP_DRAGGING,
+  PROP_PAGE,
+  PROP_INVERTED,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+  SIGNAL_EXTRA_DRAG_DROP,
+  SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static inline void
+set_style_class (GtkWidget  *widget,
+                 const char *style_class,
+                 gboolean    enabled)
+{
+  if (enabled)
+    gtk_widget_add_css_class (widget, style_class);
+  else
+    gtk_widget_remove_css_class (widget, style_class);
+}
+
+static void
+update_tooltip (AdwTabThumbnail *self)
+{
+  AdwTabPage *page = adw_tab_thumbnail_get_page (ADW_TAB_THUMBNAIL (self));
+  const char *tooltip = adw_tab_page_get_tooltip (page);
+
+  if (tooltip && g_strcmp0 (tooltip, "") != 0)
+    gtk_widget_set_tooltip_markup (GTK_WIDGET (self), tooltip);
+  else
+    gtk_widget_set_tooltip_text (GTK_WIDGET (self),
+                                 adw_tab_page_get_title (page));
+}
+
+static void
+update_spinner (AdwTabThumbnail *self)
+{
+  gboolean loading = self->page && adw_tab_page_get_loading (self->page);
+  gboolean mapped = gtk_widget_get_mapped (GTK_WIDGET (self));
+
+  /* Don't use CPU when not needed */
+  gtk_spinner_set_spinning (self->spinner, loading && mapped);
+}
+
+static void
+update_icon (AdwTabThumbnail *self)
+{
+  GIcon *gicon = adw_tab_page_get_icon (self->page);
+  gboolean loading = adw_tab_page_get_loading (self->page);
+  const char *name = loading ? "spinner" : "icon";
+
+  gtk_image_set_from_gicon (self->icon, gicon);
+  gtk_widget_set_visible (self->icon_stack,
+                          (gicon != NULL || loading));
+  gtk_stack_set_visible_child_name (GTK_STACK (self->icon_stack), name);
+}
+
+static void
+update_loading (AdwTabThumbnail *self)
+{
+  update_icon (self);
+  update_spinner (self);
+  set_style_class (GTK_WIDGET (self), "loading",
+                   adw_tab_page_get_loading (self->page));
+}
+
+static void
+update_indicator (AdwTabThumbnail *self)
+{
+  GIcon *indicator = adw_tab_page_get_indicator_icon (self->page);
+  gboolean activatable = self->page && adw_tab_page_get_indicator_activatable (self->page);
+
+  gtk_image_set_from_gicon (self->indicator_icon, indicator);
+  gtk_widget_set_visible (self->indicator_btn, indicator != NULL);
+  gtk_widget_set_can_target (self->indicator_btn, activatable);
+}
+
+static gboolean
+close_idle_cb (AdwTabThumbnail *self)
+{
+  adw_tab_view_close_page (self->view, self->page);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+close_clicked_cb (AdwTabThumbnail *self)
+{
+  if (!self->page)
+    return;
+
+  /* When animations are disabled, we don't want to immediately remove the
+   * whole tab mid-click. Instead, defer it until the click has happened.
+   */
+  g_idle_add ((GSourceFunc) close_idle_cb, self);
+}
+
+static void
+indicator_clicked_cb (AdwTabThumbnail *self)
+{
+  if (!self->page)
+    return;
+
+  g_signal_emit_by_name (self->view, "indicator-activated", self->page);
+}
+
+static gboolean
+drop_cb (AdwTabThumbnail *self,
+         GValue          *value)
+{
+  gboolean ret = GDK_EVENT_PROPAGATE;
+
+  g_signal_emit (self, signals[SIGNAL_EXTRA_DRAG_DROP], 0, value, &ret);
+
+  return ret;
+}
+
+static void
+adw_tab_thumbnail_map (GtkWidget *widget)
+{
+  AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (widget);
+
+  GTK_WIDGET_CLASS (adw_tab_thumbnail_parent_class)->map (widget);
+
+  update_spinner (self);
+}
+
+static void
+adw_tab_thumbnail_unmap (GtkWidget *widget)
+{
+  AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (widget);
+
+  GTK_WIDGET_CLASS (adw_tab_thumbnail_parent_class)->unmap (widget);
+
+  update_spinner (self);
+}
+
+static void
+adw_tab_thumbnail_constructed (GObject *object)
+{
+//  AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (object);
+
+  G_OBJECT_CLASS (adw_tab_thumbnail_parent_class)->constructed (object);
+
+  /*
+  g_signal_connect_object (view, "notify::default-icon",
+                           G_CALLBACK (update_icons), object,
+                           G_CONNECT_SWAPPED);
+*/
+}
+
+static void
+adw_tab_thumbnail_dispose (GObject *object)
+{
+  AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (object);
+
+  adw_tab_thumbnail_set_page (self, NULL);
+
+  if (self->contents)
+    gtk_widget_unparent (self->contents);
+
+  G_OBJECT_CLASS (adw_tab_thumbnail_parent_class)->dispose (object);
+}
+
+static void
+adw_tab_thumbnail_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (object);
+
+  switch (prop_id) {
+  case PROP_VIEW:
+    g_value_set_object (value, self->view);
+    break;
+
+  case PROP_PAGE:
+    g_value_set_object (value, adw_tab_thumbnail_get_page (self));
+    break;
+
+  case PROP_PINNED:
+    g_value_set_boolean (value, self->pinned);
+    break;
+
+  case PROP_DRAGGING:
+    g_value_set_boolean (value, adw_tab_thumbnail_get_dragging (self));
+    break;
+
+  case PROP_INVERTED:
+    g_value_set_boolean (value, adw_tab_thumbnail_get_inverted (self));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_tab_thumbnail_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (object);
+
+  switch (prop_id) {
+  case PROP_VIEW:
+    self->view = g_value_get_object (value);
+    break;
+
+  case PROP_PAGE:
+    adw_tab_thumbnail_set_page (self, g_value_get_object (value));
+    break;
+
+  case PROP_PINNED:
+    self->pinned = g_value_get_boolean (value);
+    break;
+
+  case PROP_DRAGGING:
+    adw_tab_thumbnail_set_dragging (self, g_value_get_boolean (value));
+    break;
+
+  case PROP_INVERTED:
+    adw_tab_thumbnail_set_inverted (self, g_value_get_boolean (value));
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_tab_thumbnail_class_init (AdwTabThumbnailClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = adw_tab_thumbnail_dispose;
+  object_class->constructed = adw_tab_thumbnail_constructed;
+  object_class->get_property = adw_tab_thumbnail_get_property;
+  object_class->set_property = adw_tab_thumbnail_set_property;
+
+  widget_class->map = adw_tab_thumbnail_map;
+  widget_class->unmap = adw_tab_thumbnail_unmap;
+
+  props[PROP_VIEW] =
+    g_param_spec_object ("view", NULL, NULL,
+                         ADW_TYPE_TAB_VIEW,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  props[PROP_PINNED] =
+    g_param_spec_boolean ("pinned", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+  props[PROP_DRAGGING] =
+    g_param_spec_boolean ("dragging", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_PAGE] =
+    g_param_spec_object ("page", NULL, NULL,
+                         ADW_TYPE_TAB_PAGE,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  props[PROP_INVERTED] =
+    g_param_spec_boolean ("inverted", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  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,
+                  1,
+                  G_TYPE_VALUE);
+
+  gtk_widget_class_set_template_from_resource (widget_class,
+                                               "/org/gnome/Adwaita/ui/adw-tab-thumbnail.ui");
+  gtk_widget_class_bind_template_child (widget_class, AdwTabThumbnail, picture);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabThumbnail, contents);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabThumbnail, icon_stack);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabThumbnail, icon);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabThumbnail, spinner);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabThumbnail, indicator_icon);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabThumbnail, indicator_btn);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabThumbnail, drop_target);
+  gtk_widget_class_bind_template_callback (widget_class, close_clicked_cb);
+  gtk_widget_class_bind_template_callback (widget_class, indicator_clicked_cb);
+  gtk_widget_class_bind_template_callback (widget_class, drop_cb);
+
+  gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+  gtk_widget_class_set_css_name (widget_class, "tabthumbnail");
+
+  g_type_ensure (ADW_TYPE_FADING_LABEL);
+}
+
+static void
+adw_tab_thumbnail_init (AdwTabThumbnail *self)
+{
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN);
+}
+
+AdwTabThumbnail *
+adw_tab_thumbnail_new (AdwTabView *view,
+                       gboolean    pinned)
+{
+  g_return_val_if_fail (ADW_IS_TAB_VIEW (view), NULL);
+
+  return g_object_new (ADW_TYPE_TAB_THUMBNAIL,
+                       "view", view,
+                       "pinned", pinned,
+                       NULL);
+}
+
+AdwTabPage *
+adw_tab_thumbnail_get_page (AdwTabThumbnail *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_THUMBNAIL (self), NULL);
+
+  return self->page;
+}
+
+void
+adw_tab_thumbnail_set_page (AdwTabThumbnail *self,
+                            AdwTabPage      *page)
+{
+  g_return_if_fail (ADW_IS_TAB_THUMBNAIL (self));
+  g_return_if_fail (page == NULL || ADW_IS_TAB_PAGE (page));
+
+  if (self->page == page)
+    return;
+
+  if (self->page) {
+    gtk_picture_set_paintable (GTK_PICTURE (self->picture), NULL);
+
+    g_signal_handlers_disconnect_by_func (self->page, update_tooltip, self);
+    g_signal_handlers_disconnect_by_func (self->page, update_icon, self);
+    g_signal_handlers_disconnect_by_func (self->page, update_indicator, self);
+    g_signal_handlers_disconnect_by_func (self->page, update_loading, self);
+/*
+    g_signal_handlers_disconnect_by_func (self->page, update_selected, self);
+    g_signal_handlers_disconnect_by_func (self->page, update_title, self);
+    g_signal_handlers_disconnect_by_func (self->page, update_tooltip, self);
+    g_signal_handlers_disconnect_by_func (self->page, update_icons, self);
+    g_signal_handlers_disconnect_by_func (self->page, update_indicator, self);
+    g_signal_handlers_disconnect_by_func (self->page, update_needs_attention, self);
+    g_signal_handlers_disconnect_by_func (self->page, update_loading, self);*/
+  }
+
+  g_set_object (&self->page, page);
+
+  if (self->page) {
+    gtk_picture_set_paintable (GTK_PICTURE (self->picture),
+                               adw_tab_paintable_new (self->view, self->page));
+
+    update_tooltip (self);
+    update_spinner (self);
+    update_icon (self);
+    update_indicator (self);
+    update_loading (self);
+
+    g_signal_connect_object (self->page, "notify::title",
+                             G_CALLBACK (update_tooltip), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::tooltip",
+                             G_CALLBACK (update_tooltip), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::icon",
+                             G_CALLBACK (update_icon), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::indicator-icon",
+                             G_CALLBACK (update_indicator), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::indicator-activatable",
+                             G_CALLBACK (update_indicator), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::loading",
+                             G_CALLBACK (update_loading), self,
+                             G_CONNECT_SWAPPED);
+
+/*
+    update_selected (self);
+    update_state (self);
+    update_title (self);
+    update_tooltip (self);
+    update_spinner (self);
+    update_icons (self);
+    update_indicator (self);
+    update_needs_attention (self);
+    update_loading (self);
+
+    g_signal_connect_object (self->page, "notify::selected",
+                             G_CALLBACK (update_selected), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::title",
+                             G_CALLBACK (update_title), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::tooltip",
+                             G_CALLBACK (update_tooltip), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::icon",
+                             G_CALLBACK (update_icons), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::indicator-icon",
+                             G_CALLBACK (update_icons), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::indicator-activatable",
+                             G_CALLBACK (update_indicator), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::needs-attention",
+                             G_CALLBACK (update_needs_attention), self,
+                             G_CONNECT_SWAPPED);
+    g_signal_connect_object (self->page, "notify::loading",
+                             G_CALLBACK (update_loading), self,
+                             G_CONNECT_SWAPPED);*/
+  }
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PAGE]);
+}
+
+gboolean
+adw_tab_thumbnail_get_dragging (AdwTabThumbnail *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_THUMBNAIL (self), FALSE);
+
+  return self->dragging;
+}
+
+void
+adw_tab_thumbnail_set_dragging (AdwTabThumbnail *self,
+                                gboolean         dragging)
+{
+  g_return_if_fail (ADW_IS_TAB_THUMBNAIL (self));
+
+  dragging = !!dragging;
+
+  if (self->dragging == dragging)
+    return;
+
+  self->dragging = dragging;
+
+//  update_state (self);
+//  update_selected (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DRAGGING]);
+}
+
+gboolean
+adw_tab_thumbnail_get_inverted (AdwTabThumbnail *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_THUMBNAIL (self), FALSE);
+
+  return self->inverted;
+}
+
+void
+adw_tab_thumbnail_set_inverted (AdwTabThumbnail *self,
+                                gboolean         inverted)
+{
+  g_return_if_fail (ADW_IS_TAB_THUMBNAIL (self));
+
+  inverted = !!inverted;
+
+  if (self->inverted == inverted)
+    return;
+
+  self->inverted = inverted;
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INVERTED]);
+}
+
+void
+adw_tab_thumbnail_set_fully_visible (AdwTabThumbnail *self,
+                                     gboolean         fully_visible)
+{
+  g_return_if_fail (ADW_IS_TAB_THUMBNAIL (self));
+/*
+  fully_visible = !!fully_visible;
+
+  if (self->fully_visible == fully_visible)
+    return;
+
+  self->fully_visible = fully_visible;
+
+  update_state (self);
+  update_indicator (self);*/
+}
+
+void
+adw_tab_thumbnail_setup_extra_drop_target (AdwTabThumbnail *self,
+                                           GdkDragAction    actions,
+                                           GType           *types,
+                                           gsize            n_types)
+{
+  g_return_if_fail (ADW_IS_TAB_THUMBNAIL (self));
+  g_return_if_fail (n_types == 0 || types != NULL);
+
+  gtk_drop_target_set_actions (self->drop_target, actions);
+  gtk_drop_target_set_gtypes (self->drop_target, types, n_types);
+}
+
+GtkPicture *
+adw_tab_thumbnail_get_picture (AdwTabThumbnail *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_THUMBNAIL (self), NULL);
+
+  return GTK_PICTURE (self->picture);
+}
diff --git a/src/adw-tab-thumbnail.ui b/src/adw-tab-thumbnail.ui
new file mode 100644
index 00000000..0f2b5215
--- /dev/null
+++ b/src/adw-tab-thumbnail.ui
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface domain="libadwaita">
+  <requires lib="gtk" version="4.0"/>
+  <template class="AdwTabThumbnail" parent="GtkWidget">
+    <child>
+      <object class="GtkDropTarget" id="drop_target">
+        <signal name="drop" handler="drop_cb" swapped="true"/>
+      </object>
+    </child>
+    <child>
+      <object class="GtkBox" id="contents">
+        <property name="orientation">vertical</property>
+        <property name="spacing">6</property>
+        <property name="vexpand">False</property>
+        <child>
+          <object class="GtkOverlay">
+            <property name="child">
+              <object class="GtkPicture" id="picture">
+                <property name="can-shrink">True</property>
+                <property name="keep-aspect-ratio">False</property>
+                <property name="overflow">hidden</property>
+                <property name="vexpand">True</property>
+                <style>
+                  <class name="card"/>
+                </style>
+              </object>
+            </property>
+            <child type="overlay">
+              <object class="GtkButton">
+                <property name="can-focus">False</property>
+                <property name="tooltip-text" translatable="yes">Close Tab</property>
+                <property name="icon-name">window-close-symbolic</property>
+                <property name="valign">start</property>
+                <property name="halign">end</property>
+                <signal name="clicked" handler="close_clicked_cb" swapped="true"/>
+                <style>
+                  <class name="circular"/>
+                  <class name="tab-close-button"/>
+                </style>
+              </object>
+            </child>
+            <child type="overlay">
+              <object class="GtkButton" id="indicator_btn">
+                <property name="can-focus">False</property>
+                <property name="valign">start</property>
+                <property name="halign">start</property>
+                <binding name="tooltip-markup">
+                  <lookup name="indicator-tooltip" type="AdwTabPage">
+                    <lookup name="page">AdwTabThumbnail</lookup>
+                  </lookup>
+                </binding>
+                <signal name="clicked" handler="indicator_clicked_cb" swapped="true"/>
+                <style>
+                  <class name="circular"/>
+                  <class name="tab-indicator"/>
+                  <class name="image-button"/>
+                </style>
+                <property name="child">
+                  <object class="GtkImage" id="indicator_icon"/>
+                </property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox">
+            <property name="orientation">horizontal</property>
+            <property name="halign">center</property>
+            <property name="spacing">6</property>
+            <child>
+              <object class="GtkStack" id="icon_stack">
+                <property name="margin-start">4</property>
+                <property name="margin-end">2</property>
+                <child>
+                  <object class="GtkStackPage">
+                    <property name="name">icon</property>
+                    <property name="child">
+                      <object class="GtkImage" id="icon">
+                        <style>
+                          <class name="tab-icon"/>
+                        </style>
+                      </object>
+                    </property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkStackPage">
+                    <property name="name">spinner</property>
+                    <property name="child">
+                      <object class="GtkSpinner" id="spinner"/>
+                    </property>
+                  </object>
+                </child>
+              </object>
+            </child>
+            <child>
+              <object class="AdwFadingLabel">
+                <binding name="label">
+                  <lookup name="title" type="AdwTabPage">
+                    <lookup name="page">AdwTabThumbnail</lookup>
+                  </lookup>
+                </binding>
+              </object>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </template>
+</interface>
diff --git a/src/adwaita.gresources.xml b/src/adwaita.gresources.xml
index 095d250e..b993036a 100644
--- a/src/adwaita.gresources.xml
+++ b/src/adwaita.gresources.xml
@@ -3,6 +3,7 @@
   <gresource prefix="/org/gnome/Adwaita">
     <file>glsl/fade.glsl</file>
     <file>glsl/mask.glsl</file>
+    <file>glsl/tab-overview.glsl</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/adw-entry-apply-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/adw-external-link-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/actions/adw-expander-arrow-symbolic.svg</file>
@@ -27,6 +28,8 @@
     <file preprocess="xml-stripblanks">adw-tab.ui</file>
     <file preprocess="xml-stripblanks">adw-tab-bar.ui</file>
     <file preprocess="xml-stripblanks">adw-tab-button.ui</file>
+    <file preprocess="xml-stripblanks">adw-tab-overview.ui</file>
+    <file preprocess="xml-stripblanks">adw-tab-thumbnail.ui</file>
     <file preprocess="xml-stripblanks">adw-toast-widget.ui</file>
     <file preprocess="xml-stripblanks">adw-view-switcher-bar.ui</file>
     <file preprocess="xml-stripblanks">adw-view-switcher-button.ui</file>
diff --git a/src/adwaita.h b/src/adwaita.h
index 6707b57c..9e277ded 100644
--- a/src/adwaita.h
+++ b/src/adwaita.h
@@ -65,6 +65,7 @@ G_BEGIN_DECLS
 #include "adw-swipeable.h"
 #include "adw-tab-bar.h"
 #include "adw-tab-button.h"
+#include "adw-tab-overview.h"
 #include "adw-tab-view.h"
 #include "adw-timed-animation.h"
 #include "adw-toast-overlay.h"
diff --git a/src/glsl/tab-overview.glsl b/src/glsl/tab-overview.glsl
new file mode 100644
index 00000000..7ddd9dfa
--- /dev/null
+++ b/src/glsl/tab-overview.glsl
@@ -0,0 +1,18 @@
+uniform vec2 origin;
+uniform vec2 size;
+uniform float opacity;
+
+uniform sampler2D u_texture1;
+
+void
+mainImage(out vec4 fragColor,
+          in vec2  fragCoord,
+          in vec2  resolution,
+          in vec2  uv)
+{
+  if (fragCoord.x >= origin.x && fragCoord.x <= origin.x + size.x &&
+      fragCoord.y >= origin.y && fragCoord.y <= origin.y + size.y)
+    fragColor = vec4(0.0, 0.0, 0.0, 0.0);
+  else
+    fragColor = opacity * GskTexture(u_texture1, uv);
+}
diff --git a/src/meson.build b/src/meson.build
index dd6536a0..e14484ca 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -126,6 +126,7 @@ src_headers = [
   'adw-swipeable.h',
   'adw-tab-bar.h',
   'adw-tab-button.h',
+  'adw-tab-overview.h',
   'adw-tab-view.h',
   'adw-timed-animation.h',
   'adw-toast.h',
@@ -191,6 +192,7 @@ src_sources = [
   'adw-swipeable.c',
   'adw-tab-bar.c',
   'adw-tab-button.c',
+  'adw-tab-overview.c',
   'adw-tab-view.c',
   'adw-timed-animation.c',
   'adw-toast.c',
@@ -216,6 +218,8 @@ libadwaita_private_sources += files([
   'adw-shadow-helper.c',
   'adw-tab.c',
   'adw-tab-box.c',
+  'adw-tab-grid.c',
+  'adw-tab-thumbnail.c',
   'adw-toast-widget.c',
   'adw-view-switcher-button.c',
   'adw-widget-utils.c',
diff --git a/src/stylesheet/widgets/_tab-view.scss b/src/stylesheet/widgets/_tab-view.scss
index 1b140c40..c56a8173 100644
--- a/src/stylesheet/widgets/_tab-view.scss
+++ b/src/stylesheet/widgets/_tab-view.scss
@@ -139,7 +139,40 @@ dnd {
   }
 }
 
+taboverview {
+  background: gtkmix($window_bg_color, $window_fg_color, 85%);
+}
+
+tabthumbnail > box {
+  margin: 10px;
+
+  picture {
+    background: $window_bg_color;
+  }
+
+  button.circular {
+    margin: 6px;
+    color: $window_fg_color;
+    background-color: gtkalpha($window_bg_color, .75);
+    min-width: 24px;
+    min-height: 24px;
+
+    @if $contrast == 'high' {
+      box-shadow: 0 0 0 1px currentColor;
+    }
+
+    &:hover {
+      background-color: gtkalpha(gtkmix($window_bg_color, currentColor, 90%), .75);
+    }
+
+    &:active {
+      background-color: gtkalpha(gtkmix($window_bg_color, currentColor, 80%), .75);
+    }
+  }
+}
+
 tabview:drop(active),
-tabbox:drop(active) {
+tabbox:drop(active),
+tabgrid:drop(active) {
   box-shadow: none;
 }


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