diff --git a/common/gesture.c b/common/gesture.c index 58170443..a2c0908b 100644 --- a/common/gesture.c +++ b/common/gesture.c @@ -90,6 +90,60 @@ char *gesture_parse(const char *input, struct gesture *output) { return NULL; } +// Similar to gesture_parse but with fewer checks to match the different +// workspace_gesture syntax +char *workspace_gesture_parse(const char *input, struct gesture *output) { + // Clear output in case of failure + output->type = GESTURE_TYPE_NONE; + output->fingers = GESTURE_FINGERS_ANY; + output->directions = GESTURE_DIRECTION_NONE; + + // Split input type, fingers and directions + list_t *split = split_string(input, ":"); + if (split->length < 1 || split->length > 2) { + return format_str( + "expected [:][:direction], got %s", + input); + } + + // Parse optional arguments + if (split->length > 1) { + char *next = split->items[0]; + + // Try to parse as finger count (1-9) + if (strlen(next) == 1 && '1' <= next[0] && next[0] <= '9') { + output->fingers = atoi(next); + + // Move to next if available + next = split->length == 2 ? split->items[1] : NULL; + } else if (split->length == 2) { + // Fail here if argument can only be finger count + return format_str("expected 1-9, got %s", next); + } + + // If there is an argument left, try to parse as direction + if (next) { + list_t *directions = split_string(next, "+"); + + for (int i = 0; i < directions->length; ++i) { + const char *item = directions->items[i]; + if (strcmp(item, "horizontal") == 0) { + output->type = GESTURE_TYPE_WORKSPACE_SWIPE_HORIZONTAL; + } else if (strcmp(item, "vertical") == 0) { + output->type = GESTURE_TYPE_WORKSPACE_SWIPE_VERTICAL; + } else { + return format_str("expected direction, got %s", item); + } + } + list_free_items_and_destroy(directions); + } + } // if optional args + + list_free_items_and_destroy(split); + + return NULL; +} + const char *gesture_type_string(enum gesture_type type) { switch (type) { case GESTURE_TYPE_NONE: @@ -100,6 +154,10 @@ const char *gesture_type_string(enum gesture_type type) { return "pinch"; case GESTURE_TYPE_SWIPE: return "swipe"; + case GESTURE_TYPE_WORKSPACE_SWIPE_HORIZONTAL: + return "workspace_swipe_horizontal"; + case GESTURE_TYPE_WORKSPACE_SWIPE_VERTICAL: + return "workspace_swipe_vertical"; } return NULL; @@ -313,6 +371,8 @@ struct gesture *gesture_tracker_end(struct gesture_tracker *tracker) { } // Gesture without any direction case GESTURE_TYPE_HOLD: + case GESTURE_TYPE_WORKSPACE_SWIPE_HORIZONTAL: + case GESTURE_TYPE_WORKSPACE_SWIPE_VERTICAL: break; // Not tracking any gesture case GESTURE_TYPE_NONE: diff --git a/include/gesture.h b/include/gesture.h index 9c6b0f91..38e9ff6f 100644 --- a/include/gesture.h +++ b/include/gesture.h @@ -12,6 +12,8 @@ enum gesture_type { GESTURE_TYPE_HOLD, GESTURE_TYPE_PINCH, GESTURE_TYPE_SWIPE, + GESTURE_TYPE_WORKSPACE_SWIPE_HORIZONTAL, + GESTURE_TYPE_WORKSPACE_SWIPE_VERTICAL, }; // Turns single type enum value to constant string representation. @@ -57,6 +59,13 @@ struct gesture { */ char *gesture_parse(const char *input, struct gesture *output); +/** + * Parses workspace gesture from [:][:] string. + * + * Return NULL on success, otherwise error message string + */ +char *workspace_gesture_parse(const char *input, struct gesture *output); + // Turns gesture into string representation char *gesture_to_string(struct gesture *gesture); diff --git a/include/sway/commands.h b/include/sway/commands.h index 8f21b989..e5c6ab27 100644 --- a/include/sway/commands.h +++ b/include/sway/commands.h @@ -115,6 +115,7 @@ sway_cmd cmd_bindcode; sway_cmd cmd_bindgesture; sway_cmd cmd_bindswitch; sway_cmd cmd_bindsym; +sway_cmd cmd_bindworkspacegesture; sway_cmd cmd_blur; sway_cmd cmd_blur_brightness; sway_cmd cmd_blur_contrast; @@ -223,11 +224,15 @@ sway_cmd cmd_unbindcode; sway_cmd cmd_unbindswitch; sway_cmd cmd_unbindgesture; sway_cmd cmd_unbindsym; +sway_cmd cmd_unbindworkspacegesture; sway_cmd cmd_unmark; sway_cmd cmd_urgent; sway_cmd cmd_workspace; sway_cmd cmd_workspace_layout; sway_cmd cmd_ws_auto_back_and_forth; +sway_cmd cmd_ws_gesture_spring_size; +sway_cmd cmd_ws_gesture_threshold; +sway_cmd cmd_ws_gesture_wrap_around; sway_cmd cmd_xwayland; sway_cmd bar_cmd_bindcode; diff --git a/include/sway/config.h b/include/sway/config.h index 84e4ab8e..e81198d5 100644 --- a/include/sway/config.h +++ b/include/sway/config.h @@ -51,6 +51,7 @@ enum binding_flags { BINDING_INHIBITED = 1 << 7, // keyboard only: ignore shortcut inhibitor BINDING_NOREPEAT = 1 << 8, // keyboard only; do not trigger when repeating a held key BINDING_EXACT = 1 << 9, // gesture only; only trigger on exact match + BINDING_INVERTED = 1 << 10, // workspace gesture only; gesture is inverted }; /** @@ -501,6 +502,10 @@ struct sway_config { bool titlebar_separator; bool scratchpad_minimize; + int workspace_gesture_spring_size; + bool workspace_gesture_wrap_around; + float workspace_gesture_threshold; + list_t *layer_criteria; char *swaynag_command; diff --git a/include/sway/fx_util/animation_utils.h b/include/sway/fx_util/animation_utils.h new file mode 100644 index 00000000..63e3b487 --- /dev/null +++ b/include/sway/fx_util/animation_utils.h @@ -0,0 +1,10 @@ +#ifndef ANIMATION_UTILS_H +#define ANIMATION_UTILS_H + +double lerp (double a, double b, double t); + +double ease_out_cubic (double t); + +// TODO: Add more easing functions in the future like ease_in and ease_in_out, etc... + +#endif diff --git a/include/sway/output.h b/include/sway/output.h index a60ddec8..940b85b4 100644 --- a/include/sway/output.h +++ b/include/sway/output.h @@ -15,6 +15,19 @@ struct decoration_data get_undecorated_decoration_data(); struct sway_server; struct sway_container; +enum swipe_gesture_direction { + SWIPE_GESTURE_DIRECTION_NONE, + SWIPE_GESTURE_DIRECTION_HORIZONTAL, + SWIPE_GESTURE_DIRECTION_VERTICAL, +}; + +struct workspace_scroll { + double percent; + double avg_velocity; + int num_updates; + enum swipe_gesture_direction direction; +}; + struct decoration_data { float alpha; float saturation; @@ -32,6 +45,7 @@ struct render_data { pixman_region32_t *damage; struct wlr_box *clip_box; struct decoration_data deco_data; + bool on_focused_workspace; struct sway_view *view; }; @@ -71,6 +85,8 @@ struct sway_output { struct wl_listener needs_frame; struct wl_listener request_state; + struct workspace_scroll workspace_scroll; + struct { struct wl_signal disable; } events; @@ -227,6 +243,20 @@ void handle_output_manager_test(struct wl_listener *listener, void *data); void handle_output_power_manager_set_mode(struct wl_listener *listener, void *data); +struct workspace_scroll workspace_scroll_get_default(); + +bool workspace_scroll_equal(struct workspace_scroll *a, struct workspace_scroll *b); + +void workspace_scroll_begin(struct sway_seat *seat, + enum swipe_gesture_direction direction); + +void workspace_scroll_update(struct sway_seat *seat, struct gesture_tracker *tracker, + struct wlr_pointer_swipe_update_event *event, int invert); + +void workspace_scroll_end(struct sway_seat *seat); + +void workspace_scroll_reset(struct sway_seat *seat, struct sway_workspace *ws); + struct sway_output_non_desktop *output_non_desktop_create(struct wlr_output *wlr_output); #endif diff --git a/include/sway/tree/workspace.h b/include/sway/tree/workspace.h index 1e7d9b82..9772734f 100644 --- a/include/sway/tree/workspace.h +++ b/include/sway/tree/workspace.h @@ -70,11 +70,13 @@ struct sway_workspace *workspace_by_number(const char* name); struct sway_workspace *workspace_by_name(const char*); -struct sway_workspace *workspace_output_next(struct sway_workspace *current); +struct sway_workspace *workspace_output_next(struct sway_workspace *current, + bool should_wrap); struct sway_workspace *workspace_next(struct sway_workspace *current); -struct sway_workspace *workspace_output_prev(struct sway_workspace *current); +struct sway_workspace *workspace_output_prev(struct sway_workspace *current, + bool should_wrap); struct sway_workspace *workspace_prev(struct sway_workspace *current); diff --git a/sway/commands.c b/sway/commands.c index 2e048203..eec7362c 100644 --- a/sway/commands.c +++ b/sway/commands.c @@ -49,6 +49,7 @@ static const struct cmd_handler handlers[] = { { "bindgesture", cmd_bindgesture }, { "bindswitch", cmd_bindswitch }, { "bindsym", cmd_bindsym }, + { "bindworkspacegesture", cmd_bindworkspacegesture }, { "blur", cmd_blur }, { "blur_brightness", cmd_blur_brightness }, { "blur_contrast", cmd_blur_contrast }, @@ -118,8 +119,12 @@ static const struct cmd_handler handlers[] = { { "unbindgesture", cmd_unbindgesture }, { "unbindswitch", cmd_unbindswitch }, { "unbindsym", cmd_unbindsym }, + { "unbindworkspacegesture", cmd_unbindworkspacegesture }, { "workspace", cmd_workspace }, { "workspace_auto_back_and_forth", cmd_ws_auto_back_and_forth }, + { "workspace_gesture_spring_size", cmd_ws_gesture_spring_size }, + { "workspace_gesture_threshold", cmd_ws_gesture_threshold }, + { "workspace_gesture_wrap_around", cmd_ws_gesture_wrap_around }, }; /* Config-time only commands. Keep alphabetized */ diff --git a/sway/commands/gesture.c b/sway/commands/gesture.c index d4442cc3..1cc010a1 100644 --- a/sway/commands/gesture.c +++ b/sway/commands/gesture.c @@ -164,3 +164,76 @@ struct cmd_results *cmd_bindgesture(int argc, char **argv) { struct cmd_results *cmd_unbindgesture(int argc, char **argv) { return cmd_bind_or_unbind_gesture(argc, argv, true); } + +/** + * Parse and execute bindgesture or unbindgesture command. + */ +static struct cmd_results *cmd_bind_or_unbind_workspacegesture(int argc, + char **argv, bool unbind) { + int minargs = 1; + char *bindtype = "bindgesture"; + if (unbind) { + bindtype = "unbindgesture"; + } + + struct cmd_results *error = NULL; + if ((error = checkarg(argc, bindtype, EXPECTED_AT_LEAST, minargs))) { + return error; + } + struct sway_gesture_binding *binding = calloc(1, sizeof(struct sway_gesture_binding)); + if (!binding) { + return cmd_results_new(CMD_FAILURE, "Unable to allocate binding"); + } + binding->input = strdup("*"); + + bool warn = true; + + // Handle flags + binding->flags |= BINDING_EXACT; + while (argc > 0) { + if (strcmp("--inverted", argv[0]) == 0) { + binding->flags |= BINDING_INVERTED; + } else if (strcmp("--no-warn", argv[0]) == 0) { + warn = false; + } else if (strncmp("--input-device=", argv[0], + strlen("--input-device=")) == 0) { + free(binding->input); + binding->input = strdup(argv[0] + strlen("--input-device=")); + } else { + break; + } + argv++; + argc--; + } + + if (argc < minargs) { + free(binding); + return cmd_results_new(CMD_FAILURE, + "Invalid %s command (expected at least %d " + "non-option arguments, got %d)", bindtype, minargs, argc); + } + + char* errmsg = NULL; + if ((errmsg = workspace_gesture_parse(argv[0], &binding->gesture))) { + free(binding); + struct cmd_results *final = cmd_results_new(CMD_FAILURE, + "Invalid %s command (%s)", + bindtype, errmsg); + free(errmsg); + return final; + } + + if (unbind) { + return gesture_binding_remove(binding, argv[0]); + } + binding->command = NULL; + return gesture_binding_add(binding, argv[0], warn); +} + +struct cmd_results *cmd_bindworkspacegesture(int argc, char **argv) { + return cmd_bind_or_unbind_workspacegesture(argc, argv, false); +} + +struct cmd_results *cmd_unbindworkspacegesture(int argc, char **argv) { + return cmd_bind_or_unbind_workspacegesture(argc, argv, true); +} diff --git a/sway/commands/workspace.c b/sway/commands/workspace.c index 03e488ba..a1d7808e 100644 --- a/sway/commands/workspace.c +++ b/sway/commands/workspace.c @@ -6,10 +6,12 @@ #include "sway/commands.h" #include "sway/config.h" #include "sway/input/seat.h" +#include "sway/output.h" #include "sway/tree/workspace.h" #include "list.h" #include "log.h" #include "stringop.h" +#include "util.h" static struct workspace_config *workspace_config_find_or_create(char *ws_name) { struct workspace_config *wsc = workspace_find_config(ws_name); diff --git a/sway/commands/workspace_gesture.c b/sway/commands/workspace_gesture.c new file mode 100644 index 00000000..6c007066 --- /dev/null +++ b/sway/commands/workspace_gesture.c @@ -0,0 +1,49 @@ +#define _POSIX_C_SOURCE 200809L +#include "sway/commands.h" +#include "sway/config.h" +#include "util.h" + +struct cmd_results *cmd_ws_gesture_spring_size(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "workspace_gesture_spring_size", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + char *inv; + int value = strtol(argv[0], &inv, 10); + if (*inv != '\0' || value < 0 || value > 250) { + return cmd_results_new(CMD_FAILURE, "Invalid size specified"); + } + + config->workspace_gesture_spring_size = value; + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +struct cmd_results *cmd_ws_gesture_wrap_around(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "workspace_gesture_wrap_around", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + config->workspace_gesture_wrap_around = parse_boolean(argv[0], true); + + return cmd_results_new(CMD_SUCCESS, NULL); +} + +struct cmd_results *cmd_ws_gesture_threshold(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "workspace_gesture_threshold", EXPECTED_EQUAL_TO, 1))) { + return error; + } + + char *err; + float val = strtof(argv[0], &err); + if (*err || val < 0.1f || val > 0.9f) { + return cmd_results_new(CMD_INVALID, "workspace_gesture_threshold float invalid. " + "Should be between 0.1 and 0.9"); + } + config->workspace_gesture_threshold = val; + + return cmd_results_new(CMD_SUCCESS, NULL); +} diff --git a/sway/config.c b/sway/config.c index 616965c1..507a2e4d 100644 --- a/sway/config.c +++ b/sway/config.c @@ -369,6 +369,10 @@ static void config_defaults(struct sway_config *config) { config->titlebar_separator = true; config->scratchpad_minimize = false; + config->workspace_gesture_spring_size = 50; + config->workspace_gesture_wrap_around = false; + config->workspace_gesture_threshold = 0.5; + if (!(config->layer_criteria = create_list())) goto cleanup; // The keysym to keycode translation diff --git a/sway/desktop/output.c b/sway/desktop/output.c index 2ccb28f2..b927150e 100644 --- a/sway/desktop/output.c +++ b/sway/desktop/output.c @@ -23,6 +23,7 @@ #include "log.h" #include "scenefx/render/pass.h" #include "sway/config.h" +#include "sway/fx_util/animation_utils.h" #include "sway/desktop/transaction.h" #include "sway/input/input-manager.h" #include "sway/input/seat.h" @@ -36,6 +37,10 @@ #include "sway/tree/root.h" #include "sway/tree/view.h" #include "sway/tree/workspace.h" +#include "util.h" + +#define PREV_WS_LIMIT -1.0f +#define NEXT_WS_LIMIT 1.0f #if WLR_HAS_DRM_BACKEND #include @@ -615,7 +620,8 @@ static int output_repaint_timer_handler(void *data) { if (fullscreen_con && fullscreen_con->view && !debug.noscanout // Only output to monitor without compositing when saturation is changed - && fullscreen_con->saturation == 1.0f) { + && fullscreen_con->saturation == 1.0f && + output->workspace_scroll.percent == 0.0f) { // Try to scan-out the fullscreen view static bool last_scanned_out = false; bool scanned_out = @@ -1114,6 +1120,8 @@ void handle_new_output(struct wl_listener *listener, void *data) { wlr_output_transformed_resolution(output->wlr_output, &width, &height); wlr_damage_ring_set_bounds(&output->damage_ring, width, height); update_output_manager_config(server); + + output->workspace_scroll = workspace_scroll_get_default(); } void handle_output_layout_change(struct wl_listener *listener, @@ -1241,3 +1249,166 @@ void handle_output_power_manager_set_mode(struct wl_listener *listener, oc = store_output_config(oc); apply_output_config(oc, output); } + +struct workspace_scroll workspace_scroll_get_default() { + return (struct workspace_scroll) { + .percent = 0, + .avg_velocity = 0, + .num_updates = 0, + .direction = SWIPE_GESTURE_DIRECTION_NONE, + }; +} + +bool workspace_scroll_equal(struct workspace_scroll *a, struct workspace_scroll *b) { + return a->avg_velocity == b->avg_velocity && + a->direction == b->direction && + a->num_updates == b->num_updates && + a->percent == b->percent; +} + +void workspace_scroll_begin(struct sway_seat *seat, + enum swipe_gesture_direction direction) { + struct sway_workspace *focused_ws = seat_get_focused_workspace(seat); + struct sway_output *output = focused_ws->output; + + // Reset the state + output->workspace_scroll = workspace_scroll_get_default(); + output->workspace_scroll.direction = direction; + + output_damage_whole(output); + transaction_commit_dirty(); + + // Unset focus + seat_set_focus_workspace(seat, NULL); +} + +void workspace_scroll_update(struct sway_seat *seat, struct gesture_tracker *tracker, + struct wlr_pointer_swipe_update_event *event, int invert) { + double delta_sum; + enum swipe_gesture_direction direction; + switch (tracker->type) { + case GESTURE_TYPE_WORKSPACE_SWIPE_HORIZONTAL: + direction = SWIPE_GESTURE_DIRECTION_HORIZONTAL; + delta_sum = tracker->dx + event->dx * invert; + break; + case GESTURE_TYPE_WORKSPACE_SWIPE_VERTICAL: + direction = SWIPE_GESTURE_DIRECTION_VERTICAL; + delta_sum = tracker->dy + event->dy * invert; + break; + default: + return; + } + + struct sway_workspace *focused_ws = seat_get_focused_workspace(seat); + struct sway_output *output = focused_ws->output; + struct workspace_scroll *ws_scroll = &output->workspace_scroll; + + if (direction != ws_scroll->direction) { + return; + } + + int visible_index = list_find(output->workspaces, focused_ws); + if (visible_index == -1) { + return; + } + + // Get the updated average velocity + ws_scroll->avg_velocity = fabs(delta_sum) / (++ws_scroll->num_updates); + // TODO: Make configurable + const int SPEED_FACTOR = 750; + double percent = delta_sum / SPEED_FACTOR; + + double min = PREV_WS_LIMIT, max = NEXT_WS_LIMIT; + if (!config->workspace_gesture_wrap_around) { + // Visualized to the user that this is the last / first workspace by + // allowing a small eased swipe, a "Spring effect" + double spring_limit = (double) config->workspace_gesture_spring_size / + output->width * output->wlr_output->scale; + // Make sure that the limit is always smaller than the threshold to + // avoid accidental workspace switches + double small_threshold = MAX(config->workspace_gesture_threshold - 0.1, 0); + spring_limit = MIN(small_threshold, spring_limit); + // Limit the percent depending on if the workspace is the first/last or in + // the middle somewhere. Uses ease_out to make the limit feel more natural. + if (visible_index + 1 >= output->workspaces->length) { + max = spring_limit; + if (percent > 0) { + percent = lerp(0, max, ease_out_cubic(fabs(percent))); + min = 0; + } + } + if (visible_index == 0) { + min = -spring_limit; + if (percent < 0) { + percent = lerp(0, min, ease_out_cubic(fabs(percent))); + max = 0; + } + } + } + + // Update the tracker data if we aren't exceeding the max swipe limit + if (percent < max && percent > min) { + tracker->dx += event->dx * invert; + tracker->dy += event->dy * invert; + } + + ws_scroll->percent = CLAMP(percent, min, max); + ws_scroll->direction = direction; + + output_damage_whole(output); + transaction_commit_dirty(); +} + +void workspace_scroll_end(struct sway_seat *seat) { + struct sway_workspace *focused_ws = seat_get_focused_workspace(seat); + struct sway_output *output = focused_ws->output; + struct workspace_scroll *ws_scroll = &output->workspace_scroll; + + int visible_index = list_find(output->workspaces, focused_ws); + + bool not_edge_ws = config->workspace_gesture_wrap_around; + int dir; + if (ws_scroll->percent < 0) { + dir = PREV_WS_LIMIT; + not_edge_ws |= visible_index > 0; + } else if (ws_scroll->percent > 0) { + dir = NEXT_WS_LIMIT; + not_edge_ws |= visible_index + 1 < output->workspaces->length; + } else { + // Skip setting workspace if the percentage is zero + goto reset_state; + } + + // TODO: Make configurable + const int VELOCITY_NEEDED = 8; + // Only switch workspaces when the percent exceeds the threshold or if + // the avg_speed exceeds the limit (for fast but short swipes). + bool threshold_met = fabs(ws_scroll->percent) >= config->workspace_gesture_threshold; + bool enough_velocity = ws_scroll->avg_velocity >= VELOCITY_NEEDED && not_edge_ws; + if (!threshold_met && !enough_velocity) { + goto reset_state; + } + + size_t ws_index = wrap(visible_index + dir, output->workspaces->length); + focused_ws = output->workspaces->items[ws_index]; + sway_log(SWAY_DEBUG, "Switched to workspace: %s\n", focused_ws->name); + +reset_state: + workspace_scroll_reset(seat, focused_ws); +} + +void workspace_scroll_reset(struct sway_seat *seat, struct sway_workspace *ws) { + if (!ws) { + ws = seat_get_focused_workspace(seat); + } + struct sway_output *output = ws->output; + + workspace_switch(ws); + seat_consider_warp_to_focus(seat); + + // Reset the state + output->workspace_scroll = workspace_scroll_get_default(); + + output_damage_whole(output); + transaction_commit_dirty(); +} diff --git a/sway/desktop/render.c b/sway/desktop/render.c index e469d716..a3e532df 100644 --- a/sway/desktop/render.c +++ b/sway/desktop/render.c @@ -92,6 +92,83 @@ struct decoration_data get_undecorated_decoration_data() { }; } +// Adjust the box position when switching the workspace +static void adjust_box_to_workspace_offset(struct wlr_box *box, + bool on_focused_workspace, struct sway_output *output) { + float scroll_percent = output->workspace_scroll.percent; + + int ws_dimen; + int *box_coord = NULL; + switch (output->workspace_scroll.direction) { + case SWIPE_GESTURE_DIRECTION_NONE: + return; + case SWIPE_GESTURE_DIRECTION_HORIZONTAL: + ws_dimen = output->width; + box_coord = &box->x; + break; + case SWIPE_GESTURE_DIRECTION_VERTICAL: + ws_dimen = output->height; + box_coord = &box->y; + break; + } + + if (box_coord == NULL || ws_dimen == 0) { + return; + } + + *box_coord -= ws_dimen * scroll_percent; + if (!on_focused_workspace) { + if (scroll_percent > 0) { + *box_coord += ws_dimen; + } else if (scroll_percent < 0) { + *box_coord -= ws_dimen; + } + } +} + +// Clips the rendered damage to the workspace region. +// Fixes containers being rendered across workspaces while switching. +static void adjust_damage_to_workspace_bounds(pixman_region32_t *damage, + bool on_focused_workspace, struct sway_output *output) { + float scale = output->wlr_output->scale; + float scroll_percent = output->workspace_scroll.percent; + int x = 0, y = 0; + + int ws_dimen = 0; + int *coord = NULL; + switch (output->workspace_scroll.direction) { + case SWIPE_GESTURE_DIRECTION_NONE: + return; + case SWIPE_GESTURE_DIRECTION_HORIZONTAL: + ws_dimen = output->width; + coord = &x; + break; + case SWIPE_GESTURE_DIRECTION_VERTICAL: + ws_dimen = output->height; + coord = &y; + break; + } + + if (coord == NULL || ws_dimen == 0) { + return; + } + + *coord = round(-ws_dimen * scroll_percent); + if (!on_focused_workspace) { + if (scroll_percent > 0) { + *coord += ws_dimen; + } else if (scroll_percent < 0) { + *coord -= ws_dimen; + } + } + + int width, height; + wlr_output_transformed_resolution(output->wlr_output, &width, &height); + pixman_region32_intersect_rect(damage, damage, + 0, 0, width, height); + pixman_region32_translate(damage, x * scale, y * scale); +} + /** * Apply scale to a width or height. * @@ -350,6 +427,12 @@ static void render_surface_iterator(struct sway_output *output, clip_box.x = fmax(dst_box.x, data->clip_box->x); clip_box.y = fmax(dst_box.y, data->clip_box->y); } + + if (data->view) { + adjust_box_to_workspace_offset(&dst_box, data->on_focused_workspace, output); + adjust_box_to_workspace_offset(&clip_box, data->on_focused_workspace, output); + } + scale_box(&dst_box, wlr_output->scale); scale_box(&clip_box, wlr_output->scale); @@ -420,6 +503,7 @@ static void render_layer_iterator(struct sway_output *output, static void render_layer_toplevel(struct fx_render_context *ctx, struct wl_list *layer_surfaces) { struct render_data data = { .deco_data = get_undecorated_decoration_data(), + .on_focused_workspace = true, .ctx = ctx, }; output_layer_for_each_toplevel_surface(ctx->output, layer_surfaces, @@ -429,6 +513,7 @@ static void render_layer_toplevel(struct fx_render_context *ctx, struct wl_list static void render_layer_popups(struct fx_render_context *ctx, struct wl_list *layer_surfaces) { struct render_data data = { .deco_data = get_undecorated_decoration_data(), + .on_focused_workspace = true, .ctx = ctx, }; output_layer_for_each_popup_surface(ctx->output, layer_surfaces, @@ -439,6 +524,7 @@ static void render_layer_toplevel(struct fx_render_context *ctx, struct wl_list static void render_unmanaged(struct fx_render_context *ctx, struct wl_list *unmanaged) { struct render_data data = { .deco_data = get_undecorated_decoration_data(), + .on_focused_workspace = true, .ctx = ctx, }; output_unmanaged_for_each_surface(ctx->output, unmanaged, @@ -449,6 +535,7 @@ static void render_unmanaged(struct fx_render_context *ctx, struct wl_list *unma static void render_drag_icons(struct fx_render_context *ctx, struct wl_list *drag_icons) { struct render_data data = { .deco_data = get_undecorated_decoration_data(), + .on_focused_workspace = true, .ctx = ctx, }; output_drag_icons_for_each_surface(ctx->output, drag_icons, @@ -547,9 +634,10 @@ void premultiply_alpha(float color[4], float opacity) { } static void render_view_toplevels(struct fx_render_context *ctx, - struct sway_view *view, struct decoration_data deco_data) { + struct sway_view *view, struct decoration_data deco_data, bool on_focused_workspace) { struct render_data data = { .deco_data = deco_data, + .on_focused_workspace = on_focused_workspace, .view = view, .ctx = ctx, }; @@ -597,6 +685,7 @@ static void render_view_popups(struct fx_render_context *ctx, struct sway_view * struct decoration_data deco_data) { struct render_data data = { .deco_data = deco_data, + .on_focused_workspace = true, .view = view, .ctx = ctx, }; @@ -699,13 +788,14 @@ static void render_saved_view(struct fx_render_context *ctx, struct sway_view *v * Render a view's surface, shadow, and left/bottom/right borders. */ static void render_view(struct fx_render_context *ctx, struct sway_container *con, - struct border_colors *colors, struct decoration_data deco_data) { + struct border_colors *colors, struct decoration_data deco_data, + bool on_focused_workspace) { struct sway_view *view = con->view; if (!wl_list_empty(&view->saved_buffers)) { render_saved_view(ctx, view, deco_data); } else if (view->surface) { - render_view_toplevels(ctx, view, deco_data); + render_view_toplevels(ctx, view, deco_data, on_focused_workspace); } struct sway_container_state *state = &con->current; @@ -724,6 +814,7 @@ static void render_view(struct fx_render_context *ctx, struct sway_container *co box.y = floor(state->y) - ctx->output->ly; box.width = state->width; box.height = state->height; + adjust_box_to_workspace_offset(&box, on_focused_workspace, ctx->output); scale_box(&box, output_scale); int shadow_corner_radius = corner_radius == 0 ? 0 : corner_radius + state->border_thickness; float* shadow_color = view_is_urgent(view) || state->focused ? @@ -747,6 +838,7 @@ static void render_view(struct fx_render_context *ctx, struct sway_container *co box.y = floor(state->content_y); box.width = state->border_thickness; box.height = state->content_height - corner_radius; + adjust_box_to_workspace_offset(&box, on_focused_workspace, ctx->output); if (corner_radius && !deco_data.has_titlebar) { box.y += corner_radius; box.height -= corner_radius; @@ -770,6 +862,7 @@ static void render_view(struct fx_render_context *ctx, struct sway_container *co box.y = floor(state->content_y); box.width = state->border_thickness; box.height = state->content_height - corner_radius; + adjust_box_to_workspace_offset(&box, on_focused_workspace, ctx->output); if (corner_radius && !deco_data.has_titlebar) { box.y += corner_radius; box.height -= corner_radius; @@ -789,6 +882,8 @@ static void render_view(struct fx_render_context *ctx, struct sway_container *co box.y = floor(state->content_y + state->content_height); box.width = state->width; box.height = state->border_thickness; + adjust_box_to_workspace_offset(&box, on_focused_workspace, ctx->output); + // adjust sizing for rounded border corners if (deco_data.corner_radius) { box.x += corner_radius + state->border_thickness; @@ -804,6 +899,7 @@ static void render_view(struct fx_render_context *ctx, struct sway_container *co if (state->border_left) { box.x = floor(state->x); box.y = floor(state->y + state->height - size); + adjust_box_to_workspace_offset(&box, on_focused_workspace, ctx->output); box.width = size; box.height = size; scale_box(&box, output_scale); @@ -813,8 +909,7 @@ static void render_view(struct fx_render_context *ctx, struct sway_container *co if (state->border_right) { box.x = floor(state->x + state->width - size); box.y = floor(state->y + state->height - size); - box.width = size; - box.height = size; + adjust_box_to_workspace_offset(&box, on_focused_workspace, ctx->output); scale_box(&box, output_scale); render_rounded_border_corner(ctx, &box, color, scaled_corner_radius, scaled_border_thickness, BOTTOM_RIGHT); @@ -835,7 +930,7 @@ static void render_view(struct fx_render_context *ctx, struct sway_container *co static void render_titlebar(struct fx_render_context *ctx, struct sway_container *con, int x, int y, int width, struct border_colors *colors, int corner_radius, enum corner_location corner_location, struct wlr_texture *title_texture, - struct wlr_texture *marks_texture) { + struct wlr_texture *marks_texture, bool on_focused_workspace) { struct wlr_box box; float color[4]; struct sway_output *output = ctx->output; @@ -867,6 +962,7 @@ static void render_titlebar(struct fx_render_context *ctx, struct sway_container box.width -= corner_radius; } } + adjust_box_to_workspace_offset(&box, on_focused_workspace, output); scale_box(&box, output_scale); render_rect(ctx, &box, color); @@ -876,6 +972,7 @@ static void render_titlebar(struct fx_render_context *ctx, struct sway_container box.y = y + container_titlebar_height() - titlebar_border_thickness; box.width = width; box.height = titlebar_border_thickness; + adjust_box_to_workspace_offset(&box, on_focused_workspace, output); scale_box(&box, output_scale); render_rect(ctx, &box, color); } @@ -889,6 +986,7 @@ static void render_titlebar(struct fx_render_context *ctx, struct sway_container box.height -= corner_radius; box.y += corner_radius; } + adjust_box_to_workspace_offset(&box, on_focused_workspace, output); scale_box(&box, output_scale); render_rect(ctx, &box, color); @@ -901,6 +999,7 @@ static void render_titlebar(struct fx_render_context *ctx, struct sway_container box.height -= corner_radius; box.y += corner_radius; } + adjust_box_to_workspace_offset(&box, on_focused_workspace, output); scale_box(&box, output_scale); render_rect(ctx, &box, color); @@ -912,6 +1011,7 @@ static void render_titlebar(struct fx_render_context *ctx, struct sway_container box.y = y; box.width = corner_radius * 2; box.height = corner_radius * 2; + adjust_box_to_workspace_offset(&box, on_focused_workspace, output); scale_box(&box, output_scale); render_rounded_border_corner(ctx, &box, color, corner_radius, titlebar_border_thickness * output_scale, TOP_LEFT); @@ -923,6 +1023,7 @@ static void render_titlebar(struct fx_render_context *ctx, struct sway_container box.y = y; box.width = corner_radius * 2; box.height = corner_radius * 2; + adjust_box_to_workspace_offset(&box, on_focused_workspace, output); scale_box(&box, output_scale); render_rounded_border_corner(ctx, &box, color, corner_radius, titlebar_border_thickness * output_scale, TOP_RIGHT); @@ -976,6 +1077,7 @@ static void render_titlebar(struct fx_render_context *ctx, struct sway_container if (ob_inner_width < clip_box.width) { clip_box.width = ob_inner_width; } + adjust_box_to_workspace_offset(&texture_box, on_focused_workspace, output); render_texture(ctx, marks_texture, NULL, &texture_box, &clip_box, WL_OUTPUT_TRANSFORM_NORMAL, deco_data); @@ -986,11 +1088,13 @@ static void render_titlebar(struct fx_render_context *ctx, struct sway_container box.y = roundf((y + titlebar_border_thickness) * output_scale); box.width = clip_box.width; box.height = ob_padding_above; + adjust_box_to_workspace_offset(&box, on_focused_workspace, output); render_rect(ctx, &box, color); // Padding below box.y += ob_padding_above + clip_box.height; box.height = ob_padding_below + bottom_border_compensation; + adjust_box_to_workspace_offset(&box, on_focused_workspace, output); render_rect(ctx, &box, color); } @@ -1048,6 +1152,8 @@ static void render_titlebar(struct fx_render_context *ctx, struct sway_container clip_box.width = ob_inner_width - ob_marks_width; } + adjust_box_to_workspace_offset(&texture_box, on_focused_workspace, output); + adjust_box_to_workspace_offset(&clip_box, on_focused_workspace, output); render_texture(ctx, title_texture, NULL, &texture_box, &clip_box, WL_OUTPUT_TRANSFORM_NORMAL, deco_data); @@ -1058,6 +1164,7 @@ static void render_titlebar(struct fx_render_context *ctx, struct sway_container box.y = roundf((y + titlebar_border_thickness) * output_scale); box.width = clip_box.width; box.height = ob_padding_above; + // adjust_box_to_workspace_offset(&box, on_focused_workspace, output); render_rect(ctx, &box, color); // Padding below @@ -1097,6 +1204,7 @@ static void render_titlebar(struct fx_render_context *ctx, struct sway_container box.x = ob_left_x + ob_left_width + round(output_x * output_scale); box.y = roundf(bg_y * output_scale); box.height = ob_bg_height + bottom_border_compensation; + adjust_box_to_workspace_offset(&box, on_focused_workspace, output); render_rect(ctx, &box, color); } @@ -1111,6 +1219,7 @@ static void render_titlebar(struct fx_render_context *ctx, struct sway_container if (box.x + box.width < left_x) { box.width += left_x - box.x - box.width; } + adjust_box_to_workspace_offset(&box, on_focused_workspace, output); if (corner_radius && (corner_location == TOP_LEFT || corner_location == ALL)) { render_rounded_rect(ctx, &box, color, corner_radius, TOP_LEFT); } else { @@ -1129,6 +1238,7 @@ static void render_titlebar(struct fx_render_context *ctx, struct sway_container box.width += box.x - right_rx; box.x = right_rx; } + adjust_box_to_workspace_offset(&box, on_focused_workspace, output); if (corner_radius && (corner_location == TOP_RIGHT || corner_location == ALL)) { render_rounded_rect(ctx, &box, color, corner_radius, TOP_RIGHT); } else { @@ -1140,7 +1250,7 @@ static void render_titlebar(struct fx_render_context *ctx, struct sway_container * Render the top border line for a view using "border pixel". */ static void render_top_border(struct fx_render_context *ctx, struct sway_container *con, - struct border_colors *colors, int corner_radius) { + struct border_colors *colors, int corner_radius, bool on_focused_workspace) { struct sway_container_state *state = &con->current; if (!state->border_top) { return; @@ -1156,6 +1266,7 @@ static void render_top_border(struct fx_render_context *ctx, struct sway_contain box.y = floor(state->y); box.width = state->width - (2 * corner_radius); box.height = state->border_thickness; + adjust_box_to_workspace_offset(&box, on_focused_workspace, ctx->output); scale_box(&box, output_scale); render_rect(ctx, &box, color); @@ -1166,6 +1277,7 @@ static void render_top_border(struct fx_render_context *ctx, struct sway_contain if (state->border_left) { box.x = floor(state->x); box.y = floor(state->y); + adjust_box_to_workspace_offset(&box, on_focused_workspace, ctx->output); box.width = size; box.height = size; scale_box(&box, output_scale); @@ -1175,6 +1287,7 @@ static void render_top_border(struct fx_render_context *ctx, struct sway_contain if (state->border_right) { box.x = floor(state->x + state->width - size); box.y = floor(state->y); + adjust_box_to_workspace_offset(&box, on_focused_workspace, ctx->output); box.width = size; box.height = size; scale_box(&box, output_scale); @@ -1190,10 +1303,13 @@ struct parent_data { list_t *children; bool focused; struct sway_container *active_child; + + // Indicates whether the target is on the focused workspace or not. + bool on_focused_workspace; }; static void render_container(struct fx_render_context *ctx, - struct sway_container *con, bool parent_focused); + struct sway_container *con, bool parent_focused, bool is_current_ws); // TODO: no rounding top corners when rendering with titlebar /** @@ -1253,17 +1369,18 @@ static void render_containers_linear(struct fx_render_context *ctx, struct paren .discard_transparent = false, .shadow = child->shadow_enabled, }; - render_view(ctx, child, colors, deco_data); + render_view(ctx, child, colors, deco_data, parent->on_focused_workspace); if (has_titlebar) { render_titlebar(ctx, child, floor(state->x), floor(state->y), state->width, colors, deco_data.corner_radius, - ALL, title_texture, marks_texture); + ALL, title_texture, marks_texture, parent->on_focused_workspace); } else if (state->border == B_PIXEL) { - render_top_border(ctx, child, colors, deco_data.corner_radius); + render_top_border(ctx, child, colors, deco_data.corner_radius, parent->on_focused_workspace); } } else { render_container(ctx, child, - parent->focused || child->current.focused); + parent->focused || child->current.focused, + parent->on_focused_workspace); } } } @@ -1358,7 +1475,8 @@ static void render_containers_tabbed(struct fx_render_context *ctx, struct paren } render_titlebar(ctx, child, x, parent->box.y, tab_width, colors, - corner_radius, corner_location, title_texture, marks_texture); + corner_radius, corner_location, title_texture, marks_texture, + parent->on_focused_workspace); if (child == current) { current_colors = colors; @@ -1367,10 +1485,12 @@ static void render_containers_tabbed(struct fx_render_context *ctx, struct paren // Render surface and left/right/bottom borders if (current->view) { - render_view(ctx, current, current_colors, deco_data); + render_view(ctx, current, current_colors, deco_data, + parent->on_focused_workspace); } else { render_container(ctx, current, - parent->focused || current->current.focused); + parent->focused || current->current.focused, + parent->on_focused_workspace); } } @@ -1437,7 +1557,7 @@ static void render_containers_stacked(struct fx_render_context *ctx, struct pare int y = parent->box.y + titlebar_height * i; int corner_radius = i != 0 ? 0 : deco_data.corner_radius; render_titlebar(ctx, child, parent->box.x, y, parent->box.width, colors, - corner_radius, ALL, title_texture, marks_texture); + corner_radius, ALL, title_texture, marks_texture, parent->on_focused_workspace); if (child == current) { current_colors = colors; @@ -1446,10 +1566,12 @@ static void render_containers_stacked(struct fx_render_context *ctx, struct pare // Render surface and left/right/bottom borders if (current->view) { - render_view(ctx, current, current_colors, deco_data); + render_view(ctx, current, current_colors, deco_data, + parent->on_focused_workspace); } else { render_container(ctx, current, - parent->focused || current->current.focused); + parent->focused || current->current.focused, + parent->on_focused_workspace); } } @@ -1478,7 +1600,7 @@ static void render_containers(struct fx_render_context *ctx, struct parent_data } static void render_container(struct fx_render_context *ctx, - struct sway_container *con, bool focused) { + struct sway_container *con, bool focused, bool is_focused_ws) { struct parent_data data = { .layout = con->current.layout, .box = { @@ -1490,12 +1612,13 @@ static void render_container(struct fx_render_context *ctx, .children = con->current.children, .focused = focused, .active_child = con->current.focused_inactive_child, + .on_focused_workspace = is_focused_ws, }; render_containers(ctx, &data); } static void render_workspace(struct fx_render_context *ctx, - struct sway_workspace *ws, bool focused) { + struct sway_workspace *ws, bool focused, bool on_focused_workspace) { struct parent_data data = { .layout = ws->current.layout, .box = { @@ -1507,12 +1630,13 @@ static void render_workspace(struct fx_render_context *ctx, .children = ws->current.tiling, .focused = focused, .active_child = ws->current.focused_inactive_child, + .on_focused_workspace = on_focused_workspace, }; render_containers(ctx, &data); } static void render_floating_container(struct fx_render_context *ctx, - struct sway_container *con) { + struct sway_container *con, bool is_focused_ws) { struct sway_container_state *state = &con->current; if (con->view) { struct sway_view *view = con->view; @@ -1548,32 +1672,44 @@ static void render_floating_container(struct fx_render_context *ctx, .discard_transparent = false, .shadow = con->shadow_enabled, }; - render_view(ctx, con, colors, deco_data); + render_view(ctx, con, colors, deco_data, is_focused_ws); if (has_titlebar) { render_titlebar(ctx, con, floor(con->current.x), floor(con->current.y), con->current.width, - colors, deco_data.corner_radius, ALL, title_texture, marks_texture); + colors, deco_data.corner_radius, ALL, title_texture, marks_texture, is_focused_ws); } else if (state->border == B_PIXEL) { - render_top_border(ctx, con, colors, deco_data.corner_radius); + render_top_border(ctx, con, colors, deco_data.corner_radius, is_focused_ws); } } else { - render_container(ctx, con, state->focused); + render_container(ctx, con, state->focused, is_focused_ws); } } -static void render_floating(struct fx_render_context *ctx) { +static void render_floating(struct fx_render_context *ctx, + struct sway_workspace *other_ws, bool has_fullscreen) { for (int i = 0; i < root->outputs->length; ++i) { struct sway_output *output = root->outputs->items[i]; + + // Don't render floating windows across outputs when switching workspaces + if (output->workspace_scroll.percent != 0 && output != ctx->output) { + continue; + } + + struct sway_workspace *visible_ws = output->current.active_workspace; for (int j = 0; j < output->current.workspaces->length; ++j) { struct sway_workspace *ws = output->current.workspaces->items[j]; - if (!workspace_is_visible(ws)) { + + // Only render affected workspaces + if ((ws != other_ws && ws != visible_ws) || + (workspace_is_visible(ws) && has_fullscreen)) { continue; } + for (int k = 0; k < ws->current.floating->length; ++k) { struct sway_container *floater = ws->current.floating->items[k]; if (floater->current.fullscreen_mode != FULLSCREEN_NONE) { continue; } - render_floating_container(ctx, floater); + render_floating_container(ctx, floater, ws == visible_ws); } } } @@ -1586,6 +1722,57 @@ static void render_seatops(struct fx_render_context *ctx) { } } +static void render_fullscreen_con(struct fx_render_context *ctx, + pixman_region32_t *transformed_damage, struct sway_container *fullscreen_con, + struct sway_workspace *workspace, bool clear_whole_screen) { + struct wlr_output *wlr_output = ctx->output->wlr_output; + + bool on_focused_workspace = workspace == ctx->output->current.active_workspace; + + // Only clear the transformed fullscreen bounds + pixman_region32_t dmg; + pixman_region32_init(&dmg); + if (!clear_whole_screen) { + pixman_region32_copy(&dmg, transformed_damage); + adjust_damage_to_workspace_bounds(&dmg, on_focused_workspace, ctx->output); + transformed_damage = &dmg; + } + + + fx_render_pass_add_rect(ctx->pass, &(struct fx_render_rect_options){ + .base = { + .box = { .width = wlr_output->width, .height = wlr_output->height }, + .color = { .r = 0, .g = 0, .b = 0, .a = 1 }, + .clip = transformed_damage, + }, + }); + + if (fullscreen_con->view) { + struct decoration_data deco_data = get_undecorated_decoration_data(); + deco_data.saturation = fullscreen_con->saturation; + if (!wl_list_empty(&fullscreen_con->view->saved_buffers)) { + render_saved_view(ctx, fullscreen_con->view, deco_data); + } else if (fullscreen_con->view->surface) { + render_view_toplevels(ctx, fullscreen_con->view, deco_data, on_focused_workspace); + } + } else { + render_container(ctx, fullscreen_con, fullscreen_con->current.focused, on_focused_workspace); + } + + for (int i = 0; i < workspace->current.floating->length; ++i) { + struct sway_container *floater = + workspace->current.floating->items[i]; + if (container_is_transient_for(floater, fullscreen_con)) { + render_floating_container(ctx, floater, on_focused_workspace); + } + } +#if HAVE_XWAYLAND + render_unmanaged(ctx, &root->xwayland_unmanaged); +#endif + + pixman_region32_fini(&dmg); +} + void output_render(struct fx_render_context *ctx) { struct wlr_output *wlr_output = ctx->output->wlr_output; struct sway_output *output = ctx->output; @@ -1598,10 +1785,29 @@ void output_render(struct fx_render_context *ctx) { return; } + // Get the sibling workspaces + struct sway_workspace *other_ws = NULL; + if (output->workspace_scroll.percent < 0) { + other_ws = workspace_output_prev(workspace, + config->workspace_gesture_wrap_around); + } else if (output->workspace_scroll.percent > 0) { + other_ws = workspace_output_next(workspace, + config->workspace_gesture_wrap_around); + } + struct sway_container *fullscreen_con = root->fullscreen_global; if (!fullscreen_con) { fullscreen_con = workspace->current.fullscreen; } + // Also check if the sibling workspace has a fullscreen container + bool has_fullscreen = fullscreen_con != NULL; + bool other_ws_has_fullscreen = false; + if (other_ws) { + other_ws_has_fullscreen = other_ws->current.fullscreen; + if (!has_fullscreen) { + has_fullscreen = other_ws_has_fullscreen; + } + } if (!pixman_region32_not_empty(damage)) { // Output isn't damaged but needs buffer swap @@ -1642,6 +1848,7 @@ void output_render(struct fx_render_context *ctx) { if (server.session_lock.lock != NULL) { struct render_data data = { .deco_data = get_undecorated_decoration_data(), + .on_focused_workspace = true, .ctx = ctx, }; @@ -1665,37 +1872,9 @@ void output_render(struct fx_render_context *ctx) { goto render_overlay; } - if (fullscreen_con) { - fx_render_pass_add_rect(ctx->pass, &(struct fx_render_rect_options){ - .base = { - .box = { .width = wlr_output->width, .height = wlr_output->height }, - .color = { .r = 0, .g = 0, .b = 0, .a = 1 }, - .clip = &transformed_damage, - }, - }); - - if (fullscreen_con->view) { - struct decoration_data deco_data = get_undecorated_decoration_data(); - deco_data.saturation = fullscreen_con->saturation; - if (!wl_list_empty(&fullscreen_con->view->saved_buffers)) { - render_saved_view(ctx, fullscreen_con->view, deco_data); - } else if (fullscreen_con->view->surface) { - render_view_toplevels(ctx, fullscreen_con->view, deco_data); - } - } else { - render_container(ctx, fullscreen_con, fullscreen_con->current.focused); - } - - for (int i = 0; i < workspace->current.floating->length; ++i) { - struct sway_container *floater = - workspace->current.floating->items[i]; - if (container_is_transient_for(floater, fullscreen_con)) { - render_floating_container(ctx, floater); - } - } -#if HAVE_XWAYLAND - render_unmanaged(ctx, &root->xwayland_unmanaged); -#endif + if (fullscreen_con && output->workspace_scroll.percent == 0) { + // Only draw fullscreen con if not transitioning between workspaces + render_fullscreen_con(ctx, &transformed_damage, fullscreen_con, workspace, true); } else { int output_width, output_height; wlr_output_transformed_resolution(wlr_output, &output_width, &output_height); @@ -1778,8 +1957,16 @@ void output_render(struct fx_render_context *ctx) { fx_render_pass_add_optimized_blur(ctx->pass, &blur_options); } - render_workspace(ctx, workspace, workspace->current.focused); - render_floating(ctx); + // Render both workspaces + if (!other_ws_has_fullscreen && other_ws) { + render_workspace(ctx, other_ws, false, false); + } + if (!fullscreen_con) { + render_workspace(ctx, workspace, workspace->current.focused, true); + } + render_floating(ctx, + !other_ws_has_fullscreen ? other_ws : NULL, + fullscreen_con != NULL); #if HAVE_XWAYLAND render_unmanaged(ctx, &root->xwayland_unmanaged); #endif @@ -1792,6 +1979,43 @@ void output_render(struct fx_render_context *ctx) { &output->layers[ZWLR_LAYER_SHELL_V1_LAYER_BOTTOM]); render_layer_popups(ctx, &output->layers[ZWLR_LAYER_SHELL_V1_LAYER_TOP]); + + // Render the fullscreen containers on top + if (has_fullscreen) { + float clear_color[] = {0.25f, 0.25f, 0.25f, 1.0f}; + // Render a blank rect next to the fullscreen container if + // there's no sibling workspace in the swipes direction + if (!other_ws) { + struct wlr_box mon_box = { 0, 0, output->width, output->height }; + adjust_box_to_workspace_offset(&mon_box, false, output); + scale_box(&mon_box, output->wlr_output->scale); + render_rect(ctx, &mon_box, clear_color); + + // Render a shadow to separate the edge and the fullscreen + // container + if (config_should_parameters_shadow()) { + struct wlr_box shadow_box = { 0, 0, output->width, output->height }; + adjust_box_to_workspace_offset(&shadow_box, true, output); + scale_box(&shadow_box, output->wlr_output->scale); + // Render rect to fix minor pixel gaps between fullscreen + // container and shadow + render_rect(ctx, &shadow_box, clear_color); + render_box_shadow(ctx, &shadow_box, + config->shadow_color, config->shadow_blur_sigma, 0, + 0, 0); + } + } else { + // Render sibling fullscreen container + struct sway_container *f_con = other_ws->current.fullscreen; + if (other_ws->current.fullscreen) { + render_fullscreen_con(ctx, &transformed_damage, f_con, other_ws, false); + } + } + // Render focused fullscreen container + if (fullscreen_con) { + render_fullscreen_con(ctx, &transformed_damage, fullscreen_con, workspace, false); + } + } } render_seatops(ctx); diff --git a/sway/fx_util/animation_utils.c b/sway/fx_util/animation_utils.c new file mode 100644 index 00000000..75ec40ad --- /dev/null +++ b/sway/fx_util/animation_utils.c @@ -0,0 +1,12 @@ +#include + +#include "sway/fx_util/animation_utils.h" + +double lerp (double a, double b, double t) { + return a * (1.0 - t) + b * t; +} + +double ease_out_cubic (double t) { + double p = t - 1; + return pow(p, 3) + 1; +} diff --git a/sway/input/seatop_default.c b/sway/input/seatop_default.c index 1dce6dae..bd98d60d 100644 --- a/sway/input/seatop_default.c +++ b/sway/input/seatop_default.c @@ -1053,6 +1053,14 @@ static void handle_swipe_begin(struct sway_seat *seat, if (gesture_binding_check(bindings, GESTURE_TYPE_SWIPE, event->fingers, device)) { struct seatop_default_event *seatop = seat->seatop_data; gesture_tracker_begin(&seatop->gestures, GESTURE_TYPE_SWIPE, event->fingers); + } else if (gesture_binding_check(bindings, GESTURE_TYPE_WORKSPACE_SWIPE_HORIZONTAL, event->fingers, device)) { + struct seatop_default_event *seatop = seat->seatop_data; + gesture_tracker_begin(&seatop->gestures, GESTURE_TYPE_WORKSPACE_SWIPE_HORIZONTAL, event->fingers); + workspace_scroll_begin(seat, SWIPE_GESTURE_DIRECTION_HORIZONTAL); + } else if (gesture_binding_check(bindings, GESTURE_TYPE_WORKSPACE_SWIPE_VERTICAL, event->fingers, device)) { + struct seatop_default_event *seatop = seat->seatop_data; + gesture_tracker_begin(&seatop->gestures, GESTURE_TYPE_WORKSPACE_SWIPE_VERTICAL, event->fingers); + workspace_scroll_begin(seat, SWIPE_GESTURE_DIRECTION_VERTICAL); } else { // ... otherwise forward to client struct sway_cursor *cursor = seat->cursor; @@ -1070,6 +1078,29 @@ static void handle_swipe_update(struct sway_seat *seat, if (gesture_tracker_check(&seatop->gestures, GESTURE_TYPE_SWIPE)) { gesture_tracker_update(&seatop->gestures, event->dx, event->dy, NAN, NAN); + } else if (gesture_tracker_check(&seatop->gestures, GESTURE_TYPE_WORKSPACE_SWIPE_HORIZONTAL) || + gesture_tracker_check(&seatop->gestures, GESTURE_TYPE_WORKSPACE_SWIPE_VERTICAL)) { + // Find the gesture and update the swipe percentage + struct gesture_tracker *tracker = &seatop->gestures; + struct sway_input_device *device = + event->pointer ? event->pointer->base.data : NULL; + // Determine name of input that received gesture + char *input = device + ? input_device_get_identifier(device->wlr_device) + : strdup("*"); + + struct gesture gesture = { + .fingers = tracker->fingers, + .type = tracker->type, + .directions = GESTURE_DIRECTION_NONE, + }; + struct sway_gesture_binding *binding = gesture_binding_match( + config->current_mode->gesture_bindings, &gesture, input); + + if (binding) { + int invert = binding->flags & BINDING_INVERTED ? 1 : -1; + workspace_scroll_update(seat, tracker, event, invert); + } } else { // ... otherwise forward to client struct sway_cursor *cursor = seat->cursor; @@ -1083,13 +1114,17 @@ static void handle_swipe_end(struct sway_seat *seat, struct wlr_pointer_swipe_end_event *event) { // Ensure gesture is being tracked and was not cancelled struct seatop_default_event *seatop = seat->seatop_data; - if (!gesture_tracker_check(&seatop->gestures, GESTURE_TYPE_SWIPE)) { + if (!gesture_tracker_check(&seatop->gestures, GESTURE_TYPE_SWIPE) && + !gesture_tracker_check(&seatop->gestures, GESTURE_TYPE_WORKSPACE_SWIPE_HORIZONTAL) && + !gesture_tracker_check(&seatop->gestures, GESTURE_TYPE_WORKSPACE_SWIPE_VERTICAL)) { struct sway_cursor *cursor = seat->cursor; wlr_pointer_gestures_v1_send_swipe_end(server.input->pointer_gestures, cursor->seat->wlr_seat, event->time_msec, event->cancelled); return; } - if (event->cancelled) { + if (event->cancelled && + seatop->gestures.type != GESTURE_TYPE_WORKSPACE_SWIPE_HORIZONTAL + && seatop->gestures.type != GESTURE_TYPE_WORKSPACE_SWIPE_VERTICAL) { gesture_tracker_cancel(&seatop->gestures); return; } @@ -1101,7 +1136,15 @@ static void handle_swipe_end(struct sway_seat *seat, &seatop->gestures, device); if (binding) { - gesture_binding_execute(seat, binding); + switch (binding->gesture.type) { + case GESTURE_TYPE_WORKSPACE_SWIPE_HORIZONTAL: + case GESTURE_TYPE_WORKSPACE_SWIPE_VERTICAL: + workspace_scroll_end(seat); + break; + default: + gesture_binding_execute(seat, binding); + break; + } } } @@ -1117,6 +1160,23 @@ static void handle_rebase(struct sway_seat *seat, uint32_t time_msec) { e->previous_node = node_at_coords(seat, cursor->cursor->x, cursor->cursor->y, &surface, &sx, &sy); + // Reset the swipe if any other button is pressed during the swipe + struct sway_workspace *focused_ws = seat_get_focused_workspace(seat); + if (focused_ws) { + struct sway_output *output = focused_ws->output; + struct workspace_scroll workspace_scroll_default = workspace_scroll_get_default(); + switch (e->gestures.type) { + default: + break; + case GESTURE_TYPE_WORKSPACE_SWIPE_HORIZONTAL: + case GESTURE_TYPE_WORKSPACE_SWIPE_VERTICAL: + if (!workspace_scroll_equal(&output->workspace_scroll, &workspace_scroll_default)) { + workspace_scroll_reset(seat, NULL); + } + break; + } + } + if (surface) { if (seat_is_input_allowed(seat, surface)) { wlr_seat_pointer_notify_enter(seat->wlr_seat, surface, sx, sy); diff --git a/sway/meson.build b/sway/meson.build index 40c7382e..3b2b6d88 100644 --- a/sway/meson.build +++ b/sway/meson.build @@ -24,6 +24,8 @@ sway_sources = files( 'desktop/xdg_shell.c', 'desktop/launcher.c', + 'fx_util/animation_utils.c', + 'input/input-manager.c', 'input/cursor.c', 'input/keyboard.c', @@ -122,7 +124,7 @@ sway_sources = files( 'commands/set.c', 'commands/shadow_blur_radius.c', 'commands/shadow_color.c', - 'commands/shadow_offset.c', + 'commands/shadow_offset.c', 'commands/shadow_inactive_color.c', 'commands/shadows.c', 'commands/shadows_on_csd.c', @@ -144,6 +146,7 @@ sway_sources = files( 'commands/unmark.c', 'commands/urgent.c', 'commands/workspace.c', + 'commands/workspace_gesture.c', 'commands/workspace_layout.c', 'commands/ws_auto_back_and_forth.c', 'commands/xwayland.c', diff --git a/sway/sway.5.scd b/sway/sway.5.scd index 10ae1e0a..95b31106 100644 --- a/sway/sway.5.scd +++ b/sway/sway.5.scd @@ -548,6 +548,61 @@ runtime. ``` +*bindworkspacegesture* [--input-device=] [--no-warn] \ +[--inverted] [][:direction] + Binds the swipe to switch workspaces in a 1:1 fashion, but only when + detected. Optionally can be limited to bind to a certain number of + _fingers_ or to certain _directions_. + +[[ *type* +:[ *fingers* +:< *direction* +| swipe +: 3 - 5 +: horizontal, vertical + + The _fingers_ can be limited to any sensible number or left empty to accept + any finger counts. + Valid directions are _horizontal_ and _vertical_. + + If a _input-device_ is given, the binding will only be executed for + that input device and will be executed instead of any binding that is + generic to all devices. By default, if you overwrite a binding, + swaynag will give you a warning. To silence this, use the _--no-warn_ flag. + + If _inverted_ is given, the swipe direction will be reversed. + + The priority for matching bindings is as follows: input device, then + exact matches. + + Gestures executed while the pointer is above a bar are not handled by sway. + See the respective documentation, e.g. *bindgesture* in *sway-bar*(5). + + Example: +``` + # Allow switching between workspaces with left and right swipes + bindworkspacegesture 4:horizontal + # Allow switching between workspaces with up and down swipes + bindworkspacegesture 3:vertical + # Allow switching between workspaces with up and down swipes but inverted + bindworkspacegesture --inverted 3:vertical + +``` + +*workspace_gesture_spring_size* + Adjusts the workspace gestures spring size. Can use values between + 0 (disabled) and 100 while 50 is the default value. + +*workspace_gesture_wrap_around* + Sets whether or not the workspace gesture should wrap around when trying to + swipe past the first/last workspace. Disables the + _workspace_gesture_spring_size_ config option. Disabled by default. + +*workspace_gesture_threshold* + Adjusts the swipe threshold of switching workspaces. A lower value makes it + easier to switch workspaces. Accepts values between 0.1 (small swipes) and + 0.9 (large swipes). The default value is set to 0.5. + *client.background* This command is ignored and is only present for i3 compatibility. diff --git a/sway/tree/workspace.c b/sway/tree/workspace.c index 98a177fa..6c95648e 100644 --- a/sway/tree/workspace.c +++ b/sway/tree/workspace.c @@ -354,11 +354,11 @@ struct sway_workspace *workspace_by_name(const char *name) { if (current && strcmp(name, "prev") == 0) { return workspace_prev(current); } else if (current && strcmp(name, "prev_on_output") == 0) { - return workspace_output_prev(current); + return workspace_output_prev(current, true); } else if (current && strcmp(name, "next") == 0) { return workspace_next(current); } else if (current && strcmp(name, "next_on_output") == 0) { - return workspace_output_next(current); + return workspace_output_next(current, true); } else if (strcmp(name, "current") == 0) { return current; } else if (strcasecmp(name, "back_and_forth") == 0) { @@ -521,7 +521,7 @@ struct sway_workspace *workspace_next(struct sway_workspace *workspace) { * otherwise the next one is returned. */ static struct sway_workspace *workspace_output_prev_next_impl( - struct sway_output *output, int dir) { + struct sway_output *output, int dir, bool should_wrap) { struct sway_seat *seat = input_manager_current_seat(); struct sway_workspace *workspace = seat_get_focused_workspace(seat); if (!workspace) { @@ -531,17 +531,27 @@ static struct sway_workspace *workspace_output_prev_next_impl( } int index = list_find(output->workspaces, workspace); - size_t new_index = wrap(index + dir, output->workspaces->length); + size_t new_index; + if (!should_wrap) { + new_index = index += dir; + if (index < 0 || index >= output->workspaces->length) { + return NULL; + } + } else { + new_index = wrap(index + dir, output->workspaces->length); + } return output->workspaces->items[new_index]; } -struct sway_workspace *workspace_output_next(struct sway_workspace *current) { - return workspace_output_prev_next_impl(current->output, 1); +struct sway_workspace *workspace_output_next(struct sway_workspace *current, + bool should_wrap) { + return workspace_output_prev_next_impl(current->output, 1, should_wrap); } -struct sway_workspace *workspace_output_prev(struct sway_workspace *current) { - return workspace_output_prev_next_impl(current->output, -1); +struct sway_workspace *workspace_output_prev(struct sway_workspace *current, + bool should_wrap) { + return workspace_output_prev_next_impl(current->output, -1, should_wrap); } struct sway_workspace *workspace_auto_back_and_forth( @@ -571,6 +581,7 @@ bool workspace_switch(struct sway_workspace *workspace) { sway_log(SWAY_DEBUG, "Switching to workspace %p:%s", workspace, workspace->name); + workspace->output->workspace_scroll = workspace_scroll_get_default(); struct sway_node *next = seat_get_focus_inactive(seat, &workspace->node); if (next == NULL) { next = &workspace->node;