[libadwaita/wip/exalm/tab-overview: 9/15] Add AdwTabOverview
- From: Alexander Mikhaylenko <alexm src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libadwaita/wip/exalm/tab-overview: 9/15] Add AdwTabOverview
- Date: Sat, 10 Sep 2022 23:28:26 +0000 (UTC)
commit 7b759d73d8a57962b8d938047d2ed24474b880ab
Author: Alexander Mikhaylenko <alexm gnome org>
Date: Fri Aug 12 03:04:59 2022 +0400
Add AdwTabOverview
doc/images/tab-overview-dark.png | Bin 0 -> 26785 bytes
doc/images/tab-overview.png | Bin 0 -> 26253 bytes
doc/libadwaita.toml.in | 2 +
doc/tools/data/tab-overview.ui | 82 +
doc/tools/screenshot.c | 6 +
doc/tools/screenshot.gresources.xml | 1 +
doc/tools/style-dark.css | 3 +
doc/tools/style.css | 4 +
doc/visual-index.md | 5 +
src/adw-tab-grid-private.h | 87 +
src/adw-tab-grid.c | 3820 ++++++++++++++++++++
src/adw-tab-overview-private.h | 24 +
src/adw-tab-overview.c | 2398 ++++++++++++
src/adw-tab-overview.h | 93 +
src/adw-tab-overview.ui | 147 +
src/adw-tab-thumbnail-private.h | 46 +
src/adw-tab-thumbnail.c | 678 ++++
src/adw-tab-thumbnail.ui | 161 +
src/adw-tab-view-private.h | 5 +
src/adw-tab-view.c | 1020 +++++-
src/adw-tab-view.h | 30 +
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 | 106 +-
tests/meson.build | 1 +
tests/test-tab-overview.c | 329 ++
tests/test-tab-view.c | 127 +
31 files changed, 9166 insertions(+), 25 deletions(-)
---
diff --git a/doc/images/tab-overview-dark.png b/doc/images/tab-overview-dark.png
new file mode 100644
index 00000000..932646fe
Binary files /dev/null and b/doc/images/tab-overview-dark.png differ
diff --git a/doc/images/tab-overview.png b/doc/images/tab-overview.png
new file mode 100644
index 00000000..ce21d897
Binary files /dev/null and b/doc/images/tab-overview.png differ
diff --git a/doc/libadwaita.toml.in b/doc/libadwaita.toml.in
index d75d00fd..3f333f50 100644
--- a/doc/libadwaita.toml.in
+++ b/doc/libadwaita.toml.in
@@ -200,6 +200,8 @@ content_images = [
"images/tab-bar-inline-dark.png",
"images/tab-button.png",
"images/tab-button-dark.png",
+ "images/tab-overview.png",
+ "images/tab-overview-dark.png",
"images/toast-action.png",
"images/toast-action-dark.png",
"images/toast-overlay.png",
diff --git a/doc/tools/data/tab-overview.ui b/doc/tools/data/tab-overview.ui
new file mode 100644
index 00000000..6638dab2
--- /dev/null
+++ b/doc/tools/data/tab-overview.ui
@@ -0,0 +1,82 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk" version="4.0"/>
+ <requires lib="libadwaita" version="1.0"/>
+ <object class="AdwWindow" id="widget">
+ <property name="title">Tab Bar</property>
+ <property name="default-width">600</property>
+ <property name="default-height">480</property>
+ <property name="content">
+ <object class="AdwTabOverview">
+ <property name="view">view</property>
+ <property name="open">True</property>
+ <property name="enable-new-tab">True</property>
+ <property name="child">
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkHeaderBar"/>
+ </child>
+ <child>
+ <object class="AdwTabView" id="view">
+ <property name="vexpand">True</property>
+ <child>
+ <object class="AdwTabPage">
+ <property name="title">Page 1</property>
+ <property name="child">
+ <object class="AdwBin">
+ <style>
+ <class name="overview-tab-page"/>
+ </style>
+ </object>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="AdwTabPage">
+ <property name="title">Page 2</property>
+ <property name="child">
+ <object class="AdwBin"/>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="AdwTabPage">
+ <property name="title">Page 3</property>
+ <property name="child">
+ <object class="AdwBin"/>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="AdwTabPage">
+ <property name="title">Page 4</property>
+ <property name="child">
+ <object class="AdwBin"/>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="AdwTabPage">
+ <property name="title">Page 5</property>
+ <property name="child">
+ <object class="AdwBin"/>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="AdwTabPage">
+ <property name="title">Page 6</property>
+ <property name="child">
+ <object class="AdwBin"/>
+ </property>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </property>
+ </object>
+ </property>
+ </object>
+</interface>
diff --git a/doc/tools/screenshot.c b/doc/tools/screenshot.c
index a7e7701d..ffa6b37e 100644
--- a/doc/tools/screenshot.c
+++ b/doc/tools/screenshot.c
@@ -19,6 +19,7 @@ typedef struct {
GdkPaintable *paintable;
char *name;
GtkCssProvider *provider;
+ GtkCssProvider *provider_dark;
} ScreenshotData;
static void
@@ -27,6 +28,7 @@ screenshot_data_free (ScreenshotData *data)
g_object_unref (data->paintable);
gtk_window_destroy (GTK_WINDOW (gtk_widget_get_root (data->widget)));
g_object_unref (data->provider);
+ g_clear_object (&data->provider_dark);
g_free (data->name);
g_free (data);
}
@@ -216,6 +218,10 @@ take_screenshot (const char *name,
data->paintable = gtk_widget_paintable_new (data->widget);
data->name = g_file_get_path (output_file);
data->provider = load_css ("style");
+
+ if (dark)
+ data->provider_dark = load_css ("style-dark");
+
if (hover_widget)
data->hover_widget = GTK_WIDGET (hover_widget);
diff --git a/doc/tools/screenshot.gresources.xml b/doc/tools/screenshot.gresources.xml
index cbe21e7f..a8fc670e 100644
--- a/doc/tools/screenshot.gresources.xml
+++ b/doc/tools/screenshot.gresources.xml
@@ -4,5 +4,6 @@
<file preprocess="xml-stripblanks">icons/scalable/apps/org.gnome.Boxes.svg</file>
<file compressed="true">style.css</file>
+ <file compressed="true">style-dark.css</file>
</gresource>
</gresources>
diff --git a/doc/tools/style-dark.css b/doc/tools/style-dark.css
new file mode 100644
index 00000000..c9dc04bf
--- /dev/null
+++ b/doc/tools/style-dark.css
@@ -0,0 +1,3 @@
+.overview-tab-page {
+ background: linear-gradient(to bottom right, @blue_5, @green_5 80%);
+}
diff --git a/doc/tools/style.css b/doc/tools/style.css
index 2cab3c82..530b2ea7 100644
--- a/doc/tools/style.css
+++ b/doc/tools/style.css
@@ -12,3 +12,7 @@
color: @warning_fg_color;
background-color: @warning_bg_color;
}
+
+.overview-tab-page {
+ background: linear-gradient(to bottom right, @blue_1 25%, @green_1);
+}
diff --git a/doc/visual-index.md b/doc/visual-index.md
index cd115e6c..329f11de 100644
--- a/doc/visual-index.md
+++ b/doc/visual-index.md
@@ -140,6 +140,11 @@ Slug: visual-index
<img src="tab-bar.png" alt="tab-bar">
</picture>](class.TabBar.html)
+[<picture>
+ <source srcset="tab-overview-dark.png" media="(prefers-color-scheme: dark)">
+ <img src="tab-overview.png" alt="tab-overview">
+</picture>](class.TabOverview.html)
+
[<picture>
<source srcset="tab-button-dark.png" media="(prefers-color-scheme: dark)">
<img src="tab-button.png" alt="tab-button">
diff --git a/src/adw-tab-grid-private.h b/src/adw-tab-grid-private.h
new file mode 100644
index 00000000..4e861d19
--- /dev/null
+++ b/src/adw-tab-grid-private.h
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2020-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 <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);
+
+int adw_tab_grid_measure_height_final (AdwTabGrid *self,
+ int for_width);
+
+G_END_DECLS
diff --git a/src/adw-tab-grid.c b/src/adw-tab-grid.c
new file mode 100644
index 00000000..6ca49dd2
--- /dev/null
+++ b/src/adw-tab-grid.c
@@ -0,0 +1,3820 @@
+/*
+ * Copyright (C) 2020-2022 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 MIN_SCALE 0.75
+#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;
+ gboolean is_hidden;
+} 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);
+ int n_tabs = 0;
+ int other_n_tabs = 0;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ max_columns += info->appear_progress;
+
+ if (info->page)
+ n_tabs++;
+ }
+
+ for (l = other_grid->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ other_max_columns += info->appear_progress;
+
+ if (info->page)
+ other_n_tabs++;
+ }
+
+ max_columns = MAX (1, max_columns);
+ other_max_columns = MAX (1, other_max_columns);
+
+ /* Let's say we have one pinned and two regular tab, and we pin one of them.
+ * During this animation max number of columns goes from 2 back to 2, but
+ * dips in the middle of the animation. We want to keep it at 2 the whole
+ * animation instead. */
+ if ((n_tabs == other_n_tabs + 1 &&
+ max_columns < n_tabs &&
+ other_max_columns > other_n_tabs) ||
+ (other_n_tabs == n_tabs + 1 &&
+ max_columns > n_tabs &&
+ other_max_columns < other_n_tabs)) {
+ return MAX (n_tabs, other_n_tabs);
+ }
+
+ 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 = 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) &&
+ 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 != self->reordered_tab &&
+ 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 (!info->is_hidden)
+ gtk_widget_set_opacity (info->container, info->appear_progress);
+
+ 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);
+
+ gtk_widget_insert_after (GTK_WIDGET (info->container),
+ GTK_WIDGET (self), NULL);
+
+ 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);
+
+ info->is_hidden = TRUE;
+ 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;
+
+ self->reorder_placeholder->is_hidden = FALSE;
+ 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;
+
+ if (self->n_tabs == 0 || (self->searching && get_n_visible_tabs (self) == 0))
+ set_empty (self, TRUE);
+}
+
+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);
+
+ detached_info->is_hidden = TRUE;
+ 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
+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;
+ }
+}
+
+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;
+ }
+
+ adw_tab_view_set_selected_page (self->view, info->page);
+ 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;
+
+ 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;
+ GskTransform *transform = NULL;
+ int x, y, w, h;
+
+ if (!gtk_widget_should_layout (info->container))
+ continue;
+
+ x = ((info == self->reordered_tab) ? self->reorder_window_x : info->pos_x);
+ y = ((info == self->reordered_tab) ? self->reorder_window_y : info->pos_y);
+ w = MAX (0, info->width);
+ h = MAX (0, info->height);
+
+ transform = gsk_transform_translate (transform, &GRAPHENE_POINT_INIT (x, y));
+
+ if (info->appear_progress < 1) {
+ double scale = MIN_SCALE + (1 - MIN_SCALE) * info->appear_progress;
+ transform = gsk_transform_translate (transform, &GRAPHENE_POINT_INIT (w / 2.0f, h / 2.0f));
+ transform = gsk_transform_scale (transform, scale, scale);
+ transform = gsk_transform_translate (transform, &GRAPHENE_POINT_INIT (-w / 2.0f, -h / 2.0f));
+ }
+
+ gtk_widget_allocate (info->container, w, h, baseline, transform);
+ }
+}
+
+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;
+ self->empty = TRUE;
+
+ 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);
+}
+
+int
+adw_tab_grid_measure_height_final (AdwTabGrid *self,
+ int for_width)
+{
+ int minimum;
+
+ measure_tab_grid (self, GTK_ORIENTATION_VERTICAL, for_width, &minimum, NULL, FALSE);
+
+ return minimum;
+}
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..ef7a0f4b
--- /dev/null
+++ b/src/adw-tab-overview.c
@@ -0,0 +1,2398 @@
+/*
+ * Copyright (C) 2021-2022 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-tab-view-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:
+ *
+ * A tab overview for [class@TabView].
+ *
+ * <picture>
+ * <source srcset="tab-overview-dark.png" media="(prefers-color-scheme: dark)">
+ * <img src="tab-overview.png" alt="tab-overview">
+ * </picture>
+ *
+ * `AdwTabOverview` is a widget that can display tabs from an `AdwTabView` in a
+ * grid.
+ *
+ * `AdwTabOverview` shows a thumbnail for each tab. By default thumbnails are
+ * static for all pages except the selected one. They can be made always live
+ * by setting [property@TabPage:live-thumbnail] to `TRUE`, or refreshed with
+ * [method@TabPage.invalidate_thumbnail] or
+ * [method@TabView.invalidate_thumbnails] otherwise.
+ *
+ * If the pages are too tall or too wide, the thumbnails will be cropped; use
+ * [property@TabPage:thumbnail-xalign] and [property@TabPage:thumbnail-yalign] to
+ * control which part of the page should be visible in this case.
+ *
+ * Pinned tabs are shown as smaller cards without thumbnails above the other
+ * tabs. Unlike in [class@TabBar], they still have titles, as well as an unpin
+ * button.
+ *
+ * `AdwTabOverview` provides search in open tabs. It searches in tab titles and
+ * tooltips, as well as [property@TabPage:keyword].
+ *
+ * If [property@TabOverview:enable-new-tab] is set to `TRUE`, a new tab button
+ * will be shown. Connect to the [signal@TabOverview::create-tab] signal to use
+ * it.
+ *
+ * [property@TabOverview:secondary-menu] can be used to provide a secondary menu
+ * for the overview. Use it to add extra actions, e.g. to open a new window or
+ * undo closed tab.
+ *
+ * `AdwTabOverview` is intended to be cover the whole window and shows window
+ * buttons by default. They can be disabled by setting
+ * [property@TabOverview:show-start-title-buttons] and/or
+ * [property@TabOverview:show-end-title-buttons] to `FALSE`.
+ *
+ * If search and window buttons are disabled, and secondary menu is not set, the
+ * header bar will be hidden.
+ *
+ * ## Actions
+ *
+ * `AdwTabOverview` defines the `overview.open` and `overview.close` actions for
+ * opening and closing itself. They can be convenient when used together with
+ * [class@TabButton].
+ *
+ * ## CSS nodes
+ *
+ * `AdwTabOverview` has a single CSS node with name `taboverview`.
+ *
+ * Since: 1.3
+ */
+
+struct _AdwTabOverview
+{
+ GtkWidget parent_instance;
+
+ GtkWidget *overview;
+ GtkWidget *empty_state;
+ GtkWidget *search_empty_state;
+ GtkWidget *scrollable;
+ GtkWidget *child_bin;
+ GtkWidget *header_bar;
+ GtkWidget *title;
+ GtkWidget *new_tab_button;
+ GtkWidget *search_button;
+ GtkWidget *search_bar;
+ GtkWidget *search_entry;
+ GtkWidget *secondary_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_SECONDARY_MENU,
+ PROP_SHOW_START_TITLE_BUTTONS,
+ PROP_SHOW_END_TITLE_BUTTONS,
+ LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_CREATE_TAB,
+ 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 final_upper)
+{
+ 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);
+
+ to += scrolled_y + get_grid_offset (self, self->scroll_animation_grid);
+ to = CLAMP (to, 0, final_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_allocate (GTK_WIDGET (self));
+}
+
+static void
+scroll_animation_done_cb (AdwTabOverviewScrollable *self)
+{
+ self->scroll_animation_done = TRUE;
+ gtk_widget_queue_allocate (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);
+
+ 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;
+ int final_grid_height, final_pinned_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);
+
+ final_grid_height = adw_tab_grid_measure_height_final (ADW_TAB_GRID (self->grid), width);
+ final_pinned_height = adw_tab_grid_measure_height_final (ADW_TAB_GRID (self->pinned_grid), width);
+
+ 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,
+ final_grid_height +
+ final_pinned_height +
+ new_button_height);
+
+ 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 = floor (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 =
+ adw_tab_grid_get_empty (self->grid) &&
+ adw_tab_grid_get_empty (self->pinned_grid);
+
+ gtk_widget_set_visible (self->empty_state, empty && !self->search_active);
+ gtk_widget_set_visible (self->search_empty_state, empty && self->search_active);
+}
+
+static void
+update_actions (AdwTabOverview *self)
+{
+ gboolean has_view = self->view != NULL;
+ gboolean has_pages = has_view && adw_tab_view_get_n_pages (self->view) > 0;
+
+ gtk_widget_action_set_enabled (GTK_WIDGET (self), "overview.open",
+ !self->is_open && has_view);
+ gtk_widget_action_set_enabled (GTK_WIDGET (self), "overview.close",
+ self->is_open && has_view && has_pages);
+}
+
+static void
+update_header_bar (AdwTabOverview *self)
+{
+ gtk_widget_set_visible (self->header_bar,
+ self->enable_search ||
+ adw_tab_overview_get_secondary_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 AdwTabPage *
+create_tab (AdwTabOverview *self)
+{
+ AdwTabPage *new_page = NULL;
+
+ g_signal_emit (self, signals[SIGNAL_CREATE_TAB], 0, &new_page);
+
+ if (!new_page) {
+ g_critical ("AdwTabOverview::create-tab handler must not return NULL");
+
+ return NULL;
+ }
+
+ return new_page;
+}
+
+static void
+new_tab_clicked_cb (AdwTabOverview *self)
+{
+ AdwTabPage *new_page = create_tab (self);
+ GtkWidget *child;
+
+ if (!new_page)
+ return;
+
+ child = adw_tab_page_get_child (new_page);
+
+ adw_tab_view_set_selected_page (self->view, new_page);
+ adw_tab_overview_set_open (self, FALSE);
+
+ gtk_widget_grab_focus (child);
+}
+
+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);
+ update_actions (self);
+}
+
+static void
+page_detached_cb (AdwTabOverview *self,
+ AdwTabPage *page,
+ int position)
+{
+ g_signal_handlers_disconnect_by_func (page, notify_pinned_cb, self);
+ update_actions (self);
+}
+
+static void
+open_animation_value_cb (double value,
+ AdwTabOverview *self)
+{
+ self->progress = value;
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+set_overview_visible (AdwTabOverview *self,
+ gboolean visible,
+ gboolean animating)
+{
+ gtk_widget_set_child_visible (self->overview, visible || animating);
+ gtk_widget_set_can_target (self->overview, visible);
+ gtk_widget_set_can_focus (self->overview, visible);
+ gtk_widget_set_can_target (self->child_bin, !visible && !animating);
+ gtk_widget_set_can_focus (self->child_bin, !visible && !animating);
+
+ if (visible || animating)
+ gtk_widget_add_css_class (self->child_bin, "background");
+ else
+ gtk_widget_remove_css_class (self->child_bin, "background");
+}
+
+static void
+open_animation_done_cb (AdwTabOverview *self)
+{
+ if (self->transition_picture) {
+ g_clear_object (&self->transition_picture);
+
+ adw_tab_thumbnail_fade_in (self->transition_thumbnail);
+ self->transition_thumbnail = NULL;
+ }
+
+ set_overview_visible (self, self->is_open, FALSE);
+
+ if (!self->is_open) {
+ adw_tab_view_close_overview (self->view);
+
+ 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_error ("AdwTabView %p must be inside its AdwTabOverview %p", self->view, self);
+
+ 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 || hc) {
+ /* 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->secondary_menu_button ||
+ gtk_widget_is_ancestor (focus, self->secondary_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_SECONDARY_MENU:
+ g_value_set_object (value, adw_tab_overview_get_secondary_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_SECONDARY_MENU:
+ adw_tab_overview_set_secondary_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 gboolean
+escape_cb (AdwTabOverview *self)
+{
+ if (!self->is_open)
+ return GDK_EVENT_PROPAGATE;
+
+ if (gtk_search_bar_get_search_mode (GTK_SEARCH_BAR (self->search_bar))) {
+ gtk_search_bar_set_search_mode (GTK_SEARCH_BAR (self->search_bar), FALSE);
+ return GDK_EVENT_STOP;
+ }
+
+ adw_tab_overview_set_open (self, FALSE);
+ return GDK_EVENT_STOP;
+}
+
+static gboolean
+object_handled_accumulator (GSignalInvocationHint *ihint,
+ GValue *return_accu,
+ const GValue *handler_return,
+ gpointer data)
+{
+ GObject *object = g_value_get_object (handler_return);
+
+ g_value_set_object (return_accu, object);
+
+ return !object;
+}
+
+static void
+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.
+ *
+ * The view must be inside the tab overview, see [property@TabOverview:child].
+ *
+ * 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 or unpin buttons 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)
+ *
+ * Whether to enable search in tabs.
+ *
+ * Search matches tab titles and tooltips, as well as keywords, set via
+ * [property@TabPage:keyword]. Use keywords to search in e.g. page URLs in a
+ * web browser.
+ *
+ * During search, tab reordering and drag-n-drop are disabled.
+ *
+ * Use [property@TabOverview:search-active] to check out if search is
+ * currently active.
+ *
+ * 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)
+ *
+ * Whether search is currently active.
+ *
+ * See [property@TabOverview:enable-search].
+ *
+ * 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)
+ *
+ * Whether to enable new tab button.
+ *
+ * Connect to the [signal@TabOverview::create-tab] signal to use it.
+ *
+ * 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:secondary-menu: (attributes org.gtk.Property.get=adw_tab_overview_get_secondary_menu
org.gtk.Property.set=adw_tab_overview_set_secondary_menu)
+ *
+ * The secondary menu model.
+ *
+ * Use it to add extra actions, e.g. to open a new window or undo closed tab.
+ *
+ * Since: 1.3
+ */
+ props[PROP_SECONDARY_MENU] =
+ g_param_spec_object ("secondary-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)
+ *
+ * Whether to show start title buttons in the overview's header bar.
+ *
+ * See [property@HeaderBar:show-end-title-buttons] for the other side.
+ *
+ * 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)
+ *
+ * Whether to show end title buttons in the overview's header bar.
+ *
+ * See [property@HeaderBar:show-start-title-buttons] for the other side.
+ *
+ * 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::create-tab:
+ * @self: a tab overview
+ *
+ * Emitted when a tab needs to be created;
+ *
+ * This can happen after the new tab button has been pressed, see
+ * [property@TabOverview:enable-new-tab].
+ *
+ * The signal handler is expected to create a new page in the corresponding
+ * [class@TabView] and return it.
+ *
+ * Returns: (transfer none): the newly created page
+ *
+ * Since: 1.3
+ */
+ signals[SIGNAL_CREATE_TAB] =
+ g_signal_new ("create-tab",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ object_handled_accumulator,
+ NULL, NULL,
+ ADW_TYPE_TAB_PAGE,
+ 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 (widget_class, GDK_KEY_Escape, 0,
+ (GtkShortcutFunc) escape_cb, 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, empty_state);
+ gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, search_empty_state);
+ 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, secondary_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, 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.
+ *
+ * The view must be inside @self, see [property@TabOverview:child].
+ *
+ * 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);
+ }
+
+ update_actions (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 @self 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 to open @self.
+ *
+ * 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;
+
+ if (open && !self->view) {
+ g_warning ("Trying to open AdwTabOverview %p, but it doesn't have a view set", self);
+ return;
+ }
+
+ if (!adw_tab_view_get_n_pages (self->view)) {
+ if (open)
+ g_warning ("Trying to open AdwTabOverview %p with no pages in its AdwTabView", self);
+ else
+ g_warning ("Trying to close AdwTabOverview %p with no pages in its AdwTabView", self);
+
+ 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;
+
+ update_actions (self);
+
+ 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);
+ }
+
+ adw_tab_view_open_overview (self->view);
+
+ set_overview_visible (self, self->is_open, TRUE);
+
+ adw_tab_grid_try_focus_selected_tab (grid, FALSE);
+ } else {
+ set_overview_visible (self, self->is_open, TRUE);
+ }
+
+ if (self->transition_picture)
+ adw_tab_thumbnail_fade_in (self->transition_thumbnail);
+
+ self->transition_thumbnail = adw_tab_grid_get_transition_thumbnail (grid);
+ self->transition_picture = g_object_ref (adw_tab_thumbnail_get_thumbnail (self->transition_thumbnail));
+ adw_tab_thumbnail_fade_out (self->transition_thumbnail);
+
+ 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 thumbnails use inverted layout.
+ *
+ * If set to `TRUE`, thumbnails will have the close or unpin 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 whether search in tabs is enabled for @self.
+ *
+ * Returns: whether search is enabled
+ *
+ * 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: whether to enable search
+ *
+ * Sets whether to enable search in tabs for @self.
+ *
+ * Search matches tab titles and tooltips, as well as keywords, set via
+ * [property@TabPage:keyword]. Use keywords to search in e.g. page URLs in a web
+ * browser.
+ *
+ * During search, tab reordering and drag-n-drop are disabled.
+ *
+ * Use [property@TabOverview:search-active] to check out if search is currently
+ * active.
+ *
+ * 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 whether search is currently active for @self.
+ *
+ * See [property@TabOverview:enable-search].
+ *
+ * Returns: whether search is active
+ *
+ * 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 whether to new tab button is enabled for @self.
+ *
+ * Returns: whether new tab button is enabled
+ *
+ * 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: whether to enable new tab button
+ *
+ * Sets whether to enable new tab button for @self.
+ *
+ * Connect to the [signal@TabOverview::create-tab] signal to use it.
+ *
+ * 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_secondary_menu: (attributes org.gtk.Method.get_property=secondary-menu)
+ * @self: a tab overview
+ *
+ * Gets the secondary menu model for @self.
+ *
+ * Returns: (transfer none) (nullable): the secondary menu model
+ *
+ * Since: 1.3
+ */
+GMenuModel *
+adw_tab_overview_get_secondary_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->secondary_menu_button));
+}
+
+/**
+ * adw_tab_overview_set_secondary_menu: (attributes org.gtk.Method.set_property=secondary-menu)
+ * @self: a tab overview
+ * @secondary_menu: (nullable): a menu model
+ *
+ * Sets the secondary menu model for @self.
+ *
+ * Use it to add extra actions, e.g. to open a new window or undo closed tab.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_secondary_menu (AdwTabOverview *self,
+ GMenuModel *secondary_menu)
+{
+ g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+ g_return_if_fail (secondary_menu == NULL || G_IS_MENU_MODEL (secondary_menu));
+
+ if (secondary_menu == adw_tab_overview_get_secondary_menu (self))
+ return;
+
+ gtk_menu_button_set_menu_model (GTK_MENU_BUTTON (self->secondary_menu_button),
+ secondary_menu);
+ gtk_widget_set_visible (self->secondary_menu_button, !!secondary_menu);
+ update_header_bar (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SECONDARY_MENU]);
+}
+
+/**
+ * adw_tab_overview_get_show_start_title_buttons: (attributes
org.gtk.Method.get_property=show-start-title-buttons)
+ * @self: a tab overview
+ *
+ * Gets whether start title buttons are shown in @self's header bar.
+ *
+ * Returns: whether start title buttons are shown
+ *
+ * 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: whether to show start title buttons
+ *
+ * Sets whether to show start title buttons in @self's header bar.
+ *
+ * See [property@HeaderBar:show-end-title-buttons] for the other side.
+ *
+ * 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 whether end title buttons are shown in @self's header bar.
+ *
+ * Returns: whether end title buttons are shown
+ *
+ * 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: whether to show end title buttons
+ *
+ * Sets whether to show end title buttons in @self's header bar.
+ *
+ * See [property@HeaderBar:show-start-title-buttons] for the other side.
+ *
+ * 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..dd303ca7
--- /dev/null
+++ b/src/adw-tab-overview.h
@@ -0,0 +1,93 @@
+/*
+ * Copyright (C) 2021-2022 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_3
+G_DECLARE_FINAL_TYPE (AdwTabOverview, adw_tab_overview, ADW, TAB_OVERVIEW, GtkWidget)
+
+ADW_AVAILABLE_IN_1_3
+GtkWidget *adw_tab_overview_new (void) G_GNUC_WARN_UNUSED_RESULT;
+
+ADW_AVAILABLE_IN_1_3
+AdwTabView *adw_tab_overview_get_view (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_3
+void adw_tab_overview_set_view (AdwTabOverview *self,
+ AdwTabView *view);
+
+ADW_AVAILABLE_IN_1_3
+GtkWidget *adw_tab_overview_get_child (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_3
+void adw_tab_overview_set_child (AdwTabOverview *self,
+ GtkWidget *child);
+
+ADW_AVAILABLE_IN_1_3
+gboolean adw_tab_overview_get_open (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_3
+void adw_tab_overview_set_open (AdwTabOverview *self,
+ gboolean open);
+
+ADW_AVAILABLE_IN_1_3
+gboolean adw_tab_overview_get_inverted (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_3
+void adw_tab_overview_set_inverted (AdwTabOverview *self,
+ gboolean inverted);
+
+ADW_AVAILABLE_IN_1_3
+gboolean adw_tab_overview_get_enable_search (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_3
+void adw_tab_overview_set_enable_search (AdwTabOverview *self,
+ gboolean enable_search);
+
+ADW_AVAILABLE_IN_1_3
+gboolean adw_tab_overview_get_search_active (AdwTabOverview *self);
+
+ADW_AVAILABLE_IN_1_3
+gboolean adw_tab_overview_get_enable_new_tab (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_3
+void adw_tab_overview_set_enable_new_tab (AdwTabOverview *self,
+ gboolean enable_new_tab);
+
+ADW_AVAILABLE_IN_1_3
+GMenuModel *adw_tab_overview_get_secondary_menu (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_3
+void adw_tab_overview_set_secondary_menu (AdwTabOverview *self,
+ GMenuModel *secondary_menu);
+
+ADW_AVAILABLE_IN_1_3
+gboolean adw_tab_overview_get_show_start_title_buttons (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_3
+void adw_tab_overview_set_show_start_title_buttons (AdwTabOverview *self,
+ gboolean show_start_title_buttons);
+
+ADW_AVAILABLE_IN_1_3
+gboolean adw_tab_overview_get_show_end_title_buttons (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_3
+void adw_tab_overview_set_show_end_title_buttons (AdwTabOverview *self,
+ gboolean show_end_title_buttons);
+
+ADW_AVAILABLE_IN_1_3
+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..30e35410
--- /dev/null
+++ b/src/adw-tab-overview.ui
@@ -0,0 +1,147 @@
+<?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"/>
+ </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="background"/>
+ <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="secondary_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="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>
+ <child type="overlay">
+ <object class="AdwStatusPage" id="empty_state">
+ <property name="visible">False</property>
+ <property name="can-target">False</property>
+ <property name="can-focus">False</property>
+ <property name="icon-name">view-grid-symbolic</property>
+ <property name="title" translatable="yes">No Open Tabs</property>
+ <layout>
+ <property name="measure">True</property>
+ <property name="clip-overlay">True</property>
+ </layout>
+ </object>
+ </child>
+ <child type="overlay">
+ <object class="AdwStatusPage" id="search_empty_state">
+ <property name="visible">False</property>
+ <property name="can-target">False</property>
+ <property name="can-focus">False</property>
+ <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>
+ <layout>
+ <property name="measure">True</property>
+ <property name="clip-overlay">True</property>
+ </layout>
+ </object>
+ </child>
+ <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="clicked" handler="new_tab_clicked_cb" swapped="yes"/>
+ <layout>
+ <property name="measure">True</property>
+ <property name="clip-overlay">True</property>
+ </layout>
+ <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..95266e2b
--- /dev/null
+++ b/src/adw-tab-thumbnail-private.h
@@ -0,0 +1,46 @@
+/*
+ * 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 <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);
+void adw_tab_thumbnail_fade_in (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..bcf0cee5
--- /dev/null
+++ b/src/adw-tab-thumbnail.c
@@ -0,0 +1,678 @@
+/*
+ * Copyright (C) 2022 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 *needs_attention_revealer;
+ 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);
+ gtk_widget_set_opacity (self->needs_attention_revealer, 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, needs_attention_revealer);
+ 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_reset (self->fade_animation);
+}
+
+void
+adw_tab_thumbnail_fade_in (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..b7f6c487
--- /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" id="needs_attention_revealer">
+ <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..e26b4f2b 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);
@@ -27,4 +29,7 @@ void adw_tab_view_attach_page (AdwTabView *self,
AdwTabView *adw_tab_view_create_window (AdwTabView *self) G_GNUC_WARN_UNUSED_RESULT;
+void adw_tab_view_open_overview (AdwTabView *self);
+void adw_tab_view_close_overview (AdwTabView *self);
+
G_END_DECLS
diff --git a/src/adw-tab-view.c b/src/adw-tab-view.c
index 8470291d..ccabd2e8 100644
--- a/src/adw-tab-view.c
+++ b/src/adw-tab-view.c
@@ -13,11 +13,17 @@
#include "adw-bin.h"
#include "adw-gizmo-private.h"
#include "adw-macros-private.h"
+#include "adw-style-manager.h"
#include "adw-widget-utils-private.h"
/* FIXME replace with groups */
static GSList *tab_view_list;
+#define MIN_ASPECT_RATIO 0.8
+#define MAX_ASPECT_RATIO 2.7
+#define DEFAULT_ICON_ALPHA_HC 0.3
+#define DEFAULT_ICON_ALPHA 0.15
+
/**
* AdwTabView:
*
@@ -26,7 +32,7 @@ static GSList *tab_view_list;
* `AdwTabView` is a container which shows one child at a time. While it
* provides keyboard shortcuts for switching between pages, it does not provide
* a visible tab switcher and relies on external widgets for that, such as
- * [class@TabBar] and [class@TabButton].
+ * [class@TabBar], [class@TabOverview] and [class@TabButton].
*
* `AdwTabView` maintains a [class@TabPage] object for each page, which holds
* additional per-page properties. You can obtain the `AdwTabPage` for a page
@@ -129,11 +135,18 @@ struct _AdwTabPage
char *indicator_tooltip;
gboolean indicator_activatable;
gboolean needs_attention;
+ char *keyword;
+ float thumbnail_xalign;
+ float thumbnail_yalign;
GtkWidget *last_focus;
GBinding *transfer_binding;
gboolean closing;
+ GdkPaintable *paintable;
+
+ gboolean live_thumbnail;
+ gboolean invalidated;
};
G_DEFINE_FINAL_TYPE (AdwTabPage, adw_tab_page, G_TYPE_OBJECT)
@@ -152,6 +165,10 @@ enum {
PAGE_PROP_INDICATOR_TOOLTIP,
PAGE_PROP_INDICATOR_ACTIVATABLE,
PAGE_PROP_NEEDS_ATTENTION,
+ PAGE_PROP_KEYWORD,
+ PAGE_PROP_THUMBNAIL_XALIGN,
+ PAGE_PROP_THUMBNAIL_YALIGN,
+ PAGE_PROP_LIVE_THUMBNAIL,
LAST_PAGE_PROP
};
@@ -171,6 +188,8 @@ struct _AdwTabView
AdwTabViewShortcuts shortcuts;
int transfer_count;
+ int overview_count;
+ gulong unmap_extra_pages_cb;
GtkSelectionModel *pages;
};
@@ -210,6 +229,16 @@ enum {
static guint signals[SIGNAL_LAST_SIGNAL];
+static gboolean
+page_should_be_visible (AdwTabView *view,
+ AdwTabPage *page)
+{
+ if (!view->overview_count)
+ return FALSE;
+
+ return page->live_thumbnail || page->invalidated;
+}
+
static void
set_page_selected (AdwTabPage *self,
gboolean selected)
@@ -283,6 +312,33 @@ set_page_parent (AdwTabPage *self,
g_object_notify_by_pspec (G_OBJECT (self), props[PAGE_PROP_PARENT]);
}
+static void
+map_or_unmap_page (AdwTabPage *self)
+{
+ GtkWidget *parent;
+ AdwTabView *view;
+ gboolean should_be_visible;
+
+ parent = gtk_widget_get_parent (self->bin);
+
+ if (!ADW_IS_TAB_VIEW (parent))
+ return;
+
+ view = ADW_TAB_VIEW (parent);
+
+ if (!view->overview_count)
+ return;
+
+ should_be_visible = self == view->selected_page ||
+ page_should_be_visible (view, self);
+
+ if (gtk_widget_get_child_visible (self->bin) == should_be_visible)
+ return;
+
+ gtk_widget_set_child_visible (self->bin, should_be_visible);
+ gtk_widget_queue_allocate (parent);
+}
+
static void
adw_tab_page_dispose (GObject *object)
{
@@ -291,6 +347,7 @@ adw_tab_page_dispose (GObject *object)
set_page_parent (self, NULL);
g_clear_object (&self->bin);
+ g_clear_object (&self->paintable);
G_OBJECT_CLASS (adw_tab_page_parent_class)->dispose (object);
}
@@ -306,6 +363,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);
if (self->last_focus)
g_object_remove_weak_pointer (G_OBJECT (self->last_focus),
@@ -371,6 +429,22 @@ 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;
+
+ case PAGE_PROP_LIVE_THUMBNAIL:
+ g_value_set_boolean (value, adw_tab_page_get_live_thumbnail (self));
+ break;
+
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
@@ -426,6 +500,22 @@ 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;
+
+ case PAGE_PROP_LIVE_THUMBNAIL:
+ adw_tab_page_set_live_thumbnail (self, g_value_get_boolean (value));
+ break;
+
default:
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
}
@@ -501,6 +591,10 @@ adw_tab_page_class_init (AdwTabPageClass *klass)
* [class@TabBar] will display it in the center of the tab unless it's pinned,
* and will use it as a tooltip unless [property@TabPage:tooltip] is set.
*
+ * [class@TabOverview] will display it below the thumbnail unless it's pinned,
+ * or inside the card otherwise, and will use it as a tooltip unless
+ * [property@TabPage:tooltip] is set.
+ *
* Since: 1.0
*/
page_props[PAGE_PROP_TITLE] =
@@ -515,8 +609,8 @@ adw_tab_page_class_init (AdwTabPageClass *klass)
*
* The tooltip can be marked up with the Pango text markup language.
*
- * If not set, [class@TabBar] will use [property@TabPage:title] as a tooltip
- * instead.
+ * If not set, [class@TabBar] and [class@TabOverview] will use
+ * [property@TabPage:title] as a tooltip instead.
*
* Since: 1.0
*/
@@ -530,10 +624,11 @@ adw_tab_page_class_init (AdwTabPageClass *klass)
*
* The icon of the page.
*
- * [class@TabBar] displays the icon next to the title.
+ * [class@TabBar] and [class@TabOverview] display the icon next to the title,
+ * unless [property@TabPage:loading] is set to `TRUE`.
*
- * It will not show the icon if [property@TabPage:loading] is set to `TRUE`,
- * or if the page is pinned and [propertyTabPage:indicator-icon] is set.
+ * `AdwTabBar` also won't show the icon if the page is pinned and
+ * [propertyTabPage:indicator-icon] is set.
*
* Since: 1.0
*/
@@ -547,10 +642,11 @@ adw_tab_page_class_init (AdwTabPageClass *klass)
*
* Whether the page is loading.
*
- * If set to `TRUE`, [class@TabBar] will display a spinner in place of icon.
+ * If set to `TRUE`, [class@TabBar] and [class@TabOverview] will display a
+ * spinner in place of icon.
*
- * If the page is pinned and [property@TabPage:indicator-icon] is set, the
- * loading status will not be visible.
+ * If the page is pinned and [property@TabPage:indicator-icon] is set,
+ * loading status will not be visible with `AdwTabBar`.
*
* Since: 1.0
*/
@@ -572,6 +668,8 @@ adw_tab_page_class_init (AdwTabPageClass *klass)
* If the page is pinned, the indicator will be shown instead of icon or
* spinner.
*
+ * [class@TabOverview] will show it at the at the top part of the thumbnail.
+ *
* [property@TabPage:indicator-tooltip] can be used to set the tooltip on the
* indicator icon.
*
@@ -627,6 +725,9 @@ adw_tab_page_class_init (AdwTabPageClass *klass)
* set to `TRUE`. If the tab is not visible, the corresponding edge of the tab
* bar will be highlighted.
*
+ * [class@TabOverview] will display a dot in the corner of the thumbnail if set
+ * to `TRUE`.
+ *
* [class@TabButton] will display a dot if any of the pages that aren't
* selected have this property set to `TRUE`.
*
@@ -637,6 +738,87 @@ 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)
+ *
+ * The search keyboard of the page.
+ *
+ * [class@TabOverview] can search pages by their keywords in addition to their
+ * titles and tooltips.
+ *
+ * Keywords allow to include e.g. page URLs into tab search in a web browser.
+ *
+ * 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)
+ *
+ * The horizontal alignment of the page thumbnail.
+ *
+ * If the page is so wide that [class@TabOverview] can't display it completely
+ * and has to crop it, horizontal alignment will determine which part of the
+ * page will be visible.
+ *
+ * For example, 0.5 means the center of the page will be visible, 0 means the
+ * start edge will be visible and 1 means the end edge will be visible.
+ *
+ * The default horizontal alignment is 0.
+ *
+ * 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)
+ *
+ * The vertical alignment of the page thumbnail.
+ *
+ * If the page is so tall that [class@TabOverview] can't display it completely
+ * and has to crop it, vertical alignment will determine which part of the
+ * page will be visible.
+ *
+ * For example, 0.5 means the center of the page will be visible, 0 means the
+ * top edge will be visible and 1 means the bottom edge will be visible.
+ *
+ * The default vertical alignment is 0.
+ *
+ * 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);
+
+ /**
+ * AdwTabPage:live-thumbnail: (attributes org.gtk.Property.get=adw_tab_page_get_live_thumbnail
org.gtk.Property.set=adw_tab_page_set_live_thumbnail)
+ *
+ * Whether to enable live thumbnail for this page.
+ *
+ * When set to `TRUE`, the page's thumbnail in [class@TabOverview] will update
+ * immediately when the page is redrawn or resized.
+ *
+ * If it's set to `FALSE`, the thumbnail will only be live when the page is
+ * selected, and otherwise it will be static and will only update when
+ * [method@TabPage.invalidate_thumbnail] or
+ * [method@TabView.invalidate_thumbnails] is called.
+ *
+ * Since: 1.3
+ */
+ page_props[PAGE_PROP_LIVE_THUMBNAIL] =
+ g_param_spec_boolean ("live-thumbnail", NULL, NULL,
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
g_object_class_install_properties (object_class, LAST_PAGE_PROP, page_props);
}
@@ -646,9 +828,379 @@ 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;
self->bin = g_object_ref_sink (adw_bin_new ());
}
+#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 *child_paintable;
+
+ GdkPaintable *cached_paintable;
+ double cached_aspect_ratio;
+
+ gboolean frozen;
+
+ double last_xalign;
+ double last_yalign;
+ GdkRGBA last_bg_color;
+};
+
+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, "thumbnail_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->frozen &&
+ self->page->bin &&
+ gtk_widget_get_mapped (self->page->bin))
+ g_clear_object (&self->cached_paintable);
+}
+
+static double
+get_unclamped_aspect_ratio (AdwTabPaintable *self)
+{
+ if (self->frozen || !self->view_paintable)
+ return self->cached_aspect_ratio;
+
+ return gdk_paintable_get_intrinsic_aspect_ratio (self->view_paintable);
+}
+
+static void
+child_unmap_cb (AdwTabPaintable *self)
+{
+ if (self->frozen)
+ return;
+
+ g_clear_object (&self->cached_paintable);
+ self->cached_paintable = gdk_paintable_get_current_image (self->child_paintable);
+ self->cached_aspect_ratio = get_unclamped_aspect_ratio (self);
+}
+
+static void
+connect_to_view (AdwTabPaintable *self)
+{
+ if (self->view || !gtk_widget_get_parent (self->page->bin))
+ return;
+
+ self->view = gtk_widget_get_parent (self->page->bin);
+ self->view_paintable = gtk_widget_paintable_new (self->view);
+
+ 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 = get_unclamped_aspect_ratio (self);
+
+ return CLAMP (ratio, MIN_ASPECT_RATIO, MAX_ASPECT_RATIO);
+}
+
+static void
+snapshot_default_icon (GtkSnapshot *snapshot,
+ double width,
+ double height,
+ GtkWidget *view)
+{
+ GdkDisplay *display;
+ GtkIconTheme *icon_theme;
+ GIcon *default_icon;
+ GtkIconPaintable *icon;
+ GtkStyleContext *context;
+ GdkRGBA colors[4];
+ double x, y;
+ double view_width, view_height;
+ double view_ratio, snapshot_ratio;
+ double icon_size;
+ gboolean hc;
+
+ view_width = gtk_widget_get_width (view);
+ view_height = gtk_widget_get_height (view);
+
+ view_ratio = view_width / view_height;
+ snapshot_ratio = width / height;
+
+ if (view_ratio > snapshot_ratio) {
+ double new_width = height * view_ratio;
+
+ gtk_snapshot_translate (snapshot,
+ &GRAPHENE_POINT_INIT ((float) (width - new_width) / 2, 0));
+
+ width = new_width;
+ } else if (view_ratio < snapshot_ratio) {
+ double new_height = width / view_ratio;
+
+ gtk_snapshot_translate (snapshot,
+ &GRAPHENE_POINT_INIT (0, (float) (height - new_height) / 2));
+
+ height = new_height;
+ }
+
+ icon_size = MIN (view_width / 4, view_height / 4);
+
+ display = gtk_widget_get_display (view);
+ icon_theme = gtk_icon_theme_get_for_display (display);
+ default_icon = adw_tab_view_get_default_icon (ADW_TAB_VIEW (view));
+ icon = gtk_icon_theme_lookup_by_gicon (icon_theme, default_icon, icon_size,
+ gtk_widget_get_scale_factor (view),
+ gtk_widget_get_direction (view),
+ GTK_ICON_LOOKUP_FORCE_SYMBOLIC);
+
+ context = gtk_widget_get_style_context (view);
+ gtk_style_context_get_color (context, &colors[GTK_SYMBOLIC_COLOR_FOREGROUND]);
+ gtk_style_context_lookup_color (context, "error-color", &colors[GTK_SYMBOLIC_COLOR_ERROR]);
+ gtk_style_context_lookup_color (context, "warning-color", &colors[GTK_SYMBOLIC_COLOR_WARNING]);
+ gtk_style_context_lookup_color (context, "success-color", &colors[GTK_SYMBOLIC_COLOR_SUCCESS]);
+
+ hc = adw_style_manager_get_high_contrast (adw_style_manager_get_for_display (display));
+
+ gtk_snapshot_push_opacity (snapshot, hc ? DEFAULT_ICON_ALPHA_HC : DEFAULT_ICON_ALPHA);
+
+ gtk_snapshot_scale (snapshot, width / view_width, height / view_height);
+
+ x = (view_width - icon_size) / 2;
+ y = (view_height - icon_size) / 2;
+ gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (x, y));
+
+ gtk_symbolic_paintable_snapshot_symbolic (GTK_SYMBOLIC_PAINTABLE (icon),
+ snapshot,
+ icon_size,
+ icon_size,
+ colors,
+ 4);
+
+ gtk_snapshot_pop (snapshot);
+}
+
+static void
+snapshot_paintable (GtkSnapshot *snapshot,
+ double width,
+ double height,
+ GdkPaintable *paintable,
+ double paintable_ratio,
+ double xalign,
+ double yalign)
+{
+ double snapshot_ratio = width / height;
+
+ 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;
+ } else 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->frozen) {
+ xalign = self->last_xalign;
+ yalign = self->last_yalign;
+ child = NULL;
+ } else {
+ xalign = adw_tab_page_get_thumbnail_xalign (self->page);
+ yalign = adw_tab_page_get_thumbnail_yalign (self->page);
+ child = self->page->bin;
+
+ if (gtk_widget_get_direction (child) == GTK_TEXT_DIR_RTL)
+ xalign = 1 - xalign;
+ }
+
+ if (self->cached_paintable) {
+ snapshot_paintable (GTK_SNAPSHOT (snapshot), width, height,
+ self->cached_paintable, self->cached_aspect_ratio,
+ xalign, yalign);
+ return;
+ }
+
+ if (child && gtk_widget_get_mapped (child)) {
+ double aspect_ratio = get_unclamped_aspect_ratio (self);
+
+ snapshot_paintable (GTK_SNAPSHOT (snapshot), width, height,
+ self->child_paintable, aspect_ratio,
+ xalign, yalign);
+ return;
+ }
+
+ if (self->frozen)
+ bg = self->last_bg_color;
+ else
+ get_background_color (self, &bg);
+
+ gtk_snapshot_append_color (GTK_SNAPSHOT (snapshot), &bg,
+ &GRAPHENE_RECT_INIT (0, 0, width, height));
+
+ if (self->view)
+ snapshot_default_icon (snapshot, width, height, self->view);
+}
+
+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->child_paintable);
+ g_clear_object (&self->cached_paintable);
+
+ G_OBJECT_CLASS (adw_tab_paintable_parent_class)->dispose (object);
+}
+
+static void
+adw_tab_paintable_class_init (AdwTabPaintableClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = adw_tab_paintable_dispose;
+}
+
+static void
+adw_tab_paintable_init (AdwTabPaintable *self)
+{
+}
+
+static GdkPaintable *
+adw_tab_paintable_new (AdwTabPage *page)
+{
+ AdwTabPaintable *self = g_object_new (ADW_TYPE_TAB_PAINTABLE, NULL);
+
+ self->page = page;
+
+ connect_to_view (self);
+
+ self->child_paintable = gtk_widget_paintable_new (page->bin);
+
+ g_signal_connect_swapped (self->child_paintable, "invalidate-contents",
+ G_CALLBACK (invalidate_contents_and_clear_cache), 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 (page->bin, "notify::parent",
+ G_CALLBACK (child_parent_changed), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (page->bin, "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->last_xalign = adw_tab_page_get_thumbnail_xalign (self->page);
+ self->last_yalign = adw_tab_page_get_thumbnail_yalign (self->page);
+ get_background_color (self, &self->last_bg_color);
+
+ if (gtk_widget_get_direction (self->page->bin) == GTK_TEXT_DIR_RTL)
+ self->last_xalign = 1 - self->last_xalign;
+
+ self->frozen = TRUE;
+
+ g_clear_object (&self->child_paintable);
+}
+
#define ADW_TYPE_TAB_PAGES (adw_tab_pages_get_type ())
G_DECLARE_FINAL_TYPE (AdwTabPages, adw_tab_pages, ADW, TAB_PAGES, GObject)
@@ -867,7 +1419,8 @@ attach_page (AdwTabView *self,
g_list_store_insert (self->children, position, page);
- gtk_widget_set_child_visible (page->bin, FALSE);
+ gtk_widget_set_child_visible (page->bin,
+ page_should_be_visible (self, page));
gtk_widget_set_parent (page->bin, GTK_WIDGET (self));
page->transfer_binding =
g_object_bind_property (self, "is-transferring-page",
@@ -928,7 +1481,8 @@ set_selected_page (AdwTabView *self,
}
if (self->selected_page->bin)
- gtk_widget_set_child_visible (self->selected_page->bin, FALSE);
+ gtk_widget_set_child_visible (self->selected_page->bin,
+ page_should_be_visible (self, self->selected_page));
set_page_selected (self->selected_page, FALSE);
}
@@ -1387,11 +1941,80 @@ adw_tab_view_size_allocate (GtkWidget *widget,
int baseline)
{
AdwTabView *self = ADW_TAB_VIEW (widget);
+ int i;
- if (!self->selected_page)
- return;
+ for (i = 0; i < self->n_pages; i++) {
+ AdwTabPage *page = adw_tab_view_get_nth_page (self, i);
- gtk_widget_allocate (self->selected_page->bin, width, height, baseline, NULL);
+ if (gtk_widget_get_child_visible (page->bin))
+ gtk_widget_allocate (page->bin, width, height, baseline, NULL);
+ }
+}
+
+static gboolean
+unmap_extra_pages (AdwTabView *self)
+{
+ int i;
+
+ for (i = 0; i < self->n_pages; i++) {
+ AdwTabPage *page = adw_tab_view_get_nth_page (self, i);
+
+ if (page == self->selected_page)
+ continue;
+
+ if (!gtk_widget_get_child_visible (page->bin))
+ continue;
+
+ if (page_should_be_visible (self, page))
+ continue;
+
+ gtk_widget_set_child_visible (page->bin, FALSE);
+ }
+
+ self->unmap_extra_pages_cb = 0;
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+adw_tab_view_snapshot (GtkWidget *widget,
+ GtkSnapshot *snapshot)
+{
+ AdwTabView *self = ADW_TAB_VIEW (widget);
+ int i;
+
+ if (self->selected_page)
+ gtk_widget_snapshot_child (widget, self->selected_page->bin, snapshot);
+
+ for (i = 0; i < self->n_pages; i++) {
+ AdwTabPage *page = adw_tab_view_get_nth_page (self, i);
+
+ if (page == self->selected_page) {
+ page->invalidated = FALSE;
+ continue;
+ }
+
+ if (!gtk_widget_get_child_visible (page->bin))
+ continue;
+
+ if (page->paintable) {
+ /* We don't want to actually draw the child, but we do need it
+ * to redraw so that it can be displayed by its paintable */
+ GtkSnapshot *child_snapshot = gtk_snapshot_new ();
+
+ gtk_widget_snapshot_child (widget, page->bin, child_snapshot);
+
+ child_unmap_cb (ADW_TAB_PAINTABLE (page->paintable));
+
+ g_object_unref (child_snapshot);
+ }
+
+ page->invalidated = FALSE;
+
+ if (!self->unmap_extra_pages_cb)
+ self->unmap_extra_pages_cb =
+ g_idle_add ((GSourceFunc) unmap_extra_pages, self);
+ }
}
static void
@@ -1399,6 +2022,11 @@ adw_tab_view_dispose (GObject *object)
{
AdwTabView *self = ADW_TAB_VIEW (object);
+ if (self->unmap_extra_pages_cb) {
+ g_source_remove (self->unmap_extra_pages_cb);
+ self->unmap_extra_pages_cb = 0;
+ }
+
if (self->pages)
g_list_model_items_changed (G_LIST_MODEL (self->pages), 0, self->n_pages, 0);
@@ -1519,6 +2147,7 @@ adw_tab_view_class_init (AdwTabViewClass *klass)
widget_class->measure = adw_tab_view_measure;
widget_class->size_allocate = adw_tab_view_size_allocate;
+ widget_class->snapshot = adw_tab_view_snapshot;
widget_class->get_request_mode = adw_widget_get_request_mode;
widget_class->compute_expand = adw_widget_compute_expand;
@@ -1591,6 +2220,9 @@ adw_tab_view_class_init (AdwTabViewClass *klass)
* not loading, doesn't have an icon and an indicator. Default icon is never
* used for tabs that aren't pinned.
*
+ * [class@TabOverview] will use default icon for pages with missing
+ * thumbnails.
+ *
* By default, the `adw-tab-icon-missing-symbolic` icon is used.
*
* Since: 1.0
@@ -1995,6 +2627,10 @@ adw_tab_page_get_title (AdwTabPage *self)
* [class@TabBar] will display it in the center of the tab unless it's pinned,
* and will use it as a tooltip unless [property@TabPage:tooltip] is set.
*
+ * [class@TabOverview] will display it below the thumbnail unless it's pinned,
+ * or inside the card otherwise, and will use it as a tooltip unless
+ * [property@TabPage:tooltip] is set.
+ *
* Sets the title of @self.
*
* Since: 1.0
@@ -2041,8 +2677,8 @@ adw_tab_page_get_tooltip (AdwTabPage *self)
*
* The tooltip can be marked up with the Pango text markup language.
*
- * If not set, [class@TabBar] will use [property@TabPage:title] as a tooltip
- * instead.
+ * If not set, [class@TabBar] and [class@TabOverview] will use
+ * [property@TabPage:title] as a tooltip instead.
*
* Since: 1.0
*/
@@ -2086,10 +2722,11 @@ adw_tab_page_get_icon (AdwTabPage *self)
*
* Sets the icon of @self.
*
- * [class@TabBar] displays the icon next to the title.
+ * [class@TabBar] and [class@TabOverview] display the icon next to the title,
+ * unless [property@TabPage:loading] is set to `TRUE`.
*
- * It will not show the icon if [property@TabPage:loading] is set to `TRUE`,
- * or if the page is pinned and [propertyTabPage:indicator-icon] is set.
+ * `AdwTabBar` also won't show the icon if the page is pinned and
+ * [propertyTabPage:indicator-icon] is set.
*
* Since: 1.0
*/
@@ -2133,10 +2770,11 @@ adw_tab_page_get_loading (AdwTabPage *self)
*
* Sets whether @self is loading.
*
- * If set to `TRUE`, [class@TabBar] will display a spinner in place of icon.
+ * If set to `TRUE`, [class@TabBar] and [class@TabOverview] will display a
+ * spinner in place of icon.
*
- * If the page is pinned and [property@TabPage:indicator-icon] is set, the
- * loading status will not be visible.
+ * If the page is pinned and [property@TabPage:indicator-icon] is set, loading
+ * status will not be visible with `AdwTabBar`.
*
* Since: 1.0
*/
@@ -2189,6 +2827,8 @@ adw_tab_page_get_indicator_icon (AdwTabPage *self)
* If the page is pinned, the indicator will be shown instead of icon or
* spinner.
*
+ * [class@TabOverview] will show it at the at the top part of the thumbnail.
+ *
* [property@TabPage:indicator-tooltip] can be used to set the tooltip on the
* indicator icon.
*
@@ -2337,6 +2977,9 @@ adw_tab_page_get_needs_attention (AdwTabPage *self)
* set to `TRUE`. If the tab is not visible, the corresponding edge of the tab
* bar will be highlighted.
*
+ * [class@TabOverview] will display a dot in the corner of the thumbnail if set
+ * to `TRUE`.
+ *
* [class@TabButton] will display a dot if any of the pages that aren't
* selected have [property@TabPage:needs-attention] set to `TRUE`.
*
@@ -2358,6 +3001,247 @@ 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 search keyword of @self.
+ *
+ * Returns: (nullable): the search keyword 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 search keyword
+ *
+ * Sets the search keyword for @self.
+ *
+ * [class@TabOverview] can search pages by their keywords in addition to their
+ * titles and tooltips.
+ *
+ * Keywords allow to include e.g. page URLs into tab search in a web browser.
+ *
+ * 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 the horizontal alignment of the thumbnail for @self.
+ *
+ * Returns: the horizontal alignment
+ *
+ * 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: the new value
+ *
+ * Sets the horizontal alignment of the thumbnail for @self.
+ *
+ * If the page is so wide that [class@TabOverview] can't display it completely
+ * and has to crop it, horizontal alignment will determine which part of the
+ * page will be visible.
+ *
+ * For example, 0.5 means the center of the page will be visible, 0 means the
+ * start edge will be visible and 1 means the end edge will be visible.
+ *
+ * The default horizontal alignment is 0.
+ *
+ * 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 the vertical alignment of the thumbnail for @self.
+ *
+ * Returns: the vertical alignment
+ *
+ * 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
+ * @yalign: the new value
+ *
+ * Sets the vertical alignment of the thumbnail for @self.
+ *
+ * If the page is so tall that [class@TabOverview] can't display it completely
+ * and has to crop it, vertical alignment will determine which part of the page
+ * will be visible.
+ *
+ * For example, 0.5 means the center of the page will be visible, 0 means the
+ * top edge will be visible and 1 means the bottom edge will be visible.
+ *
+ * The default vertical alignment is 0.
+ *
+ * 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]);
+}
+
+/**
+ * adw_tab_page_get_live_thumbnail: (attributes org.gtk.Method.get_property=live-thumbnail)
+ * @self: a tab overview
+ *
+ * Gets whether to live thumbnail is enabled @self.
+ *
+ * Returns: whether live thumbnail is enabled
+ *
+ * Since: 1.3
+ */
+gboolean
+adw_tab_page_get_live_thumbnail (AdwTabPage *self)
+{
+ g_return_val_if_fail (ADW_IS_TAB_PAGE (self), FALSE);
+
+ return self->live_thumbnail;
+}
+
+/**
+ * adw_tab_page_set_live_thumbnail: (attributes org.gtk.Method.set_property=live-thumbnail)
+ * @self: a tab page
+ * @live_thumbnail: whether to enable live thumbnail
+ *
+ * Sets whether to enable live thumbnail for @self.
+ *
+ * When set to `TRUE`, @self's thumbnail in [class@TabOverview] will update
+ * immediately when @self is redrawn or resized.
+ *
+ * If it's set to `FALSE`, the thumbnail will only be live when the @self is
+ * selected, and otherwise it will be static and will only update when
+ * [method@TabPage.invalidate_thumbnail] or
+ * [method@TabView.invalidate_thumbnails] is called.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_page_set_live_thumbnail (AdwTabPage *self,
+ gboolean live_thumbnail)
+{
+ g_return_if_fail (ADW_IS_TAB_PAGE (self));
+
+ live_thumbnail = !!live_thumbnail;
+
+ if (self->live_thumbnail == live_thumbnail)
+ return;
+
+ self->live_thumbnail = live_thumbnail;
+
+ map_or_unmap_page (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_LIVE_THUMBNAIL]);
+}
+
+
+/**
+ * adw_tab_page_invalidate_thumbnail:
+ * @self: a tab page
+ *
+ * Invalidates thumbnail for @self.
+ *
+ * If an [class@TabOverview] is open, the thumbnail representing @self will be
+ * immediately updated. Otherwise it will be update when opening the overview.
+ *
+ * Does nothing if [property@TabPage:live-thumbnail] is set to `TRUE`.
+ *
+ * See also [method@TabView.invalidate_thumbnails].
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_page_invalidate_thumbnail (AdwTabPage *self)
+{
+ g_return_if_fail (ADW_IS_TAB_PAGE (self));
+
+ self->invalidated = TRUE;
+
+ map_or_unmap_page (self);
+}
+
+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:
*
@@ -2638,6 +3522,8 @@ adw_tab_view_get_default_icon (AdwTabView *self)
* loading, doesn't have an icon and an indicator. Default icon is never used
* for tabs that aren't pinned.
*
+ * [class@TabOverview] will use default icon for pages with missing thumbnails.
+ *
* By default, the `adw-tab-icon-missing-symbolic` icon is used.
*
* Since: 1.0
@@ -2648,12 +3534,20 @@ adw_tab_view_set_default_icon (AdwTabView *self,
{
g_return_if_fail (ADW_IS_TAB_VIEW (self));
g_return_if_fail (G_IS_ICON (default_icon));
+ int i;
if (self->default_icon == default_icon)
return;
g_set_object (&self->default_icon, default_icon);
+ for (i = 0; i < self->n_pages; i++) {
+ AdwTabPage *page = adw_tab_view_get_nth_page (self, i);
+
+ if (page->paintable)
+ gdk_paintable_invalidate_contents (page->paintable);
+ }
+
g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEFAULT_ICON]);
}
@@ -2820,6 +3714,10 @@ adw_tab_view_remove_shortcuts (AdwTabView *self,
* 3. [property@TabPage:icon]
* 4. [property@TabView:default-icon]
*
+ * [class@TabOverview] will not show a thumbnail for pinned pages, and replace
+ * the close button with an unpin button. Unlike `AdwTabBar`, it will still
+ * display the page's title, icon and indicator separately.
+ *
* Pinned pages cannot be closed by default, see [signal@TabView::close-page]
* for how to override that behavior.
*
@@ -3229,8 +4127,13 @@ adw_tab_view_close_page_finish (AdwTabView *self,
page->closing = FALSE;
- if (confirm)
- detach_page (self, page, FALSE);
+ if (!confirm)
+ return;
+
+ if (page->paintable)
+ adw_tab_paintable_freeze (ADW_TAB_PAINTABLE (page->paintable));
+
+ detach_page (self, page, FALSE);
}
/**
@@ -3605,6 +4508,30 @@ adw_tab_view_get_pages (AdwTabView *self)
return self->pages;
}
+/**
+ * adw_tab_view_invalidate_thumbnails:
+ * @self: a tab view
+ *
+ * Invalidates thumbnails for all pages in @self.
+ *
+ * This is a convenience method, equivalent to calling
+ * [method@TabPage.invalidate_thumbnail] on each page.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_view_invalidate_thumbnails (AdwTabView *self)
+{
+ int i;
+ g_return_if_fail (ADW_IS_TAB_VIEW (self));
+
+ for (i = 0; i < self->n_pages; i++) {
+ AdwTabPage *page = adw_tab_view_get_nth_page (self, i);
+
+ adw_tab_page_invalidate_thumbnail (page);
+ }
+}
+
AdwTabView *
adw_tab_view_create_window (AdwTabView *self)
{
@@ -3622,3 +4549,48 @@ adw_tab_view_create_window (AdwTabView *self)
return new_view;
}
+
+void
+adw_tab_view_open_overview (AdwTabView *self)
+{
+ g_return_if_fail (ADW_IS_TAB_VIEW (self));
+
+ if (self->overview_count == 0) {
+ int i;
+
+ for (i = 0; i < self->n_pages; i++) {
+ AdwTabPage *page = adw_tab_view_get_nth_page (self, i);
+
+ if (page->live_thumbnail || page->invalidated)
+ gtk_widget_set_child_visible (page->bin, TRUE);
+ }
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+ }
+
+ self->overview_count++;
+}
+
+void
+adw_tab_view_close_overview (AdwTabView *self)
+{
+ g_return_if_fail (ADW_IS_TAB_VIEW (self));
+
+ self->overview_count--;
+
+ if (self->overview_count == 0) {
+ int i;
+
+ for (i = 0; i < self->n_pages; i++) {
+ AdwTabPage *page = adw_tab_view_get_nth_page (self, i);
+
+ if (page->live_thumbnail || page->invalidated)
+ gtk_widget_set_child_visible (page->bin,
+ page == self->selected_page);
+ }
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+ }
+
+ g_assert (self->overview_count >= 0);
+}
diff --git a/src/adw-tab-view.h b/src/adw-tab-view.h
index 4c1af2da..d1bf5fc5 100644
--- a/src/adw-tab-view.h
+++ b/src/adw-tab-view.h
@@ -101,6 +101,33 @@ ADW_AVAILABLE_IN_ALL
void adw_tab_page_set_needs_attention (AdwTabPage *self,
gboolean needs_attention);
+ADW_AVAILABLE_IN_1_3
+const char *adw_tab_page_get_keyword (AdwTabPage *self);
+ADW_AVAILABLE_IN_1_3
+void adw_tab_page_set_keyword (AdwTabPage *self,
+ const char *keyword);
+
+ADW_AVAILABLE_IN_1_3
+float adw_tab_page_get_thumbnail_xalign (AdwTabPage *self);
+ADW_AVAILABLE_IN_1_3
+void adw_tab_page_set_thumbnail_xalign (AdwTabPage *self,
+ float xalign);
+
+ADW_AVAILABLE_IN_1_3
+float adw_tab_page_get_thumbnail_yalign (AdwTabPage *self);
+ADW_AVAILABLE_IN_1_3
+void adw_tab_page_set_thumbnail_yalign (AdwTabPage *self,
+ float yalign);
+
+ADW_AVAILABLE_IN_1_3
+gboolean adw_tab_page_get_live_thumbnail (AdwTabPage *self);
+ADW_AVAILABLE_IN_1_3
+void adw_tab_page_set_live_thumbnail (AdwTabPage *self,
+ gboolean live_thumbnail);
+
+ADW_AVAILABLE_IN_1_3
+void adw_tab_page_invalidate_thumbnail (AdwTabPage *self);
+
#define ADW_TYPE_TAB_VIEW (adw_tab_view_get_type())
ADW_AVAILABLE_IN_ALL
@@ -240,4 +267,7 @@ void adw_tab_view_transfer_page (AdwTabView *self,
ADW_AVAILABLE_IN_ALL
GtkSelectionModel *adw_tab_view_get_pages (AdwTabView *self) G_GNUC_WARN_UNUSED_RESULT;
+ADW_AVAILABLE_IN_1_3
+void adw_tab_view_invalidate_thumbnails (AdwTabView *self);
+
G_END_DECLS
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..2a55b168 100644
--- a/src/stylesheet/widgets/_tab-view.scss
+++ b/src/stylesheet/widgets/_tab-view.scss
@@ -139,7 +139,111 @@ 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(8px, -8px); }
+ &:dir(rtl) { transform: translate(-8px, -8px); }
+
+ > widget {
+ background: $accent_color;
+ min-width: 12px;
+ min-height: 12px;
+ border-radius: 8px;
+ margin: 3px;
+ box-shadow: 0 1px 2px gtkalpha($accent_color, .4);
+ }
+ }
+
+ .card {
+ picture {
+ outline: 1px solid $window_outline_color;
+ outline-offset: -1px;
+ border-radius: $card_radius;
+ }
+
+ background: none;
+ color: inherit;
+
+ @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);
+ }
+ }
+
+ &.pinned .card {
+ background-color: $thumbnail_bg_color;
+ color: $thumbnail_fg_color;
+
+ @if $contrast == 'high' {
+ outline: 1px solid $window_outline_color;
+ outline-offset: -1px;
+ }
+ }
+
+ .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;
}
diff --git a/tests/meson.build b/tests/meson.build
index 9bddd07a..e485ddcd 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -54,6 +54,7 @@ test_names = [
'test-style-manager',
'test-tab-bar',
'test-tab-button',
+ 'test-tab-overview',
'test-tab-view',
'test-timed-animation',
'test-toast',
diff --git a/tests/test-tab-overview.c b/tests/test-tab-overview.c
new file mode 100644
index 00000000..bf599623
--- /dev/null
+++ b/tests/test-tab-overview.c
@@ -0,0 +1,329 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#include <adwaita.h>
+
+int notified;
+
+static void
+notify_cb (GtkWidget *widget, gpointer data)
+{
+ notified++;
+}
+
+static void
+test_adw_tab_overview_view (void)
+{
+ AdwTabOverview *overview = g_object_ref_sink (ADW_TAB_OVERVIEW (adw_tab_overview_new ()));
+ AdwTabView *view;
+
+ g_assert_nonnull (overview);
+
+ notified = 0;
+ g_signal_connect (overview, "notify::view", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (overview, "view", &view, NULL);
+ g_assert_null (view);
+
+ adw_tab_overview_set_view (overview, NULL);
+ g_assert_cmpint (notified, ==, 0);
+
+ view = g_object_ref_sink (ADW_TAB_VIEW (adw_tab_view_new ()));
+ adw_tab_overview_set_view (overview, view);
+ g_assert_true (adw_tab_overview_get_view (overview) == view);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (overview, "view", NULL, NULL);
+ g_assert_null (adw_tab_overview_get_view (overview));
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (overview);
+ g_assert_finalize_object (view);
+}
+
+static void
+test_adw_tab_overview_child (void)
+{
+ AdwTabOverview *overview = g_object_ref_sink (ADW_TAB_OVERVIEW (adw_tab_overview_new ()));
+ GtkWidget *widget = NULL;
+
+ g_assert_nonnull (overview);
+
+ notified = 0;
+ g_signal_connect (overview, "notify::child", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (overview, "child", &widget, NULL);
+ g_assert_null (widget);
+
+ adw_tab_overview_set_child (overview, NULL);
+ g_assert_cmpint (notified, ==, 0);
+
+ widget = gtk_button_new ();
+ adw_tab_overview_set_child (overview, widget);
+ g_assert_true (adw_tab_overview_get_child (overview) == widget);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (overview, "child", NULL, NULL);
+ g_assert_null (adw_tab_overview_get_child (overview));
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (overview);
+}
+
+static void
+test_adw_tab_overview_open (void)
+{
+ AdwTabOverview *overview = g_object_ref_sink (ADW_TAB_OVERVIEW (adw_tab_overview_new ()));
+ AdwTabView *view = ADW_TAB_VIEW (adw_tab_view_new ());
+ gboolean open = FALSE;
+
+ g_assert_nonnull (overview);
+ g_assert_nonnull (view);
+
+ adw_tab_view_add_page (view, gtk_button_new (), NULL);
+
+ adw_tab_overview_set_child (overview, GTK_WIDGET (view));
+ adw_tab_overview_set_view (overview, g_object_ref (view));
+
+ notified = 0;
+ g_signal_connect (overview, "notify::open", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (overview, "open", &open, NULL);
+ g_assert_false (open);
+
+ adw_tab_overview_set_open (overview, FALSE);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_tab_overview_set_open (overview, TRUE);
+ g_assert_true (adw_tab_overview_get_open (overview));
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (overview, "open", FALSE, NULL);
+ g_assert_false (adw_tab_overview_get_open (overview));
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (overview);
+ g_assert_finalize_object (view);
+}
+
+static void
+test_adw_tab_overview_inverted (void)
+{
+ AdwTabOverview *overview = g_object_ref_sink (ADW_TAB_OVERVIEW (adw_tab_overview_new ()));
+ gboolean inverted = FALSE;
+
+ g_assert_nonnull (overview);
+
+ notified = 0;
+ g_signal_connect (overview, "notify::inverted", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (overview, "inverted", &inverted, NULL);
+ g_assert_false (inverted);
+
+ adw_tab_overview_set_inverted (overview, FALSE);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_tab_overview_set_inverted (overview, TRUE);
+ g_assert_true (adw_tab_overview_get_inverted (overview));
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (overview, "inverted", FALSE, NULL);
+ g_assert_false (adw_tab_overview_get_inverted (overview));
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (overview);
+}
+
+static void
+test_adw_tab_overview_enable_search (void)
+{
+ AdwTabOverview *overview = g_object_ref_sink (ADW_TAB_OVERVIEW (adw_tab_overview_new ()));
+ gboolean enable_search = FALSE;
+
+ g_assert_nonnull (overview);
+
+ notified = 0;
+ g_signal_connect (overview, "notify::enable-search", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (overview, "enable-search", &enable_search, NULL);
+ g_assert_true (enable_search);
+
+ adw_tab_overview_set_enable_search (overview, TRUE);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_tab_overview_set_enable_search (overview, FALSE);
+ g_assert_false (adw_tab_overview_get_enable_search (overview));
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (overview, "enable-search", TRUE, NULL);
+ g_assert_true (adw_tab_overview_get_enable_search (overview));
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (overview);
+}
+
+static void
+test_adw_tab_overview_enable_new_tab (void)
+{
+ AdwTabOverview *overview = g_object_ref_sink (ADW_TAB_OVERVIEW (adw_tab_overview_new ()));
+ gboolean enable_new_tab = FALSE;
+
+ g_assert_nonnull (overview);
+
+ notified = 0;
+ g_signal_connect (overview, "notify::enable-new-tab", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (overview, "enable-new-tab", &enable_new_tab, NULL);
+ g_assert_false (enable_new_tab);
+
+ adw_tab_overview_set_enable_new_tab (overview, FALSE);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_tab_overview_set_enable_new_tab (overview, TRUE);
+ g_assert_true (adw_tab_overview_get_enable_new_tab (overview));
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (overview, "enable-new-tab", FALSE, NULL);
+ g_assert_false (adw_tab_overview_get_enable_new_tab (overview));
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (overview);
+}
+
+static void
+test_adw_tab_overview_show_start_title_buttons (void)
+{
+ AdwTabOverview *overview = g_object_ref_sink (ADW_TAB_OVERVIEW (adw_tab_overview_new ()));
+ gboolean show_start_title_buttons = FALSE;
+
+ g_assert_nonnull (overview);
+
+ notified = 0;
+ g_signal_connect (overview, "notify::show-start-title-buttons", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (overview, "show-start-title-buttons", &show_start_title_buttons, NULL);
+ g_assert_true (show_start_title_buttons);
+
+ adw_tab_overview_set_show_start_title_buttons (overview, TRUE);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_tab_overview_set_show_start_title_buttons (overview, FALSE);
+ g_assert_false (adw_tab_overview_get_show_start_title_buttons (overview));
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (overview, "show-start-title-buttons", TRUE, NULL);
+ g_assert_true (adw_tab_overview_get_show_start_title_buttons (overview));
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (overview);
+}
+
+static void
+test_adw_tab_overview_show_end_title_buttons (void)
+{
+ AdwTabOverview *overview = g_object_ref_sink (ADW_TAB_OVERVIEW (adw_tab_overview_new ()));
+ gboolean show_end_title_buttons = FALSE;
+
+ g_assert_nonnull (overview);
+
+ notified = 0;
+ g_signal_connect (overview, "notify::show-end-title-buttons", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (overview, "show-end-title-buttons", &show_end_title_buttons, NULL);
+ g_assert_true (show_end_title_buttons);
+
+ adw_tab_overview_set_show_end_title_buttons (overview, TRUE);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_tab_overview_set_show_end_title_buttons (overview, FALSE);
+ g_assert_false (adw_tab_overview_get_show_end_title_buttons (overview));
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (overview, "show-end-title-buttons", TRUE, NULL);
+ g_assert_true (adw_tab_overview_get_show_end_title_buttons (overview));
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (overview);
+}
+
+static void
+test_adw_tab_overview_secondary_menu (void)
+{
+ AdwTabOverview *overview = g_object_ref_sink (ADW_TAB_OVERVIEW (adw_tab_overview_new ()));
+ GMenuModel *model;
+ GMenuModel *model1 = G_MENU_MODEL (g_menu_new ());
+ GMenuModel *model2 = G_MENU_MODEL (g_menu_new ());
+
+ g_assert_nonnull (overview);
+
+ notified = 0;
+ g_signal_connect (overview, "notify::secondary-menu", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (overview, "secondary-menu", &model, NULL);
+ g_assert_null (model);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_tab_overview_set_secondary_menu (overview, model1);
+ g_assert_true (adw_tab_overview_get_secondary_menu (overview) == model1);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (overview, "secondary-menu", model2, NULL);
+ g_assert_true (adw_tab_overview_get_secondary_menu (overview) == model2);
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (overview);
+ g_assert_finalize_object (model1);
+ g_assert_finalize_object (model2);
+}
+
+static void
+test_adw_tab_overview_actions (void)
+{
+ AdwTabOverview *overview = g_object_ref_sink (ADW_TAB_OVERVIEW (adw_tab_overview_new ()));
+ AdwTabView *view = ADW_TAB_VIEW (adw_tab_view_new ());
+
+ g_assert_nonnull (overview);
+ g_assert_nonnull (view);
+
+ adw_tab_view_add_page (view, gtk_button_new (), NULL);
+
+ adw_tab_overview_set_child (overview, GTK_WIDGET (view));
+ adw_tab_overview_set_view (overview, g_object_ref (view));
+
+ gtk_widget_activate_action (GTK_WIDGET (overview), "overview.open", NULL);
+
+ g_assert_true (adw_tab_overview_get_open (overview));
+
+ gtk_widget_activate_action (GTK_WIDGET (overview), "overview.close", NULL);
+
+ g_assert_false (adw_tab_overview_get_open (overview));
+
+ g_assert_finalize_object (overview);
+ g_assert_finalize_object (view);
+}
+
+int
+main (int argc,
+ char *argv[])
+{
+ gtk_test_init (&argc, &argv, NULL);
+ adw_init ();
+
+ g_test_add_func ("/Adwaita/TabOverview/view", test_adw_tab_overview_view);
+ g_test_add_func ("/Adwaita/TabOverview/child", test_adw_tab_overview_child);
+ g_test_add_func ("/Adwaita/TabOverview/open", test_adw_tab_overview_open);
+ g_test_add_func ("/Adwaita/TabOverview/inverted", test_adw_tab_overview_inverted);
+ g_test_add_func ("/Adwaita/TabOverview/enable_search", test_adw_tab_overview_enable_search);
+ g_test_add_func ("/Adwaita/TabOverview/enable_new_tab", test_adw_tab_overview_enable_new_tab);
+ g_test_add_func ("/Adwaita/TabOverview/secondary_menu", test_adw_tab_overview_secondary_menu);
+ g_test_add_func ("/Adwaita/TabOverview/show_start_title_buttons",
test_adw_tab_overview_show_start_title_buttons);
+ g_test_add_func ("/Adwaita/TabOverview/show_end_title_buttons",
test_adw_tab_overview_show_end_title_buttons);
+ g_test_add_func ("/Adwaita/TabOverview/actions", test_adw_tab_overview_actions);
+
+ return g_test_run ();
+}
diff --git a/tests/test-tab-view.c b/tests/test-tab-view.c
index f1a0cc7f..8f775962 100644
--- a/tests/test-tab-view.c
+++ b/tests/test-tab-view.c
@@ -1025,6 +1025,36 @@ test_adw_tab_page_tooltip (void)
g_assert_finalize_object (view);
}
+static void
+test_adw_tab_page_keyword (void)
+{
+ AdwTabView *view = g_object_ref_sink (ADW_TAB_VIEW (adw_tab_view_new ()));
+ AdwTabPage *page;
+ char *keyword;
+
+ g_assert_nonnull (view);
+
+ page = adw_tab_view_append (view, gtk_button_new ());
+ g_assert_nonnull (page);
+
+ notified = 0;
+ g_signal_connect (page, "notify::keyword", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (page, "keyword", &keyword, NULL);
+ g_assert_null (keyword);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_tab_page_set_keyword (page, "Some keyword");
+ g_assert_cmpstr (adw_tab_page_get_keyword (page), ==, "Some keyword");
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (page, "keyword", "Some other keyword", NULL);
+ g_assert_cmpstr (adw_tab_page_get_keyword (page), ==, "Some other keyword");
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (view);
+}
+
static void
test_adw_tab_page_icon (void)
{
@@ -1217,6 +1247,99 @@ test_adw_tab_page_needs_attention (void)
g_assert_finalize_object (view);
}
+static void
+test_adw_tab_page_thumbnail_xalign (void)
+{
+ AdwTabView *view = g_object_ref_sink (ADW_TAB_VIEW (adw_tab_view_new ()));
+ AdwTabPage *page;
+ float xalign;
+
+ g_assert_nonnull (view);
+
+ page = adw_tab_view_append (view, gtk_button_new ());
+ g_assert_nonnull (page);
+
+ notified = 0;
+ g_signal_connect (page, "notify::thumbnail-xalign", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (page, "thumbnail_xalign", &xalign, NULL);
+ g_assert_cmpfloat (xalign, ==, 0);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_tab_page_set_thumbnail_xalign (page, 1);
+ g_object_get (page, "thumbnail-xalign", &xalign, NULL);
+ g_assert_cmpfloat (xalign, ==, 1);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (page, "thumbnail-xalign", 0.5, NULL);
+ g_assert_cmpfloat (adw_tab_page_get_thumbnail_xalign (page), ==, 0.5);
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (view);
+}
+
+static void
+test_adw_tab_page_thumbnail_yalign (void)
+{
+ AdwTabView *view = g_object_ref_sink (ADW_TAB_VIEW (adw_tab_view_new ()));
+ AdwTabPage *page;
+ float yalign;
+
+ g_assert_nonnull (view);
+
+ page = adw_tab_view_append (view, gtk_button_new ());
+ g_assert_nonnull (page);
+
+ notified = 0;
+ g_signal_connect (page, "notify::thumbnail-yalign", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (page, "thumbnail_yalign", &yalign, NULL);
+ g_assert_cmpfloat (yalign, ==, 0);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_tab_page_set_thumbnail_yalign (page, 1);
+ g_object_get (page, "thumbnail-yalign", &yalign, NULL);
+ g_assert_cmpfloat (yalign, ==, 1);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (page, "thumbnail-yalign", 0.5, NULL);
+ g_assert_cmpfloat (adw_tab_page_get_thumbnail_yalign (page), ==, 0.5);
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (view);
+}
+
+static void
+test_adw_tab_page_live_thumbnail (void)
+{
+ AdwTabView *view = g_object_ref_sink (ADW_TAB_VIEW (adw_tab_view_new ()));
+ AdwTabPage *page;
+ gboolean live_thumbnail;
+
+ g_assert_nonnull (view);
+
+ page = adw_tab_view_append (view, gtk_button_new ());
+ g_assert_nonnull (page);
+
+ notified = 0;
+ g_signal_connect (page, "notify::live-thumbnail", G_CALLBACK (notify_cb), NULL);
+
+ g_object_get (page, "live-thumbnail", &live_thumbnail, NULL);
+ g_assert_false (live_thumbnail);
+ g_assert_cmpint (notified, ==, 0);
+
+ adw_tab_page_set_live_thumbnail (page, TRUE);
+ g_object_get (page, "live-thumbnail", &live_thumbnail, NULL);
+ g_assert_true (live_thumbnail);
+ g_assert_cmpint (notified, ==, 1);
+
+ g_object_set (page, "live-thumbnail", FALSE, NULL);
+ g_assert_false (adw_tab_page_get_live_thumbnail (page));
+ g_assert_cmpint (notified, ==, 2);
+
+ g_assert_finalize_object (view);
+}
+
static void
test_adw_tab_view_pages_to_list_view_setup (GtkSignalListItemFactory *factory,
GtkListItem *list_item,
@@ -1315,12 +1438,16 @@ main (int argc,
g_test_add_func ("/Adwaita/TabView/pages_to_list_view", test_adw_tab_view_pages_to_list_view);
g_test_add_func ("/Adwaita/TabPage/title", test_adw_tab_page_title);
g_test_add_func ("/Adwaita/TabPage/tooltip", test_adw_tab_page_tooltip);
+ g_test_add_func ("/Adwaita/TabPage/keyword", test_adw_tab_page_keyword);
g_test_add_func ("/Adwaita/TabPage/icon", test_adw_tab_page_icon);
g_test_add_func ("/Adwaita/TabPage/loading", test_adw_tab_page_loading);
g_test_add_func ("/Adwaita/TabPage/indicator_icon", test_adw_tab_page_indicator_icon);
g_test_add_func ("/Adwaita/TabPage/indicator_tooltip", test_adw_tab_page_indicator_tooltip);
g_test_add_func ("/Adwaita/TabPage/indicator_activatable", test_adw_tab_page_indicator_activatable);
g_test_add_func ("/Adwaita/TabPage/needs_attention", test_adw_tab_page_needs_attention);
+ g_test_add_func ("/Adwaita/TabPage/thumbnail_xalign", test_adw_tab_page_thumbnail_xalign);
+ g_test_add_func ("/Adwaita/TabPage/thumbnail_yalign", test_adw_tab_page_thumbnail_yalign);
+ g_test_add_func ("/Adwaita/TabPage/live_thumbnail", test_adw_tab_page_live_thumbnail);
return g_test_run ();
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]