smoother scrub feel, natural scrolling support and better snap back animations

This commit is contained in:
Ernesto Cruz 2026-06-29 21:28:57 +01:00
parent 8216cacb3b
commit 596e0a9ed3
6 changed files with 399 additions and 5 deletions

253
src/animation/tag_scrub.h Normal file
View file

@ -0,0 +1,253 @@
#ifndef TAG_SCRUB_H
#define TAG_SCRUB_H
#include "tag_scrub_math.h"
static inline uint32_t tag_scrub_occupied_mask(Monitor *m) {
uint32_t mask = 0;
Client *c;
wl_list_for_each(c, &clients, link) {
if (c->mon == m && !c->iskilling)
mask |= (c->tags & TAGMASK);
}
return mask;
}
static inline void tag_scrub_unstage(Monitor *m) {
if (!m->scrub_incoming_tag) {
m->scrub_dir = 0;
m->scrub_rubberband = false;
return;
}
Client *c;
uint32_t inbit = 1u << (m->scrub_incoming_tag - 1);
wl_list_for_each(c, &clients, link) {
if (c->mon != m || !(c->tags & inbit))
continue;
c->animation.tagining = false;
c->animation.running = false;
c->is_clip_to_hide = false;
wlr_scene_node_set_enabled(&c->scene->node, false);
}
m->scrub_incoming_tag = 0;
m->scrub_dir = 0;
m->scrub_rubberband = false;
}
static inline void tag_scrub_stage(Monitor *m, int dir) {
int curtag = (int)m->pertag->curtag;
int ntags = LENGTH(tags);
uint32_t mask = tag_scrub_occupied_mask(m);
int target = tag_scrub_neighbor(curtag, dir, ntags, mask,
m->scrub_have_client, config.tag_carousel);
m->scrub_dir = (int8_t)dir;
m->carousel_anim_dir = (int8_t)dir;
if (target == 0) {
m->scrub_rubberband = true;
m->scrub_incoming_tag = 0;
return;
}
m->scrub_rubberband = false;
m->scrub_incoming_tag = (uint32_t)target;
Client *c;
uint32_t inbit = 1u << (target - 1);
wl_list_for_each(c, &clients, link) {
if (c->mon != m || !(c->tags & inbit) || !ISTILED(c))
continue;
if (c->isglobal || c->isunglobal)
continue;
wlr_scene_node_set_enabled(&c->scene->node, true);
c->animation.tagining = true;
c->animation.tagouting = false;
c->animation.running = false;
c->animation.current = c->geom;
set_tagin_animation(m, c);
c->animation.current = c->animainit_geom;
}
}
static inline void tag_scrub_apply(Monitor *m, double progress) {
if (progress < 0.0)
progress = 0.0;
if (progress > 1.0)
progress = 1.0;
m->scrub_progress = progress;
double eff = m->scrub_rubberband ? progress * 0.2 : progress;
Client *c;
uint32_t inbit =
m->scrub_incoming_tag ? (1u << (m->scrub_incoming_tag - 1)) : 0;
uint32_t curbit = 1u << (m->pertag->curtag - 1);
wl_list_for_each(c, &clients, link) {
if (c->mon != m || !ISTILED(c))
continue;
struct wlr_box g;
if (inbit && (c->tags & inbit) && !(c->isglobal || c->isunglobal)) {
g.x = (int)(c->animainit_geom.x +
(c->geom.x - c->animainit_geom.x) * eff);
g.y = (int)(c->animainit_geom.y +
(c->geom.y - c->animainit_geom.y) * eff);
g.width = c->geom.width;
g.height = c->geom.height;
c->animation.current = g;
wlr_scene_node_set_position(&c->scene->node, g.x, g.y);
client_apply_clip(c, 1.0);
c->is_clip_to_hide = false;
wlr_scene_node_set_enabled(&c->scene->node, true);
c->animation.running = false;
c->need_output_flush = false;
} else if (c->tags & curbit) {
int off = (int)(-(int)m->scrub_dir *
(config.tag_animation_direction == HORIZONTAL
? m->m.width
: m->m.height) *
eff);
g = c->geom;
if (config.tag_animation_direction == HORIZONTAL)
g.x = c->geom.x + off;
else
g.y = c->geom.y + off;
c->animation.tagouting = (eff > 0.0);
c->animation.current = g;
wlr_scene_node_set_position(&c->scene->node, g.x, g.y);
client_apply_clip(c, 1.0);
c->animation.running = false;
c->need_output_flush = false;
}
}
request_fresh_all_monitors();
}
static inline bool tag_scrub_active(Monitor *m) { return m && m->scrub_active; }
static inline bool tag_scrub_arm(Monitor *m, const GestureBinding *g) {
if (!m || m->isoverview || m->pertag->curtag == 0)
return false;
m->scrub_active = true;
m->scrub_axis = g->axis;
m->scrub_have_client = g->have_client;
m->scrub_dir = 0;
m->scrub_incoming_tag = 0;
m->scrub_rubberband = false;
m->scrub_accum = 0.0;
m->scrub_progress = 0.0;
m->scrub_velocity = 0.0;
m->scrub_last_delta = 0.0;
m->scrub_last_time = 0;
return true;
}
static inline void tag_scrub_feed(Monitor *m, double dx, double dy,
uint32_t time_msec) {
if (!m->scrub_active)
return;
double delta = (m->scrub_axis == HORIZONTAL) ? dx : dy;
if (config.trackpad_natural_scrolling)
delta = -delta;
m->scrub_accum += delta;
double dim = (double)config.gesture_swipe_distance;
uint32_t dt = (m->scrub_last_time && time_msec > m->scrub_last_time)
? (time_msec - m->scrub_last_time)
: 1;
double inst_v = (dim > 0) ? (delta / dim) / (double)dt : 0.0;
m->scrub_velocity = m->scrub_velocity * 0.5 + inst_v * 0.5;
m->scrub_last_delta = delta;
m->scrub_last_time = time_msec;
int dir = (m->scrub_accum > 0) ? +1 : (m->scrub_accum < 0 ? -1 : 0);
double signed_p = tag_scrub_progress(m->scrub_accum, dim);
double mag_p = signed_p < 0 ? -signed_p : signed_p;
if (!config.animations) {
m->scrub_dir = (int8_t)dir;
m->scrub_progress = mag_p;
return;
}
if (dir != 0 && dir != m->scrub_dir) {
if (m->scrub_incoming_tag || m->scrub_rubberband)
tag_scrub_unstage(m);
tag_scrub_stage(m, dir);
}
tag_scrub_apply(m, mag_p);
}
static inline void tag_scrub_release(Monitor *m, bool cancelled) {
if (!m->scrub_active)
return;
if (!config.animations) {
double oriented_v = m->scrub_velocity * (double)m->scrub_dir;
if (!cancelled && m->scrub_dir != 0 &&
gesture_scrub_should_commit(m->scrub_progress, oriented_v,
config.gesture_commit_ratio)) {
uint32_t mask = tag_scrub_occupied_mask(m);
int target = tag_scrub_neighbor(
(int)m->pertag->curtag, m->scrub_dir, LENGTH(tags), mask,
m->scrub_have_client, config.tag_carousel);
if (target != 0)
view_in_mon(&(Arg){.ui = 1u << (target - 1)}, false, m, true);
}
m->scrub_active = false;
m->scrub_dir = 0;
m->scrub_progress = 0.0;
m->scrub_accum = 0.0;
m->scrub_velocity = 0.0;
return;
}
double oriented_v = m->scrub_velocity * (double)m->scrub_dir;
bool commit = !cancelled && !m->scrub_rubberband && m->scrub_incoming_tag &&
gesture_scrub_should_commit(m->scrub_progress, oriented_v,
config.gesture_commit_ratio);
if (commit) {
Client *c;
uint32_t inbit = 1u << (m->scrub_incoming_tag - 1);
uint32_t curbit = 1u << (m->pertag->curtag - 1);
wl_list_for_each(c, &clients, link) {
if (c->mon == m && ISTILED(c) &&
((c->tags & inbit) || (c->tags & curbit)))
c->animation.running = true;
}
view_in_mon(&(Arg){.ui = inbit}, true, m, true);
} else {
uint32_t curbit = 1u << (m->pertag->curtag - 1);
uint32_t inbit =
m->scrub_incoming_tag ? (1u << (m->scrub_incoming_tag - 1)) : 0;
uint32_t now = get_now_in_ms();
Client *c;
wl_list_for_each(c, &clients, link) {
if (c->mon != m || !ISTILED(c))
continue;
bool is_incoming =
inbit && (c->tags & inbit) && !(c->isglobal || c->isunglobal);
if (!is_incoming && !(c->tags & curbit))
continue;
c->animation.initial = c->animation.current;
c->current = is_incoming ? c->animainit_geom : c->geom;
c->animation.tagining = false;
c->animation.tagouting = is_incoming;
c->animation.action = TAG;
c->animation.duration = config.animation_duration_tag;
c->animation.time_started = now;
c->animation.running = true;
c->need_output_flush = true;
}
request_fresh_all_monitors();
}
m->scrub_active = false;
m->scrub_dir = 0;
m->scrub_progress = 0.0;
m->scrub_accum = 0.0;
m->scrub_velocity = 0.0;
m->scrub_incoming_tag = 0;
m->scrub_rubberband = false;
m->carousel_anim_dir = 0;
}
#endif /* TAG_SCRUB_H */

