[libhandy/tabs: 1/62] t
- From: Alexander Mikhaylenko <alexm src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libhandy/tabs: 1/62] t
- Date: Sat, 12 Sep 2020 19:27:42 +0000 (UTC)
commit 2167c9cf9eb7bdafa4ecaea0fd058d4044487683
Author: Alexander Mikhaylenko <alexm gnome org>
Date: Wed Sep 2 20:52:25 2020 +0500
t
doc/meson.build | 4 +
src/handy.gresources.xml | 3 +
src/handy.h | 2 +
src/hdy-animation-private.h | 21 +
src/hdy-animation.c | 156 ++
src/hdy-css-private.h | 9 +
src/hdy-css.c | 78 +-
src/hdy-enums.c.in | 1 +
src/hdy-header-bar.c | 1 +
src/hdy-tab-bar-private.h | 19 +
src/hdy-tab-bar.c | 622 +++++
src/hdy-tab-bar.h | 58 +
src/hdy-tab-bar.ui | 75 +
src/hdy-tab-box-private.h | 41 +
src/hdy-tab-box.c | 3318 +++++++++++++++++++++++++++
src/hdy-tab-private.h | 45 +
src/hdy-tab-view-private.h | 26 +
src/hdy-tab-view.c | 1791 +++++++++++++++
src/hdy-tab-view.h | 202 ++
src/hdy-tab.c | 774 +++++++
src/hdy-tab.ui | 135 ++
src/icons/hdy-tab-icon-missing-symbolic.svg | 32 +
src/meson.build | 8 +
src/themes/Adwaita-dark.css | 72 +-
src/themes/Adwaita.css | 72 +-
src/themes/HighContrast.css | 72 +-
src/themes/HighContrastInverse.css | 72 +-
src/themes/_Adwaita-base.scss | 231 ++
28 files changed, 7922 insertions(+), 18 deletions(-)
---
diff --git a/doc/meson.build b/doc/meson.build
index eeab57a4..e22d073e 100644
--- a/doc/meson.build
+++ b/doc/meson.build
@@ -19,6 +19,10 @@ private_headers = [
'hdy-shadow-helper-private.h',
'hdy-stackable-box-private.h',
'hdy-swipe-tracker-private.h',
+ 'hdy-tab-private.h',
+ 'hdy-tab-bar-private.h',
+ 'hdy-tab-box-private.h',
+ 'hdy-tab-view-private.h',
'hdy-types.h',
'hdy-view-switcher-button-private.h',
'hdy-window-handle-controller-private.h',
diff --git a/src/handy.gresources.xml b/src/handy.gresources.xml
index b96444b5..affa0e27 100644
--- a/src/handy.gresources.xml
+++ b/src/handy.gresources.xml
@@ -3,6 +3,7 @@
<gresource prefix="/sm/puri/handy">
<file preprocess="xml-stripblanks">icons/avatar-default-symbolic.svg</file>
<file preprocess="xml-stripblanks">icons/hdy-expander-arrow-symbolic.svg</file>
+ <file preprocess="xml-stripblanks">icons/hdy-tab-icon-missing-symbolic.svg</file>
<file compressed="true">themes/Adwaita.css</file>
<file compressed="true">themes/Adwaita-dark.css</file>
<file compressed="true">themes/fallback.css</file>
@@ -21,6 +22,8 @@
<file preprocess="xml-stripblanks">hdy-preferences-page.ui</file>
<file preprocess="xml-stripblanks">hdy-preferences-window.ui</file>
<file preprocess="xml-stripblanks">hdy-search-bar.ui</file>
+ <file preprocess="xml-stripblanks">hdy-tab.ui</file>
+ <file preprocess="xml-stripblanks">hdy-tab-bar.ui</file>
<file preprocess="xml-stripblanks">hdy-view-switcher-bar.ui</file>
<file preprocess="xml-stripblanks">hdy-view-switcher-button.ui</file>
<file preprocess="xml-stripblanks">hdy-view-switcher-title.ui</file>
diff --git a/src/handy.h b/src/handy.h
index 1ea48a7e..4b59c598 100644
--- a/src/handy.h
+++ b/src/handy.h
@@ -49,6 +49,8 @@ G_BEGIN_DECLS
#include "hdy-swipe-group.h"
#include "hdy-swipe-tracker.h"
#include "hdy-swipeable.h"
+#include "hdy-tab-bar.h"
+#include "hdy-tab-view.h"
#include "hdy-title-bar.h"
#include "hdy-types.h"
#include "hdy-value-object.h"
diff --git a/src/hdy-animation-private.h b/src/hdy-animation-private.h
index f31002ad..8eb0400a 100644
--- a/src/hdy-animation-private.h
+++ b/src/hdy-animation-private.h
@@ -14,6 +14,27 @@
G_BEGIN_DECLS
+#define HDY_TYPE_ANIMATION (hdy_animation_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyAnimation, hdy_animation, HDY, ANIMATION, GObject)
+
+typedef void (*HdyAnimationValueCallback) (gdouble value, gpointer user_data);
+typedef void (*HdyAnimationDoneCallback) (gpointer user_data);
+
+HdyAnimation *hdy_animation_new (GtkWidget *widget,
+ gdouble from,
+ gdouble to,
+ gint64 duration,
+ HdyAnimationValueCallback value_cb,
+ HdyAnimationDoneCallback done_cb,
+ gpointer user_data);
+
+void hdy_animation_start (HdyAnimation *self);
+void hdy_animation_stop (HdyAnimation *self);
+
+gdouble hdy_animation_get_value (HdyAnimation *self);
+
gdouble hdy_lerp (gdouble a, gdouble b, gdouble t);
+gdouble hdy_ease_in_cubic (gdouble t);
G_END_DECLS
diff --git a/src/hdy-animation.c b/src/hdy-animation.c
index ce5bf64f..f5292f1b 100644
--- a/src/hdy-animation.c
+++ b/src/hdy-animation.c
@@ -18,6 +18,156 @@
* Since: 0.0.11
*/
+struct _HdyAnimation
+{
+ GObject parent_instance;
+
+ GtkWidget *widget;
+
+ gdouble value;
+
+ gdouble value_from;
+ gdouble value_to;
+ gint64 duration;
+
+ gint64 start_time;
+ guint tick_cb_id;
+
+ HdyAnimationValueCallback value_cb;
+ HdyAnimationDoneCallback done_cb;
+ gpointer user_data;
+};
+
+G_DEFINE_TYPE (HdyAnimation, hdy_animation, G_TYPE_OBJECT)
+
+static void
+set_value (HdyAnimation *self,
+ gdouble value)
+{
+ self->value = value;
+ self->value_cb (value, self->user_data);
+}
+
+static gboolean
+tick_cb (GtkWidget *widget,
+ GdkFrameClock *frame_clock,
+ HdyAnimation *self)
+{
+ gint64 frame_time = gdk_frame_clock_get_frame_time (frame_clock) / 1000;
+ gdouble t = (gdouble) (frame_time - self->start_time) / self->duration;
+
+ if (t >= 1) {
+ self->tick_cb_id = 0;
+
+ set_value (self, self->value_to);
+ self->done_cb (self->user_data);
+
+ return G_SOURCE_REMOVE;
+ }
+
+ set_value (self, hdy_lerp (self->value_from, self->value_to, hdy_ease_out_cubic (t)));
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+hdy_animation_dispose (GObject *object)
+{
+ HdyAnimation *self = HDY_ANIMATION (object);
+
+ hdy_animation_stop (self);
+
+ G_OBJECT_CLASS (hdy_animation_parent_class)->finalize (object);
+}
+
+static void
+hdy_animation_class_init (HdyAnimationClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = hdy_animation_dispose;
+}
+
+static void
+hdy_animation_init (HdyAnimation *self)
+{
+}
+
+HdyAnimation *
+hdy_animation_new (GtkWidget *widget,
+ gdouble from,
+ gdouble to,
+ gint64 duration,
+ HdyAnimationValueCallback value_cb,
+ HdyAnimationDoneCallback done_cb,
+ gpointer user_data)
+{
+ HdyAnimation *anim = g_object_new (HDY_TYPE_ANIMATION, NULL);
+
+ anim->widget = widget;
+ anim->value_from = from;
+ anim->value_to = to;
+ anim->duration = duration;
+ anim->value_cb = value_cb;
+ anim->done_cb = done_cb;
+ anim->user_data = user_data;
+
+ anim->value = from;
+
+ return anim;
+}
+
+void
+hdy_animation_start (HdyAnimation *self)
+{
+ g_return_if_fail (HDY_IS_ANIMATION (self));
+
+ if (!hdy_get_enable_animations (self->widget) ||
+ !gtk_widget_get_mapped (self->widget) ||
+ self->duration <= 0) {
+ set_value (self, self->value_to);
+
+ self->done_cb (self->user_data);
+
+ return;
+ }
+
+ if (self->tick_cb_id) {
+ gtk_widget_remove_tick_callback (self->widget, self->tick_cb_id);
+ self->tick_cb_id = 0;
+ }
+
+ self->start_time = gdk_frame_clock_get_frame_time (gtk_widget_get_frame_clock (self->widget)) / 1000;
+ self->tick_cb_id = gtk_widget_add_tick_callback (self->widget, (GtkTickCallback) tick_cb, self, NULL);
+
+ g_signal_connect_object (self->widget, "unmap", G_CALLBACK (hdy_animation_stop), self, G_CONNECT_SWAPPED);
+}
+
+void
+hdy_animation_stop (HdyAnimation *self)
+{
+ g_return_if_fail (HDY_IS_ANIMATION (self));
+
+ if (!self->tick_cb_id)
+ return;
+
+ gtk_widget_remove_tick_callback (self->widget, self->tick_cb_id);
+ self->tick_cb_id = 0;
+
+ g_signal_handlers_disconnect_by_func (self->widget, hdy_animation_stop, self);
+
+ self->done_cb (self->user_data);
+}
+
+gdouble
+hdy_animation_get_value (HdyAnimation *self)
+{
+ g_return_val_if_fail (HDY_IS_ANIMATION (self), 0.0);
+
+ return self->value;
+}
+
+
/**
* hdy_get_enable_animations:
* @widget: a #GtkWidget
@@ -81,3 +231,9 @@ hdy_ease_out_cubic (gdouble t)
gdouble p = t - 1;
return p * p * p + 1;
}
+
+gdouble
+hdy_ease_in_cubic (gdouble t)
+{
+ return t * t * t;
+}
diff --git a/src/hdy-css-private.h b/src/hdy-css-private.h
index d8190b57..09a5197c 100644
--- a/src/hdy-css-private.h
+++ b/src/hdy-css-private.h
@@ -22,4 +22,13 @@ void hdy_css_measure (GtkWidget *widget,
void hdy_css_size_allocate (GtkWidget *widget,
GtkAllocation *allocation);
+void hdy_css_size_allocate_self (GtkWidget *widget,
+ GtkAllocation *allocation);
+
+void hdy_css_size_allocate_children (GtkWidget *widget,
+ GtkAllocation *allocation);
+
+void hdy_css_draw (GtkWidget *widget,
+ cairo_t *cr);
+
G_END_DECLS
diff --git a/src/hdy-css.c b/src/hdy-css.c
index 7a056e29..5d6e54de 100644
--- a/src/hdy-css.c
+++ b/src/hdy-css.c
@@ -44,30 +44,96 @@ hdy_css_measure (GtkWidget *widget,
border.left + margin.left + padding.left +
border.right + margin.right + padding.right;
}
+
+ *minimum = MAX (*minimum, 0);
+ *natural = MAX (*natural, 0);
}
void
hdy_css_size_allocate (GtkWidget *widget,
GtkAllocation *allocation)
+{
+ hdy_css_size_allocate_self (widget, allocation);
+ hdy_css_size_allocate_children (widget, allocation);
+}
+
+void
+hdy_css_size_allocate_self (GtkWidget *widget,
+ GtkAllocation *allocation)
{
GtkStyleContext *style_context;
GtkStateFlags state_flags;
- GtkBorder border, margin, padding;
+ GtkBorder margin;
/* Manually apply the border, the padding and the margin as we can't use the
* private GtkGagdet.
*/
style_context = gtk_widget_get_style_context (widget);
state_flags = gtk_widget_get_state_flags (widget);
- gtk_style_context_get_border (style_context, state_flags, &border);
+
gtk_style_context_get_margin (style_context, state_flags, &margin);
+
+ allocation->width -= margin.left + margin.right;
+ allocation->height -= margin.top + margin.bottom;
+ allocation->x += margin.left;
+ allocation->y += margin.top;
+}
+
+void
+hdy_css_size_allocate_children (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ GtkStyleContext *style_context;
+ GtkStateFlags state_flags;
+ GtkBorder border, padding;
+
+ /* Manually apply the border, the padding and the margin as we can't use the
+ * private GtkGagdet.
+ */
+ style_context = gtk_widget_get_style_context (widget);
+ state_flags = gtk_widget_get_state_flags (widget);
+
+ gtk_style_context_get_border (style_context, state_flags, &border);
gtk_style_context_get_padding (style_context, state_flags, &padding);
+
allocation->width -= border.left + border.right +
- margin.left + margin.right +
padding.left + padding.right;
allocation->height -= border.top + border.bottom +
- margin.top + margin.bottom +
padding.top + padding.bottom;
- allocation->x += border.left + margin.left + padding.left;
- allocation->y += border.top + margin.top + padding.top;
+ allocation->x += border.left + padding.left;
+ allocation->y += border.top + padding.top;
+}
+
+void
+hdy_css_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ gint width = gtk_widget_get_allocated_width (widget);
+ gint height = gtk_widget_get_allocated_height (widget);
+ GtkStyleContext *style_context;
+
+ if (width <= 0 || height <= 0)
+ return;
+
+ /* Manually apply the border, the padding and the margin as we can't use the
+ * private GtkGagdet.
+ */
+ style_context = gtk_widget_get_style_context (widget);
+
+ gtk_render_background (style_context, cr, 0, 0, width, height);
+ gtk_render_frame (style_context, cr, 0, 0, width, height);
+
+ if (gtk_widget_has_visible_focus (widget)) {
+ GtkStateFlags state_flags;
+ GtkBorder border;
+
+ state_flags = gtk_widget_get_state_flags (widget);
+
+ gtk_style_context_get_border (style_context, state_flags, &border);
+
+ gtk_render_focus (style_context, cr,
+ border.left, border.top,
+ width - border.left - border.right,
+ height - border.top - border.bottom);
+ }
}
diff --git a/src/hdy-enums.c.in b/src/hdy-enums.c.in
index a6305554..8b773024 100644
--- a/src/hdy-enums.c.in
+++ b/src/hdy-enums.c.in
@@ -8,6 +8,7 @@
#include "hdy-leaflet.h"
#include "hdy-navigation-direction.h"
#include "hdy-squeezer.h"
+#include "hdy-tab-bar.h"
#include "hdy-view-switcher.h"
/*** END file-header ***/
diff --git a/src/hdy-header-bar.c b/src/hdy-header-bar.c
index 32833006..73fd7ab8 100644
--- a/src/hdy-header-bar.c
+++ b/src/hdy-header-bar.c
@@ -1545,6 +1545,7 @@ hdy_header_bar_destroy (GtkWidget *widget)
{
HdyHeaderBarPrivate *priv = hdy_header_bar_get_instance_private (HDY_HEADER_BAR (widget));
+ g_print ("destroy?\n");
if (priv->label_sizing_box) {
gtk_widget_destroy (priv->label_sizing_box);
g_clear_object (&priv->label_sizing_box);
diff --git a/src/hdy-tab-bar-private.h b/src/hdy-tab-bar-private.h
new file mode 100644
index 00000000..f3990c2d
--- /dev/null
+++ b/src/hdy-tab-bar-private.h
@@ -0,0 +1,19 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-tab-bar.h"
+
+G_BEGIN_DECLS
+
+gboolean hdy_tab_bar_tabs_have_visible_focus (HdyTabBar *self);
+
+G_END_DECLS
diff --git a/src/hdy-tab-bar.c b/src/hdy-tab-bar.c
new file mode 100644
index 00000000..02abbcc6
--- /dev/null
+++ b/src/hdy-tab-bar.c
@@ -0,0 +1,622 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-tab-bar-private.h"
+#include "hdy-tab-box-private.h"
+
+struct _HdyTabBar
+{
+ GtkBin parent_instance;
+
+ GtkRevealer *revealer;
+ HdyTabBox *pinned_box;
+ HdyTabBox *scroll_box;
+ GtkViewport *viewport;
+ GtkScrolledWindow *scrolled_window;
+ GtkBin *start_action_bin;
+ GtkBin *end_action_bin;
+
+ HdyTabView *view;
+ HdyTabBarPosition position;
+};
+
+static void hdy_tab_bar_buildable_init (GtkBuildableIface *iface);
+
+G_DEFINE_TYPE_WITH_CODE (HdyTabBar, hdy_tab_bar, GTK_TYPE_BIN,
+ G_IMPLEMENT_INTERFACE (GTK_TYPE_BUILDABLE,
+ hdy_tab_bar_buildable_init))
+
+enum {
+ PROP_0,
+ PROP_VIEW,
+ PROP_START_ACTION_WIDGET,
+ PROP_END_ACTION_WIDGET,
+ PROP_POSITION,
+ LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void
+update_autohide_cb (HdyTabBar *self)
+{
+ guint n_tabs = 0, n_pinned_tabs = 0;
+ gboolean is_dragging;
+
+ if (!self->view) {
+ gtk_revealer_set_reveal_child (self->revealer, FALSE);
+
+ return;
+ }
+
+ n_tabs = hdy_tab_view_get_n_pages (self->view);
+ n_pinned_tabs = hdy_tab_view_get_n_pinned_pages (self->view);
+ is_dragging = hdy_tab_view_get_is_dragging (self->view);
+
+ gtk_revealer_set_reveal_child (self->revealer,
+ n_tabs > 1 ||
+ n_pinned_tabs >= 1 ||
+ is_dragging);
+}
+
+static void
+notify_selected_page_cb (HdyTabBar *self)
+{
+ HdyTabPage *page = hdy_tab_view_get_selected_page (self->view);
+
+ if (hdy_tab_page_get_pinned (page)) {
+ hdy_tab_box_select_page (self->pinned_box, page);
+ hdy_tab_box_select_page (self->scroll_box, page);
+ } else {
+ hdy_tab_box_select_page (self->scroll_box, page);
+ hdy_tab_box_select_page (self->pinned_box, page);
+ }
+
+ /* FIXME HACK elementary active tab shadows */
+/*
+ var animation = new Animation (this, 0, 1, 300, () => {
+ pinned_box.queue_draw ();
+ scroll_box.queue_draw ();
+ }, null);
+ animation.start ();
+*/
+}
+
+static void
+page_pinned_cb (HdyTabBar *self,
+ HdyTabPage *page)
+{
+ gboolean should_focus = hdy_tab_box_is_page_focused (self->scroll_box, page);
+
+ hdy_tab_box_remove_page (self->scroll_box, page);
+ hdy_tab_box_add_page (self->pinned_box, page,
+ hdy_tab_view_get_n_pinned_pages (self->view));
+
+ if (should_focus)
+ hdy_tab_box_try_focus_selected_tab (self->pinned_box);
+}
+
+static void
+page_unpinned_cb (HdyTabBar *self,
+ HdyTabPage *page)
+{
+ gboolean should_focus = hdy_tab_box_is_page_focused (self->pinned_box, page);
+
+ hdy_tab_box_remove_page (self->pinned_box, page);
+ hdy_tab_box_add_page (self->scroll_box, page,
+ hdy_tab_view_get_n_pinned_pages (self->view));
+
+ if (should_focus)
+ hdy_tab_box_try_focus_selected_tab (self->scroll_box);
+}
+
+static void
+notify_needs_attention_cb (HdyTabBar *self)
+{
+ GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ gboolean left, right;
+
+ g_object_get (self->scroll_box,
+ "needs-attention-left", &left,
+ "needs-attention-right", &right,
+ NULL);
+
+ if (left)
+ gtk_style_context_add_class (context, "needs-attention-left");
+ else
+ gtk_style_context_remove_class (context, "needs-attention-left");
+
+ if (right)
+ gtk_style_context_add_class (context, "needs-attention-right");
+ else
+ gtk_style_context_remove_class (context, "needs-attention-right");
+
+ /* FIXME HACK: Undershoot indicator doesn't redraw on ist own, do a
+ * manual animation */
+/*
+ var animation = new Animation (this, 0, 1, 300, () => {
+ pinned_box.queue_draw ();
+ scroll_box.queue_draw ();
+ }, null);
+ animation.start ();
+*/
+}
+
+static void
+stop_kinetic_scrolling_cb (HdyTabBar *self)
+{
+ /* HACK: Need to cancel kinetic scrolling. If only the built-in adjustment
+ * animation API was public, we wouldn't have to do any of this... */
+ gtk_scrolled_window_set_kinetic_scrolling (self->scrolled_window, FALSE);
+ gtk_scrolled_window_set_kinetic_scrolling (self->scrolled_window, TRUE);
+}
+
+static void
+view_destroy_cb (HdyTabBar *self)
+{
+ hdy_tab_bar_set_view (self, NULL);
+}
+
+static void
+destroy_widget (GtkWidget *widget,
+ gpointer user_data)
+{
+ gtk_widget_destroy (widget);
+}
+
+static void
+hdy_tab_bar_destroy (GtkWidget *widget)
+{
+ gtk_container_forall (GTK_CONTAINER (widget), destroy_widget, NULL);
+
+ GTK_WIDGET_CLASS (hdy_tab_bar_parent_class)->destroy (widget);
+}
+
+static gboolean
+hdy_tab_bar_focus (GtkWidget *widget,
+ GtkDirectionType direction)
+{
+ HdyTabBar *self = HDY_TAB_BAR (widget);
+ gboolean is_rtl;
+ GtkDirectionType start, end;
+
+ if (!self->view)
+ return GDK_EVENT_PROPAGATE;
+
+ if (!gtk_container_get_focus_child (GTK_CONTAINER (self)))
+ return gtk_widget_child_focus (GTK_WIDGET (self->pinned_box), direction) ||
+ gtk_widget_child_focus (GTK_WIDGET (self->scroll_box), direction);
+
+ is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
+ start = is_rtl ? GTK_DIR_RIGHT : GTK_DIR_LEFT;
+ end = is_rtl ? GTK_DIR_LEFT : GTK_DIR_RIGHT;
+
+ if (direction == start) {
+ if (hdy_tab_view_select_previous_page (self->view))
+ return GDK_EVENT_STOP;
+
+ return gtk_widget_keynav_failed (widget, direction);
+ }
+
+ if (direction == end) {
+ if (hdy_tab_view_select_next_page (self->view))
+ return GDK_EVENT_STOP;
+
+ return gtk_widget_keynav_failed (widget, direction);
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+hdy_tab_bar_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ HdyTabBar *self = HDY_TAB_BAR (widget);
+
+ /* On RTL, the adjustment value is modified and will interfere with animations */
+ hdy_tab_box_set_block_scrolling (self->scroll_box, TRUE);
+
+ GTK_WIDGET_CLASS (hdy_tab_bar_parent_class)->size_allocate (widget,
+ allocation);
+
+ hdy_tab_box_set_block_scrolling (self->scroll_box, FALSE);
+}
+
+static void
+hdy_tab_bar_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyTabBar *self = HDY_TAB_BAR (container);
+ GtkWidget *start, *end;
+
+ if (include_internals) {
+ GTK_CONTAINER_CLASS (hdy_tab_bar_parent_class)->forall (container,
+ include_internals,
+ callback,
+ callback_data);
+
+ return;
+ }
+
+ start = hdy_tab_bar_get_start_action_widget (self);
+ end = hdy_tab_bar_get_end_action_widget (self);
+
+ if (start)
+ callback (start, callback_data);
+
+ if (end)
+ callback (end, callback_data);
+}
+
+static void
+hdy_tab_bar_dispose (GObject *object)
+{
+ HdyTabBar *self = HDY_TAB_BAR (object);
+
+ hdy_tab_bar_set_view (self, NULL);
+
+ G_OBJECT_CLASS (hdy_tab_bar_parent_class)->dispose (object);
+}
+
+static void
+hdy_tab_bar_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyTabBar *self = HDY_TAB_BAR (object);
+
+ switch (prop_id) {
+ case PROP_VIEW:
+ g_value_set_object (value, hdy_tab_bar_get_view (self));
+ break;
+
+ case PROP_START_ACTION_WIDGET:
+ g_value_set_object (value, hdy_tab_bar_get_start_action_widget (self));
+ break;
+
+ case PROP_END_ACTION_WIDGET:
+ g_value_set_object (value, hdy_tab_bar_get_end_action_widget (self));
+ break;
+
+ case PROP_POSITION:
+ g_value_set_enum (value, hdy_tab_bar_get_position (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_tab_bar_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyTabBar *self = HDY_TAB_BAR (object);
+
+ switch (prop_id) {
+ case PROP_VIEW:
+ hdy_tab_bar_set_view (self, g_value_get_object (value));
+ break;
+
+ case PROP_START_ACTION_WIDGET:
+ hdy_tab_bar_set_start_action_widget (self, g_value_get_object (value));
+ break;
+
+ case PROP_END_ACTION_WIDGET:
+ hdy_tab_bar_set_end_action_widget (self, g_value_get_object (value));
+ break;
+
+ case PROP_POSITION:
+ hdy_tab_bar_set_position (self, g_value_get_enum (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_tab_bar_class_init (HdyTabBarClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->dispose = hdy_tab_bar_dispose;
+ object_class->get_property = hdy_tab_bar_get_property;
+ object_class->set_property = hdy_tab_bar_set_property;
+
+ widget_class->destroy = hdy_tab_bar_destroy;
+ widget_class->focus = hdy_tab_bar_focus;
+ widget_class->size_allocate = hdy_tab_bar_size_allocate;
+
+ container_class->forall = hdy_tab_bar_forall;
+
+ props[PROP_VIEW] =
+ g_param_spec_object ("view",
+ _("View"),
+ _("View"),
+ HDY_TYPE_TAB_VIEW,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_START_ACTION_WIDGET] =
+ g_param_spec_object ("start-action-widget",
+ _("Start Action Widget"),
+ _("Start Action Widget"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_END_ACTION_WIDGET] =
+ g_param_spec_object ("end-action-widget",
+ _("End Action Widget"),
+ _("End Action Widget"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_POSITION] =
+ g_param_spec_enum ("position",
+ _("Position"),
+ _("Position"),
+ HDY_TYPE_TAB_BAR_POSITION,
+ HDY_TAB_BAR_POSITION_TOP,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-tab-bar.ui");
+ gtk_widget_class_bind_template_child (widget_class, HdyTabBar, revealer);
+ gtk_widget_class_bind_template_child (widget_class, HdyTabBar, pinned_box);
+ gtk_widget_class_bind_template_child (widget_class, HdyTabBar, scroll_box);
+ gtk_widget_class_bind_template_child (widget_class, HdyTabBar, viewport);
+ gtk_widget_class_bind_template_child (widget_class, HdyTabBar, scrolled_window);
+ gtk_widget_class_bind_template_child (widget_class, HdyTabBar, start_action_bin);
+ gtk_widget_class_bind_template_child (widget_class, HdyTabBar, end_action_bin);
+ gtk_widget_class_bind_template_callback (widget_class, notify_needs_attention_cb);
+ gtk_widget_class_bind_template_callback (widget_class, stop_kinetic_scrolling_cb);
+
+ gtk_widget_class_set_css_name (widget_class, "tabbar");
+}
+
+static void
+hdy_tab_bar_init (HdyTabBar *self)
+{
+ g_type_ensure (HDY_TYPE_TAB_BOX);
+
+ gtk_widget_init_template (GTK_WIDGET (self));
+
+ hdy_tab_box_set_adjustment (self->scroll_box,
+ gtk_scrolled_window_get_hadjustment (self->scrolled_window));
+
+ /* HdyTabBox scrolls on focus itself, and does it better than GtkViewport */
+ gtk_container_set_focus_hadjustment (GTK_CONTAINER (self->viewport), NULL);
+}
+
+static void
+hdy_tab_bar_buildable_add_child (GtkBuildable *buildable,
+ GtkBuilder *builder,
+ GObject *child,
+ const gchar *type)
+{
+ HdyTabBar *self = HDY_TAB_BAR (buildable);
+
+ if (!self->revealer) {
+ gtk_container_add (GTK_CONTAINER (self), GTK_WIDGET (child));
+
+ return;
+ }
+
+ if (!type || !g_strcmp0 (type, "start"))
+ hdy_tab_bar_set_start_action_widget (self, GTK_WIDGET (child));
+ else if (!g_strcmp0 (type, "end"))
+ hdy_tab_bar_set_end_action_widget (self, GTK_WIDGET (child));
+ else
+ GTK_BUILDER_WARN_INVALID_CHILD_TYPE (HDY_TAB_BAR (self), type);
+}
+
+static void
+hdy_tab_bar_buildable_init (GtkBuildableIface *iface)
+{
+ iface->add_child = hdy_tab_bar_buildable_add_child;
+}
+
+gboolean
+hdy_tab_bar_tabs_have_visible_focus (HdyTabBar *self)
+{
+ GtkWidget *pinned_focus_child, *scroll_focus_child;
+
+ pinned_focus_child = gtk_container_get_focus_child (GTK_CONTAINER (self->pinned_box));
+ scroll_focus_child = gtk_container_get_focus_child (GTK_CONTAINER (self->scroll_box));
+
+ 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;
+}
+
+HdyTabBar *
+hdy_tab_bar_new (void)
+{
+ return g_object_new (HDY_TYPE_TAB_BAR, NULL);
+}
+
+HdyTabView *
+hdy_tab_bar_get_view (HdyTabBar *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_BAR (self), NULL);
+
+ return self->view;
+}
+
+void
+hdy_tab_bar_set_view (HdyTabBar *self,
+ HdyTabView *view)
+{
+ g_return_if_fail (HDY_IS_TAB_BAR (self));
+ g_return_if_fail (HDY_IS_TAB_VIEW (view) || view == NULL);
+
+ if (self->view == view)
+ return;
+
+ if (self->view) {
+ g_signal_handlers_disconnect_by_func (self->view, update_autohide_cb, self);
+ g_signal_handlers_disconnect_by_func (self->view, notify_selected_page_cb, self);
+ g_signal_handlers_disconnect_by_func (self->view, page_pinned_cb, self);
+ g_signal_handlers_disconnect_by_func (self->view, page_unpinned_cb, self);
+ }
+
+ g_set_object (&self->view, view);
+
+ if (self->view) {
+ g_signal_connect_object (self->view, "notify::is-dragging",
+ G_CALLBACK (update_autohide_cb), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->view, "notify::n-pages",
+ G_CALLBACK (update_autohide_cb), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->view, "notify::n-pinned-pages",
+ G_CALLBACK (update_autohide_cb), self,
+ G_CONNECT_SWAPPED);
+ 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-pinned",
+ G_CALLBACK (page_pinned_cb), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->view, "page-unpinned",
+ G_CALLBACK (page_unpinned_cb), self,
+ G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->view, "destroy",
+ G_CALLBACK (view_destroy_cb), self,
+ G_CONNECT_SWAPPED);
+ }
+
+ hdy_tab_box_set_view (self->pinned_box, view);
+ hdy_tab_box_set_view (self->scroll_box, view);
+
+ update_autohide_cb (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW]);
+}
+
+GtkWidget *
+hdy_tab_bar_get_start_action_widget (HdyTabBar *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_BAR (self), NULL);
+
+ return gtk_bin_get_child (self->start_action_bin);
+}
+
+void
+hdy_tab_bar_set_start_action_widget (HdyTabBar *self,
+ GtkWidget *widget)
+{
+ GtkWidget *old_widget;
+
+ g_return_if_fail (HDY_IS_TAB_BAR (self));
+ g_return_if_fail (GTK_IS_WIDGET (widget) || widget == NULL);
+
+ old_widget = gtk_bin_get_child (self->start_action_bin);
+
+ if (old_widget == widget)
+ return;
+
+ if (old_widget)
+ gtk_container_remove (GTK_CONTAINER (self->start_action_bin), old_widget);
+
+ if (widget)
+ gtk_container_add (GTK_CONTAINER (self->start_action_bin), widget);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_START_ACTION_WIDGET]);
+}
+
+GtkWidget *
+hdy_tab_bar_get_end_action_widget (HdyTabBar *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_BAR (self), NULL);
+
+ return gtk_bin_get_child (self->end_action_bin);
+}
+
+void
+hdy_tab_bar_set_end_action_widget (HdyTabBar *self,
+ GtkWidget *widget)
+{
+ GtkWidget *old_widget;
+
+ g_return_if_fail (HDY_IS_TAB_BAR (self));
+ g_return_if_fail (GTK_IS_WIDGET (widget) || widget == NULL);
+
+ old_widget = gtk_bin_get_child (self->end_action_bin);
+
+ if (old_widget == widget)
+ return;
+
+ if (old_widget)
+ gtk_container_remove (GTK_CONTAINER (self->end_action_bin), old_widget);
+
+ if (widget)
+ gtk_container_add (GTK_CONTAINER (self->end_action_bin), widget);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_END_ACTION_WIDGET]);
+}
+
+HdyTabBarPosition
+hdy_tab_bar_get_position (HdyTabBar *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_BAR (self), 0);
+
+ return self->position;
+}
+
+void
+hdy_tab_bar_set_position (HdyTabBar *self,
+ HdyTabBarPosition position)
+{
+ GtkStyleContext *context;
+
+ g_return_if_fail (HDY_IS_TAB_BAR (self));
+
+ if (self->position == position)
+ return;
+
+ context = gtk_widget_get_style_context (GTK_WIDGET (self));
+
+ self->position = position;
+
+ switch (position) {
+ case HDY_TAB_BAR_POSITION_TOP:
+ gtk_style_context_add_class (context, "top");
+ gtk_style_context_remove_class (context, "bottom");
+ gtk_revealer_set_transition_type (self->revealer,
+ GTK_REVEALER_TRANSITION_TYPE_SLIDE_DOWN);
+ break;
+
+ case HDY_TAB_BAR_POSITION_BOTTOM:
+ gtk_style_context_add_class (context, "bottom");
+ gtk_style_context_remove_class (context, "top");
+ gtk_revealer_set_transition_type (self->revealer,
+ GTK_REVEALER_TRANSITION_TYPE_SLIDE_UP);
+ break;
+
+ default:
+ g_assert_not_reached ();
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_POSITION]);
+}
diff --git a/src/hdy-tab-bar.h b/src/hdy-tab-bar.h
new file mode 100644
index 00000000..b1ec9c19
--- /dev/null
+++ b/src/hdy-tab-bar.h
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+#include "hdy-enums.h"
+#include "hdy-tab-view.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_TAB_BAR (hdy_tab_bar_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyTabBar, hdy_tab_bar, HDY, TAB_BAR, GtkBin)
+
+typedef enum {
+ HDY_TAB_BAR_POSITION_TOP,
+ HDY_TAB_BAR_POSITION_BOTTOM,
+} HdyTabBarPosition;
+
+HDY_AVAILABLE_IN_ALL
+HdyTabBar *hdy_tab_bar_new (void);
+
+HDY_AVAILABLE_IN_ALL
+HdyTabView *hdy_tab_bar_get_view (HdyTabBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_bar_set_view (HdyTabBar *self,
+ HdyTabView *view);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_tab_bar_get_start_action_widget (HdyTabBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_bar_set_start_action_widget (HdyTabBar *self,
+ GtkWidget *widget);
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_tab_bar_get_end_action_widget (HdyTabBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_bar_set_end_action_widget (HdyTabBar *self,
+ GtkWidget *widget);
+
+HDY_AVAILABLE_IN_ALL
+HdyTabBarPosition hdy_tab_bar_get_position (HdyTabBar *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_bar_set_position (HdyTabBar *self,
+ HdyTabBarPosition position);
+
+G_END_DECLS
diff --git a/src/hdy-tab-bar.ui b/src/hdy-tab-bar.ui
new file mode 100644
index 00000000..f63b2dc6
--- /dev/null
+++ b/src/hdy-tab-bar.ui
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.24"/>
+ <template class="HdyTabBar" parent="GtkBin">
+ <style>
+ <class name="top"/>
+ </style>
+ <child>
+ <object class="GtkRevealer" id="revealer">
+ <property name="visible">True</property>
+ <property name="transition-duration">200</property>
+ <property name="transition-type">slide-down</property>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <style>
+ <class name="box"/>
+ </style>
+ <child>
+ <object class="GtkEventBox" id="start_action_bin">
+ <property name="visible">True</property>
+ <style>
+ <class name="start-action"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="HdyTabBox" id="pinned_box">
+ <property name="visible">True</property>
+ <property name="pinned">True</property>
+ <property name="hexpand">False</property>
+ <property name="tab-bar">HdyTabBar</property>
+ <style>
+ <class name="pinned"/>
+ </style>
+ </object>
+ </child>
+ <child>
+ <object class="GtkScrolledWindow" id="scrolled_window">
+ <property name="visible">True</property>
+ <property name="can-focus">False</property>
+ <property name="hscrollbar-policy">external</property>
+ <property name="vscrollbar-policy">never</property>
+ <property name="overlay-scrolling">False</property>
+ <property name="hexpand">True</property>
+ <child>
+ <object class="GtkViewport" id="viewport">
+ <property name="visible">True</property>
+ <child>
+ <object class="HdyTabBox" id="scroll_box">
+ <property name="visible">True</property>
+ <property name="tab-bar">HdyTabBar</property>
+ <signal name="notify::needs-attention-left" handler="notify_needs_attention_cb"
swapped="true"/>
+ <signal name="notify::needs-attention-right" handler="notify_needs_attention_cb"
swapped="true"/>
+ <signal name="stop-kinetic-scrolling" handler="stop_kinetic_scrolling_cb"
swapped="true"/>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkEventBox" id="end_action_bin">
+ <property name="visible">True</property>
+ <style>
+ <class name="end-action"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/hdy-tab-box-private.h b/src/hdy-tab-box-private.h
new file mode 100644
index 00000000..8f5ac06a
--- /dev/null
+++ b/src/hdy-tab-box-private.h
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include "hdy-tab-view.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_TAB_BOX (hdy_tab_box_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyTabBox, hdy_tab_box, HDY, TAB_BOX, GtkContainer)
+
+void hdy_tab_box_set_view (HdyTabBox *self,
+ HdyTabView *view);
+void hdy_tab_box_set_adjustment (HdyTabBox *self,
+ GtkAdjustment *adjustment);
+void hdy_tab_box_set_block_scrolling (HdyTabBox *self,
+ gboolean block_scrolling);
+
+void hdy_tab_box_add_page (HdyTabBox *self,
+ HdyTabPage *page,
+ guint position);
+void hdy_tab_box_remove_page (HdyTabBox *self,
+ HdyTabPage *page);
+void hdy_tab_box_select_page (HdyTabBox *self,
+ HdyTabPage *page);
+
+void hdy_tab_box_try_focus_selected_tab (HdyTabBox *self);
+gboolean hdy_tab_box_is_page_focused (HdyTabBox *self,
+ HdyTabPage *page);
+
+G_END_DECLS
diff --git a/src/hdy-tab-box.c b/src/hdy-tab-box.c
new file mode 100644
index 00000000..f2c58414
--- /dev/null
+++ b/src/hdy-tab-box.c
@@ -0,0 +1,3318 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-tab-box-private.h"
+#include "hdy-animation-private.h"
+#include "hdy-css-private.h"
+#include "hdy-tab-private.h"
+#include "hdy-tab-bar-private.h"
+#include "hdy-tab-view-private.h"
+#include <math.h>
+
+/* Border collapsing without glitches */
+#define OVERLAP 1
+#define DND_THRESHOLD_MULTIPLIER 4
+
+#define AUTOSCROLL_AREA_WIDTH 65
+#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_WIDTH,
+ TAB_RESIZE_FIXED_END_PADDING
+} TabResizeMode;
+
+static const GtkTargetEntry src_targets [] = {
+ { "HDY_TAB", GTK_TARGET_SAME_APP, 0 },
+ { "application/x-rootwindow-drop", 0, 0 },
+};
+
+static const GtkTargetEntry dst_targets [] = {
+ { "HDY_TAB", GTK_TARGET_SAME_APP, 0 },
+};
+
+typedef struct {
+ GtkWidget *window;
+ GdkDragContext *context;
+
+ HdyTab *tab;
+ GtkBorder tab_margin;
+
+ gint hotspot_x;
+ gint hotspot_y;
+
+ gint width;
+ gint target_width;
+ HdyAnimation *resize_animation;
+} DragIcon;
+
+typedef struct {
+ HdyTabPage *page;
+ HdyTab *tab;
+
+ gint pos;
+ gint width;
+ gint last_width;
+
+ gdouble end_reorder_offset;
+ gdouble reorder_offset;
+
+ HdyAnimation *reorder_animation;
+ gboolean reorder_ignore_bounds;
+
+ gdouble appear_progress;
+ HdyAnimation *appear_animation;
+
+ gulong notify_needs_attention_id;
+} TabInfo;
+
+struct _HdyTabBox
+{
+ GtkContainer parent_instance;
+
+ gboolean pinned;
+ HdyTabBar *tab_bar;
+ HdyTabView *view;
+ GtkAdjustment *adjustment;
+ gboolean needs_attention_left;
+ gboolean needs_attention_right;
+
+ GList *tabs;
+ gint n_tabs;
+
+ GdkWindow *window;
+ GdkWindow *reorder_window;
+
+ GtkMenu *context_menu;
+ GtkPopover *touch_menu;
+ GtkGesture *touch_menu_gesture;
+
+ gint allocated_width;
+ gint last_width;
+ gint end_padding;
+ gint initial_end_padding;
+ TabResizeMode tab_resize_mode;
+ HdyAnimation *resize_animation;
+
+ TabInfo *selected_tab;
+
+ gboolean hovering;
+ gdouble hover_x;
+ gdouble hover_y;
+ TabInfo *hovered_tab;
+
+ TabInfo *reordered_tab;
+ HdyAnimation *reorder_animation;
+ gboolean pressed;
+
+ gint reorder_x;
+ gint reorder_y;
+ guint reorder_index;
+ gint reorder_window_x;
+ gboolean continue_reorder;
+ gboolean indirect_reordering;
+ gint pressed_button;
+
+ gboolean dragging;
+ gdouble drag_begin_x;
+ gdouble drag_begin_y;
+ gdouble drag_offset_x;
+ gdouble drag_offset_y;
+ GdkSeat *drag_seat;
+
+ guint drag_autoscroll_cb_id;
+ gint64 drag_autoscroll_prev_time;
+
+ HdyTabPage *detached_page;
+ guint detached_index;
+ TabInfo *reorder_placeholder;
+ HdyTabPage *placeholder_page;
+ gboolean can_remove_placeholder;
+ DragIcon *drag_icon;
+ gboolean should_detach_into_new_window;
+ GtkTargetList *source_targets;
+
+ HdyAnimation *scroll_animation;
+ gboolean scroll_animation_done;
+ gdouble scroll_animation_from;
+ gdouble scroll_animation_offset;
+ TabInfo *scroll_animation_tab;
+ gboolean block_scrolling;
+ gdouble adjustment_prev_value;
+};
+
+G_DEFINE_TYPE (HdyTabBox, hdy_tab_box, GTK_TYPE_CONTAINER)
+
+enum {
+ PROP_0,
+ PROP_PINNED,
+ PROP_TAB_BAR,
+ PROP_VIEW,
+ PROP_ADJUSTMENT,
+ PROP_NEEDS_ATTENTION_LEFT,
+ PROP_NEEDS_ATTENTION_RIGHT,
+ LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_STOP_KINETIC_SCROLLING,
+ SIGNAL_ACTIVATE_TAB,
+ SIGNAL_FOCUS_TAB,
+ SIGNAL_REORDER_TAB,
+ 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 gint
+get_tab_position (HdyTabBox *self,
+ TabInfo *info)
+{
+ if (info == self->reordered_tab) {
+ gint pos = 0;
+ gdk_window_get_position (self->reorder_window, &pos, NULL);
+
+ return pos;
+ }
+
+ return info->pos;
+}
+
+static inline TabInfo *
+find_tab_info_at (HdyTabBox *self,
+ double x)
+{
+ GList *l;
+
+ if (self->reordered_tab) {
+ gint 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 (HdyTabBox *self,
+ HdyTabPage *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 (HdyTabBox *self,
+ HdyTabPage *page)
+{
+ GList *l = find_link_for_page (self, page);
+
+ return l ? l->data : NULL;
+}
+
+static GList *
+find_nth_alive_tab (HdyTabBox *self,
+ uint 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 gint
+calculate_tab_width (TabInfo *info,
+ gint base_width)
+{
+ return OVERLAP + (gint) ((base_width - OVERLAP) * info->appear_progress);
+}
+
+static gint
+get_base_tab_width (HdyTabBox *self)
+{
+ gdouble max_progress = 0;
+ gdouble n = 0;
+ gdouble used_width;
+ GList *l;
+
+ 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 - self->end_padding) * max_progress;
+
+ return (gint) floor (used_width / n);
+}
+
+static gint
+predict_tab_width (HdyTabBox *self,
+ TabInfo *info,
+ gboolean assume_placeholder)
+{
+ gint n = self->n_tabs;
+ gint width = self->allocated_width;
+ gint min;
+
+ if (assume_placeholder && !self->reorder_placeholder)
+ n++;
+
+ width += OVERLAP * (n + 1) - self->end_padding;
+
+ min = hdy_tab_get_child_min_width (info->tab);
+
+ return MAX ((gint) floor (width / (gdouble) n), min);
+}
+
+static gint
+calculate_tab_offset (HdyTabBox *self,
+ TabInfo *info)
+{
+ gint width;
+
+ if (!self->reordered_tab)
+ return 0;
+
+ width = self->reordered_tab->width - OVERLAP;
+
+ if (gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL)
+ width = -width;
+
+ return (gint) round (width * info->reorder_offset);
+}
+
+static gboolean
+get_widget_coordinates (HdyTabBox *self,
+ GdkEvent *event,
+ gdouble *x,
+ gdouble *y)
+{
+ GdkWindow *window = gdk_event_get_window (event);
+ gdouble tx, ty, out_x = -1, out_y = -1;
+
+ if (!gdk_event_get_coords (event, &tx, &ty))
+ goto out;
+
+ while (window && window != self->window) {
+ gint window_x, window_y;
+
+ gdk_window_get_position (window, &window_x, &window_y);
+
+ tx += window_x;
+ ty += window_y;
+
+ window = gdk_window_get_parent (window);
+ }
+
+ if (window) {
+ out_x = tx;
+ out_y = ty;
+ goto out;
+ }
+
+out:
+ if (x)
+ *x = out_x;
+
+ if (y)
+ *y = out_y;
+
+ return out_x >= 0 && out_y >= 0;
+}
+
+static void
+get_visible_range (HdyTabBox *self,
+ gint *lower,
+ gint *upper)
+{
+ gint min, max;
+ GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ GtkStateFlags flags = gtk_widget_get_state_flags (GTK_WIDGET (self));
+ GtkBorder border, padding;
+
+ gtk_style_context_get_border (context, flags, &border);
+ gtk_style_context_get_padding (context, flags, &padding);
+
+ min = border.left + padding.left - OVERLAP;
+ max = border.left + padding.left + self->allocated_width + OVERLAP;
+
+ if (self->adjustment) {
+ GtkBorder margin;
+ gint scroll_min, scroll_max;
+ gdouble value, page_size;
+
+ gtk_style_context_get_margin (context, flags, &margin);
+
+ value = gtk_adjustment_get_value (self->adjustment);
+ page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ scroll_min = (gint) floor (value);
+ scroll_max = (gint) ceil (value + page_size);
+
+ min = MAX (min, scroll_min - margin.left - OVERLAP);
+ max = MIN (max, scroll_max - margin.left + OVERLAP);
+ }
+
+ if (lower)
+ *lower = min;
+
+ if (upper)
+ *upper = max;
+}
+
+/* Tab resize delay */
+
+static void
+resize_animation_value_cb (gdouble value,
+ gpointer user_data)
+{
+ HdyTabBox *self = HDY_TAB_BOX (user_data);
+
+ self->end_padding = (gint) floor (self->initial_end_padding * value);
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+resize_animation_done_cb (gpointer user_data)
+{
+ HdyTabBox *self = HDY_TAB_BOX (user_data);
+
+ g_clear_object (&self->resize_animation);
+}
+
+static void
+set_tab_resize_mode (HdyTabBox *self,
+ TabResizeMode mode)
+{
+ 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 = hdy_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 =
+ hdy_animation_new (GTK_WIDGET (self), 1, 0,
+ RESIZE_ANIMATION_DURATION,
+ resize_animation_value_cb,
+ resize_animation_done_cb,
+ self);
+
+ hdy_animation_start (self->resize_animation);
+ }
+
+ self->tab_resize_mode = mode;
+}
+
+/* Hover */
+
+static void
+update_hover (HdyTabBox *self)
+{
+ TabInfo *info;
+
+ if (self->dragging)
+ return;
+
+ if (!self->hovering) {
+ set_tab_resize_mode (self, TAB_RESIZE_NORMAL);
+
+ if (self->hovered_tab) {
+ hdy_tab_set_hovering (self->hovered_tab->tab, FALSE);
+ self->hovered_tab = NULL;
+ }
+
+ return;
+ }
+
+ info = find_tab_info_at (self, self->hover_x);
+
+ if (info != self->hovered_tab) {
+ if (self->hovered_tab)
+ hdy_tab_set_hovering (self->hovered_tab->tab, FALSE);
+
+ self->hovered_tab = info;
+
+ if (self->hovered_tab)
+ hdy_tab_set_hovering (self->hovered_tab->tab, TRUE);
+ }
+}
+
+/* Keybindings */
+
+static void
+add_focus_bindings (GtkBindingSet *binding_set,
+ 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_binding_entry_add_signal (binding_set, keysym, 0,
+ "focus-tab", 2,
+ GTK_TYPE_DIRECTION_TYPE, direction,
+ G_TYPE_BOOLEAN, last);
+ gtk_binding_entry_add_signal (binding_set, keypad_keysym, 0,
+ "focus-tab", 2,
+ GTK_TYPE_DIRECTION_TYPE, direction,
+ G_TYPE_BOOLEAN, last);
+}
+
+static void
+add_reorder_bindings (GtkBindingSet *binding_set,
+ 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_binding_entry_add_signal (binding_set, keysym, GDK_MOD1_MASK,
+ "reorder-tab", 2,
+ GTK_TYPE_DIRECTION_TYPE, direction,
+ G_TYPE_BOOLEAN, last);
+ gtk_binding_entry_add_signal (binding_set, keypad_keysym, GDK_MOD1_MASK,
+ "reorder-tab", 2,
+ GTK_TYPE_DIRECTION_TYPE, direction,
+ G_TYPE_BOOLEAN, last);
+}
+
+static void
+activate_tab (HdyTabBox *self)
+{
+ GtkWidget *content;
+
+ if (!self->selected_tab || !self->selected_tab->page)
+ return;
+
+ content = hdy_tab_page_get_content (self->selected_tab->page);
+
+ // FIXME this does nothing somehow
+ gtk_widget_grab_focus (content);
+}
+
+static void
+focus_tab_cb (HdyTabBox *self,
+ GtkDirectionType direction,
+ gboolean last)
+{
+ gboolean is_rtl, success = last;
+
+ if (!self->view || !self->selected_tab)
+ return;
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+ 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 = hdy_tab_view_select_first_page (self->view);
+ else
+ success = hdy_tab_view_select_previous_page (self->view);
+ } else if (direction == GTK_DIR_TAB_FORWARD) {
+ if (last)
+ success = hdy_tab_view_select_last_page (self->view);
+ else
+ success = hdy_tab_view_select_next_page (self->view);
+ }
+
+ if (!success)
+ gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+static void
+reorder_tab_cb (HdyTabBox *self,
+ GtkDirectionType direction,
+ gboolean last)
+{
+ gboolean is_rtl, success = last;
+
+ if (!self->view || !self->selected_tab || !self->selected_tab->page)
+ return;
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+ 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 = hdy_tab_view_reorder_first (self->view, self->selected_tab->page);
+ else
+ success = hdy_tab_view_reorder_backward (self->view, self->selected_tab->page);
+ } else if (direction == GTK_DIR_TAB_FORWARD) {
+ if (last)
+ success = hdy_tab_view_reorder_last (self->view, self->selected_tab->page);
+ else
+ success = hdy_tab_view_reorder_forward (self->view, self->selected_tab->page);
+ }
+
+ if (!success)
+ gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+/* Scrolling */
+
+static void
+update_needs_attention (HdyTabBox *self)
+{
+ gboolean left = FALSE, right = FALSE;
+ GList *l;
+ gdouble 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;
+ gint pos;
+
+ if (!info->page)
+ continue;
+
+ if (!hdy_tab_page_get_needs_attention (info->page))
+ continue;
+
+ pos = get_tab_position (self, info);
+
+ 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 gdouble
+get_scroll_animation_value (HdyTabBox *self)
+{
+ gdouble to, value;
+
+ g_assert (self->scroll_animation);
+
+ to = self->scroll_animation_offset;
+
+ if (self->scroll_animation_tab) {
+ gdouble lower, upper, page_size;
+
+ to += get_tab_position (self, self->scroll_animation_tab);
+
+ g_object_get (self->adjustment,
+ "lower", &lower,
+ "upper", &upper,
+ "page-size", &page_size,
+ NULL);
+
+ to = CLAMP (to, lower, upper - page_size);
+ }
+
+ value = hdy_animation_get_value (self->scroll_animation);
+
+ return round (hdy_lerp (self->scroll_animation_from, to, value));
+}
+
+static void
+adjustment_value_changed_cb (HdyTabBox *self)
+{
+ gdouble value = gtk_adjustment_get_value (self->adjustment);
+
+ self->hover_x += (value - self->adjustment_prev_value);
+
+ update_hover (self);
+ update_needs_attention (self);
+
+ self->adjustment_prev_value = value;
+
+ if (self->block_scrolling)
+ return;
+
+ if (self->scroll_animation)
+ hdy_animation_stop (self->scroll_animation);
+}
+
+static void
+scroll_animation_value_cb (gdouble value,
+ gpointer user_data)
+{
+ gtk_widget_queue_resize (GTK_WIDGET (user_data));
+}
+
+static void
+scroll_animation_done_cb (gpointer user_data)
+{
+ HdyTabBox *self = HDY_TAB_BOX (user_data);
+
+ self->scroll_animation_done = TRUE;
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+animate_scroll (HdyTabBox *self,
+ TabInfo *info,
+ gdouble offset,
+ gint64 duration)
+{
+ if (!self->adjustment)
+ return;
+
+ g_signal_emit (self, signals[SIGNAL_STOP_KINETIC_SCROLLING], 0);
+
+ if (self->scroll_animation)
+ hdy_animation_stop (self->scroll_animation);
+
+ g_clear_object (&self->scroll_animation);
+ 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 =
+ hdy_animation_new (GTK_WIDGET (self), 0, 1, duration,
+ scroll_animation_value_cb,
+ scroll_animation_done_cb,
+ self);
+
+ hdy_animation_start (self->scroll_animation);
+}
+
+static void
+animate_scroll_relative (HdyTabBox *self,
+ gdouble delta,
+ gint64 duration)
+{
+ gdouble 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_with_pos (HdyTabBox *self,
+ TabInfo *info,
+ gint pos,
+ gint64 duration)
+{
+ gint tab_width;
+ gdouble padding, value, page_size;
+
+ if (!self->adjustment)
+ return;
+
+ tab_width = info->width;
+
+ if (info->appear_animation)
+ tab_width = hdy_tab_get_display_width (info->tab);
+
+ value = gtk_adjustment_get_value (self->adjustment);
+ page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ padding = MIN (info->width, page_size - tab_width) / 2.0;
+
+ 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 (HdyTabBox *self,
+ TabInfo *info,
+ gint64 duration)
+{
+ scroll_to_tab_with_pos (self, info, get_tab_position (self, info), duration);
+}
+
+/* Reordering */
+
+static void
+force_end_reordering (HdyTabBox *self)
+{
+ GList *l;
+
+ if (self->dragging || !self->reordered_tab)
+ return;
+
+ if (self->reorder_animation)
+ hdy_animation_stop (self->reorder_animation);
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ if (info->reorder_animation)
+ hdy_animation_stop (info->reorder_animation);
+ }
+}
+
+static void
+check_end_reordering (HdyTabBox *self)
+{
+ gboolean should_focus;
+ GtkWidget *tab_widget;
+ 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;
+ }
+
+ tab_widget = GTK_WIDGET (self->reordered_tab->tab);
+
+ should_focus = gtk_widget_has_visible_focus (tab_widget);
+
+ gtk_widget_set_child_visible (tab_widget, FALSE);
+ gtk_widget_unrealize (tab_widget);
+ gtk_widget_set_parent_window (tab_widget, self->window);
+ gtk_widget_set_child_visible (tab_widget, TRUE);
+ gtk_widget_set_has_tooltip (tab_widget, TRUE);
+
+ self->reordered_tab->reorder_ignore_bounds = FALSE;
+
+ if (should_focus)
+ gtk_widget_grab_focus (tab_widget);
+
+ gdk_window_hide (self->reorder_window);
+
+ 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 (HdyTabBox *self,
+ TabInfo *info)
+{
+ gboolean should_focus;
+ GtkWidget *tab_widget;
+
+ self->reordered_tab = info;
+
+ tab_widget = GTK_WIDGET (self->reordered_tab->tab);
+
+ should_focus = gtk_widget_has_visible_focus (tab_widget);
+
+ gtk_widget_set_has_tooltip (tab_widget, FALSE);
+ gtk_widget_set_child_visible (tab_widget, FALSE);
+ gtk_widget_unrealize (tab_widget);
+ gtk_widget_set_parent_window (tab_widget, self->reorder_window);
+ gtk_widget_set_child_visible (tab_widget, TRUE);
+
+ if (should_focus)
+ gtk_widget_grab_focus (tab_widget);
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+}
+
+static gint
+get_reorder_position (HdyTabBox *self)
+{
+ gint 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 (gdouble value,
+ gpointer user_data)
+{
+ TabInfo *dest_tab = user_data;
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (dest_tab->tab));
+ HdyTabBox *self = HDY_TAB_BOX (parent);
+ gboolean is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+ gdouble x1, x2;
+
+ x1 = get_reorder_position (self);
+ x2 = dest_tab->pos - calculate_tab_offset (self, dest_tab);
+
+ if (dest_tab->end_reorder_offset * (is_rtl ? 1 : -1) > 0)
+ x2 += dest_tab->width - self->reordered_tab->width;
+
+ self->reorder_window_x = (gint) round (hdy_lerp (x1, x2, value));
+
+ gdk_window_move_resize (self->reorder_window,
+ self->reorder_window_x,
+ 0,
+ self->reordered_tab->width,
+ gtk_widget_get_allocated_height (GTK_WIDGET (self)));
+
+ update_hover (self);
+ gtk_widget_queue_draw (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));
+ HdyTabBox *self = HDY_TAB_BOX (parent);
+
+ g_clear_object (&self->reorder_animation);
+ check_end_reordering (self);
+}
+
+static void
+animate_reordering (HdyTabBox *self,
+ TabInfo *dest_tab)
+{
+ if (self->reorder_animation)
+ hdy_animation_stop (self->reorder_animation);
+
+ self->reorder_animation =
+ hdy_animation_new (GTK_WIDGET (self), 0, 1,
+ REORDER_ANIMATION_DURATION,
+ reorder_animation_value_cb,
+ reorder_animation_done_cb,
+ dest_tab);
+
+ hdy_animation_start (self->reorder_animation);
+
+ check_end_reordering (self);
+}
+
+static void
+reorder_offset_animation_value_cb (gdouble 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));
+ HdyTabBox *self = HDY_TAB_BOX (parent);
+
+ g_clear_object (&info->reorder_animation);
+ check_end_reordering (self);
+}
+
+static void
+animate_reorder_offset (HdyTabBox *self,
+ TabInfo *info,
+ gdouble 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)
+ hdy_animation_stop (info->reorder_animation);
+
+ info->reorder_animation =
+ hdy_animation_new (GTK_WIDGET (self), info->reorder_offset, offset,
+ REORDER_ANIMATION_DURATION,
+ reorder_offset_animation_value_cb,
+ reorder_offset_animation_done_cb,
+ info);
+
+ hdy_animation_start (info->reorder_animation);
+}
+
+static void
+reset_reorder_animations (HdyTabBox *self)
+{
+ guint i, original_index;
+ GList *l;
+
+ if (!hdy_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
+reorder_page (HdyTabBox *self,
+ HdyTabPage *page,
+ guint index)
+{
+ GList *link;
+ guint original_index;
+ TabInfo *info, *dest_tab;
+ gboolean is_rtl;
+
+ if (hdy_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);
+
+ gdk_window_show (self->reorder_window);
+
+ 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 -= hdy_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_with_pos (self, self->selected_tab, dest_tab->pos, REORDER_ANIMATION_DURATION);
+
+ 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 (hdy_get_enable_animations (GTK_WIDGET (self)) &&
+ gtk_widget_get_mapped (GTK_WIDGET (self))) {
+ guint 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
+prepare_drag_window (GdkSeat *seat,
+ GdkWindow *window,
+ gpointer user_data)
+{
+ gdk_window_show (window);
+}
+
+static void
+update_dragging (HdyTabBox *self)
+{
+ gboolean is_rtl, after_selected, found_index;
+ gint x;
+ guint i = 0;
+ GList *l;
+
+ if (!self->dragging)
+ return;
+
+ x = get_reorder_position (self);
+
+ gdk_window_move_resize (self->reorder_window,
+ x, 0,
+ self->reordered_tab->width,
+ gtk_widget_get_allocated_height (GTK_WIDGET (self)));
+
+ gtk_widget_queue_draw (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;
+ gint center = info->pos - calculate_tab_offset (self, info) + info->width / 2;
+ gdouble offset = 0;
+
+ if (x + self->reordered_tab->width > center &&
+ x < center &&
+ (!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 + self->reordered_tab->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,
+ HdyTabBox *self)
+{
+ gdouble value, lower, upper, page_size;
+ gdouble x, delta_ms, start_threshold, end_threshold, autoscroll_factor;
+ gint64 time;
+ gint offset = 0;
+
+ g_object_get (self->adjustment,
+ "value", &value,
+ "lower", &lower,
+ "upper", &upper,
+ "page-size", &page_size,
+ NULL);
+
+ x = CLAMP ((gdouble) self->reorder_x,
+ lower + AUTOSCROLL_AREA_WIDTH,
+ upper - self->reordered_tab->width - AUTOSCROLL_AREA_WIDTH);
+
+ time = gdk_frame_clock_get_frame_time (frame_clock);
+ delta_ms = (time - self->drag_autoscroll_prev_time) / 1000.0;
+
+ start_threshold = value + AUTOSCROLL_AREA_WIDTH;
+ end_threshold = value + page_size - self->reordered_tab->width - AUTOSCROLL_AREA_WIDTH;
+ autoscroll_factor = 0;
+
+ if (x < start_threshold)
+ autoscroll_factor = -(start_threshold - x) / AUTOSCROLL_AREA_WIDTH;
+ else if (x > end_threshold)
+ autoscroll_factor = (x - end_threshold) / AUTOSCROLL_AREA_WIDTH;
+
+ autoscroll_factor = CLAMP (autoscroll_factor, -1, 1);
+ autoscroll_factor = hdy_ease_in_cubic (autoscroll_factor);
+ self->drag_autoscroll_prev_time = time;
+
+ if (autoscroll_factor == 0)
+ return G_SOURCE_CONTINUE;
+
+ if (autoscroll_factor > 0)
+ offset = (gint) ceil (autoscroll_factor * delta_ms * AUTOSCROLL_SPEED);
+ else
+ offset = (gint) floor (autoscroll_factor * delta_ms * AUTOSCROLL_SPEED);
+
+ self->reorder_x += offset;
+ gtk_adjustment_set_value (self->adjustment, value + offset);
+ update_dragging (self);
+
+ return G_SOURCE_CONTINUE;
+}
+
+static void
+start_dragging (HdyTabBox *self,
+ GdkEvent *event,
+ TabInfo *info)
+{
+ if (self->dragging)
+ return;
+
+ if (!info)
+ return;
+
+ self->continue_reorder = info == self->reordered_tab;
+
+ if (self->continue_reorder) {
+ if (self->reorder_animation)
+ hdy_animation_stop (self->reorder_animation);
+
+ reset_reorder_animations (self);
+
+ self->reorder_x = (gint) round (self->hover_x - self->drag_offset_x);
+ self->reorder_y = (gint) round (self->hover_y - self->drag_offset_y);
+ } else
+ force_end_reordering (self);
+
+ if (self->adjustment) {
+ GdkFrameClock *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);
+ }
+
+ self->dragging = TRUE;
+
+ if (!self->continue_reorder)
+ start_reordering (self, info);
+
+ if (!self->indirect_reordering) {
+ GdkDevice *device = gdk_event_get_device (event);
+
+ self->drag_seat = gdk_device_get_seat (device);
+ gdk_seat_grab (self->drag_seat,
+ self->reorder_window,
+ GDK_SEAT_CAPABILITY_ALL,
+ FALSE,
+ NULL, // FIXME maybe use an actual cursor
+ event,
+ prepare_drag_window,
+ self);
+ }
+}
+
+static void
+end_dragging (HdyTabBox *self)
+{
+ TabInfo *dest_tab;
+
+ if (!self->dragging)
+ return;
+
+ self->dragging = FALSE;
+
+ 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;
+ }
+
+ dest_tab = g_list_nth_data (self->tabs, self->reorder_index);
+
+ if (!self->indirect_reordering) {
+ guint index;
+
+ gdk_seat_ungrab (self->drag_seat);
+ self->drag_seat = NULL;
+
+ index = self->reorder_index;
+
+ if (!self->pinned)
+ index += hdy_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, reorder_page, self);
+
+ hdy_tab_view_reorder_page (self->view, self->reordered_tab->page, index);
+
+ g_signal_handlers_unblock_by_func (self->view, reorder_page, self);
+ }
+
+ animate_reordering (self, dest_tab);
+}
+
+/* Selection */
+
+static void
+select_page (HdyTabBox *self,
+ HdyTabPage *page)
+{
+ if (!page) {
+ self->selected_tab = NULL;
+
+ gtk_container_set_focus_child (GTK_CONTAINER (self), NULL);
+
+ return;
+ }
+
+ self->selected_tab = find_info_for_page (self, page);
+
+ if (!self->selected_tab)
+ return;
+
+ if (hdy_tab_bar_tabs_have_visible_focus (self->tab_bar))
+ gtk_widget_grab_focus (GTK_WIDGET (self->selected_tab->tab));
+
+ gtk_container_set_focus_child (GTK_CONTAINER (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 void
+appear_animation_value_cb (gdouble value,
+ gpointer user_data)
+{
+ TabInfo *info = user_data;
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (info->tab));
+
+ info->appear_progress = value;
+ gtk_widget_queue_resize (parent);
+}
+
+static void
+open_animation_done_cb (gpointer user_data)
+{
+ TabInfo *info = user_data;
+
+ g_clear_object (&info->appear_animation);
+}
+
+static TabInfo *
+create_tab_info (HdyTabBox *self,
+ HdyTabPage *page)
+{
+ TabInfo *info;
+
+ info = g_new0 (TabInfo, 1);
+ info->page = page;
+ info->pos = -1;
+ info->width = -1;
+ info->tab = hdy_tab_new (self->view, self->pinned, FALSE);
+
+ hdy_tab_set_page (info->tab, page);
+
+ gtk_widget_set_parent (GTK_WIDGET (info->tab), GTK_WIDGET (self));
+
+ if (self->window)
+ gtk_widget_set_parent_window (GTK_WIDGET (info->tab), self->window);
+
+ return info;
+}
+
+static void
+add_page (HdyTabBox *self,
+ HdyTabPage *page,
+ guint position)
+{
+ TabInfo *info;
+ GList *l;
+
+ if (hdy_tab_page_get_pinned (page) != self->pinned)
+ return;
+
+ if (!self->pinned)
+ position -= hdy_tab_view_get_n_pinned_pages (self->view);
+
+ force_end_reordering (self);
+
+ info = create_tab_info (self, page);
+
+ gtk_widget_show (GTK_WIDGET (info->tab));
+
+ if (!self->pinned)
+ info->notify_needs_attention_id =
+ g_signal_connect_object (page,
+ "notify::needs-attention",
+ G_CALLBACK (update_needs_attention),
+ self,
+ G_CONNECT_SWAPPED);
+
+ info->appear_animation =
+ hdy_animation_new (GTK_WIDGET (self), 0, 1,
+ OPEN_ANIMATION_DURATION,
+ 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++;
+
+ hdy_animation_start (info->appear_animation);
+
+ if (page == hdy_tab_view_get_selected_page (self->view))
+ hdy_tab_box_select_page (self, page);
+}
+
+/* Closing */
+
+static void
+close_animation_done_cb (gpointer user_data)
+{
+ TabInfo *info = user_data;
+ GtkWidget *parent = gtk_widget_get_parent (GTK_WIDGET (info->tab));
+ HdyTabBox *self = HDY_TAB_BOX (parent);
+
+ g_clear_object (&info->appear_animation);
+
+ self->tabs = g_list_remove (self->tabs, info);
+
+ if (info == self->hovered_tab)
+ self->hovered_tab = NULL;
+
+ remove_and_free_tab_info (info);
+
+ self->n_tabs--;
+}
+
+static void
+remove_page (HdyTabBox *self,
+ HdyTabPage *page)
+{
+ TabInfo *info;
+ GList *page_link;
+
+ page_link = find_link_for_page (self, page);
+
+ if (!page_link)
+ return;
+
+ info = page_link->data;
+
+ force_end_reordering (self);
+
+ if (self->hovering) {
+ gboolean is_last = TRUE;
+ GList *l;
+
+ for (l = page_link; l; l = l->next) {
+ TabInfo *i = l->data;
+
+ if (l != page_link && i->page)
+ is_last = FALSE;
+ }
+
+ if (is_last && self->tab_resize_mode != TAB_RESIZE_NORMAL)
+ set_tab_resize_mode (self, TAB_RESIZE_FIXED_END_PADDING);
+ else if (!is_last)
+ set_tab_resize_mode (self, TAB_RESIZE_FIXED_TAB_WIDTH);
+ }
+
+ g_assert (info->page);
+
+ if (gtk_widget_is_focus (GTK_WIDGET (info->tab)))
+ hdy_tab_box_try_focus_selected_tab (self);
+
+ if (info == self->selected_tab)
+ hdy_tab_box_select_page (self, NULL);
+
+ hdy_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;
+
+ info->appear_animation =
+ hdy_animation_new (GTK_WIDGET (self), info->appear_progress, 0,
+ CLOSE_ANIMATION_DURATION,
+ appear_animation_value_cb,
+ close_animation_done_cb,
+ info);
+
+ hdy_animation_start (info->appear_animation);
+}
+
+/* DND */
+
+static gboolean
+check_dnd_threshold (HdyTabBox *self)
+{
+ gint threshold;
+ GtkAllocation alloc;
+
+ g_object_get (gtk_settings_get_default (),
+ "gtk-dnd-drag-threshold", &threshold,
+ NULL);
+
+ threshold *= DND_THRESHOLD_MULTIPLIER;
+
+ gtk_widget_get_allocation (GTK_WIDGET (self), &alloc);
+
+ alloc.x -= threshold;
+ alloc.y -= threshold;
+ alloc.width += 2 * threshold;
+ alloc.height += 2 * threshold;
+
+ return self->hover_x < alloc.x - threshold ||
+ self->hover_y < alloc.y - threshold ||
+ self->hover_x > alloc.x + alloc.width + threshold ||
+ self->hover_y > alloc.x + alloc.height + threshold;
+}
+
+static gint
+calculate_placeholder_index (HdyTabBox *self,
+ gint x)
+{
+ gint 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);
+
+ if ((x <= end && !is_rtl) || (x >= end && is_rtl))
+ break;
+
+ pos += tab_width + (is_rtl ? OVERLAP : -OVERLAP);
+ i++;
+ }
+
+ return i;
+}
+
+static void
+insert_placeholder (HdyTabBox *self,
+ HdyTabPage *page,
+ gint pos)
+{
+ gdouble initial_progress = 0;
+ TabInfo *info = self->reorder_placeholder;
+
+ if (info) {
+ initial_progress = info->appear_progress;
+
+ if (info->appear_animation)
+ hdy_animation_stop (info->appear_animation);
+ } else {
+ gint index;
+
+ self->placeholder_page = page;
+
+ info = create_tab_info (self, page);
+
+ info->reorder_ignore_bounds = TRUE;
+
+ index = calculate_placeholder_index (self, pos);
+
+ 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);
+ }
+
+ info->appear_animation =
+ hdy_animation_new (GTK_WIDGET (self), initial_progress, 1,
+ OPEN_ANIMATION_DURATION,
+ appear_animation_value_cb,
+ open_animation_done_cb,
+ info);
+
+ hdy_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));
+ HdyTabBox *self = HDY_TAB_BOX (parent);
+
+ g_clear_object (&info->appear_animation);
+ self->reorder_placeholder = NULL;
+ self->can_remove_placeholder = TRUE;
+}
+
+static void
+replace_placeholder (HdyTabBox *self,
+ HdyTabPage *page)
+{
+ TabInfo *info = self->reorder_placeholder;
+ gdouble initial_progress;
+
+ gtk_widget_show (GTK_WIDGET (self->reorder_placeholder->tab));
+
+ if (!info->appear_animation) {
+ self->reorder_placeholder = NULL;
+
+ return;
+ }
+
+ initial_progress = info->appear_progress;
+
+ self->can_remove_placeholder = FALSE;
+
+ hdy_tab_set_page (info->tab, page);
+ info->page = page;
+
+ hdy_animation_stop (info->appear_animation);
+
+ info->appear_animation =
+ hdy_animation_new (GTK_WIDGET (self), initial_progress, 1,
+ OPEN_ANIMATION_DURATION,
+ appear_animation_value_cb,
+ replace_animation_done_cb,
+ info);
+
+ hdy_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));
+ HdyTabBox *self = HDY_TAB_BOX (parent);
+
+ if (!self->can_remove_placeholder) {
+ hdy_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)
+ hdy_animation_stop (info->reorder_animation);
+
+ self->reordered_tab = NULL;
+ }
+
+ self->tabs = g_list_remove (self->tabs, info);
+
+ remove_and_free_tab_info (info);
+
+ self->n_tabs--;
+
+ self->reorder_placeholder = NULL;
+}
+
+static void
+remove_placeholder (HdyTabBox *self)
+{
+ TabInfo *info = self->reorder_placeholder;
+
+ if (!info || !info->page)
+ return;
+
+ hdy_tab_set_page (info->tab, NULL);
+ info->page = NULL;
+
+ if (info->appear_animation)
+ hdy_animation_stop (info->appear_animation);
+
+ info->appear_animation =
+ hdy_animation_new (GTK_WIDGET (self), info->appear_progress, 0,
+ CLOSE_ANIMATION_DURATION,
+ appear_animation_value_cb,
+ remove_animation_done_cb,
+ info);
+
+ hdy_animation_start (info->appear_animation);
+}
+
+static HdyTabBox *
+get_source_tab_box (GdkDragContext *context)
+{
+ GtkWidget *source = gtk_drag_get_source_widget (context);
+
+ if (!source || !HDY_IS_TAB_BOX (source))
+ return NULL;
+
+ return HDY_TAB_BOX (source);
+}
+
+static gboolean
+do_drag_drop (HdyTabBox *self,
+ GdkDragContext *context,
+ guint time)
+{
+ GdkAtom target, tab_target;
+ HdyTabBox *source_tab_box;
+ HdyTabPage *page;
+
+ target = gtk_drag_dest_find_target (GTK_WIDGET (self), context, NULL);
+ tab_target = gdk_atom_intern_static_string ("HDY_TAB");
+
+ if (target != tab_target)
+ return GDK_EVENT_PROPAGATE;
+
+ source_tab_box = get_source_tab_box (context);
+
+ if (!source_tab_box)
+ return GDK_EVENT_PROPAGATE;
+
+ page = source_tab_box->detached_page;
+
+ if (self->reorder_placeholder) {
+ replace_placeholder (self, page);
+ end_dragging (self);
+
+ g_signal_handlers_block_by_func (self->view, add_page, self);
+
+ hdy_tab_view_attach_page (self->view, page, self->reorder_index);
+
+ g_signal_handlers_unblock_by_func (self->view, add_page, self);
+ } else {
+ hdy_tab_view_attach_page (self->view, page, self->reorder_index);
+ }
+
+ source_tab_box->detached_page = NULL;
+
+ self->indirect_reordering = FALSE;
+ gtk_drag_finish (context, TRUE, FALSE, time);
+
+ return GDK_EVENT_STOP;
+}
+
+static void
+detach_into_new_window (HdyTabBox *self,
+ GdkDragContext *context)
+{
+ HdyTabBox *source_tab_box;
+ HdyTabPage *page;
+ HdyTabView *new_view;
+
+ source_tab_box = get_source_tab_box (context);
+
+ if (!source_tab_box)
+ return;
+
+ page = source_tab_box->detached_page;
+
+ // FIXME
+ g_signal_emit_by_name (source_tab_box->view, "create-window", &new_view);
+
+ if (!HDY_IS_TAB_VIEW (new_view))
+ g_error ("Cannot detach into new window");
+
+ hdy_tab_view_attach_page (new_view, page, 0);
+
+ self->should_detach_into_new_window = FALSE;
+}
+
+static gboolean
+is_view_in_the_same_group (HdyTabBox *self,
+ HdyTabView *other_view)
+{
+ GSList *group = hdy_tab_view_get_group (self->view);
+
+ return g_slist_index (group, other_view) >= 0;
+}
+
+static gboolean
+view_drag_drop_cb (HdyTabBox *self,
+ GdkDragContext *context,
+ gint x,
+ gint y,
+ guint time)
+{
+ HdyTabBox *source_tab_box;
+
+ if (self->pinned)
+ return GDK_EVENT_PROPAGATE;
+
+ source_tab_box = get_source_tab_box (context);
+
+ 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 = hdy_tab_view_get_n_pages (self->view) -
+ hdy_tab_view_get_n_pinned_pages (self->view);
+
+ return do_drag_drop (self, context, time);
+}
+
+static void
+create_drag_icon (HdyTabBox *self,
+ GdkDragContext *context)
+{
+ DragIcon *icon;
+
+ icon = g_new0 (DragIcon, 1);
+
+ icon->window = gtk_window_new (GTK_WINDOW_POPUP);
+ icon->context = context;
+
+ icon->width = predict_tab_width (self, self->reordered_tab, FALSE);
+ icon->target_width = icon->width;
+
+ gtk_widget_set_app_paintable (icon->window, TRUE);
+ gtk_window_set_resizable (GTK_WINDOW (icon->window), FALSE);
+ gtk_window_set_decorated (GTK_WINDOW (icon->window), FALSE);
+
+ gtk_style_context_add_class (gtk_widget_get_style_context (icon->window),
+ "tab-drag-icon");
+
+ icon->tab = hdy_tab_new (self->view, FALSE, TRUE);
+ hdy_tab_set_page (icon->tab, self->reordered_tab->page);
+ gtk_widget_show (GTK_WIDGET (icon->tab));
+ gtk_widget_set_halign (GTK_WIDGET (icon->tab), GTK_ALIGN_START);
+
+ gtk_container_add (GTK_CONTAINER (icon->window), GTK_WIDGET (icon->tab));
+
+ gtk_style_context_get_margin (gtk_widget_get_style_context (GTK_WIDGET (icon->tab)),
+ gtk_widget_get_state_flags (GTK_WIDGET (icon->tab)),
+ &icon->tab_margin);
+
+ hdy_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);
+
+ gtk_window_set_screen (GTK_WINDOW (icon->window),
+ gtk_widget_get_screen (GTK_WIDGET (self)));
+
+ icon->hotspot_x = (gint) self->drag_offset_x;
+ icon->hotspot_y = (gint) self->drag_offset_y;
+
+ gtk_drag_set_icon_widget (context, icon->window,
+ 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 (gdouble value,
+ gpointer user_data)
+{
+ DragIcon *icon = user_data;
+ gdouble relative_pos;
+
+ relative_pos = (gdouble) icon->hotspot_x / icon->width;
+
+ icon->width = (gint) round (value);
+
+ hdy_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 = (gint) round (icon->width * relative_pos);
+
+ gdk_drag_context_set_hotspot (icon->context,
+ icon->hotspot_x + icon->tab_margin.left,
+ icon->hotspot_y + icon->tab_margin.top);
+
+ gtk_widget_queue_resize (GTK_WIDGET (icon->window));
+}
+
+static void
+icon_resize_animation_done_cb (gpointer user_data)
+{
+ DragIcon *icon = user_data;
+
+ g_clear_object (&icon->resize_animation);
+}
+
+static void
+resize_drag_icon (HdyTabBox *self,
+ gint width)
+{
+ DragIcon *icon = self->drag_icon;
+
+ if (width == icon->target_width)
+ return;
+
+ if (icon->resize_animation)
+ hdy_animation_stop (icon->resize_animation);
+
+ icon->target_width = width;
+
+ icon->resize_animation =
+ hdy_animation_new (icon->window, icon->width, width,
+ ICON_RESIZE_ANIMATION_DURATION,
+ icon_resize_animation_value_cb,
+ icon_resize_animation_done_cb,
+ icon);
+
+ hdy_animation_start (icon->resize_animation);
+}
+
+/* Context menu */
+
+static void
+do_touch_popup (HdyTabBox *self,
+ TabInfo *info)
+{
+ GMenuModel *model = hdy_tab_view_get_menu_model (self->view);
+
+ if (!G_IS_MENU_MODEL (model))
+ return;
+
+ // FIXME
+ g_signal_emit_by_name (self->view, "setup-menu", info->page, NULL);
+
+ if (!self->touch_menu)
+ self->touch_menu = GTK_POPOVER (gtk_popover_new_from_model (GTK_WIDGET (info->tab), model));
+ else
+ gtk_popover_set_relative_to (self->touch_menu, GTK_WIDGET (info->tab));
+
+ gtk_popover_popup (self->touch_menu);
+}
+
+static void
+touch_menu_gesture_pressed (HdyTabBox *self)
+{
+ self->pressed = FALSE;
+ end_dragging (self);
+
+ do_touch_popup (self, self->hovered_tab);
+ gtk_gesture_set_state (self->touch_menu_gesture,
+ GTK_EVENT_SEQUENCE_CLAIMED);
+}
+
+static void
+popup_menu_detach (HdyTabBox *self,
+ GtkMenu *menu)
+{
+ self->context_menu = NULL;
+}
+
+static void
+popup_menu_deactivate_cb (HdyTabBox *self)
+{
+ self->hovering = FALSE;
+ update_hover (self);
+}
+
+static void
+do_popup (HdyTabBox *self,
+ TabInfo *info,
+ GdkEvent *event)
+{
+ GMenuModel *model = hdy_tab_view_get_menu_model (self->view);
+
+ if (!G_IS_MENU_MODEL (model))
+ return;
+
+ // FIXME
+ g_signal_emit_by_name (self->view, "setup-menu", info->page, NULL);
+
+ if (!self->context_menu) {
+ self->context_menu = GTK_MENU (gtk_menu_new_from_model (model));
+ gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self->context_menu)),
+ GTK_STYLE_CLASS_CONTEXT_MENU);
+
+ g_signal_connect_object (self->context_menu,
+ "deactivate",
+ G_CALLBACK (popup_menu_deactivate_cb),
+ self,
+ G_CONNECT_SWAPPED);
+
+ gtk_menu_attach_to_widget (self->context_menu, GTK_WIDGET (self),
+ (GtkMenuDetachFunc) popup_menu_detach);
+ }
+
+ if (event && gdk_event_triggers_context_menu (event))
+ gtk_menu_popup_at_pointer (self->context_menu, event);
+ else {
+ gtk_menu_popup_at_widget (self->context_menu,
+ hdy_tab_get_child (info->tab),
+ GDK_GRAVITY_SOUTH_WEST,
+ GDK_GRAVITY_NORTH_WEST,
+ event);
+
+ gtk_menu_shell_select_first (GTK_MENU_SHELL (self->context_menu), FALSE);
+ }
+}
+
+/* Overrides */
+
+static void
+hdy_tab_box_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+ gint 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) {
+ gint width = self->end_padding - OVERLAP;
+ GList *l;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ gint child_width = hdy_tab_get_child_min_width (info->tab);
+
+ width += calculate_tab_width (info, child_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;
+ gint child_min, child_nat;
+
+ gtk_widget_get_preferred_height (GTK_WIDGET (info->tab),
+ &child_min,
+ &child_nat);
+
+ if (child_min > min)
+ min = child_min;
+
+ if (child_nat > nat)
+ nat = child_nat;
+ }
+ }
+
+ hdy_css_measure (widget, orientation, &min, &nat);
+
+ if (minimum)
+ *minimum = min;
+
+ if (natural)
+ *natural = nat;
+
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+
+ if (natural_baseline)
+ *natural_baseline = -1;
+}
+
+static void
+hdy_tab_box_get_preferred_width (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_tab_box_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static void
+hdy_tab_box_get_preferred_height (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_tab_box_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static void
+hdy_tab_box_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_tab_box_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static void
+hdy_tab_box_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_tab_box_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static void
+hdy_tab_box_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+ gboolean is_rtl, should_scroll_to_selected;
+ GList *l;
+ GtkAllocation child_allocation;
+ gint pos;
+
+ is_rtl = gtk_widget_get_direction (widget) == GTK_TEXT_DIR_RTL;
+ should_scroll_to_selected = self->selected_tab && self->selected_tab->width < 0;
+
+ hdy_css_size_allocate_self (widget, allocation);
+
+ GTK_WIDGET_CLASS (hdy_tab_box_parent_class)->size_allocate (widget, allocation);
+
+ if (gtk_widget_get_realized (widget))
+ gdk_window_move_resize (self->window,
+ allocation->x, allocation->y,
+ allocation->width, allocation->height);
+
+ allocation->x = 0;
+ allocation->y = 0;
+ hdy_css_size_allocate_children (widget, allocation);
+
+ self->allocated_width = allocation->width;
+
+ if (!self->n_tabs)
+ return;
+
+ if (self->pinned) {
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+ gdouble min_width = hdy_tab_get_child_min_width (info->tab);
+
+ info->width = calculate_tab_width (info, min_width);
+ }
+ } else if (self->tab_resize_mode == TAB_RESIZE_FIXED_TAB_WIDTH) {
+ self->end_padding = allocation->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 {
+ gint tab_width = get_base_tab_width (self);
+ gint excess = allocation->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 = allocation->x + (is_rtl ? allocation->width + OVERLAP : -OVERLAP);
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ if (!info->appear_animation)
+ hdy_tab_set_display_width (info->tab, info->width);
+ else if (info->page)
+ hdy_tab_set_display_width (info->tab, predict_tab_width (self, info, FALSE));
+
+ info->pos = pos + calculate_tab_offset (self, info);
+
+ if (is_rtl)
+ info->pos -= info->width;
+
+ child_allocation.x = (info == self->reordered_tab) ? 0 : info->pos;
+ child_allocation.y = allocation->y;
+ child_allocation.width = info->width;
+ child_allocation.height = allocation->height;
+
+ gtk_widget_size_allocate (GTK_WIDGET (info->tab), &child_allocation);
+
+ pos += (is_rtl ? -1 : 1) * (info->width - OVERLAP);
+ }
+
+ if (should_scroll_to_selected)
+ scroll_to_tab (self, self->selected_tab, FOCUS_ANIMATION_DURATION);
+
+ if (self->scroll_animation) {
+ hdy_tab_box_set_block_scrolling (self, TRUE);
+ gtk_adjustment_set_value (self->adjustment,
+ get_scroll_animation_value (self));
+ hdy_tab_box_set_block_scrolling (self, FALSE);
+
+ if (self->scroll_animation_done) {
+ self->scroll_animation_done = FALSE;
+ self->scroll_animation_tab = NULL;
+ g_clear_object (&self->scroll_animation);
+ }
+ }
+
+ update_hover (self);
+ update_needs_attention (self);
+}
+
+static gboolean
+hdy_tab_box_focus (GtkWidget *widget,
+ GtkDirectionType direction)
+{
+ HdyTabBox *self = HDY_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
+hdy_tab_box_realize (GtkWidget *widget)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+ GtkAllocation allocation;
+ GdkWindowAttr attributes;
+ GdkWindowAttributesType attributes_mask;
+ GList *l;
+
+ gtk_widget_set_realized (widget, TRUE);
+
+ gtk_widget_get_allocation (widget, &allocation);
+
+ attributes.x = allocation.x;
+ attributes.y = allocation.y;
+ attributes.width = allocation.width;
+ attributes.height = allocation.height;
+ attributes.window_type = GDK_WINDOW_CHILD;
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes.visual = gtk_widget_get_visual (widget);
+ attributes.event_mask = gtk_widget_get_events (widget);
+ attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
+
+ self->window = gdk_window_new (gtk_widget_get_parent_window (widget),
+ &attributes,
+ attributes_mask);
+
+ gtk_widget_set_window (widget, self->window);
+ gtk_widget_register_window (widget, self->window);
+
+ self->reorder_window = gdk_window_new (self->window, &attributes, attributes_mask);
+ gtk_widget_register_window (widget, self->reorder_window);
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ gtk_widget_set_parent_window (GTK_WIDGET (info->tab), self->window);
+ }
+}
+
+static void
+hdy_tab_box_unrealize (GtkWidget *widget)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+
+ self->window = NULL;
+
+ if (self->reorder_window) {
+ gtk_widget_unregister_window (widget, self->reorder_window);
+ gdk_window_destroy (self->reorder_window);
+ self->reorder_window = NULL;
+ }
+
+ if (self->context_menu) {
+ gtk_widget_destroy (GTK_WIDGET (self->context_menu));
+ self->context_menu = NULL;
+ }
+
+ if (self->touch_menu) {
+ gtk_widget_destroy (GTK_WIDGET (self->touch_menu));
+ self->touch_menu = NULL;
+ }
+
+ GTK_WIDGET_CLASS (hdy_tab_box_parent_class)->unrealize (widget);
+}
+
+static void
+hdy_tab_box_map (GtkWidget *widget)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+
+ GTK_WIDGET_CLASS (hdy_tab_box_parent_class)->map (widget);
+
+ gdk_window_show_unraised (self->window);
+
+ if (self->reordered_tab)
+ gdk_window_show (self->reorder_window);
+}
+
+static void
+hdy_tab_box_unmap (GtkWidget *widget)
+{
+ HdyTabBox *self = HDY_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;
+ }
+
+ if (self->reordered_tab)
+ gdk_window_hide (self->reorder_window);
+
+ self->hovering = FALSE;
+ update_hover (self);
+
+ gdk_window_hide (self->window);
+
+ GTK_WIDGET_CLASS (hdy_tab_box_parent_class)->unmap (widget);
+}
+
+static void
+hdy_tab_box_direction_changed (GtkWidget *widget,
+ GtkTextDirection previous_direction)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+ gdouble upper, page_size;
+
+ if (!self->adjustment)
+ return;
+
+ if (gtk_widget_get_direction (widget) == previous_direction)
+ return;
+
+ upper = gtk_adjustment_get_value (self->adjustment);
+ page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ gtk_adjustment_set_value (self->adjustment,
+ upper - page_size - self->adjustment_prev_value);
+}
+
+static gboolean
+hdy_tab_box_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+
+ if (!self->n_tabs)
+ return GDK_EVENT_PROPAGATE;
+
+ hdy_css_draw (widget, cr);
+
+ return GTK_WIDGET_CLASS (hdy_tab_box_parent_class)->draw (widget, cr);
+}
+
+static gboolean
+hdy_tab_box_popup_menu (GtkWidget *widget)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+
+ if (self->selected_tab && self->selected_tab->page) {
+ do_popup (self, self->selected_tab, NULL);
+
+ return GDK_EVENT_STOP;
+ }
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+hdy_tab_box_enter_notify_event (GtkWidget *widget,
+ GdkEventCrossing *event)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+
+ if (event->window != self->window || event->detail == GDK_NOTIFY_INFERIOR)
+ return GDK_EVENT_PROPAGATE;
+
+ self->hovering = TRUE;
+
+ get_widget_coordinates (self, (GdkEvent *) event, &self->hover_x, &self->hover_y);
+ update_hover (self);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+hdy_tab_box_leave_notify_event (GtkWidget *widget,
+ GdkEventCrossing *event)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+
+ if (event->window != self->window || event->detail == GDK_NOTIFY_INFERIOR)
+ return GDK_EVENT_PROPAGATE;
+
+ self->hovering = FALSE;
+ update_hover (self);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+hdy_tab_box_motion_notify_event (GtkWidget *widget,
+ GdkEventMotion *event)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+
+ self->hovering = TRUE;
+
+ get_widget_coordinates (self, (GdkEvent *) event, &self->hover_x, &self->hover_y);
+
+ update_hover (self);
+
+ if (!self->pressed)
+ return GDK_EVENT_PROPAGATE;
+
+ if (self->hovered_tab &&
+ gtk_drag_check_threshold (widget,
+ (gint) self->drag_begin_x,
+ (gint) self->drag_begin_y,
+ (gint) self->hover_x,
+ (gint) self->hover_y))
+ start_dragging (self, (GdkEvent *) event, self->hovered_tab);
+
+ if (!self->dragging)
+ return GDK_EVENT_PROPAGATE;
+
+ self->reorder_x = (gint) round (self->hover_x - self->drag_offset_x);
+ self->reorder_y = (gint) round (self->hover_y - self->drag_offset_y);
+
+ if (!self->pinned &&
+ self->hovered_tab &&
+ self->hovered_tab != self->reorder_placeholder &&
+ self->hovered_tab->page &&
+ hdy_tab_view_get_n_pages (self->view) > 1 &&
+ check_dnd_threshold (self)) {
+ gtk_drag_begin_with_coordinates (widget,
+ self->source_targets,
+ GDK_ACTION_MOVE,
+ (gint) self->pressed_button,
+ (GdkEvent *) event,
+ self->reorder_x,
+ self->reorder_y);
+
+ return GDK_EVENT_STOP;
+ }
+
+ update_dragging (self);
+
+ return GDK_EVENT_STOP;
+}
+
+static gboolean
+hdy_tab_box_button_press_event (GtkWidget *widget,
+ GdkEventButton *event)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+ gboolean can_grab_focus;
+
+ get_widget_coordinates (self, (GdkEvent *) event, &self->hover_x, &self->hover_y);
+
+ update_hover (self);
+
+ if (!self->hovered_tab || !self->hovered_tab->page)
+ return GDK_EVENT_PROPAGATE;
+
+ if (gdk_event_triggers_context_menu ((GdkEvent *) event)) {
+ do_popup (self, self->hovered_tab, (GdkEvent *) event);
+
+ return GDK_EVENT_STOP;
+ }
+
+ self->pressed_button = event->button;
+
+ if (self->pressed_button == GDK_BUTTON_MIDDLE && !self->pinned) {
+ hdy_tab_view_close_page (self->view, self->hovered_tab->page);
+
+ return GDK_EVENT_STOP;
+ }
+
+ if (self->pressed_button != GDK_BUTTON_PRIMARY)
+ return GDK_EVENT_PROPAGATE;
+
+ if (self->adjustment) {
+ gint pos = get_tab_position (self, self->hovered_tab);
+ gdouble value = gtk_adjustment_get_value (self->adjustment);
+ gdouble page_size = gtk_adjustment_get_page_size (self->adjustment);
+
+ if (pos + OVERLAP < value ||
+ pos + self->hovered_tab->width - OVERLAP > value + page_size) {
+ scroll_to_tab (self, self->hovered_tab, SCROLL_ANIMATION_DURATION);
+
+ return GDK_EVENT_STOP;
+ }
+ }
+
+ can_grab_focus = hdy_tab_bar_tabs_have_visible_focus (self->tab_bar);
+
+ if (self->hovered_tab == self->selected_tab)
+ can_grab_focus = TRUE;
+ else
+ hdy_tab_view_set_selected_page (self->view, self->hovered_tab->page);
+
+ if (can_grab_focus)
+ gtk_widget_grab_focus (GTK_WIDGET (self->hovered_tab->tab));
+ else
+ activate_tab (self);
+
+ self->pressed = TRUE;
+ self->drag_begin_x = self->hover_x;
+ self->drag_begin_y = self->hover_y;
+ self->drag_offset_x = self->drag_begin_x - get_tab_position (self, self->hovered_tab);
+ self->drag_offset_y = self->drag_begin_y;
+
+ if (!self->reorder_animation) {
+ self->reorder_x = (gint) round (self->hover_x - self->drag_offset_x);
+ self->reorder_y = (gint) round (self->hover_y - self->drag_offset_y);
+ }
+
+ return GDK_EVENT_STOP;
+}
+
+static gboolean
+hdy_tab_box_button_release_event (GtkWidget *widget,
+ GdkEventButton *event)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+
+ self->pressed = FALSE;
+ self->pressed_button = 0;
+
+ end_dragging (self);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+hdy_tab_box_scroll_event (GtkWidget *widget,
+ GdkEventScroll *event)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+ gdouble page_size, pow_unit, scroll_unit;
+ GdkDevice *source_device;
+ GdkInputSource input_source;
+ gdouble dx, dy;
+
+ if (!self->adjustment)
+ return GDK_EVENT_PROPAGATE;
+
+ source_device = gdk_event_get_source_device ((GdkEvent *) event);
+ input_source = gdk_device_get_source (source_device);
+
+ if (input_source != GDK_SOURCE_MOUSE)
+ return GDK_EVENT_PROPAGATE;
+
+ if (!gdk_event_get_scroll_deltas ((GdkEvent *) event, &dx, &dy)) {
+ switch (event->direction) {
+ case GDK_SCROLL_UP:
+ dy = -1;
+ break;
+
+ case GDK_SCROLL_DOWN:
+ dy = 1;
+ break;
+
+ case GDK_SCROLL_LEFT:
+ dx = -1;
+ break;
+
+ case GDK_SCROLL_RIGHT:
+ dx = 1;
+ break;
+
+ case GDK_SCROLL_SMOOTH:
+ default:
+ g_assert_not_reached ();
+ }
+ }
+
+ if (dx != 0)
+ 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 (widget) == GTK_TEXT_DIR_RTL)
+ dy = -dy;
+
+ animate_scroll_relative (self, dy * scroll_unit, SCROLL_ANIMATION_DURATION);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+hdy_tab_box_drag_begin (GtkWidget *widget,
+ GdkDragContext *context)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+ TabInfo *detached_tab;
+
+ if (self->pinned)
+ return;
+
+ create_drag_icon (self, context);
+
+ self->hovering = TRUE;
+ self->pressed = FALSE;
+ self->pressed_button = 0;
+
+ detached_tab = self->reordered_tab;
+ self->detached_page = detached_tab->page;
+
+ self->indirect_reordering = TRUE;
+
+ end_dragging (self);
+ update_hover (self);
+
+ gtk_widget_hide (GTK_WIDGET (detached_tab->tab));
+ self->detached_index = g_list_index (self->tabs, detached_tab);
+
+ hdy_tab_view_start_drag (self->view);
+ hdy_tab_view_detach_page (self->view, self->detached_page);
+
+ self->indirect_reordering = FALSE;
+}
+
+static void
+hdy_tab_box_drag_end (GtkWidget *widget,
+ GdkDragContext *context)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+
+ if (self->pinned)
+ return;
+
+ if (self->should_detach_into_new_window)
+ detach_into_new_window (self, context);
+
+ self->detached_page = NULL;
+
+ gtk_widget_destroy (self->drag_icon->window);
+ g_clear_pointer (&self->drag_icon, g_free);
+
+ hdy_tab_view_end_drag (self->view);
+}
+
+static gboolean
+hdy_tab_box_drag_motion (GtkWidget *widget,
+ GdkDragContext *context,
+ gint x,
+ gint y,
+ guint time)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+ HdyTabBox *source_tab_box;
+ GdkAtom target, tab_target;
+ gdouble center, display_width;
+
+ if (self->pinned)
+ return GDK_EVENT_PROPAGATE;
+
+ target = gtk_drag_dest_find_target (GTK_WIDGET (self), context, NULL);
+ tab_target = gdk_atom_intern_static_string ("HDY_TAB");
+
+ if (target != tab_target)
+ return GDK_EVENT_PROPAGATE;
+
+ source_tab_box = get_source_tab_box (context);
+
+ 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;
+
+ center = x - source_tab_box->drag_icon->hotspot_x + source_tab_box->drag_icon->width / 2;
+
+ self->can_remove_placeholder = FALSE;
+
+ if (!self->reorder_placeholder || !self->reorder_placeholder->page) {
+ HdyTabPage *page = source_tab_box->detached_page;
+
+ insert_placeholder (self, page, center);
+
+ self->indirect_reordering = TRUE;
+
+ self->drag_offset_x = source_tab_box->drag_icon->hotspot_x;
+ self->drag_offset_y = source_tab_box->drag_icon->hotspot_y;
+
+ display_width = hdy_tab_get_display_width (self->reorder_placeholder->tab);
+ self->reorder_x = (gint) round (center - display_width / 2);
+
+ start_dragging (self, gtk_get_current_event (), self->reorder_placeholder);
+
+ resize_drag_icon (source_tab_box, predict_tab_width (self, self->reorder_placeholder, TRUE));
+
+ gdk_drag_status (context, GDK_ACTION_MOVE, time);
+
+ return GDK_EVENT_PROPAGATE;
+ }
+
+ display_width = hdy_tab_get_display_width (self->reorder_placeholder->tab);
+ self->reorder_x = (gint) round (center - display_width / 2);
+
+ update_dragging (self);
+
+ gdk_drag_status (context, GDK_ACTION_MOVE, time);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+hdy_tab_box_drag_leave (GtkWidget *widget,
+ GdkDragContext *context,
+ guint time)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+ HdyTabBox *source_tab_box;
+ GdkAtom target, tab_target;
+
+ if (self->pinned)
+ return;
+
+ source_tab_box = get_source_tab_box (context);
+
+ if (!source_tab_box)
+ return;
+
+ if (!self->view || !is_view_in_the_same_group (self, source_tab_box->view))
+ return;
+
+ target = gtk_drag_dest_find_target (GTK_WIDGET (self), context, NULL);
+ tab_target = gdk_atom_intern_static_string ("HDY_TAB");
+
+ if (target != tab_target)
+ return;
+
+ self->can_remove_placeholder = TRUE;
+
+ end_dragging (self);
+ remove_placeholder (self);
+
+ self->indirect_reordering = FALSE;
+}
+
+static gboolean
+hdy_tab_box_drag_drop (GtkWidget *widget,
+ GdkDragContext *context,
+ gint x,
+ gint y,
+ guint time)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+ HdyTabBox *source_tab_box;
+
+ if (self->pinned)
+ return GDK_EVENT_PROPAGATE;
+
+ source_tab_box = get_source_tab_box (context);
+
+ 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;
+
+ return do_drag_drop (self, context, time);
+}
+
+static gboolean
+hdy_tab_box_drag_failed (GtkWidget *widget,
+ GdkDragContext *context,
+ GtkDragResult result)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+
+ self->should_detach_into_new_window = FALSE;
+
+ if (result == GTK_DRAG_RESULT_NO_TARGET) {
+ detach_into_new_window (self, context);
+
+ return GDK_EVENT_STOP;
+ }
+
+ hdy_tab_view_attach_page (self->view,
+ self->detached_page,
+ self->detached_index);
+
+ self->indirect_reordering = FALSE;
+
+ return GDK_EVENT_STOP;
+}
+
+static void
+hdy_tab_box_drag_data_get (GtkWidget *widget,
+ GdkDragContext *context,
+ GtkSelectionData *data,
+ guint info,
+ guint time)
+{
+ HdyTabBox *self = HDY_TAB_BOX (widget);
+ GdkAtom target, rootwindow_target;
+
+ target = gtk_selection_data_get_target (data);
+ rootwindow_target = gdk_atom_intern_static_string ("application/x-rootwindow-drop");
+
+ if (target == rootwindow_target) {
+ self->should_detach_into_new_window = TRUE;
+ gtk_selection_data_set (data, target, 8, NULL, 0);
+ }
+}
+
+static void
+hdy_tab_box_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyTabBox *self = HDY_TAB_BOX (container);
+ GList *l;
+
+ if (!include_internals)
+ return;
+
+ for (l = self->tabs; l; l = l->next) {
+ TabInfo *info = l->data;
+
+ callback (GTK_WIDGET (info->tab), callback_data);
+ }
+}
+
+static void
+hdy_tab_box_dispose (GObject *object)
+{
+ HdyTabBox *self = HDY_TAB_BOX (object);
+
+ self->tab_bar = NULL;
+ hdy_tab_box_set_view (self, NULL);
+ hdy_tab_box_set_adjustment (self, NULL);
+
+ G_OBJECT_CLASS (hdy_tab_box_parent_class)->dispose (object);
+}
+
+static void
+hdy_tab_box_finalize (GObject *object)
+{
+ HdyTabBox *self = (HdyTabBox *) object;
+
+ g_clear_object (&self->touch_menu_gesture);
+
+ G_OBJECT_CLASS (hdy_tab_box_parent_class)->finalize (object);
+}
+
+static void
+hdy_tab_box_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyTabBox *self = HDY_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_ADJUSTMENT:
+ g_value_set_object (value, self->adjustment);
+ 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;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_tab_box_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyTabBox *self = HDY_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:
+ hdy_tab_box_set_view (self, g_value_get_object (value));
+ break;
+
+ case PROP_ADJUSTMENT:
+ hdy_tab_box_set_adjustment (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_tab_box_class_init (HdyTabBoxClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+ GtkBindingSet *binding_set;
+
+ object_class->dispose = hdy_tab_box_dispose;
+ object_class->finalize = hdy_tab_box_finalize;
+ object_class->get_property = hdy_tab_box_get_property;
+ object_class->set_property = hdy_tab_box_set_property;
+
+ widget_class->get_preferred_width = hdy_tab_box_get_preferred_width;
+ widget_class->get_preferred_height = hdy_tab_box_get_preferred_height;
+ widget_class->get_preferred_width_for_height = hdy_tab_box_get_preferred_width_for_height;
+ widget_class->get_preferred_height_for_width = hdy_tab_box_get_preferred_height_for_width;
+ widget_class->size_allocate = hdy_tab_box_size_allocate;
+ widget_class->focus = hdy_tab_box_focus;
+ widget_class->realize = hdy_tab_box_realize;
+ widget_class->unrealize = hdy_tab_box_unrealize;
+ widget_class->map = hdy_tab_box_map;
+ widget_class->unmap = hdy_tab_box_unmap;
+ widget_class->direction_changed = hdy_tab_box_direction_changed;
+ widget_class->draw = hdy_tab_box_draw;
+ widget_class->popup_menu = hdy_tab_box_popup_menu;
+ widget_class->enter_notify_event = hdy_tab_box_enter_notify_event;
+ widget_class->leave_notify_event = hdy_tab_box_leave_notify_event;
+ widget_class->motion_notify_event = hdy_tab_box_motion_notify_event;
+ widget_class->button_press_event = hdy_tab_box_button_press_event;
+ widget_class->button_release_event = hdy_tab_box_button_release_event;
+ widget_class->scroll_event = hdy_tab_box_scroll_event;
+ widget_class->drag_begin = hdy_tab_box_drag_begin;
+ widget_class->drag_end = hdy_tab_box_drag_end;
+ widget_class->drag_motion = hdy_tab_box_drag_motion;
+ widget_class->drag_leave = hdy_tab_box_drag_leave;
+ widget_class->drag_drop = hdy_tab_box_drag_drop;
+ widget_class->drag_failed = hdy_tab_box_drag_failed;
+ widget_class->drag_data_get = hdy_tab_box_drag_data_get;
+
+ container_class->forall = hdy_tab_box_forall;
+
+ 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"),
+ HDY_TYPE_TAB_BAR,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ props[PROP_VIEW] =
+ g_param_spec_object ("view",
+ _("View"),
+ _("View"),
+ HDY_TYPE_TAB_VIEW,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_ADJUSTMENT] =
+ g_param_spec_object ("adjustment",
+ _("Adjustment"),
+ _("Adjustment"),
+ GTK_TYPE_ADJUSTMENT,
+ 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);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ 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_ACTIVATE_TAB] =
+ g_signal_new ("activate-tab",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 0);
+
+ signals[SIGNAL_FOCUS_TAB] =
+ g_signal_new ("focus-tab",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 2, GTK_TYPE_DIRECTION_TYPE, G_TYPE_BOOLEAN);
+
+ signals[SIGNAL_REORDER_TAB] =
+ g_signal_new ("reorder-tab",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 2, GTK_TYPE_DIRECTION_TYPE, G_TYPE_BOOLEAN);
+
+ binding_set = gtk_binding_set_by_class (klass);
+
+ gtk_binding_entry_add_signal (binding_set, GDK_KEY_space, 0, "activate-tab", 0);
+ gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Space, 0, "activate-tab", 0);
+ gtk_binding_entry_add_signal (binding_set, GDK_KEY_Return, 0, "activate-tab", 0);
+ gtk_binding_entry_add_signal (binding_set, GDK_KEY_ISO_Enter, 0, "activate-tab", 0);
+ gtk_binding_entry_add_signal (binding_set, GDK_KEY_KP_Enter, 0, "activate-tab", 0);
+
+ add_focus_bindings (binding_set, GDK_KEY_Page_Up, GTK_DIR_TAB_BACKWARD, FALSE);
+ add_focus_bindings (binding_set, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD, FALSE);
+ add_focus_bindings (binding_set, GDK_KEY_Home, GTK_DIR_TAB_BACKWARD, TRUE);
+ add_focus_bindings (binding_set, GDK_KEY_End, GTK_DIR_TAB_FORWARD, TRUE);
+
+ add_reorder_bindings (binding_set, GDK_KEY_Left, GTK_DIR_LEFT, FALSE);
+ add_reorder_bindings (binding_set, GDK_KEY_Right, GTK_DIR_RIGHT, FALSE);
+ add_reorder_bindings (binding_set, GDK_KEY_Page_Up, GTK_DIR_TAB_BACKWARD, FALSE);
+ add_reorder_bindings (binding_set, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD, FALSE);
+ add_reorder_bindings (binding_set, GDK_KEY_Home, GTK_DIR_TAB_BACKWARD, TRUE);
+ add_reorder_bindings (binding_set, GDK_KEY_End, GTK_DIR_TAB_FORWARD, TRUE);
+
+ gtk_widget_class_set_css_name (widget_class, "tabbox");
+}
+
+static void
+hdy_tab_box_init (HdyTabBox *self)
+{
+ self->can_remove_placeholder = TRUE;
+
+ gtk_widget_add_events (GTK_WIDGET (self),
+ GDK_BUTTON_PRESS_MASK |
+ GDK_BUTTON_RELEASE_MASK |
+ GDK_BUTTON_MOTION_MASK |
+ GDK_POINTER_MOTION_MASK |
+ GDK_TOUCH_MASK |
+ GDK_ENTER_NOTIFY_MASK |
+ GDK_LEAVE_NOTIFY_MASK |
+ GDK_SCROLL_MASK |
+ GDK_SMOOTH_SCROLL_MASK);
+
+ self->touch_menu_gesture = g_object_new (GTK_TYPE_GESTURE_LONG_PRESS,
+ "widget", GTK_WIDGET (self),
+ "propagation-phase", GTK_PHASE_TARGET,
+ "touch-only", TRUE,
+ NULL);
+
+ g_signal_connect_object (self->touch_menu_gesture,
+ "pressed",
+ G_CALLBACK (touch_menu_gesture_pressed),
+ self,
+ G_CONNECT_SWAPPED);
+
+ gtk_drag_dest_set (GTK_WIDGET (self),
+ GTK_DEST_DEFAULT_MOTION,
+ dst_targets,
+ G_N_ELEMENTS (dst_targets),
+ GDK_ACTION_MOVE);
+
+ self->source_targets = gtk_target_list_new (src_targets,
+ G_N_ELEMENTS (src_targets));
+
+ g_signal_connect_object (self, "activate-tab", G_CALLBACK (activate_tab), self, G_CONNECT_AFTER);
+ g_signal_connect_object (self, "focus-tab", G_CALLBACK (focus_tab_cb), self, 0);
+ g_signal_connect_object (self, "reorder-tab", G_CALLBACK (reorder_tab_cb), self, 0);
+}
+
+void
+hdy_tab_box_set_view (HdyTabBox *self,
+ HdyTabView *view)
+{
+ g_return_if_fail (HDY_IS_TAB_BOX (self));
+ g_return_if_fail (HDY_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, add_page, self);
+ g_signal_handlers_disconnect_by_func (self->view, remove_page, self);
+ g_signal_handlers_disconnect_by_func (self->view, reorder_page, self);
+
+ if (!self->pinned)
+ g_signal_handlers_disconnect_by_func (self->view, view_drag_drop_cb, self);
+
+ g_list_free_full (self->tabs, (GDestroyNotify) remove_and_free_tab_info);
+ self->tabs = NULL;
+ self->n_tabs = 0;
+
+ gtk_widget_queue_allocate (GTK_WIDGET (self));
+ }
+
+ self->view = view;
+
+ if (self->view) {
+ int i, n_pages = hdy_tab_view_get_n_pages (self->view);
+
+ for (i = n_pages - 1; i >= 0; i--)
+ add_page (self, hdy_tab_view_get_nth_page (self->view, i), 0);
+
+ g_signal_connect_object (self->view, "page-added", G_CALLBACK (add_page), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->view, "page-removed", G_CALLBACK (remove_page), self, G_CONNECT_SWAPPED);
+ g_signal_connect_object (self->view, "page-reordered", G_CALLBACK (reorder_page), self,
G_CONNECT_SWAPPED);
+
+ if (!self->pinned)
+ g_signal_connect_object (self->view, "drag-drop", G_CALLBACK (view_drag_drop_cb), self,
G_CONNECT_SWAPPED);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_VIEW]);
+}
+
+void
+hdy_tab_box_set_adjustment (HdyTabBox *self,
+ GtkAdjustment *adjustment)
+{
+ g_return_if_fail (HDY_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_needs_attention, 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_needs_attention),
self, G_CONNECT_SWAPPED);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ADJUSTMENT]);
+}
+
+void
+hdy_tab_box_set_block_scrolling (HdyTabBox *self,
+ gboolean block_scrolling)
+{
+ self->block_scrolling = block_scrolling;
+}
+
+void
+hdy_tab_box_add_page (HdyTabBox *self,
+ HdyTabPage *page,
+ guint position)
+{
+ add_page (self, page, position);
+}
+
+void
+hdy_tab_box_remove_page (HdyTabBox *self,
+ HdyTabPage *page)
+{
+ remove_page (self, page);
+}
+
+void
+hdy_tab_box_select_page (HdyTabBox *self,
+ HdyTabPage *page)
+{
+ select_page (self, page);
+}
+
+void
+hdy_tab_box_try_focus_selected_tab (HdyTabBox *self)
+{
+ if (self->selected_tab)
+ gtk_widget_grab_focus (GTK_WIDGET (self->selected_tab->tab));
+}
+
+gboolean
+hdy_tab_box_is_page_focused (HdyTabBox *self,
+ HdyTabPage *page)
+{
+ TabInfo *info = find_info_for_page (self, page);
+
+ return info && gtk_widget_is_focus (GTK_WIDGET (info->tab));
+}
diff --git a/src/hdy-tab-private.h b/src/hdy-tab-private.h
new file mode 100644
index 00000000..0ee2c856
--- /dev/null
+++ b/src/hdy-tab-private.h
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include <gtk/gtk.h>
+#include "hdy-tab-view.h"
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_TAB (hdy_tab_get_type())
+
+G_DECLARE_FINAL_TYPE (HdyTab, hdy_tab, HDY, TAB, GtkContainer)
+
+HdyTab *hdy_tab_new (HdyTabView *view,
+ gboolean pinned,
+ gboolean dragging);
+
+void hdy_tab_set_page (HdyTab *self,
+ HdyTabPage *page);
+
+GtkWidget *hdy_tab_get_child (HdyTab *self);
+
+gint hdy_tab_get_child_min_width (HdyTab *self);
+
+gint hdy_tab_get_display_width (HdyTab *self);
+void hdy_tab_set_display_width (HdyTab *self,
+ gint width);
+
+gboolean hdy_tab_get_hovering (HdyTab *self);
+void hdy_tab_set_hovering (HdyTab *self,
+ gboolean hovering);
+
+gboolean hdy_tab_get_selected (HdyTab *self);
+void hdy_tab_set_selected (HdyTab *self,
+ gboolean selected);
+
+G_END_DECLS
diff --git a/src/hdy-tab-view-private.h b/src/hdy-tab-view-private.h
new file mode 100644
index 00000000..6ffce07e
--- /dev/null
+++ b/src/hdy-tab-view-private.h
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-tab-view.h"
+
+G_BEGIN_DECLS
+
+void hdy_tab_view_start_drag (HdyTabView *self);
+void hdy_tab_view_end_drag (HdyTabView *self);
+
+void hdy_tab_view_detach_page (HdyTabView *self,
+ HdyTabPage *page);
+void hdy_tab_view_attach_page (HdyTabView *self,
+ HdyTabPage *page,
+ guint position);
+
+G_END_DECLS
diff --git a/src/hdy-tab-view.c b/src/hdy-tab-view.c
new file mode 100644
index 00000000..f890097a
--- /dev/null
+++ b/src/hdy-tab-view.c
@@ -0,0 +1,1791 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-tab-view-private.h"
+
+static const GtkTargetEntry dst_targets [] = {
+ { "HDY_TAB", GTK_TARGET_SAME_APP, 0 },
+};
+
+struct _HdyTabPage
+{
+ GObject parent_instance;
+
+ GtkWidget *content;
+ gboolean selected;
+ gboolean pinned;
+ gchar *title;
+ gchar *tooltip;
+ GIcon *icon;
+ gboolean loading;
+ GIcon *secondary_icon;
+ gboolean needs_attention;
+};
+
+G_DEFINE_TYPE (HdyTabPage, hdy_tab_page, G_TYPE_OBJECT)
+
+enum {
+ PAGE_PROP_0,
+ PAGE_PROP_CONTENT,
+ PAGE_PROP_SELECTED,
+ PAGE_PROP_PINNED,
+ PAGE_PROP_TITLE,
+ PAGE_PROP_TOOLTIP,
+ PAGE_PROP_ICON,
+ PAGE_PROP_LOADING,
+ PAGE_PROP_SECONDARY_ICON,
+ PAGE_PROP_NEEDS_ATTENTION,
+ LAST_PAGE_PROP
+};
+
+static GParamSpec *page_props[LAST_PAGE_PROP];
+
+struct _HdyTabView
+{
+ GtkBin parent_instance;
+
+ GtkStack *stack;
+ GListStore *pages;
+
+ guint n_pages;
+ guint n_pinned_pages;
+ HdyTabPage *selected_page;
+ GIcon *default_icon;
+ GMenuModel *menu_model;
+
+ GSList *group;
+ gboolean is_dragging;
+};
+
+G_DEFINE_TYPE (HdyTabView, hdy_tab_view, GTK_TYPE_BIN)
+
+enum {
+ PROP_0,
+ PROP_N_PAGES,
+ PROP_N_PINNED_PAGES,
+ PROP_IS_DRAGGING,
+ PROP_SELECTED_PAGE,
+ PROP_DEFAULT_ICON,
+ PROP_MENU_MODEL,
+ PROP_GROUP,
+ LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+ SIGNAL_PAGE_ADDED,
+ SIGNAL_PAGE_REMOVED,
+ SIGNAL_PAGE_REORDERED,
+ SIGNAL_PAGE_PINNED,
+ SIGNAL_PAGE_UNPINNED,
+ SIGNAL_SETUP_MENU,
+ SIGNAL_CREATE_WINDOW,
+ SIGNAL_SELECT_PAGE,
+ SIGNAL_REORDER_PAGE,
+ SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+set_page_selected (HdyTabPage *self,
+ gboolean selected)
+{
+ g_return_if_fail (HDY_IS_TAB_PAGE (self));
+
+ selected = !!selected;
+
+ if (self->selected == selected)
+ return;
+
+ self->selected = selected;
+
+ g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_SELECTED]);
+}
+
+static void
+set_page_pinned (HdyTabPage *self,
+ gboolean pinned)
+{
+ g_return_if_fail (HDY_IS_TAB_PAGE (self));
+
+ pinned = !!pinned;
+
+ if (self->pinned == pinned)
+ return;
+
+ self->pinned = pinned;
+
+ g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_PINNED]);
+}
+
+static void
+hdy_tab_page_dispose (GObject *object)
+{
+ HdyTabPage *self = HDY_TAB_PAGE (object);
+
+ g_clear_object (&self->content);
+
+ G_OBJECT_CLASS (hdy_tab_page_parent_class)->dispose (object);
+}
+
+static void
+hdy_tab_page_finalize (GObject *object)
+{
+ HdyTabPage *self = (HdyTabPage *)object;
+
+ g_clear_pointer (&self->title, g_free);
+ g_clear_pointer (&self->tooltip, g_free);
+ g_clear_object (&self->icon);
+ g_clear_object (&self->secondary_icon);
+
+ G_OBJECT_CLASS (hdy_tab_page_parent_class)->finalize (object);
+}
+
+static void
+hdy_tab_page_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyTabPage *self = HDY_TAB_PAGE (object);
+
+ switch (prop_id) {
+ case PAGE_PROP_CONTENT:
+ g_value_set_object (value, hdy_tab_page_get_content (self));
+ break;
+
+ case PAGE_PROP_SELECTED:
+ g_value_set_boolean (value, hdy_tab_page_get_selected (self));
+ break;
+
+ case PAGE_PROP_PINNED:
+ g_value_set_boolean (value, hdy_tab_page_get_pinned (self));
+ break;
+
+ case PAGE_PROP_TITLE:
+ g_value_set_string (value, hdy_tab_page_get_title (self));
+ break;
+
+ case PAGE_PROP_TOOLTIP:
+ g_value_set_string (value, hdy_tab_page_get_tooltip (self));
+ break;
+
+ case PAGE_PROP_ICON:
+ g_value_set_object (value, hdy_tab_page_get_icon (self));
+ break;
+
+ case PAGE_PROP_LOADING:
+ g_value_set_boolean (value, hdy_tab_page_get_loading (self));
+ break;
+
+ case PAGE_PROP_SECONDARY_ICON:
+ g_value_set_object (value, hdy_tab_page_get_secondary_icon (self));
+ break;
+
+ case PAGE_PROP_NEEDS_ATTENTION:
+ g_value_set_boolean (value, hdy_tab_page_get_needs_attention (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_tab_page_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyTabPage *self = HDY_TAB_PAGE (object);
+
+ switch (prop_id) {
+ case PAGE_PROP_CONTENT:
+ g_set_object (&self->content, g_value_get_object (value));
+ break;
+
+ case PAGE_PROP_TITLE:
+ hdy_tab_page_set_title (self, g_value_get_string (value));
+ break;
+
+ case PAGE_PROP_TOOLTIP:
+ hdy_tab_page_set_tooltip (self, g_value_get_string (value));
+ break;
+
+ case PAGE_PROP_ICON:
+ hdy_tab_page_set_icon (self, g_value_get_object (value));
+ break;
+
+ case PAGE_PROP_LOADING:
+ hdy_tab_page_set_loading (self, g_value_get_boolean (value));
+ break;
+
+ case PAGE_PROP_SECONDARY_ICON:
+ hdy_tab_page_set_secondary_icon (self, g_value_get_object (value));
+ break;
+
+ case PAGE_PROP_NEEDS_ATTENTION:
+ hdy_tab_page_set_needs_attention (self, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_tab_page_class_init (HdyTabPageClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+ object_class->dispose = hdy_tab_page_dispose;
+ object_class->finalize = hdy_tab_page_finalize;
+ object_class->get_property = hdy_tab_page_get_property;
+ object_class->set_property = hdy_tab_page_set_property;
+
+ page_props[PAGE_PROP_CONTENT] =
+ g_param_spec_object ("content",
+ _("Content"),
+ _("Content"),
+ GTK_TYPE_WIDGET,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ page_props[PAGE_PROP_SELECTED] =
+ g_param_spec_boolean ("selected",
+ _("Selected"),
+ _("Selected"),
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ page_props[PAGE_PROP_PINNED] =
+ g_param_spec_boolean ("pinned",
+ _("Pinned"),
+ _("Pinned"),
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ page_props[PAGE_PROP_TITLE] =
+ g_param_spec_string ("title",
+ _("Title"),
+ _("Title"),
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ page_props[PAGE_PROP_TOOLTIP] =
+ g_param_spec_string ("tooltip",
+ _("Tooltip"),
+ _("Tooltip"),
+ NULL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ page_props[PAGE_PROP_ICON] =
+ g_param_spec_object ("icon",
+ _("Icon"),
+ _("Icon"),
+ G_TYPE_ICON,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ page_props[PAGE_PROP_LOADING] =
+ g_param_spec_boolean ("loading",
+ _("Loading"),
+ _("Loading"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ page_props[PAGE_PROP_SECONDARY_ICON] =
+ g_param_spec_object ("secondary-icon",
+ _("Secondary Icon"),
+ _("Secondary Icon"),
+ G_TYPE_ICON,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ page_props[PAGE_PROP_NEEDS_ATTENTION] =
+ g_param_spec_boolean ("needs-attention",
+ _("Needs Attention"),
+ _("Needs Attention"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PAGE_PROP, page_props);
+}
+
+static void
+hdy_tab_page_init (HdyTabPage *self)
+{
+}
+
+static gboolean
+object_handled_accumulator (GSignalInvocationHint *ihint,
+ GValue *return_accu,
+ const GValue *handler_return,
+ gpointer data)
+{
+ GObject *object = g_value_get_object (handler_return);
+
+ g_value_set_object (return_accu, object);
+
+ return !object;
+}
+
+static void
+set_is_dragging (HdyTabView *self,
+ gboolean is_dragging)
+{
+ if (is_dragging == self->is_dragging)
+ return;
+
+ self->is_dragging = is_dragging;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_IS_DRAGGING]);
+}
+
+static void
+set_n_pages (HdyTabView *self,
+ guint n_pages)
+{
+ if (n_pages == self->n_pages)
+ return;
+
+ self->n_pages = n_pages;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_PAGES]);
+}
+
+static void
+set_n_pinned_pages (HdyTabView *self,
+ guint n_pinned_pages)
+{
+ if (n_pinned_pages == self->n_pinned_pages)
+ return;
+
+ self->n_pinned_pages = n_pinned_pages;
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_N_PINNED_PAGES]);
+}
+
+static void
+check_close_window (HdyTabView *self)
+{
+ GtkWidget *toplevel;
+
+ if (self->n_pages > 0)
+ return;
+
+ toplevel = gtk_widget_get_toplevel (GTK_WIDGET (self));
+
+ if (!GTK_IS_WINDOW (toplevel))
+ return;
+
+ gtk_window_close (GTK_WINDOW (toplevel));
+}
+
+static void
+attach_page (HdyTabView *self,
+ HdyTabPage *page,
+ guint position)
+{
+ gboolean pinned = hdy_tab_page_get_pinned (page);
+ GtkWidget *content = hdy_tab_page_get_content (page);
+
+ if (!pinned)
+ position += self->n_pinned_pages;
+
+ g_list_store_insert (self->pages, position, page);
+
+ gtk_container_add (GTK_CONTAINER (self->stack), content);
+ gtk_container_child_set (GTK_CONTAINER (self->stack), content,
+ "position", position,
+ NULL);
+
+ set_n_pages (self, self->n_pages + 1);
+ if (pinned)
+ set_n_pinned_pages (self, self->n_pages + 1);
+
+ g_signal_emit (self, signals[SIGNAL_PAGE_ADDED], 0, page, position);
+}
+
+static void
+detach_page (HdyTabView *self,
+ HdyTabPage *page)
+{
+ guint pos = hdy_tab_view_get_page_position (self, page);
+
+ if (page == self->selected_page)
+ if (!hdy_tab_view_select_next_page (self))
+ hdy_tab_view_select_previous_page (self);
+
+ g_list_store_remove (self->pages, pos);
+ set_n_pages (self, self->n_pages - 1);
+
+ if (hdy_tab_page_get_pinned (page))
+ set_n_pinned_pages (self, self->n_pinned_pages - 1);
+
+ gtk_container_remove (GTK_CONTAINER (self->stack),
+ hdy_tab_page_get_content (page));
+
+ g_signal_emit (self, signals[SIGNAL_PAGE_REMOVED], 0, page);
+
+ check_close_window (self);
+}
+
+static HdyTabPage *
+insert_page (HdyTabView *self,
+ GtkWidget *content,
+ guint position,
+ gboolean pinned)
+{
+ HdyTabPage *page;
+
+ g_assert (position <= self->n_pages);
+
+ page = g_object_new (HDY_TYPE_TAB_PAGE, "content", content, NULL);
+
+ set_page_pinned (page, pinned);
+
+ attach_page (self, page, position);
+
+ if (!self->selected_page)
+ hdy_tab_view_set_selected_page (self, page);
+
+ return page;
+}
+
+static void
+set_group_from_view (HdyTabView *self,
+ HdyTabView *other_view)
+{
+ GSList *slist;
+
+ if (other_view)
+ slist = hdy_tab_view_get_group (other_view);
+ else
+ slist = NULL;
+
+ hdy_tab_view_set_group (self, slist);
+}
+
+static void
+close_page (HdyTabView *self,
+ HdyTabPage *page)
+{
+ detach_page (self, page);
+
+ g_object_unref (page);
+}
+
+static void
+add_select_bindings (GtkBindingSet *binding_set,
+ 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_binding_entry_add_signal (binding_set, keysym, GDK_CONTROL_MASK,
+ "select-page", 2,
+ GTK_TYPE_DIRECTION_TYPE, direction,
+ G_TYPE_BOOLEAN, last);
+ gtk_binding_entry_add_signal (binding_set, keypad_keysym, GDK_CONTROL_MASK,
+ "select-page", 2,
+ GTK_TYPE_DIRECTION_TYPE, direction,
+ G_TYPE_BOOLEAN, last);
+}
+
+static void
+add_reorder_bindings (GtkBindingSet *binding_set,
+ 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_binding_entry_add_signal (binding_set, keysym,
+ GDK_CONTROL_MASK | GDK_MOD1_MASK,
+ "reorder-page", 2,
+ GTK_TYPE_DIRECTION_TYPE, direction,
+ G_TYPE_BOOLEAN, last);
+ gtk_binding_entry_add_signal (binding_set, keypad_keysym,
+ GDK_CONTROL_MASK | GDK_MOD1_MASK,
+ "reorder-page", 2,
+ GTK_TYPE_DIRECTION_TYPE, direction,
+ G_TYPE_BOOLEAN, last);
+}
+
+static void
+select_page_cb (HdyTabView *self,
+ GtkDirectionType direction,
+ gboolean last)
+{
+ gboolean is_rtl, success = last;
+
+ if (!self->selected_page)
+ return;
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+ 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 = hdy_tab_view_select_first_page (self);
+ else
+ success = hdy_tab_view_select_previous_page (self);
+ } else if (direction == GTK_DIR_TAB_FORWARD) {
+ if (last)
+ success = hdy_tab_view_select_last_page (self);
+ else
+ success = hdy_tab_view_select_next_page (self);
+ }
+
+ gtk_widget_grab_focus (hdy_tab_page_get_content (self->selected_page));
+
+ if (!success)
+ gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+static void
+reorder_page_cb (HdyTabView *self,
+ GtkDirectionType direction,
+ gboolean last)
+{
+ gboolean is_rtl, success = last;
+
+ if (!self->selected_page)
+ return;
+
+ is_rtl = gtk_widget_get_direction (GTK_WIDGET (self)) == GTK_TEXT_DIR_RTL;
+
+ 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 = hdy_tab_view_reorder_first (self, self->selected_page);
+ else
+ success = hdy_tab_view_reorder_backward (self, self->selected_page);
+ } else if (direction == GTK_DIR_TAB_FORWARD) {
+ if (last)
+ success = hdy_tab_view_reorder_last (self, self->selected_page);
+ else
+ success = hdy_tab_view_reorder_forward (self, self->selected_page);
+ }
+
+ if (!success)
+ gtk_widget_error_bell (GTK_WIDGET (self));
+}
+
+static void
+hdy_tab_view_dispose (GObject *object)
+{
+ HdyTabView *self = HDY_TAB_VIEW (object);
+ GSList *l;
+
+ self->group = g_slist_remove (self->group, self);
+
+ for (l = self->group; l; l = l->next) {
+ HdyTabView *view = l->data;
+
+ view->group = self->group;
+ }
+
+ self->group = NULL;
+
+ if (self->pages) {
+ while (self->n_pages) {
+ HdyTabPage *page = hdy_tab_view_get_nth_page (self, 0);
+
+ close_page (self, page);
+
+ // FIXME why is this needed
+ g_object_unref (page);
+ }
+
+ g_clear_object (&self->pages);
+ }
+
+ G_OBJECT_CLASS (hdy_tab_view_parent_class)->dispose (object);
+}
+
+static void
+hdy_tab_view_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyTabView *self = HDY_TAB_VIEW (object);
+
+ switch (prop_id) {
+ case PROP_N_PAGES:
+ g_value_set_uint (value, hdy_tab_view_get_n_pages (self));
+ break;
+
+ case PROP_N_PINNED_PAGES:
+ g_value_set_uint (value, hdy_tab_view_get_n_pinned_pages (self));
+ break;
+
+ case PROP_IS_DRAGGING:
+ g_value_set_boolean (value, hdy_tab_view_get_is_dragging (self));
+ break;
+
+ case PROP_SELECTED_PAGE:
+ g_value_set_object (value, hdy_tab_view_get_selected_page (self));
+ break;
+
+ case PROP_DEFAULT_ICON:
+ g_value_set_object (value, hdy_tab_view_get_default_icon (self));
+ break;
+
+ case PROP_MENU_MODEL:
+ g_value_set_object (value, hdy_tab_view_get_menu_model (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_tab_view_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyTabView *self = HDY_TAB_VIEW (object);
+
+ switch (prop_id) {
+ case PROP_SELECTED_PAGE:
+ hdy_tab_view_set_selected_page (self, g_value_get_object (value));
+ break;
+
+ case PROP_DEFAULT_ICON:
+ hdy_tab_view_set_default_icon (self, g_value_get_object (value));
+ break;
+
+ case PROP_MENU_MODEL:
+ hdy_tab_view_set_menu_model (self, g_value_get_object (value));
+ break;
+
+ case PROP_GROUP:
+ set_group_from_view (self, g_value_get_object (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_tab_view_class_init (HdyTabViewClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkBindingSet *binding_set;
+
+ object_class->dispose = hdy_tab_view_dispose;
+ object_class->get_property = hdy_tab_view_get_property;
+ object_class->set_property = hdy_tab_view_set_property;
+
+ props[PROP_N_PAGES] =
+ g_param_spec_uint ("n-pages",
+ _("Number of Pages"),
+ _("Number of Pages"),
+ 0, G_MAXUINT, 0,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_N_PINNED_PAGES] =
+ g_param_spec_uint ("n-pinned-pages",
+ _("Number of Pinned Pages"),
+ _("Number of Pinned Pages"),
+ 0, G_MAXUINT, 0,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_IS_DRAGGING] =
+ g_param_spec_boolean ("is-dragging",
+ _("Is Dragging"),
+ _("Is Dragging"),
+ FALSE,
+ G_PARAM_READABLE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_SELECTED_PAGE] =
+ g_param_spec_object ("selected-page",
+ _("Selected Page"),
+ _("Selected Page"),
+ HDY_TYPE_TAB_PAGE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_DEFAULT_ICON] =
+ g_param_spec_object ("default-icon",
+ _("Default Icon"),
+ _("Default Icon"),
+ G_TYPE_ICON,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_MENU_MODEL] =
+ g_param_spec_object ("menu-model",
+ _("Menu Model"),
+ _("Menu Model"),
+ G_TYPE_MENU_MODEL,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_GROUP] =
+ g_param_spec_object ("group",
+ _("Group"),
+ _("Group"),
+ HDY_TYPE_TAB_VIEW,
+ G_PARAM_WRITABLE);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ signals[SIGNAL_PAGE_ADDED] =
+ g_signal_new ("page-added",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 2,
+ HDY_TYPE_TAB_PAGE, G_TYPE_UINT);
+
+ signals[SIGNAL_PAGE_REMOVED] =
+ g_signal_new ("page-removed",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ HDY_TYPE_TAB_PAGE);
+
+ signals[SIGNAL_PAGE_REORDERED] =
+ g_signal_new ("page-reordered",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 2,
+ HDY_TYPE_TAB_PAGE, G_TYPE_UINT);
+
+ signals[SIGNAL_PAGE_PINNED] =
+ g_signal_new ("page-pinned",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ HDY_TYPE_TAB_PAGE);
+
+ signals[SIGNAL_PAGE_UNPINNED] =
+ g_signal_new ("page-unpinned",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ HDY_TYPE_TAB_PAGE);
+
+ signals[SIGNAL_SETUP_MENU] =
+ g_signal_new ("setup-menu",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE,
+ 1,
+ HDY_TYPE_TAB_PAGE);
+
+ signals[SIGNAL_CREATE_WINDOW] =
+ g_signal_new ("create-window",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST,
+ 0,
+ object_handled_accumulator,
+ NULL, NULL,
+ HDY_TYPE_TAB_VIEW,
+ 0);
+
+ signals[SIGNAL_SELECT_PAGE] =
+ g_signal_new ("select-page",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 2,
+ GTK_TYPE_DIRECTION_TYPE, G_TYPE_BOOLEAN);
+
+ signals[SIGNAL_REORDER_PAGE] =
+ g_signal_new ("reorder-page",
+ G_TYPE_FROM_CLASS (klass),
+ G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION,
+ 0,
+ NULL, NULL, NULL,
+ G_TYPE_NONE, 2,
+ GTK_TYPE_DIRECTION_TYPE, G_TYPE_BOOLEAN);
+
+ binding_set = gtk_binding_set_by_class (klass);
+
+ add_select_bindings (binding_set, GDK_KEY_Page_Up, GTK_DIR_TAB_BACKWARD, FALSE);
+ add_select_bindings (binding_set, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD, FALSE);
+ add_select_bindings (binding_set, GDK_KEY_Home, GTK_DIR_TAB_BACKWARD, TRUE);
+ add_select_bindings (binding_set, GDK_KEY_End, GTK_DIR_TAB_FORWARD, TRUE);
+
+ add_reorder_bindings (binding_set, GDK_KEY_Page_Up, GTK_DIR_TAB_BACKWARD, FALSE);
+ add_reorder_bindings (binding_set, GDK_KEY_Page_Down, GTK_DIR_TAB_FORWARD, FALSE);
+ add_reorder_bindings (binding_set, GDK_KEY_Home, GTK_DIR_TAB_BACKWARD, TRUE);
+ add_reorder_bindings (binding_set, GDK_KEY_End, GTK_DIR_TAB_FORWARD, TRUE);
+
+ gtk_widget_class_set_css_name (widget_class, "tabview");
+}
+
+static void
+hdy_tab_view_init (HdyTabView *self)
+{
+ GtkWidget *overlay, *drag_shield;
+
+ self->pages = g_list_store_new (HDY_TYPE_TAB_PAGE);
+ self->group = g_slist_prepend (NULL, self);
+ self->default_icon = G_ICON (g_themed_icon_new ("hdy-tab-icon-missing-symbolic"));
+
+ overlay = gtk_overlay_new ();
+ gtk_widget_show (overlay);
+ gtk_container_add (GTK_CONTAINER (self), overlay);
+
+ self->stack = GTK_STACK (gtk_stack_new ());
+ gtk_widget_show (GTK_WIDGET (self->stack));
+ gtk_container_add (GTK_CONTAINER (overlay), GTK_WIDGET (self->stack));
+
+ drag_shield = gtk_event_box_new ();
+ gtk_widget_add_events (drag_shield, GDK_ALL_EVENTS_MASK);
+ gtk_overlay_add_overlay (GTK_OVERLAY (overlay), drag_shield);
+
+ g_object_bind_property (self, "is-dragging",
+ drag_shield, "visible",
+ G_BINDING_DEFAULT);
+
+ gtk_drag_dest_set (GTK_WIDGET (self),
+ GTK_DEST_DEFAULT_MOTION,
+ dst_targets,
+ G_N_ELEMENTS (dst_targets),
+ GDK_ACTION_MOVE);
+
+ g_signal_connect_object (self, "select-page", G_CALLBACK (select_page_cb), self, 0);
+ g_signal_connect_object (self, "reorder-page", G_CALLBACK (reorder_page_cb), self, 0);
+}
+
+GtkWidget *
+hdy_tab_page_get_content (HdyTabPage *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (self), NULL);
+
+ return self->content;
+}
+
+gboolean
+hdy_tab_page_get_selected (HdyTabPage *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (self), FALSE);
+
+ return self->selected;
+}
+
+gboolean
+hdy_tab_page_get_pinned (HdyTabPage *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (self), FALSE);
+
+ return self->pinned;
+}
+
+const gchar *
+hdy_tab_page_get_title (HdyTabPage *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (self), NULL);
+
+ return self->title;
+}
+
+void
+hdy_tab_page_set_title (HdyTabPage *self,
+ const gchar *title)
+{
+ g_return_if_fail (HDY_IS_TAB_PAGE (self));
+
+ if (!g_strcmp0 (title, self->title))
+ return;
+
+ if (self->title)
+ g_clear_pointer (&self->title, g_free);
+
+ if (title)
+ self->title = g_strdup (title);
+
+ g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_TITLE]);
+}
+
+const gchar *
+hdy_tab_page_get_tooltip (HdyTabPage *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (self), NULL);
+
+ return self->tooltip;
+}
+
+void
+hdy_tab_page_set_tooltip (HdyTabPage *self,
+ const gchar *tooltip)
+{
+ g_return_if_fail (HDY_IS_TAB_PAGE (self));
+
+ if (!g_strcmp0 (tooltip, self->tooltip))
+ return;
+
+ if (self->tooltip)
+ g_clear_pointer (&self->tooltip, g_free);
+
+ if (tooltip)
+ self->tooltip = g_strdup (tooltip);
+
+ g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_TOOLTIP]);
+}
+
+GIcon *
+hdy_tab_page_get_icon (HdyTabPage *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (self), NULL);
+
+ return self->icon;
+}
+
+void
+hdy_tab_page_set_icon (HdyTabPage *self,
+ GIcon *icon)
+{
+ g_return_if_fail (HDY_IS_TAB_PAGE (self));
+ g_return_if_fail (G_IS_ICON (icon) || icon == NULL);
+
+ if (self->icon == icon)
+ return;
+
+ g_set_object (&self->icon, icon);
+
+ g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_ICON]);
+}
+
+gboolean
+hdy_tab_page_get_loading (HdyTabPage *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (self), FALSE);
+
+ return self->loading;
+}
+
+void
+hdy_tab_page_set_loading (HdyTabPage *self,
+ gboolean loading)
+{
+ g_return_if_fail (HDY_IS_TAB_PAGE (self));
+
+ loading = !!loading;
+
+ if (self->loading == loading)
+ return;
+
+ self->loading = loading;
+
+ g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_LOADING]);
+}
+
+GIcon *
+hdy_tab_page_get_secondary_icon (HdyTabPage *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (self), NULL);
+
+ return self->secondary_icon;
+}
+
+void
+hdy_tab_page_set_secondary_icon (HdyTabPage *self,
+ GIcon *secondary_icon)
+{
+ g_return_if_fail (HDY_IS_TAB_PAGE (self));
+ g_return_if_fail (G_IS_ICON (secondary_icon) || secondary_icon == NULL);
+
+ if (self->secondary_icon == secondary_icon)
+ return;
+
+ g_set_object (&self->secondary_icon, secondary_icon);
+
+ g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_SECONDARY_ICON]);
+}
+
+gboolean
+hdy_tab_page_get_needs_attention (HdyTabPage *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (self), FALSE);
+
+ return self->needs_attention;
+}
+
+void
+hdy_tab_page_set_needs_attention (HdyTabPage *self,
+ gboolean needs_attention)
+{
+ g_return_if_fail (HDY_IS_TAB_PAGE (self));
+
+ needs_attention = !!needs_attention;
+
+ if (self->needs_attention == needs_attention)
+ return;
+
+ self->needs_attention = needs_attention;
+
+ g_object_notify_by_pspec (G_OBJECT (self), page_props[PAGE_PROP_NEEDS_ATTENTION]);
+}
+
+HdyTabView *
+hdy_tab_view_new (void)
+{
+ return g_object_new (HDY_TYPE_TAB_VIEW, NULL);
+}
+
+guint
+hdy_tab_view_get_n_pages (HdyTabView *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), 0);
+
+ return self->n_pages;
+}
+
+guint
+hdy_tab_view_get_n_pinned_pages (HdyTabView *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), 0);
+
+ return self->n_pinned_pages;
+}
+
+gboolean
+hdy_tab_view_get_is_dragging (HdyTabView *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+
+ return self->is_dragging;
+}
+
+void
+hdy_tab_view_start_drag (HdyTabView *self)
+{
+ GSList *l;
+
+ g_return_if_fail (HDY_IS_TAB_VIEW (self));
+
+ for (l = self->group; l; l = l->next) {
+ HdyTabView *view = l->data;
+
+ set_is_dragging (view, TRUE);
+ }
+}
+
+void
+hdy_tab_view_end_drag (HdyTabView *self)
+{
+ GSList *l;
+
+ g_return_if_fail (HDY_IS_TAB_VIEW (self));
+
+ for (l = self->group; l; l = l->next) {
+ HdyTabView *view = l->data;
+
+ set_is_dragging (view, FALSE);
+ }
+}
+
+HdyTabPage *
+hdy_tab_view_get_selected_page (HdyTabView *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+
+ return self->selected_page;
+}
+
+void
+hdy_tab_view_set_selected_page (HdyTabView *self,
+ HdyTabPage *selected_page)
+{
+ g_return_if_fail (HDY_IS_TAB_VIEW (self));
+ g_return_if_fail (HDY_IS_TAB_PAGE (selected_page));
+
+ if (self->selected_page == selected_page)
+ return;
+
+ if (self->selected_page)
+ set_page_selected (self->selected_page, FALSE);
+
+ self->selected_page = selected_page;
+ gtk_stack_set_visible_child (self->stack,
+ hdy_tab_page_get_content (selected_page));
+
+ if (self->selected_page)
+ set_page_selected (self->selected_page, TRUE);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED_PAGE]);
+}
+
+gboolean
+hdy_tab_view_select_previous_page (HdyTabView *self)
+{
+ HdyTabPage *page;
+ guint pos;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+
+ if (!self->selected_page)
+ return FALSE;
+
+ pos = hdy_tab_view_get_page_position (self, self->selected_page);
+
+ if (pos <= 0)
+ return FALSE;
+
+ page = hdy_tab_view_get_nth_page (self, pos - 1);
+
+ hdy_tab_view_set_selected_page (self, page);
+
+ return TRUE;
+}
+
+gboolean
+hdy_tab_view_select_next_page (HdyTabView *self)
+{
+ HdyTabPage *page;
+ guint pos;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+
+ if (!self->selected_page)
+ return FALSE;
+
+ pos = hdy_tab_view_get_page_position (self, self->selected_page);
+
+ if (pos + 1 >= self->n_pages)
+ return FALSE;
+
+ page = hdy_tab_view_get_nth_page (self, pos + 1);
+
+ hdy_tab_view_set_selected_page (self, page);
+
+ return TRUE;
+}
+
+gboolean
+hdy_tab_view_select_first_page (HdyTabView *self)
+{
+ HdyTabPage *page;
+ guint pos;
+ gboolean pinned;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+
+ if (!self->selected_page)
+ return FALSE;
+
+ pinned = hdy_tab_page_get_pinned (self->selected_page);
+ pos = pinned ? 0 : self->n_pinned_pages;
+
+ page = hdy_tab_view_get_nth_page (self, pos);
+
+ /* If we're on the first non-pinned tab, go to the first pinned tab */
+ if (page == self->selected_page && !pinned)
+ page = hdy_tab_view_get_nth_page (self, 0);
+
+ if (page == self->selected_page)
+ return FALSE;
+
+ hdy_tab_view_set_selected_page (self, page);
+
+ return TRUE;
+}
+
+gboolean
+hdy_tab_view_select_last_page (HdyTabView *self)
+{
+ HdyTabPage *page;
+ guint pos;
+ gboolean pinned;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+
+ if (!self->selected_page)
+ return FALSE;
+
+ pinned = hdy_tab_page_get_pinned (self->selected_page);
+ pos = (pinned ? self->n_pinned_pages : self->n_pages) - 1;
+
+ page = hdy_tab_view_get_nth_page (self, pos);
+
+ /* If we're on the last pinned tab, go to the last non-pinned tab */
+ if (page == self->selected_page && pinned)
+ page = hdy_tab_view_get_nth_page (self, self->n_pages - 1);
+
+ if (page == self->selected_page)
+ return FALSE;
+
+ hdy_tab_view_set_selected_page (self, page);
+
+ return TRUE;
+}
+
+GIcon *
+hdy_tab_view_get_default_icon (HdyTabView *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+
+ return self->default_icon;
+}
+
+void
+hdy_tab_view_set_default_icon (HdyTabView *self,
+ GIcon *default_icon)
+{
+ g_return_if_fail (HDY_IS_TAB_VIEW (self));
+ g_return_if_fail (G_IS_ICON (default_icon));
+
+ if (self->default_icon == default_icon)
+ return;
+
+ g_set_object (&self->default_icon, default_icon);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DEFAULT_ICON]);
+}
+
+GMenuModel *
+hdy_tab_view_get_menu_model (HdyTabView *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+
+ return self->menu_model;
+}
+
+void
+hdy_tab_view_set_menu_model (HdyTabView *self,
+ GMenuModel *menu_model)
+{
+ g_return_if_fail (HDY_IS_TAB_VIEW (self));
+ g_return_if_fail (G_IS_MENU_MODEL (menu_model));
+
+ if (self->menu_model == menu_model)
+ return;
+
+ g_set_object (&self->menu_model, menu_model);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MENU_MODEL]);
+}
+
+GSList *
+hdy_tab_view_get_group (HdyTabView *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+
+ return self->group;
+}
+
+void
+hdy_tab_view_set_group (HdyTabView *self,
+ GSList *group)
+{
+ g_return_if_fail (HDY_IS_TAB_VIEW (self));
+
+ if (g_slist_find (group, self))
+ return;
+
+ if (self->group) {
+ GSList *l;
+
+ self->group = g_slist_remove (self->group, self);
+
+ for (l = self->group; l; l = l->next) {
+ HdyTabView *view = l->data;
+
+ view->group = self->group;
+ }
+ }
+
+ self->group = g_slist_prepend (group, self);
+
+ if (group) {
+ GSList *l;
+
+ for (l = group; l; l = l->next) {
+ HdyTabView *view = l->data;
+
+ view->group = self->group;
+ }
+ }
+}
+
+void
+hdy_tab_view_join_group (HdyTabView *self,
+ HdyTabView *source)
+{
+ g_return_if_fail (HDY_IS_TAB_VIEW (self));
+ g_return_if_fail (source == NULL || HDY_IS_TAB_VIEW (source));
+
+ if (source) {
+ GSList *group = hdy_tab_view_get_group (source);
+
+ if (!group) {
+ hdy_tab_view_set_group (source, NULL);
+ group = hdy_tab_view_get_group (source);
+ }
+
+ hdy_tab_view_set_group (self, group);
+ } else
+ hdy_tab_view_set_group (self, NULL);
+}
+
+void
+hdy_tab_view_set_page_pinned (HdyTabView *self,
+ HdyTabPage *page,
+ gboolean pinned)
+{
+ guint pos;
+
+ g_return_if_fail (HDY_IS_TAB_VIEW (self));
+ g_return_if_fail (HDY_IS_TAB_PAGE (page));
+
+ pinned = !!pinned;
+
+ if (hdy_tab_page_get_pinned (page) == pinned)
+ return;
+
+ pos = hdy_tab_view_get_page_position (self, page);
+
+ g_list_store_remove (self->pages, pos);
+
+ pos = self->n_pinned_pages;
+
+ if (!pinned)
+ pos--;
+
+ g_list_store_insert (self->pages, pos, page);
+
+ set_page_pinned (page, pinned);
+
+ if (pinned)
+ pos++;
+
+ gtk_container_child_set (GTK_CONTAINER (self->stack),
+ hdy_tab_page_get_content (page),
+ "position", self->n_pinned_pages,
+ NULL);
+
+ set_n_pinned_pages (self, pos);
+
+ if (pinned)
+ g_signal_emit (self, signals[SIGNAL_PAGE_PINNED], 0, page);
+ else
+ g_signal_emit (self, signals[SIGNAL_PAGE_UNPINNED], 0, page);
+}
+
+HdyTabPage *
+hdy_tab_view_get_page (HdyTabView *self,
+ GtkWidget *content)
+{
+ guint i;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+ g_return_val_if_fail (GTK_IS_WIDGET (content), NULL);
+
+ for (i = 0; i < self->n_pages; i++) {
+ HdyTabPage *page = g_list_model_get_item (G_LIST_MODEL (self->pages), i);
+
+ if (hdy_tab_page_get_content (page) == content)
+ return page;
+ }
+
+ return NULL;
+}
+
+HdyTabPage *
+hdy_tab_view_get_nth_page (HdyTabView *self,
+ guint position)
+{
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+ g_return_val_if_fail (position <= self->n_pages, NULL);
+
+ return g_list_model_get_item (G_LIST_MODEL (self->pages), position);
+}
+
+guint
+hdy_tab_view_get_page_position (HdyTabView *self,
+ HdyTabPage *page)
+{
+ guint pos;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), 0);
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (page), 0);
+
+ g_return_val_if_fail (g_list_store_find (self->pages, page, &pos), 0);
+
+ return pos;
+}
+
+HdyTabPage *
+hdy_tab_view_insert (HdyTabView *self,
+ GtkWidget *content,
+ guint position)
+{
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+ g_return_val_if_fail (GTK_IS_WIDGET (content), NULL);
+ g_return_val_if_fail (position <= self->n_pages, NULL);
+
+ return insert_page (self, content, position, FALSE);
+}
+
+HdyTabPage *
+hdy_tab_view_prepend (HdyTabView *self,
+ GtkWidget *content)
+{
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+ g_return_val_if_fail (GTK_IS_WIDGET (content), NULL);
+
+ return insert_page (self, content, 0, FALSE);
+}
+
+HdyTabPage *
+hdy_tab_view_append (HdyTabView *self,
+ GtkWidget *content)
+{
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+ g_return_val_if_fail (GTK_IS_WIDGET (content), NULL);
+
+ return insert_page (self, content, self->n_pages - self->n_pinned_pages, FALSE);
+}
+
+HdyTabPage *
+hdy_tab_view_insert_pinned (HdyTabView *self,
+ GtkWidget *content,
+ guint position)
+{
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+ g_return_val_if_fail (GTK_IS_WIDGET (content), NULL);
+ g_return_val_if_fail (position <= self->n_pages, NULL);
+
+ return insert_page (self, content, position, TRUE);
+}
+
+HdyTabPage *
+hdy_tab_view_prepend_pinned (HdyTabView *self,
+ GtkWidget *content)
+{
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+ g_return_val_if_fail (GTK_IS_WIDGET (content), NULL);
+
+ return insert_page (self, content, 0, TRUE);
+}
+
+HdyTabPage *
+hdy_tab_view_append_pinned (HdyTabView *self,
+ GtkWidget *content)
+{
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+ g_return_val_if_fail (GTK_IS_WIDGET (content), NULL);
+
+ return insert_page (self, content, self->n_pinned_pages, TRUE);
+}
+
+gboolean
+hdy_tab_view_close_page (HdyTabView *self,
+ HdyTabPage *page)
+{
+ gboolean can_close;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+// g_signal_emit_by_name (page, "closing", &can_close);
+ can_close = TRUE;
+
+ if (can_close)
+ close_page (self, page);
+
+ return can_close;
+}
+
+gboolean
+hdy_tab_view_close_pages (HdyTabView *self,
+ GSList *pages)
+{
+ gboolean can_close = TRUE;
+ GSList *l;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+
+ if (!pages)
+ return TRUE;
+
+ for (l = pages; l; l = l->next) {
+ HdyTabPage *page = l->data;
+ gboolean can_close_tab;
+
+// g_signal_emit_by_name (page, "closing", &can_close_tab);
+ can_close_tab = TRUE;
+
+ can_close &= can_close_tab;
+ }
+
+ if (can_close)
+ for (l = pages; l; l = l->next) {
+ HdyTabPage *page = l->data;
+
+ close_page (self, page);
+ }
+
+ g_free (pages);
+
+ return can_close;
+}
+
+gboolean
+hdy_tab_view_close_other_pages (HdyTabView *self,
+ HdyTabPage *page)
+{
+ GSList *pages = NULL;
+ guint i;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+ for (i = self->n_pinned_pages; i < self->n_pages; i++) {
+ HdyTabPage *p = hdy_tab_view_get_nth_page (self, i);
+
+ if (p == page)
+ continue;
+
+ pages = g_slist_prepend (pages, p);
+ }
+
+ pages = g_slist_reverse (pages);
+
+ return hdy_tab_view_close_pages (self, pages);
+}
+
+gboolean
+hdy_tab_view_close_pages_before (HdyTabView *self,
+ HdyTabPage *page)
+{
+ GSList *pages = NULL;
+ guint pos, i;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+ pos = hdy_tab_view_get_page_position (self, page);
+
+ for (i = self->n_pinned_pages; i < pos; i++) {
+ HdyTabPage *p = hdy_tab_view_get_nth_page (self, i);
+
+ pages = g_slist_prepend (pages, p);
+ }
+
+ pages = g_slist_reverse (pages);
+
+ return hdy_tab_view_close_pages (self, pages);
+}
+
+gboolean
+hdy_tab_view_close_pages_after (HdyTabView *self,
+ HdyTabPage *page)
+{
+ GSList *pages = NULL;
+ guint pos, i;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+ pos = hdy_tab_view_get_page_position (self, page);
+
+ /* Skip pinned tabs */
+ pos = MAX (self->n_pinned_pages, pos);
+
+ for (i = self->n_pages - 1; i > pos; i--) {
+ HdyTabPage *p = hdy_tab_view_get_nth_page (self, i);
+
+ pages = g_slist_prepend (pages, p);
+ }
+
+ return hdy_tab_view_close_pages (self, pages);
+}
+
+gboolean
+hdy_tab_view_reorder_page (HdyTabView *self,
+ HdyTabPage *page,
+ guint position)
+{
+ guint original_pos, pinned;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+ pinned = hdy_tab_page_get_pinned (page);
+
+ g_return_val_if_fail (!pinned || position < self->n_pinned_pages, FALSE);
+ g_return_val_if_fail (pinned || position >= self->n_pinned_pages, FALSE);
+ g_return_val_if_fail (pinned || position < self->n_pages, FALSE);
+
+ original_pos = hdy_tab_view_get_page_position (self, page);
+
+ if (original_pos == position)
+ return FALSE;
+
+ g_list_store_remove (self->pages, original_pos);
+ g_list_store_insert (self->pages, position, page);
+
+ gtk_container_child_set (GTK_CONTAINER (self->stack),
+ hdy_tab_page_get_content (page),
+ "position", position,
+ NULL);
+
+ g_signal_emit (self, signals[SIGNAL_PAGE_REORDERED], 0, page, position);
+
+ return TRUE;
+}
+
+gboolean
+hdy_tab_view_reorder_backward (HdyTabView *self,
+ HdyTabPage *page)
+{
+ gboolean pinned;
+ guint pos, first;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+ pos = hdy_tab_view_get_page_position (self, page);
+
+ pinned = hdy_tab_page_get_pinned (page);
+ first = pinned ? 0 : self->n_pinned_pages;
+
+ if (pos <= first)
+ return FALSE;
+
+ return hdy_tab_view_reorder_page (self, page, pos - 1);
+}
+
+gboolean
+hdy_tab_view_reorder_forward (HdyTabView *self,
+ HdyTabPage *page)
+{
+ gboolean pinned;
+ guint pos, last;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+ pos = hdy_tab_view_get_page_position (self, page);
+
+ pinned = hdy_tab_page_get_pinned (page);
+ last = (pinned ? self->n_pinned_pages : self->n_pages) - 1;
+
+ if (pos >= last)
+ return FALSE;
+
+ return hdy_tab_view_reorder_page (self, page, pos + 1);
+}
+
+gboolean
+hdy_tab_view_reorder_first (HdyTabView *self,
+ HdyTabPage *page)
+{
+ gboolean pinned;
+ guint pos;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+ pinned = hdy_tab_page_get_pinned (page);
+ pos = pinned ? 0 : self->n_pinned_pages;
+
+ return hdy_tab_view_reorder_page (self, page, pos);
+}
+
+gboolean
+hdy_tab_view_reorder_last (HdyTabView *self,
+ HdyTabPage *page)
+{
+ gboolean pinned;
+ guint pos;
+
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), FALSE);
+ g_return_val_if_fail (HDY_IS_TAB_PAGE (page), FALSE);
+
+ pinned = hdy_tab_page_get_pinned (page);
+ pos = (pinned ? self->n_pinned_pages : self->n_pages) - 1;
+
+ return hdy_tab_view_reorder_page (self, page, pos);
+}
+
+void
+hdy_tab_view_detach_page (HdyTabView *self,
+ HdyTabPage *page)
+{
+ g_return_if_fail (HDY_IS_TAB_VIEW (self));
+ g_return_if_fail (HDY_IS_TAB_PAGE (page));
+
+ detach_page (self, page);
+}
+
+void
+hdy_tab_view_attach_page (HdyTabView *self,
+ HdyTabPage *page,
+ guint position)
+{
+ g_return_if_fail (HDY_IS_TAB_VIEW (self));
+ g_return_if_fail (HDY_IS_TAB_PAGE (page));
+ g_return_if_fail (position <= self->n_pages);
+
+ attach_page (self, page, position);
+
+ hdy_tab_view_set_selected_page (self, page);
+}
+
+void
+hdy_tab_view_transfer_page (HdyTabView *self,
+ HdyTabPage *page,
+ HdyTabView *other_view,
+ guint position)
+{
+ g_return_if_fail (HDY_IS_TAB_VIEW (self));
+ g_return_if_fail (HDY_IS_TAB_PAGE (page));
+ g_return_if_fail (HDY_IS_TAB_VIEW (other_view));
+ g_return_if_fail (position <= other_view->n_pages);
+
+ hdy_tab_view_detach_page (self, page);
+ hdy_tab_view_attach_page (other_view, page, position);
+}
+
+GListModel *
+hdy_tab_view_get_pages (HdyTabView *self)
+{
+ g_return_val_if_fail (HDY_IS_TAB_VIEW (self), NULL);
+
+ return G_LIST_MODEL (self->pages);
+}
+
diff --git a/src/hdy-tab-view.h b/src/hdy-tab-view.h
new file mode 100644
index 00000000..bf14fb3d
--- /dev/null
+++ b/src/hdy-tab-view.h
@@ -0,0 +1,202 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined(_HANDY_INSIDE) && !defined(HANDY_COMPILATION)
+#error "Only <handy.h> can be included directly."
+#endif
+
+#include "hdy-version.h"
+
+#include <gtk/gtk.h>
+
+G_BEGIN_DECLS
+
+#define HDY_TYPE_TAB_PAGE (hdy_tab_page_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyTabPage, hdy_tab_page, HDY, TAB_PAGE, GObject)
+
+HDY_AVAILABLE_IN_ALL
+GtkWidget *hdy_tab_page_get_content (HdyTabPage *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_page_get_selected (HdyTabPage *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_page_get_pinned (HdyTabPage *self);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_tab_page_get_title (HdyTabPage *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_page_set_title (HdyTabPage *self,
+ const gchar *title);
+
+HDY_AVAILABLE_IN_ALL
+const gchar *hdy_tab_page_get_tooltip (HdyTabPage *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_page_set_tooltip (HdyTabPage *self,
+ const gchar *title);
+
+HDY_AVAILABLE_IN_ALL
+GIcon *hdy_tab_page_get_icon (HdyTabPage *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_page_set_icon (HdyTabPage *self,
+ GIcon *icon);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_page_get_loading (HdyTabPage *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_page_set_loading (HdyTabPage *self,
+ gboolean loading);
+
+HDY_AVAILABLE_IN_ALL
+GIcon *hdy_tab_page_get_secondary_icon (HdyTabPage *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_page_set_secondary_icon (HdyTabPage *self,
+ GIcon *secondary_icon);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_page_get_needs_attention (HdyTabPage *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_page_set_needs_attention (HdyTabPage *self,
+ gboolean needs_attention);
+
+#define HDY_TYPE_TAB_VIEW (hdy_tab_view_get_type())
+
+HDY_AVAILABLE_IN_ALL
+G_DECLARE_FINAL_TYPE (HdyTabView, hdy_tab_view, HDY, TAB_VIEW, GtkBin)
+
+HDY_AVAILABLE_IN_ALL
+HdyTabView *hdy_tab_view_new (void);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_tab_view_get_n_pages (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+guint hdy_tab_view_get_n_pinned_pages (HdyTabView *self);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_get_is_dragging (HdyTabView *self);
+
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_get_selected_page (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_view_set_selected_page (HdyTabView *self,
+ HdyTabPage *selected_page);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_select_previous_page (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_select_next_page (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_select_first_page (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_select_last_page (HdyTabView *self);
+
+HDY_AVAILABLE_IN_ALL
+GIcon *hdy_tab_view_get_default_icon (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_view_set_default_icon (HdyTabView *self,
+ GIcon *default_icon);
+
+HDY_AVAILABLE_IN_ALL
+GMenuModel *hdy_tab_view_get_menu_model (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_view_set_menu_model (HdyTabView *self,
+ GMenuModel *menu_model);
+
+HDY_AVAILABLE_IN_ALL
+GSList *hdy_tab_view_get_group (HdyTabView *self);
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_view_set_group (HdyTabView *self,
+ GSList *group);
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_view_join_group (HdyTabView *self,
+ HdyTabView *source);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_view_set_page_pinned (HdyTabView *self,
+ HdyTabPage *page,
+ gboolean pinned);
+
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_get_page (HdyTabView *self,
+ GtkWidget *content);
+
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_get_nth_page (HdyTabView *self,
+ guint position);
+
+HDY_AVAILABLE_IN_ALL
+guint hdy_tab_view_get_page_position (HdyTabView *self,
+ HdyTabPage *page);
+
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_insert (HdyTabView *self,
+ GtkWidget *content,
+ guint position);
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_prepend (HdyTabView *self,
+ GtkWidget *content);
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_append (HdyTabView *self,
+ GtkWidget *content);
+
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_insert_pinned (HdyTabView *self,
+ GtkWidget *content,
+ guint position);
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_prepend_pinned (HdyTabView *self,
+ GtkWidget *content);
+HDY_AVAILABLE_IN_ALL
+HdyTabPage *hdy_tab_view_append_pinned (HdyTabView *self,
+ GtkWidget *content);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_close_page (HdyTabView *self,
+ HdyTabPage *page);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_close_pages (HdyTabView *self,
+ GSList *pages);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_close_other_pages (HdyTabView *self,
+ HdyTabPage *page);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_close_pages_before (HdyTabView *self,
+ HdyTabPage *page);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_close_pages_after (HdyTabView *self,
+ HdyTabPage *page);
+
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_reorder_page (HdyTabView *self,
+ HdyTabPage *page,
+ guint position);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_reorder_backward (HdyTabView *self,
+ HdyTabPage *page);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_reorder_forward (HdyTabView *self,
+ HdyTabPage *page);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_reorder_first (HdyTabView *self,
+ HdyTabPage *page);
+HDY_AVAILABLE_IN_ALL
+gboolean hdy_tab_view_reorder_last (HdyTabView *self,
+ HdyTabPage *page);
+
+HDY_AVAILABLE_IN_ALL
+void hdy_tab_view_transfer_page (HdyTabView *self,
+ HdyTabPage *page,
+ HdyTabView *other_view,
+ guint position);
+
+HDY_AVAILABLE_IN_ALL
+GListModel *hdy_tab_view_get_pages (HdyTabView *self);
+
+G_END_DECLS
diff --git a/src/hdy-tab.c b/src/hdy-tab.c
new file mode 100644
index 00000000..9f4fa2ec
--- /dev/null
+++ b/src/hdy-tab.c
@@ -0,0 +1,774 @@
+/*
+ * Copyright (C) 2020 Alexander Mikhaylenko <exalm7659 gmail com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-tab-private.h"
+#include "hdy-css-private.h"
+
+struct _HdyTab
+{
+ GtkContainer parent_instance;
+
+ GtkLabel *title;
+ GtkImage *icon;
+ GtkImage *pinned_icon;
+ GtkStack *stack;
+ GtkStack *icon_stack;
+ GtkStack *pinned_icon_stack;
+ GtkImage *secondary_icon;
+
+ GtkWidget *child;
+ GdkWindow *window;
+ gboolean draw_child;
+
+ HdyTabView *view;
+ HdyTabPage *page;
+ gboolean pinned;
+ gboolean dragging;
+ gint display_width;
+
+ gboolean hovering;
+ gboolean selected;
+
+ GBinding *selected_binding;
+ GBinding *title_binding;
+};
+
+G_DEFINE_TYPE (HdyTab, hdy_tab, GTK_TYPE_CONTAINER)
+
+enum {
+ PROP_0,
+ PROP_VIEW,
+ PROP_PINNED,
+ PROP_DRAGGING,
+ PROP_PAGE,
+ PROP_DISPLAY_WIDTH,
+ PROP_HOVERING,
+ PROP_SELECTED,
+ LAST_PROP
+};
+
+static GParamSpec *props[LAST_PROP];
+
+static void
+update_state (HdyTab *self)
+{
+ GtkStateFlags new_state;
+
+ new_state = gtk_widget_get_state_flags (GTK_WIDGET (self)) &
+ ~(GTK_STATE_FLAG_PRELIGHT | GTK_STATE_FLAG_CHECKED);
+
+ if (self->hovering)
+ new_state |= GTK_STATE_FLAG_PRELIGHT;
+
+ if (self->selected)
+ new_state |= GTK_STATE_FLAG_CHECKED;
+
+ gtk_widget_set_state_flags (GTK_WIDGET (self), new_state, TRUE);
+}
+
+static void
+update_tooltip (HdyTab *self)
+{
+ const gchar *tooltip = hdy_tab_page_get_tooltip (self->page);
+
+ if (tooltip)
+ gtk_widget_set_tooltip_markup (GTK_WIDGET (self), tooltip);
+ else
+ gtk_widget_set_tooltip_text (GTK_WIDGET (self),
+ hdy_tab_page_get_title (self->page));
+}
+
+static void
+update_icon (HdyTab *self)
+{
+ GIcon *gicon = hdy_tab_page_get_icon (self->page);
+ gboolean loading = hdy_tab_page_get_loading (self->page);
+ const gchar *name = loading ? "spinner" : "icon";
+
+ gtk_image_set_from_gicon (self->icon, gicon, GTK_ICON_SIZE_BUTTON);
+ gtk_widget_set_visible (GTK_WIDGET (self->icon_stack),
+ gicon != NULL || loading);
+ gtk_stack_set_visible_child_name (self->icon_stack, name);
+
+ if (gicon)
+ gtk_image_set_from_gicon (self->pinned_icon, gicon, GTK_ICON_SIZE_BUTTON);
+ else {
+ gicon = hdy_tab_view_get_default_icon (self->view);
+
+ gtk_image_set_from_gicon (self->pinned_icon, gicon, GTK_ICON_SIZE_BUTTON);
+ }
+
+ gtk_stack_set_visible_child_name (self->pinned_icon_stack, name);
+}
+
+static void
+update_secondary_icon (HdyTab *self)
+{
+ GIcon *gicon = hdy_tab_page_get_secondary_icon (self->page);
+
+ gtk_image_set_from_gicon (self->secondary_icon, gicon, GTK_ICON_SIZE_BUTTON);
+ gtk_widget_set_visible (GTK_WIDGET (self->secondary_icon), gicon != NULL);
+}
+
+static void
+update_needs_attention (HdyTab *self)
+{
+ GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+
+ if (hdy_tab_page_get_needs_attention (self->page))
+ gtk_style_context_add_class (context, "needs-attention");
+ else
+ gtk_style_context_remove_class (context, "needs-attention");
+}
+
+static void
+update_loading (HdyTab *self)
+{
+ GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ update_icon (self);
+
+ if (hdy_tab_page_get_loading (self->page))
+ gtk_style_context_add_class (context, "loading");
+ else
+ gtk_style_context_remove_class (context, "loading");
+}
+
+static gboolean
+close_idle_cb (HdyTab *self)
+{
+ hdy_tab_view_close_page (self->view, self->page);
+
+ return G_SOURCE_REMOVE;
+}
+
+static void
+close_clicked_cb (HdyTab *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
+hdy_tab_measure (GtkWidget *widget,
+ GtkOrientation orientation,
+ gint for_size,
+ gint *minimum,
+ gint *natural,
+ gint *minimum_baseline,
+ gint *natural_baseline)
+{
+ HdyTab *self = HDY_TAB (widget);
+
+ if (!self->child) {
+ if (minimum)
+ *minimum = 0;
+
+ if (natural)
+ *natural = 0;
+ } else if (orientation == GTK_ORIENTATION_HORIZONTAL) {
+ if (minimum)
+ *minimum = 0;
+
+ gtk_widget_get_preferred_width (self->child, NULL, natural);
+ } else {
+ gtk_widget_get_preferred_height (self->child, minimum, natural);
+ hdy_css_measure (widget, orientation, minimum, natural);
+ }
+
+ if (minimum_baseline)
+ *minimum_baseline = -1;
+
+ if (natural_baseline)
+ *natural_baseline = -1;
+}
+
+static void
+hdy_tab_get_preferred_width (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_tab_measure (widget, GTK_ORIENTATION_HORIZONTAL, -1,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static void
+hdy_tab_get_preferred_height (GtkWidget *widget,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_tab_measure (widget, GTK_ORIENTATION_VERTICAL, -1,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static void
+hdy_tab_get_preferred_width_for_height (GtkWidget *widget,
+ gint height,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_tab_measure (widget, GTK_ORIENTATION_HORIZONTAL, height,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static void
+hdy_tab_get_preferred_height_for_width (GtkWidget *widget,
+ gint width,
+ gint *minimum,
+ gint *natural)
+{
+ hdy_tab_measure (widget, GTK_ORIENTATION_VERTICAL, width,
+ minimum, natural,
+ NULL, NULL);
+}
+
+static void
+hdy_tab_size_allocate (GtkWidget *widget,
+ GtkAllocation *allocation)
+{
+ HdyTab *self = HDY_TAB (widget);
+ gint width_diff = allocation->width;
+ GtkAllocation child_alloc, clip;
+
+ hdy_css_size_allocate_self (widget, allocation);
+
+ gtk_widget_set_allocation (widget, allocation);
+
+ if (self->window)
+ gdk_window_move_resize (self->window,
+ allocation->x, allocation->y,
+ allocation->width, allocation->height);
+
+ child_alloc = *allocation;
+ child_alloc.x = 0;
+ child_alloc.y = 0;
+
+ hdy_css_size_allocate_children (widget, &child_alloc);
+
+ width_diff = MAX (0, width_diff - child_alloc.width);
+
+ if (self->child) {
+ gint width = MAX (child_alloc.width, self->display_width - width_diff);
+ gint min;
+
+ gtk_widget_get_preferred_width (self->child, &min, NULL);
+
+ self->draw_child = width >= min && child_alloc.height > 0;
+
+ gtk_widget_set_child_visible (self->child, self->draw_child);
+
+ if (self->draw_child) {
+ child_alloc.x += (child_alloc.width - width) / 2;
+ child_alloc.width = width;
+
+ gtk_widget_size_allocate (self->child, &child_alloc);
+ }
+ } else
+ self->draw_child = FALSE;
+
+ if (self->draw_child) {
+ child_alloc.x = 0;
+ child_alloc.y = 0;
+ child_alloc.width = allocation->width;
+ child_alloc.height = allocation->height;
+
+ gtk_widget_set_clip (self->child, &child_alloc);
+ }
+
+ gtk_render_background_get_clip (gtk_widget_get_style_context (widget),
+ allocation->x, allocation->y,
+ allocation->width, allocation->height,
+ &clip);
+
+ gtk_widget_set_clip (widget, &clip);
+}
+
+static void
+hdy_tab_realize (GtkWidget *widget)
+{
+ HdyTab *self = HDY_TAB (widget);
+ GtkAllocation allocation;
+ GdkWindowAttr attributes;
+ GdkWindowAttributesType attributes_mask;
+
+ gtk_widget_set_realized (widget, TRUE);
+
+ gtk_widget_get_allocation (widget, &allocation);
+
+ attributes.x = allocation.x;
+ attributes.y = allocation.y;
+ attributes.width = allocation.width;
+ attributes.height = allocation.height;
+ attributes.window_type = GDK_WINDOW_CHILD;
+ attributes.wclass = GDK_INPUT_OUTPUT;
+ attributes.visual = gtk_widget_get_visual (widget);
+ attributes.event_mask = gtk_widget_get_events (widget);
+ attributes_mask = GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL;
+
+ self->window = gdk_window_new (gtk_widget_get_parent_window (widget),
+ &attributes,
+ attributes_mask);
+
+ gtk_widget_set_window (widget, self->window);
+ gtk_widget_register_window (widget, self->window);
+
+ if (self->child)
+ gtk_widget_set_parent_window (self->child, self->window);
+}
+
+static void
+hdy_tab_unrealize (GtkWidget *widget)
+{
+ HdyTab *self = HDY_TAB (widget);
+
+ GTK_WIDGET_CLASS (hdy_tab_parent_class)->unrealize (widget);
+
+ self->window = NULL;
+}
+
+static gboolean
+hdy_tab_draw (GtkWidget *widget,
+ cairo_t *cr)
+{
+ HdyTab *self = HDY_TAB (widget);
+
+ hdy_css_draw (widget, cr);
+
+ if (self->draw_child)
+ gtk_container_propagate_draw (GTK_CONTAINER (self), self->child, cr);
+
+ return GDK_EVENT_PROPAGATE;
+}
+
+static void
+hdy_tab_add (GtkContainer *container,
+ GtkWidget *widget)
+{
+ HdyTab *self = HDY_TAB (container);
+
+ if (self->child)
+ return;
+
+ self->child = widget;
+
+ if (self->child) {
+ gtk_widget_set_parent (self->child, GTK_WIDGET (self));
+
+ if (self->window)
+ gtk_widget_set_parent_window (self->child, self->window);
+ }
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+hdy_tab_remove (GtkContainer *container,
+ GtkWidget *widget)
+{
+ HdyTab *self = HDY_TAB (container);
+
+ if (widget != self->child)
+ return;
+
+ g_clear_pointer (&self->child, gtk_widget_unparent);
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+}
+
+static void
+hdy_tab_forall (GtkContainer *container,
+ gboolean include_internals,
+ GtkCallback callback,
+ gpointer callback_data)
+{
+ HdyTab *self = HDY_TAB (container);
+
+ if (!include_internals)
+ return;
+
+ if (self->child)
+ callback (self->child, callback_data);
+}
+
+static void
+hdy_tab_constructed (GObject *object)
+{
+ HdyTab *self = HDY_TAB (object);
+
+ G_OBJECT_CLASS (hdy_tab_parent_class)->constructed (object);
+
+ gtk_stack_set_visible_child_name (self->stack,
+ self->pinned ? "pinned" : "regular");
+
+ if (self->pinned)
+ gtk_style_context_add_class (gtk_widget_get_style_context (GTK_WIDGET (self)),
+ "pinned");
+
+ if (self->dragging)
+ g_object_set (self, "selected", TRUE, NULL);
+
+ g_signal_connect_object (self->view, "notify::default-icon",
+ G_CALLBACK (update_icon), self,
+ G_CONNECT_SWAPPED);
+}
+
+static void
+hdy_tab_get_property (GObject *object,
+ guint prop_id,
+ GValue *value,
+ GParamSpec *pspec)
+{
+ HdyTab *self = HDY_TAB (object);
+
+ switch (prop_id) {
+ case PROP_VIEW:
+ g_value_set_object (value, self->view);
+ break;
+
+ case PROP_PAGE:
+ g_value_set_object (value, self->page);
+ break;
+
+ case PROP_PINNED:
+ g_value_set_boolean (value, self->pinned);
+ break;
+
+ case PROP_DRAGGING:
+ g_value_set_boolean (value, self->dragging);
+ break;
+
+ case PROP_DISPLAY_WIDTH:
+ g_value_set_int (value, hdy_tab_get_display_width (self));
+ break;
+
+ case PROP_HOVERING:
+ g_value_set_boolean (value, hdy_tab_get_hovering (self));
+ break;
+
+ case PROP_SELECTED:
+ g_value_set_boolean (value, hdy_tab_get_selected (self));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_tab_set_property (GObject *object,
+ guint prop_id,
+ const GValue *value,
+ GParamSpec *pspec)
+{
+ HdyTab *self = HDY_TAB (object);
+
+ switch (prop_id) {
+ case PROP_VIEW:
+ self->view = g_value_get_object (value);
+ break;
+
+ case PROP_PAGE:
+ hdy_tab_set_page (self, g_value_get_object (value));
+ break;
+
+ case PROP_PINNED:
+ self->pinned = g_value_get_boolean (value);
+ break;
+
+ case PROP_DRAGGING:
+ self->dragging = g_value_get_boolean (value);
+ break;
+
+ case PROP_DISPLAY_WIDTH:
+ hdy_tab_set_display_width (self, g_value_get_int (value));
+ break;
+
+ case PROP_HOVERING:
+ hdy_tab_set_hovering (self, g_value_get_boolean (value));
+ break;
+
+ case PROP_SELECTED:
+ hdy_tab_set_selected (self, g_value_get_boolean (value));
+ break;
+
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ }
+}
+
+static void
+hdy_tab_dispose (GObject *object)
+{
+ HdyTab *self = HDY_TAB (object);
+
+ hdy_tab_set_page (self, NULL);
+
+ G_OBJECT_CLASS (hdy_tab_parent_class)->dispose (object);
+}
+
+static void
+hdy_tab_class_init (HdyTabClass *klass)
+{
+ GObjectClass *object_class = G_OBJECT_CLASS (klass);
+ GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass);
+ GtkContainerClass *container_class = GTK_CONTAINER_CLASS (klass);
+
+ object_class->dispose = hdy_tab_dispose;
+ object_class->constructed = hdy_tab_constructed;
+ object_class->get_property = hdy_tab_get_property;
+ object_class->set_property = hdy_tab_set_property;
+
+ widget_class->get_preferred_width = hdy_tab_get_preferred_width;
+ widget_class->get_preferred_height = hdy_tab_get_preferred_height;
+ widget_class->get_preferred_width_for_height = hdy_tab_get_preferred_width_for_height;
+ widget_class->get_preferred_height_for_width = hdy_tab_get_preferred_height_for_width;
+ widget_class->size_allocate = hdy_tab_size_allocate;
+ widget_class->realize = hdy_tab_realize;
+ widget_class->unrealize = hdy_tab_unrealize;
+ widget_class->draw = hdy_tab_draw;
+
+ container_class->add = hdy_tab_add;
+ container_class->remove = hdy_tab_remove;
+ container_class->forall = hdy_tab_forall;
+
+ props[PROP_VIEW] =
+ g_param_spec_object ("view",
+ _("View"),
+ _("View"),
+ HDY_TYPE_TAB_VIEW,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ props[PROP_PINNED] =
+ g_param_spec_boolean ("pinned",
+ _("Pinned"),
+ _("Pinned"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ props[PROP_DRAGGING] =
+ g_param_spec_boolean ("dragging",
+ _("Dragging"),
+ _("Dragging"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+ props[PROP_PAGE] =
+ g_param_spec_object ("page",
+ _("Page"),
+ _("Page"),
+ HDY_TYPE_TAB_PAGE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_DISPLAY_WIDTH] =
+ g_param_spec_int ("display-width",
+ _("Display Width"),
+ _("Display Width"),
+ 0, G_MAXINT, 0,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_HOVERING] =
+ g_param_spec_boolean ("hovering",
+ _("Hovering"),
+ _("Hovering"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ props[PROP_SELECTED] =
+ g_param_spec_boolean ("selected",
+ _("Selected"),
+ _("Selected"),
+ FALSE,
+ G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+ g_object_class_install_properties (object_class, LAST_PROP, props);
+
+ gtk_widget_class_set_template_from_resource (widget_class,
+ "/sm/puri/handy/ui/hdy-tab.ui");
+ gtk_widget_class_bind_template_child (widget_class, HdyTab, title);
+ gtk_widget_class_bind_template_child (widget_class, HdyTab, icon);
+ gtk_widget_class_bind_template_child (widget_class, HdyTab, pinned_icon);
+ gtk_widget_class_bind_template_child (widget_class, HdyTab, stack);
+ gtk_widget_class_bind_template_child (widget_class, HdyTab, icon_stack);
+ gtk_widget_class_bind_template_child (widget_class, HdyTab, pinned_icon_stack);
+ gtk_widget_class_bind_template_child (widget_class, HdyTab, secondary_icon);
+ gtk_widget_class_bind_template_callback (widget_class, close_clicked_cb);
+
+ gtk_widget_class_set_css_name (widget_class, "tab");
+}
+
+static void
+hdy_tab_init (HdyTab *self)
+{
+ gtk_widget_init_template (GTK_WIDGET (self));
+}
+
+HdyTab *
+hdy_tab_new (HdyTabView *view,
+ gboolean pinned,
+ gboolean dragging)
+{
+ g_assert (HDY_IS_TAB_VIEW (view));
+
+ return g_object_new (HDY_TYPE_TAB,
+ "view", view,
+ "pinned", pinned,
+ "dragging", dragging,
+ NULL);
+}
+
+void
+hdy_tab_set_page (HdyTab *self,
+ HdyTabPage *page)
+{
+ if (self->page == page)
+ return;
+
+ if (self->page) {
+ g_signal_handlers_disconnect_by_func (self->page, update_tooltip, self);
+ g_signal_handlers_disconnect_by_func (self->page, update_icon, self);
+ g_signal_handlers_disconnect_by_func (self->page, update_secondary_icon, 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_clear_pointer (&self->selected_binding, g_binding_unbind);
+ g_clear_pointer (&self->title_binding, g_binding_unbind);
+ }
+
+ g_set_object (&self->page, page);
+
+ if (self->page) {
+ update_state (self);
+ update_tooltip (self);
+ update_icon (self);
+ update_secondary_icon (self);
+ update_needs_attention (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::secondary-icon",
+ G_CALLBACK (update_secondary_icon), 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);
+
+ if (!self->dragging)
+ self->selected_binding = g_object_bind_property (self->page, "selected",
+ self, "selected",
+ G_BINDING_SYNC_CREATE);
+
+ self->title_binding = g_object_bind_property (self->page, "title",
+ self->title, "label",
+ G_BINDING_SYNC_CREATE);
+ }
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PAGE]);
+}
+
+GtkWidget *
+hdy_tab_get_child (HdyTab *self)
+{
+ return self->child;
+}
+
+gint
+hdy_tab_get_child_min_width (HdyTab *self)
+{
+ GtkStyleContext *context = gtk_widget_get_style_context (GTK_WIDGET (self));
+ GtkStateFlags flags = gtk_widget_get_state_flags (GTK_WIDGET (self));
+ gint min, css_width;
+
+ if (self->child)
+ gtk_widget_get_preferred_width (self->child, &min, NULL);
+ else
+ min = 0;
+
+ gtk_style_context_get (context, flags,
+ "min-width", &css_width,
+ NULL);
+
+ return MAX (min, css_width);
+}
+
+gint
+hdy_tab_get_display_width (HdyTab *self)
+{
+ return self->display_width;
+}
+
+void
+hdy_tab_set_display_width (HdyTab *self,
+ gint width)
+{
+ if (self->display_width == width)
+ return;
+
+ self->display_width = width;
+
+ gtk_widget_queue_resize (GTK_WIDGET (self));
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_DISPLAY_WIDTH]);
+}
+
+gboolean
+hdy_tab_get_hovering (HdyTab *self)
+{
+ return self->hovering;
+}
+
+void
+hdy_tab_set_hovering (HdyTab *self,
+ gboolean hovering)
+{
+ if (self->hovering == hovering)
+ return;
+
+ self->hovering = hovering;
+
+ update_state (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_HOVERING]);
+}
+
+gboolean
+hdy_tab_get_selected (HdyTab *self)
+{
+ return self->selected;
+}
+
+void
+hdy_tab_set_selected (HdyTab *self,
+ gboolean selected)
+{
+ if (self->selected == selected)
+ return;
+
+ self->selected = selected;
+
+ update_state (self);
+
+ g_object_notify_by_pspec (G_OBJECT (self), props[PROP_SELECTED]);
+}
diff --git a/src/hdy-tab.ui b/src/hdy-tab.ui
new file mode 100644
index 00000000..ac26e22c
--- /dev/null
+++ b/src/hdy-tab.ui
@@ -0,0 +1,135 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+ <requires lib="gtk+" version="3.24"/>
+ <template class="HdyTab" parent="GtkContainer">
+ <property name="can-focus">True</property>
+ <child>
+ <object class="GtkStack" id="stack">
+ <property name="visible">True</property>
+ <property name="hhomogeneous">False</property>
+ <style>
+ <class name="tab-contents"/>
+ </style>
+ <child>
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <child type="center">
+ <object class="GtkBox">
+ <property name="visible">True</property>
+ <child>
+ <object class="GtkStack" id="icon_stack">
+ <property name="visible">True</property>
+ <property name="margin-start">6</property>
+ <property name="margin-end">6</property>
+ <child>
+ <object class="GtkImage" id="icon">
+ <property name="visible">True</property>
+ <style>
+ <class name="tab-icon"/>
+ </style>
+ </object>
+ <packing>
+ <property name="name">icon</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinner">
+ <property name="visible">True</property>
+ <property name="active">True</property>
+ </object>
+ <packing>
+ <property name="name">spinner</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkLabel" id="title">
+ <property name="visible">True</property>
+ <property name="hexpand">True</property>
+ <property name="halign">center</property>
+ <property name="xalign">0</property>
+ <property name="ellipsize">end</property>
+ <property name="single-line-mode">True</property>
+ <style>
+ <class name="tab-title"/>
+ </style>
+ </object>
+ </child>
+ </object>
+ </child>
+ <child>
+ <object class="GtkButton" id="close_btn">
+ <property name="visible" bind-source="HdyTab" bind-property="selected"
bind-flags="sync-create"/>
+ <property name="valign">center</property>
+ <property name="halign">end</property>
+ <property name="can-focus">False</property>
+ <signal name="clicked" handler="close_clicked_cb" swapped="true"/>
+ <style>
+ <class name="tab-close-button"/>
+ <class name="image-button"/>
+ <class name="flat"/>
+ </style>
+ <child>
+ <object class="GtkImage">
+ <property name="visible">True</property>
+ <property name="icon-name">window-close-symbolic</property>
+ </object>
+ </child>
+ </object>
+ <packing>
+ <property name="pack-type">end</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkImage" id="secondary_icon">
+ <property name="visible">True</property>
+ <property name="margin-start">6</property>
+ <property name="margin-end">6</property>
+ <style>
+ <class name="tab-secondary-icon"/>
+ </style>
+ </object>
+ <packing>
+ <property name="pack-type">end</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">regular</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkStack" id="pinned_icon_stack">
+ <property name="visible">True</property>
+ <property name="halign">center</property>
+ <property name="hexpand">True</property>
+ <child>
+ <object class="GtkImage" id="pinned_icon">
+ <property name="visible">True</property>
+ <style>
+ <class name="tab-icon"/>
+ </style>
+ </object>
+ <packing>
+ <property name="name">icon</property>
+ </packing>
+ </child>
+ <child>
+ <object class="GtkSpinner">
+ <property name="visible">True</property>
+ <property name="active">True</property>
+ </object>
+ <packing>
+ <property name="name">spinner</property>
+ </packing>
+ </child>
+ </object>
+ <packing>
+ <property name="name">pinned</property>
+ </packing>
+ </child>
+ </object>
+ </child>
+ </template>
+</interface>
diff --git a/src/icons/hdy-tab-icon-missing-symbolic.svg b/src/icons/hdy-tab-icon-missing-symbolic.svg
new file mode 100644
index 00000000..48807915
--- /dev/null
+++ b/src/icons/hdy-tab-icon-missing-symbolic.svg
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ height="16"
+ id="svg7384"
+ version="1.1"
+ width="16">
+ <metadata
+ id="metadata90">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title>Gnome Symbolic Icon Theme</dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <title
+ id="title9167">Gnome Symbolic Icon Theme</title>
+ <defs
+ id="defs7386" />
+ <path
+
style="color:#bebebe;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;font-variant-ligatures:normal;font-variant-position:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-alternates:normal;font-feature-settings:normal;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;writing-mode:lr-tb;direction:ltr;text-orientation:mixed;dominant-baseline:auto;baseline-shift:baseline;text-anchor:start;white-space:normal;shape-padding:0;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:1;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;vector-effect:none;fill:#241f31;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;st
roke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
+ d="M 2.1992188 0 C 0.94446875 0 -2.9605947e-16 1.0997689 0 2.3183594 L 0 4 L 2 4 L 2 2.3183594 C 2
2.0752625 2.1256288 2 2.1992188 2 L 4 2 L 4 0 L 2.1992188 0 z M 6 0 L 6 2 L 10 2 L 10 0 L 6 0 z M 12 0 L 12 2
L 13.800781 2 C 13.874371 2 14 2.0752716 14 2.3183594 L 14 4 L 16 4 L 16 2.3183594 C 16 1.0997598 15.055531
-2.9605947e-16 13.800781 0 L 12 0 z M 0 6 L 0 10 L 2 10 L 2 6 L 0 6 z M 14 6 L 14 10 L 16 10 L 16 6 L 14 6 z
M 0 12 L 0 13.681641 C 0 14.900231 0.94447875 16 2.1992188 16 L 4 16 L 4 14 L 2.1992188 14 C 2.1256188 14 2
13.924738 2 13.681641 L 2 12 L 0 12 z M 14 12 L 14 13.681641 C 14 13.924729 13.874381 14 13.800781 14 L 12 14
L 12 16 L 13.800781 16 C 15.055521 16 16 14.90024 16 13.681641 L 16 12 L 14 12 z M 6 14 L 6 16 L 10 16 L 10
14 L 6 14 z "
+ id="path983" />
+</svg>
diff --git a/src/meson.build b/src/meson.build
index 11d4100e..58f235dc 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -14,6 +14,7 @@ hdy_public_enum_headers = [
'hdy-leaflet.h',
'hdy-navigation-direction.h',
'hdy-squeezer.h',
+ 'hdy-tab-bar.h',
'hdy-view-switcher.h',
]
@@ -93,6 +94,9 @@ src_headers = [
'hdy-swipe-group.h',
'hdy-swipe-tracker.h',
'hdy-swipeable.h',
+ 'hdy-tab-bar.h',
+ 'hdy-tab-view.h',
+ 'hdy-title-bar.h',
'hdy-title-bar.h',
'hdy-types.h',
'hdy-value-object.h',
@@ -149,6 +153,10 @@ src_sources = [
'hdy-swipe-group.c',
'hdy-swipe-tracker.c',
'hdy-swipeable.c',
+ 'hdy-tab.c',
+ 'hdy-tab-bar.c',
+ 'hdy-tab-box.c',
+ 'hdy-tab-view.c',
'hdy-title-bar.c',
'hdy-value-object.c',
'hdy-view-switcher.c',
diff --git a/src/themes/Adwaita-dark.css b/src/themes/Adwaita-dark.css
index cd5441d7..fc06c643 100644
--- a/src/themes/Adwaita-dark.css
+++ b/src/themes/Adwaita-dark.css
@@ -168,11 +168,11 @@ list.content, list.content list { background-color: transparent; }
list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested >
row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#353535, #2d2d2d, 0.5); }
-list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color:
mix(#eeeeec, #2d2d2d, 0.95); }
+list.content list.nested > row.activatable:not(:active):hover:not(:selected) { background-color:
mix(#eeeeec, #2d2d2d, 0.95); }
list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content >
row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander
row.header:not(:active):not(:hover):not(:selected), list.content > row.expander
row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #2d2d2d; }
-list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander
row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#eeeeec, #2d2d2d, 0.95); }
+list.content > row.activatable:not(.expander):not(:active):hover:not(:selected), list.content > row.expander
row.header.activatable:not(:active):hover:not(:selected) { background-color: mix(#eeeeec, #2d2d2d, 0.95); }
list.content > row, list.content > row list > row { border-color: alpha(#1b1b1b, 0.7); border-style: solid;
transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
@@ -182,7 +182,7 @@ list.content > row:first-child, list.content > row.expander:first-child row.head
list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content >
row.expander:checked { border-width: 1px; }
-list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content >
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content >
row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content >
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child {
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px;
-gtk-outline-bottom-right-radius: 7px; }
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content >
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content >
row.expander.checked-expander-row-previous-sibling:not(:checked) row.header, list.content >
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child {
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px;
-gtk-outline-bottom-right-radius: 7px; }
list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row {
margin-top: 6px; }
@@ -195,3 +195,69 @@ window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { b
window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255,
255, 255, 0.065); }
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized),
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration,
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration-overlay { border-radius: 8px; }
+
+tabbar .box { min-height: 38px; background: #161616; border-color: #070707; border-style: solid; }
+
+tabbar .box:backdrop { background-color: #262626; border-color: #202020; }
+
+tabbar.top .box { border-bottom-width: 1px; }
+
+tabbar.bottom .box { border-top-width: 1px; }
+
+tabbar tabbox.pinned:dir(ltr) { padding-right: 1px; box-shadow: inset -1px 0 #070707; }
+
+tabbar tabbox.pinned:dir(ltr):backdrop { box-shadow: inset -1px 0 #202020; }
+
+tabbar tabbox.pinned:dir(rtl) { padding-left: 1px; box-shadow: inset 1px 0 #070707; }
+
+tabbar tabbox.pinned:dir(rtl):backdrop { box-shadow: inset 1px 0 #202020; }
+
+tabbar undershoot.left, tabbar undershoot.right { transition: all 200ms ease; }
+
+tabbar undershoot.left { background: linear-gradient(to right, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0) 20px); }
+
+tabbar undershoot.right { background: linear-gradient(to left, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0) 20px); }
+
+tabbar.needs-attention-left undershoot.left { background: linear-gradient(to right, rgba(21, 83, 158, 0.7),
rgba(21, 83, 158, 0.5) 1px, rgba(21, 83, 158, 0) 20px); }
+
+tabbar.needs-attention-right undershoot.right { background: linear-gradient(to left, rgba(21, 83, 158, 0.7),
rgba(21, 83, 158, 0.5) 1px, rgba(21, 83, 158, 0) 20px); }
+
+tabbar tab { min-width: 130px; border-style: solid; border-color: #070707; border-width: 0 1px 0 1px;
background-color: #1e1e1e; transition: background 150ms ease-in-out; }
+
+tabbar tab.pinned { min-width: 40px; }
+
+tabbar tab:hover { background-color: #323232; }
+
+tabbar tab:checked { background-color: #262626; }
+
+tabbar tab:checked:hover { background-color: #3a3a3a; }
+
+tabbar tab:backdrop { border-color: #202020; background-color: #2d2d2d; }
+
+tabbar tab:backdrop:checked { background-color: #353535; }
+
+tabbar tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 255, 0.8),
rgba(21, 83, 158, 0.2) 15%, rgba(21, 83, 158, 0) 15%); }
+
+tabbar tab .tab-contents { padding: 6px; }
+
+tabbar tab .tab-close-button { margin: 0; padding: 0; border-radius: 9999px; }
+
+tabbar tab .tab-close-button:not(:hover) { border-color: transparent; }
+
+tabbar .start-action, tabbar .end-action { background: #1e1e1e; border-color: #070707; border-style: solid;
transition: background 150ms ease-in-out; }
+
+tabbar .start-action:backdrop, tabbar .end-action:backdrop { border-color: #202020; background-color:
#2d2d2d; }
+
+tabbar .start-action button, tabbar .end-action button { border: none; border-radius: 0; }
+
+tabbar .start-action:dir(ltr), tabbar .end-action:dir(rtl) { border-right-width: 1px; }
+
+tabbar .start-action:dir(ltr) > *, tabbar .end-action:dir(rtl) > * { margin-right: 1px; }
+
+tabbar .start-action:dir(rtl), tabbar .end-action:dir(ltr) { border-left-width: 1px; }
+
+tabbar .start-action:dir(rtl) > *, tabbar .end-action:dir(ltr) > * { margin-left: 1px; }
+
+.tab-drag-icon tab { min-height: 38px; background-color: #3a3a3a; box-shadow: 0 3px 9px 1px rgba(0, 0, 0,
0.5), 0 0 0 1px rgba(27, 27, 27, 0.9); margin: 25px; }
+
+.tab-drag-icon tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255,
255, 0.8), rgba(21, 83, 158, 0.2) 15%, rgba(21, 83, 158, 0) 15%); }
diff --git a/src/themes/Adwaita.css b/src/themes/Adwaita.css
index b7f09037..618f3a7c 100644
--- a/src/themes/Adwaita.css
+++ b/src/themes/Adwaita.css
@@ -168,11 +168,11 @@ list.content, list.content list { background-color: transparent; }
list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested >
row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#f6f5f4, #ffffff, 0.5); }
-list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color:
mix(#2e3436, #ffffff, 0.95); }
+list.content list.nested > row.activatable:not(:active):hover:not(:selected) { background-color:
mix(#2e3436, #ffffff, 0.95); }
list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content >
row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander
row.header:not(:active):not(:hover):not(:selected), list.content > row.expander
row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #ffffff; }
-list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander
row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#2e3436, #ffffff, 0.95); }
+list.content > row.activatable:not(.expander):not(:active):hover:not(:selected), list.content > row.expander
row.header.activatable:not(:active):hover:not(:selected) { background-color: mix(#2e3436, #ffffff, 0.95); }
list.content > row, list.content > row list > row { border-color: alpha(#cdc7c2, 0.7); border-style: solid;
transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
@@ -182,7 +182,7 @@ list.content > row:first-child, list.content > row.expander:first-child row.head
list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content >
row.expander:checked { border-width: 1px; }
-list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content >
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content >
row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content >
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child {
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px;
-gtk-outline-bottom-right-radius: 7px; }
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content >
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content >
row.expander.checked-expander-row-previous-sibling:not(:checked) row.header, list.content >
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child {
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px;
-gtk-outline-bottom-right-radius: 7px; }
list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row {
margin-top: 6px; }
@@ -195,3 +195,69 @@ window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { b
window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255,
255, 255, 0.34); }
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized),
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration,
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration-overlay { border-radius: 8px; }
+
+tabbar .box { min-height: 38px; background: #c6bfb9; border-color: #bfb8b1; border-style: solid; }
+
+tabbar .box:backdrop { background-color: #e1dedb; border-color: #d5d0cc; }
+
+tabbar.top .box { border-bottom-width: 1px; }
+
+tabbar.bottom .box { border-top-width: 1px; }
+
+tabbar tabbox.pinned:dir(ltr) { padding-right: 1px; box-shadow: inset -1px 0 #bfb8b1; }
+
+tabbar tabbox.pinned:dir(ltr):backdrop { box-shadow: inset -1px 0 #d5d0cc; }
+
+tabbar tabbox.pinned:dir(rtl) { padding-left: 1px; box-shadow: inset 1px 0 #bfb8b1; }
+
+tabbar tabbox.pinned:dir(rtl):backdrop { box-shadow: inset 1px 0 #d5d0cc; }
+
+tabbar undershoot.left, tabbar undershoot.right { transition: all 200ms ease; }
+
+tabbar undershoot.left { background: linear-gradient(to right, rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0) 20px); }
+
+tabbar undershoot.right { background: linear-gradient(to left, rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0) 20px); }
+
+tabbar.needs-attention-left undershoot.left { background: linear-gradient(to right, rgba(53, 132, 228, 0.7),
rgba(53, 132, 228, 0.5) 1px, rgba(53, 132, 228, 0) 20px); }
+
+tabbar.needs-attention-right undershoot.right { background: linear-gradient(to left, rgba(53, 132, 228,
0.7), rgba(53, 132, 228, 0.5) 1px, rgba(53, 132, 228, 0) 20px); }
+
+tabbar tab { min-width: 130px; border-style: solid; border-color: #bfb8b1; border-width: 0 1px 0 1px;
background-color: #cdc7c2; transition: background 150ms ease-in-out; }
+
+tabbar tab.pinned { min-width: 40px; }
+
+tabbar tab:hover { background-color: #d8d4d0; }
+
+tabbar tab:checked { background-color: #dad6d2; }
+
+tabbar tab:checked:hover { background-color: #e6e3e0; }
+
+tabbar tab:backdrop { border-color: #d5d0cc; background-color: #e8e6e3; }
+
+tabbar tab:backdrop:checked { background-color: #f6f5f4; }
+
+tabbar tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 255, 0.8),
rgba(53, 132, 228, 0.2) 15%, rgba(53, 132, 228, 0) 15%); }
+
+tabbar tab .tab-contents { padding: 6px; }
+
+tabbar tab .tab-close-button { margin: 0; padding: 0; border-radius: 9999px; }
+
+tabbar tab .tab-close-button:not(:hover) { border-color: transparent; }
+
+tabbar .start-action, tabbar .end-action { background: #cdc7c2; border-color: #bfb8b1; border-style: solid;
transition: background 150ms ease-in-out; }
+
+tabbar .start-action:backdrop, tabbar .end-action:backdrop { border-color: #d5d0cc; background-color:
#e8e6e3; }
+
+tabbar .start-action button, tabbar .end-action button { border: none; border-radius: 0; }
+
+tabbar .start-action:dir(ltr), tabbar .end-action:dir(rtl) { border-right-width: 1px; }
+
+tabbar .start-action:dir(ltr) > *, tabbar .end-action:dir(rtl) > * { margin-right: 1px; }
+
+tabbar .start-action:dir(rtl), tabbar .end-action:dir(ltr) { border-left-width: 1px; }
+
+tabbar .start-action:dir(rtl) > *, tabbar .end-action:dir(ltr) > * { margin-left: 1px; }
+
+.tab-drag-icon tab { min-height: 38px; background-color: #e6e3e0; box-shadow: 0 3px 9px 1px rgba(0, 0, 0,
0.5), 0 0 0 1px rgba(0, 0, 0, 0.23); margin: 25px; }
+
+.tab-drag-icon tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255,
255, 0.8), rgba(53, 132, 228, 0.2) 15%, rgba(53, 132, 228, 0) 15%); }
diff --git a/src/themes/HighContrast.css b/src/themes/HighContrast.css
index 20bf2281..704593d7 100644
--- a/src/themes/HighContrast.css
+++ b/src/themes/HighContrast.css
@@ -168,11 +168,11 @@ list.content, list.content list { background-color: transparent; }
list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested >
row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#fdfdfc, #ffffff, 0.5); }
-list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color:
mix(#272c2e, #ffffff, 0.95); }
+list.content list.nested > row.activatable:not(:active):hover:not(:selected) { background-color:
mix(#272c2e, #ffffff, 0.95); }
list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content >
row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander
row.header:not(:active):not(:hover):not(:selected), list.content > row.expander
row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #ffffff; }
-list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander
row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#272c2e, #ffffff, 0.95); }
+list.content > row.activatable:not(.expander):not(:active):hover:not(:selected), list.content > row.expander
row.header.activatable:not(:active):hover:not(:selected) { background-color: mix(#272c2e, #ffffff, 0.95); }
list.content > row, list.content > row list > row { border-color: alpha(#877b6e, 0.7); border-style: solid;
transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
@@ -182,7 +182,7 @@ list.content > row:first-child, list.content > row.expander:first-child row.head
list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content >
row.expander:checked { border-width: 1px; }
-list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content >
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content >
row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content >
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child {
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px;
-gtk-outline-bottom-right-radius: 7px; }
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content >
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content >
row.expander.checked-expander-row-previous-sibling:not(:checked) row.header, list.content >
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child {
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px;
-gtk-outline-bottom-right-radius: 7px; }
list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row {
margin-top: 6px; }
@@ -195,3 +195,69 @@ window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { b
window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255,
255, 255, 0.34); }
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized),
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration,
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration-overlay { border-radius: 8px; }
+
+tabbar .box { min-height: 38px; background: #cdc7c2; border-color: #6e645a; border-style: solid; }
+
+tabbar .box:backdrop { background-color: #e8e6e3; border-color: #d5d0cc; }
+
+tabbar.top .box { border-bottom-width: 1px; }
+
+tabbar.bottom .box { border-top-width: 1px; }
+
+tabbar tabbox.pinned:dir(ltr) { padding-right: 1px; box-shadow: inset -1px 0 #6e645a; }
+
+tabbar tabbox.pinned:dir(ltr):backdrop { box-shadow: inset -1px 0 #d5d0cc; }
+
+tabbar tabbox.pinned:dir(rtl) { padding-left: 1px; box-shadow: inset 1px 0 #6e645a; }
+
+tabbar tabbox.pinned:dir(rtl):backdrop { box-shadow: inset 1px 0 #d5d0cc; }
+
+tabbar undershoot.left, tabbar undershoot.right { transition: all 200ms ease; }
+
+tabbar undershoot.left { background: linear-gradient(to right, rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0) 20px); }
+
+tabbar undershoot.right { background: linear-gradient(to left, rgba(0, 0, 0, 0.07), rgba(0, 0, 0, 0) 20px); }
+
+tabbar.needs-attention-left undershoot.left { background: linear-gradient(to right, rgba(27, 106, 203, 0.7),
rgba(27, 106, 203, 0.5) 1px, rgba(27, 106, 203, 0) 20px); }
+
+tabbar.needs-attention-right undershoot.right { background: linear-gradient(to left, rgba(27, 106, 203,
0.7), rgba(27, 106, 203, 0.5) 1px, rgba(27, 106, 203, 0) 20px); }
+
+tabbar tab { min-width: 130px; border-style: solid; border-color: #6e645a; border-width: 0 1px 0 1px;
background-color: #d4cfca; transition: background 150ms ease-in-out; }
+
+tabbar tab.pinned { min-width: 40px; }
+
+tabbar tab:hover { background-color: #dfdcd8; }
+
+tabbar tab:checked { background-color: #e1dedb; }
+
+tabbar tab:checked:hover { background-color: #edebe9; }
+
+tabbar tab:backdrop { border-color: #d5d0cc; background-color: #efedec; }
+
+tabbar tab:backdrop:checked { background-color: #fdfdfc; }
+
+tabbar tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 255, 0.8),
rgba(27, 106, 203, 0.2) 15%, rgba(27, 106, 203, 0) 15%); }
+
+tabbar tab .tab-contents { padding: 6px; }
+
+tabbar tab .tab-close-button { margin: 0; padding: 0; border-radius: 9999px; }
+
+tabbar tab .tab-close-button:not(:hover) { border-color: transparent; }
+
+tabbar .start-action, tabbar .end-action { background: #d4cfca; border-color: #6e645a; border-style: solid;
transition: background 150ms ease-in-out; }
+
+tabbar .start-action:backdrop, tabbar .end-action:backdrop { border-color: #d5d0cc; background-color:
#efedec; }
+
+tabbar .start-action button, tabbar .end-action button { border: none; border-radius: 0; }
+
+tabbar .start-action:dir(ltr), tabbar .end-action:dir(rtl) { border-right-width: 1px; }
+
+tabbar .start-action:dir(ltr) > *, tabbar .end-action:dir(rtl) > * { margin-right: 1px; }
+
+tabbar .start-action:dir(rtl), tabbar .end-action:dir(ltr) { border-left-width: 1px; }
+
+tabbar .start-action:dir(rtl) > *, tabbar .end-action:dir(ltr) > * { margin-left: 1px; }
+
+.tab-drag-icon tab { min-height: 38px; background-color: #edebe9; box-shadow: 0 3px 9px 1px rgba(0, 0, 0,
0.5), 0 0 0 1px rgba(0, 0, 0, 0.23); margin: 25px; }
+
+.tab-drag-icon tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255,
255, 0.8), rgba(27, 106, 203, 0.2) 15%, rgba(27, 106, 203, 0) 15%); }
diff --git a/src/themes/HighContrastInverse.css b/src/themes/HighContrastInverse.css
index ed846282..39ff160e 100644
--- a/src/themes/HighContrastInverse.css
+++ b/src/themes/HighContrastInverse.css
@@ -168,11 +168,11 @@ list.content, list.content list { background-color: transparent; }
list.content list.nested > row:not(:active):not(:hover):not(:selected), list.content list.nested >
row:not(:active):hover:not(.activatable):not(:selected) { background-color: mix(#303030, #2d2d2d, 0.5); }
-list.content list.nested > row:not(:active):hover.activatable:not(:selected) { background-color:
mix(#f3f3f1, #2d2d2d, 0.95); }
+list.content list.nested > row.activatable:not(:active):hover:not(:selected) { background-color:
mix(#f3f3f1, #2d2d2d, 0.95); }
list.content > row:not(.expander):not(:active):not(:hover):not(:selected), list.content >
row:not(.expander):not(:active):hover:not(.activatable):not(:selected), list.content > row.expander
row.header:not(:active):not(:hover):not(:selected), list.content > row.expander
row.header:not(:active):hover:not(.activatable):not(:selected) { background-color: #2d2d2d; }
-list.content > row:not(.expander):not(:active):hover.activatable:not(:selected), list.content > row.expander
row.header:not(:active):hover.activatable:not(:selected) { background-color: mix(#f3f3f1, #2d2d2d, 0.95); }
+list.content > row.activatable:not(.expander):not(:active):hover:not(:selected), list.content > row.expander
row.header.activatable:not(:active):hover:not(:selected) { background-color: mix(#f3f3f1, #2d2d2d, 0.95); }
list.content > row, list.content > row list > row { border-color: alpha(#686868, 0.7); border-style: solid;
transition: 200ms cubic-bezier(0.25, 0.46, 0.45, 0.94); }
@@ -182,7 +182,7 @@ list.content > row:first-child, list.content > row.expander:first-child row.head
list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content >
row.expander:checked { border-width: 1px; }
-list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content >
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content >
row.expander:not(:checked).checked-expander-row-previous-sibling row.header, list.content >
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child {
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px;
-gtk-outline-bottom-right-radius: 7px; }
+list.content > row:last-child, list.content > row.checked-expander-row-previous-sibling, list.content >
row.expander:checked, list.content > row.expander:not(:checked):last-child row.header, list.content >
row.expander.checked-expander-row-previous-sibling:not(:checked) row.header, list.content >
row.expander.empty:checked row.header, list.content > row.expander list.nested > row:last-child {
border-bottom-left-radius: 8px; -gtk-outline-bottom-left-radius: 7px; border-bottom-right-radius: 8px;
-gtk-outline-bottom-right-radius: 7px; }
list.content > row.expander:checked:not(:first-child), list.content > row.expander:checked + row {
margin-top: 6px; }
@@ -195,3 +195,69 @@ window.csd.unified:not(.solid-csd):not(.fullscreen) headerbar.selection-mode { b
window.csd.unified:not(.solid-csd):not(.fullscreen) > decoration-overlay { box-shadow: inset 0 1px rgba(255,
255, 255, 0.065); }
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized),
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration,
window.csd.unified:not(.solid-csd):not(.fullscreen):not(.tiled):not(.tiled-top):not(.tiled-bottom):not(.tiled-left):not(.tiled-right):not(.maximized)
decoration-overlay { border-radius: 8px; }
+
+tabbar .box { min-height: 38px; background: #111111; border-color: #4e4e4e; border-style: solid; }
+
+tabbar .box:backdrop { background-color: #202020; border-color: #202020; }
+
+tabbar.top .box { border-bottom-width: 1px; }
+
+tabbar.bottom .box { border-top-width: 1px; }
+
+tabbar tabbox.pinned:dir(ltr) { padding-right: 1px; box-shadow: inset -1px 0 #4e4e4e; }
+
+tabbar tabbox.pinned:dir(ltr):backdrop { box-shadow: inset -1px 0 #202020; }
+
+tabbar tabbox.pinned:dir(rtl) { padding-left: 1px; box-shadow: inset 1px 0 #4e4e4e; }
+
+tabbar tabbox.pinned:dir(rtl):backdrop { box-shadow: inset 1px 0 #202020; }
+
+tabbar undershoot.left, tabbar undershoot.right { transition: all 200ms ease; }
+
+tabbar undershoot.left { background: linear-gradient(to right, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0) 20px); }
+
+tabbar undershoot.right { background: linear-gradient(to left, rgba(0, 0, 0, 0.4), rgba(0, 0, 0, 0) 20px); }
+
+tabbar.needs-attention-left undershoot.left { background: linear-gradient(to right, rgba(15, 59, 113, 0.7),
rgba(15, 59, 113, 0.5) 1px, rgba(15, 59, 113, 0) 20px); }
+
+tabbar.needs-attention-right undershoot.right { background: linear-gradient(to left, rgba(15, 59, 113, 0.7),
rgba(15, 59, 113, 0.5) 1px, rgba(15, 59, 113, 0) 20px); }
+
+tabbar tab { min-width: 130px; border-style: solid; border-color: #4e4e4e; border-width: 0 1px 0 1px;
background-color: #191919; transition: background 150ms ease-in-out; }
+
+tabbar tab.pinned { min-width: 40px; }
+
+tabbar tab:hover { background-color: #2d2d2d; }
+
+tabbar tab:checked { background-color: #202020; }
+
+tabbar tab:checked:hover { background-color: #353535; }
+
+tabbar tab:backdrop { border-color: #202020; background-color: #282828; }
+
+tabbar tab:backdrop:checked { background-color: #303030; }
+
+tabbar tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255, 255, 0.8),
rgba(15, 59, 113, 0.2) 15%, rgba(15, 59, 113, 0) 15%); }
+
+tabbar tab .tab-contents { padding: 6px; }
+
+tabbar tab .tab-close-button { margin: 0; padding: 0; border-radius: 9999px; }
+
+tabbar tab .tab-close-button:not(:hover) { border-color: transparent; }
+
+tabbar .start-action, tabbar .end-action { background: #191919; border-color: #4e4e4e; border-style: solid;
transition: background 150ms ease-in-out; }
+
+tabbar .start-action:backdrop, tabbar .end-action:backdrop { border-color: #202020; background-color:
#282828; }
+
+tabbar .start-action button, tabbar .end-action button { border: none; border-radius: 0; }
+
+tabbar .start-action:dir(ltr), tabbar .end-action:dir(rtl) { border-right-width: 1px; }
+
+tabbar .start-action:dir(ltr) > *, tabbar .end-action:dir(rtl) > * { margin-right: 1px; }
+
+tabbar .start-action:dir(rtl), tabbar .end-action:dir(ltr) { border-left-width: 1px; }
+
+tabbar .start-action:dir(rtl) > *, tabbar .end-action:dir(ltr) > * { margin-left: 1px; }
+
+.tab-drag-icon tab { min-height: 38px; background-color: #353535; box-shadow: 0 3px 9px 1px rgba(0, 0, 0,
0.5), 0 0 0 1px rgba(104, 104, 104, 0.9); margin: 25px; }
+
+.tab-drag-icon tab.needs-attention { background-image: radial-gradient(ellipse at bottom, rgba(255, 255,
255, 0.8), rgba(15, 59, 113, 0.2) 15%, rgba(15, 59, 113, 0) 15%); }
diff --git a/src/themes/_Adwaita-base.scss b/src/themes/_Adwaita-base.scss
index cc0b7542..5d6e3bdb 100644
--- a/src/themes/_Adwaita-base.scss
+++ b/src/themes/_Adwaita-base.scss
@@ -334,3 +334,234 @@ window.csd.unified:not(.solid-csd):not(.fullscreen) {
}
}
}
+
+
+
+$tab_selected_bg: darken(darken($bg_color, 10%), 2%);
+$tab_selected_bg_backdrop: $bg_color;
+
+@if $variant == 'dark' {
+ $tab_selected_bg: lighten(darken($bg_color, 10%), 4%);
+}
+
+$tab_bg: darken($tab_selected_bg, 6%);
+$tab_bg_backdrop: darken($tab_selected_bg_backdrop, 6%);
+
+$tab_bar_bg: darken($tab_selected_bg, 9%);
+$tab_bar_bg_backdrop: darken($tab_selected_bg_backdrop, 9%);
+
+$tab_hover_bg: lighten($tab_bg, 5%);
+$tab_selected_hover_bg: lighten($tab_selected_bg, 5%);
+
+@if $variant == 'dark' {
+ $tab_bg: darken($tab_selected_bg, 3%);
+ $tab_bg_backdrop: darken($tab_selected_bg_backdrop, 3%);
+
+ $tab_bar_bg: darken($tab_selected_bg, 6%);
+ $tab_bar_bg_backdrop: darken($tab_selected_bg_backdrop, 6%);
+
+ $tab_hover_bg: lighten($tab_bg, 8%);
+ $tab_selected_hover_bg: lighten($tab_selected_bg, 8%);
+}
+
+$tab_needs_attention_color: $selected_bg_color;
+
+@mixin undershoot-gradient($dir) {
+ $color: black;
+
+ @if $variant == 'dark' {
+ background: linear-gradient(to #{$dir},
+ transparentize($color, .6),
+ transparentize($color, 1) 20px);
+ }
+ @else {
+ background: linear-gradient(to #{$dir},
+ transparentize($color, .93),
+ transparentize($color, 1) 20px);
+ }
+}
+
+@mixin need-attention-gradient($dir) {
+ background: linear-gradient(to #{$dir},
+ transparentize($tab_needs_attention_color, .3),
+ transparentize($tab_needs_attention_color, .5) 1px,
+ transparentize($tab_needs_attention_color, 1) 20px);
+}
+
+tabbar {
+ .box {
+ min-height: 38px;
+ background: $tab_bar_bg;
+ border-color: $alt_borders_color;
+ border-style: solid;
+
+ &:backdrop {
+ background-color: $tab_bar_bg_backdrop;
+ border-color: $backdrop_borders_color;
+ }
+ }
+
+ &.top .box {
+ border-bottom-width: 1px;
+ }
+
+ &.bottom .box {
+ border-top-width: 1px;
+ }
+
+ tabbox.pinned {
+ &:dir(ltr) {
+ padding-right: 1px;
+ box-shadow: inset -1px 0 $alt_borders_color;
+
+ &:backdrop {
+ box-shadow: inset -1px 0 $backdrop_borders_color;
+ }
+ }
+
+ &:dir(rtl) {
+ padding-left: 1px;
+ box-shadow: inset 1px 0 $alt_borders_color;
+
+ &:backdrop {
+ box-shadow: inset 1px 0 $backdrop_borders_color;
+ }
+ }
+ }
+
+ undershoot {
+ &.left, &.right {
+ transition: all 200ms ease;
+ }
+
+ &.left {
+ @include undershoot-gradient("right");
+ }
+
+ &.right {
+ @include undershoot-gradient("left");
+ }
+ }
+
+ &.needs-attention-left undershoot.left {
+ @include need-attention-gradient("right");
+ }
+
+ &.needs-attention-right undershoot.right {
+ @include need-attention-gradient("left");
+ }
+
+ tab {
+ min-width: 130px;
+ border-style: solid;
+ border-color: $alt_borders_color;
+ border-width: 0 1px 0 1px;
+ background-color: $tab_bg;
+ transition: background 150ms ease-in-out;
+
+ &.pinned {
+ min-width: 40px;
+ }
+
+ &:hover {
+ background-color: $tab_hover_bg;
+ }
+
+ &:checked {
+ background-color: $tab_selected_bg;
+
+ &:hover {
+ background-color: $tab_selected_hover_bg;
+ }
+ }
+
+ &:backdrop {
+ border-color: $backdrop_borders_color;
+ background-color: $tab_bg_backdrop;
+
+ &:checked {
+ background-color: $tab_selected_bg_backdrop;
+ }
+ }
+
+ &.needs-attention {
+ background-image:
+ radial-gradient(ellipse at bottom,
+ transparentize(white, .2),
+ transparentize($tab_needs_attention_color, .8) 15%,
+ transparentize($tab_needs_attention_color, 1) 15%);
+ }
+
+ .tab-contents {
+ padding: 6px;
+ }
+
+ .tab-close-button {
+ margin: 0;
+ padding: 0;
+ border-radius: 9999px;
+
+ &:not(:hover) {
+ border-color: transparent;
+ }
+ }
+ }
+
+ .start-action,
+ .end-action {
+ background: $tab_bg;
+ border-color: $alt_borders_color;
+ border-style: solid;
+ transition: background 150ms ease-in-out;
+
+ &:backdrop {
+ border-color: $backdrop_borders_color;
+ background-color: $tab_bg_backdrop;
+ }
+
+ button {
+ border: none;
+ border-radius: 0;
+ }
+ }
+
+ .start-action:dir(ltr),
+ .end-action:dir(rtl) {
+ border-right-width: 1px;
+
+ > * {
+ margin-right: 1px;
+ }
+ }
+
+ .start-action:dir(rtl),
+ .end-action:dir(ltr) {
+ border-left-width: 1px;
+
+ > * {
+ margin-left: 1px;
+ }
+ }
+}
+
+.tab-drag-icon {
+ tab {
+ min-height: 38px;
+ background-color: $tab_selected_hover_bg;
+
+ $_wm_border: if($variant=='light', transparentize(black, 0.77), transparentize($borders_color, 0.1));
+
+ box-shadow: 0 3px 9px 1px transparentize(black, 0.5),
+ 0 0 0 1px $_wm_border; //doing borders with box-shadow
+
+ margin: 25px;
+
+ &.needs-attention {
+ background-image:
+ radial-gradient(ellipse at bottom,
+ transparentize(white, .2),
+ transparentize($tab_needs_attention_color, .8) 15%,
+ transparentize($tab_needs_attention_color, 1) 15%);
+ }
+ }
+}
[
Date Prev][
Date Next] [
Thread Prev][
Thread Next]
[
Thread Index]
[
Date Index]
[
Author Index]