[libadwaita/wip/exalm/tab-overview: 1/2] Add AdwTabOverview
- From: Alexander Mikhaylenko <alexm src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libadwaita/wip/exalm/tab-overview: 1/2] Add AdwTabOverview
- Date: Tue, 16 Aug 2022 19:51:30 +0000 (UTC)
commit d7aaa7db05332f1471c81c3a6b4f6747cad55b64
Author: Alexander Mikhaylenko <alexm gnome org>
Date: Fri Aug 12 03:04:59 2022 +0400
Add AdwTabOverview
src/adw-tab-grid-private.h | 54 +
src/adw-tab-grid.c | 3647 +++++++++++++++++++++++++++++++++
src/adw-tab-overview-private.h | 21 +
src/adw-tab-overview.c | 1030 ++++++++++
src/adw-tab-overview.h | 72 +
src/adw-tab-overview.ui | 92 +
src/adw-tab-thumbnail-private.h | 54 +
src/adw-tab-thumbnail.c | 818 ++++++++
src/adw-tab-thumbnail.ui | 110 +
src/adwaita.gresources.xml | 3 +
src/adwaita.h | 1 +
src/glsl/tab-overview.glsl | 18 +
src/meson.build | 4 +
src/stylesheet/widgets/_tab-view.scss | 36 +-
14 files changed, 5959 insertions(+), 1 deletion(-)
---
diff --git a/src/adw-tab-grid-private.h b/src/adw-tab-grid-private.h
new file mode 100644
index 00000000..2ffe1a91
--- /dev/null
+++ b/src/adw-tab-grid-private.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include "adw-tab-view.h"
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_TAB_GRID (adw_tab_grid_get_type())
+
+G_DECLARE_FINAL_TYPE (AdwTabGrid, adw_tab_grid, ADW, TAB_GRID, GtkWidget)
+
+void adw_tab_grid_set_view (AdwTabGrid *self,
+ AdwTabView *view);
+
+void adw_tab_grid_attach_page (AdwTabGrid *self,
+ AdwTabPage *page,
+ int position);
+void adw_tab_grid_detach_page (AdwTabGrid *self,
+ AdwTabPage *page);
+void adw_tab_grid_select_page (AdwTabGrid *self,
+ AdwTabPage *page);
+
+void adw_tab_grid_try_focus_selected_tab (AdwTabGrid *self);
+gboolean adw_tab_grid_is_page_focused (AdwTabGrid *self,
+ AdwTabPage *page);
+
+void adw_tab_grid_setup_extra_drop_target (AdwTabGrid *self,
+ GdkDragAction actions,
+ GType *types,
+ gsize n_types);
+
+gboolean adw_tab_grid_get_inverted (AdwTabGrid *self);
+void adw_tab_grid_set_inverted (AdwTabGrid *self,
+ gboolean inverted);
+
+void adw_tab_grid_set_thumbnail_align (AdwTabGrid *self,
+ float xalign,
+ float yalign);
+
+GtkPicture *adw_tab_grid_get_transition_picture (AdwTabGrid *self);
+
+G_END_DECLS
diff --git a/src/adw-tab-grid.c b/src/adw-tab-grid.c
new file mode 100644
index 00000000..79e2b4bf
--- /dev/null
+++ b/src/adw-tab-grid.c
@@ -0,0 +1,3647 @@
+/*
+ * Copyright (C) 2020-2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#include "config.h"
+
+#include "adw-tab-grid-private.h"
+
+#include "adw-animation-util.h"
+#include "adw-easing.h"
+#include "adw-gizmo-private.h"
+#include "adw-macros-private.h"
+#include "adw-tab-overview-private.h"
+#include "adw-tab-thumbnail-private.h"
+#include "adw-tab-view-private.h"
+#include "adw-timed-animation.h"
+#include "adw-widget-utils-private.h"
+#include <math.h>
+
+#define SPACING 5
+#define DND_THRESHOLD_MULTIPLIER 4
+#define DROP_SWITCH_TIMEOUT 500
+
+#define AUTOSCROLL_SPEED 2.5
+
+#define OPEN_ANIMATION_DURATION 200
+#define CLOSE_ANIMATION_DURATION 200
+#define FOCUS_ANIMATION_DURATION 200
+#define SCROLL_ANIMATION_DURATION 200
+#define RESIZE_ANIMATION_DURATION 200
+#define REORDER_ANIMATION_DURATION 250
+#define ICON_RESIZE_ANIMATION_DURATION 200
+
+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;
+
+ gulong notify_needs_attention_id;
+} TabInfo;
+
+struct _AdwTabGrid
+{
+ GtkWidget parent_instance;
+
+ gboolean pinned;
+ AdwTabOverview *tab_overview;
+ AdwTabView *view;
+ GtkAdjustment *adjustment;
+ gboolean inverted;
+ float thumbnail_xalign;
+ float thumbnail_yalign;
+
+ 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;
+ int placeholder_scroll_offset;
+ gboolean can_remove_placeholder;
+ DragIcon *drag_icon;
+ gboolean should_detach_into_new_window;
+
+ TabInfo *drop_target_tab;
+ guint drop_switch_timeout_id;
+ guint reset_drop_target_tab_id;
+ double drop_target_x;
+ double drop_target_y;
+
+ AdwAnimation *scroll_animation;
+ gboolean scroll_animation_done;
+ double scroll_animation_from;
+ double scroll_animation_offset;
+ TabInfo *scroll_animation_tab;
+ gboolean block_scrolling;
+ double adjustment_prev_value;
+
+ GdkDragAction extra_drag_actions;
+ GType *extra_drag_types;
+ gsize extra_drag_n_types;
+
+ GtkWidget *needs_attention_top;
+ GtkWidget *needs_attention_bottom;
+
+
+
+
+ double n_columns;
+
+ int tab_width;
+ int tab_height;
+};
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (AdwTabGrid, adw_tab_grid, GTK_TYPE_WIDGET,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL))
+
+enum {
+ PROP_0,
+ PROP_PINNED,
+ PROP_TAB_OVERVIEW,
+ PROP_VIEW,
+ PROP_RESIZE_FROZEN,
+ PROP_HADJUSTMENT,
+ PROP_VADJUSTMENT,
+ PROP_HSCROLL_POLICY,
+ PROP_VSCROLL_POLICY,
+ LAST_PROP = PROP_HADJUSTMENT
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_STOP_KINETIC_SCROLLING,
+ SIGNAL_EXTRA_DRAG_DROP,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+/* Helpers */
+
+static void
+remove_and_free_tab_info (TabInfo *info)
+{
+ gtk_widget_unparent (GTK_WIDGET (info->container));
+
+ g_free (info);
+}
+
+static inline int
+get_tab_x (AdwTabGrid *self,
+ TabInfo *info,
+ gboolean final)
+{
+ if (info == self->reordered_tab)
+ return self->reorder_window_x;
+
+ return final ? info->final_x : info->pos_x;
+}
+
+static inline int
+get_tab_y (AdwTabGrid *self,
+ TabInfo *info,
+ gboolean final)
+{
+ if (info == self->reordered_tab)
+ return self->reorder_window_y;
+
+ return final ? info->final_y : info->pos_y;
+}
+
+static inline TabInfo *
+find_tab_info_at (AdwTabGrid *self,
+ double x,
+ double y)
+{
+ GList *l;
+
+ if (self->reordered_tab) {
+ int pos_x = get_tab_x (self, self->reordered_tab, FALSE);
+ int pos_y = get_tab_y (self, self->reordered_tab, FALSE);
+
+ if (pos_x <= x && x < pos_x + self->reordered_tab->width &&
+ pos_y <= y && y < pos_y + self->reordered_tab->height)
+ return self->reordered_tab;
+ }
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ if (info != self->reordered_tab &&
+ info->pos_x <= x && x < info->pos_x + info->width &&
+ info->pos_y <= y && y < info->pos_y + info->height)
+ return info;
+ }
+
+ return NULL;
+}
+
+static inline GList *
+find_link_for_page (AdwTabGrid *self,
+ AdwTabPage *page)
+{
+ GList *l;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ if (info->page == page)
+ return l;
+ }
+
+ return NULL;
+}
+
+static inline TabInfo *
+find_info_for_page (AdwTabGrid *self,
+ AdwTabPage *page)
+{
+ GList *l = find_link_for_page (self, page);
+
+ return l ? l->data : NULL;
+}
+
+static GList *
+find_nth_alive_tab (AdwTabGrid *self,
+ guint position)
+{
+ GList *l;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ if (!info->page)
+ continue;
+
+ if (!position--)
+ return l;
+ }
+
+ return NULL;
+}
+
+/* Layout */
+
+static int
+get_n_columns (AdwTabGrid *self,
+ int for_width)
+{
+ int max_columns;
+
+ if (for_width < 0)
+ return self->n_tabs;
+
+ max_columns = CLAMP ((int) ceil ((double) for_width / 300.0), 1, 8);
+
+ return max_columns;
+}
+
+static int
+get_tab_width (AdwTabGrid *self,
+ int for_width)
+{
+ int n = get_n_columns (self, for_width);
+
+ return (int) ceil ((double) (for_width - SPACING * (n + 1)) / n);
+}
+
+static int
+get_tab_height (AdwTabGrid *self,
+ int tab_width)
+{
+ int height = 0;
+ GList *l;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+ int tab_height;
+
+ gtk_widget_measure (GTK_WIDGET (info->tab), GTK_ORIENTATION_VERTICAL,
+ tab_width, NULL, &tab_height, NULL, NULL);
+
+ height = MAX (height, tab_height);
+ }
+
+ return height;
+}
+
+static void
+get_position_for_index (AdwTabGrid *self,
+ double index,
+ gboolean is_rtl,
+ int *pos_x,
+ int *pos_y)
+{
+ double col = fmod (index, self->n_columns);
+ double row = (index - col) / self->n_columns;
+ int x, y;
+
+ if (col > self->n_columns - 1) {
+ double start, end, t;
+
+ if (is_rtl) {
+ start = self->allocated_width - SPACING - self->tab_width;
+ end = SPACING;
+ } else {
+ start = SPACING;
+ end = self->allocated_width - SPACING - self->tab_width;
+ }
+
+ t = self->n_columns - col;
+ x = adw_lerp (start, end, t);
+ y = SPACING + (row + 1 - t) * (self->tab_height + SPACING);
+
+ } else {
+ x = is_rtl ? self->allocated_width - SPACING - self->tab_width : SPACING;
+
+ if (is_rtl)
+ x -= col * (self->tab_width + SPACING);
+ else
+ x += col * (self->tab_width + SPACING);
+
+ y = SPACING + row * (self->tab_height + SPACING);
+ }
+
+ if (pos_x)
+ *pos_x = x;
+
+ if (pos_y)
+ *pos_y = y;
+}
+
+static inline int
+calculate_tab_width (TabInfo *info,
+ int base_width)
+{
+ return (int) floor ((base_width + SPACING) * info->appear_progress) - SPACING;
+}
+
+static void
+get_visible_range (AdwTabGrid *self,
+ int *lower,
+ int *upper)
+{
+ int min = SPACING;
+ int max = self->allocated_height - SPACING;
+
+ if (self->adjustment) {
+ double value = gtk_adjustment_get_value (self->adjustment);
+ double page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ min = MAX (min, (int) floor (value) + SPACING);
+ max = MIN (max, (int) ceil (value + page_size) - SPACING);
+ }
+
+ if (self->pinned)
+ max += SPACING;
+
+ if (lower)
+ *lower = min;
+
+ if (upper)
+ *upper = max;
+}
+
+static inline gboolean
+is_touchscreen (GtkGesture *gesture)
+{
+ GtkEventController *controller = GTK_EVENT_CONTROLLER (gesture);
+ GdkDevice *device = gtk_event_controller_get_current_event_device (controller);
+ GdkInputSource input_source = gdk_device_get_source (device);
+
+ return input_source == GDK_SOURCE_TOUCHSCREEN;
+}
+
+/* Tab resize delay */
+
+static void
+resize_animation_value_cb (double value,
+ AdwTabGrid *self)
+{
+ double target_end_padding = 0;
+ int tab_width = get_tab_width (self, self->allocated_width);
+ int tab_height = get_tab_height (self, tab_width);
+ GList *l;
+
+ target_end_padding = self->allocated_width - SPACING;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ target_end_padding -= calculate_tab_width (info, tab_height) + SPACING;
+ }
+
+ target_end_padding = MAX (target_end_padding, 0);
+
+ self->end_padding = (int) floor (adw_lerp (self->initial_end_padding, target_end_padding, value));
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+resize_animation_done_cb (AdwTabGrid *self)
+{
+ self->end_padding = 0;
+ self->final_end_padding = 0;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+set_tab_resize_mode (AdwTabGrid *self,
+ TabResizeMode mode)
+{
+ gboolean notify;
+
+ if (self->tab_resize_mode == mode)
+ return;
+
+ if (mode == TAB_RESIZE_FIXED_TAB_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;
+
+ 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]);
+}
+
+/* Hover */
+
+static void
+update_hover (AdwTabGrid *self)
+{
+ if (!self->dragging && !self->hovering)
+ set_tab_resize_mode (self, TAB_RESIZE_NORMAL);
+}
+
+static void
+motion_cb (AdwTabGrid *self,
+ double x,
+ double y,
+ GtkEventController *controller)
+{
+ GdkDevice *device = gtk_event_controller_get_current_event_device (controller);
+ GdkInputSource input_source = gdk_device_get_source (device);
+
+ if (input_source == GDK_SOURCE_TOUCHSCREEN)
+ return;
+
+ if (self->hovering)
+ return;
+
+ self->hovering = TRUE;
+
+ update_hover (self);
+}
+
+static void
+leave_cb (AdwTabGrid *self,
+ GtkEventController *controller)
+{
+ self->hovering = FALSE;
+
+ update_hover (self);
+}
+
+/* Keybindings */
+
+static void
+focus_tab_cb (AdwTabGrid *self,
+ GVariant *args)
+{
+ GtkDirectionType direction;
+ gboolean last, is_rtl, success;
+
+ if (!self->view || !self->selected_tab)
+ return;
+
+ g_variant_get (args, "(hb)", &direction, &last);
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+ success = last;
+
+ if (direction == GTK_DIR_LEFT)
+ direction = is_rtl ? GTK_DIR_TAB_FORWARD : GTK_DIR_TAB_BACKWARD;
+ else if (direction == GTK_DIR_RIGHT)
+ direction = is_rtl ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD;
+
+ if (direction == GTK_DIR_TAB_BACKWARD) {
+ if (last)
+ success = adw_tab_view_select_first_page (self->view);
+ else
+ success = adw_tab_view_select_previous_page (self->view);
+ } else if (direction == GTK_DIR_TAB_FORWARD) {
+ if (last)
+ success = adw_tab_view_select_last_page (self->view);
+ else
+ success = adw_tab_view_select_next_page (self->view);
+ }
+
+ if (!success)
+ gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+static void
+reorder_tab_cb (AdwTabGrid *self,
+ GVariant *args)
+{
+ GtkDirectionType direction;
+ gboolean last, is_rtl, success;
+
+ if (!self->view || !self->selected_tab || !self->selected_tab->page)
+ return;
+
+ g_variant_get (args, "(hb)", &direction, &last);
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+ success = last;
+
+ if (direction == GTK_DIR_LEFT)
+ direction = is_rtl ? GTK_DIR_TAB_FORWARD : GTK_DIR_TAB_BACKWARD;
+ else if (direction == GTK_DIR_RIGHT)
+ direction = is_rtl ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD;
+
+ if (direction == GTK_DIR_TAB_BACKWARD) {
+ if (last)
+ success = adw_tab_view_reorder_first (self->view, self->selected_tab->page);
+ else
+ success = adw_tab_view_reorder_backward (self->view, self->selected_tab->page);
+ } else if (direction == GTK_DIR_TAB_FORWARD) {
+ if (last)
+ success = adw_tab_view_reorder_last (self->view, self->selected_tab->page);
+ else
+ success = adw_tab_view_reorder_forward (self->view, self->selected_tab->page);
+ }
+
+ if (!success)
+ gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+static void
+add_focus_bindings (GtkWidgetClass *widget_class,
+ guint keysym,
+ GtkDirectionType direction,
+ gboolean last)
+{
+ /* All keypad keysyms are aligned at the same order as non-keypad ones */
+ guint keypad_keysym = keysym - GDK_KEY_Left + GDK_KEY_KP_Left;
+
+ gtk_widget_class_add_binding (widget_class, keysym, 0,
+ (GtkShortcutFunc) focus_tab_cb,
+ "(hb)", direction, last);
+ gtk_widget_class_add_binding (widget_class, keypad_keysym, 0,
+ (GtkShortcutFunc) focus_tab_cb,
+ "(hb)", direction, last);
+}
+
+static void
+add_reorder_bindings (GtkWidgetClass *widget_class,
+ guint keysym,
+ GtkDirectionType direction,
+ gboolean last)
+{
+ /* All keypad keysyms are aligned at the same order as non-keypad ones */
+ guint keypad_keysym = keysym - GDK_KEY_Left + GDK_KEY_KP_Left;
+
+ gtk_widget_class_add_binding (widget_class, keysym, GDK_SHIFT_MASK,
+ (GtkShortcutFunc) reorder_tab_cb,
+ "(hb)", direction, last);
+ gtk_widget_class_add_binding (widget_class, keypad_keysym, GDK_SHIFT_MASK,
+ (GtkShortcutFunc) reorder_tab_cb,
+ "(hb)", direction, last);
+}
+
+static void
+activate_tab (AdwTabGrid *self)
+{
+ GtkWidget *child;
+
+ if (!self->selected_tab || !self->selected_tab->page)
+ return;
+
+ child = adw_tab_page_get_child (self->selected_tab->page);
+
+ gtk_widget_grab_focus (child);
+}
+
+/* Scrolling */
+
+static void
+update_visible (AdwTabGrid *self)
+{
+ gboolean top = FALSE, bottom = FALSE;
+ GList *l;
+ int lower, upper;
+
+ get_visible_range (self, &lower, &upper);
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+ int pos;
+
+ if (!info->page)
+ continue;
+
+ pos = get_tab_y (self, info, FALSE);
+
+ adw_tab_thumbnail_set_fully_visible (info->tab,
+ pos >= lower &&
+ pos + info->height <= upper);
+
+ if (!adw_tab_page_get_needs_attention (info->page))
+ continue;
+
+ if (pos + info->height / 2.0 <= lower)
+ top = TRUE;
+
+ if (pos + info->height / 2.0 >= upper)
+ bottom = TRUE;
+ }
+
+ gtk_revealer_set_reveal_child (GTK_REVEALER (self->needs_attention_top), top);
+ gtk_revealer_set_reveal_child (GTK_REVEALER (self->needs_attention_bottom), bottom);
+}
+
+static double
+get_scroll_animation_value (AdwTabGrid *self)
+{
+ double to, value;
+
+ g_assert (self->scroll_animation);
+
+ if (adw_animation_get_state (self->scroll_animation) != ADW_ANIMATION_PLAYING &&
+ adw_animation_get_state (self->scroll_animation) != ADW_ANIMATION_FINISHED)
+ return gtk_adjustment_get_value (self->adjustment);
+
+ to = self->scroll_animation_offset;
+
+ if (self->scroll_animation_tab) {
+ double page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ to += get_tab_y (self, self->scroll_animation_tab, TRUE);
+ to = CLAMP (to, 0, self->allocated_height - page_size);
+ }
+
+ value = adw_animation_get_value (self->scroll_animation);
+
+ return round (adw_lerp (self->scroll_animation_from, to, value));
+}
+
+static gboolean
+drop_switch_timeout_cb (AdwTabGrid *self)
+{
+ self->drop_switch_timeout_id = 0;
+ adw_tab_view_set_selected_page (self->view,
+ self->drop_target_tab->page);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+set_drop_target_tab (AdwTabGrid *self,
+ TabInfo *info)
+{
+ if (self->drop_target_tab == info)
+ return;
+
+ if (self->drop_target_tab)
+ g_clear_handle_id (&self->drop_switch_timeout_id, g_source_remove);
+
+ self->drop_target_tab = info;
+
+ if (self->drop_target_tab) {
+ self->drop_switch_timeout_id =
+ g_timeout_add (DROP_SWITCH_TIMEOUT,
+ (GSourceFunc) drop_switch_timeout_cb,
+ self);
+ }
+}
+
+static void
+adjustment_value_changed_cb (AdwTabGrid *self)
+{
+ double value = gtk_adjustment_get_value (self->adjustment);
+
+ update_visible (self);
+
+ if (self->drop_target_tab) {
+ self->drop_target_y += (value - self->adjustment_prev_value);
+ set_drop_target_tab (self, find_tab_info_at (self,
+ self->drop_target_x,
+ self->drop_target_y));
+ }
+
+ self->adjustment_prev_value = value;
+
+ if (self->block_scrolling)
+ return;
+
+ adw_animation_pause (self->scroll_animation);
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static void
+animate_scroll (AdwTabGrid *self,
+ TabInfo *info,
+ double offset,
+ guint duration)
+{
+ if (!self->adjustment)
+ return;
+
+ g_signal_emit (self, signals[SIGNAL_STOP_KINETIC_SCROLLING], 0);
+
+ self->scroll_animation_done = FALSE;
+ self->scroll_animation_from = gtk_adjustment_get_value (self->adjustment);
+ self->scroll_animation_tab = info;
+ self->scroll_animation_offset = offset;
+
+ adw_timed_animation_set_duration (ADW_TIMED_ANIMATION (self->scroll_animation),
+ duration);
+ adw_animation_play (self->scroll_animation);
+}
+
+static void
+animate_scroll_relative (AdwTabGrid *self,
+ double delta,
+ guint duration)
+{
+ double current_value = gtk_adjustment_get_value (self->adjustment);
+
+ if (adw_animation_get_state (self->scroll_animation) == ADW_ANIMATION_PLAYING) {
+ current_value = self->scroll_animation_offset;
+
+ if (self->scroll_animation_tab)
+ current_value += get_tab_y (self, self->scroll_animation_tab, TRUE);
+ }
+
+ animate_scroll (self, NULL, current_value + delta, duration);
+}
+
+static gboolean
+scroll_to_tab_full (AdwTabGrid *self,
+ TabInfo *info,
+ int pos,
+ guint duration,
+ gboolean keep_selected_visible)
+{
+ int tab_height;
+ double padding, value, page_size;
+
+ if (!self->adjustment)
+ return FALSE;
+
+ tab_height = info->height;
+
+ if (info->appear_animation)
+ tab_height = info->final_height;
+
+ value = gtk_adjustment_get_value (self->adjustment);
+ page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ padding = MIN (tab_height, page_size - tab_height) / 2.0;
+
+ if (pos < 0)
+ pos = get_tab_y (self, info, TRUE);
+
+ if (pos - SPACING < value)
+ animate_scroll (self, info, -padding, duration);
+ else if (pos + tab_height + SPACING > value + page_size)
+ animate_scroll (self, info, tab_height + padding - page_size, duration);
+
+ return TRUE;
+}
+
+static gboolean
+scroll_to_tab (AdwTabGrid *self,
+ TabInfo *info,
+ guint duration)
+{
+ return scroll_to_tab_full (self, info, -1, duration, FALSE);
+}
+
+static void
+scroll_animation_cb (double value,
+ AdwTabGrid *self)
+{
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+scroll_animation_done_cb (AdwTabGrid *self)
+{
+ self->scroll_animation_done = TRUE;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+set_vadjustment (AdwTabGrid *self,
+ GtkAdjustment *adjustment)
+{
+ if (adjustment == self->adjustment)
+ return;
+
+ if (self->adjustment) {
+ g_signal_handlers_disconnect_by_func (self->adjustment, adjustment_value_changed_cb, self);
+ g_signal_handlers_disconnect_by_func (self->adjustment, update_visible, self);
+ }
+
+ g_set_object (&self->adjustment, adjustment);
+
+ if (self->adjustment) {
+ g_signal_connect_object (self->adjustment, "value-changed", G_CALLBACK (adjustment_value_changed_cb),
self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->adjustment, "notify::page-size", G_CALLBACK (update_visible), self,
G_CONNECT_SWAPPED);
+ }
+
+ g_object_notify (G_OBJECT (self), "vadjustment");
+}
+
+/* Reordering */
+
+static void
+force_end_reordering (AdwTabGrid *self)
+{
+ GList *l;
+
+ if (self->dragging || !self->reordered_tab)
+ return;
+
+ if (self->reorder_animation)
+ adw_animation_skip (self->reorder_animation);
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ if (info->reorder_animation)
+ adw_animation_skip (info->reorder_animation);
+ }
+}
+
+static void
+check_end_reordering (AdwTabGrid *self)
+{
+ GList *l;
+
+ if (self->dragging || !self->reordered_tab || self->continue_reorder)
+ return;
+
+ if (self->reorder_animation)
+ return;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ if (info->reorder_animation)
+ return;
+ }
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ info->end_reorder_offset = 0;
+ info->reorder_offset = 0;
+ }
+
+ self->reordered_tab->reorder_ignore_bounds = FALSE;
+
+ self->tabs = g_list_remove (self->tabs, self->reordered_tab);
+ self->tabs = g_list_insert (self->tabs, self->reordered_tab, self->reorder_index);
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+ self->reordered_tab = NULL;
+}
+
+static void
+start_reordering (AdwTabGrid *self,
+ TabInfo *info)
+{
+ self->reordered_tab = info;
+
+ /* The reordered tab should be displayed above everything else */
+ gtk_widget_insert_before (GTK_WIDGET (self->reordered_tab->container),
+ GTK_WIDGET (self), self->needs_attention_top);
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static void
+get_reorder_position (AdwTabGrid *self,
+ int *x,
+ int *y)
+{
+ int lower, upper;
+ int width;
+
+ if (self->reordered_tab->reorder_ignore_bounds) {
+ *x = self->reorder_x;
+ *y = self->reorder_y;
+ return;
+ }
+
+ get_visible_range (self, &lower, &upper);
+
+ width = gtk_widget_get_width (GTK_WIDGET (self));
+
+ *x = CLAMP (self->reorder_x, SPACING, width - self->reordered_tab->width - SPACING);
+ *y = CLAMP (self->reorder_y, lower, upper - self->reordered_tab->height);
+}
+
+static void
+reorder_animation_value_cb (double value,
+ TabInfo *dest_tab)
+{
+ AdwTabGrid *self = dest_tab->box;
+ gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+ int x1, y1, x2, y2;
+
+ get_reorder_position (self, &x1, &y1);
+ get_position_for_index (self, dest_tab->index, is_rtl, &x2, &y2);
+
+ self->reorder_window_x = (int) round (adw_lerp (x1, x2, value));
+ self->reorder_window_y = (int) round (adw_lerp (y1, y2, value));
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static void
+reorder_animation_done_cb (AdwTabGrid *self)
+{
+ g_clear_object (&self->reorder_animation);
+ check_end_reordering (self);
+}
+
+static void
+animate_reordering (AdwTabGrid *self,
+ TabInfo *dest_tab)
+{
+ AdwAnimationTarget *target;
+
+ if (self->reorder_animation)
+ adw_animation_skip (self->reorder_animation);
+
+ target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+ reorder_animation_value_cb,
+ dest_tab, NULL);
+ self->reorder_animation =
+ adw_timed_animation_new (GTK_WIDGET (self), 0, 1,
+ REORDER_ANIMATION_DURATION, target);
+
+ g_signal_connect_swapped (self->reorder_animation, "done",
+ G_CALLBACK (reorder_animation_done_cb), self);
+
+ adw_animation_play (self->reorder_animation);
+
+ check_end_reordering (self);
+}
+
+static void
+reorder_offset_animation_value_cb (double value,
+ TabInfo *info)
+{
+ AdwTabGrid *self = info->box;
+
+ info->reorder_offset = value;
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static void
+reorder_offset_animation_done_cb (TabInfo *info)
+{
+ AdwTabGrid *self = info->box;
+
+ g_clear_object (&info->reorder_animation);
+ check_end_reordering (self);
+}
+
+static void
+animate_reorder_offset (AdwTabGrid *self,
+ TabInfo *info,
+ double offset)
+{
+ gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+ AdwAnimationTarget *target;
+ double start_offset;
+
+ offset *= (is_rtl ? -1 : 1);
+
+ if (info->end_reorder_offset == offset)
+ return;
+
+ info->end_reorder_offset = offset;
+ start_offset = info->reorder_offset;
+
+ if (info->reorder_animation)
+ adw_animation_skip (info->reorder_animation);
+
+ target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+ reorder_offset_animation_value_cb,
+ info, NULL);
+ info->reorder_animation =
+ adw_timed_animation_new (GTK_WIDGET (self), start_offset, offset,
+ REORDER_ANIMATION_DURATION, target);
+
+ g_signal_connect_swapped (info->reorder_animation, "done",
+ G_CALLBACK (reorder_offset_animation_done_cb), info);
+
+ adw_animation_play (info->reorder_animation);
+}
+
+static void
+reset_reorder_animations (AdwTabGrid *self)
+{
+ int i, original_index;
+ GList *l;
+
+ if (!adw_get_enable_animations (GTK_WIDGET (self)))
+ return;
+
+ l = find_link_for_page (self, self->reordered_tab->page);
+ original_index = g_list_position (self->tabs, l);
+
+ if (self->reorder_index > original_index)
+ for (i = 0; i < self->reorder_index - original_index; i++) {
+ l = l->next;
+ animate_reorder_offset (self, l->data, 0);
+ }
+
+ if (self->reorder_index < original_index)
+ for (i = 0; i < original_index - self->reorder_index; i++) {
+ l = l->prev;
+ animate_reorder_offset (self, l->data, 0);
+ }
+}
+
+static void
+page_reordered_cb (AdwTabGrid *self,
+ AdwTabPage *page,
+ int index)
+{
+ GList *link;
+ int original_index;
+ TabInfo *info, *dest_tab;
+ gboolean is_rtl;
+
+ if (adw_tab_page_get_pinned (page) != self->pinned)
+ return;
+
+ self->continue_reorder = self->reordered_tab && page == self->reordered_tab->page;
+
+ if (self->continue_reorder)
+ reset_reorder_animations (self);
+ else
+ force_end_reordering (self);
+
+ link = find_link_for_page (self, page);
+ info = link->data;
+ original_index = g_list_position (self->tabs, link);
+
+ if (!self->continue_reorder)
+ start_reordering (self, info);
+
+ if (self->continue_reorder) {
+ self->reorder_x = self->reorder_window_x;
+ self->reorder_y = self->reorder_window_y;
+ } else {
+ self->reorder_x = info->pos_x;
+ self->reorder_y = info->pos_y;
+ }
+
+ self->reorder_index = index;
+
+ if (!self->pinned)
+ self->reorder_index -= adw_tab_view_get_n_pinned_pages (self->view);
+
+ dest_tab = g_list_nth_data (self->tabs, self->reorder_index);
+
+ if (info == self->selected_tab)
+ scroll_to_tab_full (self, self->selected_tab, dest_tab->final_y, REORDER_ANIMATION_DURATION, FALSE);
+
+ animate_reordering (self, dest_tab);
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+ /* If animations are disabled, animate_reordering() animation will have
+ * already finished and called check_end_reordering () by this point, so
+ * it's too late to animate these, so we get a crash.
+ */
+
+ if (adw_get_enable_animations (GTK_WIDGET (self)) &&
+ gtk_widget_get_mapped (GTK_WIDGET (self))) {
+ int i;
+
+ if (self->reorder_index > original_index)
+ for (i = 0; i < self->reorder_index - original_index; i++) {
+ link = link->next;
+ animate_reorder_offset (self, link->data, is_rtl ? 1 : -1);
+ }
+
+ if (self->reorder_index < original_index)
+ for (i = 0; i < original_index - self->reorder_index; i++) {
+ link = link->prev;
+ animate_reorder_offset (self, link->data, is_rtl ? -1 : 1);
+ }
+ }
+
+ self->continue_reorder = FALSE;
+}
+
+static void
+update_drag_reodering (AdwTabGrid *self)
+{
+ gboolean is_rtl;
+ int old_index = -1, new_index = -1;
+ int x, y;
+ int i = 0;
+ int width, height;
+ GList *l;
+
+ if (!self->dragging)
+ return;
+
+ get_reorder_position (self, &x, &y);
+
+ width = self->reordered_tab->final_width;
+ height = self->reordered_tab->final_height;
+
+ self->reorder_window_x = x;
+ self->reorder_window_y = y;
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+ int center_x, center_y;
+
+ center_x = info->unshifted_x + info->final_width / 2;
+ center_y = info->unshifted_y + info->final_height / 2;
+
+ if (is_rtl)
+ center_x -= info->final_width;
+
+ if (info == self->reordered_tab)
+ old_index = i;
+
+ if (x + width + SPACING > center_x && center_x > x - SPACING &&
+ y + height + SPACING > center_y && center_y > y - SPACING &&
+ new_index < 0)
+ new_index = i;
+
+ if (old_index >= 0 && new_index >= 0)
+ break;
+
+ i++;
+ }
+
+ if (new_index < 0)
+ new_index = g_list_length (self->tabs) - 1;
+
+ i = 0;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+ double offset = 0;
+
+ if (i > old_index && i <= new_index)
+ offset = is_rtl ? 1 : -1;
+
+ if (i < old_index && i >= new_index)
+ offset = is_rtl ? -1 : 1;
+
+ i++;
+
+ animate_reorder_offset (self, info, offset);
+ }
+
+ self->reorder_index = new_index;
+}
+
+static gboolean
+drag_autoscroll_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ AdwTabGrid *self)
+{
+ double value, page_size;
+ double y, delta_ms, start_threshold, end_threshold, autoscroll_factor;
+ gint64 time;
+ int offset = 0;
+ int tab_height = 0;
+ int autoscroll_area = 0;
+
+ if (self->reordered_tab) {
+ tab_height = gtk_widget_get_height (self->reordered_tab->container);
+ y = (double) self->reorder_y - SPACING;
+ } else if (self->drop_target_tab) {
+ tab_height = gtk_widget_get_height (self->drop_target_tab->container);
+ y = (double) self->drop_target_y - tab_height / 2;
+ } else {
+ return G_SOURCE_CONTINUE;
+ }
+
+ value = gtk_adjustment_get_value (self->adjustment);
+ page_size = gtk_adjustment_get_page_size (self->adjustment);
+ autoscroll_area = tab_height / 2;
+
+ y = CLAMP (y,
+ autoscroll_area,
+ self->allocated_height - tab_height - autoscroll_area);
+
+ time = gdk_frame_clock_get_frame_time (frame_clock);
+ delta_ms = (time - self->drag_autoscroll_prev_time) / 1000.0;
+
+ start_threshold = value + autoscroll_area;
+ end_threshold = value + page_size - tab_height - autoscroll_area;
+ autoscroll_factor = 0;
+
+ if (y < start_threshold)
+ autoscroll_factor = -(start_threshold - y) / autoscroll_area;
+ else if (y > end_threshold)
+ autoscroll_factor = (y - end_threshold) / autoscroll_area;
+
+ autoscroll_factor = CLAMP (autoscroll_factor, -1, 1);
+ autoscroll_factor = adw_easing_ease (ADW_EASE_IN_CUBIC,
+ autoscroll_factor);
+ self->drag_autoscroll_prev_time = time;
+
+ if (autoscroll_factor == 0)
+ return G_SOURCE_CONTINUE;
+
+ if (autoscroll_factor > 0)
+ offset = (int) ceil (autoscroll_factor * delta_ms * AUTOSCROLL_SPEED);
+ else
+ offset = (int) floor (autoscroll_factor * delta_ms * AUTOSCROLL_SPEED);
+
+ self->reorder_y += offset;
+ gtk_adjustment_set_value (self->adjustment, value + offset);
+ update_drag_reodering (self);
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+start_autoscroll (AdwTabGrid *self)
+{
+ GdkFrameClock *frame_clock;
+
+ if (!self->adjustment)
+ return;
+
+ if (self->drag_autoscroll_cb_id)
+ return;
+
+ frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
+
+ self->drag_autoscroll_prev_time = gdk_frame_clock_get_frame_time (frame_clock);
+ self->drag_autoscroll_cb_id =
+ gtk_widget_add_tick_callback (GTK_WIDGET (self),
+ (GtkTickCallback) drag_autoscroll_cb,
+ self, NULL);
+}
+
+static void
+end_autoscroll (AdwTabGrid *self)
+{
+ if (self->drag_autoscroll_cb_id) {
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self),
+ self->drag_autoscroll_cb_id);
+ self->drag_autoscroll_cb_id = 0;
+ }
+}
+
+static void
+start_drag_reodering (AdwTabGrid *self,
+ TabInfo *info,
+ double x,
+ double y)
+{
+ if (self->dragging)
+ return;
+
+ if (!info)
+ return;
+
+ self->continue_reorder = info == self->reordered_tab;
+
+ if (self->continue_reorder) {
+ if (self->reorder_animation)
+ adw_animation_skip (self->reorder_animation);
+
+ reset_reorder_animations (self);
+
+ self->reorder_x = (int) round (x - self->drag_offset_x);
+ self->reorder_y = (int) round (y - self->drag_offset_y);
+ } else
+ force_end_reordering (self);
+
+ start_autoscroll (self);
+ self->dragging = TRUE;
+
+ if (!self->continue_reorder)
+ start_reordering (self, info);
+}
+
+static void
+end_drag_reodering (AdwTabGrid *self)
+{
+ TabInfo *dest_tab;
+
+ if (!self->dragging)
+ return;
+
+ self->dragging = FALSE;
+
+ end_autoscroll (self);
+
+ dest_tab = g_list_nth_data (self->tabs, self->reorder_index);
+
+ if (!self->indirect_reordering) {
+ int index = self->reorder_index;
+
+ if (!self->pinned)
+ index += adw_tab_view_get_n_pinned_pages (self->view);
+
+ /* We've already reordered the tab here, no need to do it again */
+ g_signal_handlers_block_by_func (self->view, page_reordered_cb, self);
+
+ adw_tab_view_reorder_page (self->view, self->reordered_tab->page, index);
+
+ g_signal_handlers_unblock_by_func (self->view, page_reordered_cb, self);
+ }
+
+ animate_reordering (self, dest_tab);
+
+ self->continue_reorder = FALSE;
+}
+
+static void
+reorder_begin_cb (AdwTabGrid *self,
+ double start_x,
+ double start_y,
+ GtkGesture *gesture)
+{
+ start_y += gtk_adjustment_get_value (self->adjustment);
+
+ self->pressed_tab = find_tab_info_at (self, start_x, start_y);
+
+ 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 + gtk_adjustment_get_value (self->adjustment) + offset_y;
+
+ start_drag_reodering (self, self->pressed_tab, x, y);
+
+ if (self->dragging) {
+ adw_tab_view_set_selected_page (self->view, self->pressed_tab->page);
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+ } else {
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ self->reorder_x = (int) round (x - self->drag_offset_x);
+ self->reorder_y = (int) round (y - self->drag_offset_y);
+
+ device = gtk_event_controller_get_current_event_device (GTK_EVENT_CONTROLLER (gesture));
+
+ if (!self->pinned &&
+ self->pressed_tab &&
+ self->pressed_tab != self->reorder_placeholder &&
+ self->pressed_tab->page &&
+ !is_touchscreen (gesture) &&
+ adw_tab_view_get_n_pages (self->view) > 1 &&
+ check_dnd_threshold (self, x, y)) {
+ begin_drag (self, device);
+
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ return;
+ }
+
+ update_drag_reodering (self);
+}
+
+static void
+reorder_end_cb (AdwTabGrid *self,
+ double offset_x,
+ double offset_y,
+ GtkGesture *gesture)
+{
+ end_drag_reodering (self);
+}
+
+/* Selection */
+
+static void
+reset_focus (AdwTabGrid *self)
+{
+ GtkRoot *root = gtk_widget_get_root (GTK_WIDGET (self));
+
+ gtk_widget_set_focus_child (GTK_WIDGET (self), NULL);
+
+ if (root)
+ gtk_root_set_focus (root, NULL);
+}
+
+static void
+select_page (AdwTabGrid *self,
+ AdwTabPage *page)
+{
+ if (!page) {
+ self->selected_tab = NULL;
+
+ reset_focus (self);
+
+ return;
+ }
+
+ self->selected_tab = find_info_for_page (self, page);
+
+ if (!self->selected_tab) {
+ if (gtk_widget_get_focus_child (GTK_WIDGET (self)))
+ reset_focus (self);
+
+ return;
+ }
+
+ if (adw_tab_overview_tabs_have_visible_focus (self->tab_overview))
+ gtk_widget_grab_focus (self->selected_tab->container);
+
+ gtk_widget_set_focus_child (GTK_WIDGET (self),
+ self->selected_tab->container);
+
+ if (self->selected_tab->width >= 0)
+ scroll_to_tab (self, self->selected_tab, FOCUS_ANIMATION_DURATION);
+}
+
+/* Opening */
+
+static gboolean
+extra_drag_drop_cb (AdwTabThumbnail *tab,
+ GValue *value,
+ AdwTabGrid *self)
+{
+ gboolean ret = GDK_EVENT_PROPAGATE;
+ AdwTabPage *page = adw_tab_thumbnail_get_page (tab);
+
+ g_signal_emit (self, signals[SIGNAL_EXTRA_DRAG_DROP], 0, page, value, &ret);
+
+ return ret;
+}
+
+static void
+appear_animation_value_cb (double value,
+ TabInfo *info)
+{
+ info->appear_progress = value;
+
+ if (GTK_IS_WIDGET (info->container))
+ gtk_widget_queue_resize (info->container);
+}
+
+static void
+open_animation_done_cb (TabInfo *info)
+{
+ g_clear_object (&info->appear_animation);
+}
+
+static void
+measure_tab (AdwGizmo *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (widget));
+
+ gtk_widget_measure (child, orientation, for_size,
+ minimum, natural,
+ minimum_baseline, natural_baseline);
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL && minimum)
+ *minimum = 0;
+}
+
+static void
+allocate_tab (AdwGizmo *widget,
+ int width,
+ int height,
+ int baseline)
+{
+ TabInfo *info = g_object_get_data (G_OBJECT (widget), "info");
+ GtkWidget *child = gtk_widget_get_first_child (GTK_WIDGET (widget));
+ int allocated_width = gtk_widget_get_allocated_width (GTK_WIDGET (widget));
+ int width_diff = MAX (0, info->final_width - allocated_width);
+
+ gtk_widget_allocate (child, width + width_diff, height, baseline,
+ gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (-width_diff / 2, 0)));
+}
+
+static TabInfo *
+create_tab_info (AdwTabGrid *self,
+ AdwTabPage *page)
+{
+ TabInfo *info;
+
+ info = g_new0 (TabInfo, 1);
+ info->box = self;
+ info->page = page;
+ info->unshifted_x = -1;
+ info->unshifted_y = -1;
+ info->pos_x = -1;
+ info->pos_y = -1;
+ info->width = -1;
+ info->height = -1;
+ info->container = adw_gizmo_new ("tabgridchild", measure_tab, allocate_tab,
+ NULL, NULL,
+ (AdwGizmoFocusFunc) adw_widget_focus_child,
+ (AdwGizmoGrabFocusFunc) adw_widget_grab_focus_self);
+ info->tab = adw_tab_thumbnail_new (self->view, self->pinned);
+
+ g_object_set_data (G_OBJECT (info->container), "info", info);
+ gtk_widget_set_overflow (info->container, GTK_OVERFLOW_HIDDEN);
+ gtk_widget_set_focusable (info->container, TRUE);
+
+ adw_tab_thumbnail_set_page (info->tab, page);
+ adw_tab_thumbnail_set_inverted (info->tab, self->inverted);
+ adw_tab_thumbnail_set_align (info->tab, self->thumbnail_xalign, self->thumbnail_yalign);
+ adw_tab_thumbnail_setup_extra_drop_target (info->tab,
+ self->extra_drag_actions,
+ self->extra_drag_types,
+ self->extra_drag_n_types);
+
+ gtk_widget_set_parent (GTK_WIDGET (info->tab), info->container);
+ gtk_widget_insert_before (info->container, GTK_WIDGET (self), self->needs_attention_top);
+
+ g_signal_connect_object (info->tab, "extra-drag-drop", G_CALLBACK (extra_drag_drop_cb), self, 0);
+
+ return info;
+}
+
+static void
+page_attached_cb (AdwTabGrid *self,
+ AdwTabPage *page,
+ int position)
+{
+ AdwAnimationTarget *target;
+ TabInfo *info;
+ GList *l;
+
+ if (adw_tab_page_get_pinned (page) != self->pinned)
+ return;
+
+ if (!self->pinned)
+ position -= adw_tab_view_get_n_pinned_pages (self->view);
+
+ set_tab_resize_mode (self, TAB_RESIZE_NORMAL);
+ force_end_reordering (self);
+
+ info = create_tab_info (self, page);
+
+ info->notify_needs_attention_id =
+ g_signal_connect_object (page,
+ "notify::needs-attention",
+ G_CALLBACK (update_visible),
+ self,
+ G_CONNECT_SWAPPED);
+
+ target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+ appear_animation_value_cb,
+ info, NULL);
+ info->appear_animation =
+ adw_timed_animation_new (GTK_WIDGET (self), 0, 1,
+ OPEN_ANIMATION_DURATION, target);
+
+ g_signal_connect_swapped (info->appear_animation, "done",
+ G_CALLBACK (open_animation_done_cb), info);
+
+ l = find_nth_alive_tab (self, position);
+ self->tabs = g_list_insert_before (self->tabs, l, info);
+
+ self->n_tabs++;
+
+ adw_animation_play (info->appear_animation);
+
+ if (page == adw_tab_view_get_selected_page (self->view))
+ adw_tab_grid_select_page (self, page);
+ else
+ scroll_to_tab_full (self, info, -1, OPEN_ANIMATION_DURATION, TRUE);
+}
+
+/* Closing */
+
+static void
+close_animation_done_cb (TabInfo *info)
+{
+ AdwTabGrid *self = info->box;
+
+ g_clear_object (&info->appear_animation);
+
+ self->tabs = g_list_remove (self->tabs, info);
+
+ if (info->reorder_animation)
+ adw_animation_skip (info->reorder_animation);
+
+ if (self->reorder_animation)
+ adw_animation_skip (self->reorder_animation);
+
+ if (self->pressed_tab == info)
+ self->pressed_tab = NULL;
+
+ if (self->reordered_tab == info)
+ self->reordered_tab = NULL;
+
+ remove_and_free_tab_info (info);
+
+ self->n_tabs--;
+}
+
+static void
+page_detached_cb (AdwTabGrid *self,
+ AdwTabPage *page)
+{
+ AdwAnimationTarget *target;
+ TabInfo *info;
+ GList *page_link;
+
+ page_link = find_link_for_page (self, page);
+
+ if (!page_link)
+ return;
+
+ info = page_link->data;
+ page_link = page_link->next;
+
+ force_end_reordering (self);
+
+ if (self->hovering && !self->pinned)
+ 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);
+
+ if (info == self->selected_tab)
+ adw_tab_grid_select_page (self, NULL);
+
+ adw_tab_thumbnail_set_page (info->tab, NULL);
+
+ if (info->notify_needs_attention_id > 0) {
+ g_signal_handler_disconnect (info->page, info->notify_needs_attention_id);
+ info->notify_needs_attention_id = 0;
+ }
+
+ info->page = NULL;
+
+ if (info->appear_animation)
+ adw_animation_skip (info->appear_animation);
+
+ target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+ appear_animation_value_cb,
+ info, NULL);
+ info->appear_animation =
+ adw_timed_animation_new (GTK_WIDGET (self), info->appear_progress, 0,
+ CLOSE_ANIMATION_DURATION, target);
+
+ g_signal_connect_swapped (info->appear_animation, "done",
+ G_CALLBACK (close_animation_done_cb), info);
+
+ adw_animation_play (info->appear_animation);
+}
+
+/* Tab DND */
+
+#define ADW_TYPE_TAB_GRID_ROOT_CONTENT (adw_tab_grid_root_content_get_type ())
+
+G_DECLARE_FINAL_TYPE (AdwTabGridRootContent, adw_tab_grid_root_content, ADW, TAB_GRID_ROOT_CONTENT,
GdkContentProvider)
+
+struct _AdwTabGridRootContent
+{
+ GdkContentProvider parent_instance;
+
+ AdwTabGrid *tab_grid;
+};
+
+G_DEFINE_FINAL_TYPE (AdwTabGridRootContent, adw_tab_grid_root_content, GDK_TYPE_CONTENT_PROVIDER)
+
+static GdkContentFormats *
+adw_tab_grid_root_content_ref_formats (GdkContentProvider *provider)
+{
+ return gdk_content_formats_new ((const char *[1]) { "application/x-rootwindow-drop" }, 1);
+}
+
+static void
+adw_tab_grid_root_content_write_mime_type_async (GdkContentProvider *provider,
+ const char *mime_type,
+ GOutputStream *stream,
+ int io_priority,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ AdwTabGridRootContent *self = ADW_TAB_GRID_ROOT_CONTENT (provider);
+ GTask *task;
+
+ self->tab_grid->should_detach_into_new_window = TRUE;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_priority (task, io_priority);
+ g_task_set_source_tag (task, adw_tab_grid_root_content_write_mime_type_async);
+ g_task_return_boolean (task, TRUE);
+
+ g_object_unref (task);
+}
+
+static gboolean
+adw_tab_grid_root_content_write_mime_type_finish (GdkContentProvider *provider,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+adw_tab_grid_root_content_finalize (GObject *object)
+{
+ AdwTabGridRootContent *self = ADW_TAB_GRID_ROOT_CONTENT (object);
+
+ g_clear_object (&self->tab_grid);
+
+ G_OBJECT_CLASS (adw_tab_grid_root_content_parent_class)->finalize (object);
+}
+
+static void
+adw_tab_grid_root_content_class_init (AdwTabGridRootContentClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GdkContentProviderClass *provider_class = GDK_CONTENT_PROVIDER_CLASS (klass);
+
+ object_class->finalize = adw_tab_grid_root_content_finalize;
+
+ provider_class->ref_formats = adw_tab_grid_root_content_ref_formats;
+ provider_class->write_mime_type_async = adw_tab_grid_root_content_write_mime_type_async;
+ provider_class->write_mime_type_finish = adw_tab_grid_root_content_write_mime_type_finish;
+}
+
+static void
+adw_tab_grid_root_content_init (AdwTabGridRootContent *self)
+{
+}
+
+static GdkContentProvider *
+adw_tab_grid_root_content_new (AdwTabGrid *tab_grid)
+{
+ AdwTabGridRootContent *self = g_object_new (ADW_TYPE_TAB_GRID_ROOT_CONTENT, NULL);
+
+ self->tab_grid = g_object_ref (tab_grid);
+
+ return GDK_CONTENT_PROVIDER (self);
+}
+
+static int
+calculate_placeholder_index (AdwTabGrid *self,
+ int x,
+ int y)
+{
+ int lower, upper, i;
+ gboolean is_rtl;
+
+ get_visible_range (self, &lower, &upper);
+
+ x = CLAMP (x, 0, self->allocated_width);
+ y = CLAMP (y, lower, upper);
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+ for (i = 0; i < self->n_tabs; i++) {
+ int tab_x, tab_y;
+
+ get_position_for_index (self, i, is_rtl, &tab_x, &tab_y);
+
+ if (x <= tab_x + self->tab_height + SPACING / 2 &&
+ y <= tab_y + self->tab_width + SPACING / 2)
+ return i;
+ }
+
+ return i;
+}
+
+static void
+insert_animation_value_cb (double value,
+ TabInfo *info)
+{
+ AdwTabGrid *self = info->box;
+
+ appear_animation_value_cb (value, info);
+
+ update_drag_reodering (self);
+}
+
+static void
+insert_placeholder (AdwTabGrid *self,
+ AdwTabPage *page,
+ int x,
+ int y)
+{
+ TabInfo *info = self->reorder_placeholder;
+ double initial_progress = 0;
+ AdwAnimationTarget *target;
+
+ if (info) {
+ initial_progress = info->appear_progress;
+
+ if (info->appear_animation)
+ adw_animation_skip (info->appear_animation);
+ } else {
+ int index;
+
+ self->placeholder_page = page;
+
+ info = create_tab_info (self, page);
+
+ gtk_widget_set_opacity (info->container, 0);
+
+ adw_tab_thumbnail_set_dragging (info->tab, TRUE);
+
+ info->reorder_ignore_bounds = TRUE;
+
+ if (self->adjustment) {
+ double page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ if (self->allocated_height > page_size) {
+ gtk_widget_measure (info->container, GTK_ORIENTATION_VERTICAL, -1,
+ NULL, &self->placeholder_scroll_offset, NULL, NULL);
+
+ self->placeholder_scroll_offset /= 2;
+ } else {
+ self->placeholder_scroll_offset = 0;
+ }
+ }
+
+ index = calculate_placeholder_index (self, x, y + self->placeholder_scroll_offset);
+
+ self->tabs = g_list_insert (self->tabs, info, index);
+ self->n_tabs++;
+
+ self->reorder_placeholder = info;
+ self->reorder_index = g_list_index (self->tabs, info);
+
+ animate_scroll_relative (self, self->placeholder_scroll_offset, OPEN_ANIMATION_DURATION);
+ }
+
+ target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+ insert_animation_value_cb,
+ info, NULL);
+ info->appear_animation =
+ adw_timed_animation_new (GTK_WIDGET (self), initial_progress, 1,
+ OPEN_ANIMATION_DURATION, target);
+
+ g_signal_connect_swapped (info->appear_animation, "done",
+ G_CALLBACK (open_animation_done_cb), info);
+
+ adw_animation_play (info->appear_animation);
+}
+
+static void
+replace_animation_done_cb (TabInfo *info)
+{
+ AdwTabGrid *self = info->box;
+
+ g_clear_object (&info->appear_animation);
+ self->reorder_placeholder = NULL;
+ self->can_remove_placeholder = TRUE;
+}
+
+static void
+replace_placeholder (AdwTabGrid *self,
+ AdwTabPage *page)
+{
+ TabInfo *info = self->reorder_placeholder;
+ double initial_progress;
+ AdwAnimationTarget *target;
+
+ self->placeholder_scroll_offset = 0;
+ gtk_widget_set_opacity (self->reorder_placeholder->container, 1);
+ adw_tab_thumbnail_set_dragging (info->tab, FALSE);
+
+ if (!info->appear_animation) {
+ self->reorder_placeholder = NULL;
+
+ return;
+ }
+
+ initial_progress = info->appear_progress;
+
+ self->can_remove_placeholder = FALSE;
+
+ adw_tab_thumbnail_set_page (info->tab, page);
+ info->page = page;
+
+ adw_animation_skip (info->appear_animation);
+
+ target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+ appear_animation_value_cb,
+ info, NULL);
+ info->appear_animation =
+ adw_timed_animation_new (GTK_WIDGET (self), initial_progress, 1,
+ OPEN_ANIMATION_DURATION, target);
+
+ g_signal_connect_swapped (info->appear_animation, "done",
+ G_CALLBACK (replace_animation_done_cb), info);
+
+ adw_animation_play (info->appear_animation);
+}
+
+static void
+remove_animation_done_cb (TabInfo *info)
+{
+ AdwTabGrid *self = info->box;
+
+ g_clear_object (&info->appear_animation);
+
+ if (!self->can_remove_placeholder) {
+ adw_tab_thumbnail_set_page (info->tab, self->placeholder_page);
+ info->page = self->placeholder_page;
+
+ return;
+ }
+
+ if (self->reordered_tab == info) {
+ force_end_reordering (self);
+
+ if (info->reorder_animation)
+ adw_animation_skip (info->reorder_animation);
+
+ self->reordered_tab = NULL;
+ }
+
+ if (self->pressed_tab == info)
+ self->pressed_tab = NULL;
+
+ self->tabs = g_list_remove (self->tabs, info);
+
+ remove_and_free_tab_info (info);
+
+ self->n_tabs--;
+
+ self->reorder_placeholder = NULL;
+}
+
+static gboolean
+remove_placeholder_scroll_cb (AdwTabGrid *self)
+{
+ animate_scroll_relative (self, -self->placeholder_scroll_offset, CLOSE_ANIMATION_DURATION);
+ self->placeholder_scroll_offset = 0;
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+remove_placeholder (AdwTabGrid *self)
+{
+ TabInfo *info = self->reorder_placeholder;
+ AdwAnimationTarget *target;
+
+ if (!info || !info->page)
+ return;
+
+ adw_tab_thumbnail_set_page (info->tab, NULL);
+ info->page = NULL;
+
+ if (info->appear_animation)
+ adw_animation_skip (info->appear_animation);
+
+ g_idle_add ((GSourceFunc) remove_placeholder_scroll_cb, self);
+
+ target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+ appear_animation_value_cb,
+ info, NULL);
+ info->appear_animation =
+ adw_timed_animation_new (GTK_WIDGET (self), info->appear_progress, 0,
+ CLOSE_ANIMATION_DURATION, target);
+
+ g_signal_connect_swapped (info->appear_animation, "done",
+ G_CALLBACK (remove_animation_done_cb), info);
+
+ adw_animation_play (info->appear_animation);
+}
+
+static inline AdwTabGrid *
+get_source_tab_grid (GtkDropTarget *target)
+{
+ GdkDrop *drop = gtk_drop_target_get_current_drop (target);
+ GdkDrag *drag = gdk_drop_get_drag (drop);
+
+ if (!drag)
+ return NULL;
+
+ return ADW_TAB_GRID (g_object_get_data (G_OBJECT (drag),
+ "adw-tab-overview-drag-origin"));
+}
+
+static void
+do_drag_drop (AdwTabGrid *self,
+ AdwTabGrid *source_tab_grid)
+{
+ AdwTabPage *page = source_tab_grid->detached_page;
+ int offset = (self->pinned ? 0 : adw_tab_view_get_n_pinned_pages (self->view));
+
+ if (self->reorder_placeholder) {
+ replace_placeholder (self, page);
+ end_drag_reodering (self);
+
+ g_signal_handlers_block_by_func (self->view, page_attached_cb, self);
+
+ adw_tab_view_attach_page (self->view, page, self->reorder_index + offset);
+
+ g_signal_handlers_unblock_by_func (self->view, page_attached_cb, self);
+ } else {
+ adw_tab_view_attach_page (self->view, page, self->reorder_index + offset);
+ }
+
+ source_tab_grid->should_detach_into_new_window = FALSE;
+ source_tab_grid->detached_page = NULL;
+
+ self->indirect_reordering = FALSE;
+}
+
+static void
+detach_into_new_window (AdwTabGrid *self)
+{
+ AdwTabPage *page;
+ AdwTabView *new_view;
+
+ page = self->detached_page;
+
+ new_view = adw_tab_view_create_window (self->view);
+
+ if (ADW_IS_TAB_VIEW (new_view))
+ adw_tab_view_attach_page (new_view, page, 0);
+ else
+ adw_tab_view_attach_page (self->view, page, self->detached_index);
+
+ self->should_detach_into_new_window = FALSE;
+}
+
+static gboolean
+is_view_in_the_same_group (AdwTabGrid *self,
+ AdwTabView *other_view)
+{
+ /* TODO when we have groups, this should do the actual check */
+ return TRUE;
+}
+
+static void
+drag_end (AdwTabGrid *self,
+ GdkDrag *drag,
+ gboolean success)
+{
+ g_signal_handlers_disconnect_by_data (drag, self);
+
+ gdk_drag_drop_done (drag, success);
+
+ if (!success) {
+ adw_tab_view_attach_page (self->view,
+ self->detached_page,
+ self->detached_index);
+
+ self->indirect_reordering = FALSE;
+ }
+
+ self->detached_page = NULL;
+
+ if (self->drag_icon) {
+ g_clear_object (&self->drag_icon->resize_animation);
+ g_clear_pointer (&self->drag_icon, g_free);
+ }
+
+ g_object_unref (drag);
+}
+
+static void
+tab_drop_performed_cb (AdwTabGrid *self,
+ GdkDrag *drag)
+{
+ /* Catch drops into our windows, but outside of tab views. If this is a false
+ * positive, it will be set to FALSE in do_drag_drop(). */
+ self->should_detach_into_new_window = TRUE;
+}
+
+static void
+tab_dnd_finished_cb (AdwTabGrid *self,
+ GdkDrag *drag)
+{
+ if (self->should_detach_into_new_window)
+ detach_into_new_window (self);
+
+ drag_end (self, drag, TRUE);
+}
+
+static void
+tab_drag_cancel_cb (AdwTabGrid *self,
+ GdkDragCancelReason reason,
+ GdkDrag *drag)
+{
+ if (reason == GDK_DRAG_CANCEL_NO_TARGET) {
+ detach_into_new_window (self);
+ drag_end (self, drag, TRUE);
+
+ return;
+ }
+
+ self->should_detach_into_new_window = FALSE;
+ drag_end (self, drag, FALSE);
+}
+
+static void
+icon_resize_animation_value_cb (double value,
+ DragIcon *icon)
+{
+ double relative_x, relative_y;
+
+ relative_x = (double) icon->hotspot_x / icon->width;
+ relative_y = (double) icon->hotspot_y / icon->height;
+
+ icon->width = (int) round (adw_lerp (icon->initial_width, icon->target_width, value));
+ icon->height = (int) round (adw_lerp (icon->initial_height, icon->target_height, value));
+
+ gtk_widget_set_size_request (GTK_WIDGET (icon->tab),
+ icon->width + icon->tab_margin.left + icon->tab_margin.right,
+ icon->height + icon->tab_margin.top + icon->tab_margin.bottom);
+
+ icon->hotspot_x = (int) round (icon->width * relative_x);
+ icon->hotspot_y = (int) round (icon->height * relative_y);
+
+ gdk_drag_set_hotspot (icon->drag,
+ icon->hotspot_x + icon->tab_margin.left,
+ icon->hotspot_y + icon->tab_margin.top);
+
+ gtk_widget_queue_resize (GTK_WIDGET (icon->tab));
+}
+
+static void
+create_drag_icon (AdwTabGrid *self,
+ GdkDrag *drag)
+{
+ DragIcon *icon;
+ AdwAnimationTarget *target;
+
+ icon = g_new0 (DragIcon, 1);
+
+ icon->drag = drag;
+
+ icon->width = self->tab_width;
+ icon->initial_width = icon->width;
+ icon->target_width = icon->width;
+
+ icon->height = self->tab_height;
+ icon->initial_width = icon->height;
+ icon->target_width = icon->height;
+
+ icon->tab = adw_tab_thumbnail_new (self->view, FALSE);
+ adw_tab_thumbnail_set_page (icon->tab, self->reordered_tab->page);
+ adw_tab_thumbnail_set_dragging (icon->tab, TRUE);
+ adw_tab_thumbnail_set_inverted (icon->tab, self->inverted);
+ adw_tab_thumbnail_set_align (icon->tab, self->thumbnail_xalign, self->thumbnail_yalign);
+ gtk_widget_set_halign (GTK_WIDGET (icon->tab), GTK_ALIGN_START);
+
+ gtk_drag_icon_set_child (GTK_DRAG_ICON (gtk_drag_icon_get_for_drag (drag)),
+ GTK_WIDGET (icon->tab));
+
+ gtk_style_context_get_margin (gtk_widget_get_style_context (GTK_WIDGET (icon->tab)),
+ &icon->tab_margin);
+
+ gtk_widget_set_size_request (GTK_WIDGET (icon->tab),
+ icon->width + icon->tab_margin.left + icon->tab_margin.right,
+ icon->height + icon->tab_margin.top + icon->tab_margin.bottom);
+
+ icon->hotspot_x = (int) self->drag_offset_x;
+ icon->hotspot_y = (int) self->drag_offset_y;
+
+ gdk_drag_set_hotspot (drag,
+ icon->hotspot_x + icon->tab_margin.left,
+ icon->hotspot_y + icon->tab_margin.top);
+
+ target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+ icon_resize_animation_value_cb,
+ icon, NULL);
+ icon->resize_animation =
+ adw_timed_animation_new (GTK_WIDGET (icon->tab), 0, 1,
+ ICON_RESIZE_ANIMATION_DURATION, target);
+
+ self->drag_icon = icon;
+}
+
+static void
+resize_drag_icon (AdwTabGrid *self,
+ int width,
+ int height)
+{
+ DragIcon *icon = self->drag_icon;
+
+ if (width == icon->target_width && height == icon->target_height)
+ return;
+
+ icon->initial_width = icon->width;
+ icon->initial_height = icon->height;
+
+ icon->target_width = width;
+ icon->target_height = height;
+
+ adw_animation_play (icon->resize_animation);
+}
+
+static void
+begin_drag (AdwTabGrid *self,
+ GdkDevice *device)
+{
+ GdkContentProvider *content;
+ GtkNative *native;
+ GdkSurface *surface;
+ GdkDrag *drag;
+ TabInfo *detached_info;
+ GtkWidget *detached_tab;
+
+ native = gtk_widget_get_native (GTK_WIDGET (self));
+ surface = gtk_native_get_surface (native);
+
+ self->hovering = TRUE;
+ self->pressed_tab = NULL;
+
+ detached_info = self->reordered_tab;
+ detached_tab = g_object_ref (detached_info->container);
+ self->detached_page = detached_info->page;
+
+ self->indirect_reordering = TRUE;
+
+ content = gdk_content_provider_new_union ((GdkContentProvider *[2]) {
+ adw_tab_grid_root_content_new (self),
+ gdk_content_provider_new_typed (ADW_TYPE_TAB_PAGE,
detached_info->page)
+ }, 2);
+
+ drag = gdk_drag_begin (surface, device, content, GDK_ACTION_MOVE,
+ self->reorder_x, self->reorder_y);
+
+ g_object_set_data (G_OBJECT (drag), "adw-tab-overview-drag-origin", self);
+
+ g_signal_connect_swapped (drag, "drop-performed",
+ G_CALLBACK (tab_drop_performed_cb), self);
+ g_signal_connect_swapped (drag, "dnd-finished",
+ G_CALLBACK (tab_dnd_finished_cb), self);
+ g_signal_connect_swapped (drag, "cancel",
+ G_CALLBACK (tab_drag_cancel_cb), self);
+
+ create_drag_icon (self, drag);
+
+ end_drag_reodering (self);
+ update_hover (self);
+
+ gtk_widget_set_opacity (detached_tab, 0);
+ self->detached_index = adw_tab_view_get_page_position (self->view, self->detached_page);
+
+ adw_tab_view_detach_page (self->view, self->detached_page);
+
+ self->indirect_reordering = FALSE;
+
+ gtk_widget_measure (detached_tab, GTK_ORIENTATION_HORIZONTAL, -1,
+ NULL, &self->placeholder_scroll_offset, NULL, NULL);
+ self->placeholder_scroll_offset /= 2;
+
+ animate_scroll_relative (self, -self->placeholder_scroll_offset, CLOSE_ANIMATION_DURATION);
+
+ g_object_unref (content);
+ g_object_unref (detached_tab);
+}
+
+static GdkDragAction
+tab_drag_enter_motion_cb (AdwTabGrid *self,
+ double x,
+ double y,
+ GtkDropTarget *target)
+{
+ AdwTabGrid *source_tab_grid;
+
+ if (self->pinned)
+ return 0;
+
+ source_tab_grid = get_source_tab_grid (target);
+
+ if (!source_tab_grid)
+ return 0;
+
+ if (!self->view || !is_view_in_the_same_group (self, source_tab_grid->view))
+ return 0;
+
+ y += gtk_adjustment_get_value (self->adjustment);
+
+ self->can_remove_placeholder = FALSE;
+
+ if (!self->reorder_placeholder || !self->reorder_placeholder->page) {
+ AdwTabPage *page = source_tab_grid->detached_page;
+ double center_x = x - source_tab_grid->drag_icon->hotspot_x + source_tab_grid->drag_icon->width / 2;
+ double center_y = y - source_tab_grid->drag_icon->hotspot_y + source_tab_grid->drag_icon->height / 2;
+
+ insert_placeholder (self, page, center_x, center_y);
+
+ self->indirect_reordering = TRUE;
+
+ resize_drag_icon (source_tab_grid, self->tab_width, self->tab_height);
+ adw_tab_thumbnail_set_inverted (source_tab_grid->drag_icon->tab, self->inverted);
+ adw_tab_thumbnail_set_align (source_tab_grid->drag_icon->tab, self->thumbnail_xalign,
self->thumbnail_yalign);
+
+ self->drag_offset_x = source_tab_grid->drag_icon->hotspot_x;
+ self->drag_offset_y = source_tab_grid->drag_icon->hotspot_y;
+
+ self->reorder_x = (int) round (x - source_tab_grid->drag_icon->hotspot_x);
+ self->reorder_y = (int) round (y - source_tab_grid->drag_icon->hotspot_y);
+
+ start_drag_reodering (self, self->reorder_placeholder, x, y);
+
+ return GDK_ACTION_MOVE;
+ }
+
+ self->reorder_x = (int) round (x - source_tab_grid->drag_icon->hotspot_x);
+ self->reorder_y = (int) round (y - source_tab_grid->drag_icon->hotspot_y);
+
+ update_drag_reodering (self);
+
+ return GDK_ACTION_MOVE;
+}
+
+static void
+tab_drag_leave_cb (AdwTabGrid *self,
+ GtkDropTarget *target)
+{
+ AdwTabGrid *source_tab_grid;
+
+ if (!self->indirect_reordering)
+ return;
+
+ if (self->pinned)
+ return;
+
+ source_tab_grid = get_source_tab_grid (target);
+
+ if (!source_tab_grid)
+ return;
+
+ if (!self->view || !is_view_in_the_same_group (self, source_tab_grid->view))
+ return;
+
+ self->can_remove_placeholder = TRUE;
+
+ end_drag_reodering (self);
+ remove_placeholder (self);
+
+ self->indirect_reordering = FALSE;
+}
+
+static gboolean
+tab_drag_drop_cb (AdwTabGrid *self,
+ const GValue *value,
+ double x,
+ double y,
+ GtkDropTarget *target)
+{
+ AdwTabGrid *source_tab_grid;
+
+ if (self->pinned)
+ return GDK_EVENT_PROPAGATE;
+
+ source_tab_grid = get_source_tab_grid (target);
+
+ if (!source_tab_grid)
+ return GDK_EVENT_PROPAGATE;
+
+ if (!self->view || !is_view_in_the_same_group (self, source_tab_grid->view))
+ return GDK_EVENT_PROPAGATE;
+
+ do_drag_drop (self, source_tab_grid);
+
+ return GDK_EVENT_STOP;
+}
+
+static gboolean
+view_drag_drop_cb (AdwTabGrid *self,
+ const GValue *value,
+ double x,
+ double y,
+ GtkDropTarget *target)
+{
+ AdwTabGrid *source_tab_grid;
+
+ if (self->pinned)
+ return GDK_EVENT_PROPAGATE;
+
+ source_tab_grid = get_source_tab_grid (target);
+
+ if (!source_tab_grid)
+ return GDK_EVENT_PROPAGATE;
+
+ if (!self->view || !is_view_in_the_same_group (self, source_tab_grid->view))
+ return GDK_EVENT_PROPAGATE;
+
+ self->reorder_index = adw_tab_view_get_n_pages (self->view) -
+ adw_tab_view_get_n_pinned_pages (self->view);
+
+ do_drag_drop (self, source_tab_grid);
+
+ return GDK_EVENT_STOP;
+}
+
+/* DND autoscrolling */
+
+static gboolean
+reset_drop_target_tab_cb (AdwTabGrid *self)
+{
+ self->reset_drop_target_tab_id = 0;
+ set_drop_target_tab (self, NULL);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+drag_leave_cb (AdwTabGrid *self,
+ GtkDropControllerMotion *controller)
+{
+ GdkDrop *drop = gtk_drop_controller_motion_get_drop (controller);
+ GdkDrag *drag = gdk_drop_get_drag (drop);
+ AdwTabGrid *source = ADW_TAB_GRID (g_object_get_data (G_OBJECT (drag),
+ "adw-tab-overview-drag-origin"));
+
+ if (source)
+ return;
+
+ if (!self->reset_drop_target_tab_id)
+ self->reset_drop_target_tab_id =
+ g_idle_add ((GSourceFunc) reset_drop_target_tab_cb, self);
+
+ end_autoscroll (self);
+}
+
+static void
+drag_enter_motion_cb (AdwTabGrid *self,
+ double x,
+ double y,
+ GtkDropControllerMotion *controller)
+{
+ TabInfo *info;
+ GdkDrop *drop = gtk_drop_controller_motion_get_drop (controller);
+ GdkDrag *drag = gdk_drop_get_drag (drop);
+ AdwTabGrid *source = ADW_TAB_GRID (g_object_get_data (G_OBJECT (drag),
+ "adw-tab-overview-drag-origin"));
+
+ if (source)
+ return;
+
+ y += gtk_adjustment_get_value (self->adjustment);
+
+ info = find_tab_info_at (self, x, y);
+
+ if (!info) {
+ drag_leave_cb (self, controller);
+
+ return;
+ }
+
+ self->drop_target_x = x;
+ self->drop_target_y = y;
+ set_drop_target_tab (self, info);
+
+ start_autoscroll (self);
+}
+
+/* Context menu */
+
+static gboolean
+reset_setup_menu_cb (AdwTabGrid *self)
+{
+ g_signal_emit_by_name (self->view, "setup-menu", NULL);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+touch_menu_notify_visible_cb (AdwTabGrid *self)
+{
+ if (!self->context_menu || gtk_widget_get_visible (self->context_menu))
+ return;
+
+ self->hovering = FALSE;
+ update_hover (self);
+
+ g_idle_add ((GSourceFunc) reset_setup_menu_cb, self);
+}
+
+static void
+do_popup (AdwTabGrid *self,
+ TabInfo *info,
+ double x,
+ double y)
+{
+ GMenuModel *model = adw_tab_view_get_menu_model (self->view);
+ GdkRectangle rect;
+
+ if (!G_IS_MENU_MODEL (model))
+ return;
+
+ g_signal_emit_by_name (self->view, "setup-menu", info->page);
+
+ if (!self->context_menu) {
+ self->context_menu = gtk_popover_menu_new_from_model (model);
+ gtk_widget_set_parent (self->context_menu, GTK_WIDGET (self));
+ gtk_popover_set_position (GTK_POPOVER (self->context_menu), GTK_POS_BOTTOM);
+ gtk_popover_set_has_arrow (GTK_POPOVER (self->context_menu), FALSE);
+
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ gtk_widget_set_halign (self->context_menu, GTK_ALIGN_END);
+ else
+ gtk_widget_set_halign (self->context_menu, GTK_ALIGN_START);
+
+ g_signal_connect_object (self->context_menu, "notify::visible",
+ G_CALLBACK (touch_menu_notify_visible_cb), self,
+ G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+ }
+
+ if (x >= 0 && y >= 0) {
+ rect.x = x;
+ rect.y = y;
+ } else {
+ rect.x = info->pos_x;
+ rect.y = info->pos_y + gtk_widget_get_allocated_height (GTK_WIDGET (info->container));
+
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ rect.x += info->width;
+ }
+
+ rect.y -= gtk_adjustment_get_value (self->adjustment);
+ rect.width = 0;
+ rect.height = 0;
+
+ gtk_popover_set_pointing_to (GTK_POPOVER (self->context_menu), &rect);
+
+ gtk_popover_popup (GTK_POPOVER (self->context_menu));
+}
+
+static void
+long_pressed_cb (AdwTabGrid *self,
+ double x,
+ double y,
+ GtkGesture *gesture)
+{
+ TabInfo *info = find_tab_info_at (self, x, y);
+
+ gtk_gesture_set_state (self->drag_gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ if (!info || !info->page) {
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ y += gtk_adjustment_get_value (self->adjustment);
+
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+ do_popup (self, self->pressed_tab, x, y);
+}
+
+static void
+popup_menu_cb (GtkWidget *widget,
+ const char *action_name,
+ GVariant *parameter)
+{
+ AdwTabGrid *self = ADW_TAB_GRID (widget);
+
+ if (self->selected_tab && self->selected_tab->page)
+ do_popup (self, self->selected_tab, -1, -1);
+}
+
+/* Clicking */
+
+static void
+handle_click (AdwTabGrid *self,
+ TabInfo *info,
+ GtkGesture *gesture)
+{
+ gboolean can_grab_focus;
+
+ can_grab_focus = adw_tab_overview_tabs_have_visible_focus (self->tab_overview);
+
+ if (info == self->selected_tab)
+ can_grab_focus = TRUE;
+ else
+ adw_tab_view_set_selected_page (self->view, info->page);
+
+ if (can_grab_focus)
+ gtk_widget_grab_focus (info->container);
+ else
+ activate_tab (self);
+}
+
+static void
+pressed_cb (AdwTabGrid *self,
+ int n_press,
+ double x,
+ double y,
+ GtkGesture *gesture)
+{
+ TabInfo *info;
+ GdkEvent *event;
+ GdkEventSequence *current;
+ guint button;
+
+ if (is_touchscreen (gesture))
+ return;
+
+ y += gtk_adjustment_get_value (self->adjustment);
+
+ info = find_tab_info_at (self, x, y);
+
+ if (!info || !info->page) {
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ return;
+ }
+
+ current = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
+ event = gtk_gesture_get_last_event (gesture, current);
+
+ if (gdk_event_triggers_context_menu (event)) {
+ do_popup (self, info, x, y);
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+ gtk_event_controller_reset (GTK_EVENT_CONTROLLER (gesture));
+
+ return;
+ }
+
+ button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
+
+ if (button == GDK_BUTTON_MIDDLE) {
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+
+ return;
+ }
+
+ if (button != GDK_BUTTON_PRIMARY) {
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ return;
+ }
+
+ handle_click (self, info, gesture);
+}
+
+static void
+released_cb (AdwTabGrid *self,
+ int n_press,
+ double x,
+ double y,
+ GtkGesture *gesture)
+{
+ TabInfo *info;
+ guint button;
+
+ y += gtk_adjustment_get_value (self->adjustment);
+
+ if (x < 0 || x > gtk_widget_get_width (GTK_WIDGET (self))) {
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ return;
+ }
+
+ info = find_tab_info_at (self, x, y);
+
+ if (!info || !info->page) {
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ return;
+ }
+
+ button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
+
+ if (button == GDK_BUTTON_MIDDLE) {
+ adw_tab_view_close_page (self->view, info->page);
+
+ return;
+ }
+
+ if (is_touchscreen (gesture))
+ handle_click (self, info, gesture);
+
+ adw_tab_overview_set_open (self->tab_overview, FALSE);
+}
+
+/* Overrides */
+
+static void
+measure_tab_grid (AdwTabGrid *self,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ gboolean animated)
+{
+ int min, nat;
+
+ if (self->n_tabs == 0) {
+ if (minimum)
+ *minimum = 0;
+
+ if (natural)
+ *natural = 0;
+
+ return;
+ }
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ GList *l;
+ int indicator_min;
+ min = nat = 0;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+ int child_min, child_nat;
+
+ 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);
+
+ nat += child_nat + SPACING;
+ }
+
+ nat += SPACING;
+ min += SPACING * 2;
+
+ gtk_widget_measure (self->needs_attention_top, orientation, -1,
+ &indicator_min, NULL, NULL, NULL);
+
+ min = MAX (min, indicator_min);
+
+ gtk_widget_measure (self->needs_attention_bottom, orientation, -1,
+ &indicator_min, NULL, NULL, NULL);
+
+ min = MAX (min, indicator_min);
+ } else {
+ double n_columns, n_rows;
+ int child_width = -1, child_height;
+ double index = 0;
+ GList *l;
+ 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 (animated) {
+ index += info->appear_progress;
+ } else {
+ if (info->page)
+ index++;
+ }
+ }
+
+ n_columns = get_n_columns (self, for_size);
+ n_rows = ceil (index / n_columns);
+
+ if (animated) {
+ double col = fmod (index, n_columns);
+
+ if (col > 0 && col < 1)
+ n_rows = n_rows + col - 1;
+ }
+
+ height = (child_height + SPACING) * n_rows + SPACING + self->end_padding;
+ height = MAX (self->last_height, height);
+
+ min = nat = height;
+ }
+
+ if (minimum)
+ *minimum = min;
+
+ if (natural)
+ *natural = nat;
+}
+
+static void
+adw_tab_grid_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ AdwTabGrid *self = ADW_TAB_GRID (widget);
+
+ measure_tab_grid (self, orientation, for_size, minimum, natural, TRUE);
+
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+
+ if (natural_baseline)
+ *natural_baseline = -1;
+}
+
+static void
+adw_tab_grid_size_allocate (GtkWidget *widget,
+ int width,
+ int height,
+ int baseline)
+{
+ AdwTabGrid *self = ADW_TAB_GRID (widget);
+ gboolean is_rtl;
+ GList *l;
+ GtkAllocation child_allocation;
+ double value;
+ int indicator_size;
+ GskTransform *transform;
+ double index = 0, final_index = 0;
+
+ self->n_columns = get_n_columns (self, width);
+
+ adw_tab_grid_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ &self->allocated_width, NULL, NULL, NULL);
+ self->allocated_width = MAX (self->allocated_width, width);
+
+ adw_tab_grid_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ &self->allocated_height, NULL, NULL, NULL);
+ self->allocated_height = MAX (self->allocated_height, height);
+
+ if (self->context_menu)
+ gtk_popover_present (GTK_POPOVER (self->context_menu));
+
+ if (!self->n_tabs)
+ return;
+
+ is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
+
+ self->tab_width = get_tab_width (self, width);
+ self->tab_height = get_tab_height (self, self->tab_width);
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ get_position_for_index (self, final_index, is_rtl,
+ &info->unshifted_x, &info->unshifted_y);
+ get_position_for_index (self, index + info->reorder_offset, is_rtl,
+ &info->pos_x, &info->pos_y);
+ get_position_for_index (self, final_index + info->end_reorder_offset, is_rtl,
+ &info->final_x, &info->final_y);
+
+ info->width = calculate_tab_width (info, self->tab_width);
+ info->final_width = self->tab_width;
+
+ info->height = self->tab_height;
+ info->final_height = self->tab_height;
+
+ info->index = index;
+ info->final_index = final_index;
+
+ index += info->appear_progress;
+ final_index++;
+
+ 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;
+ }
+ }
+
+ value = get_scroll_animation_value (self);
+
+ self->block_scrolling = TRUE;
+ gtk_adjustment_configure (self->adjustment,
+ value,
+ 0,
+ self->allocated_height,
+ height * 0.1,
+ height * 0.9,
+ height);
+ self->block_scrolling = FALSE;
+
+ /* The value may have been changed during gtk_adjustment_configure() */
+ value = gtk_adjustment_get_value (self->adjustment);
+
+ if (self->scroll_animation_done) {
+ self->scroll_animation_tab = NULL;
+ self->scroll_animation_done = FALSE;
+ adw_animation_reset (self->scroll_animation);
+ }
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ child_allocation.x = ((info == self->reordered_tab) ? self->reorder_window_x : info->pos_x);
+ child_allocation.y = ((info == self->reordered_tab) ? self->reorder_window_y : info->pos_y) - (int)
floor (value);
+ child_allocation.width = MAX (0, info->width);
+ child_allocation.height = MAX (0, info->height);
+
+ gtk_widget_size_allocate (info->container, &child_allocation, baseline);
+ }
+
+ gtk_widget_measure (self->needs_attention_top, GTK_ORIENTATION_VERTICAL, -1,
+ &indicator_size, NULL, NULL, NULL);
+ gtk_widget_allocate (self->needs_attention_top, width, indicator_size, baseline, NULL);
+
+ gtk_widget_measure (self->needs_attention_bottom, GTK_ORIENTATION_VERTICAL, -1,
+ &indicator_size, NULL, NULL, NULL);
+ transform = gsk_transform_translate (NULL, &GRAPHENE_POINT_INIT (0, height - indicator_size));
+ gtk_widget_allocate (self->needs_attention_bottom, width, indicator_size, baseline, transform);
+
+ update_visible (self);
+}
+
+static GtkSizeRequestMode
+adw_tab_grid_get_request_mode (GtkWidget *widget)
+{
+ return GTK_SIZE_REQUEST_HEIGHT_FOR_WIDTH;
+}
+
+static gboolean
+adw_tab_grid_focus (GtkWidget *widget,
+ GtkDirectionType direction)
+{
+ AdwTabGrid *self = ADW_TAB_GRID (widget);
+
+ if (!self->selected_tab)
+ return GDK_EVENT_PROPAGATE;
+
+ return gtk_widget_grab_focus (self->selected_tab->container);
+}
+
+static void
+adw_tab_grid_unrealize (GtkWidget *widget)
+{
+ AdwTabGrid *self = ADW_TAB_GRID (widget);
+
+ g_clear_pointer (&self->context_menu, gtk_widget_unparent);
+
+ GTK_WIDGET_CLASS (adw_tab_grid_parent_class)->unrealize (widget);
+}
+
+static void
+adw_tab_grid_unmap (GtkWidget *widget)
+{
+ AdwTabGrid *self = ADW_TAB_GRID (widget);
+
+ force_end_reordering (self);
+
+ if (self->drag_autoscroll_cb_id) {
+ gtk_widget_remove_tick_callback (widget, self->drag_autoscroll_cb_id);
+ self->drag_autoscroll_cb_id = 0;
+ }
+
+ self->hovering = FALSE;
+ update_hover (self);
+
+ GTK_WIDGET_CLASS (adw_tab_grid_parent_class)->unmap (widget);
+}
+
+static void
+adw_tab_grid_direction_changed (GtkWidget *widget,
+ GtkTextDirection previous_direction)
+{
+ AdwTabGrid *self = ADW_TAB_GRID (widget);
+
+ if (gtk_widget_get_direction (widget) == previous_direction)
+ return;
+
+ if (self->context_menu) {
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ gtk_widget_set_halign (self->context_menu, GTK_ALIGN_END);
+ else
+ gtk_widget_set_halign (self->context_menu, GTK_ALIGN_START);
+ }
+}
+
+static void
+adw_tab_grid_dispose (GObject *object)
+{
+ AdwTabGrid *self = ADW_TAB_GRID (object);
+
+ g_clear_handle_id (&self->drop_switch_timeout_id, g_source_remove);
+
+ self->drag_gesture = NULL;
+ self->tab_overview = NULL;
+ adw_tab_grid_set_view (self, NULL);
+ set_vadjustment (self, NULL);
+
+ g_clear_object (&self->resize_animation);
+ g_clear_object (&self->scroll_animation);
+
+ g_clear_pointer (&self->needs_attention_top, gtk_widget_unparent);
+ g_clear_pointer (&self->needs_attention_bottom, gtk_widget_unparent);
+
+ G_OBJECT_CLASS (adw_tab_grid_parent_class)->dispose (object);
+}
+
+static void
+adw_tab_grid_finalize (GObject *object)
+{
+ AdwTabGrid *self = (AdwTabGrid *) object;
+
+ g_clear_pointer (&self->extra_drag_types, g_free);
+
+ G_OBJECT_CLASS (adw_tab_grid_parent_class)->finalize (object);
+}
+
+static void
+adw_tab_grid_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ AdwTabGrid *self = ADW_TAB_GRID (object);
+
+ switch (prop_id) {
+ case PROP_PINNED:
+ g_value_set_boolean (value, self->pinned);
+ break;
+
+ case PROP_TAB_OVERVIEW:
+ g_value_set_object (value, self->tab_overview);
+ break;
+
+ case PROP_VIEW:
+ g_value_set_object (value, self->view);
+ break;
+
+ case PROP_RESIZE_FROZEN:
+ g_value_set_boolean (value, self->tab_resize_mode != TAB_RESIZE_NORMAL);
+ break;
+
+ case PROP_VADJUSTMENT:
+ g_value_set_object (value, self->adjustment);
+ break;
+
+ case PROP_HADJUSTMENT:
+ case PROP_HSCROLL_POLICY:
+ case PROP_VSCROLL_POLICY:
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+adw_tab_grid_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ AdwTabGrid *self = ADW_TAB_GRID (object);
+
+ switch (prop_id) {
+ case PROP_PINNED:
+ self->pinned = g_value_get_boolean (value);
+ break;
+
+ case PROP_TAB_OVERVIEW:
+ self->tab_overview = g_value_get_object (value);
+ break;
+
+ case PROP_VIEW:
+ adw_tab_grid_set_view (self, g_value_get_object (value));
+ break;
+
+ case PROP_VADJUSTMENT:
+ set_vadjustment (self, g_value_get_object (value));
+ break;
+
+ case PROP_HADJUSTMENT:
+ case PROP_HSCROLL_POLICY:
+ case PROP_VSCROLL_POLICY:
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+adw_tab_grid_class_init (AdwTabGridClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = adw_tab_grid_dispose;
+ object_class->finalize = adw_tab_grid_finalize;
+ object_class->get_property = adw_tab_grid_get_property;
+ object_class->set_property = adw_tab_grid_set_property;
+
+ widget_class->measure = adw_tab_grid_measure;
+ widget_class->size_allocate = adw_tab_grid_size_allocate;
+ widget_class->get_request_mode = adw_tab_grid_get_request_mode;
+ widget_class->focus = adw_tab_grid_focus;
+ widget_class->unrealize = adw_tab_grid_unrealize;
+ widget_class->unmap = adw_tab_grid_unmap;
+ widget_class->direction_changed = adw_tab_grid_direction_changed;
+
+ props[PROP_PINNED] =
+ g_param_spec_boolean ("pinned", NULL, NULL,
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ props[PROP_TAB_OVERVIEW] =
+ g_param_spec_object ("tab-overview", NULL, NULL,
+ ADW_TYPE_TAB_OVERVIEW,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ props[PROP_VIEW] =
+ g_param_spec_object ("view", NULL, NULL,
+ ADW_TYPE_TAB_VIEW,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_RESIZE_FROZEN] =
+ g_param_spec_boolean ("resize-frozen", NULL, NULL,
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ g_object_class_override_property (object_class, PROP_HADJUSTMENT, "hadjustment");
+ g_object_class_override_property (object_class, PROP_VADJUSTMENT, "vadjustment");
+ g_object_class_override_property (object_class, PROP_HSCROLL_POLICY, "hscroll-policy");
+ g_object_class_override_property (object_class, PROP_VSCROLL_POLICY, "vscroll-policy");
+
+ signals[SIGNAL_STOP_KINETIC_SCROLLING] =
+ g_signal_new ("stop-kinetic-scrolling",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+
+ signals[SIGNAL_EXTRA_DRAG_DROP] =
+ g_signal_new ("extra-drag-drop",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ g_signal_accumulator_first_wins, NULL, NULL,
+ G_TYPE_BOOLEAN,
+ 2,
+ ADW_TYPE_TAB_PAGE,
+ G_TYPE_VALUE);
+
+ gtk_widget_class_install_action (widget_class, "menu.popup", NULL, popup_menu_cb);
+
+ gtk_widget_class_add_binding_action (widget_class, GDK_KEY_F10, GDK_SHIFT_MASK, "menu.popup", NULL);
+ gtk_widget_class_add_binding_action (widget_class, GDK_KEY_Menu, 0, "menu.popup", NULL);
+
+ add_focus_bindings (widget_class, GDK_KEY_Page_Up, GTK_DIR_TAB_BACKWARD, FALSE);
+ add_focus_bindings (widget_class, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD, FALSE);
+ add_focus_bindings (widget_class, GDK_KEY_Home, GTK_DIR_TAB_BACKWARD, TRUE);
+ add_focus_bindings (widget_class, GDK_KEY_End, GTK_DIR_TAB_FORWARD, TRUE);
+
+ add_reorder_bindings (widget_class, GDK_KEY_Left, GTK_DIR_LEFT, FALSE);
+ add_reorder_bindings (widget_class, GDK_KEY_Right, GTK_DIR_RIGHT, FALSE);
+ add_reorder_bindings (widget_class, GDK_KEY_Page_Up, GTK_DIR_TAB_BACKWARD, FALSE);
+ add_reorder_bindings (widget_class, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD, FALSE);
+ add_reorder_bindings (widget_class, GDK_KEY_Home, GTK_DIR_TAB_BACKWARD, TRUE);
+ add_reorder_bindings (widget_class, GDK_KEY_End, GTK_DIR_TAB_FORWARD, TRUE);
+
+ gtk_widget_class_set_css_name (widget_class, "tabgrid");
+}
+
+static void
+adw_tab_grid_init (AdwTabGrid *self)
+{
+ GtkEventController *controller;
+ AdwAnimationTarget *target;
+ GtkWidget *widget;
+
+ self->can_remove_placeholder = TRUE;
+ self->thumbnail_xalign = 0.5;
+ self->thumbnail_yalign = 0.5;
+
+ gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN);
+
+ controller = gtk_event_controller_motion_new ();
+ g_signal_connect_swapped (controller, "motion", G_CALLBACK (motion_cb), self);
+ g_signal_connect_swapped (controller, "leave", G_CALLBACK (leave_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+ controller = GTK_EVENT_CONTROLLER (gtk_gesture_click_new ());
+ gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (controller), 0);
+ gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (controller), TRUE);
+ g_signal_connect_swapped (controller, "pressed", G_CALLBACK (pressed_cb), self);
+ g_signal_connect_swapped (controller, "released", G_CALLBACK (released_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+ controller = GTK_EVENT_CONTROLLER (gtk_gesture_long_press_new ());
+ gtk_gesture_long_press_set_delay_factor (GTK_GESTURE_LONG_PRESS (controller), 2);
+ gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (controller), TRUE);
+ gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (controller), TRUE);
+ g_signal_connect_swapped (controller, "pressed", G_CALLBACK (long_pressed_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+ controller = GTK_EVENT_CONTROLLER (gtk_gesture_drag_new ());
+ gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (controller), GDK_BUTTON_PRIMARY);
+ gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (controller), TRUE);
+ g_signal_connect_swapped (controller, "drag-begin", G_CALLBACK (reorder_begin_cb), self);
+ g_signal_connect_swapped (controller, "drag-update", G_CALLBACK (reorder_update_cb), self);
+ g_signal_connect_swapped (controller, "drag-end", G_CALLBACK (reorder_end_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), controller);
+ self->drag_gesture = GTK_GESTURE (controller);
+
+ controller = gtk_drop_controller_motion_new ();
+ g_signal_connect_swapped (controller, "enter", G_CALLBACK (drag_enter_motion_cb), self);
+ g_signal_connect_swapped (controller, "motion", G_CALLBACK (drag_enter_motion_cb), self);
+ g_signal_connect_swapped (controller, "leave", G_CALLBACK (drag_leave_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+ controller = GTK_EVENT_CONTROLLER (gtk_drop_target_new (ADW_TYPE_TAB_PAGE, GDK_ACTION_MOVE));
+ gtk_drop_target_set_preload (GTK_DROP_TARGET (controller), TRUE);
+ g_signal_connect_swapped (controller, "enter", G_CALLBACK (tab_drag_enter_motion_cb), self);
+ g_signal_connect_swapped (controller, "motion", G_CALLBACK (tab_drag_enter_motion_cb), self);
+ g_signal_connect_swapped (controller, "leave", G_CALLBACK (tab_drag_leave_cb), self);
+ g_signal_connect_swapped (controller, "drop", G_CALLBACK (tab_drag_drop_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+ target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+ resize_animation_value_cb,
+ self, NULL);
+ self->resize_animation =
+ adw_timed_animation_new (GTK_WIDGET (self), 0, 1,
+ RESIZE_ANIMATION_DURATION, target);
+
+ g_signal_connect_swapped (self->resize_animation, "done",
+ G_CALLBACK (resize_animation_done_cb), self);
+
+ /* The actual update will be done in size_allocate(). After the animation
+ * finishes, don't remove it right away, it will be done in size-allocate as
+ * well after one last update, so that we don't miss the last frame.
+ */
+ target = adw_callback_animation_target_new ((AdwAnimationTargetFunc)
+ scroll_animation_cb,
+ self, NULL);
+ self->scroll_animation =
+ adw_timed_animation_new (GTK_WIDGET (self), 0, 1,
+ SCROLL_ANIMATION_DURATION, target);
+ g_signal_connect_swapped (self->scroll_animation, "done",
+ G_CALLBACK (scroll_animation_done_cb), self);
+
+ self->needs_attention_top = gtk_revealer_new ();
+ gtk_revealer_set_transition_type (GTK_REVEALER (self->needs_attention_top),
+ GTK_REVEALER_TRANSITION_TYPE_CROSSFADE);
+ gtk_widget_set_can_target (self->needs_attention_top, FALSE);
+ gtk_widget_set_can_focus (self->needs_attention_top, FALSE);
+ gtk_widget_set_parent (self->needs_attention_top, GTK_WIDGET (self));
+
+ widget = adw_gizmo_new ("indicator", NULL, NULL, NULL, NULL, NULL, NULL);
+ gtk_widget_add_css_class (widget, "top");
+ gtk_revealer_set_child (GTK_REVEALER (self->needs_attention_top), widget);
+
+ self->needs_attention_bottom = gtk_revealer_new ();
+ gtk_revealer_set_transition_type (GTK_REVEALER (self->needs_attention_bottom),
+ GTK_REVEALER_TRANSITION_TYPE_CROSSFADE);
+ gtk_widget_set_can_target (self->needs_attention_bottom, FALSE);
+ gtk_widget_set_can_focus (self->needs_attention_bottom, FALSE);
+ gtk_widget_set_parent (self->needs_attention_bottom, GTK_WIDGET (self));
+
+ widget = adw_gizmo_new ("indicator", NULL, NULL, NULL, NULL, NULL, NULL);
+ gtk_widget_add_css_class (widget, "bottom");
+ gtk_revealer_set_child (GTK_REVEALER (self->needs_attention_bottom), widget);
+}
+
+void
+adw_tab_grid_set_view (AdwTabGrid *self,
+ AdwTabView *view)
+{
+ g_return_if_fail (ADW_IS_TAB_GRID (self));
+ g_return_if_fail (view == NULL || ADW_IS_TAB_VIEW (view));
+
+ if (view == self->view)
+ return;
+
+ if (self->view) {
+ force_end_reordering (self);
+ g_signal_handlers_disconnect_by_func (self->view, page_attached_cb, self);
+ g_signal_handlers_disconnect_by_func (self->view, page_detached_cb, self);
+ g_signal_handlers_disconnect_by_func (self->view, page_reordered_cb, self);
+
+ if (!self->pinned) {
+ gtk_widget_remove_controller (GTK_WIDGET (self->view), self->view_drop_target);
+ self->view_drop_target = NULL;
+ }
+
+ g_clear_list (&self->tabs, (GDestroyNotify) remove_and_free_tab_info);
+ self->n_tabs = 0;
+ }
+
+ self->view = view;
+
+ if (self->view) {
+ int i, n_pages = adw_tab_view_get_n_pages (self->view);
+
+ for (i = n_pages - 1; i >= 0; i--)
+ page_attached_cb (self, adw_tab_view_get_nth_page (self->view, i), 0);
+
+ g_signal_connect_object (self->view, "page-attached", G_CALLBACK (page_attached_cb), self,
G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->view, "page-detached", G_CALLBACK (page_detached_cb), self,
G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->view, "page-reordered", G_CALLBACK (page_reordered_cb), self,
G_CONNECT_SWAPPED);
+
+ if (!self->pinned) {
+ self->view_drop_target = GTK_EVENT_CONTROLLER (gtk_drop_target_new (ADW_TYPE_TAB_PAGE,
GDK_ACTION_MOVE));
+
+ g_signal_connect_object (self->view_drop_target, "drop", G_CALLBACK (view_drag_drop_cb), self,
G_CONNECT_SWAPPED);
+
+ gtk_widget_add_controller (GTK_WIDGET (self->view), self->view_drop_target);
+ }
+ }
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW]);
+}
+
+void
+adw_tab_grid_attach_page (AdwTabGrid *self,
+ AdwTabPage *page,
+ int position)
+{
+ g_return_if_fail (ADW_IS_TAB_GRID (self));
+ g_return_if_fail (ADW_IS_TAB_PAGE (page));
+
+ page_attached_cb (self, page, position);
+}
+
+void
+adw_tab_grid_detach_page (AdwTabGrid *self,
+ AdwTabPage *page)
+{
+ g_return_if_fail (ADW_IS_TAB_GRID (self));
+ g_return_if_fail (ADW_IS_TAB_PAGE (page));
+
+ page_detached_cb (self, page);
+}
+
+void
+adw_tab_grid_select_page (AdwTabGrid *self,
+ AdwTabPage *page)
+{
+ g_return_if_fail (ADW_IS_TAB_GRID (self));
+ g_return_if_fail (page == NULL || ADW_IS_TAB_PAGE (page));
+
+ select_page (self, page);
+}
+
+void
+adw_tab_grid_try_focus_selected_tab (AdwTabGrid *self)
+{
+ g_return_if_fail (ADW_IS_TAB_GRID (self));
+
+ if (self->selected_tab)
+ gtk_widget_grab_focus (self->selected_tab->container);
+}
+
+gboolean
+adw_tab_grid_is_page_focused (AdwTabGrid *self,
+ AdwTabPage *page)
+{
+ TabInfo *info;
+
+ g_return_val_if_fail (ADW_IS_TAB_GRID (self), FALSE);
+ g_return_val_if_fail (ADW_IS_TAB_PAGE (page), FALSE);
+
+ info = find_info_for_page (self, page);
+
+ return info && gtk_widget_is_focus (info->container);
+}
+
+void
+adw_tab_grid_setup_extra_drop_target (AdwTabGrid *self,
+ GdkDragAction actions,
+ GType *types,
+ gsize n_types)
+{
+ GList *l;
+
+ g_return_if_fail (ADW_IS_TAB_GRID (self));
+ g_return_if_fail (n_types == 0 || types != NULL);
+
+ g_clear_pointer (&self->extra_drag_types, g_free);
+
+ self->extra_drag_actions = actions;
+#if GLIB_CHECK_VERSION(2, 67, 3)
+ self->extra_drag_types = g_memdup2 (types, sizeof (GType) * n_types);
+#else
+ self->extra_drag_types = g_memdup (types, sizeof (GType) * n_types);
+#endif
+ self->extra_drag_n_types = n_types;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ adw_tab_thumbnail_setup_extra_drop_target (info->tab,
+ self->extra_drag_actions,
+ self->extra_drag_types,
+ self->extra_drag_n_types);
+ }
+}
+
+gboolean
+adw_tab_grid_get_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);
+ }
+}
+
+void
+adw_tab_grid_set_thumbnail_align (AdwTabGrid *self,
+ float xalign,
+ float yalign)
+{
+ GList *l;
+
+ g_return_if_fail (ADW_IS_TAB_GRID (self));
+
+ self->thumbnail_xalign = xalign;
+ self->thumbnail_yalign = yalign;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ adw_tab_thumbnail_set_align (info->tab, xalign, yalign);
+ }
+}
+
+GtkPicture *
+adw_tab_grid_get_transition_picture (AdwTabGrid *self)
+{
+ g_return_val_if_fail (ADW_IS_TAB_GRID (self), NULL);
+
+ if (self->selected_tab)
+ return adw_tab_thumbnail_get_picture (self->selected_tab->tab);
+
+ return NULL;
+}
diff --git a/src/adw-tab-overview-private.h b/src/adw-tab-overview-private.h
new file mode 100644
index 00000000..c333281b
--- /dev/null
+++ b/src/adw-tab-overview-private.h
@@ -0,0 +1,21 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include "adw-tab-overview.h"
+
+G_BEGIN_DECLS
+
+gboolean adw_tab_overview_tabs_have_visible_focus (AdwTabOverview *self);
+
+G_END_DECLS
diff --git a/src/adw-tab-overview.c b/src/adw-tab-overview.c
new file mode 100644
index 00000000..039f6d2a
--- /dev/null
+++ b/src/adw-tab-overview.c
@@ -0,0 +1,1030 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "adw-tab-overview-private.h"
+
+#include "adw-bin.h"
+#include "adw-macros-private.h"
+#include "adw-tab-grid-private.h"
+#include "adw-timed-animation.h"
+#include "adw-widget-utils-private.h"
+
+#define TRANSITION_DURATION 400
+#define OFFSET_FACTOR 0.1f
+#define THUMBNAIL_BORDER_RADIUS 12
+
+/**
+ * AdwTabOverview:
+ *
+ * Since: 1.3
+ */
+
+struct _AdwTabOverview
+{
+ GtkWidget parent_instance;
+
+ GtkWidget *overview;
+ GtkWidget *child_bin;
+ AdwTabView *view;
+
+ AdwTabGrid *grid;
+ AdwTabGrid *pinned_grid;
+
+ float thumbnail_xalign;
+ float thumbnail_yalign;
+
+ gboolean is_open;
+ AdwAnimation *open_animation;
+ double progress;
+
+ GtkWidget *hidden_thumbnail;
+ GdkPaintable *transition_paintable;
+
+ GskGLShader *shader;
+ gboolean shader_compiled;
+};
+
+static void adw_tab_overview_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (AdwTabOverview, adw_tab_overview, GTK_TYPE_WIDGET,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE, adw_tab_overview_buildable_init))
+
+static GtkBuildableIface *parent_buildable_iface;
+
+enum {
+ PROP_0,
+ PROP_VIEW,
+ PROP_CHILD,
+ PROP_OPEN,
+ PROP_INVERTED,
+ PROP_THUMBNAIL_XALIGN,
+ PROP_THUMBNAIL_YALIGN,
+ LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_EXTRA_DRAG_DROP,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static gboolean
+extra_drag_drop_cb (AdwTabOverview *self,
+ AdwTabPage *page,
+ GValue *value)
+{
+ gboolean ret = GDK_EVENT_PROPAGATE;
+
+ g_signal_emit (self, signals[SIGNAL_EXTRA_DRAG_DROP], 0, page, value, &ret);
+
+ return ret;
+}
+
+static void
+view_destroy_cb (AdwTabOverview *self)
+{
+ adw_tab_overview_set_view (self, NULL);
+}
+
+static void
+notify_selected_page_cb (AdwTabOverview *self)
+{
+ AdwTabPage *page = adw_tab_view_get_selected_page (self->view);
+
+ if (!page)
+ return;
+
+ if (adw_tab_page_get_pinned (page)) {
+ adw_tab_grid_select_page (self->pinned_grid, page);
+ adw_tab_grid_select_page (self->grid, page);
+ } else {
+ adw_tab_grid_select_page (self->grid, page);
+ adw_tab_grid_select_page (self->pinned_grid, page);
+ }
+}
+
+static void
+notify_pinned_cb (AdwTabPage *page,
+ GParamSpec *pspec,
+ AdwTabOverview *self)
+{
+ AdwTabGrid *from, *to;
+ gboolean should_focus;
+
+ if (adw_tab_page_get_pinned (page)) {
+ from = self->grid;
+ to = self->pinned_grid;
+ } else {
+ from = self->pinned_grid;
+ to = self->grid;
+ }
+
+ should_focus = adw_tab_grid_is_page_focused (from, page);
+
+ adw_tab_grid_detach_page (from, page);
+ adw_tab_grid_attach_page (to, page, adw_tab_view_get_n_pinned_pages (self->view));
+
+ if (should_focus)
+ adw_tab_grid_try_focus_selected_tab (to);
+}
+
+static void
+page_attached_cb (AdwTabOverview *self,
+ AdwTabPage *page,
+ int position)
+{
+ g_signal_connect_object (page, "notify::pinned",
+ G_CALLBACK (notify_pinned_cb), self,
+ 0);
+}
+
+static void
+page_detached_cb (AdwTabOverview *self,
+ AdwTabPage *page,
+ int position)
+{
+ g_signal_handlers_disconnect_by_func (page, notify_pinned_cb, self);
+}
+
+static void
+open_animation_value_cb (double value,
+ AdwTabOverview *self)
+{
+ self->progress = value;
+ gtk_widget_queue_draw (GTK_WIDGET (self));
+}
+
+static void
+open_animation_done_cb (AdwTabOverview *self)
+{
+ gtk_widget_set_opacity (self->hidden_thumbnail, 1);
+ g_clear_object (&self->hidden_thumbnail);
+ g_clear_object (&self->transition_paintable);
+
+ if (!self->is_open) {
+// grid.did_close ();
+ gtk_widget_hide (self->overview);
+ gtk_widget_set_can_target (self->overview, FALSE);
+ }
+}
+
+static void
+ensure_shader (AdwTabOverview *self)
+{
+ GtkNative *native;
+ GskRenderer *renderer;
+ GError *error = NULL;
+
+ if (self->shader)
+ return;
+
+ self->shader = gsk_gl_shader_new_from_resource ("/org/gnome/Adwaita/glsl/tab-overview.glsl");
+
+ native = gtk_widget_get_native (GTK_WIDGET (self));
+ renderer = gtk_native_get_renderer (native);
+
+ self->shader_compiled = gsk_gl_shader_compile (self->shader, renderer, &error);
+
+ if (error) {
+ /* If shaders aren't supported, the error doesn't matter and we just
+ * silently fall back */
+ if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED))
+ g_warning ("Couldn't compile shader: %s\n", error->message);
+ }
+
+ g_clear_error (&error);
+}
+
+static void
+adw_tab_overview_snapshot (GtkWidget *widget,
+ GtkSnapshot *snapshot)
+{
+ AdwTabOverview *self = ADW_TAB_OVERVIEW (widget);
+
+ if (self->progress > 0 && self->progress < 1) {
+ graphene_rect_t view_bounds, thumbnail_bounds, transition_bounds;
+ graphene_point_t view_center;
+ int width, height;
+ float scale;
+ GskRoundedRect transition_rect;
+ GtkSnapshot *child_snapshot;
+ GskRenderNode *child_node = NULL;
+
+ ensure_shader (self);
+
+ width = gtk_widget_get_width (widget);
+ height = gtk_widget_get_height (widget);
+ scale = 1 - OFFSET_FACTOR * (float) (1 - CLAMP (self->progress, 0, 1));
+
+ if (!gtk_widget_compute_bounds (GTK_WIDGET (self->view), widget, &view_bounds))
+ g_critical ("View must be inside the overview"); // TODO
+
+ if (!gtk_widget_compute_bounds (self->hidden_thumbnail, widget, &thumbnail_bounds))
+ graphene_rect_init (&thumbnail_bounds, 0, 0, 0, 0);
+
+ graphene_rect_get_center (&view_bounds, &view_center);
+
+ graphene_rect_interpolate (&view_bounds, &thumbnail_bounds,
+ self->progress, &transition_bounds);
+
+ gsk_rounded_rect_init_from_rect (&transition_rect,
+ &GRAPHENE_RECT_INIT (0, 0,
+ transition_bounds.size.width,
+ transition_bounds.size.height),
+ (float) (THUMBNAIL_BORDER_RADIUS * self->progress));
+
+ /* Draw overview */
+ gtk_snapshot_save (snapshot);
+ gtk_snapshot_push_opacity (snapshot, self->progress);
+ gtk_snapshot_translate (snapshot, &view_center);
+ gtk_snapshot_scale (snapshot, scale, scale);
+ gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (-view_center.x, -view_center.y));
+ gtk_widget_snapshot_child (widget, self->overview, snapshot);
+ gtk_snapshot_pop (snapshot);
+ gtk_snapshot_restore (snapshot);
+
+ /* Draw the transition thumbnail. Unfortunately, since GTK widgets have
+ * integer sizes, we can't use a real widget for this and have to custom
+ * draw it instead. We also want to interpolate border-radius. */
+ if (self->transition_paintable) {
+ gtk_snapshot_save (snapshot);
+ gtk_snapshot_translate (snapshot, &transition_bounds.origin);
+ gtk_snapshot_push_rounded_clip (snapshot, &transition_rect);
+ gdk_paintable_snapshot (self->transition_paintable,
+ GDK_SNAPSHOT (snapshot),
+ transition_rect.bounds.size.width,
+ transition_rect.bounds.size.height);
+ gtk_snapshot_pop (snapshot);
+ gtk_snapshot_restore (snapshot);
+ }
+
+ /* Draw the child */
+ scale += OFFSET_FACTOR;
+ gtk_snapshot_save (snapshot);
+ gtk_snapshot_translate (snapshot, &view_center);
+ gtk_snapshot_scale (snapshot, scale, scale);
+ gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (-view_center.x, -view_center.y));
+
+ child_snapshot = gtk_snapshot_new ();
+
+ if (self->shader_compiled) {
+ graphene_vec2_t origin, size;
+ double opacity;
+
+ opacity = 1 - CLAMP (self->progress, 0, 1);
+ graphene_point_to_vec2 (&view_bounds.origin, &origin);
+ graphene_vec2_init (&size, view_bounds.size.width, view_bounds.size.height);
+
+ gtk_snapshot_push_gl_shader (child_snapshot,
+ self->shader,
+ &GRAPHENE_RECT_INIT (0, 0, width, height),
+ gsk_gl_shader_format_args (self->shader,
+ "opacity", opacity,
+ "origin", &origin,
+ "size", &size,
+ NULL));
+ } else {
+ gtk_snapshot_push_opacity (child_snapshot, 1 - self->progress);
+ }
+
+ gtk_widget_snapshot_child (widget, self->child_bin, child_snapshot);
+
+ if (self->shader_compiled)
+ gtk_snapshot_gl_shader_pop_texture (child_snapshot);
+
+ gtk_snapshot_pop (child_snapshot);
+
+ child_node = gtk_snapshot_free_to_node (child_snapshot);
+ gtk_snapshot_append_node (snapshot, child_node);
+
+ gtk_snapshot_restore (snapshot);
+ gsk_render_node_unref (child_node);
+
+ return;
+ }
+
+ if (self->progress > 0.5)
+ gtk_widget_snapshot_child (widget, self->overview, snapshot);
+
+ /* We don't want to actually draw the child, but we do need it
+ * to redraw so that it can be displayed by the paintables */
+ if (self->progress > 0.5) {
+ GtkSnapshot *child_snapshot = gtk_snapshot_new ();
+
+ gtk_widget_snapshot_child (widget, self->child_bin, child_snapshot);
+
+ g_object_unref (child_snapshot);
+ } else {
+ gtk_widget_snapshot_child (widget, self->child_bin, snapshot);
+ }
+}
+
+static void
+adw_tab_overview_unrealize (GtkWidget *widget)
+{
+ AdwTabOverview *self = ADW_TAB_OVERVIEW (widget);
+
+ GTK_WIDGET_CLASS (adw_tab_overview_parent_class)->unrealize (widget);
+
+ g_clear_object (&self->shader);
+}
+
+static void
+adw_tab_overview_dispose (GObject *object)
+{
+ AdwTabOverview *self = ADW_TAB_OVERVIEW (object);
+
+ adw_tab_overview_set_view (self, NULL);
+
+ 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_THUMBNAIL_XALIGN:
+ g_value_set_float (value, adw_tab_overview_get_thumbnail_xalign (self));
+ break;
+ case PROP_THUMBNAIL_YALIGN:
+ g_value_set_float (value, adw_tab_overview_get_thumbnail_yalign (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_THUMBNAIL_XALIGN:
+ adw_tab_overview_set_thumbnail_xalign (self, g_value_get_float (value));
+ break;
+ case PROP_THUMBNAIL_YALIGN:
+ adw_tab_overview_set_thumbnail_yalign (self, g_value_get_float (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
+adw_tab_overview_class_init (AdwTabOverviewClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = adw_tab_overview_dispose;
+ object_class->get_property = adw_tab_overview_get_property;
+ object_class->set_property = adw_tab_overview_set_property;
+
+ widget_class->snapshot = adw_tab_overview_snapshot;
+ widget_class->unrealize = adw_tab_overview_unrealize;
+ widget_class->compute_expand = adw_widget_compute_expand;
+
+ /**
+ * AdwTabOverview:view: (attributes org.gtk.Property.get=adw_tab_overview_get_view
org.gtk.Property.set=adw_tab_overview_set_view)
+ *
+ * The tab view the overview controls.
+ *
+ * Since: 1.3
+ */
+ props[PROP_VIEW] =
+ g_param_spec_object ("view", NULL, NULL,
+ ADW_TYPE_TAB_VIEW,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwTabOverview:child: (attributes org.gtk.Property.get=adw_tab_overview_get_child
org.gtk.Property.set=adw_tab_overview_set_child)
+ *
+ * The child widget.
+ *
+ * Since: 1.3
+ */
+ props[PROP_CHILD] =
+ g_param_spec_object ("child", NULL, NULL,
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwTabOverview:open: (attributes org.gtk.Property.get=adw_tab_overview_get_open
org.gtk.Property.set=adw_tab_overview_set_open)
+ *
+ * Whether the overview is open.
+ *
+ * Since: 1.3
+ */
+ props[PROP_OPEN] =
+ g_param_spec_boolean ("open", NULL, NULL,
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwTabOverview:inverted: (attributes org.gtk.Property.get=adw_tab_overview_get_inverted
org.gtk.Property.set=adw_tab_overview_set_inverted)
+ *
+ * Whether thumbnails use inverted layout.
+ *
+ * If set to `TRUE`, thumbnails will have the close button at the beginning
+ * and the indicator at the end rather than the other way around.
+ *
+ * Since: 1.3
+ */
+ props[PROP_INVERTED] =
+ g_param_spec_boolean ("inverted", NULL, NULL,
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwTabOverview:thumbnail-xalign: (attributes org.gtk.Property.get=adw_tab_overview_get_thumbnail_xalign
org.gtk.Property.set=adw_tab_overview_set_thumbnail_xalign)
+ *
+ * TODO
+ *
+ * Since: 1.3
+ */
+ props[PROP_THUMBNAIL_XALIGN] =
+ g_param_spec_float ("thumbnail-xalign", NULL, NULL,
+ 0.0, 1.0,
+ 0.5,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ /**
+ * AdwTabOverview:thumbnail-yalign: (attributes org.gtk.Property.get=adw_tab_overview_get_thumbnail_yalign
org.gtk.Property.set=adw_tab_overview_set_thumbnail_yalign)
+ *
+ * TODO
+ *
+ * Since: 1.3
+ */
+ props[PROP_THUMBNAIL_YALIGN] =
+ g_param_spec_float ("thumbnail-yalign", NULL, NULL,
+ 0.0, 1.0,
+ 0.5,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ /**
+ * AdwTabOverview::extra-drag-drop:
+ * @self: a tab overview
+ * @page: the page matching the tab the content was dropped onto
+ * @value: the `GValue` being dropped
+ *
+ * This signal is emitted when content is dropped onto a tab.
+ *
+ * The content must be of one of the types set up via
+ * [method@TabOverview.setup_extra_drop_target].
+ *
+ * See [signal@Gtk.DropTarget::drop].
+ *
+ * Returns: whether the drop was accepted for @page
+ *
+ * Since: 1.3
+ */
+ signals[SIGNAL_EXTRA_DRAG_DROP] =
+ g_signal_new ("extra-drag-drop",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ g_signal_accumulator_first_wins, NULL, NULL,
+ G_TYPE_BOOLEAN,
+ 2,
+ ADW_TYPE_TAB_PAGE,
+ G_TYPE_VALUE);
+
+ gtk_widget_class_install_action (widget_class, "overview.open", NULL,
+ (GtkWidgetActionActivateFunc) overview_open_cb);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/org/gnome/Adwaita/ui/adw-tab-overview.ui");
+
+ gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, overview);
+ gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, child_bin);
+ gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, grid);
+ gtk_widget_class_bind_template_child (widget_class, AdwTabOverview, pinned_grid);
+ gtk_widget_class_bind_template_callback (widget_class, extra_drag_drop_cb);
+
+ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+ gtk_widget_class_set_css_name (widget_class, "taboverview");
+
+ g_type_ensure (ADW_TYPE_TAB_GRID);
+}
+
+static void
+adw_tab_overview_init (AdwTabOverview *self)
+{
+ AdwAnimationTarget *target;
+
+ self->thumbnail_xalign = 0.5;
+ self->thumbnail_yalign = 0.5;
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ target = adw_callback_animation_target_new ((AdwAnimationTargetFunc) open_animation_value_cb,
+ self, NULL);
+
+ self->open_animation =
+ adw_timed_animation_new (GTK_WIDGET (self),
+ 0, 0,
+ TRANSITION_DURATION,
+ target);
+
+ g_signal_connect_swapped (self->open_animation, "done",
+ G_CALLBACK (open_animation_done_cb), self);
+}
+
+static void
+adw_tab_overview_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const char *type)
+{
+ AdwTabOverview *self = ADW_TAB_OVERVIEW (buildable);
+
+ if (!self->overview)
+ parent_buildable_iface->add_child (buildable, builder, child, type);
+ else if (GTK_IS_WIDGET (child))
+ adw_tab_overview_set_child (self, GTK_WIDGET (child));
+ else
+ parent_buildable_iface->add_child (buildable, builder, child, type);
+}
+
+static void
+adw_tab_overview_buildable_init (GtkBuildableIface *iface)
+{
+ parent_buildable_iface = g_type_interface_peek_parent (iface);
+
+ iface->add_child = adw_tab_overview_buildable_add_child;
+}
+
+/**
+ * adw_tab_overview_new:
+ *
+ * Creates a new `AdwTabOverview`.
+ *
+ * Returns: the newly created `AdwTabOverview`
+ *
+ * Since: 1.3
+ */
+GtkWidget *
+adw_tab_overview_new (void)
+{
+ return g_object_new (ADW_TYPE_TAB_OVERVIEW, NULL);
+}
+
+/**
+ * adw_tab_overview_get_view: (attributes org.gtk.Method.get_property=view)
+ * @self: a tab overview
+ *
+ * Gets the tab view @self controls.
+ *
+ * Returns: (transfer none) (nullable): the tab view
+ *
+ * Since: 1.3
+ */
+AdwTabView *
+adw_tab_overview_get_view (AdwTabOverview *self)
+{
+ g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), NULL);
+
+ return self->view;
+}
+
+/**
+ * adw_tab_overview_set_view: (attributes org.gtk.Method.set_property=view)
+ * @self: a tab overview
+ * @view: (nullable): a tab view
+ *
+ * Sets the tab view to control.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_view (AdwTabOverview *self,
+ AdwTabView *view)
+{
+ g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+ g_return_if_fail (view == NULL || ADW_IS_TAB_VIEW (view));
+
+ if (self->view == view)
+ return;
+
+ if (self->view) {
+ int i, n;
+
+ g_signal_handlers_disconnect_by_func (self->view, notify_selected_page_cb, self);
+ g_signal_handlers_disconnect_by_func (self->view, page_attached_cb, self);
+ g_signal_handlers_disconnect_by_func (self->view, page_detached_cb, self);
+ g_signal_handlers_disconnect_by_func (self->view, view_destroy_cb, self);
+
+ n = adw_tab_view_get_n_pages (self->view);
+
+ for (i = 0; i < n; i++)
+ page_detached_cb (self, adw_tab_view_get_nth_page (self->view, i), i);
+
+ adw_tab_grid_set_view (self->grid, NULL);
+ adw_tab_grid_set_view (self->pinned_grid, NULL);
+ }
+
+ g_set_object (&self->view, view);
+
+ if (self->view) {
+ int i, n;
+
+ adw_tab_grid_set_view (self->grid, view);
+ adw_tab_grid_set_view (self->pinned_grid, view);
+
+ g_signal_connect_object (self->view, "notify::selected-page",
+ G_CALLBACK (notify_selected_page_cb), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->view, "page-attached",
+ G_CALLBACK (page_attached_cb), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->view, "page-detached",
+ G_CALLBACK (page_detached_cb), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->view, "destroy",
+ G_CALLBACK (view_destroy_cb), self,
+ G_CONNECT_SWAPPED);
+
+ n = adw_tab_view_get_n_pages (self->view);
+
+ for (i = 0; i < n; i++)
+ page_attached_cb (self, adw_tab_view_get_nth_page (self->view, i), i);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW]);
+}
+
+/**
+ * adw_tab_overview_get_child: (attributes org.gtk.Method.get_property=child)
+ * @self: a `AdwTabOveview`
+ *
+ * Gets the child widget of @self.
+ *
+ * Returns: (nullable) (transfer none): the child widget of @self
+ *
+ * Since: 1.3
+ */
+GtkWidget *
+adw_tab_overview_get_child (AdwTabOverview *self)
+{
+ g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), NULL);
+
+ return adw_bin_get_child (ADW_BIN (self->child_bin));
+}
+
+/**
+ * adw_tab_overview_set_child: (attributes org.gtk.Method.set_property=child)
+ * @self: a tab overview
+ * @child: (nullable): the child widget
+ *
+ * Sets the child widget of @self.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_child (AdwTabOverview *self,
+ GtkWidget *child)
+{
+ g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+ g_return_if_fail (child == NULL || GTK_IS_WIDGET (child));
+
+ if (child == adw_tab_overview_get_child (self))
+ return;
+
+ adw_bin_set_child (ADW_BIN (self->child_bin), child);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_CHILD]);
+}
+
+/**
+ * adw_tab_overview_get_open: (attributes org.gtk.Method.get_property=open)
+ * @self: a tab overview
+ *
+ * Gets whether the overview is open.
+ *
+ * Returns: whether the overview is open
+ *
+ * Since: 1.3
+ */
+gboolean
+adw_tab_overview_get_open (AdwTabOverview *self)
+{
+ g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), FALSE);
+
+ return self->is_open;
+}
+
+/**
+ * adw_tab_overview_set_open: (attributes org.gtk.Method.set_property=open)
+ * @self: a tab overview
+ * @open: whether the overview is open
+ *
+ * Sets whether the overview is open.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_open (AdwTabOverview *self,
+ gboolean open)
+{
+ AdwTabPage *selected_page;
+ GtkPicture *picture;
+
+ g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+ open = !!open;
+
+ if (self->is_open == open)
+ return;
+
+ self->is_open = open;
+
+ if (open) {
+ adw_animation_skip (self->open_animation);
+
+ gtk_widget_show (self->overview);
+ gtk_widget_set_can_target (self->overview, TRUE);
+ }
+
+// if (open)
+// grid.will_open ();
+
+ selected_page = adw_tab_view_get_selected_page (self->view);
+
+ if (adw_tab_page_get_pinned (selected_page))
+ picture = adw_tab_grid_get_transition_picture (ADW_TAB_GRID (self->pinned_grid));
+ else
+ picture = adw_tab_grid_get_transition_picture (ADW_TAB_GRID (self->grid));
+
+ self->hidden_thumbnail = g_object_ref (GTK_WIDGET (picture));
+ gtk_widget_set_opacity (self->hidden_thumbnail, 0);
+
+ self->transition_paintable = g_object_ref (gtk_picture_get_paintable (picture));
+
+ adw_timed_animation_set_value_from (ADW_TIMED_ANIMATION (self->open_animation),
+ self->progress);
+ adw_timed_animation_set_value_to (ADW_TIMED_ANIMATION (self->open_animation),
+ open ? 1 : 0);
+
+ adw_animation_play (self->open_animation);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_OPEN]);
+}
+
+/**
+ * adw_tab_overview_get_inverted: (attributes org.gtk.Method.get_property=inverted)
+ * @self: a tab overview
+ *
+ * Gets whether thumbnails use inverted layout.
+ *
+ * Returns: whether thumbnails use inverted layout
+ *
+ * Since: 1.3
+ */
+gboolean
+adw_tab_overview_get_inverted (AdwTabOverview *self)
+{
+ g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), FALSE);
+
+ return adw_tab_grid_get_inverted (self->grid);
+}
+
+/**
+ * adw_tab_overview_set_inverted: (attributes org.gtk.Method.set_property=inverted)
+ * @self: a tab overview
+ * @inverted: whether thumbnails use inverted layout
+ *
+ * Sets whether tabs thumbnails use inverted layout.
+ *
+ * If set to `TRUE`, thumbnails will have the close button at the beginning and
+ * the indicator at the end rather than the other way around.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_inverted (AdwTabOverview *self,
+ gboolean inverted)
+{
+ g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+ inverted = !!inverted;
+
+ if (adw_tab_overview_get_inverted (self) == inverted)
+ return;
+
+ adw_tab_grid_set_inverted (self->grid, inverted);
+ adw_tab_grid_set_inverted (self->pinned_grid, inverted);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INVERTED]);
+}
+
+/**
+ * adw_tab_overview_get_thumbnail_xalign: (attributes org.gtk.Method.get_property=thumbnail-xalign)
+ * @self: a tab overview
+ *
+ * Gets TODO
+ *
+ * Returns: TODO
+ *
+ * Since: 1.3
+ */
+float
+adw_tab_overview_get_thumbnail_xalign (AdwTabOverview *self)
+{
+ g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), 0.0f);
+
+ return self->thumbnail_xalign;
+}
+
+/**
+ * adw_tab_overview_set_thumbnail_xalign: (attributes org.gtk.Method.set_property=thumbnail-xalign)
+ * @self: a tab overview
+ * @xalign: TODO
+ *
+ * Sets TODO
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_thumbnail_xalign (AdwTabOverview *self,
+ float xalign)
+{
+ g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+ xalign = CLAMP (xalign, 0.0, 1.0);
+
+ if (self->thumbnail_xalign == xalign)
+ return;
+
+ self->thumbnail_xalign = xalign;
+
+ adw_tab_grid_set_thumbnail_align (self->grid,
+ self->thumbnail_xalign,
+ self->thumbnail_yalign);
+ adw_tab_grid_set_thumbnail_align (self->pinned_grid,
+ self->thumbnail_xalign,
+ self->thumbnail_yalign);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_THUMBNAIL_XALIGN]);
+}
+
+/**
+ * adw_tab_overview_get_thumbnail_yalign: (attributes org.gtk.Method.get_property=thumbnail-yalign)
+ * @self: a tab overview
+ *
+ * Gets TODO
+ *
+ * Returns: TODO
+ *
+ * Since: 1.3
+ */
+float
+adw_tab_overview_get_thumbnail_yalign (AdwTabOverview *self)
+{
+ g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), 0.0f);
+
+ return self->thumbnail_yalign;
+}
+
+/**
+ * adw_tab_overview_set_thumbnail_xalign: (attributes org.gtk.Method.set_property=thumbnail-yalign)
+ * @self: a tab overview
+ * @xalign: TODO
+ *
+ * Sets TODO
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_set_thumbnail_yalign (AdwTabOverview *self,
+ float yalign)
+{
+ g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+
+ yalign = CLAMP (yalign, 0.0, 1.0);
+
+ if (self->thumbnail_yalign == yalign)
+ return;
+
+ self->thumbnail_yalign = yalign;
+
+ adw_tab_grid_set_thumbnail_align (self->grid,
+ self->thumbnail_xalign,
+ self->thumbnail_yalign);
+ adw_tab_grid_set_thumbnail_align (self->pinned_grid,
+ self->thumbnail_xalign,
+ self->thumbnail_yalign);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_THUMBNAIL_YALIGN]);
+}
+
+/**
+ * adw_tab_overview_setup_extra_drop_target:
+ * @self: a tab overview
+ * @actions: the supported actions
+ * @types: (nullable) (transfer none) (array length=n_types):
+ * all supported `GType`s that can be dropped
+ * @n_types: number of @types
+ *
+ * Sets the supported types for this drop target.
+ *
+ * Sets up an extra drop target on tabs.
+ *
+ * This allows to drag arbitrary content onto tabs, for example URLs in a web
+ * browser.
+ *
+ * If a tab is hovered for a certain period of time while dragging the content,
+ * it will be automatically selected.
+ *
+ * The [signal@TabOverview::extra-drag-drop] signal can be used to handle the
+ * drop.
+ *
+ * Since: 1.3
+ */
+void
+adw_tab_overview_setup_extra_drop_target (AdwTabOverview *self,
+ GdkDragAction actions,
+ GType *types,
+ gsize n_types)
+{
+ g_return_if_fail (ADW_IS_TAB_OVERVIEW (self));
+ g_return_if_fail (n_types == 0 || types != NULL);
+
+ adw_tab_grid_setup_extra_drop_target (self->grid, actions, types, n_types);
+ adw_tab_grid_setup_extra_drop_target (self->pinned_grid, actions, types, n_types);
+}
+
+gboolean
+adw_tab_overview_tabs_have_visible_focus (AdwTabOverview *self)
+{
+ GtkWidget *pinned_focus_child, *scroll_focus_child;
+
+ g_return_val_if_fail (ADW_IS_TAB_OVERVIEW (self), FALSE);
+
+ pinned_focus_child = gtk_widget_get_focus_child (GTK_WIDGET (self->pinned_grid));
+ scroll_focus_child = gtk_widget_get_focus_child (GTK_WIDGET (self->grid));
+
+ if (pinned_focus_child && gtk_widget_has_visible_focus (pinned_focus_child))
+ return TRUE;
+
+ if (scroll_focus_child && gtk_widget_has_visible_focus (scroll_focus_child))
+ return TRUE;
+
+ return FALSE;
+}
diff --git a/src/adw-tab-overview.h b/src/adw-tab-overview.h
new file mode 100644
index 00000000..8f3bfbe9
--- /dev/null
+++ b/src/adw-tab-overview.h
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include "adw-version.h"
+
+#include <gtk/gtk.h>
+#include "adw-tab-view.h"
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_TAB_OVERVIEW (adw_tab_overview_get_type())
+
+ADW_AVAILABLE_IN_1_2
+G_DECLARE_FINAL_TYPE (AdwTabOverview, adw_tab_overview, ADW, TAB_OVERVIEW, GtkWidget)
+
+ADW_AVAILABLE_IN_1_2
+GtkWidget *adw_tab_overview_new (void) G_GNUC_WARN_UNUSED_RESULT;
+
+ADW_AVAILABLE_IN_1_2
+AdwTabView *adw_tab_overview_get_view (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void adw_tab_overview_set_view (AdwTabOverview *self,
+ AdwTabView *view);
+
+ADW_AVAILABLE_IN_1_2
+GtkWidget *adw_tab_overview_get_child (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void adw_tab_overview_set_child (AdwTabOverview *self,
+ GtkWidget *child);
+
+ADW_AVAILABLE_IN_1_2
+gboolean adw_tab_overview_get_open (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void adw_tab_overview_set_open (AdwTabOverview *self,
+ gboolean open);
+
+ADW_AVAILABLE_IN_1_2
+gboolean adw_tab_overview_get_inverted (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void adw_tab_overview_set_inverted (AdwTabOverview *self,
+ gboolean inverted);
+
+ADW_AVAILABLE_IN_1_2
+float adw_tab_overview_get_thumbnail_xalign (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void adw_tab_overview_set_thumbnail_xalign (AdwTabOverview *self,
+ float xalign);
+
+ADW_AVAILABLE_IN_1_2
+float adw_tab_overview_get_thumbnail_yalign (AdwTabOverview *self);
+ADW_AVAILABLE_IN_1_2
+void adw_tab_overview_set_thumbnail_yalign (AdwTabOverview *self,
+ float xalign);
+
+ADW_AVAILABLE_IN_1_2
+void adw_tab_overview_setup_extra_drop_target (AdwTabOverview *self,
+ GdkDragAction actions,
+ GType *types,
+ gsize n_types);
+
+G_END_DECLS
diff --git a/src/adw-tab-overview.ui b/src/adw-tab-overview.ui
new file mode 100644
index 00000000..6f24fec2
--- /dev/null
+++ b/src/adw-tab-overview.ui
@@ -0,0 +1,92 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk" version="4.0"/>
+ <template class="AdwTabOverview" parent="GtkWidget">
+ <child>
+ <object class="AdwBin" id="child_bin">
+ <style>
+ <class name="background"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkOverlay" id="overview">
+ <property name="visible">False</property>
+ <property name="can-target">False</property>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">vertical</property>
+ <child>
+ <object class="GtkScrolledWindow" id="pinned_scrolled_window">
+ <property name="hscrollbar-policy">never</property>
+ <property name="propagate-natural-height">True</property>
+ <property name="vexpand">False</property>
+ <property name="margin-top">47</property>
+ <style>
+ <class name="pinned"/>
+ </style>
+ <property name="child">
+ <object class="AdwTabGrid" id="pinned_grid">
+ <property name="pinned">True</property>
+ <property name="tab-overview">AdwTabOverview</property>
+ <!--
+ <signal name="stop-kinetic-scrolling" handler="stop_kinetic_scrolling_cb"
object="pinned_scrolled_window" swapped="true"/>-->
+ <signal name="extra-drag-drop" handler="extra_drag_drop_cb" swapped="true"/>
+ </object>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled_window">
+ <property name="hscrollbar-policy">never</property>
+ <property name="vexpand">True</property>
+ <property name="min-content-height">100</property>
+ <property name="child">
+ <object class="AdwTabGrid" id="grid">
+ <property name="tab-overview">AdwTabOverview</property>
+ <!--
+ <signal name="stop-kinetic-scrolling" handler="stop_kinetic_scrolling_cb"
object="scrolled_window" swapped="true"/>-->
+ <signal name="extra-drag-drop" handler="extra_drag_drop_cb" swapped="true"/>
+ </object>
+ </property>
+ </object>
+ </child>
+<!-- <property name="child">
+ <object class="AdwClamp">
+ <property name="margin-top">50</property>
+ <property name="margin-bottom">24</property>
+ <property name="margin-start">24</property>
+ <property name="margin-end">24</property>
+ <property name="maximum-size">4000</property
+ <object class="GtkFlowBox" id="flowbox">
+ <property name="halign">fill</property>
+ <property name="valign">center</property>
+ <property name="column-spacing">24</property>
+ <property name="row-spacing">24</property>
+ <property name="selection-mode">none</property>
+ <property name="activate-on-single-click">True</property>
+ <property name="homogeneous">True</property>
+ <property name="min-children-per-line">2</property>
+ </object>
+ </property>
+ </object>-->
+ </object>
+ </child>
+ <child type="overlay">
+ <object class="GtkHeaderBar" id="headerbar">
+ <property name="valign">start</property>
+ <property name="title-widget">
+ <object class="AdwWindowTitle"/>
+ </property>
+ <layout>
+ <property name="measure">True</property>
+ </layout>
+ <style>
+ <class name="flat"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/adw-tab-thumbnail-private.h b/src/adw-tab-thumbnail-private.h
new file mode 100644
index 00000000..6ecc3dbd
--- /dev/null
+++ b/src/adw-tab-thumbnail-private.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#pragma once
+
+#if !defined(_ADWAITA_INSIDE) && !defined(ADWAITA_COMPILATION)
+#error "Only <adwaita.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+
+#include "adw-tab-view.h"
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_TAB_THUMBNAIL (adw_tab_thumbnail_get_type())
+
+G_DECLARE_FINAL_TYPE (AdwTabThumbnail, adw_tab_thumbnail, ADW, TAB_THUMBNAIL, GtkWidget)
+
+AdwTabThumbnail *adw_tab_thumbnail_new (AdwTabView *view,
+ gboolean pinned) G_GNUC_WARN_UNUSED_RESULT;
+
+AdwTabPage *adw_tab_thumbnail_get_page (AdwTabThumbnail *self);
+void adw_tab_thumbnail_set_page (AdwTabThumbnail *self,
+ AdwTabPage *page);
+
+gboolean adw_tab_thumbnail_get_dragging (AdwTabThumbnail *self);
+void adw_tab_thumbnail_set_dragging (AdwTabThumbnail *self,
+ gboolean dragging);
+
+gboolean adw_tab_thumbnail_get_inverted (AdwTabThumbnail *self);
+void adw_tab_thumbnail_set_inverted (AdwTabThumbnail *self,
+ gboolean inverted);
+
+void adw_tab_thumbnail_set_fully_visible (AdwTabThumbnail *self,
+ gboolean fully_visible);
+
+void adw_tab_thumbnail_set_align (AdwTabThumbnail *self,
+ float xalign,
+ float yalign);
+
+void adw_tab_thumbnail_setup_extra_drop_target (AdwTabThumbnail *self,
+ GdkDragAction actions,
+ GType *types,
+ gsize n_types);
+
+GtkPicture *adw_tab_thumbnail_get_picture (AdwTabThumbnail *self);
+
+G_END_DECLS
diff --git a/src/adw-tab-thumbnail.c b/src/adw-tab-thumbnail.c
new file mode 100644
index 00000000..2d907daf
--- /dev/null
+++ b/src/adw-tab-thumbnail.c
@@ -0,0 +1,818 @@
+/*
+ * Copyright (C) 2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1-or-later
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#include "config.h"
+#include "adw-tab-thumbnail-private.h"
+
+#include "adw-fading-label-private.h"
+#include "adw-macros-private.h"
+
+#define MIN_ASPECT_RATIO 0.8
+#define MAX_ASPECT_RATIO 2.0
+
+#define ADW_TYPE_TAB_PAINTABLE (adw_tab_paintable_get_type ())
+
+G_DECLARE_FINAL_TYPE (AdwTabPaintable, adw_tab_paintable, ADW, TAB_PAINTABLE, GObject)
+
+struct _AdwTabPaintable
+{
+ GObject parent_instance;
+
+ GtkWidget *view;
+ GtkWidget *child;
+
+ GdkPaintable *view_paintable;
+ GdkPaintable *cached_paintable;
+
+ float xalign;
+ float yalign;
+
+ GdkRGBA cached_bg;
+ bool schedule_clear_cache;
+};
+
+static double
+adw_tab_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable)
+{
+ AdwTabPaintable *self = ADW_TAB_PAINTABLE (paintable);
+ int width = gtk_widget_get_width (self->view);
+ int height = gtk_widget_get_height (self->view);
+ double ratio = (double) width / height;
+
+ return CLAMP (ratio, MIN_ASPECT_RATIO, MAX_ASPECT_RATIO);
+}
+
+static GdkPaintable *
+adw_tab_paintable_get_current_image (GdkPaintable *paintable)
+{
+ AdwTabPaintable *self = ADW_TAB_PAINTABLE (paintable);
+
+ if (self->cached_paintable)
+ return g_object_ref (self->cached_paintable);
+
+ return gdk_paintable_get_current_image (self->view_paintable);
+}
+
+static void
+snapshot_paintable (GtkSnapshot *snapshot,
+ double width,
+ double height,
+ GdkPaintable *paintable,
+ double xalign,
+ double yalign)
+{
+ double snapshot_ratio = width / height;
+ double paintable_ratio = gdk_paintable_get_intrinsic_aspect_ratio (paintable);
+
+ if (paintable_ratio > snapshot_ratio) {
+ double new_width = height * paintable_ratio;
+
+ gtk_snapshot_translate (snapshot,
+ &GRAPHENE_POINT_INIT ((float) (width - new_width) * xalign, 0));
+
+ width = new_width;
+ } if (paintable_ratio < snapshot_ratio) {
+ double new_height = width / paintable_ratio;
+
+ gtk_snapshot_translate (snapshot,
+ &GRAPHENE_POINT_INIT (0, (float) (height - new_height) * yalign));
+
+ height = new_height;
+ }
+
+ gdk_paintable_snapshot (paintable, snapshot, width, height);
+}
+
+static void
+get_background_color (AdwTabPaintable *self,
+ GdkRGBA *rgba)
+{
+ GtkStyleContext *context = gtk_widget_get_style_context (self->view);
+
+ if (gtk_style_context_lookup_color (context, "window_bg_color", rgba))
+ return;
+
+ rgba->red = 1;
+ rgba->green = 1;
+ rgba->blue = 1;
+ rgba->alpha = 1;
+}
+
+static void
+adw_tab_paintable_snapshot (GdkPaintable *paintable,
+ GdkSnapshot *snapshot,
+ double width,
+ double height)
+{
+ AdwTabPaintable *self = ADW_TAB_PAINTABLE (paintable);
+ GdkRGBA bg;
+
+ if (self->cached_paintable) {
+ gtk_snapshot_append_color (GTK_SNAPSHOT (snapshot), &self->cached_bg,
+ &GRAPHENE_RECT_INIT (0, 0, width, height));
+
+ snapshot_paintable (GTK_SNAPSHOT (snapshot), width, height,
+ self->cached_paintable, self->xalign, self->yalign);
+
+ return;
+ }
+
+ if (!gtk_widget_get_mapped (self->child))
+ return;
+
+ get_background_color (self, &bg);
+ gtk_snapshot_append_color (GTK_SNAPSHOT (snapshot), &bg,
+ &GRAPHENE_RECT_INIT (0, 0, width, height));
+
+ snapshot_paintable (GTK_SNAPSHOT (snapshot), width, height,
+ self->view_paintable, self->xalign, self->yalign);
+}
+
+static void
+adw_tab_paintable_iface_init (GdkPaintableInterface *iface)
+{
+ iface->get_intrinsic_aspect_ratio = adw_tab_paintable_get_intrinsic_aspect_ratio;
+ iface->get_current_image = adw_tab_paintable_get_current_image;
+ iface->snapshot = adw_tab_paintable_snapshot;
+}
+
+G_DEFINE_FINAL_TYPE_WITH_CODE (AdwTabPaintable, adw_tab_paintable, G_TYPE_OBJECT,
+ G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, adw_tab_paintable_iface_init))
+
+static void
+adw_tab_paintable_dispose (GObject *object)
+{
+ AdwTabPaintable *self = ADW_TAB_PAINTABLE (object);
+
+ g_clear_object (&self->view_paintable);
+ g_clear_object (&self->cached_paintable);
+
+ G_OBJECT_CLASS (adw_tab_paintable_parent_class)->dispose (object);
+}
+
+static void
+adw_tab_paintable_class_init (AdwTabPaintableClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = adw_tab_paintable_dispose;
+}
+
+static void
+adw_tab_paintable_init (AdwTabPaintable *self)
+{
+}
+
+static void
+invalidate_contents_cb (AdwTabPaintable *self)
+{
+ gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
+
+ if (self->schedule_clear_cache) {
+ g_clear_object (&self->cached_paintable);
+ self->schedule_clear_cache = FALSE;
+ }
+}
+
+static void
+child_map_cb (AdwTabPaintable *self)
+{
+ if (self->cached_paintable)
+ self->schedule_clear_cache = TRUE;
+}
+
+static void
+child_unmap_cb (AdwTabPaintable *self)
+{
+ self->cached_paintable = gdk_paintable_get_current_image (self->view_paintable);
+ get_background_color (self, &self->cached_bg);
+}
+
+static GdkPaintable *
+adw_tab_paintable_new (AdwTabView *view,
+ AdwTabPage *page)
+{
+ AdwTabPaintable *self = g_object_new (ADW_TYPE_TAB_PAINTABLE, NULL);
+
+ self->view = GTK_WIDGET (view);
+ self->child = adw_tab_page_get_child (page);
+ self->xalign = 0.5;
+ self->yalign = 0.5;
+
+ self->view_paintable = gtk_widget_paintable_new (self->view);
+
+ g_signal_connect_swapped (self->view_paintable, "invalidate-contents", G_CALLBACK
(invalidate_contents_cb), self);
+ g_signal_connect_swapped (self->view_paintable, "invalidate-size", G_CALLBACK
(gdk_paintable_invalidate_size), self);
+
+ g_signal_connect_swapped (self->child, "map", G_CALLBACK (child_map_cb), self);
+ g_signal_connect_swapped (self->child, "unmap", G_CALLBACK (child_unmap_cb), self);
+
+ return GDK_PAINTABLE (self);
+}
+
+static void
+adw_tab_paintable_set_align (AdwTabPaintable *self,
+ float xalign,
+ float yalign)
+{
+ g_assert (ADW_IS_TAB_PAINTABLE (self));
+
+ self->xalign = xalign;
+ self->yalign = yalign;
+
+ gdk_paintable_invalidate_contents (GDK_PAINTABLE (self));
+}
+
+
+
+
+
+
+
+
+
+
+struct _AdwTabThumbnail
+{
+ GtkWidget parent_instance;
+
+ GtkWidget *contents;
+ GtkWidget *overlay;
+ GtkPicture *picture;
+ GtkWidget *icon_stack;
+ GtkImage *icon;
+ GtkSpinner *spinner;
+ GtkImage *indicator_icon;
+ GtkWidget *indicator_btn;
+ GtkWidget *close_btn;
+ GtkDropTarget *drop_target;
+
+ AdwTabView *view;
+ AdwTabPage *page;
+ gboolean pinned;
+ gboolean dragging;
+
+ gboolean inverted;
+};
+
+G_DEFINE_FINAL_TYPE (AdwTabThumbnail, adw_tab_thumbnail, GTK_TYPE_WIDGET)
+
+enum {
+ PROP_0,
+ PROP_VIEW,
+ PROP_PINNED,
+ PROP_DRAGGING,
+ PROP_PAGE,
+ PROP_INVERTED,
+ LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_EXTRA_DRAG_DROP,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static inline void
+set_style_class (GtkWidget *widget,
+ const char *style_class,
+ gboolean enabled)
+{
+ if (enabled)
+ gtk_widget_add_css_class (widget, style_class);
+ else
+ gtk_widget_remove_css_class (widget, style_class);
+}
+
+static void
+update_tooltip (AdwTabThumbnail *self)
+{
+ AdwTabPage *page = adw_tab_thumbnail_get_page (ADW_TAB_THUMBNAIL (self));
+ const char *tooltip = adw_tab_page_get_tooltip (page);
+
+ if (tooltip && g_strcmp0 (tooltip, "") != 0)
+ gtk_widget_set_tooltip_markup (GTK_WIDGET (self), tooltip);
+ else
+ gtk_widget_set_tooltip_text (GTK_WIDGET (self),
+ adw_tab_page_get_title (page));
+}
+
+static void
+update_spinner (AdwTabThumbnail *self)
+{
+ gboolean loading = self->page && adw_tab_page_get_loading (self->page);
+ gboolean mapped = gtk_widget_get_mapped (GTK_WIDGET (self));
+
+ /* Don't use CPU when not needed */
+ gtk_spinner_set_spinning (self->spinner, loading && mapped);
+}
+
+static void
+update_icon (AdwTabThumbnail *self)
+{
+ GIcon *gicon = adw_tab_page_get_icon (self->page);
+ gboolean loading = adw_tab_page_get_loading (self->page);
+ const char *name = loading ? "spinner" : "icon";
+
+ gtk_image_set_from_gicon (self->icon, gicon);
+ gtk_widget_set_visible (self->icon_stack,
+ (gicon != NULL || loading));
+ gtk_stack_set_visible_child_name (GTK_STACK (self->icon_stack), name);
+}
+
+static void
+update_loading (AdwTabThumbnail *self)
+{
+ update_icon (self);
+ update_spinner (self);
+ set_style_class (GTK_WIDGET (self), "loading",
+ adw_tab_page_get_loading (self->page));
+}
+
+static void
+update_indicator (AdwTabThumbnail *self)
+{
+ GIcon *indicator = adw_tab_page_get_indicator_icon (self->page);
+ gboolean activatable = self->page && adw_tab_page_get_indicator_activatable (self->page);
+
+ gtk_image_set_from_gicon (self->indicator_icon, indicator);
+ gtk_widget_set_visible (self->indicator_btn, indicator != NULL);
+ gtk_widget_set_can_target (self->indicator_btn, activatable);
+}
+
+static gboolean
+close_idle_cb (AdwTabThumbnail *self)
+{
+ adw_tab_view_close_page (self->view, self->page);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+close_clicked_cb (AdwTabThumbnail *self)
+{
+ if (!self->page)
+ return;
+
+ /* When animations are disabled, we don't want to immediately remove the
+ * whole tab mid-click. Instead, defer it until the click has happened.
+ */
+ g_idle_add ((GSourceFunc) close_idle_cb, self);
+}
+
+static void
+indicator_clicked_cb (AdwTabThumbnail *self)
+{
+ if (!self->page)
+ return;
+
+ g_signal_emit_by_name (self->view, "indicator-activated", self->page);
+}
+
+static gboolean
+drop_cb (AdwTabThumbnail *self,
+ GValue *value)
+{
+ gboolean ret = GDK_EVENT_PROPAGATE;
+
+ g_signal_emit (self, signals[SIGNAL_EXTRA_DRAG_DROP], 0, value, &ret);
+
+ return ret;
+}
+
+static void
+adw_tab_thumbnail_map (GtkWidget *widget)
+{
+ AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (widget);
+
+ GTK_WIDGET_CLASS (adw_tab_thumbnail_parent_class)->map (widget);
+
+ update_spinner (self);
+}
+
+static void
+adw_tab_thumbnail_unmap (GtkWidget *widget)
+{
+ AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (widget);
+
+ GTK_WIDGET_CLASS (adw_tab_thumbnail_parent_class)->unmap (widget);
+
+ update_spinner (self);
+}
+
+static void
+adw_tab_thumbnail_constructed (GObject *object)
+{
+// AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (object);
+
+ G_OBJECT_CLASS (adw_tab_thumbnail_parent_class)->constructed (object);
+
+ /*
+ g_signal_connect_object (view, "notify::default-icon",
+ G_CALLBACK (update_icons), object,
+ G_CONNECT_SWAPPED);
+*/
+}
+
+static void
+adw_tab_thumbnail_dispose (GObject *object)
+{
+ AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (object);
+
+ adw_tab_thumbnail_set_page (self, NULL);
+
+ if (self->contents)
+ gtk_widget_unparent (self->contents);
+
+ G_OBJECT_CLASS (adw_tab_thumbnail_parent_class)->dispose (object);
+}
+
+static void
+adw_tab_thumbnail_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (object);
+
+ switch (prop_id) {
+ case PROP_VIEW:
+ g_value_set_object (value, self->view);
+ break;
+
+ case PROP_PAGE:
+ g_value_set_object (value, adw_tab_thumbnail_get_page (self));
+ break;
+
+ case PROP_PINNED:
+ g_value_set_boolean (value, self->pinned);
+ break;
+
+ case PROP_DRAGGING:
+ g_value_set_boolean (value, adw_tab_thumbnail_get_dragging (self));
+ break;
+
+ case PROP_INVERTED:
+ g_value_set_boolean (value, adw_tab_thumbnail_get_inverted (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+adw_tab_thumbnail_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ AdwTabThumbnail *self = ADW_TAB_THUMBNAIL (object);
+
+ switch (prop_id) {
+ case PROP_VIEW:
+ self->view = g_value_get_object (value);
+ break;
+
+ case PROP_PAGE:
+ adw_tab_thumbnail_set_page (self, g_value_get_object (value));
+ break;
+
+ case PROP_PINNED:
+ self->pinned = g_value_get_boolean (value);
+ break;
+
+ case PROP_DRAGGING:
+ adw_tab_thumbnail_set_dragging (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_INVERTED:
+ adw_tab_thumbnail_set_inverted (self, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+adw_tab_thumbnail_class_init (AdwTabThumbnailClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = adw_tab_thumbnail_dispose;
+ object_class->constructed = adw_tab_thumbnail_constructed;
+ object_class->get_property = adw_tab_thumbnail_get_property;
+ object_class->set_property = adw_tab_thumbnail_set_property;
+
+ widget_class->map = adw_tab_thumbnail_map;
+ widget_class->unmap = adw_tab_thumbnail_unmap;
+
+ props[PROP_VIEW] =
+ g_param_spec_object ("view", NULL, NULL,
+ ADW_TYPE_TAB_VIEW,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ props[PROP_PINNED] =
+ g_param_spec_boolean ("pinned", NULL, NULL,
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS);
+
+ props[PROP_DRAGGING] =
+ g_param_spec_boolean ("dragging", NULL, NULL,
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_PAGE] =
+ g_param_spec_object ("page", NULL, NULL,
+ ADW_TYPE_TAB_PAGE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_INVERTED] =
+ g_param_spec_boolean ("inverted", NULL, NULL,
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ signals[SIGNAL_EXTRA_DRAG_DROP] =
+ g_signal_new ("extra-drag-drop",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ g_signal_accumulator_first_wins, NULL, NULL,
+ G_TYPE_BOOLEAN,
+ 1,
+ G_TYPE_VALUE);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/org/gnome/Adwaita/ui/adw-tab-thumbnail.ui");
+ gtk_widget_class_bind_template_child (widget_class, AdwTabThumbnail, contents);
+ gtk_widget_class_bind_template_child (widget_class, AdwTabThumbnail, overlay);
+ 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, drop_target);
+ gtk_widget_class_bind_template_callback (widget_class, close_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, indicator_clicked_cb);
+ gtk_widget_class_bind_template_callback (widget_class, drop_cb);
+
+ gtk_widget_class_set_layout_manager_type (widget_class, GTK_TYPE_BIN_LAYOUT);
+ gtk_widget_class_set_css_name (widget_class, "tabthumbnail");
+
+ g_type_ensure (ADW_TYPE_FADING_LABEL);
+}
+
+static void
+adw_tab_thumbnail_init (AdwTabThumbnail *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN);
+}
+
+AdwTabThumbnail *
+adw_tab_thumbnail_new (AdwTabView *view,
+ gboolean pinned)
+{
+ g_return_val_if_fail (ADW_IS_TAB_VIEW (view), NULL);
+
+ return g_object_new (ADW_TYPE_TAB_THUMBNAIL,
+ "view", view,
+ "pinned", pinned,
+ NULL);
+}
+
+AdwTabPage *
+adw_tab_thumbnail_get_page (AdwTabThumbnail *self)
+{
+ g_return_val_if_fail (ADW_IS_TAB_THUMBNAIL (self), NULL);
+
+ return self->page;
+}
+
+void
+adw_tab_thumbnail_set_page (AdwTabThumbnail *self,
+ AdwTabPage *page)
+{
+ g_return_if_fail (ADW_IS_TAB_THUMBNAIL (self));
+ g_return_if_fail (page == NULL || ADW_IS_TAB_PAGE (page));
+
+ if (self->page == page)
+ return;
+
+ if (self->page) {
+ gtk_picture_set_paintable (GTK_PICTURE (self->picture), NULL);
+
+ g_signal_handlers_disconnect_by_func (self->page, update_tooltip, self);
+ g_signal_handlers_disconnect_by_func (self->page, update_icon, self);
+ g_signal_handlers_disconnect_by_func (self->page, update_indicator, self);
+ g_signal_handlers_disconnect_by_func (self->page, update_loading, self);
+/*
+ g_signal_handlers_disconnect_by_func (self->page, update_selected, self);
+ g_signal_handlers_disconnect_by_func (self->page, update_title, self);
+ g_signal_handlers_disconnect_by_func (self->page, update_tooltip, self);
+ g_signal_handlers_disconnect_by_func (self->page, update_icons, self);
+ g_signal_handlers_disconnect_by_func (self->page, update_indicator, self);
+ g_signal_handlers_disconnect_by_func (self->page, update_needs_attention, self);
+ g_signal_handlers_disconnect_by_func (self->page, update_loading, self);*/
+ }
+
+ g_set_object (&self->page, page);
+
+ if (self->page) {
+ gtk_picture_set_paintable (GTK_PICTURE (self->picture),
+ adw_tab_paintable_new (self->view, self->page));
+
+ update_tooltip (self);
+ update_spinner (self);
+ update_icon (self);
+ update_indicator (self);
+ update_loading (self);
+
+ g_signal_connect_object (self->page, "notify::title",
+ G_CALLBACK (update_tooltip), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->page, "notify::tooltip",
+ G_CALLBACK (update_tooltip), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->page, "notify::icon",
+ G_CALLBACK (update_icon), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->page, "notify::indicator-icon",
+ G_CALLBACK (update_indicator), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->page, "notify::indicator-activatable",
+ G_CALLBACK (update_indicator), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->page, "notify::loading",
+ G_CALLBACK (update_loading), self,
+ G_CONNECT_SWAPPED);
+
+/*
+ update_selected (self);
+ update_state (self);
+ update_title (self);
+ update_tooltip (self);
+ update_spinner (self);
+ update_icons (self);
+ update_indicator (self);
+ update_needs_attention (self);
+ update_loading (self);
+
+ g_signal_connect_object (self->page, "notify::selected",
+ G_CALLBACK (update_selected), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->page, "notify::title",
+ G_CALLBACK (update_title), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->page, "notify::tooltip",
+ G_CALLBACK (update_tooltip), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->page, "notify::icon",
+ G_CALLBACK (update_icons), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->page, "notify::indicator-icon",
+ G_CALLBACK (update_icons), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->page, "notify::indicator-activatable",
+ G_CALLBACK (update_indicator), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->page, "notify::needs-attention",
+ G_CALLBACK (update_needs_attention), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->page, "notify::loading",
+ G_CALLBACK (update_loading), self,
+ G_CONNECT_SWAPPED);*/
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PAGE]);
+}
+
+gboolean
+adw_tab_thumbnail_get_dragging (AdwTabThumbnail *self)
+{
+ g_return_val_if_fail (ADW_IS_TAB_THUMBNAIL (self), FALSE);
+
+ return self->dragging;
+}
+
+void
+adw_tab_thumbnail_set_dragging (AdwTabThumbnail *self,
+ gboolean dragging)
+{
+ g_return_if_fail (ADW_IS_TAB_THUMBNAIL (self));
+
+ dragging = !!dragging;
+
+ if (self->dragging == dragging)
+ return;
+
+ self->dragging = dragging;
+
+// update_state (self);
+// update_selected (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DRAGGING]);
+}
+
+gboolean
+adw_tab_thumbnail_get_inverted (AdwTabThumbnail *self)
+{
+ g_return_val_if_fail (ADW_IS_TAB_THUMBNAIL (self), FALSE);
+
+ return self->inverted;
+}
+
+void
+adw_tab_thumbnail_set_inverted (AdwTabThumbnail *self,
+ gboolean inverted)
+{
+ g_return_if_fail (ADW_IS_TAB_THUMBNAIL (self));
+
+ inverted = !!inverted;
+
+ if (self->inverted == inverted)
+ return;
+
+ self->inverted = inverted;
+
+ if (inverted) {
+ gtk_widget_set_halign (self->close_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->indicator_btn, GTK_ALIGN_START);
+ }
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self->overlay));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_INVERTED]);
+}
+
+void
+adw_tab_thumbnail_set_fully_visible (AdwTabThumbnail *self,
+ gboolean fully_visible)
+{
+ g_return_if_fail (ADW_IS_TAB_THUMBNAIL (self));
+/*
+ fully_visible = !!fully_visible;
+
+ if (self->fully_visible == fully_visible)
+ return;
+
+ self->fully_visible = fully_visible;
+
+ update_state (self);
+ update_indicator (self);*/
+}
+
+void
+adw_tab_thumbnail_set_align (AdwTabThumbnail *self,
+ float xalign,
+ float yalign)
+{
+ GdkPaintable *paintable;
+
+ g_return_if_fail (ADW_IS_TAB_THUMBNAIL (self));
+
+ paintable = gtk_picture_get_paintable (self->picture);
+
+ g_assert (ADW_IS_TAB_PAINTABLE (paintable));
+
+ adw_tab_paintable_set_align (ADW_TAB_PAINTABLE (paintable), xalign, yalign);
+}
+
+void
+adw_tab_thumbnail_setup_extra_drop_target (AdwTabThumbnail *self,
+ GdkDragAction actions,
+ GType *types,
+ gsize n_types)
+{
+ g_return_if_fail (ADW_IS_TAB_THUMBNAIL (self));
+ g_return_if_fail (n_types == 0 || types != NULL);
+
+ gtk_drop_target_set_actions (self->drop_target, actions);
+ gtk_drop_target_set_gtypes (self->drop_target, types, n_types);
+}
+
+GtkPicture *
+adw_tab_thumbnail_get_picture (AdwTabThumbnail *self)
+{
+ g_return_val_if_fail (ADW_IS_TAB_THUMBNAIL (self), NULL);
+
+ return GTK_PICTURE (self->picture);
+}
diff --git a/src/adw-tab-thumbnail.ui b/src/adw-tab-thumbnail.ui
new file mode 100644
index 00000000..bb801c49
--- /dev/null
+++ b/src/adw-tab-thumbnail.ui
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface domain="libadwaita">
+ <requires lib="gtk" version="4.0"/>
+ <template class="AdwTabThumbnail" parent="GtkWidget">
+ <child>
+ <object class="GtkDropTarget" id="drop_target">
+ <signal name="drop" handler="drop_cb" swapped="true"/>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox" id="contents">
+ <property name="orientation">vertical</property>
+ <property name="spacing">6</property>
+ <property name="vexpand">False</property>
+ <child>
+ <object class="GtkOverlay" id="overlay">
+ <property name="child">
+ <object class="GtkPicture" id="picture">
+ <property name="can-shrink">True</property>
+ <property name="keep-aspect-ratio">False</property>
+ <property name="overflow">hidden</property>
+ <property name="vexpand">True</property>
+ <style>
+ <class name="card"/>
+ </style>
+ </object>
+ </property>
+ <child type="overlay">
+ <object class="GtkButton" 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"/>
+ <style>
+ <class name="circular"/>
+ <class name="tab-close-button"/>
+ </style>
+ </object>
+ </child>
+ <child type="overlay">
+ <object class="GtkButton" id="indicator_btn">
+ <property name="can-focus">False</property>
+ <property name="valign">start</property>
+ <property name="halign">start</property>
+ <binding name="tooltip-markup">
+ <lookup name="indicator-tooltip" type="AdwTabPage">
+ <lookup name="page">AdwTabThumbnail</lookup>
+ </lookup>
+ </binding>
+ <signal name="clicked" handler="indicator_clicked_cb" swapped="true"/>
+ <style>
+ <class name="circular"/>
+ <class name="tab-indicator"/>
+ <class name="image-button"/>
+ </style>
+ <property name="child">
+ <object class="GtkImage" id="indicator_icon"/>
+ </property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkBox">
+ <property name="orientation">horizontal</property>
+ <property name="halign">center</property>
+ <property name="spacing">6</property>
+ <child>
+ <object class="GtkStack" id="icon_stack">
+ <property name="margin-start">4</property>
+ <property name="margin-end">2</property>
+ <child>
+ <object class="GtkStackPage">
+ <property name="name">icon</property>
+ <property name="child">
+ <object class="GtkImage" id="icon">
+ <style>
+ <class name="tab-icon"/>
+ </style>
+ </object>
+ </property>
+ </object>
+ </child>
+ <child>
+ <object class="GtkStackPage">
+ <property name="name">spinner</property>
+ <property name="child">
+ <object class="GtkSpinner" id="spinner"/>
+ </property>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="AdwFadingLabel">
+ <binding name="label">
+ <lookup name="title" type="AdwTabPage">
+ <lookup name="page">AdwTabThumbnail</lookup>
+ </lookup>
+ </binding>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/adwaita.gresources.xml b/src/adwaita.gresources.xml
index 095d250e..b993036a 100644
--- a/src/adwaita.gresources.xml
+++ b/src/adwaita.gresources.xml
@@ -3,6 +3,7 @@
<gresource prefix="/org/gnome/Adwaita">
<file>glsl/fade.glsl</file>
<file>glsl/mask.glsl</file>
+ <file>glsl/tab-overview.glsl</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/adw-entry-apply-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/adw-external-link-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/scalable/actions/adw-expander-arrow-symbolic.svg</file>
@@ -27,6 +28,8 @@
<file preprocess="xml-stripblanks">adw-tab.ui</file>
<file preprocess="xml-stripblanks">adw-tab-bar.ui</file>
<file preprocess="xml-stripblanks">adw-tab-button.ui</file>
+ <file preprocess="xml-stripblanks">adw-tab-overview.ui</file>
+ <file preprocess="xml-stripblanks">adw-tab-thumbnail.ui</file>
<file preprocess="xml-stripblanks">adw-toast-widget.ui</file>
<file preprocess="xml-stripblanks">adw-view-switcher-bar.ui</file>
<file preprocess="xml-stripblanks">adw-view-switcher-button.ui</file>
diff --git a/src/adwaita.h b/src/adwaita.h
index 6707b57c..9e277ded 100644
--- a/src/adwaita.h
+++ b/src/adwaita.h
@@ -65,6 +65,7 @@ G_BEGIN_DECLS
#include "adw-swipeable.h"
#include "adw-tab-bar.h"
#include "adw-tab-button.h"
+#include "adw-tab-overview.h"
#include "adw-tab-view.h"
#include "adw-timed-animation.h"
#include "adw-toast-overlay.h"
diff --git a/src/glsl/tab-overview.glsl b/src/glsl/tab-overview.glsl
new file mode 100644
index 00000000..7ddd9dfa
--- /dev/null
+++ b/src/glsl/tab-overview.glsl
@@ -0,0 +1,18 @@
+uniform vec2 origin;
+uniform vec2 size;
+uniform float opacity;
+
+uniform sampler2D u_texture1;
+
+void
+mainImage(out vec4 fragColor,
+ in vec2 fragCoord,
+ in vec2 resolution,
+ in vec2 uv)
+{
+ if (fragCoord.x >= origin.x && fragCoord.x <= origin.x + size.x &&
+ fragCoord.y >= origin.y && fragCoord.y <= origin.y + size.y)
+ fragColor = vec4(0.0, 0.0, 0.0, 0.0);
+ else
+ fragColor = opacity * GskTexture(u_texture1, uv);
+}
diff --git a/src/meson.build b/src/meson.build
index dd6536a0..e14484ca 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -126,6 +126,7 @@ src_headers = [
'adw-swipeable.h',
'adw-tab-bar.h',
'adw-tab-button.h',
+ 'adw-tab-overview.h',
'adw-tab-view.h',
'adw-timed-animation.h',
'adw-toast.h',
@@ -191,6 +192,7 @@ src_sources = [
'adw-swipeable.c',
'adw-tab-bar.c',
'adw-tab-button.c',
+ 'adw-tab-overview.c',
'adw-tab-view.c',
'adw-timed-animation.c',
'adw-toast.c',
@@ -216,6 +218,8 @@ libadwaita_private_sources += files([
'adw-shadow-helper.c',
'adw-tab.c',
'adw-tab-box.c',
+ 'adw-tab-grid.c',
+ 'adw-tab-thumbnail.c',
'adw-toast-widget.c',
'adw-view-switcher-button.c',
'adw-widget-utils.c',
diff --git a/src/stylesheet/widgets/_tab-view.scss b/src/stylesheet/widgets/_tab-view.scss
index 1b140c40..58bc0abe 100644
--- a/src/stylesheet/widgets/_tab-view.scss
+++ b/src/stylesheet/widgets/_tab-view.scss
@@ -139,7 +139,41 @@ dnd {
}
}
+taboverview {
+ background: gtkmix($window_bg_color, $window_fg_color, 95%);
+}
+
+tabthumbnail > box {
+ margin: 10px;
+
+ picture.card {
+ background: $window_bg_color;
+ color: $window_fg_color;
+ }
+
+ button.circular {
+ margin: 6px;
+ color: $window_fg_color;
+ background-color: gtkalpha($window_bg_color, .75);
+ min-width: 24px;
+ min-height: 24px;
+
+ @if $contrast == 'high' {
+ box-shadow: 0 0 0 1px currentColor;
+ }
+
+ &:hover {
+ background-color: gtkalpha(gtkmix($window_bg_color, currentColor, 90%), .75);
+ }
+
+ &:active {
+ background-color: gtkalpha(gtkmix($window_bg_color, currentColor, 80%), .75);
+ }
+ }
+}
+
tabview:drop(active),
-tabbox:drop(active) {
+tabbox:drop(active),
+tabgrid:drop(active) {
box-shadow: none;
}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]