[libadwaita/wip/exalm/tab-overview: 1/2] Add AdwTabOverview




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

    Add AdwTabOverview

 src/adw-tab-grid-private.h                         |   84 +
 src/adw-tab-grid.c                                 | 3775 ++++++++++++++++++++
 src/adw-tab-overview-private.h                     |   24 +
 src/adw-tab-overview.c                             | 2401 +++++++++++++
 src/adw-tab-overview.h                             |  112 +
 src/adw-tab-overview.ui                            |  142 +
 src/adw-tab-thumbnail-private.h                    |   45 +
 src/adw-tab-thumbnail.c                            |  668 ++++
 src/adw-tab-thumbnail.ui                           |  161 +
 src/adw-tab-view-private.h                         |    2 +
 src/adw-tab-view.c                                 |  503 +++
 src/adw-tab-view.h                                 |   18 +
 src/adwaita.gresources.xml                         |    3 +
 src/adwaita.h                                      |    1 +
 .../scalable/status/adw-tab-unpin-symbolic.svg     |    1 +
 src/meson.build                                    |    4 +
 src/stylesheet/_colors.scss                        |    3 +
 src/stylesheet/_defaults.scss                      |    4 +
 src/stylesheet/widgets/_tab-view.scss              |  104 +-
 19 files changed, 8054 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..db80da64