View file

@ -0,0 +1,56 @@
#ifndef TAG_SCRUB_MATH_H
#define TAG_SCRUB_MATH_H
#include <stdbool.h>
#include <stdint.h>
#define TAG_SCRUB_PROJECTION_FACTOR 18.0
static inline double tag_scrub_progress(double accumulated_delta,
double monitor_dim) {
if (monitor_dim <= 0.0)
return 0.0;
double p = accumulated_delta / monitor_dim;
if (p > 1.0)
p = 1.0;
if (p < -1.0)
p = -1.0;
return p;
}
static inline int tag_scrub_neighbor(int curtag, int dir, int ntags,
uint32_t occupied_mask, bool have_client,
bool wrap) {
if (curtag < 1 || curtag > ntags || (dir != 1 && dir != -1))
return 0;
int t = curtag;
for (int step = 0; step < ntags; step++) {
t += dir;
if (t < 1) {
if (!wrap)
return 0;
t = ntags;
} else if (t > ntags) {
if (!wrap)
return 0;
t = 1;
}
if (t == curtag)
return 0;
if (!have_client)
return t;
if (occupied_mask & (1u << (t - 1)))
return t;
}
return 0;
}
static inline bool gesture_scrub_should_commit(double progress_magnitude,
double velocity,
double commit_ratio) {
double projected =
progress_magnitude + velocity * TAG_SCRUB_PROJECTION_FACTOR;
if (projected < 0.0)
projected = -projected;
return projected >= commit_ratio;
}
#endif /* TAG_SCRUB_MATH_H */

