From d9ddaba762b5c2ae49101b6690c96d3df703bed9 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Sat, 26 Sep 2026 19:31:15 +0200 Subject: [PATCH 01/14] also make clients tiled on map While making them tiled on the first commit was needed to fix Firefox starting with rounded corners, if you don't set them tiled again on map resizing mpv after starting it with a rule that makes it floating behaves wrong. Fixes d1ebf3c. Fixes https://codeberg.org/dwl/dwl/issues/1247. --- dwl.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dwl.c b/dwl.c index ca97c30..cca1ea4 100644 --- a/dwl.c +++ b/dwl.c @@ -1924,6 +1924,8 @@ mapnotify(struct wl_listener *listener, void *data) } /* Initialize client geometry with room for border */ + client_set_tiled(c, WLR_EDGE_TOP | WLR_EDGE_BOTTOM | WLR_EDGE_LEFT | + WLR_EDGE_RIGHT); c->geom.width += 2 * c->bw; c->geom.height += 2 * c->bw; From fccce9744819289ef392d64932a7b2d63d73d487 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Mon, 28 Sep 2026 12:19:24 +0200 Subject: [PATCH 02/14] remove obsolete comment No longer accurate since a514a92. --- dwl.c | 1 - 1 file changed, 1 deletion(-) diff --git a/dwl.c b/dwl.c index cca1ea4..3e94fa2 100644 --- a/dwl.c +++ b/dwl.c @@ -642,7 +642,6 @@ arrangelayers(Monitor *m) l->layer_surface->current.keyboard_interactive != ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE || !l->mapped) continue; - /* Deactivate the focused client. */ focusclient(NULL, l, 0); return; } From d29259c26ab90caa657ed1cbeab03c2039ef5fce Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Mon, 28 Sep 2026 13:18:49 +0200 Subject: [PATCH 03/14] refactor focusclient() --- dwl.c | 28 ++++++++++------------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/dwl.c b/dwl.c index 3e94fa2..e07a269 100644 --- a/dwl.c +++ b/dwl.c @@ -1517,7 +1517,7 @@ dirtomon(enum wlr_direction dir) void focusclient(Client *c, LayerSurface *l, int lift) { - struct wlr_surface *old = seat->keyboard_state.focused_surface; + struct wlr_surface *surface, *old = seat->keyboard_state.focused_surface; int unused_lx, unused_ly, old_client_type; Client *old_c = NULL; LayerSurface *old_l = NULL; @@ -1573,31 +1573,23 @@ focusclient(Client *c, LayerSurface *l, int lift) } printstatus(); - if (l) { - /* Focus a layer surface and let the input method follow it */ - input_method_relay_set_focus(input_method_relay, l->layer_surface->surface); - client_notify_enter(l->layer_surface->surface, wlr_seat_get_keyboard(seat)); - return; - } - - if (!c) { + if (!c && !l) { /* With no client, all we have left is to clear focus */ input_method_relay_set_focus(input_method_relay, NULL); wlr_seat_keyboard_notify_clear_focus(seat); return; } - /* Change cursor surface */ + surface = c ? client_surface(c) : l->layer_surface->surface; + input_method_relay_set_focus(input_method_relay, surface); + client_notify_enter(surface, wlr_seat_get_keyboard(seat)); + + if (l) + return; + + client_activate_surface(surface, 1); motionnotify(0, NULL, 0, 0, 0, 0); - input_method_relay_set_focus(input_method_relay, client_surface(c)); - - /* Have a client, so focus its top-level wlr_surface */ - client_notify_enter(client_surface(c), wlr_seat_get_keyboard(seat)); - - /* Activate the new client */ - client_activate_surface(client_surface(c), 1); - if (!client_is_unmanaged(c)) focused_client = c; } From 4fd683cc84d670d97f6f7020001410f2fb3daa6c Mon Sep 17 00:00:00 2001 From: save196 Date: Tue, 15 Sep 2026 18:11:48 +0000 Subject: [PATCH 04/14] fix X11 clients shifting by the border width on map c->geom includes the border, but client_get_geometry() returns the content position. Subtract the border, clamped to the layout edges. --- dwl.c | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/dwl.c b/dwl.c index e07a269..26fb3a0 100644 --- a/dwl.c +++ b/dwl.c @@ -1919,6 +1919,11 @@ mapnotify(struct wl_listener *listener, void *data) WLR_EDGE_RIGHT); c->geom.width += 2 * c->bw; c->geom.height += 2 * c->bw; + if (client_is_x11(c)) { + int x = c->geom.x - (int)c->bw, y = c->geom.y - (int)c->bw; + c->geom.x = c->geom.x < sgeom.x ? x : MAX(x, sgeom.x); + c->geom.y = c->geom.y < sgeom.y ? y : MAX(y, sgeom.y); + } /* Insert this client into client lists. */ wl_list_insert(&clients, &c->link); From e2038450f2b7746d5c2600b0b04bc7717a29c986 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 29 Sep 2026 13:53:42 +0200 Subject: [PATCH 05/14] fix popup positioning Related: https://codeberg.org/dwl/dwl/pulls/1244 Co-authored-by: DanD --- dwl.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dwl.c b/dwl.c index 26fb3a0..216deb5 100644 --- a/dwl.c +++ b/dwl.c @@ -1014,8 +1014,8 @@ commitpopup(struct wl_listener *listener, void *data) return; } box = type == LayerShell ? l->mon->m : c->mon->w; - box.x -= (type == LayerShell ? l->scene->node.x : c->geom.x); - box.y -= (type == LayerShell ? l->scene->node.y : c->geom.y); + box.x -= (type == LayerShell ? l->scene->node.x : c->geom.x - c->surface.xdg->geometry.x); + box.y -= (type == LayerShell ? l->scene->node.y : c->geom.y - c->surface.xdg->geometry.y); wlr_xdg_popup_unconstrain_from_box(popup, &box); wl_list_remove(&listener->link); free(listener); From 90e7c722e9c1299db2f26c393fb43b7ab12c6501 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Mon, 19 Oct 2026 21:17:43 +0200 Subject: [PATCH 06/14] remove client_is_rendered_on_mon() and client_is_stopped() Unused since 70c33c0. --- client.h | 42 ------------------------------------------ 1 file changed, 42 deletions(-) diff --git a/client.h b/client.h index eb1bb47..aa26467 100644 --- a/client.h +++ b/client.h @@ -258,48 +258,6 @@ client_is_float_type(Client *c) (min.width == max.width || min.height == max.height)); } -static inline int -client_is_rendered_on_mon(Client *c, Monitor *m) -{ - /* This is needed for when you don't want to check formal assignment, - * but rather actual displaying of the pixels. - * Usually VISIBLEON suffices and is also faster. */ - struct wlr_surface_output *s; - int unused_lx, unused_ly; - if (!wlr_scene_node_coords(&c->scene->node, &unused_lx, &unused_ly)) - return 0; - wl_list_for_each(s, &client_surface(c)->current_outputs, link) - if (s->output == m->wlr_output) - return 1; - return 0; -} - -static inline int -client_is_stopped(Client *c) -{ - int pid; - siginfo_t in = {0}; -#ifdef XWAYLAND - if (client_is_x11(c)) - return 0; -#endif - - wl_client_get_credentials(c->surface.xdg->client->client, &pid, NULL, NULL); - if (waitid(P_PID, pid, &in, WNOHANG|WCONTINUED|WSTOPPED|WNOWAIT) < 0) { - /* This process is not our child process, while is very unlikely that - * it is stopped, in order to do not skip frames, assume that it is. */ - if (errno == ECHILD) - return 1; - } else if (in.si_pid) { - if (in.si_code == CLD_STOPPED || in.si_code == CLD_TRAPPED) - return 1; - if (in.si_code == CLD_CONTINUED) - return 0; - } - - return 0; -} - static inline int client_is_unmanaged(Client *c) { From f08d8b3913cf316bb54ddaa7bc5bf664e99513fe Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Mon, 19 Oct 2026 22:57:02 +0200 Subject: [PATCH 07/14] fix segfault when XWayland fails to initialize Fixes https://codeberg.org/dwl/dwl/issues/1202 --- dwl.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/dwl.c b/dwl.c index 216deb5..738639b 100644 --- a/dwl.c +++ b/dwl.c @@ -876,8 +876,10 @@ cleanuplisteners(void) wl_list_remove(&new_session_lock.link); wl_list_remove(&new_foreign_toplevel_capture_request.link); #ifdef XWAYLAND - wl_list_remove(&new_xwayland_surface.link); - wl_list_remove(&xwayland_ready.link); + if (xwayland) { + wl_list_remove(&new_xwayland_surface.link); + wl_list_remove(&xwayland_ready.link); + } #endif } From d3fa32993cdf2267a78e555d0ca5ecb196a95135 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Mon, 19 Oct 2026 22:50:33 +0200 Subject: [PATCH 08/14] fix flicker when arranging tiled windows Fixes https://codeberg.org/dwl/dwl/issues/552 Fixes https://codeberg.org/dwl/dwl/issues/705 Also, floating children of fullscreen windows in floating layout now go in LyrFS instead of LyrTile. --- client.h | 27 ++++- dwl.c | 305 ++++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 280 insertions(+), 52 deletions(-) diff --git a/client.h b/client.h index aa26467..ebe665b 100644 --- a/client.h +++ b/client.h @@ -324,19 +324,36 @@ client_set_scale(struct wlr_surface *s, float scale) wlr_surface_set_preferred_buffer_scale(s, (int32_t)ceilf(scale)); } -static inline void +/* Returns nonzero if the client has to commit a buffer with the new size */ +static inline uint32_t client_set_size(Client *c, uint32_t width, uint32_t height) { #ifdef XWAYLAND if (client_is_x11(c)) { wlr_xwayland_surface_configure(c->surface.xwayland, c->geom.x + c->bw, c->geom.y + c->bw, width, height); - return; + return (int)width != client_surface(c)->current.width + || (int)height != client_surface(c)->current.height; } #endif - if ((int32_t)width != c->surface.xdg->toplevel->current.width - || (int32_t)height != c->surface.xdg->toplevel->current.height) - wlr_xdg_toplevel_set_size(c->surface.xdg->toplevel, (int32_t)width, (int32_t)height); + if ((int32_t)width == c->surface.xdg->toplevel->scheduled.width + && (int32_t)height == c->surface.xdg->toplevel->scheduled.height) + return 0; + return wlr_xdg_toplevel_set_size(c->surface.xdg->toplevel, (int32_t)width, (int32_t)height); +} + +static inline void +client_send_frame_done_iterator(struct wlr_surface *s, int sx, int sy, void *data) +{ + wlr_surface_send_frame_done(s, data); +} + +static inline void +client_send_frame_done(Client *c) +{ + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + wlr_surface_for_each_surface(client_surface(c), client_send_frame_done_iterator, &now); } static inline void diff --git a/dwl.c b/dwl.c index 738639b..7d8fc1e 100644 --- a/dwl.c +++ b/dwl.c @@ -126,6 +126,9 @@ typedef struct { struct wl_list flink; struct wlr_box geom; /* layout-relative, includes border */ struct wlr_box prev; /* layout-relative, includes border */ + struct wlr_box current; /* geom currently shown in the scene graph */ + struct wlr_scene_node *snapshot; /* content shown while resizing */ + uint32_t resize_serial; /* nonzero while waiting for a resized buffer */ struct wlr_box bounds; /* only width and height are used */ union { struct wlr_xdg_surface *xdg; @@ -262,6 +265,7 @@ typedef struct { /* function declarations */ static void applybounds(Client *c, struct wlr_box *bbox); +static void applygeom(Client *c); static void applyrules(Client *c); static void arrange(Monitor *m); static void arrangelayer(Monitor *m, struct wl_list *list, @@ -352,6 +356,8 @@ static void setmon(Client *c, Monitor *m, uint32_t newtags); static void setpsel(struct wl_listener *listener, void *data); static void setsel(struct wl_listener *listener, void *data); static void setup(void); +static struct wlr_scene_node *snapshot(struct wlr_scene_node *node, + struct wlr_scene_tree *parent, void *data); static void spawn(const Arg *arg); static void startdrag(struct wl_listener *listener, void *data); static void tag(const Arg *arg); @@ -361,6 +367,11 @@ static void togglefloating(const Arg *arg); static void togglefullscreen(const Arg *arg); static void toggletag(const Arg *arg); static void toggleview(const Arg *arg); +static void transactionapply(void); +static void transactioncheck(void *data); +static void transactioncommit(Client *c, int ready); +static void transactionschedule(void); +static int transactiontimeout(void *data); static void unlocksession(struct wl_listener *listener, void *data); static void unmaplayersurfacenotify(struct wl_listener *listener, void *data); static void unmapnotify(struct wl_listener *listener, void *data); @@ -432,6 +443,14 @@ static struct wl_list mons; static Monitor *selmon; static int consumed[KEY_MAX + 1]; +/* Transactions: the scene graph is updated only once all visible clients have + * committed buffers with their new size, or after TRANSACTION_TIMEOUT ms */ +#define TRANSACTION_TIMEOUT 100 +static struct wl_event_source *transaction_idle; +static struct wl_event_source *transaction_timer; +static int transaction_waiting; +static struct wl_array ghosts; /* struct wlr_scene_node *, unmapped clients */ + /* global event handlers */ static struct wl_listener cursor_axis = {.notify = axisnotify}; static struct wl_listener cursor_button = {.notify = buttonpress}; @@ -466,6 +485,7 @@ static struct wl_listener new_foreign_toplevel_capture_request = {.notify = capt #ifdef XWAYLAND static void activatex11(struct wl_listener *listener, void *data); static void associatex11(struct wl_listener *listener, void *data); +static void commitx11(struct wl_listener *listener, void *data); static void configurex11(struct wl_listener *listener, void *data); static void createnotifyx11(struct wl_listener *listener, void *data); static void dissociatex11(struct wl_listener *listener, void *data); @@ -516,6 +536,41 @@ applybounds(Client *c, struct wlr_box *bbox) c->geom.y = bbox->y; } +void +applygeom(Client *c) +{ + struct wlr_box clip; + Client *p = client_get_parent(c); + + /* We move all floating clients to LyrTile while in floating layout + * to avoid "real" floating clients be always on top */ + wlr_scene_node_reparent(&c->scene->node, layers[c->isfullscreen + || (p && p->isfullscreen) ? LyrFS + : c->isfloating && c->mon->lt[c->mon->sellt]->arrange ? LyrFloat + : LyrTile]); + + c->current = c->geom; + if (c->snapshot) { + wlr_scene_node_destroy(c->snapshot); + c->snapshot = NULL; + wlr_scene_node_set_enabled(&c->scene_surface->node, 1); + } + + /* Update scene-graph, including borders */ + wlr_scene_node_set_position(&c->scene->node, c->geom.x, c->geom.y); + wlr_scene_node_set_position(&c->scene_surface->node, c->bw, c->bw); + wlr_scene_rect_set_size(c->border[0], c->geom.width, c->bw); + wlr_scene_rect_set_size(c->border[1], c->geom.width, c->bw); + wlr_scene_rect_set_size(c->border[2], c->bw, c->geom.height - 2 * c->bw); + wlr_scene_rect_set_size(c->border[3], c->bw, c->geom.height - 2 * c->bw); + wlr_scene_node_set_position(&c->border[1]->node, 0, c->geom.height - c->bw); + wlr_scene_node_set_position(&c->border[2]->node, 0, c->bw); + wlr_scene_node_set_position(&c->border[3]->node, c->geom.width - c->bw, c->bw); + + client_get_clip(c, &clip); + wlr_scene_subsurface_tree_set_clip(&c->scene_surface->node, &clip); +} + void applyrules(Client *c) { @@ -555,37 +610,18 @@ arrange(Monitor *m) return; wl_list_for_each(c, &clients, link) { - if (c->mon == m) { - wlr_scene_node_set_enabled(&c->scene->node, VISIBLEON(c, m)); + if (c->mon == m) client_set_suspended(c, !VISIBLEON(c, m) && (!c->image_capture_source || wl_list_empty(&c->image_capture_source->resources))); - } } - wlr_scene_node_set_enabled(&m->fullscreen_bg->node, - (c = focustop(m)) && c->isfullscreen); - strncpy(m->ltsymbol, m->lt[m->sellt]->symbol, LENGTH(m->ltsymbol)); - /* We move all clients (except fullscreen and unmanaged) to LyrTile while - * in floating layout to avoid "real" floating clients be always on top */ - wl_list_for_each(c, &clients, link) { - if (c->mon != m || c->scene->node.parent == layers[LyrFS]) - continue; - - wlr_scene_node_reparent(&c->scene->node, - (!m->lt[m->sellt]->arrange && c->isfloating) - ? layers[LyrTile] - : (m->lt[m->sellt]->arrange && c->isfloating) - ? layers[LyrFloat] - : c->scene->node.parent); - } - if (m->lt[m->sellt]->arrange) m->lt[m->sellt]->arrange(m); - motionnotify(0, NULL, 0, 0, 0, 0); - checkidleinhibitor(NULL); + + transactionschedule(); } void @@ -809,6 +845,11 @@ cleanup(void) * Destroy it until it's fixed on the wlroots side */ wlr_backend_destroy(backend); + if (transaction_idle) + wl_event_source_remove(transaction_idle); + wl_event_source_remove(transaction_timer); + wl_array_release(&ghosts); + wl_display_destroy(dpy); /* Destroy after the wayland display (when the monitors are already destroyed) to avoid destroying them with an invalid scene output. */ @@ -990,6 +1031,8 @@ commitnotify(struct wl_listener *listener, void *data) return; } + transactioncommit(c, c->resize_serial && c->surface.xdg->current.configure_serial + >= c->resize_serial); resize(c, c->geom, (c->isfloating && !c->isfullscreen)); } @@ -2385,7 +2428,7 @@ void resize(Client *c, struct wlr_box geo, int interact) { struct wlr_box *bbox; - struct wlr_box clip; + uint32_t serial; if (!c->mon || !client_surface(c)->mapped) return; @@ -2396,20 +2439,27 @@ resize(Client *c, struct wlr_box geo, int interact) c->geom = geo; applybounds(c, bbox); - /* Update scene-graph, including borders */ - wlr_scene_node_set_position(&c->scene->node, c->geom.x, c->geom.y); - wlr_scene_node_set_position(&c->scene_surface->node, c->bw, c->bw); - wlr_scene_rect_set_size(c->border[0], c->geom.width, c->bw); - wlr_scene_rect_set_size(c->border[1], c->geom.width, c->bw); - wlr_scene_rect_set_size(c->border[2], c->bw, c->geom.height - 2 * c->bw); - wlr_scene_rect_set_size(c->border[3], c->bw, c->geom.height - 2 * c->bw); - wlr_scene_node_set_position(&c->border[1]->node, 0, c->geom.height - c->bw); - wlr_scene_node_set_position(&c->border[2]->node, 0, c->bw); - wlr_scene_node_set_position(&c->border[3]->node, c->geom.width - c->bw, c->bw); + if ((serial = client_set_size(c, c->geom.width - 2 * c->bw, + c->geom.height - 2 * c->bw))) + c->resize_serial = serial; - client_set_size(c, c->geom.width - 2 * c->bw, c->geom.height - 2 * c->bw); - client_get_clip(c, &clip); - wlr_scene_subsurface_tree_set_clip(&c->scene_surface->node, &clip); + if (interact || (!c->resize_serial && wlr_box_equal(&c->geom, &c->current))) { + c->resize_serial = 0; + applygeom(c); + return; + } + + /* Until the transaction is applied, keep showing the current content + * instead of buffers with the new size at the old position. */ + if (c->resize_serial) { + if (!c->snapshot && c->scene->node.enabled + && (c->snapshot = snapshot(&c->scene_surface->node, c->scene, c))) + wlr_scene_node_set_enabled(&c->scene_surface->node, 0); + /* The scene graph doesn't send frame done events to hidden + * surfaces, but the client may be waiting for one to redraw */ + client_send_frame_done(c); + } + transactionschedule(); } void @@ -2510,14 +2560,10 @@ setcursorshape(struct wl_listener *listener, void *data) void setfloating(Client *c, int floating) { - Client *p = client_get_parent(c); c->isfloating = floating; /* If in floating layout do not change the client's layer */ if (!c->mon || !client_surface(c)->mapped || !c->mon->lt[c->mon->sellt]->arrange) return; - wlr_scene_node_reparent(&c->scene->node, layers[c->isfullscreen || - (p && p->isfullscreen) ? LyrFS - : c->isfloating ? LyrFloat : LyrTile]); arrange(c->mon); printstatus(); } @@ -2530,8 +2576,6 @@ setfullscreen(Client *c, int fullscreen) return; c->bw = fullscreen ? 0 : borderpx; client_set_fullscreen(c, fullscreen); - wlr_scene_node_reparent(&c->scene->node, layers[c->isfullscreen - ? LyrFS : c->isfloating ? LyrFloat : LyrTile]); if (fullscreen) { c->prev = c->geom; @@ -2735,6 +2779,9 @@ setup(void) wl_list_init(&clients); wl_list_init(&fstack); + transaction_timer = wl_event_loop_add_timer(event_loop, transactiontimeout, NULL); + wl_array_init(&ghosts); + xdg_shell = wlr_xdg_shell_create(dpy, 6); wl_signal_add(&xdg_shell->events.new_toplevel, &new_xdg_toplevel); wl_signal_add(&xdg_shell->events.new_popup, &new_xdg_popup); @@ -2856,6 +2903,57 @@ setup(void) #endif } +struct wlr_scene_node * +snapshot(struct wlr_scene_node *node, struct wlr_scene_tree *parent, void *data) +{ + /* Copy the visible part of a scene graph subtree, keeping references to + * the buffers so that they can be shown after the client changes them */ + struct wlr_scene_node *child, *copy; + struct wlr_scene_tree *tree; + struct wlr_scene_rect *rect; + struct wlr_scene_buffer *src, *buffer; + + if (!node->enabled) + return NULL; + + switch (node->type) { + case WLR_SCENE_NODE_TREE: + tree = wlr_scene_tree_create(parent); + wl_list_for_each(child, &wlr_scene_tree_from_node(node)->children, link) + snapshot(child, tree, data); + copy = &tree->node; + break; + case WLR_SCENE_NODE_RECT: + rect = wlr_scene_rect_from_node(node); + copy = &wlr_scene_rect_create(parent, rect->width, rect->height, rect->color)->node; + break; + case WLR_SCENE_NODE_BUFFER: + src = wlr_scene_buffer_from_node(node); + buffer = wlr_scene_buffer_create(parent, NULL); + wlr_scene_buffer_set_dest_size(buffer, src->dst_width, src->dst_height); + wlr_scene_buffer_set_opaque_region(buffer, &src->opaque_region); + wlr_scene_buffer_set_opacity(buffer, src->opacity); + wlr_scene_buffer_set_filter_mode(buffer, src->filter_mode); + wlr_scene_buffer_set_transfer_function(buffer, src->transfer_function); + wlr_scene_buffer_set_primaries(buffer, src->primaries); + wlr_scene_buffer_set_color_encoding(buffer, src->color_encoding); + wlr_scene_buffer_set_color_range(buffer, src->color_range); + wlr_scene_buffer_set_source_box(buffer, &src->src_box); + wlr_scene_buffer_set_transform(buffer, src->transform); + wlr_scene_buffer_set_buffer(buffer, src->buffer); + copy = &buffer->node; + break; + default: + return NULL; + } + + copy->data = data; + wlr_scene_node_set_position(copy, node->x, node->y); + if (node->parent == parent) + wlr_scene_node_place_above(copy, node); + return copy; +} + void spawn(const Arg *arg) { @@ -2977,6 +3075,88 @@ toggleview(const Arg *arg) printstatus(); } +void +transactionapply(void) +{ + struct wlr_scene_node **ghost; + Client *c; + Monitor *m; + + wl_event_source_timer_update(transaction_timer, 0); + transaction_waiting = 0; + + wl_array_for_each(ghost, &ghosts) + wlr_scene_node_destroy(*ghost); + ghosts.size = 0; + + wl_list_for_each(c, &clients, link) { + c->resize_serial = 0; + if (!c->mon) + continue; + if (c->mon->wlr_output->enabled) + wlr_scene_node_set_enabled(&c->scene->node, VISIBLEON(c, c->mon)); + applygeom(c); + } + + wl_list_for_each(m, &mons, link) { + if (m->wlr_output->enabled) + wlr_scene_node_set_enabled(&m->fullscreen_bg->node, + (c = focustop(m)) && c->isfullscreen); + } + + motionnotify(0, NULL, 0, 0, 0, 0); + checkidleinhibitor(NULL); +} + +void +transactioncheck(void *data) +{ + Client *c; + + transaction_idle = NULL; + + /* Hidden clients don't have to redraw before the layout is shown */ + wl_list_for_each(c, &clients, link) { + if (c->resize_serial && VISIBLEON(c, c->mon) && c->mon->wlr_output->enabled) { + if (!transaction_waiting) { + /* Don't restart the timer on updates of the pending + * layout, so the delay is bounded */ + wl_event_source_timer_update(transaction_timer, TRANSACTION_TIMEOUT); + transaction_waiting = 1; + } + return; + } + } + + transactionapply(); +} + +void +transactioncommit(Client *c, int ready) +{ + if (c->resize_serial && ready) { + c->resize_serial = 0; + transactionschedule(); + } else if (c->snapshot) { + /* This buffer is not shown, let the client draw again */ + client_send_frame_done(c); + } +} + +void +transactionschedule(void) +{ + if (!transaction_idle) + transaction_idle = wl_event_loop_add_idle(event_loop, transactioncheck, NULL); +} + +int +transactiontimeout(void *data) +{ + transactionapply(); + return 0; +} + void unlocksession(struct wl_listener *listener, void *data) { @@ -3003,6 +3183,7 @@ unmapnotify(struct wl_listener *listener, void *data) { /* Called when the surface is unmapped, and should no longer be shown. */ Client *c = wl_container_of(listener, c, unmap); + struct wlr_scene_node *ghost, **slot; if (c == grabc) { cursor_mode = CurNormal; grabc = NULL; @@ -3011,6 +3192,15 @@ unmapnotify(struct wl_listener *listener, void *data) focused_client = NULL; if (!client_is_unmanaged(c)) { + /* Keep showing the client until the transaction rearranging the + * remaining ones is applied, to not show a gap */ + if ((ghost = snapshot(&c->scene->node, c->scene->node.parent, NULL))) { + if ((slot = wl_array_add(&ghosts, sizeof(ghost)))) + *slot = ghost; + else + wlr_scene_node_destroy(ghost); + transactionschedule(); + } wl_list_remove(&c->link); setmon(c, NULL, 0); wl_list_remove(&c->flink); @@ -3034,6 +3224,9 @@ unmapnotify(struct wl_listener *listener, void *data) c->image_capture_source = NULL; wlr_scene_node_destroy(&c->scene->node); client_surface(c)->data = NULL; + c->snapshot = NULL; + c->resize_serial = 0; + c->current = (struct wlr_box){0}; printstatus(); motionnotify(0, NULL, 0, 0, 0, 0); } @@ -3233,20 +3426,26 @@ xytonode(double x, double y, struct wlr_surface **psurface, Client **pc, LayerSurface **pl, double *nx, double *ny) { struct wlr_scene_node *node; + struct wlr_scene_surface *scene_surface; struct wlr_surface *surface = NULL; + Client *c = NULL; int layer; - for (layer = NUM_LAYERS - 1; !surface && layer >= (locked ? LyrBlock : 0); layer--) { + for (layer = NUM_LAYERS - 1; !surface && !c && layer >= (locked ? LyrBlock : 0); layer--) { if (layer == LyrIMPopup || - !(node = wlr_scene_node_at(&layers[layer]->node, x, y, nx, ny))) + !(node = wlr_scene_node_at(&layers[layer]->node, x, y, nx, ny)) + || node->type != WLR_SCENE_NODE_BUFFER) continue; - if (node->type == WLR_SCENE_NODE_BUFFER) - surface = wlr_scene_surface_try_from_buffer( - wlr_scene_buffer_from_node(node))->surface; + if ((scene_surface = wlr_scene_surface_try_from_buffer( + wlr_scene_buffer_from_node(node)))) + surface = scene_surface->surface; + else /* snapshot of a client during a transaction */ + c = node->data; } if (psurface) *psurface = surface; + if (pc) *pc = c; toplevel_from_wlr_surface(surface, pc, pl); } @@ -3299,10 +3498,21 @@ associatex11(struct wl_listener *listener, void *data) { Client *c = wl_container_of(listener, c, associate); + LISTEN(&client_surface(c)->events.commit, &c->commit, commitx11); LISTEN(&client_surface(c)->events.map, &c->map, mapnotify); LISTEN(&client_surface(c)->events.unmap, &c->unmap, unmapnotify); } +void +commitx11(struct wl_listener *listener, void *data) +{ + Client *c = wl_container_of(listener, c, commit); + struct wlr_surface *surface = client_surface(c); + + transactioncommit(c, surface->current.width == c->geom.width - 2 * (int)c->bw + && surface->current.height == c->geom.height - 2 * (int)c->bw); +} + void configurex11(struct wl_listener *listener, void *data) { @@ -3351,6 +3561,7 @@ void dissociatex11(struct wl_listener *listener, void *data) { Client *c = wl_container_of(listener, c, dissociate); + wl_list_remove(&c->commit.link); wl_list_remove(&c->map.link); wl_list_remove(&c->unmap.link); } From 4ff9db2c401d8b47d4c6678ecd48a4231397b390 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 20 Oct 2026 21:26:05 +0200 Subject: [PATCH 09/14] Revert "remove zero flickering lie" This reverts commit b1c101515a579b91bd36a3e98c4642255bbb599d. Fixed by d3fa329. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fd72785..b8cddb4 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,7 @@ given the base on which it is built. Implemented default features are: - Support screen lockers via ext-session-lock-v1 protocol - Various Wayland protocols - XWayland support as provided by wlroots (can be enabled in `config.mk`) +- Zero flickering - Wayland users naturally expect that "every frame is perfect" - Layer shell popups (used by Waybar) - Damage tracking provided by scenegraph API From afbafd53ab6cd86460c1723dc2aebab7ca7d9abb Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 20 Oct 2026 22:50:32 +0200 Subject: [PATCH 10/14] don't crash when initializing more monitors than supported Fixes https://codeberg.org/dwl/dwl/issues/726 --- dwl.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dwl.c b/dwl.c index 7d8fc1e..22bbc5c 100644 --- a/dwl.c +++ b/dwl.c @@ -1262,6 +1262,10 @@ createmon(struct wl_listener *listener, void *data) * output (such as DPI, scale factor, manufacturer, etc). */ m->scene_output = wlr_scene_output_create(scene, wlr_output); + + if (!wlr_output->enabled) + return; + if (m->m.x == -1 && m->m.y == -1) wlr_output_layout_add_auto(output_layout, wlr_output); else From 9dfaea2f79f9987fdd68744ff92d213056ed9b75 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Thu, 22 Oct 2026 13:29:56 +0200 Subject: [PATCH 11/14] call updatemons() when createmon() leaves an output disabled This lets you enable it later. Fixes afbafd5. --- dwl.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/dwl.c b/dwl.c index 22bbc5c..1e6c423 100644 --- a/dwl.c +++ b/dwl.c @@ -1263,8 +1263,10 @@ createmon(struct wl_listener *listener, void *data) */ m->scene_output = wlr_scene_output_create(scene, wlr_output); - if (!wlr_output->enabled) + if (!wlr_output->enabled) { + updatemons(NULL, NULL); return; + } if (m->m.x == -1 && m->m.y == -1) wlr_output_layout_add_auto(output_layout, wlr_output); From 4953b7ea0ca40b5a2cfd7e0afb283888afa6645e Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Thu, 22 Oct 2026 13:36:27 +0200 Subject: [PATCH 12/14] keep outputs off when they reconnect Some monitors disconnect when they are turned off and reconnect shortly after, and createmon() enabled them again. Remember the names of the outputs turned off through output_power_management or output_management and don't enable them in createmon(). Fixes https://codeberg.org/dwl/dwl/issues/719 --- dwl.c | 50 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/dwl.c b/dwl.c index 1e6c423..d5f2f98 100644 --- a/dwl.c +++ b/dwl.c @@ -328,6 +328,7 @@ static void mapnotify(struct wl_listener *listener, void *data); static void maximizenotify(struct wl_listener *listener, void *data); static void monocle(Monitor *m); static void motionabsolute(struct wl_listener *listener, void *data); +static char **monoff(struct wlr_output *wlr_output); static void motionnotify(uint32_t time, struct wlr_input_device *device, double sx, double sy, double sx_unaccel, double sy_unaccel); static void motionrelative(struct wl_listener *listener, void *data); @@ -353,6 +354,7 @@ static void setfullscreen(Client *c, int fullscreen); static void setlayout(const Arg *arg); static void setmfact(const Arg *arg); static void setmon(Client *c, Monitor *m, uint32_t newtags); +static void setmonoff(struct wlr_output *wlr_output, int off); static void setpsel(struct wl_listener *listener, void *data); static void setsel(struct wl_listener *listener, void *data); static void setup(void); @@ -450,6 +452,7 @@ static struct wl_event_source *transaction_idle; static struct wl_event_source *transaction_timer; static int transaction_waiting; static struct wl_array ghosts; /* struct wlr_scene_node *, unmapped clients */ +static struct wl_array offmons; /* char *, names of outputs turned off by clients */ /* global event handlers */ static struct wl_listener cursor_axis = {.notify = axisnotify}; @@ -824,6 +827,8 @@ checkidleinhibitor(struct wlr_surface *exclude) void cleanup(void) { + char **name; + cleanuplisteners(); #ifdef XWAYLAND wlr_xwayland_destroy(xwayland); @@ -849,6 +854,9 @@ cleanup(void) wl_event_source_remove(transaction_idle); wl_event_source_remove(transaction_timer); wl_array_release(&ghosts); + wl_array_for_each(name, &offmons) + free(*name); + wl_array_release(&offmons); wl_display_destroy(dpy); /* Destroy after the wayland display (when the monitors are already destroyed) @@ -1229,14 +1237,17 @@ createmon(struct wl_listener *listener, void *data) * monitor supports only a specific set of modes. We just pick the * monitor's preferred mode; a more sophisticated compositor would let * the user configure it. */ - wlr_output_state_set_mode(&state, wlr_output_preferred_mode(wlr_output)); - /* Set up event listeners */ LISTEN(&wlr_output->events.frame, &m->frame, rendermon); LISTEN(&wlr_output->events.destroy, &m->destroy, cleanupmon); LISTEN(&wlr_output->events.request_state, &m->request_state, requestmonstate); - wlr_output_state_set_enabled(&state, 1); + /* Some monitors disconnect when they are turned off and reconnect shortly + * after, so keep outputs that were turned off disabled. */ + if (!monoff(wlr_output)) { + wlr_output_state_set_mode(&state, wlr_output_preferred_mode(wlr_output)); + wlr_output_state_set_enabled(&state, 1); + } wlr_output_commit_state(wlr_output, &state); wlr_output_state_finish(&state); @@ -2068,6 +2079,17 @@ motionabsolute(struct wl_listener *listener, void *data) motionnotify(event->time_msec, &event->pointer->base, dx, dy, dx, dy); } +char ** +monoff(struct wlr_output *wlr_output) +{ + char **name; + wl_array_for_each(name, &offmons) { + if (!strcmp(*name, wlr_output->name)) + return name; + } + return NULL; +} + void motionnotify(uint32_t time, struct wlr_input_device *device, double dx, double dy, double dx_unaccel, double dy_unaccel) @@ -2254,6 +2276,7 @@ outputmgrapplyortest(struct wlr_output_configuration_v1 *config, int test) /* Ensure displays previously disabled by wlr-output-power-management-v1 * are properly handled*/ m->asleep = 0; + setmonoff(wlr_output, !config_head->state.enabled); /* Don't move monitors if position wouldn't change. This avoids * wlroots marking the output as manually configured. @@ -2372,6 +2395,10 @@ powermgrsetmode(struct wl_listener *listener, void *data) m->gamma_lut_changed = 1; /* Reapply gamma LUT when re-enabling the output */ wlr_output_state_set_enabled(&state, event->mode); + /* The output was reconnected while off and was never modeset */ + if (event->mode && !m->wlr_output->current_mode) + wlr_output_state_set_mode(&state, wlr_output_preferred_mode(m->wlr_output)); + setmonoff(m->wlr_output, !event->mode); wlr_output_commit_state(m->wlr_output, &state); m->asleep = !event->mode; @@ -2647,6 +2674,22 @@ setmon(Client *c, Monitor *m, uint32_t newtags) focusclient(focustop(selmon), NULL, 1); } +void +setmonoff(struct wlr_output *wlr_output, int off) +{ + char **name = monoff(wlr_output), **last; + + if (off && !name && (name = wl_array_add(&offmons, sizeof(*name)))) { + if (!(*name = strdup(wlr_output->name))) + offmons.size -= sizeof(*name); + } else if (!off && name) { + free(*name); + last = (char **)((char *)offmons.data + offmons.size) - 1; + *name = *last; + offmons.size -= sizeof(*name); + } +} + void setpsel(struct wl_listener *listener, void *data) { @@ -2787,6 +2830,7 @@ setup(void) transaction_timer = wl_event_loop_add_timer(event_loop, transactiontimeout, NULL); wl_array_init(&ghosts); + wl_array_init(&offmons); xdg_shell = wlr_xdg_shell_create(dpy, 6); wl_signal_add(&xdg_shell->events.new_toplevel, &new_xdg_toplevel); From 4ecb2a9df52bbcd9b59532709d62d72e8af155c7 Mon Sep 17 00:00:00 2001 From: save196 Date: Mon, 26 Oct 2026 14:52:05 +0200 Subject: [PATCH 13/14] fix the focused border color after leaving an unmanaged client Clients focused after an unmanaged client that wants focus didn't get the focused border color. Regression from 87029e2. --- dwl.c | 1 - 1 file changed, 1 deletion(-) diff --git a/dwl.c b/dwl.c index d5f2f98..737a175 100644 --- a/dwl.c +++ b/dwl.c @@ -1610,7 +1610,6 @@ focusclient(Client *c, LayerSurface *l, int lift) /* Don't change border color if there is an exclusive focus or we are * handling a drag operation */ if (!(old_l && old_l->layer_surface->current.keyboard_interactive == ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_EXCLUSIVE) - && !(old_c && client_wants_focus(old_c)) && !seat->drag) client_set_border_color(c, focuscolor); } From 12a58564277312ff393b7a2fc27245aea597c41d Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Tue, 27 Oct 2026 23:14:43 +0100 Subject: [PATCH 14/14] don't allocate memory for drag_icon_destroy Not needed since dwl supports only one drag icon at a time. --- dwl.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dwl.c b/dwl.c index 737a175..89e8caf 100644 --- a/dwl.c +++ b/dwl.c @@ -482,6 +482,7 @@ static struct wl_listener request_set_sel = {.notify = setsel}; static struct wl_listener request_set_cursor_shape = {.notify = setcursorshape}; static struct wl_listener request_start_drag = {.notify = requeststartdrag}; static struct wl_listener start_drag = {.notify = startdrag}; +static struct wl_listener drag_icon_destroy = {.notify = destroydragicon}; static struct wl_listener new_session_lock = {.notify = locksession}; static struct wl_listener new_foreign_toplevel_capture_request = {.notify = capturerequest}; @@ -1423,7 +1424,6 @@ destroydragicon(struct wl_listener *listener, void *data) focusclient(focustop(selmon), NULL, 1); motionnotify(0, NULL, 0, 0, 0, 0); wl_list_remove(&listener->link); - free(listener); } void @@ -3022,7 +3022,7 @@ startdrag(struct wl_listener *listener, void *data) return; drag->icon->data = &wlr_scene_drag_icon_create(drag_icon, drag->icon)->node; - LISTEN_STATIC(&drag->icon->events.destroy, destroydragicon); + wl_signal_add(&drag->icon->events.destroy, &drag_icon_destroy); } void