--- /dev/null
+++ b/src/adw-tab-grid-private.h
@@ -0,0 +1,84 @@
+/*
+ * 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-thumbnail-private.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    animate);
+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_inverted (AdwTabGrid *self);
+void     adw_tab_grid_set_inverted (AdwTabGrid *self,
+                                    gboolean    inverted);
+
+AdwTabThumbnail *adw_tab_grid_get_transition_thumbnail (AdwTabGrid *self);
+
+void adw_tab_grid_set_visible_range (AdwTabGrid *self,
+                                     double      lower,
+                                     double      upper,
+                                     double      page_size);
+
+void adw_tab_grid_adjustment_shifted (AdwTabGrid *self,
+                                      double      delta);
+
+double adw_tab_grid_get_scrolled_tab_y (AdwTabGrid *self);
+
+void adw_tab_grid_reset_scrolled_tab (AdwTabGrid *self);
+
+void adw_tab_grid_scroll_to_page (AdwTabGrid *self,
+                                  AdwTabPage *page,
+                                  gboolean    animate);
+
+void adw_tab_grid_set_hovering (AdwTabGrid *self,
+                                gboolean    hovering);
+
+void adw_tab_grid_set_search_terms (AdwTabGrid *self,
+                                    const char *terms);
+
+gboolean adw_tab_grid_get_empty (AdwTabGrid *self);
+
+gboolean adw_tab_grid_focus_first_row (AdwTabGrid *self,
+                                       int         column);
+gboolean adw_tab_grid_focus_last_row  (AdwTabGrid *self,
+                                       int         column);
+
+void adw_tab_grid_focus_page (AdwTabGrid *self,
+                              AdwTabPage *page);
+
+G_END_DECLS
diff --git a/src/adw-tab-grid.c b/src/adw-tab-grid.c
new file mode 100644
index 00000000..870acc6f
--- /dev/null
+++ b/src/adw-tab-grid.c
@@ -0,0 +1,3775 @@
+/*
+ * 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-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 RESIZE_ANIMATION_DURATION 200
+#define REORDER_ANIMATION_DURATION 250
+#define ICON_RESIZE_ANIMATION_DURATION 200
+
+#define SCROLL_PADDING 16
+
+#define MIN_COLUMNS 2
+#define MAX_COLUMNS 8
+
+#define MIN_THUMBNAIL_WIDTH 100
+#define MAX_THUMBNAIL_WIDTH 500
+#define SINGLE_TAB_MAX_PERCENTAGE 0.5
+
+#define SMALL_GRID_WIDTH 360
+#define SMALL_GRID_PERCENTAGE 1
+#define SMALL_NAT_THUMBNAIL_WIDTH 200
+
+#define LARGE_GRID_WIDTH 2560
+#define LARGE_GRID_PERCENTAGE 0.85
+#define LARGE_NAT_THUMBNAIL_WIDTH 360
+
+typedef enum {
+  TAB_RESIZE_NORMAL,
+  TAB_RESIZE_FIXED_TAB_SIZE
+} 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;
+
+  gboolean visible;
+} TabInfo;
+
+struct _AdwTabGrid
+{
+  GtkWidget parent_instance;
+
+  gboolean pinned;
+  AdwTabOverview *tab_overview;
+  AdwTabView *view;
+  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_height;
+  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;
+  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;
+
+  TabInfo *scroll_animation_tab;
+
+  GdkDragAction extra_drag_actions;
+  GType *extra_drag_types;
+  gsize extra_drag_n_types;
+
+  double n_columns;
+  double max_n_columns;
+  double initial_max_n_columns;
+  int tab_width;
+  int tab_height;
+
+  double visible_lower;
+  double visible_upper;
+  double page_size;
+
+  GtkStringFilter *title_filter;
+  GtkStringFilter *tooltip_filter;
+  GtkStringFilter *keyword_filter;
+  GtkFilter *filter;
+  gboolean searching;
+
+  gboolean empty;
+};
+
+G_DEFINE_FINAL_TYPE (AdwTabGrid, adw_tab_grid, GTK_TYPE_WIDGET)
+
+enum {
+  PROP_0,
+  PROP_PINNED,
+  PROP_TAB_OVERVIEW,
+  PROP_VIEW,
+  PROP_RESIZE_FROZEN,
+  PROP_EMPTY,
+  LAST_PROP,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+  SIGNAL_SCROLL_RELATIVE,
+  SIGNAL_SCROLL_TO_TAB,
+  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 (!gtk_widget_should_layout (info->container))
+      continue;
+
+    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 inline GList *
+find_link_for_widget (AdwTabGrid *self,
+                      GtkWidget  *widget)
+{
+  GList *l;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (info->container == widget)
+      return l;
+  }
+
+  return 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;
+}
+
+static int
+get_n_visible_tabs (AdwTabGrid *self)
+{
+  GList *l;
+  int ret = 0;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (info->page && info->visible)
+      ret++;
+  }
+
+  return ret;
+}
+
+static GList *
+find_nth_visible_tab (AdwTabGrid *self,
+                      guint       position)
+{
+  GList *l;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (!info->page)
+        continue;
+
+    if (!info->visible)
+        continue;
+
+    if (!position--)
+        return l;
+  }
+
+  return NULL;
+}
+
+static TabInfo *
+get_focused_info (AdwTabGrid *self)
+{
+  GtkWidget *focus_child = gtk_widget_get_focus_child (GTK_WIDGET (self));
+  GList *l;
+
+  if (!focus_child)
+    return NULL;
+
+  l = find_link_for_widget (self, focus_child);
+
+  if (!l || !l->data)
+    return NULL;
+
+  return l->data;
+}
+
+static int
+get_focused_column (AdwTabGrid *self)
+{
+  TabInfo *info = get_focused_info (self);
+
+  if (!info)
+    return -1;
+
+  return (int) round (fmod (info->final_index, self->n_columns));
+}
+
+/* Layout */
+
+static inline AdwTabGrid *
+get_other_tab_grid (AdwTabGrid *self)
+{
+  if (self->pinned)
+    return adw_tab_overview_get_tab_grid (self->tab_overview);
+  else
+    return adw_tab_overview_get_pinned_tab_grid (self->tab_overview);
+}
+
+static double
+get_max_n_columns (AdwTabGrid *self)
+{
+  GList *l;
+  double max_columns = 0;
+  double other_max_columns = 0;
+  AdwTabGrid *other_grid = get_other_tab_grid (self);
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    max_columns += info->appear_progress;
+  }
+
+  for (l = other_grid->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    other_max_columns += info->appear_progress;
+  }
+
+  return MAX (max_columns, other_max_columns);
+}
+
+static double
+get_n_columns (AdwTabGrid *self,
+               int         for_width,
+               double      max_n_columns)
+{
+  double t;
+  double nat_width;
+
+  if (for_width < 0)
+    return MAX (max_n_columns, 1);
+
+  max_n_columns = CLAMP (max_n_columns, 1, MAX_COLUMNS);
+
+  t = CLAMP (((double) for_width - SMALL_GRID_WIDTH) /
+             (LARGE_GRID_WIDTH - SMALL_GRID_WIDTH), 0, 1);
+  nat_width = adw_lerp (SMALL_NAT_THUMBNAIL_WIDTH, LARGE_NAT_THUMBNAIL_WIDTH,
+                        adw_easing_ease (ADW_EASE_OUT_CUBIC, t));
+
+  return CLAMP (ceil ((double) for_width / nat_width),
+                MIN (MIN_COLUMNS, max_n_columns), max_n_columns);
+}
+
+static int
+get_tab_width (AdwTabGrid *self,
+               int         for_width)
+{
+  double n = get_n_columns (self, for_width, self->max_n_columns);
+  double total_size = for_width;
+  double t;
+  int ret;
+
+  t = CLAMP ((total_size - SMALL_GRID_WIDTH) / (LARGE_GRID_WIDTH - SMALL_GRID_WIDTH), 0, 1);
+  total_size *= adw_lerp (SMALL_GRID_PERCENTAGE, LARGE_GRID_PERCENTAGE,
+                          adw_easing_ease (ADW_EASE_OUT_CUBIC, t));
+
+  if (n <= self->max_n_columns) {
+    double max = get_n_columns (self, for_width, MAX_COLUMNS);
+
+    total_size *= (SINGLE_TAB_MAX_PERCENTAGE + (1 - SINGLE_TAB_MAX_PERCENTAGE) * n / max);
+  }
+
+  ret = (int) ceil ((double) (total_size - SPACING * (n + 1)) / n);
+
+  return CLAMP (ret, MIN_THUMBNAIL_WIDTH, MAX_THUMBNAIL_WIDTH);
+}
+
+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)
+{
+  int n_columns = ceil (self->n_columns);
+  double col = fmod (index, n_columns);
+  double row = (index - col) / n_columns;
+  double offset = self->allocated_width;
+  int x, y;
+
+  offset -= self->n_columns * (self->tab_width + SPACING) - SPACING;
+  offset /= 2;
+
+  if (col > n_columns - 1) {
+    double start, end, t;
+
+    if (is_rtl) {
+      start = self->allocated_width - offset - self->tab_width;
+      end = offset;
+    } else {
+      start = offset;
+      end = self->allocated_width - offset - self->tab_width;
+    }
+
+    t = 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 - offset - self->tab_width : offset;
+
+    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 void
+measure_tab_grid (AdwTabGrid     *self,
+                  GtkOrientation  orientation,
+                  int             for_size,
+                  int            *minimum,
+                  int            *natural,
+                  gboolean        animated)
+{
+  GList *l;
+  int min, nat;
+
+  min = nat = 0;
+
+  if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+    for (l = self->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+      int child_min, child_nat;
+
+      if (!gtk_widget_should_layout (info->container))
+        continue;
+
+      gtk_widget_measure (info->container, orientation, -1,
+                          &child_min, &child_nat, NULL, NULL);
+
+      if (animated)
+        min = MAX (min, calculate_tab_width (info, child_min));
+      else
+        min = MAX (min, child_min) + SPACING;
+
+      nat += child_nat + SPACING;
+    }
+
+    nat += SPACING;
+    min += SPACING;
+  } else {
+    double n_columns, n_rows;
+    int child_width = -1, child_height;
+    double index = 0;
+    int height;
+
+    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 (!gtk_widget_should_layout (info->container))
+        continue;
+
+      if (animated) {
+        index += info->appear_progress;
+      } else {
+        if (info->page)
+          index++;
+      }
+    }
+
+    n_columns = get_n_columns (self, for_size, self->max_n_columns);
+    n_rows = ceil (index / n_columns);
+
+    if (animated) {
+      double col = fmod (index, n_columns);
+
+      if (col > 0 && col < 1)
+        n_rows = n_rows + col - 1;
+    }
+
+    if (n_rows < 1)
+      height = (child_height + SPACING * 2) * n_rows + self->end_padding;
+    else
+      height = (child_height + SPACING) * n_rows + SPACING + self->end_padding;
+
+    if (!self->pinned)
+      height = MAX (self->last_height, height);
+
+    min = MAX (min, height);
+    nat = MAX (nat, height);
+  }
+
+  if (minimum)
+    *minimum = min;
+
+  if (natural)
+    *natural = nat;
+}
+
+static void
+calculate_tab_layout (AdwTabGrid *self)
+{
+  gboolean is_rtl;
+  GList *l;
+  double index = 0, final_index = 0;
+
+  if (self->tab_resize_mode != TAB_RESIZE_FIXED_TAB_SIZE &&
+      self->initial_max_n_columns < 0)
+    self->max_n_columns = get_max_n_columns (self);
+
+  self->n_columns = get_n_columns (self, self->allocated_width, self->max_n_columns);
+
+  if (self->context_menu)
+    gtk_popover_present (GTK_POPOVER (self->context_menu));
+
+  is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+  self->tab_width = get_tab_width (self, self->allocated_width);
+  self->tab_height = get_tab_height (self, self->tab_width);
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (!gtk_widget_should_layout (info->container))
+      continue;
+
+    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++;
+
+    if (self->tab_resize_mode == TAB_RESIZE_FIXED_TAB_SIZE) {
+      self->end_padding = self->allocated_height - info->pos_y - info->height - SPACING;
+      self->final_end_padding = self->allocated_height - info->final_y - info->final_height - SPACING;
+    }
+  }
+}
+
+static void
+get_visible_range (AdwTabGrid *self,
+                   int        *lower,
+                   int        *upper)
+{
+  int min = SPACING;
+  int max = self->allocated_height - SPACING;
+
+  min = MAX (min, (int) floor (self->visible_lower) + SPACING);
+  max = MIN (max, (int) ceil (self->visible_upper) - 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;
+}
+
+/* Search */
+
+static gboolean
+tab_should_be_visible (AdwTabGrid *self,
+                       AdwTabPage *page)
+{
+  if (!self->searching)
+    return TRUE;
+
+  return gtk_filter_match (self->filter, page);
+}
+
+static void
+set_empty (AdwTabGrid *self,
+           gboolean    empty)
+{
+  if (self->empty == empty)
+    return;
+
+  self->empty = empty;
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_EMPTY]);
+}
+
+static void
+search_changed_cb (AdwTabGrid      *self,
+                   GtkFilterChange  change)
+{
+  GList *l;
+  gboolean changed = FALSE;
+  gboolean empty = TRUE;
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+    gboolean visible;
+
+    if (change == GTK_FILTER_CHANGE_LESS_STRICT && info->visible) {
+      empty = FALSE;
+      continue;
+    }
+
+    if (change == GTK_FILTER_CHANGE_MORE_STRICT && !info->visible)
+      continue;
+
+    visible = tab_should_be_visible (self, info->page);
+
+    if (visible)
+      empty = FALSE;
+
+    if (visible != info->visible) {
+      info->visible = visible;
+      gtk_widget_set_visible (info->container, visible);
+      changed = TRUE;
+    }
+  }
+
+  set_empty (self, empty);
+
+  if (changed)
+    gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+/* Tab resize delay */
+
+static void
+resize_animation_value_cb (double      value,
+                           AdwTabGrid *self)
+{
+  double target_max_n_columns = get_max_n_columns (self);
+
+  self->end_padding = (int) floor (adw_lerp (self->initial_end_padding, 0, value));
+
+  self->max_n_columns = adw_lerp (self->initial_max_n_columns, target_max_n_columns, 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;
+  self->initial_max_n_columns = -1;
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+set_tab_resize_mode_do (AdwTabGrid    *self,
+                        TabResizeMode  mode)
+{
+  gboolean notify;
+
+  if (self->tab_resize_mode == mode)
+    return;
+
+  if (mode == TAB_RESIZE_FIXED_TAB_SIZE) {
+    GList *l;
+
+    self->last_height = self->allocated_height;
+
+    for (l = self->tabs; l; l = l->next) {
+      TabInfo *info = l->data;
+
+      if (info->appear_animation)
+        info->last_height = info->final_height;
+      else
+        info->last_height = info->height;
+    }
+  } else {
+    self->last_height = 0;
+  }
+
+  if (mode == TAB_RESIZE_NORMAL) {
+    self->initial_end_padding = self->end_padding;
+    self->initial_max_n_columns = self->max_n_columns;
+
+    adw_animation_play (self->resize_animation);
+  }
+
+  notify = (self->tab_resize_mode == TAB_RESIZE_NORMAL) !=
+           (mode == TAB_RESIZE_NORMAL);
+
+  self->tab_resize_mode = mode;
+
+  if (notify)
+    g_object_notify_by_pspec (G_OBJECT (self), props[PROP_RESIZE_FROZEN]);
+}
+
+static void
+set_tab_resize_mode (AdwTabGrid    *self,
+                     TabResizeMode  mode)
+{
+  set_tab_resize_mode_do (self, mode);
+  set_tab_resize_mode_do (get_other_tab_grid (self), mode);
+}
+
+/* Hover */
+
+static void
+update_hover (AdwTabGrid *self)
+{
+  if (!self->dragging && !self->hovering)
+    set_tab_resize_mode (self, TAB_RESIZE_NORMAL);
+}
+
+/* Keybindings */
+
+static void
+reorder_tab_cb (AdwTabGrid *self,
+                GVariant   *args)
+{
+  GtkDirectionType direction;
+  gboolean success = FALSE;
+  TabInfo *info = get_focused_info (self);
+
+  if (!self->view || !info || !info->page || self->searching)
+    return;
+
+  g_variant_get (args, "h", &direction);
+
+  if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL) {
+    if (direction == GTK_DIR_LEFT)
+      direction = GTK_DIR_RIGHT;
+    else if (direction == GTK_DIR_RIGHT)
+      direction = GTK_DIR_LEFT;
+  }
+
+  if (direction == GTK_DIR_LEFT) {
+    success = adw_tab_view_reorder_backward (self->view, info->page);
+  } else if (direction == GTK_DIR_RIGHT) {
+    success = adw_tab_view_reorder_forward (self->view, info->page);
+  } else if (direction == GTK_DIR_UP) {
+    int position = adw_tab_view_get_page_position (self->view, info->page);
+    position -= self->n_columns;
+
+    if (position >= adw_tab_view_get_n_pinned_pages (self->view) ||
+        (self->pinned && position >= 0))
+      success = adw_tab_view_reorder_page (self->view, info->page, position);
+  } else if (direction == GTK_DIR_DOWN) {
+    int position = adw_tab_view_get_page_position (self->view, info->page);
+    position += self->n_columns;
+
+    if ((self->pinned && position < adw_tab_view_get_n_pinned_pages (self->view)) ||
+        (!self->pinned && position < adw_tab_view_get_n_pages (self->view)))
+    success = adw_tab_view_reorder_page (self->view, info->page, position);
+  }
+
+  if (!success)
+    gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+static void
+add_reorder_bindings (GtkWidgetClass   *widget_class,
+                      guint             keysym,
+                      GtkDirectionType  direction)
+{
+  /* 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,
+                                "h", direction);
+  gtk_widget_class_add_binding (widget_class, keypad_keysym, GDK_SHIFT_MASK,
+                                (GtkShortcutFunc) reorder_tab_cb,
+                                "h", direction);
+}
+
+static void
+activate_tab (AdwTabGrid *self)
+{
+  TabInfo *info = get_focused_info (self);
+
+  if (!info || !info->page)
+    return;
+
+  adw_tab_view_set_selected_page (self->view, info->page);
+  adw_tab_overview_set_open (self->tab_overview, FALSE);
+}
+
+/* Scrolling */
+
+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
+animate_scroll_relative (AdwTabGrid *self,
+                         double      delta,
+                         guint       duration)
+{
+  g_signal_emit (self, signals[SIGNAL_SCROLL_RELATIVE], 0, delta, duration);
+}
+
+static void
+scroll_to_tab_full (AdwTabGrid *self,
+                    TabInfo    *info,
+                    double      pos,
+                    guint       duration,
+                    gboolean    keep_selected_visible)
+{
+  self->scroll_animation_tab = info;
+
+  int tab_height;
+  double padding, offset;
+
+  tab_height = info->final_height;
+
+  padding = MIN (SCROLL_PADDING, self->page_size / 2);
+
+  if (pos < 0)
+    pos = get_tab_y (self, info, TRUE);
+
+  if (pos - SPACING < self->visible_lower)
+    offset = -padding;
+  else if (pos + tab_height + SPACING > self->visible_upper)
+    offset = tab_height + padding - self->page_size;
+  else
+    return;
+
+  g_signal_emit (self, signals[SIGNAL_SCROLL_TO_TAB], 0, offset, duration);
+}
+
+static void
+scroll_to_tab (AdwTabGrid *self,
+               TabInfo    *info,
+               guint       duration)
+{
+  scroll_to_tab_full (self, info, -1, duration, FALSE);
+}
+
+/* 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), NULL);
+
+  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, 0, width - self->reordered_tab->width);
+  *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 y, delta_ms, start_threshold, end_threshold, autoscroll_factor;
+  gint64 time;
+  int offset = 0;
+  int tab_height = 0;
+  int autoscroll_area = 0;
+
+  if (self->visible_upper - self->visible_lower >= self->allocated_height)
+    return G_SOURCE_CONTINUE;
+
+  if (self->reordered_tab) {
+    tab_height = self->reordered_tab->height;
+    y = (double) self->reorder_y - SPACING;
+  } else if (self->drop_target_tab) {
+    tab_height = self->drop_target_tab->height;
+    y = (double) self->drop_target_y - tab_height / 2;
+  } else {
+    return G_SOURCE_CONTINUE;
+  }
+
+  autoscroll_area = tab_height / 4;
+
+  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 = self->visible_lower + autoscroll_area;
+  end_threshold = self->visible_upper - 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;
+  animate_scroll_relative (self, offset, 0);
+  update_drag_reodering (self);
+
+  return G_SOURCE_CONTINUE;
+}
+
+static void
+start_autoscroll (AdwTabGrid *self)
+{
+  GdkFrameClock *frame_clock;
+
+  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 (self->searching)
+    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)
+{
+  self->pressed_tab = find_tab_info_at (self, start_x, start_y);
+
+  if (!self->pressed_tab)
+    return;
+
+  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 + 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;
+  }
+
+  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 gboolean
+focus_tab (AdwGizmo         *widget,
+           GtkDirectionType  direction)
+{
+  return gtk_widget_grab_focus (GTK_WIDGET (widget));
+}
+
+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->visible = tab_should_be_visible (self, page);
+  info->container = adw_gizmo_new ("tabgridchild", measure_tab, allocate_tab,
+                                   NULL, NULL,
+                                   focus_tab,
+                                   (AdwGizmoGrabFocusFunc) adw_widget_grab_focus_self);
+  gtk_widget_set_visible (info->container, info->visible);
+  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), NULL);
+
+  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);
+
+  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++;
+
+  if (!self->searching)
+    set_empty (self, FALSE);
+
+  adw_animation_play (info->appear_animation);
+
+  calculate_tab_layout (self);
+
+  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--;
+
+  if (self->n_tabs == 0 || (self->searching && get_n_visible_tabs (self) == 0))
+    set_empty (self, TRUE);
+}
+
+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) {
+    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 && !self->pinned)
+      set_tab_resize_mode (self, TAB_RESIZE_NORMAL);
+    else
+      set_tab_resize_mode (self, TAB_RESIZE_FIXED_TAB_SIZE);
+  }
+
+  g_assert (info->page);
+
+  if (gtk_widget_is_focus (info->container))
+    adw_tab_grid_try_focus_selected_tab (self, TRUE);
+
+  if (info == self->selected_tab)
+    adw_tab_grid_select_page (self, NULL);
+
+  adw_tab_thumbnail_set_page (info->tab, NULL);
+
+  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);
+
+  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);
+
+    info->reorder_ignore_bounds = TRUE;
+
+    index = calculate_placeholder_index (self, x, y);
+
+    self->tabs = g_list_insert (self->tabs, info, index);
+    self->n_tabs++;
+
+    if (!self->searching)
+      set_empty (self, FALSE);
+
+    self->reorder_placeholder = info;
+    self->reorder_index = g_list_index (self->tabs, info);
+  }
+
+  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;
+
+  gtk_widget_set_opacity (self->reorder_placeholder->container, 1);
+
+  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 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);
+
+  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_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;
+  get_other_tab_grid (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;
+
+  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;
+
+  if (self->searching)
+    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;
+
+  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;
+
+  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;
+  get_other_tab_grid (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.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;
+  }
+
+  gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+  do_popup (self, info, x, y);
+}
+
+static void
+popup_menu_cb (GtkWidget  *widget,
+               const char *action_name,
+               GVariant   *parameter)
+{
+  AdwTabGrid *self = ADW_TAB_GRID (widget);
+  TabInfo *info = get_focused_info (self);
+
+  if (!info || !info->page)
+    return;
+
+  do_popup (self, info, -1, -1);
+}
+
+/* Clicking */
+
+static void
+handle_click (AdwTabGrid *self,
+              TabInfo    *info,
+              GtkGesture *gesture)
+{
+  adw_tab_view_set_selected_page (self->view, info->page);
+
+  gtk_widget_grab_focus (info->container);
+}
+
+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;
+
+  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;
+
+  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_set_open (self->tab_overview, FALSE);
+}
+
+/* Overrides */
+
+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);
+  GList *l;
+  GtkAllocation child_allocation;
+
+  measure_tab_grid (self, GTK_ORIENTATION_HORIZONTAL, -1,
+                    &self->allocated_width, NULL, TRUE);
+  self->allocated_width = MAX (self->allocated_width, width);
+
+  measure_tab_grid (self, GTK_ORIENTATION_VERTICAL, width,
+                    &self->allocated_height, NULL, TRUE);
+  self->allocated_height = MAX (self->allocated_height, height);
+
+  calculate_tab_layout (self);
+
+  for (l = self->tabs; l; l = l->next) {
+    TabInfo *info = l->data;
+
+    if (!gtk_widget_should_layout (info->container))
+      continue;
+
+    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);
+    child_allocation.width = MAX (0, info->width);
+    child_allocation.height = MAX (0, info->height);
+
+    gtk_widget_size_allocate (info->container, &child_allocation, baseline);
+  }
+}
+
+static GtkSizeRequestMode
+adw_tab_grid_get_request_mode (GtkWidget *widget)
+{
+  return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static inline gboolean
+page_can_be_focused (GList *l)
+{
+  return ((TabInfo *) l->data)->page && ((TabInfo *) l->data)->visible;
+}
+
+static gboolean
+adw_tab_grid_focus (GtkWidget        *widget,
+                    GtkDirectionType  direction)
+{
+  AdwTabGrid *self = ADW_TAB_GRID (widget);
+  gboolean is_rtl;
+  GtkDirectionType start, end;
+  GList *l;
+  TabInfo *info = NULL;
+  int n_columns = (int) ceil (self->n_columns);
+
+  is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
+  start = is_rtl ? GTK_DIR_RIGHT : GTK_DIR_LEFT;
+  end = is_rtl ? GTK_DIR_LEFT : GTK_DIR_RIGHT;
+
+  l = find_link_for_widget (self, gtk_widget_get_focus_child (widget));
+
+  if (!self->n_tabs)
+    return GDK_EVENT_PROPAGATE;
+
+  if (((direction == GTK_DIR_TAB_FORWARD ||
+        direction == GTK_DIR_TAB_BACKWARD) &&
+       l && l->data != self->selected_tab) || !l) {
+    info = self->selected_tab;
+  } else if (direction == start) {
+    do {
+      l = l->prev;
+    } while (l && l->data && !page_can_be_focused (l));
+
+    info = l ? l->data : NULL;
+  } else if (direction == end) {
+    do {
+      l = l->next;
+    } while (l && l->data && !page_can_be_focused (l));
+
+    info = l ? l->data : NULL;
+  } else if (direction == GTK_DIR_UP) {
+    do {
+      l = l->prev;
+
+      if (l && l->data && page_can_be_focused (l))
+        n_columns--;
+    } while (l && l->data && n_columns > 0);
+
+    info = l ? l->data : NULL;
+  } else if (direction == GTK_DIR_DOWN) {
+    GList *last_link = find_nth_visible_tab (self, get_n_visible_tabs (self) - 1);
+    TabInfo *last_info = last_link->data;
+    int last_col = (int) round (fmod (last_info->final_index, n_columns));
+    int empty_slots = n_columns - last_col;
+
+    do {
+      l = l->next;
+
+      if (l && l->data && page_can_be_focused (l))
+        n_columns--;
+    } while (l && l->data && n_columns > 0);
+
+    if (n_columns > 0 && n_columns < empty_slots)
+      l = last_link;
+
+    info = l ? l->data : NULL;
+  }
+
+  if (!info) {
+    AdwTabGrid *grid = get_other_tab_grid (self);
+
+    if (self->pinned && direction == GTK_DIR_DOWN) {
+      int column = get_focused_column (self);
+
+      return adw_tab_grid_focus_first_row (grid, column);
+    }
+
+    if (self->pinned && direction == end)
+      return adw_tab_grid_focus_first_row (grid, 0) ||
+             gtk_widget_keynav_failed (widget, direction);
+
+    if (!self->pinned && direction == GTK_DIR_UP) {
+      int column = get_focused_column (self);
+
+      return adw_tab_grid_focus_last_row (grid, column);
+    }
+
+    if (!self->pinned && direction == start)
+      return adw_tab_grid_focus_last_row (grid, -1) ||
+             gtk_widget_keynav_failed (widget, direction);
+
+    if (direction != GTK_DIR_UP && direction != GTK_DIR_DOWN)
+      return gtk_widget_keynav_failed (widget, direction);
+
+    return GDK_EVENT_PROPAGATE;
+  }
+
+  scroll_to_tab (self, info, FOCUS_ANIMATION_DURATION);
+
+  return gtk_widget_grab_focus (info->container);
+}
+
+static gboolean
+adw_tab_grid_grab_focus (GtkWidget *widget)
+{
+  AdwTabGrid *self = ADW_TAB_GRID (widget);
+
+  if (!self->selected_tab)
+    return GDK_EVENT_PROPAGATE;
+
+  scroll_to_tab (self, self->selected_tab, FOCUS_ANIMATION_DURATION);
+
+  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;
+  }
+
+  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);
+
+  g_clear_object (&self->filter);
+  self->title_filter = NULL;
+  self->tooltip_filter = NULL;
+  self->keyword_filter = NULL;
+
+  g_clear_object (&self->resize_animation);
+
+  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_EMPTY:
+    g_value_set_boolean (value, adw_tab_grid_get_empty (self));
+    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;
+
+  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->grab_focus = adw_tab_grid_grab_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);
+
+  props[PROP_EMPTY] =
+    g_param_spec_boolean ("empty", NULL, NULL,
+                          TRUE,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  signals[SIGNAL_SCROLL_RELATIVE] =
+    g_signal_new ("scroll-relative",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2,
+                  G_TYPE_DOUBLE,
+                  G_TYPE_UINT);
+
+  signals[SIGNAL_SCROLL_TO_TAB] =
+    g_signal_new ("scroll-to-tab",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2,
+                  G_TYPE_DOUBLE,
+                  G_TYPE_UINT);
+
+  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);
+
+  gtk_widget_class_add_binding (widget_class, GDK_KEY_Return, 0,
+                                (GtkShortcutFunc) activate_tab, NULL);
+  gtk_widget_class_add_binding (widget_class, GDK_KEY_ISO_Enter, 0,
+                                (GtkShortcutFunc) activate_tab, NULL);
+  gtk_widget_class_add_binding (widget_class, GDK_KEY_KP_Enter, 0,
+                                (GtkShortcutFunc) activate_tab, NULL);
+
+  add_reorder_bindings (widget_class, GDK_KEY_Left,  GTK_DIR_LEFT);
+  add_reorder_bindings (widget_class, GDK_KEY_Right, GTK_DIR_RIGHT);
+  add_reorder_bindings (widget_class, GDK_KEY_Up,    GTK_DIR_UP);
+  add_reorder_bindings (widget_class, GDK_KEY_Down,  GTK_DIR_DOWN);
+
+  gtk_widget_class_set_css_name (widget_class, "tabgrid");
+}
+
+static void
+adw_tab_grid_init (AdwTabGrid *self)
+{
+  GtkEventController *controller;
+  AdwAnimationTarget *target;
+  GtkExpression *expression;
+
+  self->can_remove_placeholder = TRUE;
+  self->initial_max_n_columns = -1;
+  self->visible_lower = 0;
+  self->visible_upper = 0;
+
+  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);
+
+  expression = gtk_property_expression_new (ADW_TYPE_TAB_PAGE, NULL, "title");
+  self->title_filter = gtk_string_filter_new (expression);
+
+  expression = gtk_property_expression_new (ADW_TYPE_TAB_PAGE, NULL, "tooltip");
+  self->tooltip_filter = gtk_string_filter_new (expression);
+
+  expression = gtk_property_expression_new (ADW_TYPE_TAB_PAGE, NULL, "keyword");
+  self->keyword_filter = gtk_string_filter_new (expression);
+
+  self->filter = GTK_FILTER (gtk_any_filter_new ());
+  gtk_multi_filter_append (GTK_MULTI_FILTER (self->filter),
+                           GTK_FILTER (self->title_filter));
+  gtk_multi_filter_append (GTK_MULTI_FILTER (self->filter),
+                           GTK_FILTER (self->tooltip_filter));
+  gtk_multi_filter_append (GTK_MULTI_FILTER (self->filter),
+                           GTK_FILTER (self->keyword_filter));
+
+  g_signal_connect_swapped (self->filter, "changed",
+                            G_CALLBACK (search_changed_cb), self);
+}
+
+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) {
+      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,
+                                     gboolean    animate)
+{
+  g_return_if_fail (ADW_IS_TAB_GRID (self));
+
+  if (!self->selected_tab)
+    return;
+
+  scroll_to_tab (self, self->selected_tab, animate ? FOCUS_ANIMATION_DURATION : 0);
+
+  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_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);
+  }
+}
+
+AdwTabThumbnail *
+adw_tab_grid_get_transition_thumbnail (AdwTabGrid *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_GRID (self), NULL);
+
+  if (self->selected_tab)
+    return self->selected_tab->tab;
+
+  return NULL;
+}
+
+void
+adw_tab_grid_set_visible_range (AdwTabGrid *self,
+                                double      lower,
+                                double      upper,
+                                double      page_size)
+{
+  g_return_if_fail (ADW_IS_TAB_GRID (self));
+
+  self->visible_lower = lower;
+  self->visible_upper = upper;
+  self->page_size = page_size;
+
+  gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+void
+adw_tab_grid_adjustment_shifted (AdwTabGrid *self,
+                                 double      delta)
+{
+  if (!self->drop_target_tab)
+    return;
+
+  self->drop_target_y += delta;
+
+  set_drop_target_tab (self, find_tab_info_at (self,
+                                               self->drop_target_x,
+                                               self->drop_target_y));
+}
+
+double
+adw_tab_grid_get_scrolled_tab_y (AdwTabGrid *self)
+{
+  if (!self->scroll_animation_tab)
+    return NAN;
+
+  return get_tab_y (self, self->scroll_animation_tab, TRUE);
+}
+
+void
+adw_tab_grid_reset_scrolled_tab (AdwTabGrid *self)
+{
+  self->scroll_animation_tab = NULL;
+}
+
+void
+adw_tab_grid_scroll_to_page (AdwTabGrid *self,
+                             AdwTabPage *page,
+                             gboolean    animate)
+{
+  TabInfo *info = find_info_for_page (self, page);
+
+  if (!info)
+    return;
+
+  scroll_to_tab (self, info, animate ? FOCUS_ANIMATION_DURATION : 0);
+}
+
+void
+adw_tab_grid_set_hovering (AdwTabGrid *self,
+                           gboolean    hovering)
+{
+  self->hovering = hovering;
+  update_hover (self);
+}
+
+void
+adw_tab_grid_set_search_terms (AdwTabGrid *self,
+                               const char *terms)
+{
+  self->searching = terms && *terms;
+  gtk_string_filter_set_search (self->title_filter, terms);
+  gtk_string_filter_set_search (self->tooltip_filter, terms);
+  gtk_string_filter_set_search (self->keyword_filter, terms);
+
+  if (!self->searching)
+    set_empty (self, self->n_tabs > 0);
+}
+
+gboolean
+adw_tab_grid_get_empty (AdwTabGrid *self)
+{
+  return self->empty;
+}
+
+gboolean
+adw_tab_grid_focus_first_row (AdwTabGrid *self,
+                              int         column)
+{
+  TabInfo *info;
+  int n_tabs;
+
+  if (!self->tabs)
+    return FALSE;
+
+  if (column < 0)
+    column = MIN (self->n_tabs, self->n_columns) - 1;
+
+  n_tabs = get_n_visible_tabs (self);
+  column = CLAMP (column, 0, MIN (n_tabs, self->n_columns) - 1);
+
+  info = find_nth_visible_tab (self, column)->data;
+
+  scroll_to_tab (self, info, FOCUS_ANIMATION_DURATION);
+
+  return gtk_widget_grab_focus (info->container);
+}
+
+gboolean
+adw_tab_grid_focus_last_row (AdwTabGrid *self,
+                             int         column)
+{
+  TabInfo *info;
+  int last_col, n_tabs;
+
+  if (!self->tabs)
+      return FALSE;
+
+  info = g_list_last (self->tabs)->data;
+
+  last_col = (int) round (fmod (info->final_index, self->n_columns));
+  n_tabs = get_n_visible_tabs (self);
+
+  if (column < 0)
+    column = (int) round (last_col);
+
+  column = CLAMP (column, 0, MIN (n_tabs - 1, last_col));
+
+  info = find_nth_visible_tab (self, n_tabs - 1 - last_col + column)->data;
+
+  scroll_to_tab (self, info, FOCUS_ANIMATION_DURATION);
+
+  return gtk_widget_grab_focus (info->container);
+}
+
+void
+adw_tab_grid_focus_page (AdwTabGrid *self,
+                         AdwTabPage *page)
+{
+  TabInfo *info = find_info_for_page (self, page);
+
+  if (!info)
+    return;
+
+  scroll_to_tab (self, info, FOCUS_ANIMATION_DURATION);
+
+  gtk_widget_grab_focus (info->container);
+}
diff --git a/src/adw-tab-overview-private.h b/src/adw-tab-overview-private.h
new file mode 100644
index 00000000..32c929e4
--- /dev/null
+++ b/src/adw-tab-overview-private.h
@@ -0,0 +1,24 @@
+/*
+ * 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"
+
+#include "adw-tab-grid-private.h"
+
+G_BEGIN_DECLS
+
+AdwTabGrid *adw_tab_overview_get_tab_grid        (AdwTabOverview *self);
+AdwTabGrid *adw_tab_overview_get_pinned_tab_grid (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..8d53a974
--- /dev/null
+++ b/src/adw-tab-overview.c
@@ -0,0 +1,2401 @@
+/*
+ * 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-animation-util.h"
+#include "adw-bin.h"
+#include "adw-header-bar.h"
+#include "adw-macros-private.h"
+#include "adw-style-manager.h"
+#include "adw-tab-grid-private.h"
+#include "adw-tab-thumbnail-private.h"
+#include "adw-timed-animation.h"
+#include "adw-widget-utils-private.h"
+#include "adw-window-title.h"
+
+#define SCROLL_ANIMATION_DURATION 200
+#define TRANSITION_DURATION 400
+#define THUMBNAIL_BORDER_RADIUS 12
+#define WINDOW_BORDER_RADIUS 12
+
+/**
+ * AdwTabOverview:
+ *
+ * TODO
+ *
+ * Since: 1.3
+ */
+
+struct _AdwTabOverview
+{
+  GtkWidget parent_instance;
+
+  GtkWidget *overview;
+  GtkWidget *stack;
+  GtkWidget *scrollable;
+  GtkWidget *child_bin;
+  GtkWidget *header_bar;
+  GtkWidget *title;
+  GtkWidget *new_tab_button;
+  GtkWidget *search_button;
+  GtkWidget *search_bar;
+  GtkWidget *search_entry;
+  GtkWidget *extra_menu_button;
+  AdwTabView *view;
+
+  AdwTabGrid *grid;
+  AdwTabGrid *pinned_grid;
+
+  gboolean enable_search;
+  gboolean enable_new_tab;
+  gboolean search_active;
+
+  gboolean is_open;
+  AdwAnimation *open_animation;
+  double progress;
+  gboolean animating;
+
+  AdwTabThumbnail *transition_thumbnail;
+  GtkWidget *transition_picture;
+  gboolean transition_pinned;
+
+  GtkWidget *last_focus;
+};
+
+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,
+  PROP_OPEN,
+  PROP_INVERTED,
+  PROP_ENABLE_SEARCH,
+  PROP_SEARCH_ACTIVE,
+  PROP_ENABLE_NEW_TAB,
+  PROP_NEW_TAB_ACTION_NAME,
+  PROP_NEW_TAB_ACTION_TARGET,
+  PROP_EXTRA_MENU,
+  PROP_SHOW_START_TITLE_BUTTONS,
+  PROP_SHOW_END_TITLE_BUTTONS,
+  LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+  SIGNAL_NEW_TAB_CLICKED,
+  SIGNAL_EXTRA_DRAG_DROP,
+  SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+#define ADW_TYPE_TAB_OVERVIEW_SCROLLABLE (adw_tab_overview_scrollable_get_type ())
+
+G_DECLARE_FINAL_TYPE (AdwTabOverviewScrollable, adw_tab_overview_scrollable, ADW, TAB_OVERVIEW_SCROLLABLE, 
GtkWidget)
+
+struct _AdwTabOverviewScrollable
+{
+  GtkWidget parent_instance;
+
+  GtkWidget *grid;
+  GtkWidget *pinned_grid;
+  GtkWidget *overview;
+  GtkWidget *new_button;
+  GtkWidget *search_entry;
+
+  GtkAdjustment *hadjustment;
+  GtkAdjustment *vadjustment;
+  GtkScrollablePolicy hscroll_policy;
+  GtkScrollablePolicy vscroll_policy;
+
+  AdwAnimation *scroll_animation;
+  AdwTabGrid *scroll_animation_grid;
+  gboolean scroll_animation_done;
+  double scroll_animation_from;
+  double scroll_animation_offset;
+  gboolean block_scrolling;
+  double adjustment_prev_value;
+
+  int grid_pos;
+  int pinned_grid_pos;
+
+  gboolean hovering;
+};
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (AdwTabOverviewScrollable, adw_tab_overview_scrollable, GTK_TYPE_WIDGET,
+                               G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL))
+
+enum {
+  SCROLLABLE_PROP_0,
+  SCROLLABLE_PROP_GRID,
+  SCROLLABLE_PROP_PINNED_GRID,
+  SCROLLABLE_PROP_OVERVIEW,
+  SCROLLABLE_PROP_NEW_BUTTON,
+  /* GtkScrollable */
+  SCROLLABLE_PROP_HADJUSTMENT,
+  SCROLLABLE_PROP_VADJUSTMENT,
+  SCROLLABLE_PROP_HSCROLL_POLICY,
+  SCROLLABLE_PROP_VSCROLL_POLICY,
+  LAST_SCROLLABLE_PROP = SCROLLABLE_PROP_HADJUSTMENT
+};
+
+static GParamSpec *scrollable_props[LAST_SCROLLABLE_PROP];
+
+static void
+vadjustment_value_changed_cb (AdwTabOverviewScrollable *self)
+{
+  double value = gtk_adjustment_get_value (self->vadjustment);
+
+  adw_tab_grid_adjustment_shifted (ADW_TAB_GRID (self->grid),
+                                   value - self->adjustment_prev_value);
+
+  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
+vadjustment_weak_notify (gpointer  data,
+                         GObject  *object)
+{
+  AdwTabOverviewScrollable *self = data;
+
+  self->vadjustment = NULL;
+}
+
+static void
+set_vadjustment (AdwTabOverviewScrollable *self,
+                 GtkAdjustment            *adjustment)
+{
+  if (self->vadjustment) {
+    g_signal_handlers_disconnect_by_func (self->vadjustment, vadjustment_value_changed_cb, self);
+
+    g_object_weak_unref (G_OBJECT (self->vadjustment), vadjustment_weak_notify, self);
+  }
+
+  self->vadjustment = adjustment;
+
+  if (self->vadjustment) {
+    g_object_weak_ref (G_OBJECT (self->vadjustment), vadjustment_weak_notify, self);
+
+    g_signal_connect_swapped (self->vadjustment, "value-changed",
+                              G_CALLBACK (vadjustment_value_changed_cb), self);
+  }
+}
+
+static inline int
+get_grid_offset (AdwTabOverviewScrollable *self,
+                 AdwTabGrid               *grid)
+{
+  if (grid == ADW_TAB_GRID (self->grid))
+    return self->grid_pos;
+
+  if (grid == ADW_TAB_GRID (self->pinned_grid))
+    return self->pinned_grid_pos;
+
+  g_assert_not_reached ();
+}
+
+static double
+get_scroll_animation_value (AdwTabOverviewScrollable *self)
+{
+  double to, value;
+  double scrolled_y;
+
+  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->vadjustment);
+
+  to = self->scroll_animation_offset;
+
+  scrolled_y = adw_tab_grid_get_scrolled_tab_y (self->scroll_animation_grid);
+  if (!isnan (scrolled_y)) {
+    double page_size = gtk_adjustment_get_page_size (self->vadjustment);
+    double upper = gtk_adjustment_get_upper (self->vadjustment);
+
+    to += scrolled_y + get_grid_offset (self, self->scroll_animation_grid);
+    to = CLAMP (to, 0, upper - page_size);
+  }
+
+  value = adw_animation_get_value (self->scroll_animation);
+
+  return round (adw_lerp (self->scroll_animation_from, to, value));
+}
+
+static void
+scroll_animation_cb (double                    value,
+                     AdwTabOverviewScrollable *self)
+{
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+scroll_animation_done_cb (AdwTabOverviewScrollable *self)
+{
+  self->scroll_animation_done = TRUE;
+  gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+stop_kinetic_scrolling (AdwTabOverviewScrollable *self)
+{
+  GtkWidget *window = gtk_widget_get_ancestor (GTK_WIDGET (self), GTK_TYPE_SCROLLED_WINDOW);
+
+  g_assert (window);
+
+  /* HACK: Need to cancel kinetic scrolling. If only the built-in adjustment
+   * animation API was public, we wouldn't have to do any of this... */
+  gtk_scrolled_window_set_kinetic_scrolling (GTK_SCROLLED_WINDOW (window), FALSE);
+  gtk_scrolled_window_set_kinetic_scrolling (GTK_SCROLLED_WINDOW (window), TRUE);
+}
+
+static void
+animate_scroll (AdwTabOverviewScrollable *self,
+                AdwTabGrid               *grid,
+                double                    offset,
+                guint                     duration)
+{
+  stop_kinetic_scrolling (self);
+
+  if (duration == 0) {
+    gtk_adjustment_set_value (self->vadjustment, offset);
+
+    return;
+  }
+
+  self->scroll_animation_done = FALSE;
+  self->scroll_animation_grid = grid;
+  self->scroll_animation_from = gtk_adjustment_get_value (self->vadjustment);
+  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
+scroll_relative_cb (AdwTabOverviewScrollable *self,
+                    double                    delta,
+                    guint                     duration,
+                    AdwTabGrid               *grid)
+{
+  double current_value = gtk_adjustment_get_value (self->vadjustment);
+
+  if (adw_animation_get_state (self->scroll_animation) == ADW_ANIMATION_PLAYING) {
+    double tab_y = adw_tab_grid_get_scrolled_tab_y (self->scroll_animation_grid);
+
+    current_value = self->scroll_animation_offset;
+
+    if (!isnan (tab_y))
+      current_value += tab_y + get_grid_offset (self, self->scroll_animation_grid);
+  }
+
+  animate_scroll (self, grid, current_value + delta, duration);
+}
+
+static void
+scroll_to_tab_cb (AdwTabOverviewScrollable *self,
+                  double                    offset,
+                  guint                     duration,
+                  AdwTabGrid               *grid)
+{
+  animate_scroll (self, grid, offset, duration);
+}
+
+static void
+set_grid (AdwTabOverviewScrollable  *self,
+          GtkWidget                **field,
+          AdwTabGrid                *grid)
+{
+  if (*field) {
+    g_signal_handlers_disconnect_by_func (*field, scroll_relative_cb, self);
+    g_signal_handlers_disconnect_by_func (*field, scroll_to_tab_cb, self);
+
+    gtk_widget_unparent (*field);
+  }
+
+  *field = GTK_WIDGET (grid);
+
+  if (*field) {
+    gtk_widget_set_parent (*field, GTK_WIDGET (self));
+
+    g_signal_connect_swapped (*field, "scroll-relative",
+                              G_CALLBACK (scroll_relative_cb), self);
+    g_signal_connect_swapped (*field, "scroll-to-tab",
+                              G_CALLBACK (scroll_to_tab_cb), self);
+  }
+}
+
+static void
+set_hovering (AdwTabOverviewScrollable *self,
+              gboolean                  hovering)
+{
+  self->hovering = hovering;
+
+  adw_tab_grid_set_hovering (ADW_TAB_GRID (self->grid), hovering);
+  adw_tab_grid_set_hovering (ADW_TAB_GRID (self->pinned_grid), hovering);
+}
+
+static void
+motion_cb (AdwTabOverviewScrollable *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;
+
+  set_hovering (self, TRUE);
+}
+
+static void
+leave_cb (AdwTabOverviewScrollable *self,
+          GtkEventController       *controller)
+{
+  set_hovering (self, FALSE);
+}
+
+static void
+adw_tab_overview_scrollable_unmap (GtkWidget *widget)
+{
+  AdwTabOverviewScrollable *self = ADW_TAB_OVERVIEW_SCROLLABLE (widget);
+
+  set_hovering (self, FALSE);
+
+  GTK_WIDGET_CLASS (adw_tab_overview_scrollable_parent_class)->unmap (widget);
+}
+
+static void
+adw_tab_overview_scrollable_measure (GtkWidget      *widget,
+                                     GtkOrientation  orientation,
+                                     int             for_size,
+                                     int            *minimum,
+                                     int            *natural,
+                                     int            *minimum_baseline,
+                                     int            *natural_baseline)
+{
+  int min = 0, nat = 0;
+  GtkWidget *child;
+
+  for (child = gtk_widget_get_first_child (widget);
+       child;
+       child = gtk_widget_get_next_sibling (child)) {
+    int child_min, child_nat;
+
+    gtk_widget_measure (child, orientation, for_size,
+                        &child_min, &child_nat, NULL, NULL);
+
+    if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+      min = MAX (min, child_min);
+      nat = MAX (nat, child_nat);
+    } else {
+      min += child_min;
+      nat += child_nat;
+    }
+  }
+
+  if (minimum_baseline)
+    *minimum_baseline = -1;
+  if (natural_baseline)
+    *natural_baseline = -1;
+}
+
+static void
+adw_tab_overview_scrollable_size_allocate (GtkWidget *widget,
+                                           int        width,
+                                           int        height,
+                                           int        baseline)
+{
+  AdwTabOverviewScrollable *self = ADW_TAB_OVERVIEW_SCROLLABLE (widget);
+  double value;
+  int grid_height, pinned_height, new_button_height;
+
+  gtk_widget_measure (self->grid, GTK_ORIENTATION_VERTICAL, width,
+                      &grid_height, NULL, NULL, NULL);
+  gtk_widget_measure (self->pinned_grid, GTK_ORIENTATION_VERTICAL, width,
+                      &pinned_height, NULL, NULL, NULL);
+
+  if (gtk_widget_should_layout (self->new_button))
+    gtk_widget_measure (self->new_button, GTK_ORIENTATION_VERTICAL, width,
+                        &new_button_height, NULL, NULL, NULL);
+  else
+    new_button_height = 0;
+
+  self->pinned_grid_pos = 0;
+  self->grid_pos = self->pinned_grid_pos + pinned_height;
+
+  grid_height = MAX (grid_height, height - new_button_height - self->grid_pos);
+
+  value = get_scroll_animation_value (self);
+
+  self->block_scrolling = TRUE;
+  gtk_adjustment_configure (self->vadjustment,
+                            value,
+                            0,
+                            self->grid_pos + grid_height + new_button_height,
+                            height * 0.1,
+                            height * 0.9,
+                            height);
+  self->block_scrolling = FALSE;
+
+  /* The value may have changed during gtk_adjustment_configure() */
+  value = gtk_adjustment_get_value (self->vadjustment);
+
+  if (value <= 0)
+    gtk_widget_add_css_class (self->overview, "scrolled-to-top");
+  else
+    gtk_widget_remove_css_class (self->overview, "scrolled-to-top");
+
+  adw_tab_grid_set_visible_range (ADW_TAB_GRID (self->pinned_grid),
+                                  CLAMP (value - self->pinned_grid_pos, 0, pinned_height),
+                                  CLAMP (value - self->pinned_grid_pos + height - new_button_height, 0, 
pinned_height),
+                                  height - new_button_height);
+  adw_tab_grid_set_visible_range (ADW_TAB_GRID (self->grid),
+                                  CLAMP (value - self->grid_pos, 0, grid_height),
+                                  CLAMP (value - self->grid_pos + height - new_button_height, 0, 
grid_height),
+                                  height - new_button_height);
+
+  if (self->scroll_animation_done) {
+    g_clear_pointer (&self->scroll_animation_grid, adw_tab_grid_reset_scrolled_tab);
+    self->scroll_animation_done = FALSE;
+    adw_animation_reset (self->scroll_animation);
+  }
+
+  gtk_widget_allocate (self->pinned_grid, width, pinned_height, baseline,
+                       gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (0, self->pinned_grid_pos - 
value)));
+  gtk_widget_allocate (self->grid, width, grid_height, baseline,
+                       gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (0, self->grid_pos - value)));
+}
+
+static void
+adw_tab_overview_scrollable_dispose (GObject *object)
+{
+  AdwTabOverviewScrollable *self = ADW_TAB_OVERVIEW_SCROLLABLE (object);
+
+  g_clear_object (&self->scroll_animation);
+
+  set_vadjustment (self, NULL);
+
+  g_clear_pointer (&self->grid, gtk_widget_unparent);
+  g_clear_pointer (&self->pinned_grid, gtk_widget_unparent);
+
+  self->overview = NULL;
+  self->new_button = NULL;
+
+  G_OBJECT_CLASS (adw_tab_overview_scrollable_parent_class)->dispose (object);
+}
+
+static void
+adw_tab_overview_scrollable_get_property (GObject    *object,
+                                          guint       prop_id,
+                                          GValue     *value,
+                                          GParamSpec *pspec)
+{
+  AdwTabOverviewScrollable *self = ADW_TAB_OVERVIEW_SCROLLABLE (object);
+
+  switch (prop_id) {
+  case SCROLLABLE_PROP_GRID:
+    g_value_set_object (value, self->grid);
+    break;
+  case SCROLLABLE_PROP_PINNED_GRID:
+    g_value_set_object (value, self->pinned_grid);
+    break;
+  case SCROLLABLE_PROP_OVERVIEW:
+    g_value_set_object (value, self->overview);
+    break;
+  case SCROLLABLE_PROP_NEW_BUTTON:
+    g_value_set_object (value, self->new_button);
+    break;
+  case SCROLLABLE_PROP_HADJUSTMENT:
+    g_value_set_object (value, self->hadjustment);
+    break;
+  case SCROLLABLE_PROP_VADJUSTMENT:
+    g_value_set_object (value, self->vadjustment);
+    break;
+  case SCROLLABLE_PROP_HSCROLL_POLICY:
+    g_value_set_enum (value, self->hscroll_policy);
+    break;
+  case SCROLLABLE_PROP_VSCROLL_POLICY:
+    g_value_set_enum (value, self->vscroll_policy);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_tab_overview_scrollable_set_property (GObject      *object,
+                                          guint         prop_id,
+                                          const GValue *value,
+                                          GParamSpec   *pspec)
+{
+  AdwTabOverviewScrollable *self = ADW_TAB_OVERVIEW_SCROLLABLE (object);
+
+  switch (prop_id) {
+  case SCROLLABLE_PROP_GRID:
+    set_grid (self, &self->grid, g_value_get_object (value));
+    break;
+  case SCROLLABLE_PROP_PINNED_GRID:
+    set_grid (self, &self->pinned_grid, g_value_get_object (value));
+    break;
+  case SCROLLABLE_PROP_OVERVIEW:
+    self->overview = g_value_get_object (value);
+    break;
+  case SCROLLABLE_PROP_NEW_BUTTON:
+    self->new_button = g_value_get_object (value);
+    break;
+  case SCROLLABLE_PROP_HADJUSTMENT:
+    self->hadjustment = g_value_get_object (value);
+    break;
+  case SCROLLABLE_PROP_VADJUSTMENT:
+    set_vadjustment (self, g_value_get_object (value));
+    break;
+  case SCROLLABLE_PROP_HSCROLL_POLICY:
+    self->hscroll_policy = g_value_get_enum (value);
+    break;
+  case SCROLLABLE_PROP_VSCROLL_POLICY:
+    self->vscroll_policy = g_value_get_enum (value);
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+adw_tab_overview_scrollable_class_init (AdwTabOverviewScrollableClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+  GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+  object_class->dispose = adw_tab_overview_scrollable_dispose;
+  object_class->get_property = adw_tab_overview_scrollable_get_property;
+  object_class->set_property = adw_tab_overview_scrollable_set_property;
+
+  widget_class->unmap = adw_tab_overview_scrollable_unmap;
+  widget_class->measure = adw_tab_overview_scrollable_measure;
+  widget_class->size_allocate = adw_tab_overview_scrollable_size_allocate;
+
+  scrollable_props[SCROLLABLE_PROP_GRID] =
+    g_param_spec_object ("grid", NULL, NULL,
+                         ADW_TYPE_TAB_GRID,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  scrollable_props[SCROLLABLE_PROP_PINNED_GRID] =
+    g_param_spec_object ("pinned-grid", NULL, NULL,
+                         ADW_TYPE_TAB_GRID,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  scrollable_props[SCROLLABLE_PROP_OVERVIEW] =
+    g_param_spec_object ("overview", NULL, NULL,
+                         GTK_TYPE_WIDGET,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  scrollable_props[SCROLLABLE_PROP_NEW_BUTTON] =
+    g_param_spec_object ("new-button", NULL, NULL,
+                         GTK_TYPE_WIDGET,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS);
+
+  g_object_class_install_properties (object_class, LAST_SCROLLABLE_PROP, scrollable_props);
+
+  g_object_class_override_property (object_class,
+                                    SCROLLABLE_PROP_HADJUSTMENT,
+                                    "hadjustment");
+  g_object_class_override_property (object_class,
+                                    SCROLLABLE_PROP_VADJUSTMENT,
+                                    "vadjustment");
+  g_object_class_override_property (object_class,
+                                    SCROLLABLE_PROP_HSCROLL_POLICY,
+                                    "hscroll-policy");
+  g_object_class_override_property (object_class,
+                                    SCROLLABLE_PROP_VSCROLL_POLICY,
+                                    "vscroll-policy");
+}
+
+static void
+adw_tab_overview_scrollable_init (AdwTabOverviewScrollable *self)
+{
+  GtkEventController *controller;
+  AdwAnimationTarget *target;
+
+  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);
+
+  /* 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);
+}
+
+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
+empty_changed_cb (AdwTabOverview *self)
+{
+  gboolean empty =
+    self->search_active &&
+    adw_tab_grid_get_empty (self->grid) &&
+    adw_tab_grid_get_empty (self->pinned_grid);
+
+  gtk_stack_set_visible_child_name (GTK_STACK (self->stack),
+                                    empty ? "empty" : "tabs");
+}
+
+static void
+update_header_bar (AdwTabOverview *self)
+{
+  gtk_widget_set_visible (self->header_bar,
+                          self->enable_search ||
+                          adw_tab_overview_get_extra_menu (self) ||
+                          adw_tab_overview_get_show_start_title_buttons (self) ||
+                          adw_tab_overview_get_show_end_title_buttons (self));
+}
+
+static void
+update_new_tab_button (AdwTabOverview *self)
+{
+  gtk_widget_set_visible (self->new_tab_button,
+                          self->enable_new_tab && !self->search_active);
+  gtk_widget_queue_resize (self->scrollable);
+}
+
+static void
+set_search_active (AdwTabOverview *self,
+                   gboolean        search_active)
+{
+  if (search_active == self->search_active)
+    return;
+
+  self->search_active = search_active;
+
+  update_new_tab_button (self);
+  empty_changed_cb (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SEARCH_ACTIVE]);
+}
+
+static void
+search_changed_cb (AdwTabOverview *self)
+{
+  const char *text = gtk_editable_get_text (GTK_EDITABLE (self->search_entry));
+
+  adw_tab_grid_set_search_terms (self->grid, text);
+  adw_tab_grid_set_search_terms (self->pinned_grid, text);
+
+  set_search_active (self, text && *text);
+}
+
+static void
+stop_search_cb (AdwTabOverview *self)
+{
+  gtk_editable_set_text (GTK_EDITABLE (self->search_entry), "");
+
+  adw_tab_grid_set_search_terms (self->grid, "");
+  adw_tab_grid_set_search_terms (self->pinned_grid, "");
+
+  set_search_active (self, FALSE);
+}
+
+static void
+notify_action_name_cb (AdwTabOverview *self)
+{
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NEW_TAB_ACTION_NAME]);
+}
+
+static void
+notify_action_target_cb (AdwTabOverview *self)
+{
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NEW_TAB_ACTION_TARGET]);
+}
+
+static gboolean
+new_tab_idle_cb (AdwTabOverview *self)
+{
+  adw_tab_overview_set_open (self, FALSE);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+new_tab_clicked_cb (AdwTabOverview *self)
+{
+  g_signal_emit (self, signals[SIGNAL_NEW_TAB_CLICKED], 0);
+
+  g_idle_add ((GSourceFunc) new_tab_idle_cb, self);
+}
+
+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_n_pages_cb (AdwTabOverview *self)
+{
+  guint n_pages;
+  char *title_str;
+
+  if (!self->view) {
+    adw_window_title_set_title (ADW_WINDOW_TITLE (self->title), "");
+    return;
+  }
+
+  n_pages = adw_tab_view_get_n_pages (self->view);
+
+  title_str = g_strdup_printf (ngettext ("%u Tab", "%u Tabs", n_pages), n_pages);
+
+  adw_window_title_set_title (ADW_WINDOW_TITLE (self->title), title_str);
+
+  g_free (title_str);
+}
+
+static void
+notify_pinned_cb (AdwTabPage     *page,
+                  GParamSpec     *pspec,
+                  AdwTabOverview *self)
+{
+  AdwTabGrid *from, *to;
+
+  if (adw_tab_page_get_pinned (page)) {
+    from = self->grid;
+    to = self->pinned_grid;
+  } else {
+    from = self->pinned_grid;
+    to = self->grid;
+  }
+
+  adw_tab_grid_detach_page (from, page);
+  adw_tab_grid_attach_page (to, page, adw_tab_view_get_n_pinned_pages (self->view));
+
+  adw_tab_grid_scroll_to_page (to, page, TRUE);
+
+  adw_tab_grid_focus_page (to, page);
+}
+
+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)
+{
+  if (self->transition_picture) {
+    gtk_widget_set_opacity (self->transition_picture, 1);
+    g_clear_object (&self->transition_picture);
+
+    adw_tab_thumbnail_fade_out (self->transition_thumbnail);
+    self->transition_thumbnail = NULL;
+  }
+
+  if (!self->is_open) {
+    gtk_widget_set_child_visible (self->overview, FALSE);
+    gtk_widget_set_can_target (self->overview, FALSE);
+    gtk_widget_set_can_focus (self->overview, FALSE);
+    gtk_widget_set_can_target (self->child_bin, TRUE);
+    gtk_widget_set_can_focus (self->child_bin, TRUE);
+
+    gtk_search_bar_set_search_mode (GTK_SEARCH_BAR (self->search_bar), FALSE);
+
+    if (self->last_focus) {
+      gtk_widget_grab_focus (self->last_focus);
+
+      g_object_remove_weak_pointer (G_OBJECT (self->last_focus),
+                                    (gpointer *) &self->last_focus);
+      self->last_focus = NULL;
+    }
+  }
+
+  self->animating = FALSE;
+  gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+calculate_bounds (AdwTabOverview  *self,
+                  graphene_rect_t *bounds,
+                  graphene_rect_t *transition_bounds,
+                  graphene_rect_t *clip_bounds,
+                  graphene_size_t *clip_scale)
+{
+  GtkWidget *widget = GTK_WIDGET (self);
+  graphene_rect_t view_bounds, thumbnail_bounds;
+  double view_ratio, thumb_ratio;
+  AdwTabPage *page = adw_tab_view_get_selected_page (self->view);
+
+  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->transition_picture, widget, &thumbnail_bounds))
+    graphene_rect_init (&thumbnail_bounds, 0, 0, 0, 0);
+
+  graphene_rect_init (bounds, 0, 0,
+                      gtk_widget_get_width (widget),
+                      gtk_widget_get_height (widget));
+
+  view_ratio = view_bounds.size.width / view_bounds.size.height;
+  thumb_ratio = thumbnail_bounds.size.width / thumbnail_bounds.size.height;
+
+  if (view_ratio > thumb_ratio) {
+    double new_width = view_bounds.size.height * thumb_ratio;
+    double xalign = adw_tab_page_get_thumbnail_xalign (page);
+
+    if (gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL)
+      xalign = 1 - xalign;
+
+    view_bounds.origin.x += (float) (view_bounds.size.width - new_width) * xalign;
+    view_bounds.size.width = new_width;
+  } else if (view_ratio < thumb_ratio) {
+    double new_height = view_bounds.size.width / thumb_ratio;
+    double yalign = adw_tab_page_get_thumbnail_yalign (page);
+
+    view_bounds.origin.y += (float) (view_bounds.size.height - new_height) * yalign;
+    view_bounds.size.height = new_height;
+  }
+
+  graphene_rect_interpolate (bounds, &view_bounds,
+                             self->progress, clip_bounds);
+
+  graphene_size_init (clip_scale,
+                      adw_lerp (1, thumbnail_bounds.size.width / view_bounds.size.width, self->progress),
+                      adw_lerp (1, thumbnail_bounds.size.height / view_bounds.size.height, self->progress));
+
+  graphene_rect_init (transition_bounds,
+                      adw_lerp (0, thumbnail_bounds.origin.x, self->progress),
+                      adw_lerp (0, thumbnail_bounds.origin.y, self->progress),
+                      clip_bounds->size.width * clip_scale->width,
+                      clip_bounds->size.height * clip_scale->height);
+}
+
+static void
+should_round_corners (AdwTabOverview *self,
+                      gboolean       *round_top_left,
+                      gboolean       *round_top_right,
+                      gboolean       *round_bottom_left,
+                      gboolean       *round_bottom_right)
+{
+  GtkRoot *root = gtk_widget_get_root (GTK_WIDGET (self));
+  GtkBorder border, padding, window_border, window_padding;
+  graphene_rect_t bounds;
+  GtkStyleContext *context;
+  GdkSurface *surface;
+  GdkToplevelState state;
+  gboolean top_left = TRUE;
+  gboolean top_right = TRUE;
+  gboolean bottom_left = TRUE;
+  gboolean bottom_right = TRUE;
+
+  *round_top_left = FALSE;
+  *round_top_right = FALSE;
+  *round_bottom_left = FALSE;
+  *round_bottom_right = FALSE;
+
+  if (!root || !GTK_IS_WINDOW (root) || !gtk_widget_get_realized (GTK_WIDGET (root)))
+    return;
+
+  surface = gtk_native_get_surface (GTK_NATIVE (root));
+  state = gdk_toplevel_get_state (GDK_TOPLEVEL (surface));
+
+  if ((state & (GDK_TOPLEVEL_STATE_FULLSCREEN |
+                GDK_TOPLEVEL_STATE_MAXIMIZED |
+                GDK_TOPLEVEL_STATE_TILED |
+                GDK_TOPLEVEL_STATE_TOP_TILED |
+                GDK_TOPLEVEL_STATE_RIGHT_TILED |
+                GDK_TOPLEVEL_STATE_BOTTOM_TILED |
+                GDK_TOPLEVEL_STATE_LEFT_TILED)) > 0)
+    return;
+
+  context = gtk_widget_get_style_context (GTK_WIDGET (root));
+
+  if (!gtk_style_context_has_class (context, "csd") ||
+      gtk_style_context_has_class (context, "solid-csd"))
+    return;
+
+  gtk_style_context_get_border (context, &window_border);
+  gtk_style_context_get_padding (context, &window_padding);
+
+  context = gtk_widget_get_style_context (GTK_WIDGET (self));
+  gtk_style_context_get_border (context, &border);
+  gtk_style_context_get_padding (context, &padding);
+
+  if (!gtk_widget_compute_bounds (GTK_WIDGET (self), GTK_WIDGET (root), &bounds))
+    return;
+
+  if (border.left + padding.left + window_border.left + window_padding.left + bounds.origin.x > 0) {
+    top_left = FALSE;
+    bottom_left = FALSE;
+  }
+
+  if (border.right + padding.right + window_border.right + window_padding.right > 0 ||
+      bounds.origin.x + bounds.size.width < gtk_widget_get_width (GTK_WIDGET (root))) {
+    top_right = FALSE;
+    bottom_right = FALSE;
+  }
+
+  if (border.top + padding.top + window_border.top + window_padding.top + bounds.origin.y > 0) {
+    top_left = FALSE;
+    top_right = FALSE;
+  }
+
+  if (border.bottom + padding.bottom + window_border.bottom + window_padding.bottom > 0 ||
+      bounds.origin.y + bounds.size.height < gtk_widget_get_height (GTK_WIDGET (root))) {
+    bottom_left = FALSE;
+    bottom_right = FALSE;
+  }
+
+  *round_top_left = top_left;
+  *round_top_right = top_right;
+  *round_bottom_left = bottom_left;
+  *round_bottom_right = bottom_right;
+}
+
+static void
+adw_tab_overview_snapshot (GtkWidget   *widget,
+                           GtkSnapshot *snapshot)
+{
+  AdwTabOverview *self = ADW_TAB_OVERVIEW (widget);
+  graphene_rect_t bounds, transition_bounds, clip_bounds;
+  graphene_size_t clip_scale, corner_size, window_corner_size;
+  GskRoundedRect transition_rect;
+  gboolean round_top_left, round_top_right;
+  gboolean round_bottom_left, round_bottom_right;
+  GdkRGBA rgba;
+  GdkDisplay *display;
+  AdwStyleManager *style_manager;
+  gboolean hc;
+
+  if (!self->animating) {
+    if (self->is_open) {
+      GtkSnapshot *child_snapshot;
+
+      gtk_widget_snapshot_child (widget, self->overview, snapshot);
+
+      /* 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 */
+      child_snapshot = gtk_snapshot_new ();
+
+      gtk_widget_snapshot_child (widget, self->child_bin, child_snapshot);
+
+      g_object_unref (child_snapshot);
+    } else {
+      gtk_widget_snapshot_child (widget, self->child_bin, snapshot);
+    }
+
+    return;
+  }
+
+  calculate_bounds (self, &bounds, &transition_bounds, &clip_bounds, &clip_scale);
+  should_round_corners (self, &round_top_left, &round_top_right,
+                        &round_bottom_left, &round_bottom_right);
+
+  graphene_size_init (&corner_size,
+                      adw_lerp (0, THUMBNAIL_BORDER_RADIUS, self->progress),
+                      adw_lerp (0, THUMBNAIL_BORDER_RADIUS, self->progress));
+
+  graphene_size_init (&window_corner_size,
+                      adw_lerp (WINDOW_BORDER_RADIUS,
+                                THUMBNAIL_BORDER_RADIUS, self->progress),
+                      adw_lerp (WINDOW_BORDER_RADIUS,
+                                THUMBNAIL_BORDER_RADIUS, self->progress));
+
+  gsk_rounded_rect_init (&transition_rect, &transition_bounds,
+                         round_top_left     ? &window_corner_size : &corner_size,
+                         round_top_right    ? &window_corner_size : &corner_size,
+                         round_bottom_right ? &window_corner_size : &corner_size,
+                         round_bottom_left  ? &window_corner_size : &corner_size);
+
+  display = gtk_widget_get_display (widget);
+  style_manager = adw_style_manager_get_for_display (display);
+  hc = adw_style_manager_get_high_contrast (style_manager);
+
+  /* Draw overview */
+  gtk_widget_snapshot_child (widget, self->overview, snapshot);
+
+  /* Draw dim layer */
+  if (!gtk_style_context_lookup_color (gtk_widget_get_style_context (widget),
+                                       "shade_color", &rgba))
+    rgba.alpha = 0;
+
+  rgba.alpha *= 1 - self->progress;
+
+  gtk_snapshot_append_color (snapshot, &rgba, &bounds);
+
+  /* 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. */
+  gtk_snapshot_push_rounded_clip (snapshot, &transition_rect);
+
+  if (self->transition_pinned)
+    gtk_snapshot_push_cross_fade (snapshot, adw_easing_ease (ADW_EASE_IN_EXPO, self->progress));
+
+  gtk_snapshot_translate (snapshot, &transition_bounds.origin);
+  gtk_snapshot_scale (snapshot, clip_scale.width, clip_scale.height);
+  gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (-clip_bounds.origin.x,
+                                                          -clip_bounds.origin.y));
+  gtk_widget_snapshot_child (widget, self->child_bin, snapshot);
+
+  if (self->transition_pinned) {
+    GtkStyleContext *context = gtk_widget_get_style_context (self->transition_picture);
+
+    if (!gtk_style_context_lookup_color (context, "thumbnail_bg_color", &rgba))
+      rgba.red = rgba.green = rgba.blue = rgba.alpha = 1;
+
+    gtk_snapshot_pop (snapshot);
+    gtk_snapshot_append_color (snapshot, &rgba, &bounds);
+    gtk_snapshot_pop (snapshot);
+  }
+
+  gtk_snapshot_pop (snapshot);
+
+  /* Draw outer outline */
+  if (hc) {
+    rgba.red = rgba.green = rgba.blue = 0;
+    rgba.alpha = 0.5;
+  } else {
+    if (!gtk_style_context_lookup_color (gtk_widget_get_style_context (widget),
+                                         "shade_color", &rgba))
+      rgba.alpha = 0;
+  }
+
+  rgba.alpha *= adw_easing_ease (ADW_EASE_OUT_EXPO, self->progress);
+
+  gtk_snapshot_append_outset_shadow (snapshot, &transition_rect,
+                                     &rgba, 0, 0, 1, 0);
+
+  /* Draw inner outline */
+  if (!self->transition_pinned) {
+    /* Keep in sync with $window_outline_color */
+    rgba.red = rgba.green = rgba.blue = 1;
+    rgba.alpha = hc ? 0.3 : 0.07;
+
+    rgba.alpha *= adw_easing_ease (ADW_EASE_OUT_EXPO, self->progress);
+
+    gtk_snapshot_append_inset_shadow (snapshot, &transition_rect,
+                                      &rgba, 0, 0, 1, 0);
+  }
+}
+
+static gboolean
+adw_tab_overview_focus (GtkWidget        *widget,
+                        GtkDirectionType  direction)
+{
+  AdwTabOverview *self = ADW_TAB_OVERVIEW (widget);
+  GtkWidget *focus;
+
+  if (!self->is_open)
+    return GTK_WIDGET_CLASS (adw_tab_overview_parent_class)->focus (widget, direction);
+
+  focus = gtk_root_get_focus (gtk_widget_get_root (widget));
+  if (!focus)
+    return GTK_WIDGET_CLASS (adw_tab_overview_parent_class)->focus (widget, direction);
+
+  if (direction != GTK_DIR_UP && direction != GTK_DIR_DOWN)
+    return GTK_WIDGET_CLASS (adw_tab_overview_parent_class)->focus (widget, direction);
+
+  if (direction == GTK_DIR_DOWN) {
+    if ((focus == self->search_button ||
+         gtk_widget_is_ancestor (focus, self->search_button)) &&
+        !gtk_search_bar_get_search_mode (GTK_SEARCH_BAR (self->search_bar))) {
+      return adw_tab_grid_focus_first_row (self->pinned_grid, 0) ||
+             adw_tab_grid_focus_first_row (self->grid, 0);
+    }
+
+    if ((focus == self->extra_menu_button ||
+         gtk_widget_is_ancestor (focus, self->extra_menu_button)) &&
+        !gtk_search_bar_get_search_mode (GTK_SEARCH_BAR (self->search_bar))) {
+      return adw_tab_grid_focus_first_row (self->pinned_grid, -1) ||
+             adw_tab_grid_focus_first_row (self->grid, -1);
+    }
+
+    if ((focus == self->search_bar ||
+         gtk_widget_is_ancestor (focus, self->search_bar))) {
+      return adw_tab_grid_focus_first_row (self->pinned_grid, 0) ||
+             adw_tab_grid_focus_first_row (self->grid, 0);
+    }
+
+    if ((focus == self->new_tab_button ||
+         gtk_widget_is_ancestor (focus, self->new_tab_button)))
+      return GDK_EVENT_PROPAGATE;
+
+    if (gtk_widget_is_ancestor (focus, GTK_WIDGET (self->grid))) {
+      return gtk_widget_child_focus (GTK_WIDGET (self->grid), direction) ||
+             gtk_widget_grab_focus (self->new_tab_button);
+    }
+
+    if (gtk_widget_is_ancestor (focus, GTK_WIDGET (self->pinned_grid)) &&
+        adw_tab_grid_get_empty (self->grid)) {
+      return gtk_widget_child_focus (GTK_WIDGET (self->pinned_grid), direction) ||
+             gtk_widget_grab_focus (self->new_tab_button);
+    }
+  }
+
+  if (direction == GTK_DIR_UP &&
+      (focus == self->new_tab_button ||
+       gtk_widget_is_ancestor (focus, self->new_tab_button))) {
+    return adw_tab_grid_focus_last_row (self->grid, -1) ||
+           adw_tab_grid_focus_last_row (self->pinned_grid, -1);
+  }
+
+  return adw_widget_focus_child (widget, direction);
+}
+
+static void
+adw_tab_overview_dispose (GObject *object)
+{
+  AdwTabOverview *self = ADW_TAB_OVERVIEW (object);
+
+  if (self->last_focus) {
+    g_object_remove_weak_pointer (G_OBJECT (self->last_focus),
+                                  (gpointer *) &self->last_focus);
+    self->last_focus = NULL;
+  }
+
+  adw_tab_overview_set_view (self, NULL);
+
+  gtk_widget_unparent (GTK_WIDGET (self->overview));
+  gtk_widget_unparent (GTK_WIDGET (self->child_bin));
+
+  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;
+  case PROP_OPEN:
+    g_value_set_boolean (value, adw_tab_overview_get_open (self));
+    break;
+  case PROP_INVERTED:
+    g_value_set_boolean (value, adw_tab_overview_get_inverted (self));
+    break;
+  case PROP_ENABLE_SEARCH:
+    g_value_set_boolean (value, adw_tab_overview_get_enable_search (self));
+    break;
+  case PROP_SEARCH_ACTIVE:
+    g_value_set_boolean (value, adw_tab_overview_get_search_active (self));
+    break;
+  case PROP_ENABLE_NEW_TAB:
+    g_value_set_boolean (value, adw_tab_overview_get_enable_new_tab (self));
+    break;
+  case PROP_NEW_TAB_ACTION_NAME:
+    g_value_set_string (value, adw_tab_overview_get_new_tab_action_name (self));
+    break;
+  case PROP_NEW_TAB_ACTION_TARGET:
+    g_value_set_variant (value, adw_tab_overview_get_new_tab_action_target_value (self));
+    break;
+  case PROP_EXTRA_MENU:
+    g_value_set_object (value, adw_tab_overview_get_extra_menu (self));
+    break;
+  case PROP_SHOW_START_TITLE_BUTTONS:
+    g_value_set_boolean (value, adw_tab_overview_get_show_start_title_buttons (self));
+    break;
+  case PROP_SHOW_END_TITLE_BUTTONS:
+    g_value_set_boolean (value, adw_tab_overview_get_show_end_title_buttons (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;
+  case PROP_OPEN:
+    adw_tab_overview_set_open (self, g_value_get_boolean (value));
+    break;
+  case PROP_INVERTED:
+    adw_tab_overview_set_inverted (self, g_value_get_boolean (value));
+    break;
+  case PROP_ENABLE_SEARCH:
+    adw_tab_overview_set_enable_search (self, g_value_get_boolean (value));
+    break;
+  case PROP_ENABLE_NEW_TAB:
+    adw_tab_overview_set_enable_new_tab (self, g_value_get_boolean (value));
+    break;
+  case PROP_NEW_TAB_ACTION_NAME:
+    adw_tab_overview_set_new_tab_action_name (self, g_value_get_string (value));
+    break;
+  case PROP_NEW_TAB_ACTION_TARGET:
+    adw_tab_overview_set_new_tab_action_target_value (self, g_value_get_variant (value));
+    break;
+  case PROP_EXTRA_MENU:
+    adw_tab_overview_set_extra_menu (self, g_value_get_object (value));
+    break;
+  case PROP_SHOW_START_TITLE_BUTTONS:
+    adw_tab_overview_set_show_start_title_buttons (self, g_value_get_boolean (value));
+    break;
+  case PROP_SHOW_END_TITLE_BUTTONS:
+    adw_tab_overview_set_show_end_title_buttons (self, g_value_get_boolean (value));
+    break;
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+overview_open_cb (AdwTabOverview *self)
+{
+  adw_tab_overview_set_open (self, TRUE);
+}
+
+static void
+overview_close_cb (AdwTabOverview *self)
+{
+  adw_tab_overview_set_open (self, FALSE);
+}
+
+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->compute_expand = adw_widget_compute_expand;
+  widget_class->focus = adw_tab_overview_focus;
+
+  /**
+   * 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_STATIC_STRINGS | 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_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabOverview:open: (attributes org.gtk.Property.get=adw_tab_overview_get_open 
org.gtk.Property.set=adw_tab_overview_set_open)
+   *
+   * Whether the overview is open.
+   *
+   * Since: 1.3
+   */
+  props[PROP_OPEN] =
+    g_param_spec_boolean ("open", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabOverview:inverted: (attributes org.gtk.Property.get=adw_tab_overview_get_inverted 
org.gtk.Property.set=adw_tab_overview_set_inverted)
+   *
+   * Whether thumbnails use inverted layout.
+   *
+   * If set to `TRUE`, thumbnails will have the close button at the beginning
+   * and the indicator at the end rather than the other way around.
+   *
+   * Since: 1.3
+   */
+  props[PROP_INVERTED] =
+    g_param_spec_boolean ("inverted", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabOverview:enable-search: (attributes org.gtk.Property.get=adw_tab_overview_get_enable_search 
org.gtk.Property.set=adw_tab_overview_set_enable_search)
+   *
+   * TODO
+   *
+   * Since: 1.3
+   */
+  props[PROP_ENABLE_SEARCH] =
+    g_param_spec_boolean ("enable-search", NULL, NULL,
+                          TRUE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabOverview:search-active: (attributes org.gtk.Property.get=adw_tab_overview_get_search_active 
org.gtk.Property.set=adw_tab_overview_set_search_active)
+   *
+   * TODO
+   *
+   * Since: 1.3
+   */
+  props[PROP_SEARCH_ACTIVE] =
+    g_param_spec_boolean ("search-active", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+  /**
+   * AdwTabOverview:enable-new-tab: (attributes org.gtk.Property.get=adw_tab_overview_get_enable_new_tab 
org.gtk.Property.set=adw_tab_overview_set_enable_new_tab)
+   *
+   * TODO
+   *
+   * Since: 1.3
+   */
+  props[PROP_ENABLE_NEW_TAB] =
+    g_param_spec_boolean ("enable-new-tab", NULL, NULL,
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabOverview:new-tab-action-name: (attributes 
org.gtk.Property.get=adw_tab_overview_get_new_tab_action_name 
org.gtk.Property.set=adw_tab_overview_set_new_tab_action_name)
+   *
+   * TODO
+   *
+   * See [property@AdwTabOverview:new-tab-action-target].
+   *
+   * Since: 1.3
+   */
+  props[PROP_NEW_TAB_ACTION_NAME] =
+    g_param_spec_string ("new-tab-action-name", NULL, NULL,
+                         NULL,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabOverview:new-tab-action-target: (attributes 
org.gtk.Property.get=adw_tab_overview_get_new_tab_action_target_value 
org.gtk.Property.set=adw_tab_overview_set_new_tab_action_target_value)
+   *
+   * TODO
+   *
+   * See [property@AdwTabOverview:new-tab-action-name].
+   *
+   * Since: 1.3
+   */
+  props[PROP_NEW_TAB_ACTION_TARGET] =
+    g_param_spec_variant ("new-tab-action-target", NULL, NULL,
+                          G_VARIANT_TYPE_ANY,
+                          NULL,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabOverview:extra-menu: (attributes org.gtk.Property.get=adw_tab_overview_get_extra_menu 
org.gtk.Property.set=adw_tab_overview_set_extra_menu)
+   *
+   * TODO
+   *
+   * Since: 1.3
+   */
+  props[PROP_EXTRA_MENU] =
+    g_param_spec_object ("extra-menu", NULL, NULL,
+                         G_TYPE_MENU_MODEL,
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabOverview:show-start-title-buttons: (attributes 
org.gtk.Property.get=adw_tab_overview_get_show_start_title_buttons 
org.gtk.Property.set=adw_tab_overview_set_show_start_title_buttons)
+   *
+   * TODO
+   *
+   * Since: 1.3
+   */
+  props[PROP_SHOW_START_TITLE_BUTTONS] =
+    g_param_spec_boolean ("show-start-title-buttons", NULL, NULL,
+                          TRUE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabOverview:show-end-title-buttons: (attributes 
org.gtk.Property.get=adw_tab_overview_get_show_end_title_buttons 
org.gtk.Property.set=adw_tab_overview_set_show_end_title_buttons)
+   *
+   * TODO
+   *
+   * Since: 1.3
+   */
+  props[PROP_SHOW_END_TITLE_BUTTONS] =
+    g_param_spec_boolean ("show-end-title-buttons", NULL, NULL,
+                          TRUE,
+                          G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  /**
+   * AdwTabOverview::new-tab-clicked:
+   * @self: a tab overview
+   *
+   * This signal is emitted when TODO.
+   *
+   * See [property@TabOverview:enable-new-tab].
+   *
+   * Since: 1.3
+   */
+  signals[SIGNAL_NEW_TAB_CLICKED] =
+    g_signal_new ("new-tab-clicked",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_LAST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  0);
+
+  /**
+   * 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,
+                                   (GtkWidgetActionActivateFunc) overview_open_cb);
+  gtk_widget_class_install_action (widget_class, "overview.close", NULL,
+                                   (GtkWidgetActionActivateFunc) overview_close_cb);
+  gtk_widget_class_add_binding_action (widget_class, GDK_KEY_Escape, 0,
+                                       "overview.close", NULL);
+
+  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, stack);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, scrollable);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, child_bin);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, header_bar);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, title);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, new_tab_button);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, search_button);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, search_bar);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, search_entry);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, extra_menu_button);
+  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_bind_template_callback (widget_class, empty_changed_cb);
+  gtk_widget_class_bind_template_callback (widget_class, search_changed_cb);
+  gtk_widget_class_bind_template_callback (widget_class, stop_search_cb);
+  gtk_widget_class_bind_template_callback (widget_class, notify_action_name_cb);
+  gtk_widget_class_bind_template_callback (widget_class, notify_action_target_cb);
+  gtk_widget_class_bind_template_callback (widget_class, new_tab_clicked_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);
+  g_type_ensure (ADW_TYPE_TAB_OVERVIEW_SCROLLABLE);
+}
+
+static void
+adw_tab_overview_init (AdwTabOverview *self)
+{
+  AdwAnimationTarget *target;
+
+  self->enable_search = TRUE;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  gtk_widget_set_child_visible (self->overview, FALSE);
+
+  gtk_search_bar_connect_entry (GTK_SEARCH_BAR (self->search_bar),
+                                GTK_EDITABLE (self->search_entry));
+
+  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, notify_n_pages_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);
+
+    notify_n_pages_cb (self);
+  }
+
+  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, "notify::n-pages",
+                             G_CALLBACK (notify_n_pages_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);
+
+    notify_n_pages_cb (self);
+  }
+
+  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 adw_bin_get_child (ADW_BIN (self->child_bin));
+}
+
+/**
+ * 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 (child == adw_tab_overview_get_child (self))
+    return;
+
+  adw_bin_set_child (ADW_BIN (self->child_bin), child);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD]);
+}
+
+/**
+ * adw_tab_overview_get_open: (attributes org.gtk.Method.get_property=open)
+ * @self: a tab overview
+ *
+ * Gets whether the overview is open.
+ *
+ * Returns: whether the overview is open
+ *
+ * Since: 1.3
+ */
+gboolean
+adw_tab_overview_get_open (AdwTabOverview *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), FALSE);
+
+  return self->is_open;
+}
+
+/**
+ * adw_tab_overview_set_open: (attributes org.gtk.Method.set_property=open)
+ * @self: a tab overview
+ * @open: whether the overview is open
+ *
+ * Sets whether the overview is open.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_open (AdwTabOverview *self,
+                           gboolean        open)
+{
+  AdwTabPage *selected_page;
+  AdwTabGrid *grid;
+
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+  open = !!open;
+
+  if (self->is_open == open)
+    return;
+
+  selected_page = adw_tab_view_get_selected_page (self->view);
+
+  self->transition_pinned = adw_tab_page_get_pinned (selected_page);
+
+  if (self->transition_pinned)
+    grid = self->pinned_grid;
+  else
+    grid = self->grid;
+
+  if (self->transition_thumbnail &&
+      self->transition_thumbnail != adw_tab_grid_get_transition_thumbnail (grid))
+    adw_animation_skip (self->open_animation);
+
+  self->is_open = open;
+
+  gtk_widget_action_set_enabled (GTK_WIDGET (self), "overview.open", !open);
+  gtk_widget_action_set_enabled (GTK_WIDGET (self), "overview.closed", open);
+
+  if (open) {
+    GtkWidget *focus = NULL;
+
+    if (gtk_widget_get_root (GTK_WIDGET (self)))
+      focus = gtk_root_get_focus (gtk_widget_get_root (GTK_WIDGET (self)));
+
+    if (focus && gtk_widget_is_ancestor (focus, self->child_bin)) {
+      if (self->last_focus)
+        g_object_remove_weak_pointer (G_OBJECT (self->last_focus),
+                                      (gpointer *)& self->last_focus);
+
+      self->last_focus = focus;
+
+      g_object_add_weak_pointer (G_OBJECT (self->last_focus),
+                                 (gpointer *) &self->last_focus);
+    }
+
+    gtk_widget_set_child_visible (self->overview, TRUE);
+    gtk_widget_set_can_target (self->overview, TRUE);
+    gtk_widget_set_can_focus (self->overview, TRUE);
+    gtk_widget_set_can_target (self->child_bin, FALSE);
+    gtk_widget_set_can_focus (self->child_bin, FALSE);
+
+    adw_tab_grid_try_focus_selected_tab (grid, FALSE);
+  }
+
+  if (self->transition_picture)
+    gtk_widget_set_opacity (self->transition_picture, 1);
+
+  self->transition_thumbnail = adw_tab_grid_get_transition_thumbnail (grid);
+  self->transition_picture = g_object_ref (adw_tab_thumbnail_get_thumbnail (self->transition_thumbnail));
+  gtk_widget_set_opacity (self->transition_picture, 0);
+
+  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),
+                                    open ? 1 : 0);
+
+  self->animating = TRUE;
+  adw_animation_play (self->open_animation);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_OPEN]);
+}
+
+/**
+ * adw_tab_overview_get_inverted: (attributes org.gtk.Method.get_property=inverted)
+ * @self: a tab overview
+ *
+ * Gets whether thumbnails use inverted layout.
+ *
+ * Returns: whether thumbnails use inverted layout
+ *
+ * Since: 1.3
+ */
+gboolean
+adw_tab_overview_get_inverted (AdwTabOverview *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), FALSE);
+
+  return adw_tab_grid_get_inverted (self->grid);
+}
+
+/**
+ * adw_tab_overview_set_inverted: (attributes org.gtk.Method.set_property=inverted)
+ * @self: a tab overview
+ * @inverted: whether thumbnails use inverted layout
+ *
+ * Sets whether tabs thumbnails use inverted layout.
+ *
+ * If set to `TRUE`, thumbnails will have the close button at the beginning and
+ * the indicator at the end rather than the other way around.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_inverted (AdwTabOverview *self,
+                               gboolean        inverted)
+{
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+  inverted = !!inverted;
+
+  if (adw_tab_overview_get_inverted (self) == inverted)
+    return;
+
+  adw_tab_grid_set_inverted (self->grid, inverted);
+  adw_tab_grid_set_inverted (self->pinned_grid, inverted);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INVERTED]);
+}
+
+/**
+ * adw_tab_overview_get_enable_search: (attributes org.gtk.Method.get_property=enable-search)
+ * @self: a tab overview
+ *
+ * Gets TODO
+ *
+ * Returns: TODO
+ *
+ * Since: 1.3
+ */
+gboolean
+adw_tab_overview_get_enable_search (AdwTabOverview *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), FALSE);
+
+  return self->enable_search;
+}
+
+/**
+ * adw_tab_overview_set_enable_search: (attributes org.gtk.Method.set_property=enable-search)
+ * @self: a tab overview
+ * @enable_search: TODO
+ *
+ * Sets TODO
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_enable_search (AdwTabOverview *self,
+                                    gboolean        enable_search)
+{
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+  enable_search = !!enable_search;
+
+  if (self->enable_search == enable_search)
+    return;
+
+  self->enable_search = enable_search;
+
+  if (!enable_search)
+    gtk_search_bar_set_search_mode (GTK_SEARCH_BAR (self->search_bar), FALSE);
+
+  gtk_search_bar_set_key_capture_widget (GTK_SEARCH_BAR (self->search_bar),
+                                         enable_search ? self->overview : NULL);
+  gtk_widget_set_visible (self->search_button, enable_search);
+  update_header_bar (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLE_SEARCH]);
+}
+
+/**
+ * adw_tab_overview_get_search_active: (attributes org.gtk.Method.get_property=search-active)
+ * @self: a tab overview
+ *
+ * Gets TODO
+ *
+ * Returns: TODO
+ *
+ * Since: 1.3
+ */
+gboolean
+adw_tab_overview_get_search_active (AdwTabOverview *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), FALSE);
+
+  return self->search_active;
+}
+
+/**
+ * adw_tab_overview_get_enable_new_tab: (attributes org.gtk.Method.get_property=enable-new-tab)
+ * @self: a tab overview
+ *
+ * Gets TODO
+ *
+ * Returns: TODO
+ *
+ * Since: 1.3
+ */
+gboolean
+adw_tab_overview_get_enable_new_tab (AdwTabOverview *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), FALSE);
+
+  return self->enable_new_tab;
+}
+
+/**
+ * adw_tab_overview_set_enable_new_tab: (attributes org.gtk.Method.set_property=enable-new-tab)
+ * @self: a tab overview
+ * @enable_new_tab: TODO
+ *
+ * Sets TODO
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_enable_new_tab (AdwTabOverview *self,
+                                     gboolean        enable_new_tab)
+{
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+  enable_new_tab = !!enable_new_tab;
+
+  if (self->enable_new_tab == enable_new_tab)
+    return;
+
+  self->enable_new_tab = enable_new_tab;
+
+  update_new_tab_button (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLE_NEW_TAB]);
+}
+
+/**
+ * adw_tab_overview_get_new_tab_action_name: (attributes org.gtk.Method.get_property=new-tab-action-name)
+ * @self: a tab overview
+ *
+ * Gets the TODO
+ *
+ * Returns: (nullable): the action name
+ *
+ * Since: 1.3
+ */
+const char *
+adw_tab_overview_get_new_tab_action_name (AdwTabOverview *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), NULL);
+
+  return gtk_actionable_get_action_name (GTK_ACTIONABLE (self->new_tab_button));
+}
+
+/**
+ * adw_tab_overview_set_new_tab_action_name: (attributes org.gtk.Method.set_property=new-tab-action-name)
+ * @self: a tab overview
+ * @action_name: (nullable): the action name
+ *
+ * TODO
+ *
+ * See [property@TabOverview:new-tab-action-target].
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_new_tab_action_name (AdwTabOverview *self,
+                                          const char     *action_name)
+{
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+  gtk_actionable_set_action_name (GTK_ACTIONABLE (self->new_tab_button),
+                                  action_name);
+}
+
+/**
+ * adw_tab_overview_get_new_tab_action_target_value: (attributes 
org.gtk.Method.get_property=new-tab-action-target)
+ * @self: a tab overview
+ *
+ * Gets the TODO
+ *
+ * Returns: (transfer none) (nullable): the action target
+ *
+ * Since: 1.3
+ */
+GVariant *
+adw_tab_overview_get_new_tab_action_target_value (AdwTabOverview *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), NULL);
+
+  return gtk_actionable_get_action_target_value (GTK_ACTIONABLE (self->new_tab_button));
+}
+
+/**
+ * adw_tab_overview_set_new_tab_action_target_value: (attributes 
org.gtk.Method.set_property=new-tab-action-target)
+ * @self: a tab overview
+ * @action_target: (nullable): the action target
+ *
+ * Sets the parameter for action invocations. TODO
+ *
+ * If the @action_target variant has a floating reference this function
+ * will sink it.
+ *
+ * See [property@TabOverview:new-tab-action-name].
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_new_tab_action_target_value (AdwTabOverview *self,
+                                                  GVariant       *action_target)
+{
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+  gtk_actionable_set_action_target_value (GTK_ACTIONABLE (self->new_tab_button),
+                                          action_target);
+}
+
+/**
+ * adw_tab_overview_set_new_tab_action_target: (skip)
+ * @self: a tab overview
+ * @format_string: (nullable): a variant format string
+ * @...: arguments appropriate for @target_format
+ *
+ * Sets the parameter for action invocations. TODO
+ *
+ * This is a convenience function that calls [ctor GLib Variant new] for
+ * @format_string and uses the result to call
+ * [method@TabOverview.set_new_tab_action_target_value].
+ *
+ * If you are setting a string-valued target and want to set
+ * the action name at the same time, you can use
+ * [method@TabOverview.set_new_tab_detailed_action_name].
+
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_new_tab_action_target (AdwTabOverview *self,
+                                            const char     *format_string,
+                                            ...)
+{
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+  va_list args;
+
+  va_start (args, format_string);
+  adw_tab_overview_set_new_tab_action_target_value (self,
+                                                    g_variant_new_va (format_string,
+                                                                      NULL, &args));
+  va_end (args);
+}
+
+/**
+ * adw_tab_overview_set_new_tab_detailed_action_name:
+ * @self: a tab overview
+ * @detailed_action_name: (nullable): the detailed action name
+ *
+ * Sets the action name and its parameter. TODO
+ *
+ * @detailed_action_name is a string in the format accepted by
+ * [func@Gio.Action.parse_detailed_name].
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_new_tab_detailed_action_name (AdwTabOverview *self,
+                                                   const char     *detailed_action_name)
+{
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+  gtk_actionable_set_detailed_action_name (GTK_ACTIONABLE (self->new_tab_button),
+                                           detailed_action_name);
+}
+
+/**
+ * adw_tab_overview_get_extra_menu: (attributes org.gtk.Method.get_property=extra-menu)
+ * @self: a tab overview
+ *
+ * Gets TODO.
+ *
+ * Returns: (transfer none) (nullable): the TODO for @self
+ *
+ * Since: 1.3
+ */
+GMenuModel *
+adw_tab_overview_get_extra_menu (AdwTabOverview *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), NULL);
+
+  return gtk_menu_button_get_menu_model (GTK_MENU_BUTTON (self->extra_menu_button));
+}
+
+/**
+ * adw_tab_overview_set_extra_menu: (attributes org.gtk.Method.set_property=extra-menu)
+ * @self: a tab overview
+ * @menu_model: (nullable): a menu model
+ *
+ * Sets the TODO
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_extra_menu (AdwTabOverview *self,
+                                 GMenuModel     *extra_menu)
+{
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+  g_return_if_fail (extra_menu == NULL || G_IS_MENU_MODEL (extra_menu));
+
+  if (extra_menu == adw_tab_overview_get_extra_menu (self))
+    return;
+
+  gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (self->extra_menu_button),
+                                  extra_menu);
+  gtk_widget_set_visible (self->extra_menu_button, !!extra_menu);
+  update_header_bar (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_EXTRA_MENU]);
+}
+
+/**
+ * adw_tab_overview_get_show_start_title_buttons: (attributes 
org.gtk.Method.get_property=show-start-title-buttons)
+ * @self: a tab overview
+ *
+ * Gets TODO
+ *
+ * Returns: TODO
+ *
+ * Since: 1.3
+ */
+gboolean
+adw_tab_overview_get_show_start_title_buttons (AdwTabOverview *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), FALSE);
+
+  return adw_header_bar_get_show_start_title_buttons (ADW_HEADER_BAR (self->header_bar));
+}
+
+/**
+ * adw_tab_overview_set_show_start_title_buttons: (attributes 
org.gtk.Method.set_property=show-start-title-buttons)
+ * @self: a tab overview
+ * @show_start_title_buttons: TODO
+ *
+ * Sets TODO
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_show_start_title_buttons (AdwTabOverview *self,
+                                               gboolean        show_start_title_buttons)
+{
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+  show_start_title_buttons = !!show_start_title_buttons;
+
+  if (adw_tab_overview_get_show_start_title_buttons (self) == show_start_title_buttons)
+    return;
+
+  adw_header_bar_set_show_start_title_buttons (ADW_HEADER_BAR (self->header_bar),
+                                               show_start_title_buttons);
+
+  update_header_bar (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_START_TITLE_BUTTONS]);
+}
+
+/**
+ * adw_tab_overview_get_show_end_title_buttons: (attributes 
org.gtk.Method.get_property=show-end-title-buttons)
+ * @self: a tab overview
+ *
+ * Gets TODO
+ *
+ * Returns: TODO
+ *
+ * Since: 1.3
+ */
+gboolean
+adw_tab_overview_get_show_end_title_buttons (AdwTabOverview *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), FALSE);
+
+  return adw_header_bar_get_show_end_title_buttons (ADW_HEADER_BAR (self->header_bar));
+}
+
+/**
+ * adw_tab_overview_set_show_end_title_buttons: (attributes 
org.gtk.Method.set_property=show-end-title-buttons)
+ * @self: a tab overview
+ * @show_end_title_buttons: TODO
+ *
+ * Sets TODO
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_show_end_title_buttons (AdwTabOverview *self,
+                                             gboolean        show_end_title_buttons)
+{
+  g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+  show_end_title_buttons = !!show_end_title_buttons;
+
+  if (adw_tab_overview_get_show_end_title_buttons (self) == show_end_title_buttons)
+    return;
+
+  adw_header_bar_set_show_end_title_buttons (ADW_HEADER_BAR (self->header_bar),
+                                             show_end_title_buttons);
+
+  update_header_bar (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SHOW_END_TITLE_BUTTONS]);
+}
+
+/**
+ * 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);
+}
+
+AdwTabGrid *
+adw_tab_overview_get_tab_grid (AdwTabOverview *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), NULL);
+
+  return self->grid;
+}
+
+AdwTabGrid *
+adw_tab_overview_get_pinned_tab_grid (AdwTabOverview *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), NULL);
+
+  return self->pinned_grid;
+}
diff --git a/src/adw-tab-overview.h b/src/adw-tab-overview.h
new file mode 100644
index 00000000..79f835b8
--- /dev/null
+++ b/src/adw-tab-overview.h
@@ -0,0 +1,112 @@
+/*
+ * 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
+gboolean adw_tab_overview_get_open  (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void     adw_tab_overview_set_open (AdwTabOverview *self,
+                                    gboolean        open);
+
+ADW_AVAILABLE_IN_1_2
+gboolean adw_tab_overview_get_inverted (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void     adw_tab_overview_set_inverted (AdwTabOverview *self,
+                                        gboolean        inverted);
+
+ADW_AVAILABLE_IN_1_2
+gboolean adw_tab_overview_get_enable_search (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void     adw_tab_overview_set_enable_search (AdwTabOverview *self,
+                                             gboolean        enable_search);
+
+ADW_AVAILABLE_IN_1_2
+gboolean adw_tab_overview_get_search_active (AdwTabOverview *self);
+
+ADW_AVAILABLE_IN_1_2
+gboolean adw_tab_overview_get_enable_new_tab (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void     adw_tab_overview_set_enable_new_tab (AdwTabOverview *self,
+                                              gboolean        enable_new_tab);
+
+ADW_AVAILABLE_IN_1_2
+const char *adw_tab_overview_get_new_tab_action_name (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void        adw_tab_overview_set_new_tab_action_name (AdwTabOverview *self,
+                                                      const char     *action_name);
+
+ADW_AVAILABLE_IN_1_2
+GVariant *adw_tab_overview_get_new_tab_action_target_value (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void      adw_tab_overview_set_new_tab_action_target_value (AdwTabOverview *self,
+                                                            GVariant       *action_target);
+ADW_AVAILABLE_IN_1_2
+void      adw_tab_overview_set_new_tab_action_target       (AdwTabOverview *self,
+                                                            const char     *format_string,
+                                                            ...);
+ADW_AVAILABLE_IN_1_2
+void adw_tab_overview_set_new_tab_detailed_action_name     (AdwTabOverview *self,
+                                                            const char     *detailed_action_name);
+
+ADW_AVAILABLE_IN_1_2
+GMenuModel *adw_tab_overview_get_extra_menu (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void        adw_tab_overview_set_extra_menu (AdwTabOverview *self,
+                                             GMenuModel     *extra_menu);
+
+ADW_AVAILABLE_IN_1_2
+gboolean adw_tab_overview_get_show_start_title_buttons (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void     adw_tab_overview_set_show_start_title_buttons (AdwTabOverview *self,
+                                                        gboolean        show_start_title_buttons);
+
+ADW_AVAILABLE_IN_1_2
+gboolean adw_tab_overview_get_show_end_title_buttons (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void     adw_tab_overview_set_show_end_title_buttons (AdwTabOverview *self,
+                                                      gboolean        show_end_title_buttons);
+
+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..155a4700
--- /dev/null
+++ b/src/adw-tab-overview.ui
@@ -0,0 +1,142 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <requires lib="gtk" version="4.0"/>
+  <template class="AdwTabOverview" parent="GtkWidget">
+    <child>
+      <object class="AdwBin" id="child_bin">
+        <style>
+          <class name="background"/>
+        </style>
+      </object>
+    </child>
+    <child>
+      <object class="GtkBox" id="overview">
+        <property name="orientation">vertical</property>
+        <property name="can-focus">False</property>
+        <property name="can-target">False</property>
+        <style>
+          <class name="overview"/>
+          <class name="scrolled-to-top"/>
+        </style>
+        <child>
+          <object class="AdwHeaderBar" id="header_bar">
+            <child type="start">
+              <object class="GtkToggleButton" id="search_button">
+                <property name="icon-name">edit-find-symbolic</property>
+                <property name="tooltip-text" translatable="yes">Search Tabs</property>
+              </object>
+            </child>
+            <property name="title-widget">
+              <object class="AdwWindowTitle" id="title">
+                <style>
+                  <class name="numeric"/>
+                </style>
+              </object>
+            </property>
+            <child type="end">
+              <object class="GtkMenuButton" id="extra_menu_button">
+                <property name="visible">False</property>
+                <property name="icon-name">view-more-symbolic</property>
+                <property name="primary">True</property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkSearchBar" id="search_bar">
+            <property name="search-mode-enabled" bind-source="search_button" bind-property="active" 
bind-flags="bidirectional"/>
+            <property name="key-capture-widget">overview</property>
+            <property name="child">
+              <object class="AdwClamp">
+                <property name="hexpand">True</property>
+                <property name="maximum-size">400</property>
+                <property name="child">
+                  <object class="GtkSearchEntry" id="search_entry">
+                    <property name="hexpand">True</property>
+                    <property name="placeholder-text" translatable="yes">Search tabs</property>
+                    <signal name="search-changed" handler="search_changed_cb" swapped="yes"/>
+                    <signal name="stop-search" handler="stop_search_cb" swapped="yes"/>
+                  </object>
+                </property>
+              </object>
+            </property>
+          </object>
+        </child>
+        <child>
+          <object class="GtkOverlay">
+            <property name="vexpand">True</property>
+            <property name="child">
+              <object class="GtkStack" id="stack">
+                <child>
+                  <object class="GtkStackPage">
+                    <property name="name">tabs</property>
+                    <property name="child">
+                      <object class="GtkScrolledWindow" id="scrolled_window">
+                        <property name="hscrollbar-policy">never</property>
+                        <property name="child">
+                          <object class="AdwTabOverviewScrollable" id="scrollable">
+                            <property name="overview">overview</property>
+                            <property name="new-button">new_tab_button</property>
+                            <property name="pinned-grid">
+                              <object class="AdwTabGrid" id="pinned_grid">
+                                <property name="pinned">True</property>
+                                <property name="tab-overview">AdwTabOverview</property>
+                                <signal name="extra-drag-drop" handler="extra_drag_drop_cb" swapped="yes"/>
+                                <signal name="notify::empty" handler="empty_changed_cb" swapped="yes"/>
+                              </object>
+                            </property>
+                            <property name="grid">
+                              <object class="AdwTabGrid" id="grid">
+                                <property name="tab-overview">AdwTabOverview</property>
+                                <signal name="extra-drag-drop" handler="extra_drag_drop_cb" swapped="yes"/>
+                                <signal name="notify::empty" handler="empty_changed_cb" swapped="yes"/>
+                              </object>
+                            </property>
+                          </object>
+                        </property>
+                      </object>
+                    </property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkStackPage">
+                    <property name="name">empty</property>
+                    <property name="child">
+                      <object class="AdwStatusPage" id="search_empty">
+                        <property name="icon-name">edit-find-symbolic</property>
+                        <property name="title" translatable="yes">No Tabs Found</property>
+                        <property name="description" translatable="yes">Try a different search.</property>
+                      </object>
+                    </property>
+                  </object>
+                </child>
+              </object>
+            </property>
+            <child type="overlay">
+              <object class="GtkButton" id="new_tab_button">
+                <property name="visible">False</property>
+                <property name="halign">center</property>
+                <property name="valign">end</property>
+                <signal name="notify::action-name" handler="notify_action_name_cb" swapped="yes"/>
+                <signal name="notify::action-target" handler="notify_action_target_cb" swapped="yes"/>
+                <signal name="clicked" handler="new_tab_clicked_cb" swapped="yes"/>
+                <property name="child">
+                  <object class="AdwButtonContent">
+                    <property name="icon-name">tab-new-symbolic</property>
+                    <property name="label" translatable="yes">New _Tab</property>
+                    <property name="use-underline">True</property>
+                  </object>
+                </property>
+                <style>
+                  <class name="pill"/>
+                  <class name="suggested-action"/>
+                  <class name="new-tab-button"/>
+                </style>
+              </object>
+            </child>
+          </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..a3e39d42
--- /dev/null
+++ b/src/adw-tab-thumbnail-private.h
@@ -0,0 +1,45 @@
+/*
+ * 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_inverted (AdwTabThumbnail *self);
+void     adw_tab_thumbnail_set_inverted (AdwTabThumbnail *self,
+                                         gboolean         inverted);
+
+void adw_tab_thumbnail_setup_extra_drop_target (AdwTabThumbnail *self,
+                                                GdkDragAction    actions,
+                                                GType           *types,
+                                                gsize            n_types);
+
+GtkWidget *adw_tab_thumbnail_get_thumbnail (AdwTabThumbnail *self);
+
+void adw_tab_thumbnail_fade_out (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..6537bf61
--- /dev/null
+++ b/src/adw-tab-thumbnail.c
@@ -0,0 +1,668 @@
+/*
+ * 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-gizmo-private.h"
+#include "adw-macros-private.h"
+#include "adw-tab-view-private.h"
+#include "adw-timed-animation.h"
+
+#define FADE_TRANSITION_DURATION 250
+
+struct _AdwTabThumbnail
+{
+  GtkWidget parent_instance;
+
+  GtkWidget *contents;
+  GtkWidget *icon_title_box;
+  GtkWidget *overlay;
+  GtkPicture *picture;
+  GtkWidget *icon_stack;
+  GtkImage *icon;
+  GtkSpinner *spinner;
+  GtkImage *indicator_icon;
+  GtkWidget *indicator_btn;
+  GtkWidget *close_btn;
+  GtkWidget *unpin_btn;
+  GtkWidget *pinned_box;
+  GtkDropTarget *drop_target;
+
+  AdwTabView *view;
+  AdwTabPage *page;
+  gboolean pinned;
+
+  gboolean inverted;
+
+  AdwAnimation *fade_animation;
+};
+
+G_DEFINE_FINAL_TYPE (AdwTabThumbnail, adw_tab_thumbnail, GTK_TYPE_WIDGET)
+
+enum {
+  PROP_0,
+  PROP_VIEW,
+  PROP_PINNED,
+  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);
+  gboolean was_visible = gtk_widget_get_visible (self->indicator_btn);
+
+  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);
+
+  if (gtk_widget_get_visible (self->indicator_btn) != was_visible) {
+    if (self->pinned)
+      gtk_widget_queue_resize (self->pinned_box);
+    else
+      gtk_widget_queue_allocate (GTK_WIDGET (self->overlay));
+  }
+
+  set_style_class (GTK_WIDGET (self), "indicator", indicator != NULL);
+}
+
+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 gboolean
+unpin_idle_cb (AdwTabThumbnail *self)
+{
+  adw_tab_view_set_page_pinned (self->view, self->page, FALSE);
+
+  return G_SOURCE_REMOVE;
+}
+
+static void
+unpin_clicked_cb (AdwTabThumbnail *self)
+{
+  if (!self->page)
+    return;
+
+  /* When animations are disabled, we don't want to immediately unpin the
+   * whole tab mid-click. Instead, defer it until the click has happened.
+   */
+  g_idle_add ((GSourceFunc) unpin_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
+fade_animation_value_cb (double           value,
+                         AdwTabThumbnail *self)
+{
+  if (self->pinned) {
+    gtk_widget_set_opacity (self->unpin_btn, value);
+    gtk_widget_set_opacity (self->icon_title_box, value);
+  } else {
+    gtk_widget_set_opacity (self->close_btn, value);
+  }
+
+  gtk_widget_set_opacity (self->indicator_btn, value);
+}
+
+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
+measure_pinned_tab (AdwGizmo       *gizmo,
+                    GtkOrientation  orientation,
+                    int             for_size,
+                    int            *minimum,
+                    int            *natural,
+                    int            *minimum_baseline,
+                    int            *natural_baseline)
+{
+  AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (gtk_widget_get_ancestor (GTK_WIDGET (gizmo),
+                                             ADW_TYPE_TAB_THUMBNAIL));
+
+  if (orientation == GTK_ORIENTATION_VERTICAL) {
+    gtk_widget_measure (self->icon_title_box, orientation, for_size,
+                        minimum, natural, minimum_baseline, natural_baseline);
+  } else {
+    int box_min, box_nat, button_min, button_nat, indicator_min, indicator_nat;
+
+    gtk_widget_measure (self->icon_title_box, orientation, for_size,
+                        &box_min, &box_nat, NULL, NULL);
+
+    if (gtk_widget_should_layout (self->indicator_btn))
+      gtk_widget_measure (self->unpin_btn, orientation, for_size,
+                          &button_min, &button_nat, NULL, NULL);
+    else
+      button_min = button_nat = 0;
+
+    if (gtk_widget_should_layout (self->indicator_btn))
+      gtk_widget_measure (self->indicator_btn, orientation, for_size,
+                          &indicator_min, &indicator_nat, NULL, NULL);
+    else
+      indicator_min = indicator_nat = 0;
+
+    if (minimum)
+      *minimum = box_min + button_min + indicator_min;
+    if (natural)
+      *natural = box_nat + button_nat + indicator_nat;
+    if (minimum_baseline)
+      *minimum_baseline = -1;
+    if (natural_baseline)
+      *natural_baseline = -1;
+  }
+}
+
+static void
+allocate_pinned_tab (AdwGizmo *gizmo,
+                     int       width,
+                     int       height,
+                     int       baseline)
+{
+  AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (gtk_widget_get_ancestor (GTK_WIDGET (gizmo),
+                                             ADW_TYPE_TAB_THUMBNAIL));
+  int left_margin = 0, right_margin = 0;
+  int box_pos, box_width;
+  gboolean is_rtl;
+  GtkBorder margin;
+
+  if (gtk_widget_should_layout (self->unpin_btn))
+  gtk_widget_measure (self->unpin_btn, GTK_ORIENTATION_HORIZONTAL, -1,
+                        &right_margin, NULL, NULL, NULL);
+  if (gtk_widget_should_layout (self->indicator_btn))
+    gtk_widget_measure (self->indicator_btn, GTK_ORIENTATION_HORIZONTAL, -1,
+                        &left_margin, NULL, NULL, NULL);
+
+  gtk_widget_measure (self->icon_title_box, GTK_ORIENTATION_HORIZONTAL, -1,
+                      NULL, &box_width, NULL, NULL);
+
+  gtk_style_context_get_margin (gtk_widget_get_style_context (GTK_WIDGET (gizmo)), &margin);
+
+  is_rtl = gtk_widget_get_direction (GTK_WIDGET (gizmo)) == GTK_TEXT_DIR_RTL;
+
+  if (is_rtl != self->inverted) {
+    int tmp = left_margin;
+    left_margin = right_margin;
+    right_margin = tmp;
+  }
+
+  left_margin = MAX (left_margin - margin.left, 0);
+  right_margin = MAX (right_margin - margin.right, 0);
+
+  box_width = MIN (width - right_margin - left_margin, box_width);
+  box_pos = (width - box_width) / 2;
+
+  if (box_pos + box_width > width - right_margin)
+    box_pos = width - right_margin - box_width;
+  if (box_pos < left_margin)
+    box_pos = left_margin;
+
+  gtk_widget_allocate (self->icon_title_box, box_width, height, baseline,
+                       gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (box_pos, 0)));
+}
+
+static void
+adw_tab_thumbnail_constructed (GObject *object)
+{
+  AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (object);
+
+  G_OBJECT_CLASS (adw_tab_thumbnail_parent_class)->constructed (object);
+
+  gtk_widget_set_visible (self->unpin_btn, self->pinned);
+  gtk_widget_set_visible (self->close_btn, !self->pinned);
+
+  if (self->pinned) {
+    gtk_widget_add_css_class (GTK_WIDGET (self), "pinned");
+
+    self->pinned_box = adw_gizmo_new ("widget",
+                                      measure_pinned_tab,
+                                      allocate_pinned_tab,
+                                      NULL, NULL, NULL, NULL);
+    gtk_widget_add_css_class (self->pinned_box, "pinned-box");
+    gtk_widget_set_can_target (self->pinned_box, FALSE);
+    gtk_overlay_add_overlay (GTK_OVERLAY (self->overlay), self->pinned_box);
+    gtk_overlay_set_measure_overlay (GTK_OVERLAY (self->overlay),
+                                     self->pinned_box , TRUE);
+
+    g_object_ref (self->icon_title_box);
+
+    gtk_box_remove (GTK_BOX (self->contents), self->icon_title_box);
+    gtk_widget_set_parent (self->icon_title_box, self->pinned_box );
+
+    g_object_unref (self->icon_title_box);
+
+    gtk_widget_set_halign (self->icon_title_box, GTK_ALIGN_FILL);
+
+    gtk_widget_hide (GTK_WIDGET (self->picture));
+  }
+}
+
+static void
+adw_tab_thumbnail_dispose (GObject *object)
+{
+  AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (object);
+
+  adw_tab_thumbnail_set_page (self, NULL);
+
+  g_clear_object (&self->fade_animation);
+
+  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_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_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_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, contents);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabThumbnail, overlay);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabThumbnail, icon_title_box);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabThumbnail, picture);
+  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, close_btn);
+  gtk_widget_class_bind_template_child (widget_class, AdwTabThumbnail, unpin_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, unpin_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)
+{
+  AdwAnimationTarget *target;
+
+  gtk_widget_init_template (GTK_WIDGET (self));
+
+  target = adw_callback_animation_target_new ((AdwAnimationTargetFunc) fade_animation_value_cb,
+                                              self, NULL);
+
+  self->fade_animation = adw_timed_animation_new (GTK_WIDGET (self),
+                                                  0, 1,
+                                                  FADE_TRANSITION_DURATION,
+                                                  target);
+}
+
+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) {
+    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_set_object (&self->page, page);
+
+  if (self->page) {
+    GdkPaintable *paintable = adw_tab_page_get_paintable (self->page);
+
+    gtk_picture_set_paintable (GTK_PICTURE (self->picture), paintable);
+
+    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);
+  }
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PAGE]);
+}
+
+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;
+
+  if (inverted) {
+    gtk_widget_set_halign (self->close_btn, GTK_ALIGN_START);
+    gtk_widget_set_halign (self->unpin_btn, GTK_ALIGN_START);
+    gtk_widget_set_halign (self->indicator_btn, GTK_ALIGN_END);
+  } else {
+    gtk_widget_set_halign (self->close_btn, GTK_ALIGN_END);
+    gtk_widget_set_halign (self->unpin_btn, GTK_ALIGN_END);
+    gtk_widget_set_halign (self->indicator_btn, GTK_ALIGN_START);
+  }
+
+  if (self->pinned)
+    gtk_widget_queue_resize (self->pinned_box);
+  else
+    gtk_widget_queue_allocate (GTK_WIDGET (self->overlay));
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INVERTED]);
+}
+
+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);
+}
+
+GtkWidget *
+adw_tab_thumbnail_get_thumbnail (AdwTabThumbnail *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_THUMBNAIL (self), NULL);
+
+  return self->overlay;
+}
+
+void
+adw_tab_thumbnail_fade_out (AdwTabThumbnail *self)
+{
+  g_return_if_fail (ADW_IS_TAB_THUMBNAIL (self));
+
+  adw_animation_play (self->fade_animation);
+}
+
diff --git a/src/adw-tab-thumbnail.ui b/src/adw-tab-thumbnail.ui
new file mode 100644
index 00000000..9bcb2123
--- /dev/null
+++ b/src/adw-tab-thumbnail.ui
@@ -0,0 +1,161 @@
+<?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">
+            <style>
+              <class name="thumbnail"/>
+            </style>
+            <property name="child">
+              <object class="GtkOverlay" id="overlay">
+                <property name="overflow">hidden</property>
+                <style>
+                  <class name="card"/>
+                </style>
+                <property name="child">
+                  <object class="GtkPicture" id="picture">
+                    <property name="can-shrink">True</property>
+                    <property name="keep-aspect-ratio">False</property>
+                    <property name="vexpand">True</property>
+                  </object>
+                </property>
+                <child type="overlay">
+                  <object class="GtkButton" id="close_btn">
+                    <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"/>
+                    <layout>
+                      <property name="measure">True</property>
+                    </layout>
+                    <style>
+                      <class name="circular"/>
+                      <class name="tab-close-button"/>
+                    </style>
+                  </object>
+                </child>
+                <child type="overlay">
+                  <object class="GtkButton" id="unpin_btn">
+                    <property name="can-focus">False</property>
+                    <property name="tooltip-text" translatable="yes">Unpin Tab</property>
+                    <property name="icon-name">adw-tab-unpin-symbolic</property>
+                    <property name="valign">start</property>
+                    <property name="halign">end</property>
+                    <signal name="clicked" handler="unpin_clicked_cb" swapped="true"/>
+                    <layout>
+                      <property name="measure">True</property>
+                    </layout>
+                    <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"/>
+                    <layout>
+                      <property name="measure">True</property>
+                    </layout>
+                    <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>
+            </property>
+            <child type="overlay">
+              <object class="GtkRevealer">
+                <property name="halign">end</property>
+                <property name="valign">start</property>
+                <property name="transition-type">crossfade</property>
+                <property name="can-focus">False</property>
+                <property name="can-target">False</property>
+                <style>
+                  <class name="needs-attention"/>
+                </style>
+                <binding name="reveal-child">
+                  <lookup name="needs-attention" type="AdwTabPage">
+                    <lookup name="page">AdwTabThumbnail</lookup>
+                  </lookup>
+                </binding>
+                <property name="child">
+                  <object class="AdwGizmo"/>
+                </property>
+              </object>
+            </child>
+          </object>
+        </child>
+        <child>
+          <object class="GtkBox" id="icon_title_box">
+            <property name="can-target">False</property>
+            <property name="orientation">horizontal</property>
+            <property name="halign">center</property>
+            <style>
+              <class name="icon-title-box"/>
+            </style>
+            <child>
+              <object class="GtkStack" id="icon_stack">
+                <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/adw-tab-view-private.h b/src/adw-tab-view-private.h
index ce65fdd4..9a250bf8 100644
--- a/src/adw-tab-view-private.h
+++ b/src/adw-tab-view-private.h
@@ -16,6 +16,8 @@
 
 G_BEGIN_DECLS
 
