diff --git a/include/sway/commands.h b/include/sway/commands.h index d39ac56c..365068ae 100644 --- a/include/sway/commands.h +++ b/include/sway/commands.h @@ -144,6 +144,7 @@ sway_cmd cmd_splitt; sway_cmd cmd_splitv; sway_cmd cmd_sticky; sway_cmd cmd_swaybg_command; +sway_cmd cmd_swap; sway_cmd cmd_title_format; sway_cmd cmd_unmark; sway_cmd cmd_workspace; diff --git a/include/sway/tree/layout.h b/include/sway/tree/layout.h index cc999871..2e0f2abf 100644 --- a/include/sway/tree/layout.h +++ b/include/sway/tree/layout.h @@ -69,4 +69,6 @@ struct sway_container *container_split(struct sway_container *child, void container_recursive_resize(struct sway_container *container, double amount, enum resize_edge edge); +void container_swap(struct sway_container *con1, struct sway_container *con2); + #endif diff --git a/sway/commands.c b/sway/commands.c index 6cba0a1c..c3728afd 100644 --- a/sway/commands.c +++ b/sway/commands.c @@ -186,6 +186,7 @@ static struct cmd_handler command_handlers[] = { { "splith", cmd_splith }, { "splitt", cmd_splitt }, { "splitv", cmd_splitv }, + { "swap", cmd_swap }, { "title_format", cmd_title_format }, { "unmark", cmd_unmark }, }; diff --git a/sway/commands/swap.c b/sway/commands/swap.c new file mode 100644 index 00000000..e925ad33 --- /dev/null +++ b/sway/commands/swap.c @@ -0,0 +1,80 @@ +#include +#include +#include "sway/commands.h" +#include "sway/tree/layout.h" +#include "sway/tree/view.h" +#include "stringop.h" + +static const char* EXPECTED_SYNTAX = + "Expected 'swap container with id|con_id|mark '"; + +static bool test_con_id(struct sway_container *container, void *con_id) { + return container->id == (size_t)con_id; +} + +static bool test_id(struct sway_container *container, void *id) { + xcb_window_t *wid = id; + return (container->type == C_VIEW + && container->sway_view->type == SWAY_VIEW_XWAYLAND + && container->sway_view->wlr_xwayland_surface->window_id == *wid); +} + +static bool test_mark(struct sway_container *container, void *mark) { + if (container->type == C_VIEW && container->sway_view->marks->length) { + return !list_seq_find(container->sway_view->marks, + (int (*)(const void *, const void *))strcmp, mark); + } + return false; +} + +struct cmd_results *cmd_swap(int argc, char **argv) { + struct cmd_results *error = NULL; + if ((error = checkarg(argc, "swap", EXPECTED_AT_LEAST, 4))) { + return error; + } + + if (strcasecmp(argv[0], "container") || strcasecmp(argv[1], "with")) { + return cmd_results_new(CMD_INVALID, "swap", EXPECTED_SYNTAX); + } + + struct sway_container *current = config->handler_context.current_container; + struct sway_container *other; + + char *value = join_args(argv + 3, argc - 3); + if (strcasecmp(argv[2], "id") == 0) { + xcb_window_t id = strtol(value, NULL, 0); + other = container_find(&root_container, test_id, (void *)&id); + } else if (strcasecmp(argv[2], "con_id") == 0) { + size_t con_id = atoi(value); + other = container_find(&root_container, test_con_id, (void *)con_id); + } else if (strcasecmp(argv[2], "mark") == 0) { + other = container_find(&root_container, test_mark, (void *)value); + } else { + free(value); + return cmd_results_new(CMD_INVALID, "swap", EXPECTED_SYNTAX); + } + + if (!other) { + error = cmd_results_new(CMD_FAILURE, "swap", + "Failed to find %s '%s'", argv[2], value); + } else if (current->type < C_CONTAINER || other->type < C_CONTAINER) { + error = cmd_results_new(CMD_FAILURE, "swap", + "Can only swap with containers and views"); + } else if (container_has_anscestor(current, other) + || container_has_anscestor(other, current)) { + error = cmd_results_new(CMD_FAILURE, "swap", + "Cannot swap ancestor and descendant"); + } else if (current->layout == L_FLOATING || other->layout == L_FLOATING) { + error = cmd_results_new(CMD_FAILURE, "swap", + "Swapping with floating containers is not supported"); + } + + free(value); + + if (error) { + return error; + } + + container_swap(current, other); + return cmd_results_new(CMD_SUCCESS, NULL, NULL); +} diff --git a/sway/meson.build b/sway/meson.build index 72347d51..9c942e8e 100644 --- a/sway/meson.build +++ b/sway/meson.build @@ -63,6 +63,7 @@ sway_sources = files( 'commands/show_marks.c', 'commands/split.c', 'commands/swaybg_command.c', + 'commands/swap.c', 'commands/title_format.c', 'commands/unmark.c', 'commands/workspace.c', diff --git a/sway/sway.5.scd b/sway/sway.5.scd index 8d1cb8a1..e6bc5a1e 100644 --- a/sway/sway.5.scd +++ b/sway/sway.5.scd @@ -167,6 +167,15 @@ They are expected to be used with *bindsym* or at runtime through *swaymsg*(1). "Sticks" a floating window to the current output so that it shows up on all workspaces. +*swap* container with id|con\_id|mark + Swaps the position, geometry, and fullscreen status of two containers. The + first container can be selected either by criteria or focus. The second + container can be selected by _id_, _con\_id_, or _mark_. _id_ can only be + used with xwayland views. If the first container has focus, it will retain + focus unless it is moved to a different workspace or the second container + becomes fullscreen on the same workspace as the first container. In either + of those cases, the second container will gain focus. + The following commands may be used either in the configuration file or at runtime. diff --git a/sway/tree/layout.c b/sway/tree/layout.c index 21cec529..624d5516 100644 --- a/sway/tree/layout.c +++ b/sway/tree/layout.c @@ -882,3 +882,127 @@ void container_recursive_resize(struct sway_container *container, } } } + +static void swap_places(struct sway_container *con1, + struct sway_container *con2) { + struct sway_container *temp = malloc(sizeof(struct sway_container)); + temp->x = con1->x; + temp->y = con1->y; + temp->width = con1->width; + temp->height = con1->height; + temp->parent = con1->parent; + + con1->x = con2->x; + con1->y = con2->y; + con1->width = con2->width; + con1->height = con2->height; + + con2->x = temp->x; + con2->y = temp->y; + con2->width = temp->width; + con2->height = temp->height; + + int temp_index = index_child(con1); + container_insert_child(con2->parent, con1, index_child(con2)); + container_insert_child(temp->parent, con2, temp_index); + + free(temp); +} + +static void swap_focus(struct sway_container *con1, + struct sway_container *con2, struct sway_seat *seat, + struct sway_container *focus) { + if (focus == con1 || focus == con2) { + struct sway_container *ws1 = container_parent(con1, C_WORKSPACE); + struct sway_container *ws2 = container_parent(con2, C_WORKSPACE); + if (focus == con1 && (con2->parent->layout == L_TABBED + || con2->parent->layout == L_STACKED)) { + if (workspace_is_visible(ws2)) { + seat_set_focus_warp(seat, con2, false); + } + seat_set_focus(seat, ws1 != ws2 ? con2 : con1); + } else if (focus == con2 && (con1->parent->layout == L_TABBED + || con1->parent->layout == L_STACKED)) { + if (workspace_is_visible(ws1)) { + seat_set_focus_warp(seat, con1, false); + } + seat_set_focus(seat, ws1 != ws2 ? con1 : con2); + } else if (ws1 != ws2) { + seat_set_focus(seat, focus == con1 ? con2 : con1); + } else { + seat_set_focus(seat, focus); + } + } else { + seat_set_focus(seat, focus); + } +} + +void container_swap(struct sway_container *con1, struct sway_container *con2) { + if (!sway_assert(con1 && con2, "Cannot swap with nothing")) { + return; + } + if (!sway_assert(con1->type >= C_CONTAINER && con2->type >= C_CONTAINER, + "Can only swap containers and views")) { + return; + } + if (!sway_assert(!container_has_anscestor(con1, con2) + && !container_has_anscestor(con2, con1), + "Cannot swap anscestor and descendant")) { + return; + } + if (!sway_assert(con1->layout != L_FLOATING && con2->layout != L_FLOATING, + "Swapping with floating containers is not supported")) { + return; + } + + wlr_log(L_DEBUG, "Swapping containers %zu and %zu", con1->id, con2->id); + + int fs1 = con1->type == C_VIEW && con1->sway_view->is_fullscreen; + int fs2 = con2->type == C_VIEW && con2->sway_view->is_fullscreen; + if (fs1) { + view_set_fullscreen(con1->sway_view, false); + } + if (fs2) { + view_set_fullscreen(con2->sway_view, false); + } + + struct sway_seat *seat = input_manager_get_default_seat(input_manager); + struct sway_container *focus = seat_get_focus(seat); + struct sway_container *vis1 = container_parent( + seat_get_focus_inactive(seat, container_parent(con1, C_OUTPUT)), + C_WORKSPACE); + struct sway_container *vis2 = container_parent( + seat_get_focus_inactive(seat, container_parent(con2, C_OUTPUT)), + C_WORKSPACE); + + char *stored_prev_name = NULL; + if (prev_workspace_name) { + stored_prev_name = strdup(prev_workspace_name); + } + + swap_places(con1, con2); + + if (!workspace_is_visible(vis1)) { + seat_set_focus(seat, seat_get_focus_inactive(seat, vis1)); + } + if (!workspace_is_visible(vis2)) { + seat_set_focus(seat, seat_get_focus_inactive(seat, vis2)); + } + + swap_focus(con1, con2, seat, focus); + + if (stored_prev_name) { + free(prev_workspace_name); + prev_workspace_name = stored_prev_name; + } + + arrange_children_of(con1->parent); + arrange_children_of(con2->parent); + + if (fs1 && con2->type == C_VIEW) { + view_set_fullscreen(con2->sway_view, true); + } + if (fs2 && con1->type == C_VIEW) { + view_set_fullscreen(con1->sway_view, true); + } +}