diff --git a/assets/config.conf b/assets/config.conf index ab607f19..aab45074 100644 --- a/assets/config.conf +++ b/assets/config.conf @@ -116,6 +116,12 @@ 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 +gesture_axis_lock=16 + # 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..4ce4c6f9 100644 --- a/docs/bindings/mouse-gestures.md +++ b/docs/bindings/mouse-gestures.md @@ -86,6 +86,34 @@ 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. + +`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 +``` + --- ## Switch Bindings diff --git a/src/animation/tag_scrub.h b/src/animation/tag_scrub.h new file mode 100644 index 00000000..d5039cca --- /dev/null +++ b/src/animation/tag_scrub.h @@ -0,0 +1,308 @@ +#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; + + /* 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) { + 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_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; + 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; + m->scrub_axis_locked = false; + return true; +} + +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; + 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; + 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; + } + + 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; + + double oriented_v = m->scrub_velocity * (double)m->scrub_dir; + + if (!config.animations) { + 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); + 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); + else + arrange(m, true, true); + } else { + 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; + } + 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; + 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..208cb2dc --- /dev/null +++ b/src/animation/tag_scrub_math.h @@ -0,0 +1,58 @@ +#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; + 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; + 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 (!skip_empty) + 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..b54aadab 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,9 @@ 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; + int32_t gesture_axis_lock; float focused_opacity; float unfocused_opacity; float *scroller_proportion_preset; @@ -1528,6 +1535,12 @@ 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, "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) { @@ -2973,6 +2986,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 +3568,11 @@ 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.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); @@ -3697,6 +3728,9 @@ 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.gesture_axis_lock = 16; config.idleinhibit_ignore_visible = 0; diff --git a/src/mango.c b/src/mango.c index 5549686c..fa70d3ec 100644 --- a/src/mango.c +++ b/src/mango.c @@ -580,6 +580,18 @@ 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; + bool scrub_axis_locked; }; typedef struct { @@ -1121,6 +1133,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 +2184,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 +2201,26 @@ 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_engaged(selmon)) { + tag_scrub_release(selmon, event->cancelled); + } else { + tag_scrub_abort(selmon); + 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); }