phosh-arch/gtk3-mobile/Add-GtkHdySwipeable-and-GtkHdySwipeTracker.patch

1718 lines
50 KiB
Diff
Raw Normal View History

2024-08-13 17:17:11 +02:00
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 7407f5b..ae63ff0 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',