From 596e0a9ed3561a053d93caece6cb15898b6990d8 Mon Sep 17 00:00:00 2001 From: Ernesto Cruz Date: Mon, 29 Jun 2026 21:28:57 +0100 Subject: [PATCH 1/2] smoother scrub feel, natural scrolling support and better snap back animations --- assets/config.conf | 5 + docs/bindings/mouse-gestures.md | 26 ++++ src/animation/tag_scrub.h | 253 ++++++++++++++++++++++++++++++++ src/animation/tag_scrub_math.h | 56 +++++++ src/config/parse_config.h | 29 ++++ src/mango.c | 35 ++++- 6 files changed, 399 insertions(+), 5 deletions(-) create mode 100644 src/animation/tag_scrub.h create mode 100644 src/animation/tag_scrub_math.h diff --git a/assets/config.conf b/assets/config.conf index ab607f19..90853a8d 100644 --- a/assets/config.conf +++ b/assets/config.conf @@ -116,6 +116,11 @@ left_handed=0 middle_button_emulation=0 swipe_min_threshold=1 +# 1:1 tag scrubbing +gesturebind=none,horizontal,4,tagscrub +gesture_swipe_distance=500 +gesture_commit_ratio=0.5 + # mouse # need relogin to make it apply mouse_natural_scrolling=0 diff --git a/docs/bindings/mouse-gestures.md b/docs/bindings/mouse-gestures.md index e96b6dbd..1c44c1c9 100644 --- a/docs/bindings/mouse-gestures.md +++ b/docs/bindings/mouse-gestures.md @@ -86,6 +86,32 @@ gesturebind=none,up,4,toggleoverview gesturebind=none,down,4,toggleoverview ``` +### Continuous (1:1) tag scrubbing +tagscrub drags the adjacent tag in proportionally to a multi-finger swipe, rather than firing once on release. Past half a screen (or a quick flick) commits the switch, otherwise the tag snaps back. + +```ini +# 4-finger horizontal: scrub between numeric-adjacent tags +gesturebind=none,horizontal,4,tagscrub + +# Skip empty tags (land on the next occupied tag), like *_have_client dispatchers +gesturebind=none,horizontal,4,tagscrub,have_client +``` + +- The direction field (`horizontal`/`vertical`) selects which finger axis drives the scrub. For best results match it to `tag_animation_direction`. + +### Scrub feel + +The scrub follows your `trackpad_natural_scrolling` setting. + +`gesture_swipe_distance` (px, default `500`) is how much finger travel maps to a full tag slide. Lower = more sensitive (less travel to switch); higher = more deliberate. + +`gesture_commit_ratio` (0–1, 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. + +```ini +gesture_swipe_distance=500 +gesture_commit_ratio=0.5 +``` + --- ## Switch Bindings diff --git a/src/animation/tag_scrub.h b/src/animation/tag_scrub.h new file mode 100644 index 00000000..d9d6c06c --- /dev/null +++ b/src/animation/tag_scrub.h @@ -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 */ diff --git a/src/animation/tag_scrub_math.h b/src/animation/tag_scrub_math.h new file mode 100644 index 00000000..a25e1e13 --- /dev/null +++ b/src/animation/tag_scrub_math.h @@ -0,0 +1,56 @@ +#ifndef TAG_SCRUB_MATH_H +#define TAG_SCRUB_MATH_H +#include +#include +#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 */ diff --git a/src/config/parse_config.h b/src/config/parse_config.h index 61af421b..8023b298 100644 --- a/src/config/parse_config.h +++ b/src/config/parse_config.h @@ -3,6 +3,7 @@ #include #include #include +#include #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; diff --git a/src/mango.c b/src/mango.c index 5549686c..3c861e2c 100644 --- a/src/mango.c +++ b/src/mango.c @@ -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); } From 868feb48b7624873fd100baaef706d83fa1133a4 Mon Sep 17 00:00:00 2001 From: Ernesto Cruz Date: Tue, 30 Jun 2026 17:19:01 +0100 Subject: [PATCH 2/2] better feedback with no animations and multi-gesture handling --- assets/config.conf | 1 + docs/bindings/mouse-gestures.md | 2 + src/animation/tag_scrub.h | 161 +++++++++++++++++++++----------- src/animation/tag_scrub_math.h | 4 +- src/config/parse_config.h | 5 + src/mango.c | 4 +- 6 files changed, 122 insertions(+), 55 deletions(-) diff --git a/assets/config.conf b/assets/config.conf index 90853a8d..aab45074 100644 --- a/assets/config.conf +++ b/assets/config.conf @@ -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 diff --git a/docs/bindings/mouse-gestures.md b/docs/bindings/mouse-gestures.md index 1c44c1c9..4ce4c6f9 100644 --- a/docs/bindings/mouse-gestures.md +++ b/docs/bindings/mouse-gestures.md @@ -107,6 +107,8 @@ The scrub follows your `trackpad_natural_scrolling` setting. `gesture_commit_ratio` (0–1, 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 diff --git a/src/animation/tag_scrub.h b/src/animation/tag_scrub.h index d9d6c06c..d5039cca 100644 --- a/src/animation/tag_scrub.h +++ b/src/animation/tag_scrub.h @@ -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; diff --git a/src/animation/tag_scrub_math.h b/src/animation/tag_scrub_math.h index a25e1e13..208cb2dc 100644 --- a/src/animation/tag_scrub_math.h +++ b/src/animation/tag_scrub_math.h @@ -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; diff --git a/src/config/parse_config.h b/src/config/parse_config.h index 8023b298..b54aadab 100644 --- a/src/config/parse_config.h +++ b/src/config/parse_config.h @@ -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; diff --git a/src/mango.c b/src/mango.c index 3c861e2c..fa70d3ec 100644 --- a/src/mango.c +++ b/src/mango.c @@ -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;