mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 16:37:27 +00:00
Initial implementation of momentum for finger based scrolling on Wayland
Needs configuration and possibly the parameter adjustment once pixel scrolling is merged.
This commit is contained in:
parent
cd25248e08
commit
e1199bcee2
6 changed files with 326 additions and 7 deletions
|
|
@ -153,6 +153,8 @@ Detailed list of changes
|
|||
0.45.1 [future]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Wayland: Add momentum scrolling for touchpads
|
||||
|
||||
- choose-files kitten: Fix JXL image preview not working (:iss:`9323`)
|
||||
|
||||
- Fix tab bar rendering glitches when using :opt:`tab_bar_filter` in some
|
||||
|
|
|
|||
1
glfw/internal.h
vendored
1
glfw/internal.h
vendored
|
|
@ -885,6 +885,7 @@ void _glfwPlatformRemoveTimer(unsigned long long timer_id);
|
|||
int _glfwPlatformSetWindowBlur(_GLFWwindow* handle, int value);
|
||||
MonitorGeometry _glfwPlatformGetMonitorGeometry(_GLFWmonitor* monitor);
|
||||
bool _glfwPlatformGrabKeyboard(bool grab);
|
||||
void glfw_handle_scroll_event_for_momentum(_GLFWwindow *w, const GLFWScrollEvent *ev, monotonic_t timestamp, bool stopped, bool is_finger_based);
|
||||
|
||||
char* _glfw_strdup(const char* source);
|
||||
|
||||
|
|
|
|||
179
glfw/momentum-scroll.c
vendored
Normal file
179
glfw/momentum-scroll.c
vendored
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
/*
|
||||
* momentum-scroll.c
|
||||
* Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
|
||||
*
|
||||
* Distributed under terms of the GPL3 license.
|
||||
*/
|
||||
|
||||
#include "internal.h"
|
||||
#include <math.h>
|
||||
|
||||
typedef struct ScrollSample {
|
||||
double dx, dy;
|
||||
monotonic_t timestamp, local_timestamp;
|
||||
} ScrollSample;
|
||||
|
||||
#define DEQUE_DATA_TYPE ScrollSample
|
||||
#define DEQUE_NAME ScrollSamples
|
||||
#include "../kitty/fixed_size_deque.h"
|
||||
|
||||
typedef enum ScrollerState { NONE, PHYSICAL_EVENT_IN_PROGRESS, MOMENTUM_IN_PROGRESS } ScrollerState;
|
||||
|
||||
typedef struct MomentumScroller {
|
||||
double friction, // Deceleration factor (0-1, lower = longer coast)
|
||||
min_velocity, // Minimum velocity before stopping
|
||||
max_velocity, // Maximum velocity to prevent runaway scrolling
|
||||
boost_factor, // How much to speed up scrolling
|
||||
velocity_scale; // Scale factor for initial velocity
|
||||
unsigned timer_interval_ms;
|
||||
|
||||
GLFWid timer_id, window_id;
|
||||
ScrollSamples samples;
|
||||
ScrollerState state;
|
||||
struct { double x, y; } velocity;
|
||||
int keyboard_modifiers;
|
||||
} MomentumScroller;
|
||||
|
||||
static MomentumScroller s = {
|
||||
.friction = 0.04,
|
||||
.min_velocity = 0.5,
|
||||
.max_velocity = 100,
|
||||
.boost_factor = 1.2,
|
||||
.velocity_scale = 0.9,
|
||||
.timer_interval_ms = 10,
|
||||
};
|
||||
|
||||
static void
|
||||
cancel_existing_scroll(void) {
|
||||
if (s.timer_id) {
|
||||
glfwRemoveTimer(s.timer_id);
|
||||
s.timer_id = 0;
|
||||
}
|
||||
if (s.state == MOMENTUM_IN_PROGRESS) {
|
||||
_GLFWwindow *w = _glfwWindowForId(s.window_id);
|
||||
if (w) _glfwInputScroll(
|
||||
w, &(GLFWScrollEvent){.momentum_type=GLFW_MOMENTUM_PHASE_CANCELED, .keyboard_modifiers=s.keyboard_modifiers});
|
||||
}
|
||||
s.window_id = 0;
|
||||
s.keyboard_modifiers = 0;
|
||||
deque_clear(&s.samples);
|
||||
s.state = NONE;
|
||||
}
|
||||
|
||||
static void
|
||||
add_sample(double dx, double dy, monotonic_t timestamp) {
|
||||
deque_push_back(&s.samples, (ScrollSample){dx, dy, timestamp, monotonic()}, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
last_sample_delta(double *dx, double *dy) {
|
||||
const ScrollSample *ss;
|
||||
if ((ss = deque_peek_back(&s.samples))) { *dx = ss->dx; *dy = ss->dy; }
|
||||
else { *dx = 0; *dy = 0; }
|
||||
}
|
||||
|
||||
static void
|
||||
trim_old_samples(monotonic_t now) {
|
||||
const ScrollSample *ss;
|
||||
while ((ss = deque_peek_front(&s.samples)) && (now - ss->local_timestamp) > ms_to_monotonic_t(150))
|
||||
deque_pop_front(&s.samples, NULL);
|
||||
}
|
||||
|
||||
static void
|
||||
add_velocity(double x, double y) {
|
||||
if (x == 0 || x * s.velocity.x >= 0) s.velocity.x += x;
|
||||
else s.velocity.x = x;
|
||||
if (y == 0 || y * s.velocity.y >= 0) s.velocity.y += y;
|
||||
else s.velocity.y = y;
|
||||
s.velocity.x = MAX(-s.max_velocity, MIN(s.velocity.x, s.max_velocity));
|
||||
s.velocity.y = MAX(-s.max_velocity, MIN(s.velocity.y, s.max_velocity));
|
||||
}
|
||||
|
||||
static void
|
||||
set_velocity_from_samples(void) {
|
||||
trim_old_samples(monotonic());
|
||||
ScrollSample ss;
|
||||
switch (deque_size(&s.samples)) {
|
||||
case 0:
|
||||
return;
|
||||
case 1:
|
||||
deque_pop_front(&s.samples, &ss);
|
||||
add_velocity(s.velocity_scale * ss.dx, s.velocity_scale * ss.dy);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use weighted average - more recent samples have higher weight
|
||||
double total_dx = 0.0, total_dy = 0.0, total_weight = 0.0;
|
||||
monotonic_t first_time = deque_peek_front(&s.samples)->local_timestamp;
|
||||
monotonic_t last_time = deque_peek_back(&s.samples)->local_timestamp;
|
||||
double time_span = MAX(1, last_time - first_time);
|
||||
for (size_t i = 0; i < deque_size(&s.samples); i++) {
|
||||
const ScrollSample *ss = deque_at(&s.samples, i);
|
||||
double weight = 1.0 + (ss->local_timestamp - first_time) / time_span;
|
||||
total_dx += ss->dx * weight; total_dy += ss->dy * weight;
|
||||
total_weight += weight;
|
||||
}
|
||||
deque_clear(&s.samples);
|
||||
if (total_weight <= 0) return;
|
||||
add_velocity((total_dx / total_weight) * s.velocity_scale, (total_dy / total_weight) * s.velocity_scale);
|
||||
}
|
||||
|
||||
static void
|
||||
send_momentum_event(bool is_start) {
|
||||
double friction = 1.0 - MAX(0, MIN(s.friction, 1.));
|
||||
s.velocity.x *= friction; s.velocity.y *= friction;
|
||||
if (fabs(s.velocity.x) < s.min_velocity) s.velocity.x = 0;
|
||||
if (fabs(s.velocity.y) < s.min_velocity) s.velocity.y = 0;
|
||||
_GLFWwindow *w = _glfwWindowForId(s.window_id);
|
||||
if (!w || w != _glfwFocusedWindow()) {
|
||||
cancel_existing_scroll();
|
||||
return;
|
||||
}
|
||||
GLFWMomentumType m = is_start ? GLFW_MOMENTUM_PHASE_BEGAN : GLFW_MOMENTUM_PHASE_ACTIVE;
|
||||
if (s.velocity.x == 0 && s.velocity.y == 0 && !is_start) {
|
||||
m = GLFW_MOMENTUM_PHASE_ENDED;
|
||||
if (s.timer_id) glfwRemoveTimer(s.timer_id);
|
||||
s.timer_id = 0;
|
||||
}
|
||||
GLFWScrollEvent e = {
|
||||
.offset_type=GLFW_SCROLL_OFFEST_HIGHRES, .momentum_type=m, .x_offset=s.velocity.x, .y_offset=s.velocity.y,
|
||||
.keyboard_modifiers=s.keyboard_modifiers
|
||||
};
|
||||
_glfwInputScroll(w, &e);
|
||||
}
|
||||
|
||||
static void
|
||||
momentum_timer_fired(unsigned long long timer_id UNUSED, void *data UNUSED) {
|
||||
send_momentum_event(false);
|
||||
}
|
||||
|
||||
static void
|
||||
start_momentum_scroll(void) {
|
||||
set_velocity_from_samples();
|
||||
send_momentum_event(true);
|
||||
s.timer_id = glfwAddTimer(ms_to_monotonic_t(s.timer_interval_ms), true, momentum_timer_fired, NULL, NULL);
|
||||
}
|
||||
|
||||
void
|
||||
glfw_handle_scroll_event_for_momentum(
|
||||
_GLFWwindow *w, const GLFWScrollEvent *ev, monotonic_t timestamp, bool stopped, bool is_finger_based
|
||||
) {
|
||||
if (!w || (w->id != s.window_id && s.window_id) || s.state != PHYSICAL_EVENT_IN_PROGRESS) cancel_existing_scroll();
|
||||
if (!w) return;
|
||||
// Check for change in direction
|
||||
double ldx, ldy; last_sample_delta(&ldx, &ldy);
|
||||
if (ldx * ev->x_offset < 0 || ldy * ev->y_offset < 0) {
|
||||
s.velocity.x = 0; s.velocity.y = 0;
|
||||
cancel_existing_scroll();
|
||||
}
|
||||
s.window_id = w->id;
|
||||
s.keyboard_modifiers = ev->keyboard_modifiers;
|
||||
if (is_finger_based) {
|
||||
add_sample(ev->x_offset, ev->y_offset, timestamp);
|
||||
s.state = stopped ? MOMENTUM_IN_PROGRESS : PHYSICAL_EVENT_IN_PROGRESS;
|
||||
} else {
|
||||
s.state = stopped ? NONE : PHYSICAL_EVENT_IN_PROGRESS;
|
||||
}
|
||||
if (s.state == MOMENTUM_IN_PROGRESS) start_momentum_scroll();
|
||||
else _glfwInputScroll(w, ev);
|
||||
}
|
||||
|
|
@ -110,6 +110,7 @@
|
|||
"linux_joystick.c",
|
||||
"linux_desktop_settings.c",
|
||||
"null_joystick.c",
|
||||
"momentum-scroll.c",
|
||||
"linux_notify.c"
|
||||
]
|
||||
},
|
||||
|
|
|
|||
13
glfw/wl_init.c
vendored
13
glfw/wl_init.c
vendored
|
|
@ -221,15 +221,14 @@ pointer_handle_frame(void *data UNUSED, struct wl_pointer *pointer UNUSED) {
|
|||
ev.offset_type = GLFW_SCROLL_OFFEST_HIGHRES;
|
||||
ev.x_offset = info.continuous.x;
|
||||
}
|
||||
float scale = (float)_glfwWaylandWindowScale(window);
|
||||
ev.x_offset *= scale; ev.y_offset *= scale;
|
||||
ev.x_offset *= -1;
|
||||
glfw_handle_scroll_event_for_momentum(
|
||||
window, &ev, MAX(info.y_start_time, info.x_start_time), info.y_stop_received || info.x_stop_received,
|
||||
info.source_type == WL_POINTER_AXIS_SOURCE_FINGER);
|
||||
/* clear pointer_curr_axis_info for next frame */
|
||||
memset(&info, 0, sizeof(info));
|
||||
|
||||
if (ev.y_offset != 0.0f || ev.x_offset != 0.0f) {
|
||||
float scale = (float)_glfwWaylandWindowScale(window);
|
||||
ev.x_offset *= scale; ev.y_offset *= scale;
|
||||
ev.x_offset *= -1;
|
||||
_glfwInputScroll(window, &ev);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
|
|
|
|||
137
kitty/fixed_size_deque.h
Normal file
137
kitty/fixed_size_deque.h
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
* fixed_size_deque.h
|
||||
* Copyright (C) 2026 Kovid Goyal <kovid at kovidgoyal.net>
|
||||
*
|
||||
* Distributed under terms of the GPL3 license.
|
||||
*
|
||||
* A fixed size deque that does not allocate. To use define DEQUE_NAME, DEQUE_CAPACITY and
|
||||
* DEQUE_DATA_TYPE and include header. Use deque_push_back() to append. To
|
||||
* iterate in append order use deque_at(i) for 0 <= i < deque_size().
|
||||
*/
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <string.h>
|
||||
|
||||
#ifndef DEQUE_NAME
|
||||
#define DEQUE_NAME CircularDeque
|
||||
#endif
|
||||
|
||||
#ifndef DEQUE_CAPACITY
|
||||
#define DEQUE_CAPACITY 50
|
||||
#endif
|
||||
|
||||
#ifndef DEQUE_DATA_TYPE
|
||||
#define DEQUE_DATA_TYPE int
|
||||
#endif
|
||||
|
||||
typedef struct {
|
||||
DEQUE_DATA_TYPE items[DEQUE_CAPACITY];
|
||||
unsigned head; // Index of first element
|
||||
unsigned tail; // Index one past last element
|
||||
unsigned count; // Number of elements
|
||||
} DEQUE_NAME;
|
||||
|
||||
// Check if empty
|
||||
static inline bool
|
||||
deque_is_empty(const DEQUE_NAME* dq) { return dq->count == 0; }
|
||||
|
||||
// Check if full
|
||||
static inline bool
|
||||
deque_is_full(const DEQUE_NAME* dq) { return dq->count == DEQUE_CAPACITY; }
|
||||
|
||||
// Get current size
|
||||
static inline unsigned
|
||||
deque_size(const DEQUE_NAME* dq) { return dq->count; }
|
||||
|
||||
// Push to back auto-evicts from front if full.
|
||||
// Returns true if an item was evicted, which will be copied to *evicted_item is not NULL.
|
||||
static inline bool
|
||||
deque_push_back(DEQUE_NAME* dq, DEQUE_DATA_TYPE item, DEQUE_DATA_TYPE *evicted_item) {
|
||||
bool evicted = false;
|
||||
if (deque_is_full(dq)) {
|
||||
// Evict front item
|
||||
if (evicted_item) *evicted_item = dq->items[dq->head];
|
||||
evicted = true;
|
||||
dq->head = (dq->head + 1) % DEQUE_CAPACITY;
|
||||
dq->count--;
|
||||
}
|
||||
dq->items[dq->tail] = item;
|
||||
dq->tail = (dq->tail + 1) % DEQUE_CAPACITY;
|
||||
dq->count++;
|
||||
return evicted;
|
||||
}
|
||||
|
||||
// Push to front, auto-evicts from back if full.
|
||||
// Returns true if an item was evicted, which will be copied to *evicted_item is not NULL.
|
||||
static inline bool
|
||||
deque_push_front(DEQUE_NAME* dq, DEQUE_DATA_TYPE item, DEQUE_DATA_TYPE *evicted_item) {
|
||||
bool evicted = false;
|
||||
|
||||
if (deque_is_full(dq)) {
|
||||
// Evict oldest (back) item
|
||||
dq->tail = (dq->tail - 1 + DEQUE_CAPACITY) % DEQUE_CAPACITY;
|
||||
if (evicted_item) *evicted_item = dq->items[dq->tail];
|
||||
evicted = true;
|
||||
dq->count--;
|
||||
}
|
||||
|
||||
dq->head = (dq->head - 1 + DEQUE_CAPACITY) % DEQUE_CAPACITY;
|
||||
dq->items[dq->head] = item;
|
||||
dq->count++;
|
||||
|
||||
return evicted;
|
||||
}
|
||||
|
||||
// Pop from front
|
||||
static inline bool
|
||||
deque_pop_front(DEQUE_NAME* dq, DEQUE_DATA_TYPE *ans) {
|
||||
if (deque_is_empty(dq)) return false;
|
||||
if (ans) *ans = dq->items[dq->head];
|
||||
dq->head = (dq->head + 1) % DEQUE_CAPACITY;
|
||||
dq->count--;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Pop from back
|
||||
static inline bool
|
||||
deque_pop_back(DEQUE_NAME* dq, DEQUE_DATA_TYPE *ans) {
|
||||
if (deque_is_empty(dq)) return false;
|
||||
dq->tail = (dq->tail - 1 + DEQUE_CAPACITY) % DEQUE_CAPACITY;
|
||||
if (ans) *ans = dq->items[dq->tail];
|
||||
dq->count--;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Peek at front without removing
|
||||
static inline const DEQUE_DATA_TYPE*
|
||||
deque_peek_front(const DEQUE_NAME* dq) {
|
||||
if (deque_is_empty(dq)) return NULL;
|
||||
return &dq->items[dq->head];
|
||||
}
|
||||
|
||||
// Peek at back without removing
|
||||
static inline const DEQUE_DATA_TYPE*
|
||||
deque_peek_back(const DEQUE_NAME* dq) {
|
||||
if (deque_is_empty(dq)) return NULL;
|
||||
int idx = (dq->tail - 1 + DEQUE_CAPACITY) % DEQUE_CAPACITY;
|
||||
return &dq->items[idx];
|
||||
}
|
||||
|
||||
// Access by index (0 = oldest, count-1 = newest)
|
||||
static inline const DEQUE_DATA_TYPE*
|
||||
deque_at(const DEQUE_NAME* dq, unsigned index) {
|
||||
if (index >= dq->count) return NULL;
|
||||
return &dq->items[(dq->head + index) % DEQUE_CAPACITY];
|
||||
}
|
||||
|
||||
// Clear all items (doesn't free items)
|
||||
static inline void
|
||||
deque_clear(DEQUE_NAME* dq) {
|
||||
dq->head = 0;
|
||||
dq->tail = 0;
|
||||
dq->count = 0;
|
||||
}
|
||||
|
||||
#undef DEQUE_CAPACITY
|
||||
#undef DEQUE_DATA_TYPE
|
||||
#undef DEQUE_NAME
|
||||
Loading…
Add table
Add a link
Reference in a new issue