better feedback with no animations and multi-gesture handling

This commit is contained in:
Ernesto Cruz 2026-06-30 17:19:01 +01:00
parent 596e0a9ed3
commit 868feb48b7
6 changed files with 122 additions and 55 deletions

View file

@ -120,6 +120,7 @@ swipe_min_threshold=1
gesturebind=none,horizontal,4,tagscrub
gesture_swipe_distance=500
gesture_commit_ratio=0.5
gesture_axis_lock=16
# mouse
# need relogin to make it apply

View file

@ -107,6 +107,8 @@ The scrub follows your `trackpad_natural_scrolling` setting.
`gesture_commit_ratio` (01, default `0.5`) sets how far through that distance you must swipe to commit a `tagscrub`, e.g. `0.3` commits after 30%. A fast flick can commit a bit below this via momentum.
`gesture_axis_lock` (px, default `16`) is how far the swipe must travel before the scrub decides whether it's along its axis. Raise it if imprecise swipes get misread.
```ini
gesture_swipe_distance=500
gesture_commit_ratio=0.5

View file

@ -50,6 +50,23 @@ static inline void tag_scrub_stage(Monitor *m, int dir) {
m->scrub_rubberband = false;
m->scrub_incoming_tag = (uint32_t)target;
/* Mango lays out tags lazily: a tag you moved a window off of
* keeps stale window geometry until it's viewed. This is very annoying
* Here i try to predict the arrangement of the tag just so the tags don't
* appear miss-aligned in the scrub.
* */
{
uint32_t saved_curtag = m->pertag->curtag;
uint32_t saved_tagset = m->tagset[m->seltags];
m->pertag->curtag = (uint32_t)target;
m->tagset[m->seltags] = (1u << (target - 1));
pre_caculate_before_arrange(m, false, false, true);
m->pertag->ltidxs[target]->arrange(m);
m->pertag->curtag = saved_curtag;
m->tagset[m->seltags] = saved_tagset;
pre_caculate_before_arrange(m, false, false, true);
}
Client *c;
uint32_t inbit = 1u << (target - 1);
wl_list_for_each(c, &clients, link) {
@ -122,6 +139,23 @@ static inline void tag_scrub_apply(Monitor *m, double progress) {
static inline bool tag_scrub_active(Monitor *m) { return m && m->scrub_active; }
static inline bool tag_scrub_engaged(Monitor *m) {
return m && m->scrub_active && m->scrub_axis_locked;
}
static inline void tag_scrub_abort(Monitor *m) {
if (!m)
return;
m->scrub_active = false;
m->scrub_axis_locked = 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;
}
static inline bool tag_scrub_arm(Monitor *m, const GestureBinding *g) {
if (!m || m->isoverview || m->pertag->curtag == 0)
return false;
@ -136,6 +170,7 @@ static inline bool tag_scrub_arm(Monitor *m, const GestureBinding *g) {
m->scrub_velocity = 0.0;
m->scrub_last_delta = 0.0;
m->scrub_last_time = 0;
m->scrub_axis_locked = false;
return true;
}
@ -143,6 +178,21 @@ static inline void tag_scrub_feed(Monitor *m, double dx, double dy,
uint32_t time_msec) {
if (!m->scrub_active)
return;
if (!m->scrub_axis_locked) {
double along = (m->scrub_axis == HORIZONTAL) ? swipe_dx : swipe_dy;
double perp = (m->scrub_axis == HORIZONTAL) ? swipe_dy : swipe_dx;
double a = along < 0 ? -along : along;
double p = perp < 0 ? -perp : perp;
if (a + p < (double)config.gesture_axis_lock)
return;
if (p > a) {
m->scrub_active = false;
return;
}
m->scrub_axis_locked = true;
}
double delta = (m->scrub_axis == HORIZONTAL) ? dx : dy;
if (config.trackpad_natural_scrolling)
delta = -delta;
@ -162,7 +212,13 @@ static inline void tag_scrub_feed(Monitor *m, double dx, double dy,
if (!config.animations) {
m->scrub_dir = (int8_t)dir;
m->scrub_progress = mag_p;
uint32_t mask = tag_scrub_occupied_mask(m);
int target =
tag_scrub_neighbor((int)m->pertag->curtag, dir, LENGTH(tags), mask,
m->scrub_have_client, config.tag_carousel);
m->scrub_rubberband = (dir != 0 && target == 0);
m->scrub_incoming_tag = 0;
tag_scrub_apply(m, mag_p);
return;
}
@ -178,69 +234,68 @@ static inline void tag_scrub_release(Monitor *m, bool cancelled) {
if (!m->scrub_active)
return;
double oriented_v = m->scrub_velocity * (double)m->scrub_dir;
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)) {
bool commit = !cancelled && m->scrub_dir != 0 && !m->scrub_rubberband &&
gesture_scrub_should_commit(m->scrub_progress, oriented_v,
config.gesture_commit_ratio);
int target = 0;
if (commit) {
uint32_t mask = tag_scrub_occupied_mask(m);
int target = tag_scrub_neighbor(
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);
if (target != 0)
view_in_mon(&(Arg){.ui = 1u << (target - 1)}, false, m, true);
else
arrange(m, true, 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;
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) {
uint32_t inbit = 1u << (m->scrub_incoming_tag - 1);
uint32_t curbit = 1u << (m->pertag->curtag - 1);
Client *c;
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;
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();
}
request_fresh_all_monitors();
}
m->scrub_active = false;
m->scrub_axis_locked = false;
m->scrub_dir = 0;
m->scrub_progress = 0.0;
m->scrub_accum = 0.0;

View file

@ -21,6 +21,8 @@ static inline int tag_scrub_neighbor(int curtag, int dir, int ntags,
bool wrap) {
if (curtag < 1 || curtag > ntags || (dir != 1 && dir != -1))
return 0;
bool only_current_occupied = (occupied_mask & ~(1u << (curtag - 1))) == 0;
bool skip_empty = have_client && !only_current_occupied;
int t = curtag;
for (int step = 0; step < ntags; step++) {
t += dir;
@ -35,7 +37,7 @@ static inline int tag_scrub_neighbor(int curtag, int dir, int ntags,
}
if (t == curtag)
return 0;
if (!have_client)
if (!skip_empty)
return t;
if (occupied_mask & (1u << (t - 1)))
return t;

View file

@ -238,6 +238,7 @@ typedef struct {
uint32_t swipe_min_threshold;
float gesture_commit_ratio;
int32_t gesture_swipe_distance;
int32_t gesture_axis_lock;
float focused_opacity;
float unfocused_opacity;
float *scroller_proportion_preset;
@ -1538,6 +1539,8 @@ bool parse_option(Config *config, char *key, char *value) {
config->gesture_commit_ratio = atof(value);
} else if (strcmp(key, "gesture_swipe_distance") == 0) {
config->gesture_swipe_distance = atoi(value);
} else if (strcmp(key, "gesture_axis_lock") == 0) {
config->gesture_axis_lock = atoi(value);
} else if (strcmp(key, "focused_opacity") == 0) {
config->focused_opacity = atof(value);
} else if (strcmp(key, "unfocused_opacity") == 0) {
@ -3569,6 +3572,7 @@ void override_config(void) {
CLAMP_FLOAT(config.gesture_commit_ratio, 0.0f, 1.0f);
config.gesture_swipe_distance =
CLAMP_INT(config.gesture_swipe_distance, 50, 4000);
config.gesture_axis_lock = CLAMP_INT(config.gesture_axis_lock, 0, 1000);
config.mouse_natural_scrolling =
CLAMP_INT(config.mouse_natural_scrolling, 0, 1);
config.mouse_accel_profile = CLAMP_INT(config.mouse_accel_profile, 0, 2);
@ -3726,6 +3730,7 @@ void set_value_default() {
config.swipe_min_threshold = 1;
config.gesture_commit_ratio = 0.5f;
config.gesture_swipe_distance = 500;
config.gesture_axis_lock = 16;
config.idleinhibit_ignore_visible = 0;

View file

@ -591,6 +591,7 @@ struct Monitor {
double scrub_last_delta;
uint32_t scrub_last_time;
double scrub_velocity;
bool scrub_axis_locked;
};
typedef struct {
@ -2212,9 +2213,10 @@ void swipe_update(struct wl_listener *listener, void *data) {
void swipe_end(struct wl_listener *listener, void *data) {
struct wlr_pointer_swipe_end_event *event = data;
if (tag_scrub_active(selmon)) {
if (tag_scrub_engaged(selmon)) {
tag_scrub_release(selmon, event->cancelled);
} else {
tag_scrub_abort(selmon);
ongesture(event);
}
swipe_dx = 0;