File Add-GtkHdySwipeable-and-GtkHdySwipeTracker.patch of Package gtk3

From: Alexander Mikhaylenko <alexm@gnome.org>
Date: Wed, 14 Oct 2020 17:10:54 +0500
Subject: Add GtkHdySwipeable and GtkHdySwipeTracker

This is imported from HdySwipeable and HdySwipeTracker from libhandy 1.0.2.
---
 gtk/hdy-swipe-tracker-private.h |   52 ++
 gtk/hdy-swipe-tracker.c         | 1246 +++++++++++++++++++++++++++++++++++++++
 gtk/hdy-swipeable-private.h     |   82 +++
 gtk/hdy-swipeable.c             |  274 +++++++++
 gtk/meson.build                 |    4 +
 5 files changed, 1658 insertions(+)
 create mode 100644 gtk/hdy-swipe-tracker-private.h
 create mode 100644 gtk/hdy-swipe-tracker.c
 create mode 100644 gtk/hdy-swipeable-private.h
 create mode 100644 gtk/hdy-swipeable.c

diff --git a/gtk/hdy-swipe-tracker-private.h b/gtk/hdy-swipe-tracker-private.h
new file mode 100644
index 0000000..c886734
--- /dev/null
+++ b/gtk/hdy-swipe-tracker-private.h
@@ -0,0 +1,52 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION)
+#error "Only <gtk/gtk.h> can be included directly."
+#endif
+
+#include "hdy-navigation-direction-private.h"
+#include "hdy-swipeable-private.h"
+
+G_BEGIN_DECLS
+
+#define GTK_HDY_SWIPE_BORDER 32
+
+#define GTK_TYPE_HDY_SWIPE_TRACKER (gtk_hdy_swipe_tracker_get_type())
+
+G_DECLARE_FINAL_TYPE (GtkHdySwipeTracker, gtk_hdy_swipe_tracker, GTK, HDY_SWIPE_TRACKER, GObject)
+
+GtkHdySwipeTracker *gtk_hdy_swipe_tracker_new (GtkHdySwipeable *swipeable);
+
+GtkHdySwipeable    *gtk_hdy_swipe_tracker_get_swipeable (GtkHdySwipeTracker *self);
+
+gboolean         gtk_hdy_swipe_tracker_get_enabled (GtkHdySwipeTracker *self);
+void             gtk_hdy_swipe_tracker_set_enabled (GtkHdySwipeTracker *self,
+                                                    gboolean            enabled);
+
+gboolean         gtk_hdy_swipe_tracker_get_reversed (GtkHdySwipeTracker *self);
+void             gtk_hdy_swipe_tracker_set_reversed (GtkHdySwipeTracker *self,
+                                                     gboolean            reversed);
+
+gboolean         gtk_hdy_swipe_tracker_get_allow_mouse_drag (GtkHdySwipeTracker *self);
+void             gtk_hdy_swipe_tracker_set_allow_mouse_drag (GtkHdySwipeTracker *self,
+                                                             gboolean            allow_mouse_drag);
+
+void             gtk_hdy_swipe_tracker_shift_position (GtkHdySwipeTracker *self,
+                                                       gdouble             delta);
+
+void gtk_hdy_swipe_tracker_emit_begin_swipe (GtkHdySwipeTracker        *self,
+                                             GtkHdyNavigationDirection  direction,
+                                             gboolean                   direct);
+void gtk_hdy_swipe_tracker_emit_update_swipe (GtkHdySwipeTracker *self,
+                                              gdouble             progress);
+void gtk_hdy_swipe_tracker_emit_end_swipe (GtkHdySwipeTracker *self,
+                                           gint64              duration,
+                                           gdouble             to);
+
+G_END_DECLS
diff --git a/gtk/hdy-swipe-tracker.c b/gtk/hdy-swipe-tracker.c
new file mode 100644
index 0000000..9c8161b
--- /dev/null
+++ b/gtk/hdy-swipe-tracker.c
@@ -0,0 +1,1246 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+#include <glib/gi18n-lib.h>
+
+#include "hdy-swipe-tracker-private.h"
+
+#include <gdk/gdk.h>
+#include "gtkbutton.h"
+#include "gtkgesture.h"
+#include "gtkgesturedrag.h"
+#include "gtkmain.h"
+#include "gtkorientable.h"
+#include "gtktypebuiltins.h"
+#include "gtkprivatetypebuiltins.h"
+#include "gtkwidget.h"
+#include "gtkwindow.h"
+
+#include <math.h>
+
+#define TOUCHPAD_BASE_DISTANCE_H 400
+#define TOUCHPAD_BASE_DISTANCE_V 300
+#define SCROLL_MULTIPLIER 10
+#define MIN_ANIMATION_DURATION 100
+#define MAX_ANIMATION_DURATION 400
+#define VELOCITY_THRESHOLD 0.4
+#define DURATION_MULTIPLIER 3
+#define ANIMATION_BASE_VELOCITY 0.002
+#define DRAG_THRESHOLD_DISTANCE 16
+
+/**
+ * SECTION:hdy-swipe-tracker
+ * @short_description: Swipe tracker used in #GtkHdyCarousel and #GtkHdyLeaflet
+ * @title: GtkHdySwipeTracker
+ * @See_also: #GtkHdyCarousel, #GtkHdyDeck, #GtkHdyLeaflet, #GtkHdySwipeable
+ *
+ * The GtkHdySwipeTracker object can be used for implementing widgets with swipe
+ * gestures. It supports touch-based swipes, pointer dragging, and touchpad
+ * scrolling.
+ *
+ * The widgets will probably want to expose #GtkHdySwipeTracker:enabled property.
+ * If they expect to use horizontal orientation, #GtkHdySwipeTracker:reversed
+ * property can be used for supporting RTL text direction.
+ *
+ * Since: 1.0
+ */
+
+typedef enum {
+  GTK_HDY_SWIPE_TRACKER_STATE_NONE,
+  GTK_HDY_SWIPE_TRACKER_STATE_PENDING,
+  GTK_HDY_SWIPE_TRACKER_STATE_SCROLLING,
+  GTK_HDY_SWIPE_TRACKER_STATE_FINISHING,
+  GTK_HDY_SWIPE_TRACKER_STATE_REJECTED,
+} GtkHdySwipeTrackerState;
+
+struct _GtkHdySwipeTracker
+{
+  GObject parent_instance;
+
+  GtkHdySwipeable *swipeable;
+  gboolean enabled;
+  gboolean reversed;
+  gboolean allow_mouse_drag;
+  GtkOrientation orientation;
+
+  gint start_x;
+  gint start_y;
+  gboolean use_capture_phase;
+
+  guint32 prev_time;
+  gdouble velocity;
+
+  gdouble initial_progress;
+  gdouble progress;
+  gboolean cancelled;
+
+  gdouble prev_offset;
+
+  gboolean is_scrolling;
+
+  GtkHdySwipeTrackerState state;
+  GtkGesture *touch_gesture;
+};
+
+G_DEFINE_TYPE_WITH_CODE (GtkHdySwipeTracker, gtk_hdy_swipe_tracker, G_TYPE_OBJECT,
+                         G_IMPLEMENT_INTERFACE (GTK_TYPE_ORIENTABLE, NULL));
+
+enum {
+  PROP_0,
+  PROP_SWIPEABLE,
+  PROP_ENABLED,
+  PROP_REVERSED,
+  PROP_ALLOW_MOUSE_DRAG,
+
+  /* GtkOrientable */
+  PROP_ORIENTATION,
+  LAST_PROP = PROP_ALLOW_MOUSE_DRAG + 1,
+};
+
+static GParamSpec *props[LAST_PROP];
+
+enum {
+  SIGNAL_BEGIN_SWIPE,
+  SIGNAL_UPDATE_SWIPE,
+  SIGNAL_END_SWIPE,
+  SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static gboolean
+get_widget_coordinates (GtkHdySwipeTracker *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 != gtk_widget_get_window (GTK_WIDGET (self->swipeable))) {
+    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
+reset (GtkHdySwipeTracker *self)
+{
+  self->state = GTK_HDY_SWIPE_TRACKER_STATE_NONE;
+
+  self->prev_offset = 0;
+
+  self->initial_progress = 0;
+  self->progress = 0;
+
+  self->start_x = 0;
+  self->start_y = 0;
+  self->use_capture_phase = FALSE;
+
+  self->prev_time = 0;
+  self->velocity = 0;
+
+  self->cancelled = FALSE;
+
+  if (self->swipeable)
+    gtk_grab_remove (GTK_WIDGET (self->swipeable));
+}
+
+static void
+get_range (GtkHdySwipeTracker *self,
+           gdouble            *first,
+           gdouble            *last)
+{
+  g_autofree gdouble *points = NULL;
+  gint n;
+
+  points = gtk_hdy_swipeable_get_snap_points (self->swipeable, &n);
+
+  *first = points[0];
+  *last = points[n - 1];
+}
+
+static void
+gesture_prepare (GtkHdySwipeTracker        *self,
+                 GtkHdyNavigationDirection  direction,
+                 gboolean                   is_drag)
+{
+  GdkRectangle rect;
+
+  if (self->state != GTK_HDY_SWIPE_TRACKER_STATE_NONE)
+    return;
+
+  gtk_hdy_swipeable_get_swipe_area (self->swipeable, direction, is_drag, &rect);
+
+  if (self->start_x < rect.x ||
+      self->start_x >= rect.x + rect.width ||
+      self->start_y < rect.y ||
+      self->start_y >= rect.y + rect.height) {
+    self->state = GTK_HDY_SWIPE_TRACKER_STATE_REJECTED;
+
+    return;
+  }
+
+  gtk_hdy_swipe_tracker_emit_begin_swipe (self, direction, TRUE);
+
+  self->initial_progress = gtk_hdy_swipeable_get_progress (self->swipeable);
+  self->progress = self->initial_progress;
+  self->velocity = 0;
+  self->state = GTK_HDY_SWIPE_TRACKER_STATE_PENDING;
+}
+
+static void
+gesture_begin (GtkHdySwipeTracker *self)
+{
+  g_autoptr (GdkEvent) event = NULL;
+
+  if (self->state != GTK_HDY_SWIPE_TRACKER_STATE_PENDING)
+    return;
+
+  event = gtk_get_current_event ();
+  self->prev_time = gdk_event_get_time (event);
+  self->state = GTK_HDY_SWIPE_TRACKER_STATE_SCROLLING;
+
+  gtk_grab_add (GTK_WIDGET (self->swipeable));
+}
+
+static void
+gesture_update (GtkHdySwipeTracker *self,
+                gdouble             delta)
+{
+  g_autoptr (GdkEvent) event = NULL;
+  guint32 time;
+  gdouble progress;
+  gdouble first_point, last_point;
+
+  if (self->state != GTK_HDY_SWIPE_TRACKER_STATE_SCROLLING)
+    return;
+
+  event = gtk_get_current_event ();
+  time = gdk_event_get_time (event);
+  if (time != self->prev_time)
+    self->velocity = delta / (time - self->prev_time);
+
+  get_range (self, &first_point, &last_point);
+
+  progress = self->progress + delta;
+  progress = CLAMP (progress, first_point, last_point);
+
+  /* FIXME: this is a hack to prevent swiping more than 1 page at once */
+  progress = CLAMP (progress, self->initial_progress - 1, self->initial_progress + 1);
+
+  self->progress = progress;
+
+  gtk_hdy_swipe_tracker_emit_update_swipe (self, progress);
+
+  self->prev_time = time;
+}
+
+static void
+get_closest_snap_points (GtkHdySwipeTracker *self,
+                         gdouble            *upper,
+                         gdouble            *lower)
+{
+  gint i, n;
+  gdouble *points;
+
+  *upper = 0;
+  *lower = 0;
+
+  points = gtk_hdy_swipeable_get_snap_points (self->swipeable, &n);
+
+  for (i = 0; i < n; i++) {
+    if (points[i] >= self->progress) {
+      *upper = points[i];
+      break;
+    }
+  }
+
+  for (i = n - 1; i >= 0; i--) {
+    if (points[i] <= self->progress) {
+      *lower = points[i];
+      break;
+    }
+  }
+
+  g_free (points);
+}
+
+static gdouble
+get_end_progress (GtkHdySwipeTracker *self,
+                  gdouble             distance)
+{
+  gdouble upper, lower, middle;
+
+  if (self->cancelled)
+    return gtk_hdy_swipeable_get_cancel_progress (self->swipeable);
+
+  get_closest_snap_points (self, &upper, &lower);
+  middle = (upper + lower) / 2;
+
+  if (self->progress > middle)
+    return (self->velocity * distance > -VELOCITY_THRESHOLD ||
+            self->initial_progress > upper) ? upper : lower;
+
+  return (self->velocity * distance < VELOCITY_THRESHOLD ||
+          self->initial_progress < lower) ? lower : upper;
+}
+
+static void
+gesture_end (GtkHdySwipeTracker *self,
+             gdouble             distance)
+{
+  gdouble end_progress, velocity;
+  gint64 duration;
+
+  if (self->state == GTK_HDY_SWIPE_TRACKER_STATE_NONE)
+    return;
+
+  end_progress = get_end_progress (self, distance);
+
+  velocity = ANIMATION_BASE_VELOCITY;
+  if ((end_progress - self->progress) * self->velocity > 0)
+    velocity = self->velocity;
+
+  duration = ABS ((self->progress - end_progress) / velocity * DURATION_MULTIPLIER);
+  if (self->progress != end_progress)
+    duration = CLAMP (duration, MIN_ANIMATION_DURATION, MAX_ANIMATION_DURATION);
+
+  gtk_hdy_swipe_tracker_emit_end_swipe (self, duration, end_progress);
+
+  if (self->cancelled)
+    reset (self);
+  else
+    self->state = GTK_HDY_SWIPE_TRACKER_STATE_FINISHING;
+}
+
+static void
+gesture_cancel (GtkHdySwipeTracker *self,
+                gdouble             distance)
+{
+  if (self->state != GTK_HDY_SWIPE_TRACKER_STATE_PENDING &&
+      self->state != GTK_HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+    reset (self);
+
+    return;
+  }
+
+  self->cancelled = TRUE;
+  gesture_end (self, distance);
+}
+
+static void
+drag_begin_cb (GtkHdySwipeTracker *self,
+               gdouble             start_x,
+               gdouble             start_y,
+               GtkGestureDrag     *gesture)
+{
+  if (self->state != GTK_HDY_SWIPE_TRACKER_STATE_NONE)
+    gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+  self->start_x = start_x;
+  self->start_y = start_y;
+}
+
+static void
+drag_update_cb (GtkHdySwipeTracker *self,
+                gdouble             offset_x,
+                gdouble             offset_y,
+                GtkGestureDrag     *gesture)
+{
+  gdouble offset, distance;
+  gboolean is_vertical, is_offset_vertical;
+
+  distance = gtk_hdy_swipeable_get_distance (self->swipeable);
+
+  is_vertical = (self->orientation == GTK_ORIENTATION_VERTICAL);
+  if (is_vertical)
+    offset = -offset_y / distance;
+  else
+    offset = -offset_x / distance;
+
+  if (self->reversed)
+    offset = -offset;
+
+  is_offset_vertical = (ABS (offset_y) > ABS (offset_x));
+
+  if (self->state == GTK_HDY_SWIPE_TRACKER_STATE_REJECTED) {
+    gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+    return;
+  }
+
+  if (self->state == GTK_HDY_SWIPE_TRACKER_STATE_NONE) {
+    if (is_vertical == is_offset_vertical)
+      gesture_prepare (self, offset > 0 ? GTK_HDY_NAVIGATION_DIRECTION_FORWARD : GTK_HDY_NAVIGATION_DIRECTION_BACK, TRUE);
+    else
+      gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+    return;
+  }
+
+  if (self->state == GTK_HDY_SWIPE_TRACKER_STATE_PENDING) {
+    gdouble drag_distance;
+    gdouble first_point, last_point;
+    gboolean is_overshooting;
+
+    get_range (self, &first_point, &last_point);
+
+    drag_distance = sqrt (offset_x * offset_x + offset_y * offset_y);
+    is_overshooting = (offset < 0 && self->progress <= first_point) ||
+                      (offset > 0 && self->progress >= last_point);
+
+    if (drag_distance >= DRAG_THRESHOLD_DISTANCE) {
+      if ((is_vertical == is_offset_vertical) && !is_overshooting) {
+        gesture_begin (self);
+        self->prev_offset = offset;
+        gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_CLAIMED);
+      } else {
+        gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+      }
+    }
+  }
+
+  if (self->state == GTK_HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+    gesture_update (self, offset - self->prev_offset);
+    self->prev_offset = offset;
+  }
+}
+
+static void
+drag_end_cb (GtkHdySwipeTracker *self,
+             gdouble             offset_x,
+             gdouble             offset_y,
+             GtkGestureDrag     *gesture)
+{
+  gdouble distance;
+
+  distance = gtk_hdy_swipeable_get_distance (self->swipeable);
+
+  if (self->state == GTK_HDY_SWIPE_TRACKER_STATE_REJECTED) {
+    gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+
+    reset (self);
+    return;
+  }
+
+  if (self->state != GTK_HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+    gesture_cancel (self, distance);
+    gtk_gesture_set_state (self->touch_gesture, GTK_EVENT_SEQUENCE_DENIED);
+    return;
+  }
+
+  gesture_end (self, distance);
+}
+
+static void
+drag_cancel_cb (GtkHdySwipeTracker *self,
+                GdkEventSequence   *sequence,
+                GtkGesture         *gesture)
+{
+  gdouble distance;
+
+  distance = gtk_hdy_swipeable_get_distance (self->swipeable);
+
+  gesture_cancel (self, distance);
+  gtk_gesture_set_state (gesture, GTK_EVENT_SEQUENCE_DENIED);
+}
+
+static gboolean
+handle_scroll_event (GtkHdySwipeTracker *self,
+                     GdkEvent           *event,
+                     gboolean            capture)
+{
+  GdkDevice *source_device;
+  GdkInputSource input_source;
+  gdouble dx, dy, delta, distance;
+  gboolean is_vertical;
+  gboolean is_delta_vertical;
+
+  is_vertical = (self->orientation == GTK_ORIENTATION_VERTICAL);
+  distance = is_vertical ? TOUCHPAD_BASE_DISTANCE_V : TOUCHPAD_BASE_DISTANCE_H;
+
+  if (gdk_event_get_scroll_direction (event, NULL))
+    return GDK_EVENT_PROPAGATE;
+
+  source_device = gdk_event_get_source_device (event);
+  input_source = gdk_device_get_source (source_device);
+  if (input_source != GDK_SOURCE_TOUCHPAD)
+    return GDK_EVENT_PROPAGATE;
+
+  gdk_event_get_scroll_deltas (event, &dx, &dy);
+  delta = is_vertical ? dy : dx;
+  if (self->reversed)
+    delta = -delta;
+
+  is_delta_vertical = (ABS (dy) > ABS (dx));
+
+  if (self->is_scrolling) {
+    gesture_cancel (self, distance);
+
+    if (gdk_event_is_scroll_stop_event (event))
+      self->is_scrolling = FALSE;
+
+    return GDK_EVENT_PROPAGATE;
+  }
+
+  if (self->state == GTK_HDY_SWIPE_TRACKER_STATE_REJECTED) {
+    if (gdk_event_is_scroll_stop_event (event))
+      reset (self);
+
+    return GDK_EVENT_PROPAGATE;
+  }
+
+  if (self->state == GTK_HDY_SWIPE_TRACKER_STATE_NONE) {
+    if (gdk_event_is_scroll_stop_event (event))
+      return GDK_EVENT_PROPAGATE;
+
+    if (is_vertical == is_delta_vertical) {
+      if (!capture) {
+        gdouble event_x, event_y;
+
+        get_widget_coordinates (self, event, &event_x, &event_y);
+
+        self->start_x = (gint) round (event_x);
+        self->start_y = (gint) round (event_y);
+
+        gesture_prepare (self, delta > 0 ? GTK_HDY_NAVIGATION_DIRECTION_FORWARD : GTK_HDY_NAVIGATION_DIRECTION_BACK, FALSE);
+      }
+    } else {
+      self->is_scrolling = TRUE;
+      return GDK_EVENT_PROPAGATE;
+    }
+  }
+
+  if (!capture && self->state == GTK_HDY_SWIPE_TRACKER_STATE_PENDING) {
+    gboolean is_overshooting;
+    gdouble first_point, last_point;
+
+    get_range (self, &first_point, &last_point);
+
+    is_overshooting = (delta < 0 && self->progress <= first_point) ||
+                      (delta > 0 && self->progress >= last_point);
+
+    if ((is_vertical == is_delta_vertical) && !is_overshooting)
+      gesture_begin (self);
+    else
+      gesture_cancel (self, distance);
+  }
+
+  if (self->state == GTK_HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+    if (gdk_event_is_scroll_stop_event (event)) {
+      gesture_end (self, distance);
+    } else {
+      gesture_update (self, delta / distance * SCROLL_MULTIPLIER);
+      return GDK_EVENT_STOP;
+    }
+  }
+
+  if (!capture && self->state == GTK_HDY_SWIPE_TRACKER_STATE_FINISHING)
+    reset (self);
+
+  return GDK_EVENT_PROPAGATE;
+}
+
+static gboolean
+is_window_handle (GtkWidget *widget)
+{
+  gboolean window_dragging;
+  GtkWidget *parent, *window, *titlebar;
+
+  gtk_widget_style_get (widget, "window-dragging", &window_dragging, NULL);
+
+  if (window_dragging)
+    return TRUE;
+
+  /* Window titlebar area is always draggable, so check if we're inside. */
+  window = gtk_widget_get_toplevel (widget);
+  if (!GTK_IS_WINDOW (window))
+    return FALSE;
+
+  titlebar = gtk_window_get_titlebar (GTK_WINDOW (window));
+  if (!titlebar)
+    return FALSE;
+
+  parent = widget;
+  while (parent && parent != titlebar)
+    parent = gtk_widget_get_parent (parent);
+
+  return parent == titlebar;
+}
+
+static gboolean
+has_conflicts (GtkHdySwipeTracker *self,
+               GtkWidget          *widget)
+{
+  GtkHdySwipeTracker *other;
+
+  if (widget == GTK_WIDGET (self->swipeable))
+    return TRUE;
+
+  if (!GTK_IS_HDY_SWIPEABLE (widget))
+    return FALSE;
+
+  other = gtk_hdy_swipeable_get_swipe_tracker (GTK_HDY_SWIPEABLE (widget));
+
+  return self->orientation == other->orientation;
+}
+
+/* HACK: Since we don't have _gtk_widget_consumes_motion(), we can't do a proper
+ * check for whether we can drag from a widget or not. So we trust the widgets
+ * to propagate or stop their events. However, GtkButton stops press events,
+ * making it impossible to drag from it.
+ */
+static gboolean
+should_force_drag (GtkHdySwipeTracker *self,
+                   GtkWidget          *widget)
+{
+  GtkWidget *parent;
+
+  if (!GTK_IS_BUTTON (widget))
+    return FALSE;
+
+  parent = widget;
+  while (parent && !has_conflicts (self, parent))
+    parent = gtk_widget_get_parent (parent);
+
+  return parent == GTK_WIDGET (self->swipeable);
+}
+
+static gboolean
+handle_event_cb (GtkHdySwipeTracker *self,
+                 GdkEvent           *event)
+{
+  GdkEventSequence *sequence;
+  gboolean retval;
+  GtkEventSequenceState state;
+  GtkWidget *widget;
+
+  if (!self->enabled && self->state != GTK_HDY_SWIPE_TRACKER_STATE_SCROLLING)
+    return GDK_EVENT_PROPAGATE;
+
+  if (self->use_capture_phase)
+    return GDK_EVENT_PROPAGATE;
+
+  if (event->type == GDK_SCROLL)
+    return handle_scroll_event (self, event, FALSE);
+
+  if (event->type != GDK_BUTTON_PRESS &&
+      event->type != GDK_BUTTON_RELEASE &&
+      event->type != GDK_MOTION_NOTIFY &&
+      event->type != GDK_TOUCH_BEGIN &&
+      event->type != GDK_TOUCH_END &&
+      event->type != GDK_TOUCH_UPDATE &&
+      event->type != GDK_TOUCH_CANCEL)
+    return GDK_EVENT_PROPAGATE;
+
+  widget = gtk_get_event_widget (event);
+  if (is_window_handle (widget))
+    return GDK_EVENT_PROPAGATE;
+
+  sequence = gdk_event_get_event_sequence (event);
+  retval = gtk_event_controller_handle_event (GTK_EVENT_CONTROLLER (self->touch_gesture), event);
+  state = gtk_gesture_get_sequence_state (self->touch_gesture, sequence);
+
+  if (state == GTK_EVENT_SEQUENCE_DENIED) {
+    gtk_event_controller_reset (GTK_EVENT_CONTROLLER (self->touch_gesture));
+    return GDK_EVENT_PROPAGATE;
+  }
+
+  if (self->state == GTK_HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+    return GDK_EVENT_STOP;
+  } else if (self->state == GTK_HDY_SWIPE_TRACKER_STATE_FINISHING) {
+    reset (self);
+    return GDK_EVENT_STOP;
+  }
+  return retval;
+}
+
+static gboolean
+captured_event_cb (GtkHdySwipeable *swipeable,
+                   GdkEvent        *event)
+{
+  GtkHdySwipeTracker *self = gtk_hdy_swipeable_get_swipe_tracker (swipeable);
+  GtkWidget *widget;
+  GdkEventSequence *sequence;
+  gboolean retval;
+  GtkEventSequenceState state;
+
+  g_assert (GTK_IS_HDY_SWIPE_TRACKER (self));
+
+  if (!self->enabled && self->state != GTK_HDY_SWIPE_TRACKER_STATE_SCROLLING)
+    return GDK_EVENT_PROPAGATE;
+
+  if (event->type == GDK_SCROLL)
+    return handle_scroll_event (self, event, TRUE);
+
+  if (event->type != GDK_BUTTON_PRESS &&
+      event->type != GDK_BUTTON_RELEASE &&
+      event->type != GDK_MOTION_NOTIFY &&
+      event->type != GDK_TOUCH_BEGIN &&
+      event->type != GDK_TOUCH_END &&
+      event->type != GDK_TOUCH_UPDATE &&
+      event->type != GDK_TOUCH_CANCEL)
+    return GDK_EVENT_PROPAGATE;
+
+  widget = gtk_get_event_widget (event);
+
+  if (!self->use_capture_phase && !should_force_drag (self, widget))
+    return GDK_EVENT_PROPAGATE;
+
+  sequence = gdk_event_get_event_sequence (event);
+
+  if (gtk_gesture_handles_sequence (self->touch_gesture, sequence))
+    self->use_capture_phase = TRUE;
+
+  retval = gtk_event_controller_handle_event (GTK_EVENT_CONTROLLER (self->touch_gesture), event);
+  state = gtk_gesture_get_sequence_state (self->touch_gesture, sequence);
+
+  if (state == GTK_EVENT_SEQUENCE_DENIED) {
+    gtk_event_controller_reset (GTK_EVENT_CONTROLLER (self->touch_gesture));
+    return GDK_EVENT_PROPAGATE;
+  }
+
+  if (self->state == GTK_HDY_SWIPE_TRACKER_STATE_SCROLLING) {
+    return GDK_EVENT_STOP;
+  } else if (self->state == GTK_HDY_SWIPE_TRACKER_STATE_FINISHING) {
+    reset (self);
+    return GDK_EVENT_STOP;
+  }
+
+  return retval;
+}
+
+static void
+gtk_hdy_swipe_tracker_constructed (GObject *object)
+{
+  GtkHdySwipeTracker *self = GTK_HDY_SWIPE_TRACKER (object);
+
+  g_assert (self->swipeable);
+
+  gtk_widget_add_events (GTK_WIDGET (self->swipeable),
+                         GDK_SMOOTH_SCROLL_MASK |
+                         GDK_BUTTON_PRESS_MASK |
+                         GDK_BUTTON_RELEASE_MASK |
+                         GDK_BUTTON_MOTION_MASK |
+                         GDK_TOUCH_MASK);
+
+  self->touch_gesture = g_object_new (GTK_TYPE_GESTURE_DRAG,
+                                      "widget", self->swipeable,
+                                      "propagation-phase", GTK_PHASE_NONE,
+                                      "touch-only", !self->allow_mouse_drag,
+                                      NULL);
+
+  g_signal_connect_swapped (self->touch_gesture, "drag-begin", G_CALLBACK (drag_begin_cb), self);
+  g_signal_connect_swapped (self->touch_gesture, "drag-update", G_CALLBACK (drag_update_cb), self);
+  g_signal_connect_swapped (self->touch_gesture, "drag-end", G_CALLBACK (drag_end_cb), self);
+  g_signal_connect_swapped (self->touch_gesture, "cancel", G_CALLBACK (drag_cancel_cb), self);
+
+  g_signal_connect_object (self->swipeable, "event", G_CALLBACK (handle_event_cb), self, G_CONNECT_SWAPPED);
+  g_signal_connect_object (self->swipeable, "unrealize", G_CALLBACK (reset), self, G_CONNECT_SWAPPED);
+
+  /*
+   * HACK: GTK3 has no other way to get events on capture phase.
+   * This is a reimplementation of _gtk_widget_set_captured_event_handler(),
+   * which is private. In GTK4 it can be replaced with GtkEventControllerLegacy
+   * with capture propagation phase
+   */
+  g_object_set_data (G_OBJECT (self->swipeable), "captured-event-handler", captured_event_cb);
+
+  G_OBJECT_CLASS (gtk_hdy_swipe_tracker_parent_class)->constructed (object);
+}
+
+static void
+gtk_hdy_swipe_tracker_dispose (GObject *object)
+{
+  GtkHdySwipeTracker *self = GTK_HDY_SWIPE_TRACKER (object);
+
+  if (self->swipeable)
+    gtk_grab_remove (GTK_WIDGET (self->swipeable));
+
+  if (self->touch_gesture)
+    g_signal_handlers_disconnect_by_data (self->touch_gesture, self);
+
+  g_object_set_data (G_OBJECT (self->swipeable), "captured-event-handler", NULL);
+
+  g_clear_object (&self->touch_gesture);
+  g_clear_object (&self->swipeable);
+
+  G_OBJECT_CLASS (gtk_hdy_swipe_tracker_parent_class)->dispose (object);
+}
+
+static void
+gtk_hdy_swipe_tracker_get_property (GObject    *object,
+                                guint       prop_id,
+                                GValue     *value,
+                                GParamSpec *pspec)
+{
+  GtkHdySwipeTracker *self = GTK_HDY_SWIPE_TRACKER (object);
+
+  switch (prop_id) {
+  case PROP_SWIPEABLE:
+    g_value_set_object (value, gtk_hdy_swipe_tracker_get_swipeable (self));
+    break;
+
+  case PROP_ENABLED:
+    g_value_set_boolean (value, gtk_hdy_swipe_tracker_get_enabled (self));
+    break;
+
+  case PROP_REVERSED:
+    g_value_set_boolean (value, gtk_hdy_swipe_tracker_get_reversed (self));
+    break;
+
+  case PROP_ALLOW_MOUSE_DRAG:
+    g_value_set_boolean (value, gtk_hdy_swipe_tracker_get_allow_mouse_drag (self));
+    break;
+
+  case PROP_ORIENTATION:
+    g_value_set_enum (value, self->orientation);
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+gtk_hdy_swipe_tracker_set_property (GObject      *object,
+                                guint         prop_id,
+                                const GValue *value,
+                                GParamSpec   *pspec)
+{
+  GtkHdySwipeTracker *self = GTK_HDY_SWIPE_TRACKER (object);
+
+  switch (prop_id) {
+  case PROP_SWIPEABLE:
+    self->swipeable = GTK_HDY_SWIPEABLE (g_object_ref (g_value_get_object (value)));
+    break;
+
+  case PROP_ENABLED:
+    gtk_hdy_swipe_tracker_set_enabled (self, g_value_get_boolean (value));
+    break;
+
+  case PROP_REVERSED:
+    gtk_hdy_swipe_tracker_set_reversed (self, g_value_get_boolean (value));
+    break;
+
+  case PROP_ALLOW_MOUSE_DRAG:
+    gtk_hdy_swipe_tracker_set_allow_mouse_drag (self, g_value_get_boolean (value));
+    break;
+
+  case PROP_ORIENTATION:
+    {
+      GtkOrientation orientation = g_value_get_enum (value);
+      if (orientation != self->orientation) {
+        self->orientation = g_value_get_enum (value);
+        g_object_notify (G_OBJECT (self), "orientation");
+      }
+    }
+    break;
+
+  default:
+    G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+  }
+}
+
+static void
+gtk_hdy_swipe_tracker_class_init (GtkHdySwipeTrackerClass *klass)
+{
+  GObjectClass *object_class = G_OBJECT_CLASS (klass);
+
+  object_class->constructed = gtk_hdy_swipe_tracker_constructed;
+  object_class->dispose = gtk_hdy_swipe_tracker_dispose;
+  object_class->get_property = gtk_hdy_swipe_tracker_get_property;
+  object_class->set_property = gtk_hdy_swipe_tracker_set_property;
+
+  /**
+   * GtkHdySwipeTracker:swipeable:
+   *
+   * The widget the swipe tracker is attached to. Must not be %NULL.
+   *
+   * Since: 1.0
+   */
+  props[PROP_SWIPEABLE] =
+    g_param_spec_object ("swipeable",
+                         _("Swipeable"),
+                         _("The swipeable the swipe tracker is attached to"),
+                         GTK_TYPE_HDY_SWIPEABLE,
+                         G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY);
+
+  /**
+   * GtkHdySwipeTracker:enabled:
+   *
+   * Whether the swipe tracker is enabled. When it's not enabled, no events
+   * will be processed. Usually widgets will want to expose this via a property.
+   *
+   * Since: 1.0
+   */
+  props[PROP_ENABLED] =
+    g_param_spec_boolean ("enabled",
+                          _("Enabled"),
+                          _("Whether the swipe tracker processes events"),
+                          TRUE,
+                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * GtkHdySwipeTracker:reversed:
+   *
+   * Whether to reverse the swipe direction. If the swipe tracker is horizontal,
+   * it can be used for supporting RTL text direction.
+   *
+   * Since: 1.0
+   */
+  props[PROP_REVERSED] =
+    g_param_spec_boolean ("reversed",
+                          _("Reversed"),
+                          _("Whether swipe direction is reversed"),
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  /**
+   * GtkHdySwipeTracker:allow-mouse-drag:
+   *
+   * Whether to allow dragging with mouse pointer. This should usually be
+   * %FALSE.
+   *
+   * Since: 1.0
+   */
+  props[PROP_ALLOW_MOUSE_DRAG] =
+    g_param_spec_boolean ("allow-mouse-drag",
+                          _("Allow mouse drag"),
+                          _("Whether to allow dragging with mouse pointer"),
+                          FALSE,
+                          G_PARAM_READWRITE | G_PARAM_EXPLICIT_NOTIFY);
+
+  g_object_class_override_property (object_class,
+                                    PROP_ORIENTATION,
+                                    "orientation");
+
+  g_object_class_install_properties (object_class, LAST_PROP, props);
+
+  /**
+   * GtkHdySwipeTracker::begin-swipe:
+   * @self: The #GtkHdySwipeTracker instance
+   * @direction: The direction of the swipe
+   * @direct: %TRUE if the swipe is directly triggered by a gesture,
+   *   %FALSE if it's triggered via a #GtkHdySwipeGroup
+   *
+   * This signal is emitted when a possible swipe is detected.
+   *
+   * The @direction value can be used to restrict the swipe to a certain
+   * direction.
+   *
+   * Since: 1.0
+   */
+  signals[SIGNAL_BEGIN_SWIPE] =
+    g_signal_new ("begin-swipe",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_FIRST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2,
+                  GTK_TYPE_HDY_NAVIGATION_DIRECTION, G_TYPE_BOOLEAN);
+
+  /**
+   * GtkHdySwipeTracker::update-swipe:
+   * @self: The #GtkHdySwipeTracker instance
+   * @progress: The current animation progress value
+   *
+   * This signal is emitted every time the progress value changes.
+   *
+   * Since: 1.0
+   */
+  signals[SIGNAL_UPDATE_SWIPE] =
+    g_signal_new ("update-swipe",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_FIRST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  1,
+                  G_TYPE_DOUBLE);
+
+  /**
+   * GtkHdySwipeTracker::end-swipe:
+   * @self: The #GtkHdySwipeTracker instance
+   * @duration: Snap-back animation duration in milliseconds
+   * @to: The progress value to animate to
+   *
+   * This signal is emitted as soon as the gesture has stopped.
+   *
+   * Since: 1.0
+   */
+  signals[SIGNAL_END_SWIPE] =
+    g_signal_new ("end-swipe",
+                  G_TYPE_FROM_CLASS (klass),
+                  G_SIGNAL_RUN_FIRST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2,
+                  G_TYPE_INT64, G_TYPE_DOUBLE);
+}
+
+static void
+gtk_hdy_swipe_tracker_init (GtkHdySwipeTracker *self)
+{
+  reset (self);
+  self->orientation = GTK_ORIENTATION_HORIZONTAL;
+  self->enabled = TRUE;
+}
+
+/**
+ * gtk_hdy_swipe_tracker_new:
+ * @swipeable: a #GtkWidget to add the tracker on
+ *
+ * Create a new #GtkHdySwipeTracker object on @widget.
+ *
+ * Returns: the newly created #GtkHdySwipeTracker object
+ *
+ * Since: 1.0
+ */
+GtkHdySwipeTracker *
+gtk_hdy_swipe_tracker_new (GtkHdySwipeable *swipeable)
+{
+  g_return_val_if_fail (GTK_IS_HDY_SWIPEABLE (swipeable), NULL);
+
+  return g_object_new (GTK_TYPE_HDY_SWIPE_TRACKER,
+                       "swipeable", swipeable,
+                       NULL);
+}
+
+/**
+ * gtk_hdy_swipe_tracker_get_swipeable:
+ * @self: a #GtkHdySwipeTracker
+ *
+ * Get @self's swipeable widget.
+ *
+ * Returns: (transfer none): the swipeable widget
+ *
+ * Since: 1.0
+ */
+GtkHdySwipeable *
+gtk_hdy_swipe_tracker_get_swipeable (GtkHdySwipeTracker *self)
+{
+  g_return_val_if_fail (GTK_IS_HDY_SWIPE_TRACKER (self), NULL);
+
+  return self->swipeable;
+}
+
+/**
+ * gtk_hdy_swipe_tracker_get_enabled:
+ * @self: a #GtkHdySwipeTracker
+ *
+ * Get whether @self is enabled. When it's not enabled, no events will be
+ * processed. Generally widgets will want to expose this via a property.
+ *
+ * Returns: %TRUE if @self is enabled
+ *
+ * Since: 1.0
+ */
+gboolean
+gtk_hdy_swipe_tracker_get_enabled (GtkHdySwipeTracker *self)
+{
+  g_return_val_if_fail (GTK_IS_HDY_SWIPE_TRACKER (self), FALSE);
+
+  return self->enabled;
+}
+
+/**
+ * gtk_hdy_swipe_tracker_set_enabled:
+ * @self: a #GtkHdySwipeTracker
+ * @enabled: whether to enable to swipe tracker
+ *
+ * Set whether @self is enabled. When it's not enabled, no events will be
+ * processed. Usually widgets will want to expose this via a property.
+ *
+ * Since: 1.0
+ */
+void
+gtk_hdy_swipe_tracker_set_enabled (GtkHdySwipeTracker *self,
+                                   gboolean            enabled)
+{
+  g_return_if_fail (GTK_IS_HDY_SWIPE_TRACKER (self));
+
+  enabled = !!enabled;
+
+  if (self->enabled == enabled)
+    return;
+
+  self->enabled = enabled;
+
+  if (!enabled && self->state != GTK_HDY_SWIPE_TRACKER_STATE_SCROLLING)
+    reset (self);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ENABLED]);
+}
+
+/**
+ * gtk_hdy_swipe_tracker_get_reversed:
+ * @self: a #GtkHdySwipeTracker
+ *
+ * Get whether @self is reversing the swipe direction.
+ *
+ * Returns: %TRUE is the direction is reversed
+ *
+ * Since: 1.0
+ */
+gboolean
+gtk_hdy_swipe_tracker_get_reversed (GtkHdySwipeTracker *self)
+{
+  g_return_val_if_fail (GTK_IS_HDY_SWIPE_TRACKER (self), FALSE);
+
+  return self->reversed;
+}
+
+/**
+ * gtk_hdy_swipe_tracker_set_reversed:
+ * @self: a #GtkHdySwipeTracker
+ * @reversed: whether to reverse the swipe direction
+ *
+ * Set whether to reverse the swipe direction. If @self is horizontal,
+ * can be used for supporting RTL text direction.
+ *
+ * Since: 1.0
+ */
+void
+gtk_hdy_swipe_tracker_set_reversed (GtkHdySwipeTracker *self,
+                                    gboolean            reversed)
+{
+  g_return_if_fail (GTK_IS_HDY_SWIPE_TRACKER (self));
+
+  reversed = !!reversed;
+
+  if (self->reversed == reversed)
+    return;
+
+  self->reversed = reversed;
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_REVERSED]);
+}
+
+/**
+ * gtk_hdy_swipe_tracker_get_allow_mouse_drag:
+ * @self: a #GtkHdySwipeTracker
+ *
+ * Get whether @self can be dragged with mouse pointer.
+ *
+ * Returns: %TRUE is mouse dragging is allowed
+ *
+ * Since: 1.0
+ */
+gboolean
+gtk_hdy_swipe_tracker_get_allow_mouse_drag (GtkHdySwipeTracker *self)
+{
+  g_return_val_if_fail (GTK_IS_HDY_SWIPE_TRACKER (self), FALSE);
+
+  return self->allow_mouse_drag;
+}
+
+/**
+ * gtk_hdy_swipe_tracker_set_allow_mouse_drag:
+ * @self: a #GtkHdySwipeTracker
+ * @allow_mouse_drag: whether to allow mouse dragging
+ *
+ * Set whether @self can be dragged with mouse pointer. This should usually be
+ * %FALSE.
+ *
+ * Since: 1.0
+ */
+void
+gtk_hdy_swipe_tracker_set_allow_mouse_drag (GtkHdySwipeTracker *self,
+                                            gboolean            allow_mouse_drag)
+{
+  g_return_if_fail (GTK_IS_HDY_SWIPE_TRACKER (self));
+
+  allow_mouse_drag = !!allow_mouse_drag;
+
+  if (self->allow_mouse_drag == allow_mouse_drag)
+    return;
+
+  self->allow_mouse_drag = allow_mouse_drag;
+
+  if (self->touch_gesture)
+    g_object_set (self->touch_gesture, "touch-only", !allow_mouse_drag, NULL);
+
+  g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ALLOW_MOUSE_DRAG]);
+}
+
+/**
+ * gtk_hdy_swipe_tracker_shift_position:
+ * @self: a #GtkHdySwipeTracker
+ * @delta: the position delta
+ *
+ * Move the current progress value by @delta. This can be used to adjust the
+ * current position if snap points move during the gesture.
+ *
+ * Since: 1.0
+ */
+void
+gtk_hdy_swipe_tracker_shift_position (GtkHdySwipeTracker *self,
+                                      gdouble             delta)
+{
+  g_return_if_fail (GTK_IS_HDY_SWIPE_TRACKER (self));
+
+  if (self->state != GTK_HDY_SWIPE_TRACKER_STATE_PENDING &&
+      self->state != GTK_HDY_SWIPE_TRACKER_STATE_SCROLLING)
+    return;
+
+  self->progress += delta;
+  self->initial_progress += delta;
+}
+
+void
+gtk_hdy_swipe_tracker_emit_begin_swipe (GtkHdySwipeTracker        *self,
+                                        GtkHdyNavigationDirection  direction,
+                                        gboolean                   direct)
+{
+  g_return_if_fail (GTK_IS_HDY_SWIPE_TRACKER (self));
+
+  g_signal_emit (self, signals[SIGNAL_BEGIN_SWIPE], 0, direction, direct);
+}
+
+void
+gtk_hdy_swipe_tracker_emit_update_swipe (GtkHdySwipeTracker *self,
+                                         gdouble             progress)
+{
+  g_return_if_fail (GTK_IS_HDY_SWIPE_TRACKER (self));
+
+  g_signal_emit (self, signals[SIGNAL_UPDATE_SWIPE], 0, progress);
+}
+
+void
+gtk_hdy_swipe_tracker_emit_end_swipe (GtkHdySwipeTracker *self,
+                                      gint64              duration,
+                                      gdouble             to)
+{
+  g_return_if_fail (GTK_IS_HDY_SWIPE_TRACKER (self));
+
+  g_signal_emit (self, signals[SIGNAL_END_SWIPE], 0, duration, to);
+}
diff --git a/gtk/hdy-swipeable-private.h b/gtk/hdy-swipeable-private.h
new file mode 100644
index 0000000..3c7ac4e
--- /dev/null
+++ b/gtk/hdy-swipeable-private.h
@@ -0,0 +1,82 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#pragma once
+
+#if !defined (__GTK_H_INSIDE__) && !defined (GTK_COMPILATION)
+#error "Only <gtk/gtk.h> can be included directly."
+#endif
+
+#include <gdk/gdk.h>
+#include "gtkwidget.h"
+#include "hdy-navigation-direction-private.h"
+
+G_BEGIN_DECLS
+
+typedef struct _GtkHdySwipeTracker GtkHdySwipeTracker;
+
+#define GTK_TYPE_HDY_SWIPEABLE (gtk_hdy_swipeable_get_type ())
+
+G_DECLARE_INTERFACE (GtkHdySwipeable, gtk_hdy_swipeable, GTK, HDY_SWIPEABLE, GtkWidget)
+
+/**
+ * GtkHdySwipeableInterface:
+ * @parent: The parent interface.
+ * @switch_child: Switches visible child.
+ * @get_swipe_tracker: Gets the swipe tracker.
+ * @get_distance: Gets the swipe distance.
+ * @get_snap_points: Gets the snap points
+ * @get_progress: Gets the current progress.
+ * @get_cancel_progress: Gets the cancel progress.
+ * @get_swipe_area: Gets the swipeable rectangle.
+ *
+ * An interface for swipeable widgets.
+ *
+ * Since: 1.0
+ **/
+struct _GtkHdySwipeableInterface
+{
+  GTypeInterface parent;
+
+  void (*switch_child) (GtkHdySwipeable *self,
+                        guint            index,
+                        gint64           duration);
+
+  GtkHdySwipeTracker * (*get_swipe_tracker)   (GtkHdySwipeable *self);
+  gdouble              (*get_distance)        (GtkHdySwipeable *self);
+  gdouble *            (*get_snap_points)     (GtkHdySwipeable *self,
+                                               gint            *n_snap_points);
+  gdouble              (*get_progress)        (GtkHdySwipeable *self);
+  gdouble              (*get_cancel_progress) (GtkHdySwipeable *self);
+  void                 (*get_swipe_area)      (GtkHdySwipeable           *self,
+                                               GtkHdyNavigationDirection  navigation_direction,
+                                               gboolean                   is_drag,
+                                               GdkRectangle              *rect);
+
+  /*< private >*/
+  gpointer padding[4];
+};
+
+void gtk_hdy_swipeable_switch_child (GtkHdySwipeable *self,
+                                     guint            index,
+                                     gint64           duration);
+
+void gtk_hdy_swipeable_emit_child_switched (GtkHdySwipeable *self,
+                                            guint            index,
+                                            gint64           duration);
+
+GtkHdySwipeTracker *gtk_hdy_swipeable_get_swipe_tracker (GtkHdySwipeable *self);
+gdouble             gtk_hdy_swipeable_get_distance        (GtkHdySwipeable *self);
+gdouble            *gtk_hdy_swipeable_get_snap_points     (GtkHdySwipeable *self,
+                                                           gint            *n_snap_points);
+gdouble             gtk_hdy_swipeable_get_progress        (GtkHdySwipeable *self);
+gdouble             gtk_hdy_swipeable_get_cancel_progress (GtkHdySwipeable *self);
+void                gtk_hdy_swipeable_get_swipe_area      (GtkHdySwipeable           *self,
+                                                           GtkHdyNavigationDirection  navigation_direction,
+                                                           gboolean                   is_drag,
+                                                           GdkRectangle              *rect);
+
+G_END_DECLS
diff --git a/gtk/hdy-swipeable.c b/gtk/hdy-swipeable.c
new file mode 100644
index 0000000..916abd9
--- /dev/null
+++ b/gtk/hdy-swipeable.c
@@ -0,0 +1,274 @@
+/*
+ * Copyright (C) 2019 Alexander Mikhaylenko <exalm7659@gmail.com>
+ *
+ * SPDX-License-Identifier: LGPL-2.1+
+ */
+
+#include "config.h"
+
+#include "hdy-swipeable-private.h"
+#include "hdy-swipe-tracker-private.h"
+
+/**
+ * SECTION:gtk_hdy-swipeable
+ * @short_description: An interface for swipeable widgets.
+ * @title: GtkHdySwipeable
+ * @See_also: #GtkHdyCarousel, #GtkHdyDeck, #GtkHdyLeaflet, #GtkHdySwipeGroup
+ *
+ * The #GtkHdySwipeable interface is implemented by all swipeable widgets. They
+ * can be synced using #GtkHdySwipeGroup.
+ *
+ * See #GtkHdySwipeTracker for details about implementing it.
+ *
+ * Since: 0.0.12
+ */
+
+G_DEFINE_INTERFACE (GtkHdySwipeable, gtk_hdy_swipeable, GTK_TYPE_WIDGET)
+
+enum {
+  SIGNAL_CHILD_SWITCHED,
+  SIGNAL_LAST_SIGNAL,
+};
+
+static guint signals[SIGNAL_LAST_SIGNAL];
+
+static void
+gtk_hdy_swipeable_default_init (GtkHdySwipeableInterface *iface)
+{
+  /**
+   * GtkHdySwipeable::child-switched:
+   * @self: The #GtkHdySwipeable instance
+   * @index: the index of the child to switch to
+   * @duration: Animation duration in milliseconds
+   *
+   * This signal should be emitted when the widget's visible child is changed.
+   *
+   * @duration can be 0 if the child is switched without animation.
+   *
+   * This is used by #GtkHdySwipeGroup, applications should not connect to it.
+   *
+   * Since: 1.0
+   */
+  signals[SIGNAL_CHILD_SWITCHED] =
+    g_signal_new ("child-switched",
+                  G_TYPE_FROM_INTERFACE (iface),
+                  G_SIGNAL_RUN_FIRST,
+                  0,
+                  NULL, NULL, NULL,
+                  G_TYPE_NONE,
+                  2,
+                  G_TYPE_UINT, G_TYPE_INT64);
+}
+
+/**
+ * gtk_hdy_swipeable_switch_child:
+ * @self: a #GtkHdySwipeable
+ * @index: the index of the child to switch to
+ * @duration: Animation duration in milliseconds
+ *
+ * See GtkHdySwipeable::child-switched.
+ *
+ * Since: 1.0
+ */
+void
+gtk_hdy_swipeable_switch_child (GtkHdySwipeable *self,
+                                guint            index,
+                                gint64           duration)
+{
+  GtkHdySwipeableInterface *iface;
+
+  g_return_if_fail (GTK_IS_HDY_SWIPEABLE (self));
+
+  iface = GTK_HDY_SWIPEABLE_GET_IFACE (self);
+  g_return_if_fail (iface->switch_child != NULL);
+
+  iface->switch_child (self, index, duration);
+}
+
+/**
+ * gtk_hdy_swipeable_emit_child_switched:
+ * @self: a #GtkHdySwipeable
+ * @index: the index of the child to switch to
+ * @duration: Animation duration in milliseconds
+ *
+ * Emits GtkHdySwipeable::child-switched signal. This should be called when the
+ * widget switches visible child widget.
+ *
+ * @duration can be 0 if the child is switched without animation.
+ *
+ * Since: 1.0
+ */
+void
+gtk_hdy_swipeable_emit_child_switched (GtkHdySwipeable *self,
+                                       guint            index,
+                                       gint64           duration)
+{
+  g_return_if_fail (GTK_IS_HDY_SWIPEABLE (self));
+
+  g_signal_emit (self, signals[SIGNAL_CHILD_SWITCHED], 0, index, duration);
+}
+
+/**
+ * gtk_hdy_swipeable_get_swipe_tracker:
+ * @self: a #GtkHdySwipeable
+ *
+ * Gets the #GtkHdySwipeTracker used by this swipeable widget.
+ *
+ * Returns: (transfer none): the swipe tracker
+ *
+ * Since: 1.0
+ */
+GtkHdySwipeTracker *
+gtk_hdy_swipeable_get_swipe_tracker (GtkHdySwipeable *self)
+{
+  GtkHdySwipeableInterface *iface;
+
+  g_return_val_if_fail (GTK_IS_HDY_SWIPEABLE (self), NULL);
+
+  iface = GTK_HDY_SWIPEABLE_GET_IFACE (self);
+  g_return_val_if_fail (iface->get_swipe_tracker != NULL, NULL);
+
+  return iface->get_swipe_tracker (self);
+}
+
+/**
+ * gtk_hdy_swipeable_get_distance:
+ * @self: a #GtkHdySwipeable
+ *
+ * Gets the swipe distance of @self. This corresponds to how many pixels
+ * 1 unit represents.
+ *
+ * Returns: the swipe distance in pixels
+ *
+ * Since: 1.0
+ */
+gdouble
+gtk_hdy_swipeable_get_distance (GtkHdySwipeable *self)
+{
+  GtkHdySwipeableInterface *iface;
+
+  g_return_val_if_fail (GTK_IS_HDY_SWIPEABLE (self), 0);
+
+  iface = GTK_HDY_SWIPEABLE_GET_IFACE (self);
+  g_return_val_if_fail (iface->get_distance != NULL, 0);
+
+  return iface->get_distance (self);
+}
+
+/**
+ * gtk_hdy_swipeable_get_snap_points: (virtual get_snap_points)
+ * @self: a #GtkHdySwipeable
+ * @n_snap_points: (out): location to return the number of the snap points
+ *
+ * Gets the snap points of @self. Each snap point represents a progress value
+ * that is considered acceptable to end the swipe on.
+ *
+ * Returns: (array length=n_snap_points) (transfer full): the snap points of
+ *     @self. The array must be freed with g_free().
+ *
+ * Since: 1.0
+ */
+gdouble *
+gtk_hdy_swipeable_get_snap_points (GtkHdySwipeable *self,
+                                   gint            *n_snap_points)
+{
+  GtkHdySwipeableInterface *iface;
+
+  g_return_val_if_fail (GTK_IS_HDY_SWIPEABLE (self), NULL);
+
+  iface = GTK_HDY_SWIPEABLE_GET_IFACE (self);
+  g_return_val_if_fail (iface->get_snap_points != NULL, NULL);
+
+  return iface->get_snap_points (self, n_snap_points);
+}
+
+/**
+ * gtk_hdy_swipeable_get_progress:
+ * @self: a #GtkHdySwipeable
+ *
+ * Gets the current progress of @self
+ *
+ * Returns: the current progress, unitless
+ *
+ * Since: 1.0
+ */
+gdouble
+gtk_hdy_swipeable_get_progress (GtkHdySwipeable *self)
+{
+  GtkHdySwipeableInterface *iface;
+
+  g_return_val_if_fail (GTK_IS_HDY_SWIPEABLE (self), 0);
+
+  iface = GTK_HDY_SWIPEABLE_GET_IFACE (self);
+  g_return_val_if_fail (iface->get_progress != NULL, 0);
+
+  return iface->get_progress (self);
+}
+
+/**
+ * gtk_hdy_swipeable_get_cancel_progress:
+ * @self: a #GtkHdySwipeable
+ *
+ * Gets the progress @self will snap back to after the gesture is canceled.
+ *
+ * Returns: the cancel progress, unitless
+ *
+ * Since: 1.0
+ */
+gdouble
+gtk_hdy_swipeable_get_cancel_progress (GtkHdySwipeable *self)
+{
+  GtkHdySwipeableInterface *iface;
+
+  g_return_val_if_fail (GTK_IS_HDY_SWIPEABLE (self), 0);
+
+  iface = GTK_HDY_SWIPEABLE_GET_IFACE (self);
+  g_return_val_if_fail (iface->get_cancel_progress != NULL, 0);
+
+  return iface->get_cancel_progress (self);
+}
+
+/**
+ * gtk_hdy_swipeable_get_swipe_area:
+ * @self: a #GtkHdySwipeable
+ * @navigation_direction: the direction of the swipe
+ * @is_drag: whether the swipe is caused by a dragging gesture
+ * @rect: (out): a pointer to a #GdkRectangle to store the swipe area
+ *
+ * Gets the area @self can start a swipe from for the given direction and
+ * gesture type.
+ * This can be used to restrict swipes to only be possible from a certain area,
+ * for example, to only allow edge swipes, or to have a draggable element and
+ * ignore swipes elsewhere.
+ *
+ * Swipe area is only considered for direct swipes (as in, not initiated by
+ * #GtkHdySwipeGroup).
+ *
+ * If not implemented, the default implementation returns the allocation of
+ * @self, allowing swipes from anywhere.
+ *
+ * Since: 1.0
+ */
+void
+gtk_hdy_swipeable_get_swipe_area (GtkHdySwipeable           *self,
+                                  GtkHdyNavigationDirection  navigation_direction,
+                                  gboolean                   is_drag,
+                                  GdkRectangle              *rect)
+{
+  GtkHdySwipeableInterface *iface;
+
+  g_return_if_fail (GTK_IS_HDY_SWIPEABLE (self));
+  g_return_if_fail (rect != NULL);
+
+  iface = GTK_HDY_SWIPEABLE_GET_IFACE (self);
+
+  if (iface->get_swipe_area) {
+    iface->get_swipe_area (self, navigation_direction, is_drag, rect);
+    return;
+  }
+
+  rect->x = 0;
+  rect->y = 0;
+  rect->width = gtk_widget_get_allocated_width (GTK_WIDGET (self));
+  rect->height = gtk_widget_get_allocated_height (GTK_WIDGET (self));
+}
diff --git a/gtk/meson.build b/gtk/meson.build
index 13af887..9c6edc7 100644
--- a/gtk/meson.build
+++ b/gtk/meson.build
@@ -388,6 +388,8 @@ gtk_sources = files(
   'hdy-navigation-direction.c',
   'hdy-shadow-helper.c',
   'hdy-squeezer.c',
+  'hdy-swipeable.c',
+  'hdy-swipe-tracker.c',
   'hdy-view-switcher-bar.c',
   'hdy-view-switcher-button.c',
   'hdy-view-switcher.c',
@@ -405,6 +407,8 @@ gtk_private_type_headers = files(
   'hdy-navigation-direction-private.h',
   'hdy-shadow-helper-private.h',
   'hdy-squeezer-private.h',
+  'hdy-swipeable-private.h',
+  'hdy-swipe-tracker-private.h',
   'hdy-view-switcher-bar-private.h',
   'hdy-view-switcher-button-private.h',
   'hdy-view-switcher-private.h',
openSUSE Build Service is sponsored by