View file

@ -3,6 +3,7 @@
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <strings.h>
#ifndef SYSCONFDIR
#define SYSCONFDIR "/etc"
@ -156,6 +157,9 @@ typedef struct {
uint32_t fingers_count;
int32_t (*func)(const Arg *);
Arg arg;
bool continuous;
uint8_t axis;
bool have_client;
} GestureBinding;
typedef struct {
@ -232,6 +236,8 @@ typedef struct {
int32_t drag_tile_to_tile;
int32_t drag_tile_small;
uint32_t swipe_min_threshold;
float gesture_commit_ratio;
int32_t gesture_swipe_distance;
float focused_opacity;
float unfocused_opacity;
float *scroller_proportion_preset;
@ -1528,6 +1534,10 @@ bool parse_option(Config *config, char *key, char *value) {
config->drag_tile_small = atoi(value);
} else if (strcmp(key, "swipe_min_threshold") == 0) {
config->swipe_min_threshold = atoi(value);
} else if (strcmp(key, "gesture_commit_ratio") == 0) {
config->gesture_commit_ratio = atof(value);
} else if (strcmp(key, "gesture_swipe_distance") == 0) {
config->gesture_swipe_distance = atoi(value);
} else if (strcmp(key, "focused_opacity") == 0) {
config->focused_opacity = atof(value);
} else if (strcmp(key, "unfocused_opacity") == 0) {
@ -2973,6 +2983,19 @@ bool parse_option(Config *config, char *key, char *value) {
binding->arg.v2 = NULL;
binding->arg.v3 = NULL;
binding->arg.tc = NULL;
if (strcmp(func_name, "tagscrub") == 0) {
binding->continuous = true;
binding->func = NULL;
if (strcasecmp(motion_str, "vertical") == 0)
binding->axis = VERTICAL;
else
binding->axis = HORIZONTAL;
binding->have_client = (strcmp(arg_value, "have_client") == 0);
config->gesture_bindings_count++;
return true;
}
binding->func =
parse_func_name(func_name, &binding->arg, arg_value, arg_value2,
arg_value3, arg_value4, arg_value5);
@ -3542,6 +3565,10 @@ void override_config(void) {
config.middle_button_emulation =
CLAMP_INT(config.middle_button_emulation, 0, 1);
config.swipe_min_threshold = CLAMP_INT(config.swipe_min_threshold, 1, 1000);
config.gesture_commit_ratio =
CLAMP_FLOAT(config.gesture_commit_ratio, 0.0f, 1.0f);
config.gesture_swipe_distance =
CLAMP_INT(config.gesture_swipe_distance, 50, 4000);
config.mouse_natural_scrolling =
CLAMP_INT(config.mouse_natural_scrolling, 0, 1);
config.mouse_accel_profile = CLAMP_INT(config.mouse_accel_profile, 0, 2);
@ -3697,6 +3724,8 @@ void set_value_default() {
config.drag_tile_small = 1;
config.enable_floating_snap = 0;
config.swipe_min_threshold = 1;
config.gesture_commit_ratio = 0.5f;
config.gesture_swipe_distance = 500;
config.idleinhibit_ignore_visible = 0;

View file

@ -580,6 +580,17 @@ struct Monitor {
struct wlr_ext_workspace_group_handle_v1 *ext_group;
bool iscleanuping;
int8_t carousel_anim_dir;
bool scrub_active;
uint8_t scrub_axis;
int8_t scrub_dir;
bool scrub_have_client;
bool scrub_rubberband;
uint32_t scrub_incoming_tag;
double scrub_accum;
double scrub_progress;
double scrub_last_delta;
uint32_t scrub_last_time;
double scrub_velocity;
};
typedef struct {
@ -1121,6 +1132,8 @@ static struct wl_event_source *sync_keymap;
#include "animation/common.h"
#include "animation/layer.h"
#include "animation/tag.h"
#include "animation/tag_scrub.h"
#include "animation/tag_scrub_math.h"
#include "dispatch/bind_define.h"
#include "ext-protocol/all.h"
#include "fetch/fetch.h"
@ -2170,7 +2183,15 @@ int32_t ongesture(struct wlr_pointer_swipe_end_event *event) {
void swipe_begin(struct wl_listener *listener, void *data) {
struct wlr_pointer_swipe_begin_event *event = data;
// Forward swipe begin event to client
int ji;
for (ji = 0; ji < config.gesture_bindings_count; ji++) {
const GestureBinding *g = &config.gesture_bindings[ji];
if (g->continuous && g->fingers_count == event->fingers) {
if (tag_scrub_arm(selmon, g))
break;
}
}
wlr_pointer_gestures_v1_send_swipe_begin(pointer_gestures, seat,
event->time_msec, event->fingers);
}
@ -2179,21 +2200,25 @@ void swipe_update(struct wl_listener *listener, void *data) {
struct wlr_pointer_swipe_update_event *event = data;
swipe_fingers = event->fingers;
// Accumulate swipe distance
swipe_dx += event->dx;
swipe_dy += event->dy;
// Forward swipe update event to client
if (tag_scrub_active(selmon))
tag_scrub_feed(selmon, event->dx, event->dy, event->time_msec);
wlr_pointer_gestures_v1_send_swipe_update(
pointer_gestures, seat, event->time_msec, event->dx, event->dy);
}
void swipe_end(struct wl_listener *listener, void *data) {
struct wlr_pointer_swipe_end_event *event = data;
ongesture(event);
if (tag_scrub_active(selmon)) {
tag_scrub_release(selmon, event->cancelled);
} else {
ongesture(event);
}
swipe_dx = 0;
swipe_dy = 0;
// Forward swipe end event to client
wlr_pointer_gestures_v1_send_swipe_end(pointer_gestures, seat,
event->time_msec, event->cancelled);
}