Created
May 3, 2025 17:14
-
-
Save altacountbabi/c7bb9e2987c7921dbf433cb162f9fb41 to your computer and use it in GitHub Desktop.
A patch for gtk4 to add basic interpolation between scrolls
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
diff --git a/gtk/gtkscrolledwindow.c b/gtk/gtkscrolledwindow.c | |
index f1b5b5112e0455d650fda8aed2c0ecf634ea94e7..252be9e2c217102e4e0e150b6bbd28b3ff9eb420 100644 | |
--- a/gtk/gtkscrolledwindow.c | |
+++ b/gtk/gtkscrolledwindow.c | |
@@ -1207,23 +1207,12 @@ check_update_scrollbar_proximity (GtkScrolledWindow *sw, | |
} | |
static double | |
-get_wheel_detent_scroll_step (GtkScrolledWindow *sw, | |
- GtkOrientation orientation) | |
+get_wheel_detent_scroll_step (GtkScrollbar *scrollbar) | |
{ | |
- GtkScrolledWindowPrivate *priv = gtk_scrolled_window_get_instance_private (sw); | |
- GtkScrollbar *scrollbar; | |
GtkAdjustment *adj; | |
double page_size; | |
double scroll_step; | |
- if (orientation == GTK_ORIENTATION_HORIZONTAL) | |
- scrollbar = GTK_SCROLLBAR (priv->hscrollbar); | |
- else | |
- scrollbar = GTK_SCROLLBAR (priv->vscrollbar); | |
- | |
- if (!scrollbar) | |
- return 0; | |
- | |
adj = gtk_scrollbar_get_adjustment (scrollbar); | |
page_size = gtk_adjustment_get_page_size (adj); | |
scroll_step = pow (page_size, 2.0 / 3.0); | |
@@ -1381,8 +1370,7 @@ scrolled_window_scroll (GtkScrolledWindow *scrolled_window, | |
if (scroll_unit == GDK_SCROLL_UNIT_WHEEL) | |
{ | |
- delta_x *= get_wheel_detent_scroll_step (scrolled_window, | |
- GTK_ORIENTATION_HORIZONTAL); | |
+ delta_x *= get_wheel_detent_scroll_step (GTK_SCROLLBAR (priv->hscrollbar)); | |
} | |
else if (scroll_unit == GDK_SCROLL_UNIT_SURFACE) | |
delta_x *= MAGIC_SCROLL_FACTOR; | |
@@ -1404,8 +1392,7 @@ scrolled_window_scroll (GtkScrolledWindow *scrolled_window, | |
if (scroll_unit == GDK_SCROLL_UNIT_WHEEL) | |
{ | |
- delta_y *= get_wheel_detent_scroll_step (scrolled_window, | |
- GTK_ORIENTATION_VERTICAL); | |
+ delta_y *= get_wheel_detent_scroll_step (GTK_SCROLLBAR (priv->vscrollbar)); | |
} | |
else if (scroll_unit == GDK_SCROLL_UNIT_SURFACE) | |
delta_y *= MAGIC_SCROLL_FACTOR; | |
@@ -1461,6 +1448,9 @@ scroll_controller_decelerate (GtkEventControllerScroll *scroll, | |
double initial_vel_y, | |
GtkScrolledWindow *scrolled_window) | |
{ | |
+ GtkScrolledWindowPrivate *priv = | |
+ gtk_scrolled_window_get_instance_private (scrolled_window); | |
+ | |
GdkScrollUnit scroll_unit; | |
gboolean shifted; | |
GdkModifierType state; | |
@@ -1481,11 +1471,11 @@ scroll_controller_decelerate (GtkEventControllerScroll *scroll, | |
if (scroll_unit == GDK_SCROLL_UNIT_WHEEL) | |
{ | |
- initial_vel_x *= get_wheel_detent_scroll_step (scrolled_window, | |
- GTK_ORIENTATION_HORIZONTAL); | |
+ initial_vel_x *= | |
+ get_wheel_detent_scroll_step (GTK_SCROLLBAR (priv->hscrollbar)); | |
- initial_vel_y *= get_wheel_detent_scroll_step (scrolled_window, | |
- GTK_ORIENTATION_VERTICAL); | |
+ initial_vel_y *= | |
+ get_wheel_detent_scroll_step (GTK_SCROLLBAR (priv->vscrollbar)); | |
} | |
else | |
{ | |
diff --git a/gtk/gtkscrolledwindow.c b/gtk/gtkscrolledwindow.c | |
index 252be9e2c217102e4e0e150b6bbd28b3ff9eb420..f0d93c5e0ffba7d98f8daf5f281379fd21e3a1c6 100644 | |
--- a/gtk/gtkscrolledwindow.c | |
+++ b/gtk/gtkscrolledwindow.c | |
@@ -198,6 +198,17 @@ | |
/* Scrolled off indication */ | |
#define UNDERSHOOT_SIZE 40 | |
+/* Min/max duration of the interpolation animation run with low | |
+ * precision mouse scroll. It's the time between the last wheel | |
+ * click and the animation end, when we arrive at the target | |
+ * scroll position. The duration of the animation will be | |
+ * shorter when we scroll farther because the animation is | |
+ * especially valuable over short distance. The value is in | |
+ * microseconds. | |
+*/ | |
+#define INTERPOLATION_ANIMATION_MIN_DURATION 100000 | |
+#define INTERPOLATION_ANIMATION_MAX_DURATION 300000 | |
+ | |
#define MAGIC_SCROLL_FACTOR 2.5 | |
typedef struct _GtkScrolledWindowClass GtkScrolledWindowClass; | |
@@ -238,6 +249,69 @@ typedef struct | |
guint over_timeout_id; | |
} Indicator; | |
+/* Interpolation scroll animation informations about | |
+ * one axis (horizontal or vertical). | |
+ */ | |
+typedef struct | |
+{ | |
+ /* Timestamp of the latest wheel click which is the | |
+ * beginning time of the animation. | |
+ */ | |
+ gint64 start_time; | |
+ | |
+ // Scroll position from where the animation starts. | |
+ double start; | |
+ | |
+ /* Speed of the animation at its beginning. This is | |
+ * the derivative of the animation curve at its | |
+ * start. | |
+ */ | |
+ double start_speed; | |
+ | |
+ /* Target scroll position of the animation. The | |
+ * animation curve must reach this point at | |
+ * start_time + duration. | |
+ */ | |
+ double target; | |
+ | |
+ // How much time the animation will durate (in microseconds). | |
+ double duration; | |
+ | |
+ /* True if a scroll animation is running on this | |
+ * axis. | |
+ */ | |
+ gboolean running; | |
+ | |
+ /* The time when we get the last continuous | |
+ * scroll delta from the mouse. Continuous | |
+ * deltas come from high precision scroll | |
+ * wheel and we don't want an interpolation | |
+ * animation for this type of device. | |
+ * | |
+ * But it's not impossible to get discrete | |
+ * scroll deltas from high precision wheel. | |
+ * So we use this variable to know if we | |
+ * have not received a continuous scroll | |
+ * delta for a long time. In this case, we | |
+ * suppose we have a low precision wheel | |
+ * and we enable the interpolation | |
+ * animation. | |
+ */ | |
+ gboolean last_continuous_delta_time_defined; | |
+ long last_continuous_delta_time; | |
+} InterpolationAnimationAxis; | |
+ | |
+// Interpolation scroll animation informations. | |
+typedef struct | |
+{ | |
+ // Axis informations. | |
+ InterpolationAnimationAxis horizontal; | |
+ InterpolationAnimationAxis vertical; | |
+ | |
+ // Tick id of the animation callback. | |
+ guint tick_id; | |
+} InterpolationAnimation; | |
+ | |
typedef struct | |
{ | |
GtkWidget *child; | |
@@ -296,6 +370,8 @@ typedef struct | |
double unclamped_hadj_value; | |
double unclamped_vadj_value; | |
+ | |
+ InterpolationAnimation interpolation_animation; | |
} GtkScrolledWindowPrivate; | |
enum { | |
@@ -1333,6 +1409,336 @@ stop_kinetic_scrolling_cb (GtkEventControllerScroll *scroll, | |
gtk_kinetic_scrolling_stop (priv->vscrolling); | |
} | |
+/* | |
+ Interpolation animation formulas explanations : | |
+ | |
+ s : start position of the curve | |
+ w : final position of the curve | |
+ v : speed at the start of the curve | |
+ t : duration of the animation | |
+ | |
+ ax³ + bx² + cx + d Position formula | |
+ 3ax² + 2bx + c Derivate of the position formula, so it's the "speed" formula | |
+ | |
+ We have 4 equalities that set the conditions of the system : | |
+ | |
+ a0³ + b0² + c0 + d = s At time 0, the position is the desired start position. | |
+ 3a0² + 2b0 + c = v At time 0, the speed is the desired start speed. | |
+ at³ + bt² + ct + d = w At the end of the animation, the position is the desired target position. | |
+ 3at² + 2bt + c = 0 At the end of the animation, the speed is 0 (smooth end). | |
+ | |
+ "s", "w", "v" and "t" are known values. We need to express "a", "b", "c", and | |
+ "d" from these values. | |
+ | |
+ a0³ + b0² + c0 + d = s | |
+ d = s | |
+ | |
+ 3a0² + 2b0 + c = v | |
+ c = v | |
+ | |
+ We have "c" and "d". We're left with "a" and "b". | |
+ | |
+ at³ + bt² + ct + d = w | |
+ | |
+ at³ = w - d - ct - bt² | |
+ | |
+ w - d - ct - bt² | |
+ a = ---------------- | |
+ t³ | |
+ | |
+ We have "a" but it is calculated with "b" which is unknown. | |
+ So we reinject this definition of "a" in the last equality. | |
+ | |
+ 3at² + 2bt + c = 0 | |
+ | |
+ w - d - ct - bt² | |
+ 3 * ---------------- * t² + 2bt + c = 0 | |
+ t³ | |
+ | |
+ ... | |
+ | |
+ 3w - 3d - 2ct | |
+ b = ------------- | |
+ t² | |
+ | |
+ We have expressed "b" with known values only. So "b" become a known value and | |
+ "a" too! | |
+*/ | |
+static inline double | |
+interpolation_animation_axis_get_current_value (const InterpolationAnimationAxis *axis_animation, | |
+ const double current_time) | |
+{ | |
+ const double t = axis_animation->duration; | |
+ const double w = axis_animation->target; | |
+ | |
+ const double d = axis_animation->start; | |
+ const double c = axis_animation->start_speed; | |
+ | |
+ const double b = (3 * w - 3 * d - 2 * c * t) / (t * t); | |
+ const double a = (w - d - c * t - b * t * t) / (t * t * t); | |
+ | |
+ const double x = current_time - axis_animation->start_time; | |
+ | |
+ return a * x * x * x + b * x * x + c * x + d; | |
+} | |
+ | |
+static inline double | |
+interpolation_animation_axis_get_current_speed (const InterpolationAnimationAxis *axis_animation, | |
+ const double current_time) | |
+{ | |
+ const double t = axis_animation->duration; | |
+ const double w = axis_animation->target; | |
+ | |
+ const double d = axis_animation->start; | |
+ const double c = axis_animation->start_speed; | |
+ | |
+ const double b = (3 * w - 3 * d - 2 * c * t) / (t * t); | |
+ const double a = (w - d - c * t - b * t * t) / (t * t * t); | |
+ | |
+ const double x = current_time - axis_animation->start_time; | |
+ | |
+ return 3 * a * x * x + 2 * b * x + c; | |
+} | |
+ | |
+// Handle scroll interpolation animation tick on one scroll axis (horizontal or vertical). | |
+static void | |
+interpolation_animation_axis_cb (InterpolationAnimationAxis *axis_animation, | |
+ const double current_time, | |
+ GtkScrolledWindow *scrolled_window, | |
+ GtkAdjustment *adj, | |
+ gboolean (*may_scroll)(GtkScrolledWindow*)) | |
+{ | |
+ // Nothing to do if the animation is not running. | |
+ if (!axis_animation->running) | |
+ { | |
+ return; | |
+ } | |
+ | |
+ const double elapsed_time = current_time - axis_animation->start_time; | |
+ | |
+ if (elapsed_time < axis_animation->duration) | |
+ { | |
+ /* If the animation duration has not been reached, | |
+ * we keep animation going. | |
+ */ | |
+ const double animation_value = | |
+ interpolation_animation_axis_get_current_value (axis_animation, current_time); | |
+ | |
+ _gtk_scrolled_window_set_adjustment_value (scrolled_window, adj, | |
+ animation_value); | |
+ } | |
+ else | |
+ { | |
+ /* If the animation duration has been reached, we set | |
+ * the scroll position to the animation target | |
+ * position and we mark the animation as stopped. | |
+ */ | |
+ _gtk_scrolled_window_set_adjustment_value (scrolled_window, adj, | |
+ axis_animation->target); | |
+ | |
+ axis_animation->running = FALSE; | |
+ } | |
+} | |
+ | |
+// Handle scroll interpolation animation tick. | |
+static gboolean | |
+interpolation_animation_cb (GtkWidget *widget, | |
+ GdkFrameClock *clock, | |
+ gpointer user_data) | |
+{ | |
+ GtkScrolledWindow *scrolled_window = GTK_SCROLLED_WINDOW (widget); | |
+ GtkScrolledWindowPrivate *priv = | |
+ gtk_scrolled_window_get_instance_private (scrolled_window); | |
+ | |
+ InterpolationAnimation *animation = &priv->interpolation_animation; | |
+ const double current_time = gdk_frame_clock_get_frame_time (clock); | |
+ | |
+ // Handle tick on each axis. | |
+ | |
+ GtkAdjustment *h_adjustment = | |
+ gtk_scrollbar_get_adjustment (GTK_SCROLLBAR (priv->hscrollbar)); | |
+ GtkAdjustment *v_adjustment = | |
+ gtk_scrollbar_get_adjustment (GTK_SCROLLBAR (priv->vscrollbar)); | |
+ | |
+ interpolation_animation_axis_cb (&animation->horizontal, | |
+ current_time, | |
+ scrolled_window, | |
+ h_adjustment, | |
+ may_hscroll); | |
+ | |
+ interpolation_animation_axis_cb (&animation->vertical, | |
+ current_time, | |
+ scrolled_window, | |
+ v_adjustment, | |
+ may_vscroll); | |
+ | |
+ gtk_scrolled_window_invalidate_overshoot (scrolled_window); | |
+ | |
+ if (animation->horizontal.running || animation->vertical.running) | |
+ { | |
+ // If at least one animation is running, we keep the callback up. | |
+ return G_SOURCE_CONTINUE; | |
+ } | |
+ else | |
+ { | |
+ if(_gtk_scrolled_window_get_overshoot (scrolled_window, NULL, NULL)) | |
+ { | |
+ // Starting the overshoot animation | |
+ gtk_scrolled_window_start_deceleration (scrolled_window); | |
+ } | |
+ | |
+ /* If all animations are done, we stop the | |
+ * interpolation animation callback. | |
+ */ | |
+ return G_SOURCE_REMOVE; | |
+ } | |
+} | |
+ | |
+static void | |
+interpolation_animation_axis_prevent_overshoot_delay (InterpolationAnimationAxis *axis_animation, | |
+ GtkScrollbar *scrollbar, | |
+ const double pixels_delta) | |
+{ | |
+ GtkAdjustment *adjustment = gtk_scrollbar_get_adjustment (scrollbar); | |
+ const double lower = gtk_adjustment_get_lower (adjustment); | |
+ const double upper = gtk_adjustment_get_upper (adjustment) | |
+ - gtk_adjustment_get_page_size (adjustment); | |
+ | |
+ /* If the animation start position is in the overshoot area and we | |
+ * want to scroll in the undershoot direction, start from | |
+ * undershoot bounds to prevent the delay effect. | |
+ */ | |
+ if ((axis_animation->start < lower && pixels_delta > 0) | |
+ || (axis_animation->start > upper && pixels_delta < 0)) | |
+ { | |
+ axis_animation->start = CLAMP (axis_animation->start, lower, upper); | |
+ axis_animation->target = axis_animation->start + pixels_delta; | |
+ axis_animation->start_speed = 0; | |
+ } | |
+} | |
+ | |
+static void | |
+interpolation_animation_axis_compute_duration (InterpolationAnimationAxis *axis_animation, | |
+ GtkScrollbar *scrollbar) | |
+{ | |
+ /* Calculate an appropriate interpolation animation duration. | |
+ * | |
+ * The interpolation animation allows the user to better "follow" | |
+ * the scrolling content on screen when he's using a low | |
+ * precision mouse wheel. | |
+ * | |
+ * When the user scroll delta is short, he doesn't want to scroll | |
+ * too far. This means that the content he would like to see is | |
+ * already almost entirely visible on the screen. Example: he's | |
+ * reading paragraph 3 and wants to make the paragraph 4 come | |
+ * into the view, even though he's still reading paragraph 3. | |
+ * In this case, the animation can be slow because the target | |
+ * is near. | |
+ * | |
+ * On the other hand, if the scroll delta is big, the user wants | |
+ * to access quickly a distant part of the document. The content | |
+ * on screen will completly change and the need of "following" | |
+ * content is less present, so the animation can be faster. If | |
+ * it is too slow, it feels too unresponsive and boring. | |
+ * | |
+ * So the larger the scroll delta, the shorter the animation | |
+ * time. | |
+ */ | |
+ | |
+ const double abs_delta = fabs(axis_animation->target - axis_animation->start); | |
+ | |
+ GtkAdjustment *adjustment = gtk_scrollbar_get_adjustment (scrollbar); | |
+ const double page_size = gtk_adjustment_get_page_size (adjustment); | |
+ | |
+ /* When delta = 0, duration = INTERPOLATION_ANIMATION_MAX_DURATION. | |
+ * When delta = scroll view size, duration = INTERPOLATION_ANIMATION_MIN_DURATION. | |
+ * We do a linear interpolation between these 2 "points". | |
+ */ | |
+ const double duration = INTERPOLATION_ANIMATION_MAX_DURATION | |
+ + abs_delta * (INTERPOLATION_ANIMATION_MIN_DURATION | |
+ - INTERPOLATION_ANIMATION_MAX_DURATION) / page_size; | |
+ | |
+ // Prevent the duration to be out of bounds. | |
+ axis_animation->duration = CLAMP (duration, INTERPOLATION_ANIMATION_MIN_DURATION, | |
+ INTERPOLATION_ANIMATION_MAX_DURATION); | |
+} | |
+ | |
+static gboolean | |
+interpolation_animation_axis_heuristic (InterpolationAnimationAxis *axis_animation, | |
+ const double current_time, | |
+ const double delta) | |
+{ | |
+ // If the delta is discrete | |
+ if (delta == (int) delta) | |
+ { | |
+ if (axis_animation->last_continuous_delta_time_defined | |
+ && axis_animation->last_continuous_delta_time > current_time - 1000000) | |
+ { | |
+ return FALSE; | |
+ } | |
+ else | |
+ { | |
+ axis_animation->last_continuous_delta_time_defined = FALSE; | |
+ return TRUE; | |
+ } | |
+ } | |
+ else // If the delta is continuous | |
+ { | |
+ axis_animation->last_continuous_delta_time_defined = TRUE; | |
+ axis_animation->last_continuous_delta_time = current_time; | |
+ | |
+ return FALSE; | |
+ } | |
+} | |
+ | |
+static void | |
+interpolation_animation_axis_handle_scroll (InterpolationAnimationAxis *axis_animation, | |
+ const double current_time, | |
+ const double delta, | |
+ GtkScrollbar *scrollbar, | |
+ const double unclamped_adj_value, | |
+ gboolean (*may_scroll)(GtkScrolledWindow*), | |
+ GtkScrolledWindow *scrolled_window) | |
+{ | |
+ const double pixels_delta = delta * get_wheel_detent_scroll_step (scrollbar); | |
+ const double elapsed_time = current_time - axis_animation->start_time; | |
+ | |
+ if (axis_animation->running && elapsed_time < axis_animation->duration) | |
+ { | |
+ /* If the animation is running, start from the current animation | |
+ * position and move the target. We keep the same speed to keep | |
+ * things smooth and not have animation jumps. | |
+ */ | |
+ const double animation_value = | |
+ interpolation_animation_axis_get_current_value (axis_animation, | |
+ current_time); | |
+ | |
+ axis_animation->start = animation_value; | |
+ axis_animation->target += pixels_delta; | |
+ axis_animation->start_speed = | |
+ interpolation_animation_axis_get_current_speed (axis_animation, | |
+ current_time); | |
+ } | |
+ else | |
+ { | |
+ // If animation was not running, smoothly start with a 0 speed. | |
+ axis_animation->start = unclamped_adj_value; | |
+ axis_animation->target = axis_animation->start + pixels_delta; | |
+ axis_animation->start_speed = 0; | |
+ | |
+ axis_animation->running = TRUE; | |
+ } | |
+ | |
+ interpolation_animation_axis_prevent_overshoot_delay (axis_animation, | |
+ scrollbar, | |
+ pixels_delta); | |
+ | |
+ interpolation_animation_axis_compute_duration (axis_animation, | |
+ scrollbar); | |
+ | |
+ axis_animation->start_time = current_time; | |
+} | |
+ | |
static void | |
scrolled_window_scroll (GtkScrolledWindow *scrolled_window, | |
double delta_x, | |
@@ -1347,8 +1753,6 @@ scrolled_window_scroll (GtkScrolledWindow *scrolled_window, | |
state = gtk_event_controller_get_current_event_state (GTK_EVENT_CONTROLLER (scroll)); | |
shifted = (state & GDK_SHIFT_MASK) != 0; | |
- gtk_scrolled_window_invalidate_overshoot (scrolled_window); | |
- | |
if (shifted) | |
{ | |
double delta; | |
@@ -1358,54 +1762,94 @@ scrolled_window_scroll (GtkScrolledWindow *scrolled_window, | |
delta_y = delta; | |
} | |
+ g_clear_handle_id (&priv->scroll_events_overshoot_id, g_source_remove); | |
+ | |
+ GdkScrollUnit scroll_unit = gtk_event_controller_scroll_get_unit (scroll); | |
+ | |
+ InterpolationAnimation *animation = &priv->interpolation_animation; | |
+ | |
+ const gboolean animation_was_running = | |
+ animation->horizontal.running || animation->vertical.running; | |
+ | |
+ GdkFrameClock *clock = gtk_widget_get_frame_clock (GTK_WIDGET (scrolled_window)); | |
+ const double current_time = gdk_frame_clock_get_frame_time (clock); | |
+ | |
if (delta_x != 0.0 && | |
may_hscroll (scrolled_window)) | |
{ | |
- GtkAdjustment *adj; | |
- double new_value; | |
- GdkScrollUnit scroll_unit; | |
- | |
- adj = gtk_scrollbar_get_adjustment (GTK_SCROLLBAR (priv->hscrollbar)); | |
- scroll_unit = gtk_event_controller_scroll_get_unit (scroll); | |
- | |
- if (scroll_unit == GDK_SCROLL_UNIT_WHEEL) | |
+ if (interpolation_animation_axis_heuristic (&animation->horizontal, current_time, delta_x)) | |
{ | |
- delta_x *= get_wheel_detent_scroll_step (GTK_SCROLLBAR (priv->hscrollbar)); | |
+ interpolation_animation_axis_handle_scroll (&animation->horizontal, | |
+ current_time, delta_x, | |
+ GTK_SCROLLBAR (priv->hscrollbar), | |
+ priv->unclamped_hadj_value, | |
+ may_hscroll, | |
+ scrolled_window); | |
} | |
- else if (scroll_unit == GDK_SCROLL_UNIT_SURFACE) | |
- delta_x *= MAGIC_SCROLL_FACTOR; | |
+ else | |
+ { | |
+ GtkAdjustment *adj = gtk_scrollbar_get_adjustment (GTK_SCROLLBAR (priv->hscrollbar)); | |
- new_value = priv->unclamped_hadj_value + delta_x; | |
- _gtk_scrolled_window_set_adjustment_value (scrolled_window, adj, | |
- new_value); | |
+ if (scroll_unit == GDK_SCROLL_UNIT_WHEEL) | |
+ { | |
+ delta_x *= get_wheel_detent_scroll_step (GTK_SCROLLBAR (priv->hscrollbar)); | |
+ } | |
+ else if (scroll_unit == GDK_SCROLL_UNIT_SURFACE) | |
+ delta_x *= MAGIC_SCROLL_FACTOR; | |
+ | |
+ double new_value = priv->unclamped_hadj_value + delta_x; | |
+ _gtk_scrolled_window_set_adjustment_value (scrolled_window, adj, | |
+ new_value); | |
+ | |
+ gtk_scrolled_window_invalidate_overshoot (scrolled_window); | |
+ } | |
} | |
if (delta_y != 0.0 && | |
may_vscroll (scrolled_window)) | |
{ | |
- GtkAdjustment *adj; | |
- double new_value; | |
- GdkScrollUnit scroll_unit; | |
- | |
- adj = gtk_scrollbar_get_adjustment (GTK_SCROLLBAR (priv->vscrollbar)); | |
- scroll_unit = gtk_event_controller_scroll_get_unit (scroll); | |
- | |
- if (scroll_unit == GDK_SCROLL_UNIT_WHEEL) | |
+ if (interpolation_animation_axis_heuristic (&animation->vertical, current_time, delta_y)) | |
{ | |
- delta_y *= get_wheel_detent_scroll_step (GTK_SCROLLBAR (priv->vscrollbar)); | |
+ interpolation_animation_axis_handle_scroll (&animation->vertical, | |
+ current_time, delta_y, | |
+ GTK_SCROLLBAR (priv->vscrollbar), | |
+ priv->unclamped_vadj_value, | |
+ may_vscroll, | |
+ scrolled_window); | |
} | |
- else if (scroll_unit == GDK_SCROLL_UNIT_SURFACE) | |
- delta_y *= MAGIC_SCROLL_FACTOR; | |
+ else | |
+ { | |
+ GtkAdjustment *adj = gtk_scrollbar_get_adjustment (GTK_SCROLLBAR (priv->vscrollbar)); | |
- new_value = priv->unclamped_vadj_value + delta_y; | |
- _gtk_scrolled_window_set_adjustment_value (scrolled_window, adj, | |
- new_value); | |
+ if (scroll_unit == GDK_SCROLL_UNIT_WHEEL) | |
+ { | |
+ delta_y *= get_wheel_detent_scroll_step (GTK_SCROLLBAR (priv->vscrollbar)); | |
+ } | |
+ else if (scroll_unit == GDK_SCROLL_UNIT_SURFACE) | |
+ delta_y *= MAGIC_SCROLL_FACTOR; | |
+ | |
+ double new_value = priv->unclamped_vadj_value + delta_y; | |
+ _gtk_scrolled_window_set_adjustment_value (scrolled_window, adj, | |
+ new_value); | |
+ | |
+ gtk_scrolled_window_invalidate_overshoot (scrolled_window); | |
+ } | |
} | |
- g_clear_handle_id (&priv->scroll_events_overshoot_id, g_source_remove); | |
+ /* Start the tick callback if the animation is running now but was not running | |
+ * before. | |
+ */ | |
+ if (!animation_was_running && | |
+ (animation->horizontal.running || animation->vertical.running)) | |
+ { | |
+ animation->tick_id = | |
+ gtk_widget_add_tick_callback (GTK_WIDGET (scrolled_window), | |
+ interpolation_animation_cb, | |
+ NULL, NULL); | |
+ } | |
- if (!priv->smooth_scroll && | |
- _gtk_scrolled_window_get_overshoot (scrolled_window, NULL, NULL)) | |
+ if (!priv->interpolation_animation.horizontal.running && !priv->interpolation_animation.vertical.running && | |
+ !priv->smooth_scroll && _gtk_scrolled_window_get_overshoot (scrolled_window, NULL, NULL)) | |
{ | |
priv->scroll_events_overshoot_id = | |
g_timeout_add (50, start_scroll_deceleration_cb, scrolled_window); | |
@@ -2086,6 +2530,12 @@ gtk_scrolled_window_init (GtkScrolledWindow *scrolled_window) | |
priv->overlay_scrolling = TRUE; | |
+ priv->interpolation_animation.horizontal.running = FALSE; | |
+ priv->interpolation_animation.vertical.running = FALSE; | |
+ | |
+ priv->interpolation_animation.horizontal.last_continuous_delta_time_defined = FALSE; | |
+ priv->interpolation_animation.vertical.last_continuous_delta_time_defined = FALSE; | |
+ | |
priv->drag_gesture = gtk_gesture_drag_new (); | |
gtk_gesture_single_set_touch_only (GTK_GESTURE_SINGLE (priv->drag_gesture), TRUE); | |
g_signal_connect_swapped (priv->drag_gesture, "drag-begin", | |
@@ -2687,6 +3137,15 @@ gtk_scrolled_window_dispose (GObject *object) | |
priv->deceleration_id = 0; | |
} | |
+ InterpolationAnimation *interpolation_animation = &priv->interpolation_animation; | |
+ | |
+ if (interpolation_animation->horizontal.running || | |
+ interpolation_animation->vertical.running) | |
+ { | |
+ gtk_widget_remove_tick_callback (GTK_WIDGET (self), | |
+ interpolation_animation->tick_id); | |
+ } | |
+ | |
g_clear_pointer (&priv->hscrolling, gtk_kinetic_scrolling_free); | |
g_clear_pointer (&priv->vscrolling, gtk_kinetic_scrolling_free); | |
g_clear_handle_id (&priv->scroll_events_overshoot_id, g_source_remove); | |
diff --git a/gtk/gtkscrolledwindow.c b/gtk/gtkscrolledwindow.c | |
index f0d93c5e0ffba7d98f8daf5f281379fd21e3a1c6..bac8446dbbf7a3b5956c7deb5d6e83aef9794494 100644 | |
--- a/gtk/gtkscrolledwindow.c | |
+++ b/gtk/gtkscrolledwindow.c | |
@@ -1764,6 +1764,10 @@ scrolled_window_scroll (GtkScrolledWindow *scrolled_window, | |
g_clear_handle_id (&priv->scroll_events_overshoot_id, g_source_remove); | |
+ GtkSettings *settings = gtk_widget_get_settings (GTK_WIDGET (scrolled_window)); | |
+ gboolean interpolation_enabled; | |
+ g_object_get (settings, "gtk-scroll-interpolation-animation", &interpolation_enabled, NULL); | |
+ | |
GdkScrollUnit scroll_unit = gtk_event_controller_scroll_get_unit (scroll); | |
InterpolationAnimation *animation = &priv->interpolation_animation; | |
@@ -1777,7 +1781,8 @@ scrolled_window_scroll (GtkScrolledWindow *scrolled_window, | |
if (delta_x != 0.0 && | |
may_hscroll (scrolled_window)) | |
{ | |
- if (interpolation_animation_axis_heuristic (&animation->horizontal, current_time, delta_x)) | |
+ if (interpolation_enabled && | |
+ interpolation_animation_axis_heuristic (&animation->horizontal, current_time, delta_x)) | |
{ | |
interpolation_animation_axis_handle_scroll (&animation->horizontal, | |
current_time, delta_x, | |
@@ -1808,7 +1813,8 @@ scrolled_window_scroll (GtkScrolledWindow *scrolled_window, | |
if (delta_y != 0.0 && | |
may_vscroll (scrolled_window)) | |
{ | |
- if (interpolation_animation_axis_heuristic (&animation->vertical, current_time, delta_y)) | |
+ if (interpolation_enabled && | |
+ interpolation_animation_axis_heuristic (&animation->vertical, current_time, delta_y)) | |
{ | |
interpolation_animation_axis_handle_scroll (&animation->vertical, | |
current_time, delta_y, | |
diff --git a/gtk/gtksettings.c b/gtk/gtksettings.c | |
index 15cc3aa0dfb25a3759c6016b5ffca8f547470fe8..c5376bce7da92c9a6d032f88dfff8fc4e21b0e0b 100644 | |
--- a/gtk/gtksettings.c | |
+++ b/gtk/gtksettings.c | |
@@ -202,6 +202,7 @@ enum { | |
PROP_KEYNAV_USE_CARET, | |
PROP_OVERLAY_SCROLLING, | |
PROP_FONT_RENDERING, | |
+ PROP_SCROLL_INTERPOLATION_ANIMATION, | |
NUM_PROPERTIES | |
}; | |
@@ -976,6 +977,23 @@ gtk_settings_class_init (GtkSettingsClass *class) | |
GTK_FONT_RENDERING_AUTOMATIC, | |
GTK_PARAM_READWRITE); | |
+ /** | |
+ * GtkSettings:gtk-scroll-interpolation-animation: | |
+ * | |
+ * Whether scroll interpolation animation is enabled. | |
+ * | |
+ * The animation is triggered on low precision mouse scroll and | |
+ * makes the content smoothly scroll and not "jumpy" scroll as | |
+ * it is without animation. The goal is to make the content | |
+ * easier to follow on screen when using low precision mouse | |
+ * scroll. | |
+ * | |
+ * Since: 4.20 | |
+ */ | |
+ pspecs[PROP_SCROLL_INTERPOLATION_ANIMATION] = g_param_spec_boolean ("gtk-scroll-interpolation-animation", NULL, NULL, | |
+ TRUE, | |
+ GTK_PARAM_READWRITE); | |
+ | |
g_object_class_install_properties (gobject_class, NUM_PROPERTIES, pspecs); | |
} | |
diff --git a/testsuite/tools/settings b/testsuite/tools/settings | |
index 19c78cf7c0235ecfeb0d1a1e45caf18fcaed4919..57c72fe56c7813fcb8ea1805be34bbac828fe9bc 100755 | |
--- a/testsuite/tools/settings | |
+++ b/testsuite/tools/settings | |
@@ -10,7 +10,7 @@ echo "1..1" | |
name=gtk-query-settings | |
result=$TEST_RESULT_DIR/$name.out | |
$GTK_QUERY_SETTINGS 2>/dev/null >$result | |
-EXPECTED=52 | |
+EXPECTED=53 | |
SEEN=$(wc -l $result | cut -f1 -d' ') | |
if [ $SEEN -eq $EXPECTED ]; then |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment