[libadwaita/wip/exalm/tabs: 3/6] Add AdwTabBox
- From: Alexander Mikhaylenko <alexm src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libadwaita/wip/exalm/tabs: 3/6] Add AdwTabBox
- Date: Tue, 11 May 2021 08:09:35 +0000 (UTC)
commit d5faf3e4e7d25b8794ef6a39b831b8426daccc07
Author: Alexander Mikhaylenko <alexm gnome org>
Date: Sun Sep 13 02:27:04 2020 +0500
Add AdwTabBox
doc/meson.build | 1 +
src/adw-tab-box-private.h | 54 +
src/adw-tab-box.c | 3569 +++++++++++++++++++++++++++++++++++++++++++++
src/meson.build | 1 +
4 files changed, 3625 insertions(+)
---
diff --git a/doc/meson.build b/doc/meson.build
index de2dd03..12dc0a1 100644
--- a/doc/meson.build
+++ b/doc/meson.build
@@ -21,6 +21,7 @@ private_headers = [
'adw-shadow-helper-private.h',
'adw-swipe-tracker-private.h',
'adw-tab-private.h',
+ 'adw-tab-box-private.h',
'adw-tab-view-private.h',
'adw-view-switcher-button-private.h',
'adw-window-mixin-private.h',
diff --git a/src/adw-tab-box-private.h b/src/adw-tab-box-private.h
new file mode 100644
index 0000000..f634711
--- /dev/null
+++ b/src/adw-tab-box-private.h
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2020 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 <gtk/gtk.h>
+#include "adw-tab-view.h"
+
+G_BEGIN_DECLS
+
+#define ADW_TYPE_TAB_BOX (adw_tab_box_get_type())
+
+G_DECLARE_FINAL_TYPE (AdwTabBox, adw_tab_box, ADW, TAB_BOX, GtkWidget)
+
+void adw_tab_box_set_view (AdwTabBox *self,
+ AdwTabView *view);
+void adw_tab_box_set_adjustment (AdwTabBox *self,
+ GtkAdjustment *adjustment);
+
+void adw_tab_box_attach_page (AdwTabBox *self,
+ AdwTabPage *page,
+ int position);
+void adw_tab_box_detach_page (AdwTabBox *self,
+ AdwTabPage *page);
+void adw_tab_box_select_page (AdwTabBox *self,
+ AdwTabPage *page);
+
+void adw_tab_box_try_focus_selected_tab (AdwTabBox *self);
+gboolean adw_tab_box_is_page_focused (AdwTabBox *self,
+ AdwTabPage *page);
+
+void adw_tab_box_setup_extra_drop_target (AdwTabBox *self,
+ GdkDragAction actions,
+ GType *types,
+ gsize n_types);
+
+gboolean adw_tab_box_get_expand_tabs (AdwTabBox *self);
+void adw_tab_box_set_expand_tabs (AdwTabBox *self,
+ gboolean expand_tabs);
+
+gboolean adw_tab_box_get_inverted (AdwTabBox *self);
+void adw_tab_box_set_inverted (AdwTabBox *self,
+ gboolean inverted);
+
+G_END_DECLS
diff --git a/src/adw-tab-box.c b/src/adw-tab-box.c
new file mode 100644
index 0000000..0967c7c
--- /dev/null
+++ b/src/adw-tab-box.c
@@ -0,0 +1,3569 @@
+/*
+ * Copyright (C) 2020-2021 Purism SPC
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ *
+ * Author: Alexander Mikhaylenko <alexander mikhaylenko puri sm>
+ */
+
+#include "config.h"
+
+#include "adw-tab-box-private.h"
+#include "adw-animation-private.h"
+#include "adw-tab-private.h"
+#include "adw-tab-bar-private.h"
+#include "adw-tab-view-private.h"
+#include <math.h>
+
+/* Border collapsing without glitches */
+#define OVERLAP 1
+#define DND_THRESHOLD_MULTIPLIER 4
+#define DROP_SWITCH_TIMEOUT 500
+
+#define AUTOSCROLL_SPEED 2.5
+
+#define OPEN_ANIMATION_DURATION 200
+#define CLOSE_ANIMATION_DURATION 200
+#define FOCUS_ANIMATION_DURATION 200
+#define SCROLL_ANIMATION_DURATION 200
+#define RESIZE_ANIMATION_DURATION 200
+#define REORDER_ANIMATION_DURATION 250
+#define ICON_RESIZE_ANIMATION_DURATION 200
+
+#define MAX_TAB_WIDTH_NON_EXPAND 220
+
+typedef enum {
+ TAB_RESIZE_NORMAL,
+ TAB_RESIZE_FIXED_TAB_WIDTH,
+ TAB_RESIZE_FIXED_END_PADDING
+} TabResizeMode;
+
+typedef struct {
+ GdkDrag *drag;
+
+ AdwTab *tab;
+ GtkBorder tab_margin;
+
+ int hotspot_x;
+ int hotspot_y;
+
+ int width;
+ int target_width;
+ AdwAnimation *resize_animation;
+} DragIcon;
+
+typedef struct {
+ AdwTabPage *page;
+ AdwTab *tab;
+
+ int pos;
+ int width;
+ int last_width;
+
+ double end_reorder_offset;
+ double reorder_offset;
+
+ AdwAnimation *reorder_animation;
+ gboolean reorder_ignore_bounds;
+
+ double appear_progress;
+ AdwAnimation *appear_animation;
+
+ gulong notify_needs_attention_id;
+} TabInfo;
+
+struct _AdwTabBox
+{
+ GtkWidget parent_instance;
+
+ gboolean pinned;
+ AdwTabBar *tab_bar;
+ AdwTabView *view;
+ GtkAdjustment *adjustment;
+ gboolean needs_attention_left;
+ gboolean needs_attention_right;
+ gboolean expand_tabs;
+ gboolean inverted;
+
+ GtkEventController *view_drop_target;
+ GtkGesture *drag_gesture;
+
+ GList *tabs;
+ int n_tabs;
+
+ GtkPopover *context_menu;
+
+ int allocated_width;
+ int last_width;
+ int end_padding;
+ int initial_end_padding;
+ TabResizeMode tab_resize_mode;
+ AdwAnimation *resize_animation;
+
+ TabInfo *selected_tab;
+
+ gboolean hovering;
+ TabInfo *pressed_tab;
+ TabInfo *reordered_tab;
+ AdwAnimation *reorder_animation;
+
+ int reorder_start_pos;
+ int reorder_x;
+ int reorder_y;
+ int reorder_index;
+ int reorder_window_x;
+ gboolean continue_reorder;
+ gboolean indirect_reordering;
+
+ gboolean dragging;
+ double drag_offset_x;
+ double drag_offset_y;
+
+ guint drag_autoscroll_cb_id;
+ gint64 drag_autoscroll_prev_time;
+
+ AdwTabPage *detached_page;
+ int detached_index;
+ TabInfo *reorder_placeholder;
+ AdwTabPage *placeholder_page;
+ int placeholder_scroll_offset;
+ gboolean can_remove_placeholder;
+ DragIcon *drag_icon;
+ gboolean should_detach_into_new_window;
+
+ TabInfo *drop_target_tab;
+ guint drop_switch_timeout_id;
+ guint reset_drop_target_tab_id;
+ double drop_target_x;
+
+ struct {
+ TabInfo *info;
+ int pos;
+ gint64 duration;
+ gboolean keep_selected_visible;
+ } scheduled_scroll;
+
+ AdwAnimation *scroll_animation;
+ gboolean scroll_animation_done;
+ double scroll_animation_from;
+ double scroll_animation_offset;
+ TabInfo *scroll_animation_tab;
+ gboolean block_scrolling;
+ double adjustment_prev_value;
+
+ GdkDragAction extra_drag_actions;
+ GType *extra_drag_types;
+ gsize extra_drag_n_types;
+};
+
+G_DEFINE_TYPE_WITH_CODE (AdwTabBox, adw_tab_box, GTK_TYPE_WIDGET,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_SCROLLABLE, NULL))
+
+enum {
+ PROP_0,
+ PROP_PINNED,
+ PROP_TAB_BAR,
+ PROP_VIEW,
+ PROP_NEEDS_ATTENTION_LEFT,
+ PROP_NEEDS_ATTENTION_RIGHT,
+ PROP_RESIZE_FROZEN,
+ PROP_HADJUSTMENT,
+ PROP_VADJUSTMENT,
+ PROP_HSCROLL_POLICY,
+ PROP_VSCROLL_POLICY,
+ LAST_PROP = PROP_HADJUSTMENT
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_STOP_KINETIC_SCROLLING,
+ SIGNAL_EXTRA_DRAG_DROP,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+/* Helpers */
+
+static void
+remove_and_free_tab_info (TabInfo *info)
+{
+ gtk_widget_unparent (GTK_WIDGET (info->tab));
+
+ g_free (info);
+}
+
+static inline int
+get_tab_position (AdwTabBox *self,
+ TabInfo *info)
+{
+ if (info == self->reordered_tab)
+ return self->reorder_window_x;
+
+ return info->pos;
+}
+
+static inline TabInfo *
+find_tab_info_at (AdwTabBox *self,
+ double x)
+{
+ GList *l;
+
+ if (self->reordered_tab) {
+ int pos = get_tab_position (self, self->reordered_tab);
+
+ if (pos <= x && x < pos + self->reordered_tab->width)
+ return self->reordered_tab;
+ }
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ if (info != self->reordered_tab &&
+ info->pos <= x && x < info->pos + info->width)
+ return info;
+ }
+
+ return NULL;
+}
+
+static inline GList *
+find_link_for_page (AdwTabBox *self,
+ AdwTabPage *page)
+{
+ GList *l;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ if (info->page == page)
+ return l;
+ }
+
+ return NULL;
+}
+
+static inline TabInfo *
+find_info_for_page (AdwTabBox *self,
+ AdwTabPage *page)
+{
+ GList *l = find_link_for_page (self, page);
+
+ return l ? l->data : NULL;
+}
+
+static GList *
+find_nth_alive_tab (AdwTabBox *self,
+ guint position)
+{
+ GList *l;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ if (!info->page)
+ continue;
+
+ if (!position--)
+ return l;
+ }
+
+ return NULL;
+}
+
+static inline int
+calculate_tab_width (TabInfo *info,
+ int base_width)
+{
+ return OVERLAP + (int) floor ((base_width - OVERLAP) * info->appear_progress);
+}
+
+static int
+get_base_tab_width (AdwTabBox *self,
+ gboolean target)
+{
+ double max_progress = 0;
+ double n = 0;
+ double used_width;
+ GList *l;
+ int ret;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ max_progress = MAX (max_progress, info->appear_progress);
+ n += info->appear_progress;
+ }
+
+ used_width = (self->allocated_width + (n + 1) * OVERLAP - (target ? 0 : self->end_padding)) * max_progress;
+
+ ret = (int) ceil (used_width / n);
+
+ if (!self->expand_tabs)
+ ret = MIN (ret, MAX_TAB_WIDTH_NON_EXPAND + OVERLAP);
+
+ return ret;
+}
+
+static int
+predict_tab_width (AdwTabBox *self,
+ TabInfo *info,
+ gboolean assume_placeholder)
+{
+ int n;
+ int width = self->allocated_width;
+ int min;
+
+ if (self->pinned)
+ n = adw_tab_view_get_n_pinned_pages (self->view);
+ else
+ n = adw_tab_view_get_n_pages (self->view) - adw_tab_view_get_n_pinned_pages (self->view);
+
+ if (assume_placeholder)
+ n++;
+
+ width += OVERLAP * (n + 1) - self->end_padding;
+
+ /* Tabs have 0 minimum width, we need natural width instead */
+ gtk_widget_measure (GTK_WIDGET (info->tab), GTK_ORIENTATION_HORIZONTAL, -1,
+ NULL, &min, NULL, NULL);
+
+ if (self->expand_tabs)
+ return MAX ((int) floor (width / (double) n), min);
+ else
+ return CLAMP ((int) floor (width / (double) n), min, MAX_TAB_WIDTH_NON_EXPAND);
+}
+
+static int
+calculate_tab_offset (AdwTabBox *self,
+ TabInfo *info,
+ gboolean target)
+{
+ int width;
+
+ if (!self->reordered_tab)
+ return 0;
+
+ width = (target ? adw_tab_get_display_width (self->reordered_tab->tab) : self->reordered_tab->width) -
OVERLAP;
+
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ width = -width;
+
+ return (int) round (width * (target ? info->end_reorder_offset : info->reorder_offset));
+}
+
+static void
+get_visible_range (AdwTabBox *self,
+ int *lower,
+ int *upper)
+{
+ int min = -OVERLAP;
+ int max = self->allocated_width + OVERLAP;
+
+ if (self->pinned) {
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ min += OVERLAP;
+ else
+ max -= OVERLAP;
+ }
+
+ if (self->adjustment) {
+ double value = gtk_adjustment_get_value (self->adjustment);
+ double page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ min = MAX (min, (int) floor (value) - OVERLAP);
+ max = MIN (max, (int) ceil (value + page_size) + OVERLAP);
+ }
+
+ if (lower)
+ *lower = min;
+
+ if (upper)
+ *upper = max;
+}
+
+static inline gboolean
+is_touchscreen (GtkGesture *gesture)
+{
+ GtkEventController *controller = GTK_EVENT_CONTROLLER (gesture);
+ GdkDevice *device = gtk_event_controller_get_current_event_device (controller);
+ GdkInputSource input_source = gdk_device_get_source (device);
+
+ return input_source == GDK_SOURCE_TOUCHSCREEN;
+}
+
+/* Tab resize delay */
+
+static void
+resize_animation_value_cb (double value,
+ gpointer user_data)
+{
+ AdwTabBox *self = ADW_TAB_BOX (user_data);
+ double target_end_padding = 0;
+
+ if (!self->expand_tabs) {
+ int predicted_tab_width = get_base_tab_width (self, TRUE);
+ GList *l;
+
+ target_end_padding = self->allocated_width + OVERLAP;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ target_end_padding -= calculate_tab_width (info, predicted_tab_width) - OVERLAP;
+ }
+
+ target_end_padding = MAX (target_end_padding, 0);
+ }
+
+ self->end_padding = (int) floor (adw_lerp (self->initial_end_padding, target_end_padding, value));
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+resize_animation_done_cb (gpointer user_data)
+{
+ AdwTabBox *self = ADW_TAB_BOX (user_data);
+
+ self->end_padding = 0;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ g_clear_pointer (&self->resize_animation, adw_animation_unref);
+}
+
+static void
+set_tab_resize_mode (AdwTabBox *self,
+ TabResizeMode mode)
+{
+ gboolean notify;
+
+ if (self->tab_resize_mode == mode)
+ return;
+
+ if (mode == TAB_RESIZE_FIXED_TAB_WIDTH) {
+ GList *l;
+
+ self->last_width = self->allocated_width;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ if (info->appear_animation)
+ info->last_width = adw_tab_get_display_width (info->tab);
+ else
+ info->last_width = info->width;
+ }
+ } else {
+ self->last_width = 0;
+ }
+
+ if (mode == TAB_RESIZE_NORMAL) {
+ self->initial_end_padding = self->end_padding;
+
+ self->resize_animation =
+ adw_animation_new (GTK_WIDGET (self), 0, 1,
+ RESIZE_ANIMATION_DURATION,
+ adw_ease_out_cubic,
+ resize_animation_value_cb,
+ resize_animation_done_cb,
+ self);
+
+ adw_animation_start (self->resize_animation);
+ }
+
+ notify = (self->tab_resize_mode == TAB_RESIZE_NORMAL) !=
+ (mode == TAB_RESIZE_NORMAL);
+
+ self->tab_resize_mode = mode;
+
+ if (notify)
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_RESIZE_FROZEN]);
+}
+
+/* Hover */
+
+static void
+update_hover (AdwTabBox *self)
+{
+ if (!self->dragging && !self->hovering)
+ set_tab_resize_mode (self, TAB_RESIZE_NORMAL);
+}
+
+static void
+motion_cb (AdwTabBox *self,
+ double x,
+ double y,
+ GtkEventController *controller)
+{
+ GdkDevice *device = gtk_event_controller_get_current_event_device (controller);
+ GdkInputSource input_source = gdk_device_get_source (device);
+
+ if (input_source == GDK_SOURCE_TOUCHSCREEN)
+ return;
+
+ if (self->hovering)
+ return;
+
+ self->hovering = TRUE;
+
+ update_hover (self);
+}
+
+static void
+leave_cb (AdwTabBox *self,
+ GtkEventController *controller)
+{
+ self->hovering = FALSE;
+
+ update_hover (self);
+}
+
+/* Keybindings */
+
+static void
+focus_tab_cb (AdwTabBox *self,
+ GVariant *args)
+{
+ GtkDirectionType direction;
+ gboolean last, is_rtl, success;
+
+ if (!self->view || !self->selected_tab)
+ return;
+
+ g_variant_get (args, "(hb)", &direction, &last);
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+ success = last;
+
+ if (direction == GTK_DIR_LEFT)
+ direction = is_rtl ? GTK_DIR_TAB_FORWARD : GTK_DIR_TAB_BACKWARD;
+ else if (direction == GTK_DIR_RIGHT)
+ direction = is_rtl ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD;
+
+ if (direction == GTK_DIR_TAB_BACKWARD) {
+ if (last)
+ success = adw_tab_view_select_first_page (self->view);
+ else
+ success = adw_tab_view_select_previous_page (self->view);
+ } else if (direction == GTK_DIR_TAB_FORWARD) {
+ if (last)
+ success = adw_tab_view_select_last_page (self->view);
+ else
+ success = adw_tab_view_select_next_page (self->view);
+ }
+
+ if (!success)
+ gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+static void
+reorder_tab_cb (AdwTabBox *self,
+ GVariant *args)
+{
+ GtkDirectionType direction;
+ gboolean last, is_rtl, success;
+
+ if (!self->view || !self->selected_tab || !self->selected_tab->page)
+ return;
+
+ g_variant_get (args, "(hb)", &direction, &last);
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+ success = last;
+
+ if (direction == GTK_DIR_LEFT)
+ direction = is_rtl ? GTK_DIR_TAB_FORWARD : GTK_DIR_TAB_BACKWARD;
+ else if (direction == GTK_DIR_RIGHT)
+ direction = is_rtl ? GTK_DIR_TAB_BACKWARD : GTK_DIR_TAB_FORWARD;
+
+ if (direction == GTK_DIR_TAB_BACKWARD) {
+ if (last)
+ success = adw_tab_view_reorder_first (self->view, self->selected_tab->page);
+ else
+ success = adw_tab_view_reorder_backward (self->view, self->selected_tab->page);
+ } else if (direction == GTK_DIR_TAB_FORWARD) {
+ if (last)
+ success = adw_tab_view_reorder_last (self->view, self->selected_tab->page);
+ else
+ success = adw_tab_view_reorder_forward (self->view, self->selected_tab->page);
+ }
+
+ if (!success)
+ gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+static void
+add_focus_bindings (GtkWidgetClass *widget_class,
+ guint keysym,
+ GtkDirectionType direction,
+ gboolean last)
+{
+ /* All keypad keysyms are aligned at the same order as non-keypad ones */
+ guint keypad_keysym = keysym - GDK_KEY_Left + GDK_KEY_KP_Left;
+
+ gtk_widget_class_add_binding (widget_class, keysym, 0,
+ (GtkShortcutFunc) focus_tab_cb,
+ "(hb)", direction, last);
+ gtk_widget_class_add_binding (widget_class, keypad_keysym, 0,
+ (GtkShortcutFunc) focus_tab_cb,
+ "(hb)", direction, last);
+}
+
+static void
+add_reorder_bindings (GtkWidgetClass *widget_class,
+ guint keysym,
+ GtkDirectionType direction,
+ gboolean last)
+{
+ /* All keypad keysyms are aligned at the same order as non-keypad ones */
+ guint keypad_keysym = keysym - GDK_KEY_Left + GDK_KEY_KP_Left;
+
+ gtk_widget_class_add_binding (widget_class, keysym, GDK_SHIFT_MASK,
+ (GtkShortcutFunc) reorder_tab_cb,
+ "(hb)", direction, last);
+ gtk_widget_class_add_binding (widget_class, keypad_keysym, GDK_SHIFT_MASK,
+ (GtkShortcutFunc) reorder_tab_cb,
+ "(hb)", direction, last);
+}
+
+static void
+activate_tab (AdwTabBox *self)
+{
+ GtkWidget *child;
+
+ if (!self->selected_tab || !self->selected_tab->page)
+ return;
+
+ child = adw_tab_page_get_child (self->selected_tab->page);
+
+ gtk_widget_grab_focus (child);
+}
+
+/* Scrolling */
+
+static void
+update_visible (AdwTabBox *self)
+{
+ gboolean left = FALSE, right = FALSE;
+ GList *l;
+ double value, page_size;
+
+ if (!self->adjustment)
+ return;
+
+ value = gtk_adjustment_get_value (self->adjustment);
+ page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ if (!self->adjustment)
+ return;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+ int pos;
+
+ if (!info->page)
+ continue;
+
+ pos = get_tab_position (self, info);
+
+ adw_tab_set_fully_visible (info->tab,
+ pos + OVERLAP >= value &&
+ pos + info->width - OVERLAP <= value + page_size);
+
+ if (!adw_tab_page_get_needs_attention (info->page))
+ continue;
+
+ if (pos + info->width / 2.0 <= value)
+ left = TRUE;
+
+ if (pos + info->width / 2.0 >= value + page_size)
+ right = TRUE;
+ }
+
+ if (self->needs_attention_left != left) {
+ self->needs_attention_left = left;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NEEDS_ATTENTION_LEFT]);
+ }
+
+ if (self->needs_attention_right != right) {
+ self->needs_attention_right = right;
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NEEDS_ATTENTION_RIGHT]);
+ }
+}
+
+static double
+get_scroll_animation_value (AdwTabBox *self)
+{
+ double to, value;
+
+ g_assert (self->scroll_animation);
+
+ to = self->scroll_animation_offset;
+
+ if (self->scroll_animation_tab) {
+ double page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ to += get_tab_position (self, self->scroll_animation_tab);
+ to = CLAMP (to, 0, self->allocated_width - page_size);
+ }
+
+ value = adw_animation_get_value (self->scroll_animation);
+
+ return round (adw_lerp (self->scroll_animation_from, to, value));
+}
+
+static gboolean
+drop_switch_timeout_cb (AdwTabBox *self)
+{
+ self->drop_switch_timeout_id = 0;
+ adw_tab_view_set_selected_page (self->view,
+ self->drop_target_tab->page);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+set_drop_target_tab (AdwTabBox *self,
+ TabInfo *info)
+{
+ if (self->drop_target_tab == info)
+ return;
+
+ if (self->drop_target_tab)
+ g_clear_handle_id (&self->drop_switch_timeout_id, g_source_remove);
+
+ self->drop_target_tab = info;
+
+ if (self->drop_target_tab) {
+ self->drop_switch_timeout_id =
+ g_timeout_add (DROP_SWITCH_TIMEOUT,
+ (GSourceFunc) drop_switch_timeout_cb,
+ self);
+ }
+}
+
+static void
+adjustment_value_changed_cb (AdwTabBox *self)
+{
+ double value = gtk_adjustment_get_value (self->adjustment);
+
+ update_visible (self);
+
+ if (self->drop_target_tab) {
+ self->drop_target_x += (value - self->adjustment_prev_value);
+ set_drop_target_tab (self, find_tab_info_at (self, self->drop_target_x));
+ }
+
+ self->adjustment_prev_value = value;
+
+ if (self->block_scrolling)
+ return;
+
+ if (self->scroll_animation)
+ adw_animation_stop (self->scroll_animation);
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static void
+scroll_animation_value_cb (double value,
+ gpointer user_data)
+{
+ gtk_widget_queue_resize (GTK_WIDGET (user_data));
+}
+
+static void
+scroll_animation_done_cb (gpointer user_data)
+{
+ AdwTabBox *self = ADW_TAB_BOX (user_data);
+
+ self->scroll_animation_done = TRUE;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+animate_scroll (AdwTabBox *self,
+ TabInfo *info,
+ double offset,
+ gint64 duration)
+{
+ if (!self->adjustment)
+ return;
+
+ g_signal_emit (self, signals[SIGNAL_STOP_KINETIC_SCROLLING], 0);
+
+ if (self->scroll_animation)
+ adw_animation_stop (self->scroll_animation);
+
+ g_clear_pointer (&self->scroll_animation, adw_animation_unref);
+ self->scroll_animation_done = FALSE;
+ self->scroll_animation_from = gtk_adjustment_get_value (self->adjustment);
+ self->scroll_animation_tab = info;
+ self->scroll_animation_offset = offset;
+
+ /* The actual update will be done in size_allocate(). After the animation
+ * finishes, don't remove it right away, it will be done in size-allocate as
+ * well after one last update, so that we don't miss the last frame.
+ */
+
+ self->scroll_animation =
+ adw_animation_new (GTK_WIDGET (self), 0, 1, duration,
+ adw_ease_out_cubic,
+ scroll_animation_value_cb,
+ scroll_animation_done_cb,
+ self);
+
+ adw_animation_start (self->scroll_animation);
+}
+
+static void
+animate_scroll_relative (AdwTabBox *self,
+ double delta,
+ gint64 duration)
+{
+ double current_value = gtk_adjustment_get_value (self->adjustment);
+
+ if (self->scroll_animation) {
+ current_value = self->scroll_animation_offset;
+
+ if (self->scroll_animation_tab)
+ current_value += get_tab_position (self, self->scroll_animation_tab);
+ }
+
+ animate_scroll (self, NULL, current_value + delta, duration);
+}
+
+static void
+scroll_to_tab_full (AdwTabBox *self,
+ TabInfo *info,
+ int pos,
+ gint64 duration,
+ gboolean keep_selected_visible)
+{
+ int tab_width;
+ double padding, value, page_size;
+
+ if (!self->adjustment)
+ return;
+
+ tab_width = info->width;
+
+ if (tab_width < 0) {
+ self->scheduled_scroll.info = info;
+ self->scheduled_scroll.pos = pos;
+ self->scheduled_scroll.duration = duration;
+ self->scheduled_scroll.keep_selected_visible = keep_selected_visible;
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+ return;
+ }
+
+ if (info->appear_animation)
+ tab_width = adw_tab_get_display_width (info->tab);
+
+ value = gtk_adjustment_get_value (self->adjustment);
+ page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ padding = MIN (tab_width, page_size - tab_width) / 2.0;
+
+ if (pos < 0)
+ pos = get_tab_position (self, info);
+
+ if (pos + OVERLAP < value)
+ animate_scroll (self, info, -padding, duration);
+ else if (pos + tab_width - OVERLAP > value + page_size)
+ animate_scroll (self, info, tab_width + padding - page_size, duration);
+}
+
+static void
+scroll_to_tab (AdwTabBox *self,
+ TabInfo *info,
+ gint64 duration)
+{
+ scroll_to_tab_full (self, info, -1, duration, FALSE);
+}
+
+static gboolean
+scroll_cb (AdwTabBox *self,
+ double dx,
+ double dy,
+ GtkEventController *controller)
+{
+ double page_size, pow_unit, scroll_unit;
+ GdkDevice *source_device;
+ GdkInputSource input_source;
+
+ if (!self->adjustment)
+ return GDK_EVENT_PROPAGATE;
+
+ source_device = gtk_event_controller_get_current_event_device (controller);
+ input_source = gdk_device_get_source (source_device);
+
+ if (input_source != GDK_SOURCE_MOUSE)
+ return GDK_EVENT_PROPAGATE;
+
+ page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ /* Copied from gtkrange.c, _gtk_range_get_wheel_delta() */
+ pow_unit = pow (page_size, 2.0 / 3.0);
+ scroll_unit = MIN (pow_unit, page_size / 2.0);
+
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ dy = -dy;
+
+ animate_scroll_relative (self, dy * scroll_unit, SCROLL_ANIMATION_DURATION);
+
+ return GDK_EVENT_STOP;
+}
+
+/* Reordering */
+
+static void
+force_end_reordering (AdwTabBox *self)
+{
+ GList *l;
+
+ if (self->dragging || !self->reordered_tab)
+ return;
+
+ if (self->reorder_animation)
+ adw_animation_stop (self->reorder_animation);
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ if (info->reorder_animation)
+ adw_animation_stop (info->reorder_animation);
+ }
+}
+
+static void
+check_end_reordering (AdwTabBox *self)
+{
+ GList *l;
+
+ if (self->dragging || !self->reordered_tab || self->continue_reorder)
+ return;
+
+ if (self->reorder_animation)
+ return;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ if (info->reorder_animation)
+ return;
+ }
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ info->end_reorder_offset = 0;
+ info->reorder_offset = 0;
+ }
+
+ self->reordered_tab->reorder_ignore_bounds = FALSE;
+
+ self->tabs = g_list_remove (self->tabs, self->reordered_tab);
+ self->tabs = g_list_insert (self->tabs, self->reordered_tab, self->reorder_index);
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+ self->reordered_tab = NULL;
+}
+
+static void
+start_reordering (AdwTabBox *self,
+ TabInfo *info)
+{
+ self->reordered_tab = info;
+
+ /* The reordered tab should be displayed above everything else */
+ gtk_widget_insert_before (GTK_WIDGET (self->reordered_tab->tab),
+ GTK_WIDGET (self), NULL);
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static int
+get_reorder_position (AdwTabBox *self)
+{
+ int lower, upper;
+
+ if (self->reordered_tab->reorder_ignore_bounds)
+ return self->reorder_x;
+
+ get_visible_range (self, &lower, &upper);
+
+ return CLAMP (self->reorder_x, lower, upper - self->reordered_tab->width);
+}
+
+static void
+reorder_animation_value_cb (double value,
+ gpointer user_data)
+{
+ TabInfo *dest_tab = user_data;
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (dest_tab->tab));
+ AdwTabBox *self = ADW_TAB_BOX (parent);
+ gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+ double x1, x2;
+
+ x1 = get_reorder_position (self);
+ x2 = dest_tab->pos - calculate_tab_offset (self, dest_tab, FALSE);
+
+ if (dest_tab->end_reorder_offset * (is_rtl ? 1 : -1) > 0)
+ x2 += dest_tab->width - self->reordered_tab->width;
+
+ self->reorder_window_x = (int) round (adw_lerp (x1, x2, value));
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static void
+reorder_animation_done_cb (gpointer user_data)
+{
+ TabInfo *dest_tab = user_data;
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (dest_tab->tab));
+ AdwTabBox *self = ADW_TAB_BOX (parent);
+
+ g_clear_pointer (&self->reorder_animation, adw_animation_unref);
+ check_end_reordering (self);
+}
+
+static void
+animate_reordering (AdwTabBox *self,
+ TabInfo *dest_tab)
+{
+ if (self->reorder_animation)
+ adw_animation_stop (self->reorder_animation);
+
+ self->reorder_animation =
+ adw_animation_new (GTK_WIDGET (self), 0, 1,
+ REORDER_ANIMATION_DURATION,
+ adw_ease_out_cubic,
+ reorder_animation_value_cb,
+ reorder_animation_done_cb,
+ dest_tab);
+
+ adw_animation_start (self->reorder_animation);
+
+ check_end_reordering (self);
+}
+
+static void
+reorder_offset_animation_value_cb (double value,
+ gpointer user_data)
+{
+ TabInfo *info = user_data;
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (info->tab));
+
+ info->reorder_offset = value;
+ gtk_widget_queue_allocate (parent);
+}
+
+static void
+reorder_offset_animation_done_cb (gpointer user_data)
+{
+ TabInfo *info = user_data;
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (info->tab));
+ AdwTabBox *self = ADW_TAB_BOX (parent);
+
+ g_clear_pointer (&info->reorder_animation, adw_animation_unref);
+ check_end_reordering (self);
+}
+
+static void
+animate_reorder_offset (AdwTabBox *self,
+ TabInfo *info,
+ double offset)
+{
+ gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+ offset *= (is_rtl ? -1 : 1);
+
+ if (info->end_reorder_offset == offset)
+ return;
+
+ info->end_reorder_offset = offset;
+
+ if (info->reorder_animation)
+ adw_animation_stop (info->reorder_animation);
+
+ info->reorder_animation =
+ adw_animation_new (GTK_WIDGET (self), info->reorder_offset, offset,
+ REORDER_ANIMATION_DURATION,
+ adw_ease_out_cubic,
+ reorder_offset_animation_value_cb,
+ reorder_offset_animation_done_cb,
+ info);
+
+ adw_animation_start (info->reorder_animation);
+}
+
+static void
+reset_reorder_animations (AdwTabBox *self)
+{
+ int i, original_index;
+ GList *l;
+
+ if (!adw_get_enable_animations (GTK_WIDGET (self)))
+ return;
+
+ l = find_link_for_page (self, self->reordered_tab->page);
+ original_index = g_list_position (self->tabs, l);
+
+ if (self->reorder_index > original_index)
+ for (i = 0; i < self->reorder_index - original_index; i++) {
+ l = l->next;
+ animate_reorder_offset (self, l->data, 0);
+ }
+
+ if (self->reorder_index < original_index)
+ for (i = 0; i < original_index - self->reorder_index; i++) {
+ l = l->prev;
+ animate_reorder_offset (self, l->data, 0);
+ }
+}
+
+static void
+page_reordered_cb (AdwTabBox *self,
+ AdwTabPage *page,
+ int index)
+{
+ GList *link;
+ int original_index;
+ TabInfo *info, *dest_tab;
+ gboolean is_rtl;
+
+ if (adw_tab_page_get_pinned (page) != self->pinned)
+ return;
+
+ self->continue_reorder = self->reordered_tab && page == self->reordered_tab->page;
+
+ if (self->continue_reorder)
+ reset_reorder_animations (self);
+ else
+ force_end_reordering (self);
+
+ link = find_link_for_page (self, page);
+ info = link->data;
+ original_index = g_list_position (self->tabs, link);
+
+ if (!self->continue_reorder)
+ start_reordering (self, info);
+
+ if (self->continue_reorder)
+ self->reorder_x = self->reorder_window_x;
+ else
+ self->reorder_x = info->pos;
+
+ self->reorder_index = index;
+
+ if (!self->pinned)
+ self->reorder_index -= adw_tab_view_get_n_pinned_pages (self->view);
+
+ dest_tab = g_list_nth_data (self->tabs, self->reorder_index);
+
+ if (info == self->selected_tab)
+ scroll_to_tab_full (self, self->selected_tab, dest_tab->pos, REORDER_ANIMATION_DURATION, FALSE);
+
+ animate_reordering (self, dest_tab);
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+ /* If animations are disabled, animate_reordering() animation will have
+ * already finished and called check_end_reordering () by this point, so
+ * it's too late to animate these, so we get a crash.
+ */
+
+ if (adw_get_enable_animations (GTK_WIDGET (self)) &&
+ gtk_widget_get_mapped (GTK_WIDGET (self))) {
+ int i;
+
+ if (self->reorder_index > original_index)
+ for (i = 0; i < self->reorder_index - original_index; i++) {
+ link = link->next;
+ animate_reorder_offset (self, link->data, is_rtl ? 1 : -1);
+ }
+
+ if (self->reorder_index < original_index)
+ for (i = 0; i < original_index - self->reorder_index; i++) {
+ link = link->prev;
+ animate_reorder_offset (self, link->data, is_rtl ? -1 : 1);
+ }
+ }
+
+ self->continue_reorder = FALSE;
+}
+
+static void
+update_drag_reodering (AdwTabBox *self)
+{
+ gboolean is_rtl, after_selected, found_index;
+ int x;
+ int i = 0;
+ int width;
+ GList *l;
+
+ if (!self->dragging)
+ return;
+
+ x = get_reorder_position (self);
+
+ width = adw_tab_get_display_width (self->reordered_tab->tab);
+
+ self->reorder_window_x = x;
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+ after_selected = FALSE;
+ found_index = FALSE;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+ int center = info->pos - calculate_tab_offset (self, info, FALSE) + info->width / 2;
+ double offset = 0;
+
+ if (x + width > center && center > x &&
+ (!found_index || after_selected)) {
+ self->reorder_index = i;
+ found_index = TRUE;
+ }
+
+ i++;
+
+ if (info == self->reordered_tab) {
+ after_selected = TRUE;
+ continue;
+ }
+
+ if (after_selected != is_rtl && x + width > center)
+ offset = -1;
+ else if (after_selected == is_rtl && x < center)
+ offset = 1;
+
+ animate_reorder_offset (self, info, offset);
+ }
+}
+
+static gboolean
+drag_autoscroll_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ AdwTabBox *self)
+{
+ double value, page_size;
+ double x, delta_ms, start_threshold, end_threshold, autoscroll_factor;
+ gint64 time;
+ int offset = 0;
+ int tab_width = 0;
+ int autoscroll_area = 0;
+
+ if (self->reordered_tab) {
+ gtk_widget_measure (GTK_WIDGET (self->reordered_tab->tab),
+ GTK_ORIENTATION_HORIZONTAL, -1,
+ NULL, &tab_width, NULL, NULL);
+ tab_width -= 2 * OVERLAP;
+ x = (double) self->reorder_x + OVERLAP;
+ } else if (self->drop_target_tab) {
+ gtk_widget_measure (GTK_WIDGET (self->drop_target_tab->tab),
+ GTK_ORIENTATION_HORIZONTAL, -1,
+ NULL, &tab_width, NULL, NULL);
+ tab_width -= 2 * OVERLAP;
+ x = (double) self->drop_target_x + OVERLAP - tab_width / 2;
+ } else {
+ return G_SOURCE_CONTINUE;
+ }
+
+ value = gtk_adjustment_get_value (self->adjustment);
+ page_size = gtk_adjustment_get_page_size (self->adjustment);
+ autoscroll_area = tab_width / 2;
+
+ x = CLAMP (x,
+ autoscroll_area,
+ self->allocated_width - tab_width - autoscroll_area);
+
+ time = gdk_frame_clock_get_frame_time (frame_clock);
+ delta_ms = (time - self->drag_autoscroll_prev_time) / 1000.0;
+
+ start_threshold = value + autoscroll_area;
+ end_threshold = value + page_size - tab_width - autoscroll_area;
+ autoscroll_factor = 0;
+
+ if (x < start_threshold)
+ autoscroll_factor = -(start_threshold - x) / autoscroll_area;
+ else if (x > end_threshold)
+ autoscroll_factor = (x - end_threshold) / autoscroll_area;
+
+ autoscroll_factor = CLAMP (autoscroll_factor, -1, 1);
+ autoscroll_factor = adw_ease_in_cubic (autoscroll_factor);
+ self->drag_autoscroll_prev_time = time;
+
+ if (autoscroll_factor == 0)
+ return G_SOURCE_CONTINUE;
+
+ if (autoscroll_factor > 0)
+ offset = (int) ceil (autoscroll_factor * delta_ms * AUTOSCROLL_SPEED);
+ else
+ offset = (int) floor (autoscroll_factor * delta_ms * AUTOSCROLL_SPEED);
+
+ self->reorder_x += offset;
+ gtk_adjustment_set_value (self->adjustment, value + offset);
+ update_drag_reodering (self);
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+start_autoscroll (AdwTabBox *self)
+{
+ GdkFrameClock *frame_clock;
+
+ if (!self->adjustment)
+ return;
+
+ if (self->drag_autoscroll_cb_id)
+ return;
+
+ frame_clock = gtk_widget_get_frame_clock (GTK_WIDGET (self));
+
+ self->drag_autoscroll_prev_time = gdk_frame_clock_get_frame_time (frame_clock);
+ self->drag_autoscroll_cb_id =
+ gtk_widget_add_tick_callback (GTK_WIDGET (self),
+ (GtkTickCallback) drag_autoscroll_cb,
+ self, NULL);
+}
+
+static void
+end_autoscroll (AdwTabBox *self)
+{
+ if (self->drag_autoscroll_cb_id) {
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self),
+ self->drag_autoscroll_cb_id);
+ self->drag_autoscroll_cb_id = 0;
+ }
+}
+
+static void
+start_drag_reodering (AdwTabBox *self,
+ TabInfo *info,
+ double x,
+ double y)
+{
+ if (self->dragging)
+ return;
+
+ if (!info)
+ return;
+
+ self->continue_reorder = info == self->reordered_tab;
+
+ if (self->continue_reorder) {
+ if (self->reorder_animation)
+ adw_animation_stop (self->reorder_animation);
+
+ reset_reorder_animations (self);
+
+ self->reorder_x = (int) round (x - self->drag_offset_x);
+ self->reorder_y = (int) round (y - self->drag_offset_y);
+ } else
+ force_end_reordering (self);
+
+ start_autoscroll (self);
+ self->dragging = TRUE;
+
+ if (!self->continue_reorder)
+ start_reordering (self, info);
+}
+
+static void
+end_drag_reodering (AdwTabBox *self)
+{
+ TabInfo *dest_tab;
+
+ if (!self->dragging)
+ return;
+
+ self->dragging = FALSE;
+
+ end_autoscroll (self);
+
+ dest_tab = g_list_nth_data (self->tabs, self->reorder_index);
+
+ if (!self->indirect_reordering) {
+ int index = self->reorder_index;
+
+ if (!self->pinned)
+ index += adw_tab_view_get_n_pinned_pages (self->view);
+
+ /* We've already reordered the tab here, no need to do it again */
+ g_signal_handlers_block_by_func (self->view, page_reordered_cb, self);
+
+ adw_tab_view_reorder_page (self->view, self->reordered_tab->page, index);
+
+ g_signal_handlers_unblock_by_func (self->view, page_reordered_cb, self);
+ }
+
+ animate_reordering (self, dest_tab);
+
+ self->continue_reorder = FALSE;
+}
+
+static void
+reorder_begin_cb (AdwTabBox *self,
+ double start_x,
+ double start_y,
+ GtkGesture *gesture)
+{
+ self->reorder_start_pos = gtk_adjustment_get_value (self->adjustment);
+
+ start_x += self->reorder_start_pos;
+
+ self->pressed_tab = find_tab_info_at (self, start_x);
+
+ self->drag_offset_x = start_x - get_tab_position (self, self->pressed_tab);
+ self->drag_offset_y = start_y;
+
+ if (!self->reorder_animation) {
+ self->reorder_x = (int) round (start_x - self->drag_offset_x);
+ self->reorder_y = (int) round (start_y - self->drag_offset_y);
+ }
+}
+
+/* Copied from gtkdragsource.c */
+static gboolean
+gtk_drag_check_threshold_double (GtkWidget *widget,
+ double start_x,
+ double start_y,
+ double current_x,
+ double current_y)
+{
+ int drag_threshold;
+
+ g_object_get (gtk_widget_get_settings (widget),
+ "gtk-dnd-drag-threshold", &drag_threshold,
+ NULL);
+
+ return (ABS (current_x - start_x) > drag_threshold ||
+ ABS (current_y - start_y) > drag_threshold);
+}
+
+static gboolean
+check_dnd_threshold (AdwTabBox *self,
+ double x,
+ double y)
+{
+ int threshold;
+ graphene_rect_t rect;
+
+ g_object_get (gtk_widget_get_settings (GTK_WIDGET (self)),
+ "gtk-dnd-drag-threshold", &threshold,
+ NULL);
+
+ threshold *= DND_THRESHOLD_MULTIPLIER;
+
+ graphene_rect_init (&rect, 0, 0,
+ self->allocated_width,
+ gtk_widget_get_height (GTK_WIDGET (self)));
+ graphene_rect_inset (&rect, -threshold, -threshold);
+
+ return !graphene_rect_contains_point (&rect, &GRAPHENE_POINT_INIT (x, y));
+}
+
+static void begin_drag (AdwTabBox *self,
+ GdkDevice *device);
+
+static void
+reorder_update_cb (AdwTabBox *self,
+ double offset_x,
+ double offset_y,
+ GtkGesture *gesture)
+{
+ double start_x, start_y, x, y;
+ GdkDevice *device;
+
+ if (!self->pressed_tab) {
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ if (!self->dragging &&
+ !gtk_drag_check_threshold_double (GTK_WIDGET (self), 0, 0,
+ offset_x, offset_y))
+ return;
+
+ gtk_gesture_drag_get_start_point (GTK_GESTURE_DRAG (gesture),
+ &start_x, &start_y);
+
+ x = start_x + gtk_adjustment_get_value (self->adjustment) + offset_x;
+ y = start_y + offset_y;
+
+ start_drag_reodering (self, self->pressed_tab, x, y);
+
+ if (self->dragging) {
+ adw_tab_view_set_selected_page (self->view, self->pressed_tab->page);
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+ } else {
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ self->reorder_x = (int) round (x - self->drag_offset_x);
+ self->reorder_y = (int) round (y - self->drag_offset_y);
+
+ device = gtk_event_controller_get_current_event_device (GTK_EVENT_CONTROLLER (gesture));
+
+ if (!self->pinned &&
+ self->pressed_tab &&
+ self->pressed_tab != self->reorder_placeholder &&
+ self->pressed_tab->page &&
+ !is_touchscreen (gesture) &&
+ adw_tab_view_get_n_pages (self->view) > 1 &&
+ check_dnd_threshold (self, x, y)) {
+ begin_drag (self, device);
+
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ return;
+ }
+
+ update_drag_reodering (self);
+}
+
+static void
+reorder_end_cb (AdwTabBox *self,
+ double offset_x,
+ double offset_y,
+ GtkGesture *gesture)
+{
+ end_drag_reodering (self);
+}
+
+/* Selection */
+
+static void
+reset_focus (AdwTabBox *self)
+{
+ GtkRoot *root = gtk_widget_get_root (GTK_WIDGET (self));
+
+ gtk_widget_set_focus_child (GTK_WIDGET (self), NULL);
+
+ if (root)
+ gtk_root_set_focus (root, NULL);
+}
+
+static void
+select_page (AdwTabBox *self,
+ AdwTabPage *page)
+{
+ if (!page) {
+ self->selected_tab = NULL;
+
+ reset_focus (self);
+
+ return;
+ }
+
+ self->selected_tab = find_info_for_page (self, page);
+
+ if (!self->selected_tab) {
+ if (gtk_widget_get_focus_child (GTK_WIDGET (self)))
+ reset_focus (self);
+
+ return;
+ }
+
+ if (adw_tab_bar_tabs_have_visible_focus (self->tab_bar))
+ gtk_widget_grab_focus (GTK_WIDGET (self->selected_tab->tab));
+
+ gtk_widget_set_focus_child (GTK_WIDGET (self),
+ GTK_WIDGET (self->selected_tab->tab));
+
+ if (self->selected_tab->width >= 0)
+ scroll_to_tab (self, self->selected_tab, FOCUS_ANIMATION_DURATION);
+}
+
+/* Opening */
+
+static gboolean
+extra_drag_drop_cb (AdwTab *tab,
+ GValue *value,
+ AdwTabBox *self)
+{
+ gboolean ret = GDK_EVENT_PROPAGATE;
+ AdwTabPage *page = adw_tab_get_page (tab);
+
+ g_signal_emit (self, signals[SIGNAL_EXTRA_DRAG_DROP], 0, page, value, &ret);
+
+ return ret;
+}
+
+static void
+appear_animation_value_cb (double value,
+ gpointer user_data)
+{
+ TabInfo *info = user_data;
+
+ info->appear_progress = value;
+
+ if (GTK_IS_WIDGET (info->tab))
+ gtk_widget_queue_resize (GTK_WIDGET (info->tab));
+}
+
+static void
+open_animation_done_cb (gpointer user_data)
+{
+ TabInfo *info = user_data;
+
+ g_clear_pointer (&info->appear_animation, adw_animation_unref);
+}
+
+static TabInfo *
+create_tab_info (AdwTabBox *self,
+ AdwTabPage *page)
+{
+ TabInfo *info;
+
+ info = g_new0 (TabInfo, 1);
+ info->page = page;
+ info->pos = -1;
+ info->width = -1;
+ info->tab = adw_tab_new (self->view, self->pinned);
+
+ adw_tab_set_page (info->tab, page);
+ adw_tab_set_inverted (info->tab, self->inverted);
+ adw_tab_setup_extra_drop_target (info->tab,
+ self->extra_drag_actions,
+ self->extra_drag_types,
+ self->extra_drag_n_types);
+
+ gtk_widget_set_parent (GTK_WIDGET (info->tab), GTK_WIDGET (self));
+
+ g_signal_connect_object (info->tab, "extra-drag-drop", G_CALLBACK (extra_drag_drop_cb), self, 0);
+
+ return info;
+}
+
+static void
+page_attached_cb (AdwTabBox *self,
+ AdwTabPage *page,
+ int position)
+{
+ TabInfo *info;
+ GList *l;
+
+ if (adw_tab_page_get_pinned (page) != self->pinned)
+ return;
+
+ if (!self->pinned)
+ position -= adw_tab_view_get_n_pinned_pages (self->view);
+
+ set_tab_resize_mode (self, TAB_RESIZE_NORMAL);
+ force_end_reordering (self);
+
+ info = create_tab_info (self, page);
+
+ info->notify_needs_attention_id =
+ g_signal_connect_object (page,
+ "notify::needs-attention",
+ G_CALLBACK (update_visible),
+ self,
+ G_CONNECT_SWAPPED);
+
+ info->appear_animation =
+ adw_animation_new (GTK_WIDGET (self), 0, 1,
+ OPEN_ANIMATION_DURATION,
+ adw_ease_out_cubic,
+ appear_animation_value_cb,
+ open_animation_done_cb,
+ info);
+
+ l = find_nth_alive_tab (self, position);
+ self->tabs = g_list_insert_before (self->tabs, l, info);
+
+ self->n_tabs++;
+
+ adw_animation_start (info->appear_animation);
+
+ if (page == adw_tab_view_get_selected_page (self->view))
+ adw_tab_box_select_page (self, page);
+ else
+ scroll_to_tab_full (self, info, -1, FOCUS_ANIMATION_DURATION, TRUE);
+}
+
+/* Closing */
+
+static void
+close_animation_done_cb (gpointer user_data)
+{
+ TabInfo *info = user_data;
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (info->tab));
+ AdwTabBox *self = ADW_TAB_BOX (parent);
+
+ g_clear_pointer (&info->appear_animation, adw_animation_unref);
+
+ self->tabs = g_list_remove (self->tabs, info);
+
+ if (info->reorder_animation)
+ adw_animation_stop (info->reorder_animation);
+
+ if (self->reorder_animation)
+ adw_animation_stop (self->reorder_animation);
+
+ if (self->pressed_tab == info)
+ self->pressed_tab = NULL;
+
+ if (self->reordered_tab == info)
+ self->reordered_tab = NULL;
+
+ remove_and_free_tab_info (info);
+
+ self->n_tabs--;
+}
+
+static void
+page_detached_cb (AdwTabBox *self,
+ AdwTabPage *page)
+{
+ TabInfo *info;
+ GList *page_link;
+
+ page_link = find_link_for_page (self, page);
+
+ if (!page_link)
+ return;
+
+ info = page_link->data;
+ page_link = page_link->next;
+
+ force_end_reordering (self);
+
+ if (self->hovering && !self->pinned) {
+ gboolean is_last = TRUE;
+
+ while (page_link) {
+ TabInfo *i = page_link->data;
+ page_link = page_link->next;
+
+ if (i->page) {
+ is_last = FALSE;
+ break;
+ }
+ }
+
+ if (is_last)
+ set_tab_resize_mode (self, self->inverted ? TAB_RESIZE_NORMAL : TAB_RESIZE_FIXED_END_PADDING);
+ else
+ set_tab_resize_mode (self, TAB_RESIZE_FIXED_TAB_WIDTH);
+ }
+
+ g_assert (info->page);
+
+ if (gtk_widget_is_focus (GTK_WIDGET (info->tab)))
+ adw_tab_box_try_focus_selected_tab (self);
+
+ if (info == self->selected_tab)
+ adw_tab_box_select_page (self, NULL);
+
+ adw_tab_set_page (info->tab, NULL);
+
+ if (info->notify_needs_attention_id > 0) {
+ g_signal_handler_disconnect (info->page, info->notify_needs_attention_id);
+ info->notify_needs_attention_id = 0;
+ }
+
+ info->page = NULL;
+
+ if (info->appear_animation)
+ adw_animation_stop (info->appear_animation);
+
+ info->appear_animation =
+ adw_animation_new (GTK_WIDGET (self), info->appear_progress, 0,
+ CLOSE_ANIMATION_DURATION,
+ adw_ease_out_cubic,
+ appear_animation_value_cb,
+ close_animation_done_cb,
+ info);
+
+ adw_animation_start (info->appear_animation);
+}
+
+/* Tab DND */
+
+#define ADW_TYPE_TAB_BOX_ROOT_CONTENT (adw_tab_box_root_content_get_type ())
+
+G_DECLARE_FINAL_TYPE (AdwTabBoxRootContent, adw_tab_box_root_content, ADW, TAB_BOX_ROOT_CONTENT,
GdkContentProvider)
+
+struct _AdwTabBoxRootContent
+{
+ GdkContentProvider parent_instance;
+
+ AdwTabBox *tab_box;
+};
+
+G_DEFINE_TYPE (AdwTabBoxRootContent, adw_tab_box_root_content, GDK_TYPE_CONTENT_PROVIDER)
+
+static GdkContentFormats *
+adw_tab_box_root_content_ref_formats (GdkContentProvider *provider)
+{
+ return gdk_content_formats_new ((const char *[1]) { "application/x-rootwindow-drop" }, 1);
+}
+
+static void
+adw_tab_box_root_content_write_mime_type_async (GdkContentProvider *provider,
+ const char *mime_type,
+ GOutputStream *stream,
+ int io_priority,
+ GCancellable *cancellable,
+ GAsyncReadyCallback callback,
+ gpointer user_data)
+{
+ AdwTabBoxRootContent *self = ADW_TAB_BOX_ROOT_CONTENT (provider);
+ g_autoptr (GTask) task = NULL;
+
+ self->tab_box->should_detach_into_new_window = TRUE;
+
+ task = g_task_new (self, cancellable, callback, user_data);
+ g_task_set_priority (task, io_priority);
+ g_task_set_source_tag (task, adw_tab_box_root_content_write_mime_type_async);
+ g_task_return_boolean (task, TRUE);
+}
+
+static gboolean
+adw_tab_box_root_content_write_mime_type_finish (GdkContentProvider *provider,
+ GAsyncResult *result,
+ GError **error)
+{
+ return g_task_propagate_boolean (G_TASK (result), error);
+}
+
+static void
+adw_tab_box_root_content_finalize (GObject *object)
+{
+ AdwTabBoxRootContent *self = ADW_TAB_BOX_ROOT_CONTENT (object);
+
+ g_clear_object (&self->tab_box);
+
+ G_OBJECT_CLASS (adw_tab_box_root_content_parent_class)->finalize (object);
+}
+
+static void
+adw_tab_box_root_content_class_init (AdwTabBoxRootContentClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GdkContentProviderClass *provider_class = GDK_CONTENT_PROVIDER_CLASS (klass);
+
+ object_class->finalize = adw_tab_box_root_content_finalize;
+
+ provider_class->ref_formats = adw_tab_box_root_content_ref_formats;
+ provider_class->write_mime_type_async = adw_tab_box_root_content_write_mime_type_async;
+ provider_class->write_mime_type_finish = adw_tab_box_root_content_write_mime_type_finish;
+}
+
+static void
+adw_tab_box_root_content_init (AdwTabBoxRootContent *self)
+{
+}
+
+static GdkContentProvider *
+adw_tab_box_root_content_new (AdwTabBox *tab_box)
+{
+ AdwTabBoxRootContent *self = g_object_new (ADW_TYPE_TAB_BOX_ROOT_CONTENT, NULL);
+
+ self->tab_box = g_object_ref (tab_box);
+
+ return GDK_CONTENT_PROVIDER (self);
+}
+
+static int
+calculate_placeholder_index (AdwTabBox *self,
+ int x)
+{
+ int lower, upper, pos, i;
+ gboolean is_rtl;
+ GList *l;
+
+ get_visible_range (self, &lower, &upper);
+
+ x = CLAMP (x, lower, upper);
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+ pos = (is_rtl ? self->allocated_width + OVERLAP : -OVERLAP);
+ i = 0;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+ int tab_width = predict_tab_width (self, info, TRUE) * (is_rtl ? -1 : 1);
+
+ int end = pos + tab_width + calculate_tab_offset (self, info, FALSE);
+
+ if ((x <= end && !is_rtl) || (x >= end && is_rtl))
+ break;
+
+ pos += tab_width + (is_rtl ? OVERLAP : -OVERLAP);
+ i++;
+ }
+
+ return i;
+}
+
+static void
+insert_animation_value_cb (double value,
+ gpointer user_data)
+{
+ TabInfo *info = user_data;
+ AdwTabBox *self = ADW_TAB_BOX (gtk_widget_get_parent (GTK_WIDGET (info->tab)));
+
+ appear_animation_value_cb (value, info);
+
+ update_drag_reodering (self);
+}
+
+static void
+insert_placeholder (AdwTabBox *self,
+ AdwTabPage *page,
+ int pos)
+{
+ TabInfo *info = self->reorder_placeholder;
+ double initial_progress = 0;
+
+ if (info) {
+ initial_progress = info->appear_progress;
+
+ if (info->appear_animation)
+ adw_animation_stop (info->appear_animation);
+ } else {
+ int index;
+
+ self->placeholder_page = page;
+
+ info = create_tab_info (self, page);
+
+ gtk_widget_set_opacity (GTK_WIDGET (info->tab), 0);
+
+ adw_tab_set_dragging (info->tab, TRUE);
+
+ info->reorder_ignore_bounds = TRUE;
+
+ if (self->adjustment) {
+ double page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ if (self->allocated_width > page_size) {
+ gtk_widget_measure (GTK_WIDGET (info->tab), GTK_ORIENTATION_HORIZONTAL, -1,
+ NULL, &self->placeholder_scroll_offset, NULL, NULL);
+
+ self->placeholder_scroll_offset /= 2;
+ } else {
+ self->placeholder_scroll_offset = 0;
+ }
+ }
+
+ index = calculate_placeholder_index (self, pos + self->placeholder_scroll_offset);
+
+ self->tabs = g_list_insert (self->tabs, info, index);
+ self->n_tabs++;
+
+ self->reorder_placeholder = info;
+ self->reorder_index = g_list_index (self->tabs, info);
+
+ animate_scroll_relative (self, self->placeholder_scroll_offset, OPEN_ANIMATION_DURATION);
+ }
+
+ info->appear_animation =
+ adw_animation_new (GTK_WIDGET (self), initial_progress, 1,
+ OPEN_ANIMATION_DURATION,
+ adw_ease_out_cubic,
+ insert_animation_value_cb,
+ open_animation_done_cb,
+ info);
+
+ adw_animation_start (info->appear_animation);
+}
+
+static void
+replace_animation_done_cb (gpointer user_data)
+{
+ TabInfo *info = user_data;
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (info->tab));
+ AdwTabBox *self = ADW_TAB_BOX (parent);
+
+ g_clear_pointer (&info->appear_animation, adw_animation_unref);
+ self->reorder_placeholder = NULL;
+ self->can_remove_placeholder = TRUE;
+}
+
+static void
+replace_placeholder (AdwTabBox *self,
+ AdwTabPage *page)
+{
+ TabInfo *info = self->reorder_placeholder;
+ double initial_progress;
+
+ self->placeholder_scroll_offset = 0;
+ gtk_widget_set_opacity (GTK_WIDGET (self->reorder_placeholder->tab), 1);
+ adw_tab_set_dragging (info->tab, FALSE);
+
+ if (!info->appear_animation) {
+ self->reorder_placeholder = NULL;
+
+ return;
+ }
+
+ initial_progress = info->appear_progress;
+
+ self->can_remove_placeholder = FALSE;
+
+ adw_tab_set_page (info->tab, page);
+ info->page = page;
+
+ adw_animation_stop (info->appear_animation);
+
+ info->appear_animation =
+ adw_animation_new (GTK_WIDGET (self), initial_progress, 1,
+ OPEN_ANIMATION_DURATION,
+ adw_ease_out_cubic,
+ appear_animation_value_cb,
+ replace_animation_done_cb,
+ info);
+
+ adw_animation_start (info->appear_animation);
+}
+
+static void
+remove_animation_done_cb (gpointer user_data)
+{
+ TabInfo *info = user_data;
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (info->tab));
+ AdwTabBox *self = ADW_TAB_BOX (parent);
+
+ g_clear_pointer (&info->appear_animation, adw_animation_unref);
+
+ if (!self->can_remove_placeholder) {
+ adw_tab_set_page (info->tab, self->placeholder_page);
+ info->page = self->placeholder_page;
+
+ return;
+ }
+
+ if (self->reordered_tab == info) {
+ force_end_reordering (self);
+
+ if (self->reorder_animation)
+ adw_animation_stop (info->reorder_animation);
+
+ self->reordered_tab = NULL;
+ }
+
+ if (self->pressed_tab == info)
+ self->pressed_tab = NULL;
+
+ self->tabs = g_list_remove (self->tabs, info);
+
+ remove_and_free_tab_info (info);
+
+ self->n_tabs--;
+
+ self->reorder_placeholder = NULL;
+}
+
+static gboolean
+remove_placeholder_scroll_cb (AdwTabBox *self)
+{
+ animate_scroll_relative (self, -self->placeholder_scroll_offset, CLOSE_ANIMATION_DURATION);
+ self->placeholder_scroll_offset = 0;
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+remove_placeholder (AdwTabBox *self)
+{
+ TabInfo *info = self->reorder_placeholder;
+
+ if (!info || !info->page)
+ return;
+
+ adw_tab_set_page (info->tab, NULL);
+ info->page = NULL;
+
+ if (info->appear_animation)
+ adw_animation_stop (info->appear_animation);
+
+ g_idle_add ((GSourceFunc) remove_placeholder_scroll_cb, self);
+
+ info->appear_animation =
+ adw_animation_new (GTK_WIDGET (self), info->appear_progress, 0,
+ CLOSE_ANIMATION_DURATION,
+ adw_ease_out_cubic,
+ appear_animation_value_cb,
+ remove_animation_done_cb,
+ info);
+
+ adw_animation_start (info->appear_animation);
+}
+
+static inline AdwTabBox *
+get_source_tab_box (GtkDropTarget *target)
+{
+ GdkDrop *drop = gtk_drop_target_get_drop (target);
+ GdkDrag *drag = gdk_drop_get_drag (drop);
+
+ if (!drag)
+ return NULL;
+
+ return ADW_TAB_BOX (g_object_get_data (G_OBJECT (drag),
+ "adw-tab-bar-drag-origin"));
+}
+
+static void
+do_drag_drop (AdwTabBox *self,
+ AdwTabBox *source_tab_box)
+{
+ AdwTabPage *page = source_tab_box->detached_page;
+ int offset = (self->pinned ? 0 : adw_tab_view_get_n_pinned_pages (self->view));
+
+ if (self->reorder_placeholder) {
+ replace_placeholder (self, page);
+ end_drag_reodering (self);
+
+ g_signal_handlers_block_by_func (self->view, page_attached_cb, self);
+
+ adw_tab_view_attach_page (self->view, page, self->reorder_index + offset);
+
+ g_signal_handlers_unblock_by_func (self->view, page_attached_cb, self);
+ } else {
+ adw_tab_view_attach_page (self->view, page, self->reorder_index + offset);
+ }
+
+ source_tab_box->should_detach_into_new_window = FALSE;
+ source_tab_box->detached_page = NULL;
+
+ self->indirect_reordering = FALSE;
+}
+
+static void
+detach_into_new_window (AdwTabBox *self)
+{
+ AdwTabPage *page;
+ AdwTabView *new_view;
+
+ page = self->detached_page;
+
+ new_view = adw_tab_view_create_window (self->view);
+
+ if (ADW_IS_TAB_VIEW (new_view))
+ adw_tab_view_attach_page (new_view, page, 0);
+ else
+ adw_tab_view_attach_page (self->view, page, self->detached_index);
+
+ self->should_detach_into_new_window = FALSE;
+}
+
+static gboolean
+is_view_in_the_same_group (AdwTabBox *self,
+ AdwTabView *other_view)
+{
+ /* TODO when we have groups, this should do the actual check */
+ return TRUE;
+}
+
+static void
+drag_end (AdwTabBox *self,
+ GdkDrag *drag,
+ gboolean success)
+{
+ g_signal_handlers_disconnect_by_data (drag, self);
+
+ gdk_drag_drop_done (drag, success);
+
+ if (!success) {
+ adw_tab_view_attach_page (self->view,
+ self->detached_page,
+ self->detached_index);
+
+ self->indirect_reordering = FALSE;
+ }
+
+ self->detached_page = NULL;
+
+ if (self->drag_icon)
+ g_clear_pointer (&self->drag_icon, g_free);
+
+ g_object_unref (drag);
+}
+
+static void
+tab_drop_performed_cb (AdwTabBox *self,
+ GdkDrag *drag)
+{
+ /* Catch drops into our windows, but outside of tab views. If this is a false
+ * positive, it will be set to FALSE in do_drag_drop(). */
+ self->should_detach_into_new_window = TRUE;
+}
+
+static void
+tab_dnd_finished_cb (AdwTabBox *self,
+ GdkDrag *drag)
+{
+ if (self->should_detach_into_new_window)
+ detach_into_new_window (self);
+
+ drag_end (self, drag, TRUE);
+}
+
+static void
+tab_drag_cancel_cb (AdwTabBox *self,
+ GdkDragCancelReason reason,
+ GdkDrag *drag)
+{
+ if (reason == GDK_DRAG_CANCEL_NO_TARGET) {
+ detach_into_new_window (self);
+ drag_end (self, drag, TRUE);
+
+ return;
+ }
+
+ self->should_detach_into_new_window = FALSE;
+ drag_end (self, drag, FALSE);
+}
+
+static void
+create_drag_icon (AdwTabBox *self,
+ GdkDrag *drag)
+{
+ DragIcon *icon;
+
+ icon = g_new0 (DragIcon, 1);
+
+ icon->drag = drag;
+
+ icon->width = predict_tab_width (self, self->reordered_tab, FALSE);
+ icon->target_width = icon->width;
+
+ icon->tab = adw_tab_new (self->view, FALSE);
+ adw_tab_set_page (icon->tab, self->reordered_tab->page);
+ adw_tab_set_dragging (icon->tab, TRUE);
+ adw_tab_set_inverted (icon->tab, self->inverted);
+ adw_tab_set_display_width (icon->tab, icon->width);
+ gtk_widget_set_halign (GTK_WIDGET (icon->tab), GTK_ALIGN_START);
+
+ gtk_drag_icon_set_child (GTK_DRAG_ICON (gtk_drag_icon_get_for_drag (drag)),
+ GTK_WIDGET (icon->tab));
+
+ gtk_style_context_get_margin (gtk_widget_get_style_context (GTK_WIDGET (icon->tab)),
+ &icon->tab_margin);
+
+ gtk_widget_set_size_request (GTK_WIDGET (icon->tab),
+ icon->width + icon->tab_margin.left + icon->tab_margin.right,
+ -1);
+
+ icon->hotspot_x = (int) self->drag_offset_x;
+ icon->hotspot_y = (int) self->drag_offset_y;
+
+ gdk_drag_set_hotspot (drag,
+ icon->hotspot_x + icon->tab_margin.left,
+ icon->hotspot_y + icon->tab_margin.top);
+
+ self->drag_icon = icon;
+}
+
+static void
+icon_resize_animation_value_cb (double value,
+ gpointer user_data)
+{
+ DragIcon *icon = user_data;
+ double relative_pos;
+
+ relative_pos = (double) icon->hotspot_x / icon->width;
+
+ icon->width = (int) round (value);
+
+ adw_tab_set_display_width (icon->tab, icon->width);
+ gtk_widget_set_size_request (GTK_WIDGET (icon->tab),
+ icon->width + icon->tab_margin.left + icon->tab_margin.right,
+ -1);
+
+ icon->hotspot_x = (int) round (icon->width * relative_pos);
+
+ gdk_drag_set_hotspot (icon->drag,
+ icon->hotspot_x + icon->tab_margin.left,
+ icon->hotspot_y + icon->tab_margin.top);
+
+ gtk_widget_queue_resize (GTK_WIDGET (icon->tab));
+}
+
+static void
+icon_resize_animation_done_cb (gpointer user_data)
+{
+ DragIcon *icon = user_data;
+
+ g_clear_pointer (&icon->resize_animation, adw_animation_unref);
+}
+
+static void
+resize_drag_icon (AdwTabBox *self,
+ int width)
+{
+ DragIcon *icon = self->drag_icon;
+
+ if (width == icon->target_width)
+ return;
+
+ if (icon->resize_animation)
+ adw_animation_stop (icon->resize_animation);
+
+ icon->target_width = width;
+
+ icon->resize_animation =
+ adw_animation_new (GTK_WIDGET (icon->tab), icon->width, width,
+ ICON_RESIZE_ANIMATION_DURATION,
+ adw_ease_out_cubic,
+ icon_resize_animation_value_cb,
+ icon_resize_animation_done_cb,
+ icon);
+
+ adw_animation_start (icon->resize_animation);
+}
+
+static void
+begin_drag (AdwTabBox *self,
+ GdkDevice *device)
+{
+ GtkNative *native;
+ GdkSurface *surface;
+ GdkContentProvider *content;
+ GdkDrag *drag;
+ TabInfo *detached_info;
+ AdwTab *detached_tab;
+
+ native = gtk_widget_get_native (GTK_WIDGET (self));
+ surface = gtk_native_get_surface (native);
+
+ self->hovering = TRUE;
+ self->pressed_tab = NULL;
+
+ detached_info = self->reordered_tab;
+ detached_tab = g_object_ref (detached_info->tab);
+ self->detached_page = detached_info->page;
+
+ self->indirect_reordering = TRUE;
+
+ content = gdk_content_provider_new_union ((GdkContentProvider *[2]) {
+ adw_tab_box_root_content_new (self),
+ gdk_content_provider_new_typed (ADW_TYPE_TAB_PAGE,
detached_info->page)
+ }, 2);
+
+ drag = gdk_drag_begin (surface, device, content, GDK_ACTION_MOVE,
+ self->reorder_x, self->reorder_y);
+
+ g_object_set_data (G_OBJECT (drag), "adw-tab-bar-drag-origin", self);
+
+ g_signal_connect_swapped (drag, "drop-performed",
+ G_CALLBACK (tab_drop_performed_cb), self);
+ g_signal_connect_swapped (drag, "dnd-finished",
+ G_CALLBACK (tab_dnd_finished_cb), self);
+ g_signal_connect_swapped (drag, "cancel",
+ G_CALLBACK (tab_drag_cancel_cb), self);
+
+ create_drag_icon (self, drag);
+
+ end_drag_reodering (self);
+ update_hover (self);
+
+ gtk_widget_set_opacity (GTK_WIDGET (detached_tab), 0);
+ self->detached_index = adw_tab_view_get_page_position (self->view, self->detached_page);
+
+ adw_tab_view_detach_page (self->view, self->detached_page);
+
+ self->indirect_reordering = FALSE;
+
+ gtk_widget_measure (GTK_WIDGET (detached_tab),
+ GTK_ORIENTATION_HORIZONTAL, -1,
+ NULL, &self->placeholder_scroll_offset, NULL, NULL);
+ self->placeholder_scroll_offset /= 2;
+
+ animate_scroll_relative (self, -self->placeholder_scroll_offset, CLOSE_ANIMATION_DURATION);
+
+ g_object_unref (detached_tab);
+}
+
+static GdkDragAction
+tab_drag_enter_motion_cb (AdwTabBox *self,
+ double x,
+ double y,
+ GtkDropTarget *target)
+{
+ AdwTabBox *source_tab_box;
+
+ if (self->pinned)
+ return 0;
+
+ source_tab_box = get_source_tab_box (target);
+
+ if (!source_tab_box)
+ return 0;
+
+ if (!self->view || !is_view_in_the_same_group (self, source_tab_box->view))
+ return 0;
+
+ x += gtk_adjustment_get_value (self->adjustment);
+
+ self->can_remove_placeholder = FALSE;
+
+ if (!self->reorder_placeholder || !self->reorder_placeholder->page) {
+ AdwTabPage *page = source_tab_box->detached_page;
+ double center = x - source_tab_box->drag_icon->hotspot_x + source_tab_box->drag_icon->width / 2;
+
+ insert_placeholder (self, page, center);
+
+ self->indirect_reordering = TRUE;
+
+ resize_drag_icon (source_tab_box, predict_tab_width (self, self->reorder_placeholder, TRUE));
+ adw_tab_set_display_width (self->reorder_placeholder->tab, source_tab_box->drag_icon->target_width);
+ adw_tab_set_inverted (source_tab_box->drag_icon->tab, self->inverted);
+
+ self->drag_offset_x = source_tab_box->drag_icon->hotspot_x;
+ self->drag_offset_y = source_tab_box->drag_icon->hotspot_y;
+
+ self->reorder_x = (int) round (x - source_tab_box->drag_icon->hotspot_x);
+
+ start_drag_reodering (self, self->reorder_placeholder, x, y);
+
+ return GDK_ACTION_MOVE;
+ }
+
+ self->reorder_x = (int) round (x - source_tab_box->drag_icon->hotspot_x);
+
+ update_drag_reodering (self);
+
+ return GDK_ACTION_MOVE;
+}
+
+static void
+tab_drag_leave_cb (AdwTabBox *self,
+ GtkDropTarget *target)
+{
+ AdwTabBox *source_tab_box;
+
+ if (!self->indirect_reordering)
+ return;
+
+ if (self->pinned)
+ return;
+
+ source_tab_box = get_source_tab_box (target);
+
+ if (!source_tab_box)
+ return;
+
+ if (!self->view || !is_view_in_the_same_group (self, source_tab_box->view))
+ return;
+
+ self->can_remove_placeholder = TRUE;
+
+ end_drag_reodering (self);
+ remove_placeholder (self);
+
+ self->indirect_reordering = FALSE;
+}
+
+static gboolean
+tab_drag_drop_cb (AdwTabBox *self,
+ const GValue *value,
+ double x,
+ double y,
+ GtkDropTarget *target)
+{
+ AdwTabBox *source_tab_box;
+
+ if (self->pinned)
+ return GDK_EVENT_PROPAGATE;
+
+ source_tab_box = get_source_tab_box (target);
+
+ if (!source_tab_box)
+ return GDK_EVENT_PROPAGATE;
+
+ if (!self->view || !is_view_in_the_same_group (self, source_tab_box->view))
+ return GDK_EVENT_PROPAGATE;
+
+ do_drag_drop (self, source_tab_box);
+
+ return GDK_EVENT_STOP;
+}
+
+static gboolean
+view_drag_drop_cb (AdwTabBox *self,
+ const GValue *value,
+ double x,
+ double y,
+ GtkDropTarget *target)
+{
+ AdwTabBox *source_tab_box;
+
+ if (self->pinned)
+ return GDK_EVENT_PROPAGATE;
+
+ source_tab_box = get_source_tab_box (target);
+
+ if (!source_tab_box)
+ return GDK_EVENT_PROPAGATE;
+
+ if (!self->view || !is_view_in_the_same_group (self, source_tab_box->view))
+ return GDK_EVENT_PROPAGATE;
+
+ self->reorder_index = adw_tab_view_get_n_pages (self->view) -
+ adw_tab_view_get_n_pinned_pages (self->view);
+
+ do_drag_drop (self, source_tab_box);
+
+ return GDK_EVENT_STOP;
+}
+
+/* DND autoscrolling */
+
+static gboolean
+reset_drop_target_tab_cb (AdwTabBox *self)
+{
+ self->reset_drop_target_tab_id = 0;
+ set_drop_target_tab (self, NULL);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+drag_leave_cb (AdwTabBox *self,
+ GtkDropControllerMotion *controller)
+{
+ GdkDrop *drop = gtk_drop_controller_motion_get_drop (controller);
+ GdkDrag *drag = gdk_drop_get_drag (drop);
+ AdwTabBox *source = ADW_TAB_BOX (g_object_get_data (G_OBJECT (drag),
+ "adw-tab-bar-drag-origin"));
+
+ if (source)
+ return;
+
+ if (!self->reset_drop_target_tab_id)
+ self->reset_drop_target_tab_id =
+ g_idle_add ((GSourceFunc) reset_drop_target_tab_cb, self);
+
+ end_autoscroll (self);
+}
+
+static void
+drag_enter_motion_cb (AdwTabBox *self,
+ double x,
+ double y,
+ GtkDropControllerMotion *controller)
+{
+ TabInfo *info;
+ GdkDrop *drop = gtk_drop_controller_motion_get_drop (controller);
+ GdkDrag *drag = gdk_drop_get_drag (drop);
+ AdwTabBox *source = ADW_TAB_BOX (g_object_get_data (G_OBJECT (drag),
+ "adw-tab-bar-drag-origin"));
+
+ if (source)
+ return;
+
+ x += gtk_adjustment_get_value (self->adjustment);
+
+ info = find_tab_info_at (self, x);
+
+ if (!info) {
+ drag_leave_cb (self, controller);
+
+ return;
+ }
+
+ self->drop_target_x = x;
+ set_drop_target_tab (self, info);
+
+ start_autoscroll (self);
+}
+
+/* Context menu */
+
+static gboolean
+reset_setup_menu_cb (AdwTabBox *self)
+{
+ g_signal_emit_by_name (self->view, "setup-menu", NULL);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+touch_menu_notify_visible_cb (AdwTabBox *self)
+{
+ if (!self->context_menu || gtk_widget_get_visible (GTK_WIDGET (self->context_menu)))
+ return;
+
+ self->hovering = FALSE;
+ update_hover (self);
+
+ g_idle_add ((GSourceFunc) reset_setup_menu_cb, self);
+}
+
+static void
+do_popup (AdwTabBox *self,
+ TabInfo *info,
+ double x,
+ double y)
+{
+ GMenuModel *model = adw_tab_view_get_menu_model (self->view);
+ GdkRectangle rect;
+
+ if (!G_IS_MENU_MODEL (model))
+ return;
+
+ g_signal_emit_by_name (self->view, "setup-menu", info->page);
+
+ if (!self->context_menu) {
+ self->context_menu = GTK_POPOVER (gtk_popover_menu_new_from_model (model));
+ gtk_widget_set_parent (GTK_WIDGET (self->context_menu), GTK_WIDGET (self));
+ gtk_popover_set_position (self->context_menu, GTK_POS_BOTTOM);
+ gtk_popover_set_has_arrow (self->context_menu, FALSE);
+
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ gtk_widget_set_halign (GTK_WIDGET (self->context_menu), GTK_ALIGN_END);
+ else
+ gtk_widget_set_halign (GTK_WIDGET (self->context_menu), GTK_ALIGN_START);
+
+ g_signal_connect_object (self->context_menu, "notify::visible",
+ G_CALLBACK (touch_menu_notify_visible_cb), self,
+ G_CONNECT_AFTER | G_CONNECT_SWAPPED);
+ }
+
+ if (x >= 0 && y >= 0) {
+ rect.x = x;
+ rect.y = y;
+ } else {
+ rect.x = info->pos;
+ rect.y = gtk_widget_get_allocated_height (GTK_WIDGET (info->tab));
+
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ rect.x += info->width;
+ }
+
+ rect.x -= gtk_adjustment_get_value (self->adjustment);
+ rect.width = 0;
+ rect.height = 0;
+
+ gtk_popover_set_pointing_to (self->context_menu, &rect);
+
+ gtk_popover_popup (self->context_menu);
+}
+
+static void
+long_pressed_cb (AdwTabBox *self,
+ double x,
+ double y,
+ GtkGesture *gesture)
+{
+ TabInfo *info = find_tab_info_at (self, x);
+
+ gtk_gesture_set_state (self->drag_gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ if (!info || !info->page) {
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+ return;
+ }
+
+ x += gtk_adjustment_get_value (self->adjustment);
+
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+ do_popup (self, self->pressed_tab, x, y);
+}
+
+static void
+popup_menu_cb (GtkWidget *widget,
+ const char *action_name,
+ GVariant *parameter)
+{
+ AdwTabBox *self = ADW_TAB_BOX (widget);
+
+ if (self->selected_tab && self->selected_tab->page)
+ do_popup (self, self->selected_tab, -1, -1);
+}
+
+/* Clicking */
+
+static void
+handle_click (AdwTabBox *self,
+ TabInfo *info,
+ GtkGesture *gesture)
+{
+ gboolean can_grab_focus;
+
+ if (self->adjustment) {
+ int pos = get_tab_position (self, info);
+ double value = gtk_adjustment_get_value (self->adjustment);
+ double page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ if (pos + OVERLAP < value ||
+ pos + info->width - OVERLAP > value + page_size) {
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+
+ scroll_to_tab (self, info, SCROLL_ANIMATION_DURATION);
+
+ return;
+ }
+ }
+
+ can_grab_focus = adw_tab_bar_tabs_have_visible_focus (self->tab_bar);
+
+ if (info == self->selected_tab)
+ can_grab_focus = TRUE;
+ else
+ adw_tab_view_set_selected_page (self->view, info->page);
+
+ if (can_grab_focus)
+ gtk_widget_grab_focus (GTK_WIDGET (info->tab));
+ else
+ activate_tab (self);
+}
+
+static void
+pressed_cb (AdwTabBox *self,
+ int n_press,
+ double x,
+ double y,
+ GtkGesture *gesture)
+{
+ TabInfo *info;
+ GdkEvent *event;
+ GdkEventSequence *current;
+ guint button;
+
+ if (is_touchscreen (gesture))
+ return;
+
+ x += gtk_adjustment_get_value (self->adjustment);
+
+ info = find_tab_info_at (self, x);
+
+ if (!info || !info->page) {
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ return;
+ }
+
+ current = gtk_gesture_single_get_current_sequence (GTK_GESTURE_SINGLE (gesture));
+ event = gtk_gesture_get_last_event (gesture, current);
+
+ if (gdk_event_triggers_context_menu (event)) {
+ do_popup (self, info, x, y);
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+ gtk_event_controller_reset (GTK_EVENT_CONTROLLER (gesture));
+
+ return;
+ }
+
+ button = gtk_gesture_single_get_current_button (GTK_GESTURE_SINGLE (gesture));
+
+ if (button == GDK_BUTTON_MIDDLE) {
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+ adw_tab_view_close_page (self->view, info->page);
+
+ return;
+ }
+
+ if (button != GDK_BUTTON_PRIMARY) {
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ return;
+ }
+
+ handle_click (self, info, gesture);
+}
+
+static void
+released_cb (AdwTabBox *self,
+ int n_press,
+ double x,
+ double y,
+ GtkGesture *gesture)
+{
+ TabInfo *info;
+
+ if (!is_touchscreen (gesture))
+ return;
+
+ x += gtk_adjustment_get_value (self->adjustment);
+
+ info = find_tab_info_at (self, x);
+
+ if (!info || !info->page) {
+ gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+ return;
+ }
+
+ handle_click (self, info, gesture);
+}
+
+/* Overrides */
+
+static void
+adw_tab_box_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ int for_size,
+ int *minimum,
+ int *natural,
+ int *minimum_baseline,
+ int *natural_baseline)
+{
+ AdwTabBox *self = ADW_TAB_BOX (widget);
+ int min, nat;
+
+ if (self->n_tabs == 0) {
+ if (minimum)
+ *minimum = 0;
+
+ if (natural)
+ *natural = 0;
+
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+
+ if (natural_baseline)
+ *natural_baseline = -1;
+
+ return;
+ }
+
+ if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ int width = self->end_padding;
+ GList *l;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+ int child_width;
+
+ gtk_widget_measure (GTK_WIDGET (info->tab), orientation, -1,
+ NULL, &child_width, NULL, NULL);
+
+ width += calculate_tab_width (info, child_width) - OVERLAP;
+ }
+
+ if (!self->pinned)
+ width -= OVERLAP;
+
+ min = nat = MAX (self->last_width, width);
+ } else {
+ GList *l;
+
+ min = nat = 0;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+ int child_min, child_nat;
+
+ gtk_widget_measure (GTK_WIDGET (info->tab), orientation, -1,
+ &child_min, &child_nat, NULL, NULL);
+
+ if (child_min > min)
+ min = child_min;
+
+ if (child_nat > nat)
+ nat = child_nat;
+ }
+ }
+
+ if (minimum)
+ *minimum = min;
+
+ if (natural)
+ *natural = nat;
+
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+
+ if (natural_baseline)
+ *natural_baseline = -1;
+}
+
+static void
+adw_tab_box_size_allocate (GtkWidget *widget,
+ int width,
+ int height,
+ int baseline)
+{
+ AdwTabBox *self = ADW_TAB_BOX (widget);
+ gboolean is_rtl;
+ GList *l;
+ GtkAllocation child_allocation;
+ int pos;
+ double value;
+
+ adw_tab_box_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ &self->allocated_width, NULL, NULL, NULL);
+ self->allocated_width = MAX (self->allocated_width, width);
+
+ value = gtk_adjustment_get_value (self->adjustment);
+
+ gtk_adjustment_configure (self->adjustment,
+ value,
+ 0,
+ self->allocated_width,
+ width * 0.1,
+ width * 0.9,
+ width);
+
+ if (self->context_menu)
+ gtk_popover_present (self->context_menu);
+
+ if (!self->n_tabs)
+ return;
+
+ is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
+
+ if (self->pinned) {
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+ int child_width;
+
+ gtk_widget_measure (GTK_WIDGET (info->tab), GTK_ORIENTATION_HORIZONTAL, -1,
+ NULL, &child_width, NULL, NULL);
+
+ info->width = calculate_tab_width (info, child_width);
+ }
+ } else if (self->tab_resize_mode == TAB_RESIZE_FIXED_TAB_WIDTH) {
+ self->end_padding = self->allocated_width + OVERLAP;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ info->width = calculate_tab_width (info, info->last_width);
+ self->end_padding -= info->width - OVERLAP;
+ }
+ } else {
+ int tab_width = get_base_tab_width (self, FALSE);
+ int excess = self->allocated_width + OVERLAP - self->end_padding;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ info->width = calculate_tab_width (info, tab_width);
+ excess -= info->width - OVERLAP;
+ }
+
+ /* Now spread excess width across the tabs */
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ if (excess >= 0)
+ break;
+
+ info->width--;
+ excess++;
+ }
+ }
+
+ pos = is_rtl ? self->allocated_width + OVERLAP : -OVERLAP;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ if (!info->appear_animation)
+ adw_tab_set_display_width (info->tab, info->width);
+ else if (info->page && info != self->reorder_placeholder)
+ adw_tab_set_display_width (info->tab, predict_tab_width (self, info, FALSE));
+
+ info->pos = pos + calculate_tab_offset (self, info, FALSE);
+
+ if (is_rtl)
+ info->pos -= info->width;
+
+ child_allocation.x = ((info == self->reordered_tab) ? self->reorder_window_x : info->pos) - value;
+ child_allocation.y = 0;
+ child_allocation.width = info->width;
+ child_allocation.height = height;
+
+ gtk_widget_size_allocate (GTK_WIDGET (info->tab), &child_allocation, baseline);
+
+ pos += (is_rtl ? -1 : 1) * (info->width - OVERLAP);
+ }
+
+ if (self->scheduled_scroll.info) {
+ scroll_to_tab_full (self,
+ self->scheduled_scroll.info,
+ self->scheduled_scroll.pos,
+ self->scheduled_scroll.duration,
+ self->scheduled_scroll.keep_selected_visible);
+ self->scheduled_scroll.info = NULL;
+ }
+
+ if (self->scroll_animation) {
+ self->block_scrolling = TRUE;
+ gtk_adjustment_set_value (self->adjustment,
+ get_scroll_animation_value (self));
+ self->block_scrolling = FALSE;
+
+ if (self->scroll_animation_done) {
+ self->scroll_animation_done = FALSE;
+ self->scroll_animation_tab = NULL;
+ g_clear_pointer (&self->scroll_animation, adw_animation_unref);
+ }
+ }
+
+ update_visible (self);
+}
+
+static gboolean
+adw_tab_box_focus (GtkWidget *widget,
+ GtkDirectionType direction)
+{
+ AdwTabBox *self = ADW_TAB_BOX (widget);
+
+ if (!self->selected_tab)
+ return GDK_EVENT_PROPAGATE;
+
+ return gtk_widget_child_focus (GTK_WIDGET (self->selected_tab->tab), direction);
+}
+
+static void
+adw_tab_box_unrealize (GtkWidget *widget)
+{
+ AdwTabBox *self = ADW_TAB_BOX (widget);
+
+ g_clear_pointer ((GtkWidget **) &self->context_menu, gtk_widget_unparent);
+
+ GTK_WIDGET_CLASS (adw_tab_box_parent_class)->unrealize (widget);
+}
+
+static void
+adw_tab_box_unmap (GtkWidget *widget)
+{
+ AdwTabBox *self = ADW_TAB_BOX (widget);
+
+ force_end_reordering (self);
+
+ if (self->drag_autoscroll_cb_id) {
+ gtk_widget_remove_tick_callback (widget, self->drag_autoscroll_cb_id);
+ self->drag_autoscroll_cb_id = 0;
+ }
+
+ self->hovering = FALSE;
+ update_hover (self);
+
+ GTK_WIDGET_CLASS (adw_tab_box_parent_class)->unmap (widget);
+}
+
+static void
+adw_tab_box_direction_changed (GtkWidget *widget,
+ GtkTextDirection previous_direction)
+{
+ AdwTabBox *self = ADW_TAB_BOX (widget);
+ double upper, page_size;
+
+ if (!self->adjustment)
+ return;
+
+ if (gtk_widget_get_direction (widget) == previous_direction)
+ return;
+
+ upper = gtk_adjustment_get_upper (self->adjustment);
+ page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ gtk_adjustment_set_value (self->adjustment,
+ upper - page_size - self->adjustment_prev_value);
+
+ if (self->context_menu) {
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ gtk_widget_set_halign (GTK_WIDGET (self->context_menu), GTK_ALIGN_END);
+ else
+ gtk_widget_set_halign (GTK_WIDGET (self->context_menu), GTK_ALIGN_START);
+ }
+}
+
+static void
+adw_tab_box_dispose (GObject *object)
+{
+ AdwTabBox *self = ADW_TAB_BOX (object);
+
+ g_clear_handle_id (&self->drop_switch_timeout_id, g_source_remove);
+
+ self->drag_gesture = NULL;
+ self->tab_bar = NULL;
+ adw_tab_box_set_view (self, NULL);
+ adw_tab_box_set_adjustment (self, NULL);
+
+ G_OBJECT_CLASS (adw_tab_box_parent_class)->dispose (object);
+}
+
+static void
+adw_tab_box_finalize (GObject *object)
+{
+ AdwTabBox *self = (AdwTabBox *) object;
+
+ g_clear_pointer (&self->extra_drag_types, g_free);
+
+ G_OBJECT_CLASS (adw_tab_box_parent_class)->finalize (object);
+}
+
+static void
+adw_tab_box_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ AdwTabBox *self = ADW_TAB_BOX (object);
+
+ switch (prop_id) {
+ case PROP_PINNED:
+ g_value_set_boolean (value, self->pinned);
+ break;
+
+ case PROP_TAB_BAR:
+ g_value_set_object (value, self->tab_bar);
+ break;
+
+ case PROP_VIEW:
+ g_value_set_object (value, self->view);
+ break;
+
+ case PROP_NEEDS_ATTENTION_LEFT:
+ g_value_set_boolean (value, self->needs_attention_left);
+ break;
+
+ case PROP_NEEDS_ATTENTION_RIGHT:
+ g_value_set_boolean (value, self->needs_attention_right);
+ break;
+
+ case PROP_RESIZE_FROZEN:
+ g_value_set_boolean (value, self->tab_resize_mode != TAB_RESIZE_NORMAL);
+ break;
+
+ case PROP_HADJUSTMENT:
+ g_value_set_object (value, self->adjustment);
+ break;
+
+ case PROP_VADJUSTMENT:
+ case PROP_HSCROLL_POLICY:
+ case PROP_VSCROLL_POLICY:
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+adw_tab_box_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ AdwTabBox *self = ADW_TAB_BOX (object);
+
+ switch (prop_id) {
+ case PROP_PINNED:
+ self->pinned = g_value_get_boolean (value);
+ break;
+
+ case PROP_TAB_BAR:
+ self->tab_bar = g_value_get_object (value);
+ break;
+
+ case PROP_VIEW:
+ adw_tab_box_set_view (self, g_value_get_object (value));
+ break;
+
+ case PROP_HADJUSTMENT:
+ adw_tab_box_set_adjustment (self, g_value_get_object (value));
+ break;
+
+ case PROP_VADJUSTMENT:
+ case PROP_HSCROLL_POLICY:
+ case PROP_VSCROLL_POLICY:
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+adw_tab_box_class_init (AdwTabBoxClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+
+ object_class->dispose = adw_tab_box_dispose;
+ object_class->finalize = adw_tab_box_finalize;
+ object_class->get_property = adw_tab_box_get_property;
+ object_class->set_property = adw_tab_box_set_property;
+
+ widget_class->measure = adw_tab_box_measure;
+ widget_class->size_allocate = adw_tab_box_size_allocate;
+ widget_class->focus = adw_tab_box_focus;
+ widget_class->unrealize = adw_tab_box_unrealize;
+ widget_class->unmap = adw_tab_box_unmap;
+ widget_class->direction_changed = adw_tab_box_direction_changed;
+
+ props[PROP_PINNED] =
+ g_param_spec_boolean ("pinned",
+ "Pinned",
+ "Pinned",
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ props[PROP_TAB_BAR] =
+ g_param_spec_object ("tab-bar",
+ "Tab Bar",
+ "Tab Bar",
+ ADW_TYPE_TAB_BAR,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ props[PROP_VIEW] =
+ g_param_spec_object ("view",
+ "View",
+ "View",
+ ADW_TYPE_TAB_VIEW,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_NEEDS_ATTENTION_LEFT] =
+ g_param_spec_boolean ("needs-attention-left",
+ "Needs Attention Left",
+ "Needs Attention Left",
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_NEEDS_ATTENTION_RIGHT] =
+ g_param_spec_boolean ("needs-attention-right",
+ "Needs Attention Right",
+ "Needs Attention Right",
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_RESIZE_FROZEN] =
+ g_param_spec_boolean ("resize-frozen",
+ "Resize Frozen",
+ "Resize Frozen",
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ g_object_class_override_property (object_class, PROP_HADJUSTMENT, "hadjustment");
+ g_object_class_override_property (object_class, PROP_VADJUSTMENT, "vadjustment");
+ g_object_class_override_property (object_class, PROP_HSCROLL_POLICY, "hscroll-policy");
+ g_object_class_override_property (object_class, PROP_VSCROLL_POLICY, "vscroll-policy");
+
+ signals[SIGNAL_STOP_KINETIC_SCROLLING] =
+ g_signal_new ("stop-kinetic-scrolling",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+
+ signals[SIGNAL_EXTRA_DRAG_DROP] =
+ g_signal_new ("extra-drag-drop",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ g_signal_accumulator_first_wins, NULL, NULL,
+ G_TYPE_BOOLEAN,
+ 2,
+ ADW_TYPE_TAB_PAGE,
+ G_TYPE_VALUE);
+
+ gtk_widget_class_install_action (widget_class, "menu.popup", NULL, popup_menu_cb);
+
+ gtk_widget_class_add_binding_action (widget_class, GDK_KEY_F10, GDK_SHIFT_MASK, "menu.popup", NULL);
+ gtk_widget_class_add_binding_action (widget_class, GDK_KEY_Menu, 0, "menu.popup", NULL);
+
+ add_focus_bindings (widget_class, GDK_KEY_Page_Up, GTK_DIR_TAB_BACKWARD, FALSE);
+ add_focus_bindings (widget_class, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD, FALSE);
+ add_focus_bindings (widget_class, GDK_KEY_Home, GTK_DIR_TAB_BACKWARD, TRUE);
+ add_focus_bindings (widget_class, GDK_KEY_End, GTK_DIR_TAB_FORWARD, TRUE);
+
+ add_reorder_bindings (widget_class, GDK_KEY_Left, GTK_DIR_LEFT, FALSE);
+ add_reorder_bindings (widget_class, GDK_KEY_Right, GTK_DIR_RIGHT, FALSE);
+ add_reorder_bindings (widget_class, GDK_KEY_Page_Up, GTK_DIR_TAB_BACKWARD, FALSE);
+ add_reorder_bindings (widget_class, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD, FALSE);
+ add_reorder_bindings (widget_class, GDK_KEY_Home, GTK_DIR_TAB_BACKWARD, TRUE);
+ add_reorder_bindings (widget_class, GDK_KEY_End, GTK_DIR_TAB_FORWARD, TRUE);
+
+ gtk_widget_class_set_css_name (widget_class, "tabbox");
+}
+
+static void
+adw_tab_box_init (AdwTabBox *self)
+{
+ GtkEventController *controller;
+
+ self->can_remove_placeholder = TRUE;
+ self->expand_tabs = TRUE;
+
+ gtk_widget_set_overflow (GTK_WIDGET (self), GTK_OVERFLOW_HIDDEN);
+
+ controller = gtk_event_controller_motion_new ();
+ g_signal_connect_swapped (controller, "motion", G_CALLBACK (motion_cb), self);
+ g_signal_connect_swapped (controller, "leave", G_CALLBACK (leave_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+ controller = gtk_event_controller_scroll_new (GTK_EVENT_CONTROLLER_SCROLL_VERTICAL);
+ g_signal_connect_swapped (controller, "scroll", G_CALLBACK (scroll_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+ controller = GTK_EVENT_CONTROLLER (gtk_gesture_click_new ());
+ gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (controller), 0);
+ gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (controller), TRUE);
+ g_signal_connect_swapped (controller, "pressed", G_CALLBACK (pressed_cb), self);
+ g_signal_connect_swapped (controller, "released", G_CALLBACK (released_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+ controller = GTK_EVENT_CONTROLLER (gtk_gesture_long_press_new ());
+ gtk_gesture_long_press_set_delay_factor (GTK_GESTURE_LONG_PRESS (controller), 2);
+ gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (controller), TRUE);
+ gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (controller), TRUE);
+ g_signal_connect_swapped (controller, "pressed", G_CALLBACK (long_pressed_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+ controller = GTK_EVENT_CONTROLLER (gtk_gesture_drag_new ());
+ gtk_gesture_single_set_button (GTK_GESTURE_SINGLE (controller), GDK_BUTTON_PRIMARY);
+ gtk_gesture_single_set_exclusive (GTK_GESTURE_SINGLE (controller), TRUE);
+ g_signal_connect_swapped (controller, "drag-begin", G_CALLBACK (reorder_begin_cb), self);
+ g_signal_connect_swapped (controller, "drag-update", G_CALLBACK (reorder_update_cb), self);
+ g_signal_connect_swapped (controller, "drag-end", G_CALLBACK (reorder_end_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), controller);
+ self->drag_gesture = GTK_GESTURE (controller);
+
+ controller = gtk_drop_controller_motion_new ();
+ g_signal_connect_swapped (controller, "enter", G_CALLBACK (drag_enter_motion_cb), self);
+ g_signal_connect_swapped (controller, "motion", G_CALLBACK (drag_enter_motion_cb), self);
+ g_signal_connect_swapped (controller, "leave", G_CALLBACK (drag_leave_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), controller);
+
+ controller = GTK_EVENT_CONTROLLER (gtk_drop_target_new (ADW_TYPE_TAB_PAGE, GDK_ACTION_MOVE));
+ gtk_drop_target_set_preload (GTK_DROP_TARGET (controller), TRUE);
+ g_signal_connect_swapped (controller, "enter", G_CALLBACK (tab_drag_enter_motion_cb), self);
+ g_signal_connect_swapped (controller, "motion", G_CALLBACK (tab_drag_enter_motion_cb), self);
+ g_signal_connect_swapped (controller, "leave", G_CALLBACK (tab_drag_leave_cb), self);
+ g_signal_connect_swapped (controller, "drop", G_CALLBACK (tab_drag_drop_cb), self);
+ gtk_widget_add_controller (GTK_WIDGET (self), controller);
+}
+
+void
+adw_tab_box_set_view (AdwTabBox *self,
+ AdwTabView *view)
+{
+ g_return_if_fail (ADW_IS_TAB_BOX (self));
+ g_return_if_fail (ADW_IS_TAB_VIEW (view) || view == NULL);
+
+ if (view == self->view)
+ return;
+
+ if (self->view) {
+ force_end_reordering (self);
+ g_signal_handlers_disconnect_by_func (self->view, page_attached_cb, self);
+ g_signal_handlers_disconnect_by_func (self->view, page_detached_cb, self);
+ g_signal_handlers_disconnect_by_func (self->view, page_reordered_cb, self);
+
+ if (!self->pinned) {
+ gtk_widget_remove_controller (GTK_WIDGET (self->view), self->view_drop_target);
+ self->view_drop_target = NULL;
+ }
+
+ g_list_free_full (self->tabs, (GDestroyNotify) remove_and_free_tab_info);
+
+ self->tabs = NULL;
+ self->n_tabs = 0;
+ }
+
+ self->view = view;
+
+ if (self->view) {
+ int i, n_pages = adw_tab_view_get_n_pages (self->view);
+
+ for (i = n_pages - 1; i >= 0; i--)
+ page_attached_cb (self, adw_tab_view_get_nth_page (self->view, i), 0);
+
+ g_signal_connect_object (self->view, "page-attached", G_CALLBACK (page_attached_cb), self,
G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->view, "page-detached", G_CALLBACK (page_detached_cb), self,
G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->view, "page-reordered", G_CALLBACK (page_reordered_cb), self,
G_CONNECT_SWAPPED);
+
+ if (!self->pinned) {
+ self->view_drop_target = GTK_EVENT_CONTROLLER (gtk_drop_target_new (ADW_TYPE_TAB_PAGE,
GDK_ACTION_MOVE));
+
+ g_signal_connect_object (self->view_drop_target, "drop", G_CALLBACK (view_drag_drop_cb), self,
G_CONNECT_SWAPPED);
+
+ gtk_widget_add_controller (GTK_WIDGET (self->view), self->view_drop_target);
+ }
+ }
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW]);
+}
+
+void
+adw_tab_box_set_adjustment (AdwTabBox *self,
+ GtkAdjustment *adjustment)
+{
+ g_return_if_fail (ADW_IS_TAB_BOX (self));
+ g_return_if_fail (GTK_IS_ADJUSTMENT (adjustment) || adjustment == NULL);
+
+ if (adjustment == self->adjustment)
+ return;
+
+ if (self->adjustment) {
+ g_signal_handlers_disconnect_by_func (self->adjustment, adjustment_value_changed_cb, self);
+ g_signal_handlers_disconnect_by_func (self->adjustment, update_visible, self);
+ }
+
+ g_set_object (&self->adjustment, adjustment);
+
+ if (self->adjustment) {
+ g_signal_connect_object (self->adjustment, "value-changed", G_CALLBACK (adjustment_value_changed_cb),
self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->adjustment, "notify::page-size", G_CALLBACK (update_visible), self,
G_CONNECT_SWAPPED);
+ }
+
+ g_object_notify (G_OBJECT (self), "hadjustment");
+}
+
+void
+adw_tab_box_attach_page (AdwTabBox *self,
+ AdwTabPage *page,
+ int position)
+{
+ g_return_if_fail (ADW_IS_TAB_BOX (self));
+ g_return_if_fail (ADW_IS_TAB_PAGE (page));
+
+ page_attached_cb (self, page, position);
+}
+
+void
+adw_tab_box_detach_page (AdwTabBox *self,
+ AdwTabPage *page)
+{
+ g_return_if_fail (ADW_IS_TAB_BOX (self));
+ g_return_if_fail (ADW_IS_TAB_PAGE (page));
+
+ page_detached_cb (self, page);
+}
+
+void
+adw_tab_box_select_page (AdwTabBox *self,
+ AdwTabPage *page)
+{
+ g_return_if_fail (ADW_IS_TAB_BOX (self));
+ g_return_if_fail (ADW_IS_TAB_PAGE (page) || page == NULL);
+
+ select_page (self, page);
+}
+
+void
+adw_tab_box_try_focus_selected_tab (AdwTabBox *self)
+{
+ g_return_if_fail (ADW_IS_TAB_BOX (self));
+
+ if (self->selected_tab)
+ gtk_widget_grab_focus (GTK_WIDGET (self->selected_tab->tab));
+}
+
+gboolean
+adw_tab_box_is_page_focused (AdwTabBox *self,
+ AdwTabPage *page)
+{
+ TabInfo *info;
+
+ g_return_val_if_fail (ADW_IS_TAB_BOX (self), FALSE);
+ g_return_val_if_fail (ADW_IS_TAB_PAGE (page), FALSE);
+
+ info = find_info_for_page (self, page);
+
+ return info && gtk_widget_is_focus (GTK_WIDGET (info->tab));
+}
+
+void
+adw_tab_box_setup_extra_drop_target (AdwTabBox *self,
+ GdkDragAction actions,
+ GType *types,
+ gsize n_types)
+{
+ GList *l;
+
+ g_return_if_fail (ADW_IS_TAB_BOX (self));
+ g_return_if_fail (n_types == 0 || types != NULL);
+
+ g_clear_pointer (&self->extra_drag_types, g_free);
+
+ self->extra_drag_actions = actions;
+#if GLIB_CHECK_VERSION(2, 67, 3)
+ self->extra_drag_types = g_memdup2 (types, sizeof (GType) * n_types);
+#else
+ self->extra_drag_types = g_memdup (types, sizeof (GType) * n_types);
+#endif
+ self->extra_drag_n_types = n_types;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ adw_tab_setup_extra_drop_target (info->tab,
+ self->extra_drag_actions,
+ self->extra_drag_types,
+ self->extra_drag_n_types);
+ }
+}
+
+gboolean
+adw_tab_box_get_expand_tabs (AdwTabBox *self)
+{
+ g_return_val_if_fail (ADW_IS_TAB_BOX (self), FALSE);
+
+ return self->expand_tabs;
+}
+
+void
+adw_tab_box_set_expand_tabs (AdwTabBox *self,
+ gboolean expand_tabs)
+{
+ g_return_if_fail (ADW_IS_TAB_BOX (self));
+
+ expand_tabs = !!expand_tabs;
+
+ if (expand_tabs == self->expand_tabs)
+ return;
+
+ self->expand_tabs = expand_tabs;
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+gboolean
+adw_tab_box_get_inverted (AdwTabBox *self)
+{
+ g_return_val_if_fail (ADW_IS_TAB_BOX (self), FALSE);
+
+ return self->inverted;
+}
+
+void
+adw_tab_box_set_inverted (AdwTabBox *self,
+ gboolean inverted)
+{
+ GList *l;
+
+ g_return_if_fail (ADW_IS_TAB_BOX (self));
+
+ inverted = !!inverted;
+
+ if (inverted == self->inverted)
+ return;
+
+ self->inverted = inverted;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ adw_tab_set_inverted (info->tab, inverted);
+ }
+}
diff --git a/src/meson.build b/src/meson.build
index ea240df..7b072cc 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -149,6 +149,7 @@ src_sources = [
'adw-swipe-tracker.c',
'adw-swipeable.c',
'adw-tab.c',
+ 'adw-tab-box.c',
'adw-tab-view.c',
'adw-value-object.c',
'adw-view-switcher.c',
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]