+GdkPaintable *adw_tab_page_get_paintable (AdwTabPage *self);
+
 gboolean adw_tab_view_select_first_page (AdwTabView *self);
 gboolean adw_tab_view_select_last_page  (AdwTabView *self);
 
diff --git a/src/adw-tab-view.c b/src/adw-tab-view.c
index ba51d8d6..47016cc7 100644
--- a/src/adw-tab-view.c
+++ b/src/adw-tab-view.c
@@ -17,6 +17,9 @@
 /* FIXME replace with groups */
 static GSList *tab_view_list;
 
+#define MIN_ASPECT_RATIO 0.8
+#define MAX_ASPECT_RATIO 2.7
+
 /**
  * AdwTabView:
  *
@@ -127,8 +130,12 @@ struct _AdwTabPage
   char *indicator_tooltip;
   gboolean indicator_activatable;
   gboolean needs_attention;
+  char *keyword;
+  float thumbnail_xalign;
+  float thumbnail_yalign;
 
   gboolean closing;
+  GdkPaintable *paintable;
 };
 
 G_DEFINE_FINAL_TYPE (AdwTabPage, adw_tab_page, G_TYPE_OBJECT)
@@ -147,6 +154,9 @@ enum {
   PAGE_PROP_INDICATOR_TOOLTIP,
   PAGE_PROP_INDICATOR_ACTIVATABLE,
   PAGE_PROP_NEEDS_ATTENTION,
+  PAGE_PROP_KEYWORD,
+  PAGE_PROP_THUMBNAIL_XALIGN,
+  PAGE_PROP_THUMBNAIL_YALIGN,
   LAST_PAGE_PROP
 };
 
@@ -286,6 +296,8 @@ adw_tab_page_dispose (GObject *object)
 
   set_page_parent (self, NULL);
 
+  g_clear_object (&self->paintable);
+
   G_OBJECT_CLASS (adw_tab_page_parent_class)->dispose (object);
 }
 
@@ -300,6 +312,7 @@ adw_tab_page_finalize (GObject *object)
   g_clear_object (&self->icon);
   g_clear_object (&self->indicator_icon);
   g_clear_pointer (&self->indicator_tooltip, g_free);
+  g_clear_pointer (&self->keyword, g_free);
 
   G_OBJECT_CLASS (adw_tab_page_parent_class)->finalize (object);
 }
@@ -361,6 +374,18 @@ adw_tab_page_get_property (GObject    *object,
     g_value_set_boolean (value, adw_tab_page_get_needs_attention (self));
     break;
 
+  case PAGE_PROP_KEYWORD:
+    g_value_set_string (value, adw_tab_page_get_keyword (self));
+    break;
+
+  case PAGE_PROP_THUMBNAIL_XALIGN:
+    g_value_set_float (value, adw_tab_page_get_thumbnail_xalign (self));
+    break;
+
+  case PAGE_PROP_THUMBNAIL_YALIGN:
+    g_value_set_float (value, adw_tab_page_get_thumbnail_yalign (self));
+    break;
+
   default:
     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
   }
@@ -415,6 +440,18 @@ adw_tab_page_set_property (GObject      *object,
     adw_tab_page_set_needs_attention (self, g_value_get_boolean (value));
     break;
 
+  case PAGE_PROP_KEYWORD:
+    adw_tab_page_set_keyword (self, g_value_get_string (value));
+    break;
+
+  case PAGE_PROP_THUMBNAIL_XALIGN:
+    adw_tab_page_set_thumbnail_xalign (self, g_value_get_float (value));
+    break;
+
+  case PAGE_PROP_THUMBNAIL_YALIGN:
+    adw_tab_page_set_thumbnail_yalign (self, g_value_get_float (value));
+    break;
+
   default:
     G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
   }
@@ -623,6 +660,44 @@ adw_tab_page_class_init (AdwTabPageClass *klass)
                           FALSE,
                           G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
 
+  /**
+   * AdwTabPage:keyword: (attributes org.gtk.Property.get=adw_tab_page_get_keyword 
org.gtk.Property.set=adw_tab_page_set_keyword)
+   *
+   * TODO
+   *
+   * Since: 1.3
+   */
+  page_props[PAGE_PROP_KEYWORD] =
+    g_param_spec_string ("keyword", NULL, NULL,
+                         "",
+                         G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabPage:thumbnail-xalign: (attributes org.gtk.Property.get=adw_tab_page_get_thumbnail_xalign 
org.gtk.Property.set=adw_tab_page_set_thumbnail_xalign)
+   *
+   * TODO
+   *
+   * Since: 1.3
+   */
+  page_props[PAGE_PROP_THUMBNAIL_XALIGN] =
+    g_param_spec_float ("thumbnail-xalign", NULL, NULL,
+                        0.0, 1.0,
+                        0.0,
+                        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * AdwTabPage:thumbnail-yalign: (attributes org.gtk.Property.get=adw_tab_page_get_thumbnail_yalign 
org.gtk.Property.set=adw_tab_page_set_thumbnail_yalign)
+   *
+   * TODO
+   *
+   * Since: 1.3
+   */
+  page_props[PAGE_PROP_THUMBNAIL_YALIGN] =
+    g_param_spec_float ("thumbnail-yalign", NULL, NULL,
+                        0.0, 1.0,
+                        0.0,
+                        G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
   g_object_class_install_properties (object_class, LAST_PAGE_PROP, page_props);
 }
 
@@ -632,6 +707,292 @@ adw_tab_page_init (AdwTabPage *self)
   self->title = g_strdup ("");
   self->tooltip = g_strdup ("");
   self->indicator_tooltip = g_strdup ("");
+  self->thumbnail_xalign = 0;
+  self->thumbnail_yalign = 0;
+}
+
+#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;
+  AdwTabPage *page;
+
+  GdkPaintable *view_paintable;
+
+  GdkPaintable *cached_paintable;
+  GdkRGBA cached_bg;
+
+  gboolean schedule_clear_cache;
+  gboolean frozen;
+};
+
+static void
+get_background_color (AdwTabPaintable *self,
+                      GdkRGBA         *rgba)
+{
+  GtkWidget *child = adw_tab_page_get_child (self->page);
+  GtkStyleContext *context = gtk_widget_get_style_context (child);
+
+  if (gtk_style_context_lookup_color (context, "window_bg_color", rgba))
+    return;
+
+  rgba->red = 1;
+  rgba->green = 1;
+  rgba->blue = 1;
+  rgba->alpha = 1;
+}
+
+static void
+invalidate_contents_and_clear_cache (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->frozen)
+    return;
+
+  gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
+
+  if (self->cached_paintable)
+    self->schedule_clear_cache = TRUE;
+}
+
+static void
+child_unmap_cb (AdwTabPaintable *self)
+{
+  if (self->frozen || self->cached_paintable || !self->view)
+    return;
+
+  g_clear_object (&self->cached_paintable);
+  self->cached_paintable = gdk_paintable_get_current_image (self->view_paintable);
+
+  get_background_color (self, &self->cached_bg);
+}
+
+static void
+connect_to_view (AdwTabPaintable *self)
+{
+  GtkWidget *child = adw_tab_page_get_child (self->page);
+
+  if (self->view || !gtk_widget_get_parent (child))
+    return;
+
+  self->view = gtk_widget_get_ancestor (child, ADW_TYPE_TAB_VIEW);
+  self->view_paintable = gtk_widget_paintable_new (self->view);
+
+  g_signal_connect_swapped (self->view_paintable, "invalidate-contents",
+                            G_CALLBACK (invalidate_contents_and_clear_cache), self);
+  g_signal_connect_swapped (self->view_paintable, "invalidate-size",
+                            G_CALLBACK (gdk_paintable_invalidate_size), self);
+}
+
+static void
+disconnect_from_view (AdwTabPaintable *self)
+{
+  g_clear_object (&self->view_paintable);
+
+  self->view = NULL;
+}
+
+static void
+child_parent_changed (AdwTabPaintable *self)
+{
+  disconnect_from_view (self);
+  connect_to_view (self);
+}
+
+static double
+adw_tab_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable)
+{
+  AdwTabPaintable *self = ADW_TAB_PAINTABLE (paintable);
+  double ratio;
+
+  if (!self->view)
+    return 0;
+
+  ratio = gdk_paintable_get_intrinsic_aspect_ratio (self->view_paintable);
+
+  return CLAMP (ratio, MIN_ASPECT_RATIO, MAX_ASPECT_RATIO);
+}
+
+static void
+snapshot_paintable (GtkSnapshot       *snapshot,
+                    double             width,
+                    double             height,
+                    GdkPaintable      *paintable,
+                    double             xalign,
+                    double             yalign)
+{
+  double snapshot_ratio = width / height;
+  double paintable_ratio = gdk_paintable_get_intrinsic_aspect_ratio (paintable);
+
+  if (paintable_ratio > snapshot_ratio) {
+    double new_width = height * paintable_ratio;
+
+    gtk_snapshot_translate (snapshot,
+                            &GRAPHENE_POINT_INIT ((float) (width - new_width) * xalign, 0));
+
+    width = new_width;
+  } if (paintable_ratio < snapshot_ratio) {
+    double new_height = width / paintable_ratio;
+
+    gtk_snapshot_translate (snapshot,
+                            &GRAPHENE_POINT_INIT (0, (float) (height - new_height) * yalign));
+
+    height = new_height;
+  }
+
+  gdk_paintable_snapshot (paintable, snapshot, width, height);
+}
+
+static GdkPaintable *
+adw_tab_paintable_get_current_image (GdkPaintable *paintable)
+{
+  AdwTabPaintable *self = ADW_TAB_PAINTABLE (paintable);
+  GtkSnapshot *snapshot = gtk_snapshot_new ();
+  int width, height;
+
+  if (!self->view)
+    return NULL;
+
+  width = gtk_widget_get_width (self->view);
+  height = gtk_widget_get_height (self->view);
+
+  gdk_paintable_snapshot (paintable, GDK_SNAPSHOT (snapshot), width, height);
+
+  return gtk_snapshot_free_to_paintable (snapshot,
+                                         &GRAPHENE_SIZE_INIT (width, height));
+}
+
+static void
+adw_tab_paintable_snapshot (GdkPaintable *paintable,
+                            GdkSnapshot  *snapshot,
+                            double        width,
+                            double        height)
+{
+  AdwTabPaintable *self = ADW_TAB_PAINTABLE (paintable);
+  GtkWidget *child;
+  GdkRGBA bg;
+  double xalign, yalign;
+
+  if (!self->view)
+    return;
+
+  child = adw_tab_page_get_child (self->page);
+  xalign = adw_tab_page_get_thumbnail_xalign (self->page);
+  yalign = adw_tab_page_get_thumbnail_yalign (self->page);
+
+  if (gtk_widget_get_direction (child) == GTK_TEXT_DIR_RTL)
+    xalign = 1 - xalign;
+
+  if (self->cached_paintable) {
+    gtk_snapshot_append_color (GTK_SNAPSHOT (snapshot), &self->cached_bg,
+                               &GRAPHENE_RECT_INIT (0, 0, width, height));
+
+    snapshot_paintable (GTK_SNAPSHOT (snapshot), width, height,
+                        self->cached_paintable, xalign, yalign);
+
+    return;
+  }
+
+  if (!gtk_widget_get_mapped (child))
+    return;
+
+  get_background_color (self, &bg);
+  gtk_snapshot_append_color (GTK_SNAPSHOT (snapshot), &bg,
+                             &GRAPHENE_RECT_INIT (0, 0, width, height));
+
+  snapshot_paintable (GTK_SNAPSHOT (snapshot), width, height,
+                      self->view_paintable, xalign, yalign);
+}
+
+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);
+
+  disconnect_from_view (self);
+
+  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 GdkPaintable *
+adw_tab_paintable_new (AdwTabPage *page)
+{
+  AdwTabPaintable *self = g_object_new (ADW_TYPE_TAB_PAINTABLE, NULL);
+  GtkWidget *child;
+
+  self->page = page;
+
+  child = adw_tab_page_get_child (page);
+
+  connect_to_view (self);
+
+  g_signal_connect_object (self->page, "notify::thumbnail-xalign",
+                           G_CALLBACK (gdk_paintable_invalidate_contents), self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (self->page, "notify::thumbnail-yalign",
+                           G_CALLBACK (gdk_paintable_invalidate_contents), self,
+                           G_CONNECT_SWAPPED);
+
+  g_signal_connect_object (child, "notify::parent",
+                           G_CALLBACK (child_parent_changed), self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (child, "map",
+                           G_CALLBACK (child_map_cb), self,
+                           G_CONNECT_SWAPPED);
+  g_signal_connect_object (child, "unmap",
+                           G_CALLBACK (child_unmap_cb), self,
+                           G_CONNECT_SWAPPED);
+
+  return GDK_PAINTABLE (self);
+}
+
+static void
+adw_tab_paintable_freeze (AdwTabPaintable *self)
+{
+  child_unmap_cb (self);
+  self->schedule_clear_cache = FALSE;
+  self->frozen = TRUE;
 }
 
 #define ADW_TYPE_TAB_PAGES (adw_tab_pages_get_type ())
@@ -2249,6 +2610,145 @@ adw_tab_page_set_needs_attention (AdwTabPage *self,
   g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_NEEDS_ATTENTION]);
 }
 
