From 438010938c38020c071841b43509185d67c462c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Mar=C3=A7ais?= Date: Mon, 12 Jan 2026 15:50:39 +0100 Subject: [PATCH 01/25] Fix logical error in criteria matching When evaluating a __focused__ pattern, sway's logic is to consider the pattern match to have failed for a given view and attribute if a view is focused and the value of attribute in question of the focused view is not that of the view in question. Expected behaviour is that the pattern matches if there is a focused view and that the values of the attribute for the focused view and the other view are equal. (See #6753, which is the case where the `workspace` attribute is matched against.). I.e., if we write out the function `criteria_matches_view` as a logical formula, expected behaviour is criteria_matches_view(criteria, view) <=> forall (attribute, pattern) in criteria, (pattern is __focused__ and focused view exists and view.attribute == focused.attribute) or (pattern is not __focused__ and view.attribute =~ pattern) but it is actually (pay attention to the fourth line) criteria_matches_view(criteria, view) <=> forall (attribute, pattern) in criteria, (pattern is __focused__ and (no focused view or view.attribute == focused.attribute)) or (pattern is not __focused__ and view.attribute =~ pattern). Fix program logic to reflect (the disjunctive form of) the first formula to be compatible with i3. (In passing, this fixes #6753.) --- sway/criteria.c | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/sway/criteria.c b/sway/criteria.c index e200d4c8f..230f47a18 100644 --- a/sway/criteria.c +++ b/sway/criteria.c @@ -204,7 +204,7 @@ static bool criteria_matches_view(struct criteria *criteria, switch (criteria->title->match_type) { case PATTERN_FOCUSED: - if (focused && lenient_strcmp(title, view_get_title(focused))) { + if (!focused || lenient_strcmp(title, view_get_title(focused))) { return false; } break; @@ -224,7 +224,7 @@ static bool criteria_matches_view(struct criteria *criteria, switch (criteria->shell->match_type) { case PATTERN_FOCUSED: - if (focused && strcmp(shell, view_get_shell(focused))) { + if (!focused || strcmp(shell, view_get_shell(focused))) { return false; } break; @@ -244,7 +244,7 @@ static bool criteria_matches_view(struct criteria *criteria, switch (criteria->app_id->match_type) { case PATTERN_FOCUSED: - if (focused && lenient_strcmp(app_id, view_get_app_id(focused))) { + if (!focused || lenient_strcmp(app_id, view_get_app_id(focused))) { return false; } break; @@ -264,7 +264,7 @@ static bool criteria_matches_view(struct criteria *criteria, switch (criteria->sandbox_engine->match_type) { case PATTERN_FOCUSED: - if (focused && lenient_strcmp(sandbox_engine, view_get_sandbox_engine(focused))) { + if (!focused || lenient_strcmp(sandbox_engine, view_get_sandbox_engine(focused))) { return false; } break; @@ -284,7 +284,7 @@ static bool criteria_matches_view(struct criteria *criteria, switch (criteria->sandbox_app_id->match_type) { case PATTERN_FOCUSED: - if (focused && lenient_strcmp(sandbox_app_id, view_get_sandbox_app_id(focused))) { + if (!focused || lenient_strcmp(sandbox_app_id, view_get_sandbox_app_id(focused))) { return false; } break; @@ -304,7 +304,7 @@ static bool criteria_matches_view(struct criteria *criteria, switch (criteria->sandbox_instance_id->match_type) { case PATTERN_FOCUSED: - if (focused && lenient_strcmp(sandbox_instance_id, view_get_sandbox_instance_id(focused))) { + if (!focused || lenient_strcmp(sandbox_instance_id, view_get_sandbox_instance_id(focused))) { return false; } break; @@ -324,7 +324,7 @@ static bool criteria_matches_view(struct criteria *criteria, switch (criteria->tag->match_type) { case PATTERN_FOCUSED: - if (focused && lenient_strcmp(tag, view_get_tag(focused))) { + if (!focused || lenient_strcmp(tag, view_get_tag(focused))) { return false; } break; @@ -356,7 +356,7 @@ static bool criteria_matches_view(struct criteria *criteria, switch (criteria->class->match_type) { case PATTERN_FOCUSED: - if (focused && lenient_strcmp(class, view_get_class(focused))) { + if (!focused || lenient_strcmp(class, view_get_class(focused))) { return false; } break; @@ -376,7 +376,7 @@ static bool criteria_matches_view(struct criteria *criteria, switch (criteria->instance->match_type) { case PATTERN_FOCUSED: - if (focused && lenient_strcmp(instance, view_get_instance(focused))) { + if (!focused || lenient_strcmp(instance, view_get_instance(focused))) { return false; } break; @@ -396,7 +396,7 @@ static bool criteria_matches_view(struct criteria *criteria, switch (criteria->window_role->match_type) { case PATTERN_FOCUSED: - if (focused && lenient_strcmp(window_role, view_get_window_role(focused))) { + if (!focused || lenient_strcmp(window_role, view_get_window_role(focused))) { return false; } break; @@ -454,7 +454,7 @@ static bool criteria_matches_view(struct criteria *criteria, switch (criteria->workspace->match_type) { case PATTERN_FOCUSED: - if (focused && + if (!focused || strcmp(ws->name, focused->container->pending.workspace->name)) { return false; } From fa497964fd55632beacf5f425e964ae4893e25b9 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Thu, 12 Feb 2026 21:51:54 +0100 Subject: [PATCH 02/25] Add support for keypad slide switches See the libinput docs: https://wayland.freedesktop.org/libinput/doc/latest/api/group__device.html#gga507e97278f2eb7d2271023ef2a3d31a4aed050386a5b52b2b51d05d95bb594c30 --- meson.build | 2 +- sway/commands/bind.c | 4 ++++ sway/sway.5.scd | 11 ++++++----- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/meson.build b/meson.build index de5a620c4..974a4071b 100644 --- a/meson.build +++ b/meson.build @@ -110,7 +110,7 @@ conf_data.set10('HAVE_LIBSYSTEMD', sdbus.found() and sdbus.name() == 'libsystemd conf_data.set10('HAVE_LIBELOGIND', sdbus.found() and sdbus.name() == 'libelogind') conf_data.set10('HAVE_BASU', sdbus.found() and sdbus.name() == 'basu') conf_data.set10('HAVE_TRAY', have_tray) -foreach sym : ['LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM', 'LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_STICKY'] +foreach sym : ['LIBINPUT_CONFIG_ACCEL_PROFILE_CUSTOM', 'LIBINPUT_CONFIG_DRAG_LOCK_ENABLED_STICKY', 'LIBINPUT_SWITCH_KEYPAD_SLIDE'] conf_data.set10('HAVE_' + sym, cc.has_header_symbol('libinput.h', sym, dependencies: libinput)) endforeach diff --git a/sway/commands/bind.c b/sway/commands/bind.c index 15373d5a8..627d994a0 100644 --- a/sway/commands/bind.c +++ b/sway/commands/bind.c @@ -543,6 +543,10 @@ struct cmd_results *cmd_bind_or_unbind_switch(int argc, char **argv, binding->type = WLR_SWITCH_TYPE_TABLET_MODE; } else if (strcmp(split->items[0], "lid") == 0) { binding->type = WLR_SWITCH_TYPE_LID; +#if HAVE_LIBINPUT_SWITCH_KEYPAD_SLIDE + } else if (strcmp(split->items[0], "keypad_slide") == 0) { + binding->type = WLR_SWITCH_TYPE_KEYPAD_SLIDE; +#endif } else { free_switch_binding(binding); return cmd_results_new(CMD_FAILURE, diff --git a/sway/sway.5.scd b/sway/sway.5.scd index 53393bcc9..952d243d2 100644 --- a/sway/sway.5.scd +++ b/sway/sway.5.scd @@ -506,11 +506,12 @@ runtime. *bindswitch* [--locked] [--no-warn] [--reload] : Binds to execute the sway command _command_ on state changes. - Supported switches are _lid_ (laptop lid) and _tablet_ (tablet mode) - switches. Valid values for _state_ are _on_, _off_ and _toggle_. These - switches are on when the device lid is shut and when tablet mode is active - respectively. _toggle_ is also supported to run a command both when the - switch is toggled on or off. + Supported switches are _lid_ (laptop lid), _tablet_ (tablet mode) and + _keypad_slide_ (whether the device keypad is exposed or not) switches. Valid + values for _state_ are _on_, _off_ and _toggle_. These switches are on when + the device lid is shut, when tablet mode is active and when the keypad is + exposed respectively. _toggle_ is also supported to run a command both when + the switch is toggled on or off. Unless the flag _--locked_ is set, the command will not be run when a screen locking program is active. If there is a matching binding with From 99e17d5efb28cf1017743a709aa28f2e0177f43f Mon Sep 17 00:00:00 2001 From: Milad Alizadeh Date: Wed, 18 Feb 2026 16:32:11 +0000 Subject: [PATCH 03/25] tree/workspace: fix output priority in workspace assignment workspace_valid_on_output() and workspace_next_name() check whether an output appears anywhere in a workspace's output list, ignoring priority order. This allows a lower-priority output to claim a workspace even when a higher-priority output is available. Fix by stopping early when iterating the output list: if a different available output is found before the current one, the workspace belongs to that higher-priority output. --- sway/tree/workspace.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sway/tree/workspace.c b/sway/tree/workspace.c index 733a002b4..9a8e53d25 100644 --- a/sway/tree/workspace.c +++ b/sway/tree/workspace.c @@ -205,6 +205,9 @@ static bool workspace_valid_on_output(const char *output_name, if (output_match_name_or_id(output, wsc->outputs->items[i])) { return true; } + if (output_by_name_or_id(wsc->outputs->items[i])) { + return false; // a higher-priority output is available + } } return false; @@ -326,6 +329,9 @@ char *workspace_next_name(const char *output_name) { target = strdup(wsc->workspace); break; } + if (output_by_name_or_id(wsc->outputs->items[j])) { + break; // a higher-priority output is available + } } if (found) { break; From f66b69762d6844cee473c591dbf3789800ecb785 Mon Sep 17 00:00:00 2001 From: Milad Alizadeh Date: Fri, 20 Feb 2026 21:49:14 +0000 Subject: [PATCH 04/25] simplify output priority check --- sway/tree/workspace.c | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/sway/tree/workspace.c b/sway/tree/workspace.c index 9a8e53d25..49a830e7f 100644 --- a/sway/tree/workspace.c +++ b/sway/tree/workspace.c @@ -202,11 +202,10 @@ static bool workspace_valid_on_output(const char *output_name, } for (int i = 0; i < wsc->outputs->length; i++) { - if (output_match_name_or_id(output, wsc->outputs->items[i])) { - return true; - } - if (output_by_name_or_id(wsc->outputs->items[i])) { - return false; // a higher-priority output is available + struct sway_output *ws_output = + output_by_name_or_id(wsc->outputs->items[i]); + if (ws_output) { + return ws_output == output; } } @@ -323,15 +322,16 @@ char *workspace_next_name(const char *output_name) { } bool found = false; for (int j = 0; j < wsc->outputs->length; ++j) { - if (output_match_name_or_id(output, wsc->outputs->items[j])) { - found = true; - free(target); - target = strdup(wsc->workspace); + struct sway_output *ws_output = + output_by_name_or_id(wsc->outputs->items[j]); + if (ws_output) { + if (ws_output == output) { + found = true; + free(target); + target = strdup(wsc->workspace); + } break; } - if (output_by_name_or_id(wsc->outputs->items[j])) { - break; // a higher-priority output is available - } } if (found) { break; From ee61bf1d36fd743fab06ff0fce8204f7ff5523e3 Mon Sep 17 00:00:00 2001 From: Kenny Levinsen Date: Tue, 24 Feb 2026 11:40:00 +0100 Subject: [PATCH 05/25] linux-dmabuf-v1: Bump to version 5 References: https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/4459 --- sway/server.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sway/server.c b/sway/server.c index aef0d0b0c..f58ea9471 100644 --- a/sway/server.c +++ b/sway/server.c @@ -272,7 +272,7 @@ bool server_init(struct sway_server *server) { if (wlr_renderer_get_texture_formats(server->renderer, WLR_BUFFER_CAP_DMABUF) != NULL) { server->linux_dmabuf_v1 = wlr_linux_dmabuf_v1_create_with_renderer( - server->wl_display, 4, server->renderer); + server->wl_display, 5, server->renderer); } if (wlr_renderer_get_drm_fd(server->renderer) >= 0 && server->renderer->features.timeline && From 468d4bc537e003b55ce7942db11396868b6e488a Mon Sep 17 00:00:00 2001 From: Kenny Levinsen Date: Tue, 24 Feb 2026 11:34:01 +0100 Subject: [PATCH 06/25] cursor-shape-v1: Bump to version 2 This adds two new shapes: dnd_ask and all_resize. References: https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/4866 --- sway/server.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sway/server.c b/sway/server.c index f58ea9471..cb8f4cf04 100644 --- a/sway/server.c +++ b/sway/server.c @@ -440,7 +440,7 @@ bool server_init(struct sway_server *server) { &server->xdg_toplevel_tag_manager_v1_set_tag); struct wlr_cursor_shape_manager_v1 *cursor_shape_manager = - wlr_cursor_shape_manager_v1_create(server->wl_display, 1); + wlr_cursor_shape_manager_v1_create(server->wl_display, 2); server->request_set_cursor_shape.notify = handle_request_set_cursor_shape; wl_signal_add(&cursor_shape_manager->events.request_set_shape, &server->request_set_cursor_shape); From 8b1f48d25b7d27ed9c3fe3e08a2b948e01d8deec Mon Sep 17 00:00:00 2001 From: Kenny Levinsen Date: Tue, 24 Feb 2026 11:55:04 +0100 Subject: [PATCH 07/25] layer-shell-v1: Bump to version 5 This adds support for set_exclusive_edge. References: https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/5275 --- sway/server.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sway/server.c b/sway/server.c index cb8f4cf04..5d446579f 100644 --- a/sway/server.c +++ b/sway/server.c @@ -74,7 +74,7 @@ #endif #define SWAY_XDG_SHELL_VERSION 5 -#define SWAY_LAYER_SHELL_VERSION 4 +#define SWAY_LAYER_SHELL_VERSION 5 #define SWAY_FOREIGN_TOPLEVEL_LIST_VERSION 1 #define SWAY_PRESENTATION_VERSION 2 From d44248ce64c3fe3a477ab096a0e43c7c92241004 Mon Sep 17 00:00:00 2001 From: Kenny Levinsen Date: Tue, 24 Feb 2026 14:58:43 +0100 Subject: [PATCH 08/25] protocols: Bump vendored wlr-protocols --- protocols/wlr-layer-shell-unstable-v1.xml | 21 +++++++++++++++++-- ...lr-output-power-management-unstable-v1.xml | 4 ++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/protocols/wlr-layer-shell-unstable-v1.xml b/protocols/wlr-layer-shell-unstable-v1.xml index d62fd51e9..e9f27e4fd 100644 --- a/protocols/wlr-layer-shell-unstable-v1.xml +++ b/protocols/wlr-layer-shell-unstable-v1.xml @@ -25,7 +25,7 @@ THIS SOFTWARE. - + Clients can use this interface to assign the surface_layer role to wl_surfaces. Such surfaces are assigned to a "layer" of the output and @@ -100,7 +100,7 @@ - + An interface that may be implemented by a wl_surface, for surfaces that are designed to be rendered as a layer of a stacked desktop-like @@ -367,6 +367,7 @@ + @@ -386,5 +387,21 @@ + + + + + + Requests an edge for the exclusive zone to apply. The exclusive + edge will be automatically deduced from anchor points when possible, + but when the surface is anchored to a corner, it will be necessary + to set it explicitly to disambiguate, as it is not possible to deduce + which one of the two corner edges should be used. + + The edge must be one the surface is anchored to, otherwise the + invalid_exclusive_edge protocol error will be raised. + + + diff --git a/protocols/wlr-output-power-management-unstable-v1.xml b/protocols/wlr-output-power-management-unstable-v1.xml index a97783991..20dbb7760 100644 --- a/protocols/wlr-output-power-management-unstable-v1.xml +++ b/protocols/wlr-output-power-management-unstable-v1.xml @@ -50,7 +50,7 @@ - Create a output power management mode control that can be used to + Create an output power management mode control that can be used to adjust the power management mode for a given output. @@ -79,7 +79,7 @@ - + From d6425c527a0b0af26e763f2a1201df7c7e24cbc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Poisot?= Date: Thu, 26 Feb 2026 20:40:04 +0000 Subject: [PATCH 09/25] sway/config/output: fix hdr+color_profile conflict detection Fixes: 26eb393d6dd7631dc5b7edd0df95342d606e907c `color_profile --device-primaries gamma22` was not inhibiting HDR like all other non-default color_profile values --- sway/config/output.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sway/config/output.c b/sway/config/output.c index 3d25b46c7..6d6afdc25 100644 --- a/sway/config/output.c +++ b/sway/config/output.c @@ -555,8 +555,10 @@ static void queue_output_config(struct output_config *oc, } bool hdr = oc && oc->hdr == 1; - if (hdr && oc->color_transform != NULL) { - sway_log(SWAY_ERROR, "Cannot HDR on output %s: output has an ICC profile set", wlr_output->name); + bool color_profile = oc && (oc->color_transform != NULL + || oc->color_profile == COLOR_PROFILE_TRANSFORM_WITH_DEVICE_PRIMARIES); + if (hdr && color_profile) { + sway_log(SWAY_ERROR, "Cannot use HDR on output %s: output has a color profile set", wlr_output->name); hdr = false; } set_hdr(wlr_output, pending, hdr); From 0356a020c1c14b4f9e9c3583d6a87dd7764714a2 Mon Sep 17 00:00:00 2001 From: "Lars-Ragnar A. Haugen" Date: Thu, 26 Feb 2026 16:34:37 +0100 Subject: [PATCH 10/25] layer-shell: handle popup reposition for unconstraining Layer shell popups were missing a handler for the xdg_popup reposition event. When a client (e.g. GTK4) creates a popup and then sends a reposition request, wlroots resets the scheduled geometry back to the positioner's original value. Without a reposition handler, the unconstrained geometry computed on the initial commit was lost, causing popups such as tooltips to render outside the screen viewport. This was most visible with GTK4 layer shell apps (e.g. taskbars) where tooltips would appear below the bottom edge of the screen instead of being flipped/slid into the visible area. Also switch the destroy listener from wlr_popup->base->events.destroy to wlr_popup->events.destroy so that cleanup runs before wlroots asserts that all popup signal listeners have been removed. Fixes #8518 --- include/sway/layers.h | 1 + sway/desktop/layer_shell.c | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/include/sway/layers.h b/include/sway/layers.h index 27b5dde1b..e257da0bd 100644 --- a/include/sway/layers.h +++ b/include/sway/layers.h @@ -33,6 +33,7 @@ struct sway_layer_popup { struct wl_listener destroy; struct wl_listener new_popup; struct wl_listener commit; + struct wl_listener reposition; }; struct sway_output; diff --git a/sway/desktop/layer_shell.c b/sway/desktop/layer_shell.c index 8c54d71aa..c8f485971 100644 --- a/sway/desktop/layer_shell.c +++ b/sway/desktop/layer_shell.c @@ -321,6 +321,7 @@ static void popup_handle_destroy(struct wl_listener *listener, void *data) { wl_list_remove(&popup->destroy.link); wl_list_remove(&popup->new_popup.link); wl_list_remove(&popup->commit.link); + wl_list_remove(&popup->reposition.link); free(popup); } @@ -356,6 +357,11 @@ static void popup_handle_commit(struct wl_listener *listener, void *data) { } } +static void popup_handle_reposition(struct wl_listener *listener, void *data) { + struct sway_layer_popup *popup = wl_container_of(listener, popup, reposition); + popup_unconstrain(popup); +} + static void popup_handle_new_popup(struct wl_listener *listener, void *data); static struct sway_layer_popup *create_popup(struct wlr_xdg_popup *wlr_popup, @@ -376,11 +382,13 @@ static struct sway_layer_popup *create_popup(struct wlr_xdg_popup *wlr_popup, } popup->destroy.notify = popup_handle_destroy; - wl_signal_add(&wlr_popup->base->events.destroy, &popup->destroy); + wl_signal_add(&wlr_popup->events.destroy, &popup->destroy); popup->new_popup.notify = popup_handle_new_popup; wl_signal_add(&wlr_popup->base->events.new_popup, &popup->new_popup); popup->commit.notify = popup_handle_commit; wl_signal_add(&wlr_popup->base->surface->events.commit, &popup->commit); + popup->reposition.notify = popup_handle_reposition; + wl_signal_add(&wlr_popup->events.reposition, &popup->reposition); return popup; } From c57daaf0d1640b45579d75ce9775b8c0d03299b7 Mon Sep 17 00:00:00 2001 From: Willow Barraco Date: Wed, 25 Feb 2026 07:42:24 +0100 Subject: [PATCH 11/25] Skip checking if criteria is matching the view when not mapped Multiple checks in this method use view->container, but it can be NULL at some points. Bail early to avoid crash. --- sway/criteria.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sway/criteria.c b/sway/criteria.c index 230f47a18..6be6e7042 100644 --- a/sway/criteria.c +++ b/sway/criteria.c @@ -196,6 +196,10 @@ static bool criteria_matches_view(struct criteria *criteria, struct sway_container *focus = seat_get_focused_container(seat); struct sway_view *focused = focus ? focus->view : NULL; + if (!view->container) { + return false; + } + if (criteria->title) { const char *title = view_get_title(view); if (!title) { From 6d25b100a23a17e9663cab5c286934089f2c4460 Mon Sep 17 00:00:00 2001 From: Alexander Orzechowski Date: Fri, 13 Mar 2026 14:51:47 -0400 Subject: [PATCH 12/25] Chase wlroots!3879 https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/3879 --- include/sway/tree/container.h | 3 +-- sway/tree/container.c | 51 ++++++++++++++++++----------------- 2 files changed, 28 insertions(+), 26 deletions(-) diff --git a/include/sway/tree/container.h b/include/sway/tree/container.h index e18fd00ac..12a53c843 100644 --- a/include/sway/tree/container.h +++ b/include/sway/tree/container.h @@ -93,8 +93,7 @@ struct sway_container { struct wlr_scene_tree *content_tree; struct wlr_scene_buffer *output_handler; - struct wl_listener output_enter; - struct wl_listener output_leave; + struct wl_listener outputs_update; struct wl_listener output_handler_destroy; struct sway_container_state current; diff --git a/sway/tree/container.c b/sway/tree/container.c index c9ec852fc..fd9abadc6 100644 --- a/sway/tree/container.c +++ b/sway/tree/container.c @@ -25,27 +25,34 @@ #include "log.h" #include "stringop.h" -static void handle_output_enter( +static void handle_outputs_update( struct wl_listener *listener, void *data) { struct sway_container *con = wl_container_of( - listener, con, output_enter); - struct wlr_scene_output *output = data; + listener, con, outputs_update); + struct wlr_scene_outputs_update_event *event = data; - if (con->view->foreign_toplevel) { - wlr_foreign_toplevel_handle_v1_output_enter( - con->view->foreign_toplevel, output->output); - } -} + struct wlr_foreign_toplevel_handle_v1 *toplevel = con->view->foreign_toplevel; + if (toplevel) { + struct wlr_foreign_toplevel_handle_v1_output *toplevel_output, *tmp; + wl_list_for_each_safe(toplevel_output, tmp, &toplevel->outputs, link) { + bool active = false; + for (size_t i = 0; i < event->size; i++) { + struct wlr_scene_output *scene_output = event->active[i]; + if (scene_output->output == toplevel_output->output) { + active = true; + break; + } + } -static void handle_output_leave( - struct wl_listener *listener, void *data) { - struct sway_container *con = wl_container_of( - listener, con, output_leave); - struct wlr_scene_output *output = data; + if (!active) { + wlr_foreign_toplevel_handle_v1_output_leave(toplevel, toplevel_output->output); + } + } - if (con->view->foreign_toplevel) { - wlr_foreign_toplevel_handle_v1_output_leave( - con->view->foreign_toplevel, output->output); + for (size_t i = 0; i < event->size; i++) { + struct wlr_scene_output *scene_output = event->active[i]; + wlr_foreign_toplevel_handle_v1_output_enter(toplevel, scene_output->output); + } } } @@ -136,12 +143,9 @@ struct sway_container *container_create(struct sway_view *view) { } if (!failed) { - c->output_enter.notify = handle_output_enter; - wl_signal_add(&c->output_handler->events.output_enter, - &c->output_enter); - c->output_leave.notify = handle_output_leave; - wl_signal_add(&c->output_handler->events.output_leave, - &c->output_leave); + c->outputs_update.notify = handle_outputs_update; + wl_signal_add(&c->output_handler->events.outputs_update, + &c->outputs_update); c->output_handler_destroy.notify = handle_destroy; wl_signal_add(&c->output_handler->node.events.destroy, &c->output_handler_destroy); @@ -562,8 +566,7 @@ void container_begin_destroy(struct sway_container *con) { } if (con->view && con->view->container == con) { - wl_list_remove(&con->output_enter.link); - wl_list_remove(&con->output_leave.link); + wl_list_remove(&con->outputs_update.link); wl_list_remove(&con->output_handler_destroy.link); } } From 2c2a2ec38055092f368b44d4affefdfa8df17ff9 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Sat, 14 Mar 2026 13:08:24 +0100 Subject: [PATCH 13/25] common/pango: ensure we return zero on get_text_size() error Avoid leaving out pointers undefined. --- common/pango.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/pango.c b/common/pango.c index e52b52b93..9efa93f7e 100644 --- a/common/pango.c +++ b/common/pango.c @@ -84,6 +84,8 @@ PangoLayout *get_pango_layout(cairo_t *cairo, const PangoFontDescription *desc, void get_text_size(cairo_t *cairo, const PangoFontDescription *desc, int *width, int *height, int *baseline, double scale, bool markup, const char *fmt, ...) { + *width = *height = *baseline = 0; + va_list args; va_start(args, fmt); char *buf = vformat_str(fmt, args); From 9d0dbe66c307bbb227ce746d76b66b647350906b Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Sat, 14 Mar 2026 13:09:52 +0100 Subject: [PATCH 14/25] common/pango: log on get_text_size()/render_text() format failure --- common/pango.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/common/pango.c b/common/pango.c index 9efa93f7e..5c7ff3076 100644 --- a/common/pango.c +++ b/common/pango.c @@ -91,6 +91,7 @@ void get_text_size(cairo_t *cairo, const PangoFontDescription *desc, int *width, char *buf = vformat_str(fmt, args); va_end(args); if (buf == NULL) { + sway_log(SWAY_ERROR, "Failed to format string"); return; } @@ -127,6 +128,7 @@ void render_text(cairo_t *cairo, const PangoFontDescription *desc, char *buf = vformat_str(fmt, args); va_end(args); if (buf == NULL) { + sway_log(SWAY_ERROR, "Failed to format string"); return; } From 40e1dcd29f1950236e184bd653ee5ede55d8202b Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Sat, 14 Mar 2026 13:21:55 +0100 Subject: [PATCH 15/25] common/pango: add error handling to get_text_size()/render_text() Closes: https://github.com/swaywm/sway/issues/9054 --- common/pango.c | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/common/pango.c b/common/pango.c index 5c7ff3076..2445b7227 100644 --- a/common/pango.c +++ b/common/pango.c @@ -97,12 +97,20 @@ void get_text_size(cairo_t *cairo, const PangoFontDescription *desc, int *width, PangoLayout *layout = get_pango_layout(cairo, desc, buf, scale, markup); pango_cairo_update_layout(cairo, layout); + cairo_status_t status = cairo_status(cairo); + if (status != CAIRO_STATUS_SUCCESS) { + sway_log(SWAY_ERROR, "pango_cairo_update_layout() failed: %s", + cairo_status_to_string(status)); + goto out; + } + pango_layout_get_pixel_size(layout, width, height); if (baseline) { *baseline = pango_layout_get_baseline(layout) / PANGO_SCALE; } - g_object_unref(layout); +out: + g_object_unref(layout); free(buf); } @@ -137,9 +145,18 @@ void render_text(cairo_t *cairo, const PangoFontDescription *desc, cairo_get_font_options(cairo, fo); pango_cairo_context_set_font_options(pango_layout_get_context(layout), fo); cairo_font_options_destroy(fo); - pango_cairo_update_layout(cairo, layout); - pango_cairo_show_layout(cairo, layout); - g_object_unref(layout); + pango_cairo_update_layout(cairo, layout); + cairo_status_t status = cairo_status(cairo); + if (status != CAIRO_STATUS_SUCCESS) { + sway_log(SWAY_ERROR, "pango_cairo_update_layout() failed: %s", + cairo_status_to_string(status)); + goto out; + } + + pango_cairo_show_layout(cairo, layout); + +out: + g_object_unref(layout); free(buf); } From 85a4b19ac44f766561d4b47f587d97575c780ad0 Mon Sep 17 00:00:00 2001 From: llyyr Date: Fri, 20 Mar 2026 01:49:08 +0530 Subject: [PATCH 16/25] build: bump wlroots version Upstream bumped to 0.21.0-dev https://gitlab.freedesktop.org/wlroots/wlroots/-/commit/627da39e76d1f5a3cd18730bd305a0351bb1a121 --- meson.build | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/meson.build b/meson.build index 974a4071b..77d7ff613 100644 --- a/meson.build +++ b/meson.build @@ -39,14 +39,14 @@ if is_freebsd endif # Execute the wlroots subproject, if any -wlroots_version = ['>=0.20.0', '<0.21.0'] +wlroots_version = ['>=0.21.0', '<0.22.0'] subproject( 'wlroots', default_options: ['examples=false'], required: false, version: wlroots_version, ) -wlroots = dependency('wlroots-0.20', version: wlroots_version, fallback: 'wlroots') +wlroots = dependency('wlroots-0.21', version: wlroots_version, fallback: 'wlroots') wlroots_features = { 'xwayland': false, 'libinput_backend': false, From 82227d6103940bade22d7fa1e63256228df37b9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Poisot?= Date: Fri, 20 Mar 2026 08:47:57 +0000 Subject: [PATCH 17/25] common/pango: get_text_size out pointers may be NULL Fixes: 2c2a2ec38055092f368b44d4affefdfa8df17ff9 Closes: https://github.com/swaywm/sway/issues/9082 --- common/pango.c | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/common/pango.c b/common/pango.c index 2445b7227..662b3699e 100644 --- a/common/pango.c +++ b/common/pango.c @@ -84,7 +84,15 @@ PangoLayout *get_pango_layout(cairo_t *cairo, const PangoFontDescription *desc, void get_text_size(cairo_t *cairo, const PangoFontDescription *desc, int *width, int *height, int *baseline, double scale, bool markup, const char *fmt, ...) { - *width = *height = *baseline = 0; + if (width) { + *width = 0; + } + if (height) { + *height = 0; + } + if (baseline) { + *baseline = 0; + } va_list args; va_start(args, fmt); From 8378c560c15906d365a49095deba96d2b560da47 Mon Sep 17 00:00:00 2001 From: Stephane Fontaine Date: Sat, 17 Jan 2026 16:12:24 +0400 Subject: [PATCH 18/25] call disable container in arrange_root --- sway/desktop/transaction.c | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/sway/desktop/transaction.c b/sway/desktop/transaction.c index 325a30226..d912b39bb 100644 --- a/sway/desktop/transaction.c +++ b/sway/desktop/transaction.c @@ -648,15 +648,7 @@ static void arrange_root(struct sway_root *root) { for (int i = 0; i < root->scratchpad->length; i++) { struct sway_container *con = root->scratchpad->items[i]; - // When a container is moved to a scratchpad, it's possible that it - // was moved into a floating container as part of the same transaction. - // In this case, we need to make sure we reparent all the container's - // children so that disabling the container will disable all descendants. - if (!con->view) for (int ii = 0; ii < con->current.children->length; ii++) { - struct sway_container *child = con->current.children->items[ii]; - wlr_scene_node_reparent(&child->scene_tree->node, con->content_tree); - } - + disable_container(con); wlr_scene_node_set_enabled(&con->scene_tree->node, false); } From e4870d84a204213b7dbf4d445ae07e2b9c84c7b1 Mon Sep 17 00:00:00 2001 From: llyyr Date: Sat, 21 Mar 2026 12:01:18 +0530 Subject: [PATCH 19/25] sway_text_node: fix cairo_create without a backing surface This fixes sway not being able to draw text on text nodes. cairo_create(NULL) returns a nil object in an error state rather than NULL, causing the null check to never trigger and passing a broken cairo context to get_text_size, which was fine until 40e1dcd29f19 adding error handling to it and causing pango_cairo_update_layout to fail with a NULL pointer. --- sway/sway_text_node.c | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sway/sway_text_node.c b/sway/sway_text_node.c index 89ece91e2..46ddac93c 100644 --- a/sway/sway_text_node.c +++ b/sway/sway_text_node.c @@ -198,7 +198,10 @@ static void handle_destroy(struct wl_listener *listener, void *data) { static void text_calc_size(struct text_buffer *buffer) { struct sway_text_node *props = &buffer->props; - cairo_t *c = cairo_create(NULL); + cairo_surface_t *recorder = cairo_recording_surface_create( + CAIRO_CONTENT_COLOR_ALPHA, NULL); + cairo_t *c = cairo_create(recorder); + cairo_surface_destroy(recorder); if (!c) { sway_log(SWAY_ERROR, "cairo_t allocation failed"); return; From dea166a27c22fdac11a92d2194c5c2c2be8cedd8 Mon Sep 17 00:00:00 2001 From: llyyr Date: Sat, 21 Mar 2026 12:09:29 +0530 Subject: [PATCH 20/25] common/pango: use pangocairo directly instead of cairo_create(NULL) We never need a cairo context for anything here. Use pango_cairo_font_map_get_default() and pango_font_map_create_context() directly instead of bootstrapping via a nil cairo context. Same as last commit, but just a cosmetic fix in this case since we don't actually use the cairo context for anything --- common/pango.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/common/pango.c b/common/pango.c index 662b3699e..781d7b312 100644 --- a/common/pango.c +++ b/common/pango.c @@ -123,8 +123,8 @@ out: } void get_text_metrics(const PangoFontDescription *description, int *height, int *baseline) { - cairo_t *cairo = cairo_create(NULL); - PangoContext *pango = pango_cairo_create_context(cairo); + PangoFontMap *fontmap = pango_cairo_font_map_get_default(); + PangoContext *pango = pango_font_map_create_context(fontmap); pango_context_set_round_glyph_positions(pango, false); // When passing NULL as a language, pango uses the current locale. PangoFontMetrics *metrics = pango_context_get_metrics(pango, description, NULL); @@ -134,7 +134,6 @@ void get_text_metrics(const PangoFontDescription *description, int *height, int pango_font_metrics_unref(metrics); g_object_unref(pango); - cairo_destroy(cairo); } void render_text(cairo_t *cairo, const PangoFontDescription *desc, From 131045ce554f040224d25738b46524ab2fc23a3a Mon Sep 17 00:00:00 2001 From: llyyr Date: Sat, 21 Mar 2026 12:14:48 +0530 Subject: [PATCH 21/25] sway_text_node: properly check cairo_t status in text_calc_size cairo_create never returns NULL, so the previous null check never triggered. Use cairo_status instead. --- sway/sway_text_node.c | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sway/sway_text_node.c b/sway/sway_text_node.c index 46ddac93c..c4fd3a260 100644 --- a/sway/sway_text_node.c +++ b/sway/sway_text_node.c @@ -202,18 +202,20 @@ static void text_calc_size(struct text_buffer *buffer) { CAIRO_CONTENT_COLOR_ALPHA, NULL); cairo_t *c = cairo_create(recorder); cairo_surface_destroy(recorder); - if (!c) { - sway_log(SWAY_ERROR, "cairo_t allocation failed"); - return; + if (cairo_status(c) != CAIRO_STATUS_SUCCESS) { + sway_log(SWAY_ERROR, "cairo_t allocation failed: %s", + cairo_status_to_string(cairo_status(c))); + goto out; } cairo_set_antialias(c, CAIRO_ANTIALIAS_BEST); get_text_size(c, config->font_description, &props->width, NULL, &props->baseline, 1, props->pango_markup, "%s", buffer->text); - cairo_destroy(c); wlr_scene_buffer_set_dest_size(buffer->buffer_node, get_text_width(props), props->height); +out: + cairo_destroy(c); } struct sway_text_node *sway_text_node_create(struct wlr_scene_tree *parent, From 7ba11d6dee4b4bc4e916157e27159e671b90a446 Mon Sep 17 00:00:00 2001 From: Hugo Osvaldo Barrera Date: Thu, 12 Mar 2026 06:02:42 +0100 Subject: [PATCH 22/25] Make workspace_move_to_output reusable Move workspace_move_to_output out of the command handler, so it can be re-used for ext_workspace_handle_v1::assign. --- include/sway/tree/workspace.h | 3 +++ sway/commands/move.c | 36 ++--------------------------------- sway/tree/workspace.c | 32 +++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 34 deletions(-) diff --git a/include/sway/tree/workspace.h b/include/sway/tree/workspace.h index 27ed649fd..e8b9903e2 100644 --- a/include/sway/tree/workspace.h +++ b/include/sway/tree/workspace.h @@ -157,4 +157,7 @@ size_t workspace_num_sticky_containers(struct sway_workspace *ws); */ void workspace_squash(struct sway_workspace *workspace); +void workspace_move_to_output(struct sway_workspace *workspace, + struct sway_output *output); + #endif diff --git a/sway/commands/move.c b/sway/commands/move.c index 90e8585b4..43fce0d6d 100644 --- a/sway/commands/move.c +++ b/sway/commands/move.c @@ -627,40 +627,6 @@ static struct cmd_results *cmd_move_container(bool no_auto_back_and_forth, return cmd_results_new(CMD_SUCCESS, NULL); } -static void workspace_move_to_output(struct sway_workspace *workspace, - struct sway_output *output) { - if (workspace->output == output) { - return; - } - struct sway_output *old_output = workspace->output; - workspace_detach(workspace); - struct sway_workspace *new_output_old_ws = - output_get_active_workspace(output); - if (!sway_assert(new_output_old_ws, "Expected output to have a workspace")) { - return; - } - - output_add_workspace(output, workspace); - - // If moving the last workspace from the old output, create a new workspace - // on the old output - struct sway_seat *seat = config->handler_context.seat; - if (old_output->workspaces->length == 0) { - char *ws_name = workspace_next_name(old_output->wlr_output->name); - struct sway_workspace *ws = workspace_create(old_output, ws_name); - free(ws_name); - seat_set_raw_focus(seat, &ws->node); - } - - workspace_consider_destroy(new_output_old_ws); - - output_sort_workspaces(output); - struct sway_node *focus = seat_get_focus_inactive(seat, &workspace->node); - seat_set_focus(seat, focus); - workspace_output_raise_priority(workspace, old_output, output); - ipc_event_workspace(NULL, workspace, "move"); -} - static struct cmd_results *cmd_move_workspace(int argc, char **argv) { struct cmd_results *error = NULL; if ((error = checkarg(argc, "move workspace", EXPECTED_AT_LEAST, 1))) { @@ -696,6 +662,8 @@ static struct cmd_results *cmd_move_workspace(int argc, char **argv) { arrange_output(new_output); struct sway_seat *seat = config->handler_context.seat; + struct sway_node *focus = seat_get_focus_inactive(seat, &workspace->node); + seat_set_focus(seat, focus); seat_consider_warp_to_focus(seat); return cmd_results_new(CMD_SUCCESS, NULL); diff --git a/sway/tree/workspace.c b/sway/tree/workspace.c index 49a830e7f..e479cbad4 100644 --- a/sway/tree/workspace.c +++ b/sway/tree/workspace.c @@ -985,3 +985,35 @@ void workspace_squash(struct sway_workspace *workspace) { i += container_squash(child); } } + +void workspace_move_to_output(struct sway_workspace *workspace, + struct sway_output *output) { + if (workspace->output == output) { + return; + } + struct sway_output *old_output = workspace->output; + workspace_detach(workspace); + struct sway_workspace *new_output_old_ws = + output_get_active_workspace(output); + if (!sway_assert(new_output_old_ws, "Expected output to have a workspace")) { + return; + } + + output_add_workspace(output, workspace); + + // If moving the last workspace from the old output, create a new workspace + // on the old output + if (old_output->workspaces->length == 0) { + char *ws_name = workspace_next_name(old_output->wlr_output->name); + struct sway_workspace *ws = workspace_create(old_output, ws_name); + free(ws_name); + struct sway_seat *seat = input_manager_current_seat(); + seat_set_raw_focus(seat, &ws->node); + } + + workspace_consider_destroy(new_output_old_ws); + + output_sort_workspaces(output); + workspace_output_raise_priority(workspace, old_output, output); + ipc_event_workspace(NULL, workspace, "move"); +} From f50f78c0d92e441111c8183636f4c3c589aff8b1 Mon Sep 17 00:00:00 2001 From: Hugo Osvaldo Barrera Date: Thu, 12 Mar 2026 01:09:47 +0100 Subject: [PATCH 23/25] ext-workspace-v1: initial implementation Maintain a 1:1 relationship between workspace groups and outputs, so that moving a workspace across groups effectively moves it across outputs. ext_workspace_handle_v1::id is never emitted; sway has no concept of ids or of stable vs temporary workspaces. Everything is ephemeral to the current session. ext_workspace_handle_v1::coordinates is never emitted; sway does not organise workspaces into any sort of grid. ext_workspace_handle_v1::assign is mostly untested, because no client current implements this. Perhaps it's best to not-advertise the feature for now? Deactivating a workspace is a no-op. This functionality doesn't really align with sway, although it could potentially be implemented to "switch to previous workspace on this output" as a follow-up. Removing a workspace is a no-op. Implements: https://github.com/swaywm/sway/issues/8812 --- include/sway/output.h | 1 + include/sway/server.h | 3 + include/sway/tree/workspace.h | 6 ++ sway/commands/rename.c | 3 + sway/input/seat.c | 10 +++ sway/server.c | 3 + sway/tree/output.c | 8 ++ sway/tree/workspace.c | 143 +++++++++++++++++++++++++++++++++- 8 files changed, 176 insertions(+), 1 deletion(-) diff --git a/include/sway/output.h b/include/sway/output.h index 787527ee7..ae2e50d36 100644 --- a/include/sway/output.h +++ b/include/sway/output.h @@ -63,6 +63,7 @@ struct sway_output { struct wl_listener request_state; struct wlr_color_transform *color_transform; + struct wlr_ext_workspace_group_handle_v1 *ext_workspace_group; struct timespec last_presentation; uint32_t refresh_nsec; diff --git a/include/sway/server.h b/include/sway/server.h index 978f05d0a..318cd39c1 100644 --- a/include/sway/server.h +++ b/include/sway/server.h @@ -127,6 +127,9 @@ struct sway_server { struct wl_listener tearing_control_new_object; struct wl_list tearing_controllers; // sway_tearing_controller::link + struct wlr_ext_workspace_manager_v1 *workspace_manager_v1; + struct wl_listener workspace_manager_v1_commit; + struct wl_list pending_launcher_ctxs; // launcher_ctx::link // The timeout for transactions, after which a transaction is applied diff --git a/include/sway/tree/workspace.h b/include/sway/tree/workspace.h index e8b9903e2..7ce48f173 100644 --- a/include/sway/tree/workspace.h +++ b/include/sway/tree/workspace.h @@ -3,6 +3,7 @@ #include #include +#include #include "sway/config.h" #include "sway/tree/container.h" #include "sway/tree/node.h" @@ -51,6 +52,7 @@ struct sway_workspace { bool urgent; struct sway_workspace_state current; + struct wlr_ext_workspace_handle_v1 *ext_workspace; // Always set. }; struct workspace_config *workspace_find_config(const char *ws_name); @@ -160,4 +162,8 @@ void workspace_squash(struct sway_workspace *workspace); void workspace_move_to_output(struct sway_workspace *workspace, struct sway_output *output); +void sway_ext_workspace_init(void); +void sway_ext_workspace_output_enable(struct sway_output *output); +void sway_ext_workspace_output_disable(struct sway_output *output); + #endif diff --git a/sway/commands/rename.c b/sway/commands/rename.c index 0d36cc21e..63fac05cc 100644 --- a/sway/commands/rename.c +++ b/sway/commands/rename.c @@ -1,6 +1,7 @@ #include #include #include +#include #include "log.h" #include "stringop.h" #include "sway/commands.h" @@ -95,6 +96,8 @@ struct cmd_results *cmd_rename(int argc, char **argv) { free(workspace->name); workspace->name = new_name; + wlr_ext_workspace_handle_v1_set_name(workspace->ext_workspace, workspace->name); + output_sort_workspaces(workspace->output); ipc_event_workspace(NULL, workspace, "rename"); diff --git a/sway/input/seat.c b/sway/input/seat.c index ab31b6746..ebdbd91ed 100644 --- a/sway/input/seat.c +++ b/sway/input/seat.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -1204,6 +1205,15 @@ static void seat_set_workspace_focus(struct sway_seat *seat, struct sway_node *n ipc_event_window(container, "focus"); } + if (last_workspace && last_workspace != new_workspace) { + wlr_ext_workspace_handle_v1_set_active(last_workspace->ext_workspace, + workspace_is_visible(last_workspace)); + } + if (new_workspace) { + wlr_ext_workspace_handle_v1_set_active(new_workspace->ext_workspace, + workspace_is_visible(new_workspace)); + } + // Move sticky containers to new workspace if (new_workspace && new_output_last_ws && new_workspace != new_output_last_ws) { diff --git a/sway/server.c b/sway/server.c index 5d446579f..a49017424 100644 --- a/sway/server.c +++ b/sway/server.c @@ -63,6 +63,7 @@ #include "sway/server.h" #include "sway/input/cursor.h" #include "sway/tree/root.h" +#include "sway/tree/workspace.h" #if WLR_HAS_XWAYLAND #include @@ -377,6 +378,7 @@ bool server_init(struct sway_server *server) { wlr_foreign_toplevel_manager_v1_create(server->wl_display); sway_session_lock_init(); + sway_ext_workspace_init(); #if WLR_HAS_DRM_BACKEND server->drm_lease_manager= @@ -543,6 +545,7 @@ void server_fini(struct sway_server *server) { wl_list_remove(&server->xdg_toplevel_tag_manager_v1_set_tag.link); wl_list_remove(&server->request_set_cursor_shape.link); wl_list_remove(&server->new_foreign_toplevel_capture_request.link); + wl_list_remove(&server->workspace_manager_v1_commit.link); input_manager_finish(server->input); // TODO: free sway-specific resources diff --git a/sway/tree/output.c b/sway/tree/output.c index 90ec9331d..c401d0f1d 100644 --- a/sway/tree/output.c +++ b/sway/tree/output.c @@ -2,6 +2,8 @@ #include #include #include +#include +#include "sway/tree/workspace.h" #include "sway/ipc-server.h" #include "sway/layers.h" #include "sway/output.h" @@ -153,6 +155,7 @@ void output_enable(struct sway_output *output) { output->enabled = true; list_add(root->outputs, output); + sway_ext_workspace_output_enable(output); restore_workspaces(output); struct sway_workspace *ws = NULL; @@ -292,6 +295,7 @@ void output_disable(struct sway_output *output) { destroy_layers(output); output_evacuate(output); + sway_ext_workspace_output_disable(output); } void output_begin_destroy(struct sway_output *output) { @@ -333,6 +337,10 @@ void output_add_workspace(struct sway_output *output, } list_add(output->workspaces, workspace); workspace->output = output; + if (workspace->output && workspace->output->ext_workspace_group) { + wlr_ext_workspace_handle_v1_set_group(workspace->ext_workspace, + workspace->output->ext_workspace_group); + } node_set_dirty(&output->node); node_set_dirty(&workspace->node); } diff --git a/sway/tree/workspace.c b/sway/tree/workspace.c index e479cbad4..d366d19aa 100644 --- a/sway/tree/workspace.c +++ b/sway/tree/workspace.c @@ -3,8 +3,12 @@ #include #include #include +#include #include +#include +#include "log.h" #include "stringop.h" +#include "sway/desktop/transaction.h" #include "sway/input/input-manager.h" #include "sway/input/cursor.h" #include "sway/input/seat.h" @@ -17,9 +21,124 @@ #include "sway/tree/view.h" #include "sway/tree/workspace.h" #include "list.h" -#include "log.h" #include "util.h" +static const uint32_t WORKSPACE_CAPABILITIES = + EXT_WORKSPACE_HANDLE_V1_WORKSPACE_CAPABILITIES_ACTIVATE | + EXT_WORKSPACE_HANDLE_V1_WORKSPACE_CAPABILITIES_ASSIGN; + +static const uint32_t GROUP_CAPABILITIES = + EXT_WORKSPACE_GROUP_HANDLE_V1_GROUP_CAPABILITIES_CREATE_WORKSPACE; + +// Helper to find the output associated with a workspace group. +static struct sway_output *group_to_output( + struct wlr_ext_workspace_group_handle_v1 *group) { + for (int i = 0; i < root->outputs->length; i++) { + struct sway_output *output = root->outputs->items[i]; + if (output->ext_workspace_group == group) { + return output; + } + } + abort(); // unreachable +} + +// Callback for ext-workspace-v1 commit events. +static void handle_commit(struct wl_listener *listener, void *data) { + struct sway_server *server = + wl_container_of(listener, server, workspace_manager_v1_commit); + struct wlr_ext_workspace_v1_commit_event *event = data; + + struct wlr_ext_workspace_v1_request *req, *tmp; + wl_list_for_each_safe(req, tmp, event->requests, link) { + switch (req->type) { + case WLR_EXT_WORKSPACE_V1_REQUEST_ACTIVATE: + if (req->activate.workspace) { + workspace_switch(req->activate.workspace->data); + } + break; + case WLR_EXT_WORKSPACE_V1_REQUEST_CREATE_WORKSPACE:; + struct sway_output *output = group_to_output(req->create_workspace.group); + sway_assert(output, "NULL output given to create_workspace"); + + char *name; + if (req->create_workspace.name) { + if (workspace_by_name(req->create_workspace.name)) { + sway_log(SWAY_ERROR, "Refusing to create workspace with duplicate name."); + break; // Already exists. + } + name = strdup(req->create_workspace.name); + } else { + name = workspace_next_name(output->wlr_output->name); + } + + struct sway_workspace *new_ws = workspace_create(output, name); + if (new_ws) { + workspace_switch(new_ws); + } + free(name); + break; + case WLR_EXT_WORKSPACE_V1_REQUEST_ASSIGN:; + if (!req->assign.workspace || !req->assign.group) break; + + struct sway_workspace *ws = req->assign.workspace->data; + struct sway_output *new_output = group_to_output(req->assign.group); + struct sway_output *old_output = ws->output; + workspace_move_to_output(ws, new_output); + arrange_output(old_output); + arrange_output(new_output); + break; + case WLR_EXT_WORKSPACE_V1_REQUEST_DEACTIVATE: + case WLR_EXT_WORKSPACE_V1_REQUEST_REMOVE: + break; // No-op. + } + } + + transaction_commit_dirty(); +} + +// Initialize ext-workspace. Must be called once at startup. +void sway_ext_workspace_init(void) { + server.workspace_manager_v1 = + wlr_ext_workspace_manager_v1_create(server.wl_display, 1); + if (!server.workspace_manager_v1) { + sway_log(SWAY_ERROR, "Failed to create ext_workspace_manager_v1"); + return; + } + + server.workspace_manager_v1_commit.notify = handle_commit; + wl_signal_add(&server.workspace_manager_v1->events.commit, + &server.workspace_manager_v1_commit); +} + +// Must be called whenever an output is enabled. +void sway_ext_workspace_output_enable(struct sway_output *output) { + if (!output->wlr_output) { + return; + } + + output->ext_workspace_group = + wlr_ext_workspace_group_handle_v1_create( + server.workspace_manager_v1, GROUP_CAPABILITIES); + if (!output->ext_workspace_group) { + sway_log(SWAY_ERROR, "Failed to create workspace group for output '%s'", + output->wlr_output->name); + return; + } + + wlr_ext_workspace_group_handle_v1_output_enter( + output->ext_workspace_group, output->wlr_output); +} + +// Must be called whenever an output is disabled. +void sway_ext_workspace_output_disable(struct sway_output *output) { + if (!output->ext_workspace_group) { + return; + } + + wlr_ext_workspace_group_handle_v1_destroy(output->ext_workspace_group); + output->ext_workspace_group = NULL; +} + struct workspace_config *workspace_find_config(const char *ws_name) { for (int i = 0; i < config->workspace_configs->length; ++i) { struct workspace_config *wsc = config->workspace_configs->items[i]; @@ -70,6 +189,16 @@ struct sway_workspace *workspace_create(struct sway_output *output, sway_log(SWAY_ERROR, "Unable to allocate sway_workspace"); return NULL; } + + ws->ext_workspace = wlr_ext_workspace_handle_v1_create( + server.workspace_manager_v1, NULL, WORKSPACE_CAPABILITIES); + if (!ws->ext_workspace) { + sway_log(SWAY_ERROR, "Failed to create ext_workspace for '%s'", name); + free(ws); + return NULL; + } + ws->ext_workspace->data = ws; + node_init(&ws->node, N_WORKSPACE, ws); bool failed = false; @@ -79,6 +208,7 @@ struct sway_workspace *workspace_create(struct sway_output *output, if (failed) { wlr_scene_node_destroy(&ws->layers.tiling->node); wlr_scene_node_destroy(&ws->layers.fullscreen->node); + wlr_ext_workspace_handle_v1_destroy(ws->ext_workspace); free(ws); return NULL; } @@ -127,6 +257,13 @@ struct sway_workspace *workspace_create(struct sway_output *output, output_add_workspace(output, ws); output_sort_workspaces(output); + wlr_ext_workspace_handle_v1_set_name(ws->ext_workspace, ws->name); + if (ws->output && ws->output->ext_workspace_group) { + wlr_ext_workspace_handle_v1_set_group(ws->ext_workspace, + ws->output->ext_workspace_group); + } + wlr_ext_workspace_handle_v1_set_active(ws->ext_workspace, + workspace_is_visible(ws)); ipc_event_workspace(NULL, ws, "init"); wl_signal_emit_mutable(&root->events.new_node, &ws->node); @@ -163,6 +300,9 @@ void workspace_begin_destroy(struct sway_workspace *workspace) { ipc_event_workspace(NULL, workspace, "empty"); // intentional wl_signal_emit_mutable(&workspace->node.events.destroy, &workspace->node); + wlr_ext_workspace_handle_v1_destroy(workspace->ext_workspace); + workspace->ext_workspace = NULL; + if (workspace->output) { workspace_detach(workspace); } @@ -687,6 +827,7 @@ void workspace_detect_urgent(struct sway_workspace *workspace) { if (workspace->urgent != new_urgent) { workspace->urgent = new_urgent; + wlr_ext_workspace_handle_v1_set_urgent(workspace->ext_workspace, workspace->urgent); ipc_event_workspace(NULL, workspace, "urgent"); } } From ec7e0186e04528c483242202e4431dec79a8498b Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Thu, 26 Mar 2026 16:30:37 +0100 Subject: [PATCH 24/25] build: bump version to 1.13-dev --- meson.build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meson.build b/meson.build index 77d7ff613..17d65c334 100644 --- a/meson.build +++ b/meson.build @@ -1,7 +1,7 @@ project( 'sway', 'c', - version: '1.12-dev', + version: '1.13-dev', license: 'MIT', meson_version: '>=1.3', default_options: [ From 909a2ddb5fff528e735341529a028d2ef21836db Mon Sep 17 00:00:00 2001 From: Hugo Osvaldo Barrera Date: Mon, 23 Mar 2026 22:18:24 +0100 Subject: [PATCH 25/25] Centre fullscreen surfaces smaller than output Sway renders fullscreen surfaces smaller than the output left-aligned. From xdg-shell: > If the surface doesn't cover the whole output, the compositor will > position the surface in the center of the output and compensate with > with border fill covering the rest of the output. The content of the > border fill is undefined, but should be assumed to be in some way that > attempts to blend into the surrounding area (e.g. solid black). Render surfaces smaller than the output centred. Can be tested easily with: weston-simple-egl -f -r Fixes: https://github.com/swaywm/sway/issues/8845 --- sway/tree/view.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sway/tree/view.c b/sway/tree/view.c index eab2a5e2b..6dde3f63c 100644 --- a/sway/tree/view.c +++ b/sway/tree/view.c @@ -988,7 +988,7 @@ void view_center_and_clip_surface(struct sway_view *view) { bool clip_to_geometry = true; - if (container_is_floating(con)) { + if (container_is_floating(con) || con->pending.fullscreen_mode != FULLSCREEN_NONE) { // We always center the current coordinates rather than the next, as the // geometry immediately affects the currently active rendering. int x = (int) fmax(0, (con->current.content_width - view->geometry.width) / 2);