+/**
+ * adw_tab_page_get_keyword: (attributes org.gtk.Method.get_property=keyword)
+ * @self: a tab page
+ *
+ * Gets the TODO of @self.
+ *
+ * Returns: (nullable): the tooltip of @self
+ *
+ * Since: 1.3
+ */
+const char *
+adw_tab_page_get_keyword (AdwTabPage *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_PAGE (self), NULL);
+
+  return self->keyword;
+}
+
+/**
+ * adw_tab_page_set_keyword: (attributes org.gtk.Method.set_property=keyword)
+ * @self: a tab page
+ * @keyword: the keyword of @self
+ *
+ * Sets the TODO of @self.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_page_set_keyword (AdwTabPage *self,
+                          const char *keyword)
+{
+  g_return_if_fail (ADW_IS_TAB_PAGE (self));
+
+  if (!g_strcmp0 (keyword, self->keyword))
+    return;
+
+  g_clear_pointer (&self->keyword, g_free);
+  self->keyword = g_strdup (keyword);
+
+  g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_KEYWORD]);
+}
+
+/**
+ * adw_tab_page_get_thumbnail_xalign: (attributes org.gtk.Method.get_property=thumbnail-xalign)
+ * @self: a tab page
+ *
+ * Gets TODO
+ *
+ * Returns: TODO
+ *
+ * Since: 1.3
+ */
+float
+adw_tab_page_get_thumbnail_xalign (AdwTabPage *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_PAGE (self), 0.0f);
+
+  return self->thumbnail_xalign;
+}
+
+/**
+ * adw_tab_page_set_thumbnail_xalign: (attributes org.gtk.Method.set_property=thumbnail-xalign)
+ * @self: a tab page
+ * @xalign: TODO
+ *
+ * Sets TODO
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_page_set_thumbnail_xalign (AdwTabPage *self,
+                                   float       xalign)
+{
+  g_return_if_fail (ADW_IS_TAB_PAGE (self));
+
+  xalign = CLAMP (xalign, 0.0, 1.0);
+
+  if (self->thumbnail_xalign == xalign)
+    return;
+
+  self->thumbnail_xalign = xalign;
+
+  g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_THUMBNAIL_XALIGN]);
+}
+
+/**
+ * adw_tab_page_get_thumbnail_yalign: (attributes org.gtk.Method.get_property=thumbnail-yalign)
+ * @self: a tab overview
+ *
+ * Gets TODO
+ *
+ * Returns: TODO
+ *
+ * Since: 1.3
+ */
+float
+adw_tab_page_get_thumbnail_yalign (AdwTabPage *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_PAGE (self), 0.0f);
+
+  return self->thumbnail_yalign;
+}
+
+/**
+ * adw_tab_page_set_thumbnail_yalign: (attributes org.gtk.Method.set_property=thumbnail-yalign)
+ * @self: a tab page
+ * @xalign: TODO
+ *
+ * Sets TODO
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_page_set_thumbnail_yalign (AdwTabPage *self,
+                                   float       yalign)
+{
+  g_return_if_fail (ADW_IS_TAB_PAGE (self));
+
+  yalign = CLAMP (yalign, 0.0, 1.0);
+
+  if (self->thumbnail_yalign == yalign)
+    return;
+
+  self->thumbnail_yalign = yalign;
+
+  g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_THUMBNAIL_YALIGN]);
+}
+
+GdkPaintable *
+adw_tab_page_get_paintable (AdwTabPage *self)
+{
+  g_return_val_if_fail (ADW_IS_TAB_PAGE (self), NULL);
+
+  if (!self->paintable)
+    self->paintable = adw_tab_paintable_new (self);
+
+  return self->paintable;
+}
+
 /**
  * adw_tab_view_new:
  *
@@ -3120,6 +3620,9 @@ adw_tab_view_close_page_finish (AdwTabView *self,
 
   page->closing = FALSE;
 
+  if (page->paintable)
+    adw_tab_paintable_freeze (ADW_TAB_PAINTABLE (page->paintable));
+
   if (confirm)
     detach_page (self, page, TRUE);
 }
diff --git a/src/adw-tab-view.h b/src/adw-tab-view.h
index 4c1af2da..ad4fdf11 100644
--- a/src/adw-tab-view.h
+++ b/src/adw-tab-view.h
@@ -101,6 +101,24 @@ ADW_AVAILABLE_IN_ALL
 void     adw_tab_page_set_needs_attention (AdwTabPage *self,
                                            gboolean    needs_attention);
 
+ADW_AVAILABLE_IN_1_2
+const char *adw_tab_page_get_keyword (AdwTabPage *self);
+ADW_AVAILABLE_IN_1_2
+void        adw_tab_page_set_keyword (AdwTabPage *self,
+                                      const char *keyword);
+
+ADW_AVAILABLE_IN_1_2
+float adw_tab_page_get_thumbnail_xalign (AdwTabPage *self);
+ADW_AVAILABLE_IN_1_2
+void  adw_tab_page_set_thumbnail_xalign (AdwTabPage *self,
+                                         float       xalign);
+
+ADW_AVAILABLE_IN_1_2
+float adw_tab_page_get_thumbnail_yalign (AdwTabPage *self);
+ADW_AVAILABLE_IN_1_2
+void  adw_tab_page_set_thumbnail_yalign (AdwTabPage *self,
+                                         float       yalign);
+
 #define ADW_TYPE_TAB_VIEW (adw_tab_view_get_type())
 
 ADW_AVAILABLE_IN_ALL
diff --git a/src/adwaita.gresources.xml b/src/adwaita.gresources.xml
index 095d250e..159fc0e7 100644
--- a/src/adwaita.gresources.xml
+++ b/src/adwaita.gresources.xml
@@ -11,6 +11,7 @@
     <file preprocess="xml-stripblanks">icons/scalable/status/adw-tab-counter-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/status/adw-tab-icon-missing-symbolic.svg</file>
     <file preprocess="xml-stripblanks">icons/scalable/status/adw-tab-overflow-symbolic.svg</file>
+    <file preprocess="xml-stripblanks">icons/scalable/status/adw-tab-unpin-symbolic.svg</file>
   </gresource>
   <gresource prefix="/org/gnome/Adwaita/ui">
     <file preprocess="xml-stripblanks">adw-about-window.ui</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/icons/scalable/status/adw-tab-unpin-symbolic.svg 
b/src/icons/scalable/status/adw-tab-unpin-symbolic.svg
new file mode 100644
index 00000000..52a7a039
--- /dev/null
+++ b/src/icons/scalable/status/adw-tab-unpin-symbolic.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg"; width="16" height="16"><path 
style="stroke:none;fill-rule:nonzero;fill:#222;fill-opacity:1" d="M9 11H7v3l1 1 1-1Zm2.223-8.707A1 1 0 0 0 
10.516 2H5.5a1 1 0 1 0 0 2h5.016a1.002 1.002 0 0 0 .707-1.707ZM4 10c0-2.21 1.79-4 4-4s4 1.79 4 4Zm0 0"/><path 
style="stroke:none;fill-rule:nonzero;fill:#222;fill-opacity:1" d="m5.441 2.973.895 5.164H9.71l.848-5.11Zm0 
0"/></svg>
\ No newline at end of file
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/_colors.scss b/src/stylesheet/_colors.scss
index d33250cc..35216297 100644
--- a/src/stylesheet/_colors.scss
+++ b/src/stylesheet/_colors.scss
@@ -46,6 +46,9 @@ $dialog_fg_color: gtkcolor(dialog_fg_color);
 $popover_bg_color: gtkcolor(popover_bg_color);
 $popover_fg_color: gtkcolor(popover_fg_color);
 
+$thumbnail_bg_color: gtkcolor(thumbnail_bg_color);
+$thumbnail_fg_color: gtkcolor(thumbnail_fg_color);
+
 $shade_color: gtkcolor(shade_color);
 $scrollbar_outline_color: gtkcolor(scrollbar_outline_color);
 
diff --git a/src/stylesheet/_defaults.scss b/src/stylesheet/_defaults.scss
index e313d5f9..d671f4af 100644
--- a/src/stylesheet/_defaults.scss
+++ b/src/stylesheet/_defaults.scss
@@ -59,6 +59,10 @@
 @define-color popover_bg_color #{if($variant == 'light', #ffffff, #383838)};
 @define-color popover_fg_color #{if($variant == 'light', transparentize(black, .2), white)};
 
+// Thumbnails
+@define-color thumbnail_bg_color #{if($variant == 'light', #ffffff, #383838)};
+@define-color thumbnail_fg_color #{if($variant == 'light', transparentize(black, .2), white)};
+
 // Miscellaneous
 @define-color shade_color #{if($variant == 'light', transparentize(black, .93), transparentize(black, .64))};
 @define-color scrollbar_outline_color #{if($variant == 'light', white, transparentize(black, .5))};
diff --git a/src/stylesheet/widgets/_tab-view.scss b/src/stylesheet/widgets/_tab-view.scss
index 1b140c40..6bc914aa 100644
--- a/src/stylesheet/widgets/_tab-view.scss
+++ b/src/stylesheet/widgets/_tab-view.scss
@@ -139,7 +139,109 @@ dnd {
   }
 }
 
+tabgrid > tabgridchild {
+  @include focus-ring(".card", $offset: 0, $outer: true);
+}
+
+tabthumbnail {
+  border-radius: $card_radius + 4px;
+
+  > box {
+    margin: 6px;
+  }
+
+  &:drop(active) {
+    box-shadow: inset 0 0 0 2px gtkalpha($drop_target_color, .4);
+    background-color: gtkalpha($drop_target_color, .1);
+  }
+
+  transition: box-shadow 200ms $ease-out-quad, background-color $ease-out-quad;
+
+  .needs-attention {
+    &:dir(ltr) { transform: translate(6px, -6px); }
+    &:dir(rtl) { transform: translate(-6px, -6px); }
+
+    > widget {
+      background: $accent_color;
+      min-width: 12px;
+      min-height: 12px;
+      border-radius: 8px;
+      margin: 3px;
+      box-shadow: 0 1px 2px gtkalpha($accent_color, .4);
+    }
+  }
+
+  .thumbnail {
+    transition: transform 200ms $ease-out-quad;
+  }
+
+  .card {
+    picture {
+      outline: 1px solid $window_outline_color;
+      outline-offset: -1px;
+      border-radius: $card_radius;
+    }
+
+    background-color: $thumbnail_bg_color;
+    color: $thumbnail_fg_color;
+
+    @if $contrast == 'high' {
+      box-shadow: 0 0 0 1px transparentize(black, 0.5),
+                  0 1px 3px 1px transparentize(black, .93),
+                  0 2px 6px 2px transparentize(black, .97);
+    }
+  }
+
+  :hover:not(:active) .thumbnail {
+    transform: scale(1.02);
+  }
+
+  .pinned-box {
+    margin-left: 10px;
+    margin-right: 10px;
+  }
+
+  .icon-title-box {
+    border-spacing: 6px;
+  }
+
+  button.circular {
+    margin: 6px;
+    background-color: gtkalpha($thumbnail_bg_color, .75);
+    min-width: 24px;
+    min-height: 24px;
+
+    @if $contrast == 'high' {
+      box-shadow: 0 0 0 1px currentColor;
+    }
+
+    &:hover {
+      background-color: gtkalpha(gtkmix($thumbnail_bg_color, currentColor, 90%), .75);
+    }
+
+    &:active {
+      background-color: gtkalpha(gtkmix($thumbnail_bg_color, currentColor, 80%), .75);
+    }
+  }
+}
+
+taboverview > .overview {
+  &.scrolled-to-top {
+    headerbar,
+    searchbar > revealer > box {
+      background: none;
+      color: inherit;
+      box-shadow: none;
+    }
+  }
+
+  .new-tab-button {
+    margin: 18px;
+  }
+}
+
 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]