From 5c582bda09de01f1bd172f83b1f5620923ec8c8b Mon Sep 17 00:00:00 2001 From: daniel <193309918+danielfrrrr@users.noreply.github.com> Date: Wed, 1 Apr 2026 06:49:47 -0300 Subject: [PATCH 01/20] config: migrate from amixer to pactl --- README.md | 6 +++--- docs/labwc-config.5.scd | 2 +- docs/rc.xml.all | 6 +++--- include/config/default-bindings.h | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index ec00083d..9cc81e88 100644 --- a/README.md +++ b/README.md @@ -217,9 +217,9 @@ If you have not created an rc.xml config file, default bindings will be: | `super`-`mouse-right` | resize window | `super`-`arrow` | resize window to fill half the output | `alt`-`space` | show the window menu -| `XF86AudioLowerVolume` | amixer sset Master 5%- -| `XF86AudioRaiseVolume` | amixer sset Master 5%+ -| `XF86AudioMute` | amixer sset Master toggle +| `XF86AudioLowerVolume` | pactl set-sink-volume @DEFAULT_SINK@ -5% +| `XF86AudioRaiseVolume` | pactl set-sink-volume @DEFAULT_SINK@ +5% +| `XF86AudioMute` | pactl set-sink-mute @DEFAULT_SINK@ toggle | `XF86MonBrightnessUp` | brightnessctl set +10% | `XF86MonBrightnessDown` | brightnessctl set 10%- diff --git a/docs/labwc-config.5.scd b/docs/labwc-config.5.scd index ac94d54c..32943eb7 100644 --- a/docs/labwc-config.5.scd +++ b/docs/labwc-config.5.scd @@ -851,7 +851,7 @@ overrideInhibition="">* A-Space - show window menu ``` - Audio and MonBrightness keys are also bound to amixer and + Audio and MonBrightness keys are also bound to pactl and brightnessctl, respectively. ** diff --git a/docs/rc.xml.all b/docs/rc.xml.all index 6d543b46..9ddd69a8 100644 --- a/docs/rc.xml.all +++ b/docs/rc.xml.all @@ -292,13 +292,13 @@ - + - + - + diff --git a/include/config/default-bindings.h b/include/config/default-bindings.h index f2042c28..a49f60f4 100644 --- a/include/config/default-bindings.h +++ b/include/config/default-bindings.h @@ -88,21 +88,21 @@ static struct key_combos { .action = "Execute", .attributes[0] = { .name = "command", - .value = "amixer sset Master 5%-", + .value = "pactl set-sink-volume @DEFAULT_SINK@ -5%", }, }, { .binding = "XF86AudioRaiseVolume", .action = "Execute", .attributes[0] = { .name = "command", - .value = "amixer sset Master 5%+", + .value = "pactl set-sink-volume @DEFAULT_SINK@ +5%", }, }, { .binding = "XF86AudioMute", .action = "Execute", .attributes[0] = { .name = "command", - .value = "amixer sset Master toggle", + .value = "pactl set-sink-mute @DEFAULT_SINK@ toggle", }, }, { .binding = "XF86MonBrightnessUp", From 5f668a82eedfda31482d7e7eb6c717d014333900 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorbj=C3=B8rn=20Lindeijer?= Date: Fri, 17 Apr 2026 18:52:01 +0200 Subject: [PATCH 02/20] interactive: allow resize on fully maximized views Modifier+right-drag resize was silently ignored on fully maximized views because of an early-return guard in interactive_begin(). The axis-specific un-maximization logic introduced in #3043 already handles partial maximization correctly; extend that to the VIEW_AXIS_BOTH case so both axes are cleared while keeping the current geometry as the starting point of the resize. Move already permits dragging maximized views, so this also removes an asymmetry between the move and resize paths. Fixes: #3524 --- src/interactive.c | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/interactive.c b/src/interactive.c index f4d8eea1..9214ba30 100644 --- a/src/interactive.c +++ b/src/interactive.c @@ -123,11 +123,12 @@ interactive_begin(struct view *view, enum input_mode mode, enum lab_edge edges) cursor_shape = LAB_CURSOR_GRAB; break; case LAB_INPUT_STATE_RESIZE: { - if (view->shaded || view->fullscreen || - view->maximized == VIEW_AXIS_BOTH) { + if (view->shaded || view->fullscreen) { /* - * We don't allow resizing while shaded, - * fullscreen or maximized in both directions. + * We don't allow resizing while shaded or fullscreen. + * Maximized views are handled below by un-maximizing + * the axes being resized while keeping the current + * geometry as the starting point. */ return; } @@ -141,9 +142,9 @@ interactive_begin(struct view *view, enum input_mode mode, enum lab_edge edges) } /* - * If tiled or maximized in only one direction, reset - * tiled state and un-maximize the relevant axes, but - * keep the same geometry as the starting point. + * If tiled or maximized, reset tiled state and un-maximize + * the axes that are being resized, but keep the same + * geometry as the starting point. */ enum view_axis maximized = view->maximized; if (server.resize_edges & LAB_EDGES_LEFT_RIGHT) { From 20540d76f9976988ccf2dc1ecfeb5e2fb65fe18b Mon Sep 17 00:00:00 2001 From: elviosak <33790211+elviosak@users.noreply.github.com> Date: Fri, 24 Apr 2026 16:04:43 -0300 Subject: [PATCH 03/20] fix docs typo --- docs/labwc-actions.5.scd | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/labwc-actions.5.scd b/docs/labwc-actions.5.scd index de1dc00d..fb616340 100644 --- a/docs/labwc-actions.5.scd +++ b/docs/labwc-actions.5.scd @@ -514,7 +514,7 @@ Actions that execute other actions. Used in keyboard/mouse bindings. "right-occupied" directions will not wrap. *tiled* [up|right|down|left|up-left|up-right|down-left|down-right|center|any] - Whether the client is tiled (snapped) along the the + Whether the client is tiled (snapped) along the indicated screen edge. *tiled_region* From 7d264c907fd23f4fa6ba2a190a4e250590d58a59 Mon Sep 17 00:00:00 2001 From: tokyo4j Date: Sun, 26 Apr 2026 18:56:03 +0900 Subject: [PATCH 04/20] src/cycle/cycle.c: fix typo --- src/cycle/cycle.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cycle/cycle.c b/src/cycle/cycle.c index 77195176..2649f1e5 100644 --- a/src/cycle/cycle.c +++ b/src/cycle/cycle.c @@ -167,7 +167,7 @@ cycle_begin(enum lab_cycle_dir direction, struct view *active_view = server.active_view; if (active_view && active_view->cycle_link.next) { - /* Select the active view it's in the cycle list */ + /* Select the active view if it's in the cycle list */ server.cycle.selected_view = active_view; } else { /* Otherwise, select the first view in the cycle list */ From 5c7bfe3c67d27934f345d50239bc7084a03193d3 Mon Sep 17 00:00:00 2001 From: diniamo Date: Sat, 25 Apr 2026 20:44:02 +0200 Subject: [PATCH 05/20] Add onbutton scrollMethod, scrollButton --- docs/labwc-config.5.scd | 12 +++++++++--- docs/rc.xml.all | 3 ++- include/config/libinput.h | 1 + src/config/libinput.c | 1 + src/config/rcxml.c | 10 ++++++++++ src/seat.c | 10 ++++++++++ 6 files changed, 33 insertions(+), 4 deletions(-) diff --git a/docs/labwc-config.5.scd b/docs/labwc-config.5.scd index 32943eb7..6f198532 100644 --- a/docs/labwc-config.5.scd +++ b/docs/labwc-config.5.scd @@ -1140,6 +1140,7 @@ Note: To rotate touch events with output rotation, use the libinput + 1.0 @@ -1244,19 +1245,24 @@ Note: To rotate touch events with output rotation, use the libinput The default method depends on the touchpad hardware. -** [none|twofinger|edge] - Configure the method by which physical movements on a touchpad are - mapped to scroll events. +** [none|twofinger|edge|onbutton] + Configure the method by which physical movements are mapped to scroll events. The scroll methods available are: - *twofinger* - Scroll by two fingers being placed on the surface of the touchpad, then moving those fingers vertically or horizontally. - *edge* - Scroll by moving a single finger along the right edge (vertical scroll) or bottom edge (horizontal scroll). + - *onbutton* - Scroll by pressing a button. - *none* - No scroll events will be produced. The default method depends on the touchpad hardware. +** [button] + Set the button used for the *onbutton* scroll method. + + *button* is the decimal form of a value from `linux/input-event-codes.h`. + ** [yes|no|disabledOnExternalMouse] Optionally enable or disable sending any device events. diff --git a/docs/rc.xml.all b/docs/rc.xml.all index 9ddd69a8..61ea121a 100644 --- a/docs/rc.xml.all +++ b/docs/rc.xml.all @@ -592,7 +592,7 @@ - accelProfile [flat|adaptive] - tapButtonMap [lrm|lmr] - clickMethod [none|buttonAreas|clickfinger] - - scrollMethod [twoFinger|edge|none] + - scrollMethod [twoFinger|edge|onbutton|none] - sendEventsMode [yes|no|disabledOnExternalMouse] - calibrationMatrix [six float values split by space] - scrollFactor [float] @@ -618,6 +618,7 @@ + 1.0 diff --git a/include/config/libinput.h b/include/config/libinput.h index 80b3fc10..077bc011 100644 --- a/include/config/libinput.h +++ b/include/config/libinput.h @@ -31,6 +31,7 @@ struct libinput_category { int dwt; /* -1 or libinput_config_dwt_state */ int click_method; /* -1 or libinput_config_click_method */ int scroll_method; /* -1 or libinput_config_scroll_method */ + int scroll_button; /* -1 or a button from linux/input_event_codes.h */ int send_events_mode; /* -1 or libinput_config_send_events_mode */ bool have_calibration_matrix; double scroll_factor; diff --git a/src/config/libinput.c b/src/config/libinput.c index 1209d267..59bf469e 100644 --- a/src/config/libinput.c +++ b/src/config/libinput.c @@ -25,6 +25,7 @@ libinput_category_init(struct libinput_category *l) l->dwt = -1; l->click_method = -1; l->scroll_method = -1; + l->scroll_button = -1; l->send_events_mode = -1; l->have_calibration_matrix = false; l->scroll_factor = 1.0; diff --git a/src/config/rcxml.c b/src/config/rcxml.c index 20c236b4..e3187af2 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -891,9 +891,19 @@ fill_libinput_category(xmlNode *node) } else if (!strcasecmp(content, "twofinger")) { category->scroll_method = LIBINPUT_CONFIG_SCROLL_2FG; + } else if (!strcasecmp(content, "onbutton")) { + category->scroll_method = + LIBINPUT_CONFIG_SCROLL_ON_BUTTON_DOWN; } else { wlr_log(WLR_ERROR, "invalid scrollMethod"); } + } else if (!strcasecmp(key, "scrollButton")) { + int button = atoi(content); + if (button != 0) { + category->scroll_button = button; + } else { + wlr_log(WLR_ERROR, "invalid scrollButton"); + } } else if (!strcasecmp(key, "sendEventsMode")) { category->send_events_mode = get_send_events_mode(content); diff --git a/src/seat.c b/src/seat.c index 93d108be..fe85dc26 100644 --- a/src/seat.c +++ b/src/seat.c @@ -328,6 +328,16 @@ configure_libinput(struct wlr_input_device *wlr_input_device) libinput_device_config_scroll_set_method(libinput_dev, dc->scroll_method); } + libinput_device_config_scroll_set_button(libinput_dev, + libinput_device_config_scroll_get_default_button(libinput_dev)); + if (dc->scroll_button < 0) { + wlr_log(WLR_INFO, "scroll button not configured"); + } else { + wlr_log(WLR_INFO, "scroll button configured (%d)", + dc->scroll_button); + libinput_device_config_scroll_set_button(libinput_dev, dc->scroll_button); + } + libinput_device_config_send_events_set_mode(libinput_dev, libinput_device_config_send_events_get_default_mode(libinput_dev)); if ((dc->send_events_mode != LIBINPUT_CONFIG_SEND_EVENTS_ENABLED From d8119cb3546db179d289ebd1dd26e0eb596740ad Mon Sep 17 00:00:00 2001 From: Jos Dehaes Date: Mon, 20 Apr 2026 15:53:34 +0200 Subject: [PATCH 06/20] build: ship labwc-session.target systemd user unit Add a small systemd user target modelled on miracle-wm-session.target. It binds to graphical-session.target and orders after graphical-session-pre.target, so systemd user services declaring WantedBy=graphical-session.target (panels, portals, notification daemons, ...) start and stop in sync with a labwc session. Installed into $systemduserunitdir when the systemd dependency is available at configure time; on systems without systemd the install is skipped and labwc's runtime activation of the target fails gracefully. --- data/labwc-session.target | 6 ++++++ meson.build | 9 +++++++++ 2 files changed, 15 insertions(+) create mode 100644 data/labwc-session.target diff --git a/data/labwc-session.target b/data/labwc-session.target new file mode 100644 index 00000000..e71f20e5 --- /dev/null +++ b/data/labwc-session.target @@ -0,0 +1,6 @@ +[Unit] +Description=labwc session +Documentation=man:labwc(1) man:systemd.special(7) +BindsTo=graphical-session.target +Wants=graphical-session-pre.target +After=graphical-session-pre.target diff --git a/meson.build b/meson.build index 2c2d8e57..e2d2f585 100644 --- a/meson.build +++ b/meson.build @@ -211,6 +211,15 @@ install_data('data/labwc.desktop', install_dir: get_option('datadir') / 'wayland install_data('data/labwc-portals.conf', install_dir: get_option('datadir') / 'xdg-desktop-portal') +# Install labwc-session.target so that systemd user services with +# WantedBy=graphical-session.target start under labwc. Labwc activates +# this target itself in src/config/session.c on autostart. +systemd = dependency('systemd', required: false) +if systemd.found() + install_data('data/labwc-session.target', + install_dir: systemd.get_variable('systemduserunitdir')) +endif + icons = ['labwc-symbolic.svg', 'labwc.svg'] foreach icon : icons icon_path = join_paths('data', icon) From af277b09ed465691fff2915663b6f1dad7de62a9 Mon Sep 17 00:00:00 2001 From: Jos Dehaes Date: Tue, 21 Apr 2026 16:31:30 +0200 Subject: [PATCH 07/20] docs: document labwc-session.target integration Describe the shipped labwc-session.target in labwc(1) SESSION MANAGEMENT and add commented-out systemctl start/stop lines to the example autostart and shutdown files. Users on systemd-based distros can uncomment these to pull in graphical-session.target when labwc starts and tear it down cleanly on exit, without labwc itself mandating any specific init system. --- docs/autostart | 8 ++++++++ docs/labwc.1.scd | 19 +++++++++++++++++++ docs/shutdown | 8 ++++++++ 3 files changed, 35 insertions(+) diff --git a/docs/autostart b/docs/autostart index b045ed82..17fcc270 100644 --- a/docs/autostart +++ b/docs/autostart @@ -1,5 +1,13 @@ # Example autostart file +# When running under systemd, uncomment the systemctl line below to pull in +# graphical-session.target via labwc-session.target. This lets systemd user +# services declaring WantedBy=graphical-session.target (panels, portals, +# notification daemons, etc.) start in sync with the labwc session. Enable +# individual services with: systemctl --user enable +# +# systemctl --user --no-block start labwc-session.target + # Set background color. swaybg -c '#113344' >/dev/null 2>&1 & diff --git a/docs/labwc.1.scd b/docs/labwc.1.scd index 2dab30a5..31f28c19 100644 --- a/docs/labwc.1.scd +++ b/docs/labwc.1.scd @@ -118,6 +118,25 @@ this is accomplished by setting the session variables to empty strings. For systemd, the command `systemctl --user unset-environment` will be invoked to actually remove the variables from the activation environment. +A systemd user unit named `labwc-session.target` is also shipped alongside +the compositor for users who want to integrate labwc with systemd. It binds +to the standard `graphical-session.target`, so systemd user services can +start and stop in sync with the labwc session when they declare a WantedBy +or PartOf relationship to that target. Labwc does not activate the target +itself; users opt in by adding lines like the following to their +*autostart* and *shutdown* files: + +``` +systemctl --user --no-block start labwc-session.target +systemctl --user stop graphical-session.target +``` + +The example *autostart* and *shutdown* files shipped with labwc include +these commented out. To have a user service automatically started with +the session, enable it so the corresponding symlink under the +graphical-session.target.wants directory exists, for example by running +"systemctl --user enable dms.service". + # ENVIRONMENT VARIABLES Set the environment variables listed below to enable specific debug options. diff --git a/docs/shutdown b/docs/shutdown index feed6508..a036ff53 100644 --- a/docs/shutdown +++ b/docs/shutdown @@ -3,3 +3,11 @@ # This file is executed as a shell script when labwc is preparing to terminate # itself. # For further details see labwc-config(5). + +# When running under systemd, uncomment the systemctl line below to tear down +# graphical-session.target (which cascades to labwc-session.target via +# BindsTo, and to any service declaring PartOf=graphical-session.target). +# Running synchronously here ensures those services are stopped before the +# Wayland socket goes away, avoiding "Broken pipe" failures on teardown. +# +# systemctl --user stop graphical-session.target From 70e3173f9993ef002ec8a4e8e809a7a8be612629 Mon Sep 17 00:00:00 2001 From: Jos Dehaes Date: Fri, 24 Apr 2026 10:27:55 +0200 Subject: [PATCH 08/20] build: add systemd-session feature option Let distributors opt out of installing labwc-session.target at configure time. Default is 'auto' (install if the systemd pkg-config file is present), 'enabled' forces it on, 'disabled' skips it entirely. --- meson.build | 8 +++++--- meson_options.txt | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/meson.build b/meson.build index e2d2f585..7e345ba6 100644 --- a/meson.build +++ b/meson.build @@ -212,9 +212,11 @@ install_data('data/labwc.desktop', install_dir: get_option('datadir') / 'wayland install_data('data/labwc-portals.conf', install_dir: get_option('datadir') / 'xdg-desktop-portal') # Install labwc-session.target so that systemd user services with -# WantedBy=graphical-session.target start under labwc. Labwc activates -# this target itself in src/config/session.c on autostart. -systemd = dependency('systemd', required: false) +# WantedBy=graphical-session.target can be started and stopped in sync +# with a labwc session (see labwc(1) SESSION MANAGEMENT for the opt-in +# autostart/shutdown snippet). +systemd_feat = get_option('systemd-session') +systemd = dependency('systemd', required: systemd_feat) if systemd.found() install_data('data/labwc-session.target', install_dir: systemd.get_variable('systemduserunitdir')) diff --git a/meson_options.txt b/meson_options.txt index a47efa86..e059220d 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -4,6 +4,7 @@ option('svg', type: 'feature', value: 'enabled', description: 'Enable svg window option('icon', type: 'feature', value: 'enabled', description: 'Enable window icons') option('labnag', type: 'feature', value: 'auto', description: 'Build labnag notification daemon') option('nls', type: 'feature', value: 'auto', description: 'Enable native language support') +option('systemd-session', type: 'feature', value: 'auto', description: 'Install labwc-session.target systemd user unit') option('static_analyzer', type: 'feature', value: 'disabled', description: 'Run gcc static analyzer') option('test', type: 'feature', value: 'disabled', description: 'Run tests') option('sections', type: 'feature', value: 'disabled', description: 'Show unused functions') From 7b3f37725fac33affe6d013c69f9b59bb140d5c8 Mon Sep 17 00:00:00 2001 From: Jos Dehaes Date: Wed, 15 Apr 2026 14:34:37 +0200 Subject: [PATCH 09/20] rcxml: add raiseOnFocusDelay option Add a new element accepting an integer in milliseconds. The default of 0 preserves the current behavior (raise immediately when raiseOnFocus is enabled). The new field is carried as uint32_t raise_on_focus_delay_ms on struct rcxml. This commit only adds the parser and default; the actual delay logic follows in a subsequent commit. --- include/config/rcxml.h | 1 + src/config/rcxml.c | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/include/config/rcxml.h b/include/config/rcxml.h index 8a2c606c..3ef7bd67 100644 --- a/include/config/rcxml.h +++ b/include/config/rcxml.h @@ -90,6 +90,7 @@ struct rcxml { bool focus_follow_mouse; bool focus_follow_mouse_requires_movement; bool raise_on_focus; + uint32_t raise_on_focus_delay_ms; /* theme */ char *theme_name; diff --git a/src/config/rcxml.c b/src/config/rcxml.c index e3187af2..d0bb76db 100644 --- a/src/config/rcxml.c +++ b/src/config/rcxml.c @@ -1195,6 +1195,9 @@ entry(xmlNode *node, char *nodename, char *content) set_bool(content, &rc.focus_follow_mouse_requires_movement); } else if (!strcasecmp(nodename, "raiseOnFocus.focus")) { set_bool(content, &rc.raise_on_focus); + } else if (!strcasecmp(nodename, "raiseOnFocusDelay.focus")) { + long val = strtol(content, NULL, 10); + rc.raise_on_focus_delay_ms = val > 0 ? (uint32_t)val : 0; } else if (!strcasecmp(nodename, "doubleClickTime.mouse")) { long doubleclick_time_parsed = strtol(content, NULL, 10); if (doubleclick_time_parsed > 0) { @@ -1530,6 +1533,7 @@ rcxml_init(void) rc.focus_follow_mouse = false; rc.focus_follow_mouse_requires_movement = true; rc.raise_on_focus = false; + rc.raise_on_focus_delay_ms = 0; rc.doubleclick_time = 500; From fe6ea66b8221ac77edba84e9efe30ef417553804 Mon Sep 17 00:00:00 2001 From: Jos Dehaes Date: Wed, 15 Apr 2026 14:35:27 +0200 Subject: [PATCH 10/20] server: add pending auto-raise state + decl Add two fields to struct server: struct view *pending_auto_raise_view; struct wl_event_source *pending_auto_raise_timer; and forward-declare desktop_cancel_pending_auto_raise() in labwc.h. The state is a single 'slot' (at most one view/timer pending) since a new focus change supersedes any previous pending raise. This commit just reserves the state and the public API; the behaviour is implemented in the following commit. --- include/labwc.h | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/include/labwc.h b/include/labwc.h index 511fdd65..dc4c1311 100644 --- a/include/labwc.h +++ b/include/labwc.h @@ -150,6 +150,11 @@ struct seat { struct server { struct wl_display *wl_display; struct wl_event_loop *wl_event_loop; /* Can be used for timer events */ + + /* Pending auto-raise timer (used when rc.raise_on_focus_delay_ms > 0) */ + struct view *pending_auto_raise_view; + struct wl_event_source *pending_auto_raise_timer; + struct wlr_renderer *renderer; struct wlr_allocator *allocator; struct wlr_backend *backend; @@ -343,6 +348,13 @@ void xdg_shell_finish(void); */ void desktop_focus_view(struct view *view, bool raise); +/** + * desktop_cancel_pending_auto_raise() - cancel any pending delayed auto-raise + * (from raiseOnFocusDelay). Called when a view is being destroyed, on config + * reload, or when a new focus change with raise=false supersedes the pending. + */ +void desktop_cancel_pending_auto_raise(void); + /** * desktop_focus_view_or_surface() - like desktop_focus_view() but can * also focus other (e.g. xwayland-unmanaged) surfaces From 28908adfbecb5415aa99b21c4edf00f225e67feb Mon Sep 17 00:00:00 2001 From: Jos Dehaes Date: Wed, 15 Apr 2026 14:36:14 +0200 Subject: [PATCH 11/20] desktop: implement delayed auto-raise timer logic When raiseOnFocus is enabled and raiseOnFocusDelay is > 0, defer view_move_to_front() until a wl_event_loop timer elapses instead of raising immediately. The pending view/timer pair lives on struct server so that at most one raise can be pending per compositor. Semantics: delay_ms == 0 : immediate raise (preserves prior behavior) delay_ms > 0, hover A : schedule raise of A after delay_ms another hover before : timer is reset and the new view becomes expiry the pending one (brief flyby hovers do not stack up raises) focus change, raise=false: pending raise is cancelled This is most useful with followMouse to avoid brief passes of the cursor stacking up z-order changes. --- src/desktop.c | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/src/desktop.c b/src/desktop.c index 4c7870f1..966bdaab 100644 --- a/src/desktop.c +++ b/src/desktop.c @@ -9,6 +9,7 @@ #include #include #include "common/scene-helpers.h" +#include "config/rcxml.h" #include "dnd.h" #include "labwc.h" #include "layers.h" @@ -65,6 +66,49 @@ set_or_offer_focus(struct view *view) } } +static int +handle_auto_raise_timer(void *data) +{ + (void)data; + struct view *view = server.pending_auto_raise_view; + server.pending_auto_raise_view = NULL; + + if (view && view->mapped) { + view_move_to_front(view); + } + return 0; /* ignored per wl_event_loop docs */ +} + +void +desktop_cancel_pending_auto_raise(void) +{ + server.pending_auto_raise_view = NULL; + if (server.pending_auto_raise_timer) { + /* Disarm by setting to 0 ms */ + wl_event_source_timer_update(server.pending_auto_raise_timer, 0); + } +} + +static void +schedule_auto_raise(struct view *view) +{ + if (rc.raise_on_focus_delay_ms == 0) { + /* Immediate raise — preserves original behavior */ + desktop_cancel_pending_auto_raise(); + view_move_to_front(view); + return; + } + + server.pending_auto_raise_view = view; + if (!server.pending_auto_raise_timer) { + server.pending_auto_raise_timer = + wl_event_loop_add_timer(server.wl_event_loop, + handle_auto_raise_timer, NULL); + } + wl_event_source_timer_update(server.pending_auto_raise_timer, + rc.raise_on_focus_delay_ms); +} + void desktop_focus_view(struct view *view, bool raise) { @@ -104,7 +148,13 @@ desktop_focus_view(struct view *view, bool raise) } if (raise) { - view_move_to_front(view); + schedule_auto_raise(view); + } else { + /* + * A new focus change without a raise supersedes any + * pending auto-raise from a previous focus event. + */ + desktop_cancel_pending_auto_raise(); } /* From ecc55656868a4f7e9350d7487ef91b33c259a125 Mon Sep 17 00:00:00 2001 From: Jos Dehaes Date: Wed, 15 Apr 2026 14:36:33 +0200 Subject: [PATCH 12/20] view: cancel pending auto-raise on view destroy The pending_auto_raise_view pointer would become dangling if the view it references is destroyed before the timer fires. Clear it in view_destroy() alongside the existing active_view cleanup. --- src/view.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/view.c b/src/view.c index 1d43fcfa..9b6604ad 100644 --- a/src/view.c +++ b/src/view.c @@ -2521,6 +2521,10 @@ view_destroy(struct view *view) server.active_view = NULL; } + if (server.pending_auto_raise_view == view) { + desktop_cancel_pending_auto_raise(); + } + if (server.session_lock_manager->last_active_view == view) { server.session_lock_manager->last_active_view = NULL; } From 9661ed4285e7f7298851a45c0770cd2b9c9839b9 Mon Sep 17 00:00:00 2001 From: Jos Dehaes Date: Wed, 15 Apr 2026 14:36:56 +0200 Subject: [PATCH 13/20] server: cancel pending auto-raise on config reload If the user disables raiseOnFocus or lowers raiseOnFocusDelay while a raise is queued, the queued raise should not fire against the new config. Cancel it in reload_config_and_theme() before rereading the rc.xml. --- src/server.c | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/server.c b/src/server.c index e4b27026..c792d8b4 100644 --- a/src/server.c +++ b/src/server.c @@ -89,6 +89,12 @@ reload_config_and_theme(void) /* Avoid UAF when dialog client is used during reconfigure */ action_prompts_destroy(); + /* + * Cancel any pending auto-raise before reloading config in case the + * raiseOnFocusDelay option was disabled or changed. + */ + desktop_cancel_pending_auto_raise(); + scaled_buffer_invalidate_sharing(); rcxml_finish(); rcxml_read(rc.config_file); From 6237e26a1d44ca88e27b09c41619df51de6aae81 Mon Sep 17 00:00:00 2001 From: Jos Dehaes Date: Wed, 15 Apr 2026 14:37:29 +0200 Subject: [PATCH 14/20] docs: document raiseOnFocusDelay Add the new option to labwc-config(5) and the example rc.xml.all. --- docs/labwc-config.5.scd | 7 +++++++ docs/rc.xml.all | 2 ++ 2 files changed, 9 insertions(+) diff --git a/docs/labwc-config.5.scd b/docs/labwc-config.5.scd index 6f198532..8ea88d8f 100644 --- a/docs/labwc-config.5.scd +++ b/docs/labwc-config.5.scd @@ -493,6 +493,13 @@ this is for compatibility with Openbox. ** [yes|no] Raise window to top when focused. Default is no. +** [milliseconds] + When raiseOnFocus is enabled, delay the actual raise by this many + milliseconds. Default is 0 (raise immediately). A subsequent focus + change before the timer elapses restarts or cancels the pending raise. + Useful together with followMouse to avoid brief passes of the cursor + stacking up z-order changes. + ## WINDOW SNAPPING Windows may be "snapped" to an edge or user-defined region of an output when diff --git a/docs/rc.xml.all b/docs/rc.xml.all index 61ea121a..9755530b 100644 --- a/docs/rc.xml.all +++ b/docs/rc.xml.all @@ -158,6 +158,8 @@ no yes no + + 0 From e61d58e54dd7b4c751f312ab86d305e0a7b77488 Mon Sep 17 00:00:00 2001 From: Jos Dehaes Date: Sat, 18 Apr 2026 20:54:28 +0200 Subject: [PATCH 15/20] desktop: only apply raise-on-focus delay to sloppy-focus The raise_on_focus_delay is meant to dampen z-order churn from focus-follows-mouse cursor passes. Applying it to every focus change meant explicit actions (alt-tab cycle finish, Focus action, xdg/xwayland activation, view map, etc.) also waited for the delay before raising, which felt laggy. Route all non-sloppy-focus callers through an immediate raise and keep the timer-based raise only for desktop_focus_view_or_surface(), which is the sloppy-focus entry point. --- src/desktop.c | 45 +++++++++++++++++++++++++++------------------ 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/src/desktop.c b/src/desktop.c index 966bdaab..57ef9e3c 100644 --- a/src/desktop.c +++ b/src/desktop.c @@ -90,15 +90,8 @@ desktop_cancel_pending_auto_raise(void) } static void -schedule_auto_raise(struct view *view) +schedule_delayed_auto_raise(struct view *view) { - if (rc.raise_on_focus_delay_ms == 0) { - /* Immediate raise — preserves original behavior */ - desktop_cancel_pending_auto_raise(); - view_move_to_front(view); - return; - } - server.pending_auto_raise_view = view; if (!server.pending_auto_raise_timer) { server.pending_auto_raise_timer = @@ -109,8 +102,15 @@ schedule_auto_raise(struct view *view) rc.raise_on_focus_delay_ms); } -void -desktop_focus_view(struct view *view, bool raise) +/* + * The raise_on_focus_delay is only meant to dampen z-order churn from + * focus-follows-mouse cursor passes. Explicit focus changes (alt-tab, + * Focus action, xdg/xwayland activation, etc.) should raise immediately. + * allow_delay is therefore only set when the caller is the sloppy-focus + * path in desktop_focus_view_or_surface(). + */ +static void +desktop_focus_view_internal(struct view *view, bool raise, bool allow_delay) { assert(view); /* @@ -147,14 +147,17 @@ desktop_focus_view(struct view *view, bool raise) workspaces_switch_to(view->workspace, /*update_focus*/ false); } + /* + * A new focus change supersedes any pending auto-raise from a + * previous focus event, regardless of whether we raise now. + */ + desktop_cancel_pending_auto_raise(); if (raise) { - schedule_auto_raise(view); - } else { - /* - * A new focus change without a raise supersedes any - * pending auto-raise from a previous focus event. - */ - desktop_cancel_pending_auto_raise(); + if (allow_delay && rc.raise_on_focus_delay_ms > 0) { + schedule_delayed_auto_raise(view); + } else { + view_move_to_front(view); + } } /* @@ -168,6 +171,12 @@ desktop_focus_view(struct view *view, bool raise) show_desktop_reset(); } +void +desktop_focus_view(struct view *view, bool raise) +{ + desktop_focus_view_internal(view, raise, /*allow_delay*/ false); +} + /* TODO: focus layer-shell surfaces also? */ void desktop_focus_view_or_surface(struct seat *seat, struct view *view, @@ -175,7 +184,7 @@ desktop_focus_view_or_surface(struct seat *seat, struct view *view, { assert(view || surface); if (view) { - desktop_focus_view(view, raise); + desktop_focus_view_internal(view, raise, /*allow_delay*/ true); #if HAVE_XWAYLAND } else { struct wlr_xwayland_surface *xsurface = From de3870246a06ed083db71d9069cd1bbcc6fa3aa3 Mon Sep 17 00:00:00 2001 From: stormshadow <190884359+st0rm-shad0w@users.noreply.github.com> Date: Fri, 24 Apr 2026 05:42:11 +0530 Subject: [PATCH 16/20] labnag: remove +1 offset from button Y-position calculation It essentially removes an awkward top margin from the button on the main labnag bar. --- clients/labnag.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clients/labnag.c b/clients/labnag.c index 82b6e8cd..5abd989e 100644 --- a/clients/labnag.c +++ b/clients/labnag.c @@ -447,7 +447,7 @@ render_button(cairo_t *cairo, struct nag *nag, struct button *button, } button->x = *x - border - text_width - padding * 2 + 1; - button->y = (int)(ideal_height - text_height) / 2 - padding + 1; + button->y = (int)(ideal_height - text_height) / 2 - padding; button->width = text_width + padding * 2; button->height = text_height + padding * 2; From 91d89f71ce8bd46e7a6b149a1e1006cce29350b8 Mon Sep 17 00:00:00 2001 From: stormshadow <190884359+st0rm-shad0w@users.noreply.github.com> Date: Fri, 24 Apr 2026 06:16:23 +0530 Subject: [PATCH 17/20] labnag: add details border color and margin options Adds --details-border-color and --details-margin command line options to configure the border color and margin of the details pane. --- clients/labnag.c | 37 ++++++++++++++++++++++++++++++++----- docs/labnag.1.scd | 6 ++++++ 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/clients/labnag.c b/clients/labnag.c index 5abd989e..5fe5164f 100644 --- a/clients/labnag.c +++ b/clients/labnag.c @@ -46,6 +46,7 @@ struct conf { uint32_t button_text; uint32_t button_background; uint32_t details_background; + uint32_t details_border_color; uint32_t background; uint32_t text; uint32_t button_border; @@ -60,6 +61,7 @@ struct conf { ssize_t button_gap_close; ssize_t button_margin_right; ssize_t button_padding; + ssize_t details_margin; }; struct pointer { @@ -343,8 +345,9 @@ render_detailed(cairo_t *cairo, struct nag *nag, uint32_t y) uint32_t width = nag->width; int border = nag->conf->details_border_thickness; + int margin = nag->conf->details_margin; int padding = nag->conf->message_padding; - int decor = padding + border; + int decor = margin + border; nag->details.x = decor; nag->details.y = y + decor; @@ -401,6 +404,8 @@ render_detailed(cairo_t *cairo, struct nag *nag, uint32_t y) nag->details.visible_lines = pango_layout_get_line_count(layout); + int border_rect_height = nag->details.height + 2 * border; + if (show_buttons) { nag->details.button_up.x = nag->details.x + nag->details.width; nag->details.button_up.y = nag->details.y; @@ -416,6 +421,11 @@ render_detailed(cairo_t *cairo, struct nag *nag, uint32_t y) render_details_scroll_button(cairo, nag, &nag->details.button_down); } + cairo_set_source_u32(cairo, nag->conf->details_border_color); + cairo_rectangle(cairo, margin, nag->details.y - border, + nag->details.width + 2 * border, border_rect_height); + cairo_fill(cairo); + cairo_set_source_u32(cairo, nag->conf->details_background); cairo_rectangle(cairo, nag->details.x, nag->details.y, nag->details.width, nag->details.height); @@ -1464,14 +1474,16 @@ conf_init(struct conf *conf) conf->keyboard_focus = ZWLR_LAYER_SURFACE_V1_KEYBOARD_INTERACTIVITY_NONE; conf->bar_border_thickness = 2; conf->message_padding = 8; - conf->details_border_thickness = 3; conf->button_border_thickness = 3; conf->button_gap = 20; conf->button_gap_close = 15; conf->button_margin_right = 2; conf->button_padding = 3; conf->button_background = 0x680A0AFF; + conf->details_margin = 11; + conf->details_border_thickness = 3; conf->details_background = 0x680A0AFF; + conf->details_border_color = 0x680A0AFF; conf->background = 0x900000FF; conf->text = 0xFFFFFFFF; conf->button_text = 0xFFFFFFFF; @@ -1551,16 +1563,18 @@ nag_parse_options(int argc, char **argv, struct nag *nag, TO_COLOR_BORDER_BOTTOM, TO_COLOR_BUTTON_BG, TO_COLOR_DETAILS, + TO_COLOR_DETAILS_BORDER, TO_COLOR_TEXT, TO_COLOR_BUTTON_TEXT, TO_THICK_BAR_BORDER, TO_PADDING_MESSAGE, - TO_THICK_DET_BORDER, + TO_THICK_DETAILS_BORDER, TO_THICK_BTN_BORDER, TO_GAP_BTN, TO_GAP_BTN_DISMISS, TO_MARGIN_BTN_RIGHT, TO_PADDING_BTN, + TO_MARGIN_DETAILS, }; static const struct option opts[] = { @@ -1587,8 +1601,10 @@ nag_parse_options(int argc, char **argv, struct nag *nag, {"button-text-color", required_argument, NULL, TO_COLOR_BUTTON_TEXT}, {"border-bottom-size", required_argument, NULL, TO_THICK_BAR_BORDER}, {"message-padding", required_argument, NULL, TO_PADDING_MESSAGE}, - {"details-border-size", required_argument, NULL, TO_THICK_DET_BORDER}, + {"details-border-size", required_argument, NULL, TO_THICK_DETAILS_BORDER}, {"details-background-color", required_argument, NULL, TO_COLOR_DETAILS}, + {"details-border-color", required_argument, NULL, TO_COLOR_DETAILS_BORDER}, + {"details-margin", required_argument, NULL, TO_MARGIN_DETAILS}, {"button-border-size", required_argument, NULL, TO_THICK_BTN_BORDER}, {"button-gap", required_argument, NULL, TO_GAP_BTN}, {"button-dismiss-gap", required_argument, NULL, TO_GAP_BTN_DISMISS}, @@ -1633,6 +1649,9 @@ nag_parse_options(int argc, char **argv, struct nag *nag, " --details-border-size size Thickness for the details border.\n" " --details-background-color RRGGBB[AA]\n" " Details background color.\n" + " --details-border-color RRGGBB[AA]\n" + " Details border color.\n" + " --details-margin margin Margin for the details.\n" " --button-border-size size Thickness for the button border.\n" " --button-gap gap Size of the gap between buttons\n" " --button-dismiss-gap gap Size of the gap for dismiss button.\n" @@ -1769,6 +1788,11 @@ nag_parse_options(int argc, char **argv, struct nag *nag, fprintf(stderr, "Invalid details background color: %s\n", optarg); } break; + case TO_COLOR_DETAILS_BORDER: + if (!parse_color(optarg, &conf->details_border_color)) { + fprintf(stderr, "Invalid details border color: %s\n", optarg); + } + break; case TO_COLOR_TEXT: /* Text color */ if (!parse_color(optarg, &conf->text)) { fprintf(stderr, "Invalid text color: %s\n", optarg); @@ -1785,7 +1809,7 @@ nag_parse_options(int argc, char **argv, struct nag *nag, case TO_PADDING_MESSAGE: /* Message padding */ conf->message_padding = strtol(optarg, NULL, 0); break; - case TO_THICK_DET_BORDER: /* Details border thickness */ + case TO_THICK_DETAILS_BORDER: /* Details border thickness */ conf->details_border_thickness = strtol(optarg, NULL, 0); break; case TO_THICK_BTN_BORDER: /* Button border thickness */ @@ -1803,6 +1827,9 @@ nag_parse_options(int argc, char **argv, struct nag *nag, case TO_PADDING_BTN: /* Padding for the button text */ conf->button_padding = strtol(optarg, NULL, 0); break; + case TO_MARGIN_DETAILS: + conf->details_margin = strtol(optarg, NULL, 0); + break; default: /* Help or unknown flag */ fprintf(c == 'h' ? stdout : stderr, "%s", usage); return LAB_EXIT_FAILURE; diff --git a/docs/labnag.1.scd b/docs/labnag.1.scd index 6d4b959f..bf237d00 100644 --- a/docs/labnag.1.scd +++ b/docs/labnag.1.scd @@ -95,6 +95,12 @@ _labnag_ [options...] *--details-border-size* Set the thickness for the details border. +*--details-border-color* + Set the color of the details border. + +*--details-margin* + Set the margin for the details. + *--button-border-size* Set the thickness for the button border. From e209de3eb19c560cf5e0eb16ba7523941b12b67a Mon Sep 17 00:00:00 2001 From: stormshadow <190884359+st0rm-shad0w@users.noreply.github.com> Date: Fri, 24 Apr 2026 07:40:01 +0530 Subject: [PATCH 18/20] labnag: separate details scroll button styling from regular buttons Use details-specific border thickness and color config options instead of regular button options. Adjust padding for a more compact look on details scroll buttons. --- clients/labnag.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/clients/labnag.c b/clients/labnag.c index 5fe5164f..99c44d59 100644 --- a/clients/labnag.c +++ b/clients/labnag.c @@ -302,10 +302,10 @@ render_details_scroll_button(cairo_t *cairo, struct nag *nag, get_text_size(cairo, nag->conf->font_description, &text_width, &text_height, NULL, 1, true, "%s", button->text); - int border = nag->conf->button_border_thickness; - int padding = nag->conf->button_padding; + int border = nag->conf->details_border_thickness; + int padding = (nag->conf->button_padding / 3) + 2; - cairo_set_source_u32(cairo, nag->conf->details_background); + cairo_set_source_u32(cairo, nag->conf->details_border_color); cairo_rectangle(cairo, button->x, button->y, button->width, button->height); cairo_fill(cairo); @@ -333,8 +333,8 @@ get_detailed_scroll_button_width(cairo_t *cairo, struct nag *nag) NULL, 1, true, "%s", nag->details.button_down.text); int text_width = up_width > down_width ? up_width : down_width; - int border = nag->conf->button_border_thickness; - int padding = nag->conf->button_padding; + int border = nag->conf->details_border_thickness; + int padding = (nag->conf->button_padding / 3) + 2; return text_width + border * 2 + padding * 2; } From ccdef5e854dd59d9bc67e7f983f21d79d1729504 Mon Sep 17 00:00:00 2001 From: stormshadow <190884359+st0rm-shad0w@users.noreply.github.com> Date: Fri, 24 Apr 2026 08:58:06 +0530 Subject: [PATCH 19/20] labnag: fix details scroll button group height calculation Previously the scroll button group height was shorter than intended as it was calculated using details.height instead of the border dimensions. Calculate button heights using border_rect_height to properly fill the bordered region. --- clients/labnag.c | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/clients/labnag.c b/clients/labnag.c index 99c44d59..a3a526c5 100644 --- a/clients/labnag.c +++ b/clients/labnag.c @@ -375,7 +375,7 @@ render_detailed(cairo_t *cairo, struct nag *nag, uint32_t y) bool show_buttons = nag->details.offset > 0; int button_width = get_detailed_scroll_button_width(cairo, nag); if (show_buttons) { - nag->details.width -= button_width; + nag->details.width += border - button_width; pango_layout_set_width(layout, (nag->details.width - padding * 2) * PANGO_SCALE); } @@ -388,7 +388,7 @@ render_detailed(cairo_t *cairo, struct nag *nag, uint32_t y) if (!show_buttons) { show_buttons = true; - nag->details.width -= button_width; + nag->details.width += border - button_width; pango_layout_set_width(layout, (nag->details.width - padding * 2) * PANGO_SCALE); } @@ -408,16 +408,17 @@ render_detailed(cairo_t *cairo, struct nag *nag, uint32_t y) if (show_buttons) { nag->details.button_up.x = nag->details.x + nag->details.width; - nag->details.button_up.y = nag->details.y; + nag->details.button_up.y = nag->details.y - border; nag->details.button_up.width = button_width; - nag->details.button_up.height = nag->details.height / 2; + nag->details.button_up.height = (border_rect_height + border) / 2; render_details_scroll_button(cairo, nag, &nag->details.button_up); nag->details.button_down.x = nag->details.x + nag->details.width; nag->details.button_down.y = nag->details.button_up.y + nag->details.button_up.height; nag->details.button_down.width = button_width; - nag->details.button_down.height = nag->details.height / 2; + nag->details.button_down.height = + border_rect_height - nag->details.button_up.height; render_details_scroll_button(cairo, nag, &nag->details.button_down); } From 7ec7a322d24f488515e307c4babe6b0536c493c8 Mon Sep 17 00:00:00 2001 From: stormshadow <190884359+st0rm-shad0w@users.noreply.github.com> Date: Tue, 28 Apr 2026 02:09:44 +0530 Subject: [PATCH 20/20] labnag: remove doubled border between scroll buttons and adjust text position Each scroll button draws its own full border, so placing the up and down buttons flush produced a divider twice the intended thickness. Shift button_down up by one border width and extend its height accordingly so its top border overlaps button_up's bottom border. Also drop a stray \`+ border\` offset from the scroll button text Y-position that was pushing the label below the button's center. --- clients/labnag.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/clients/labnag.c b/clients/labnag.c index a3a526c5..8d47ee29 100644 --- a/clients/labnag.c +++ b/clients/labnag.c @@ -318,7 +318,7 @@ render_details_scroll_button(cairo_t *cairo, struct nag *nag, cairo_set_source_u32(cairo, nag->conf->button_text); cairo_move_to(cairo, button->x + border + padding, - button->y + border + (button->height - text_height) / 2); + button->y + (button->height - text_height) / 2); render_text(cairo, nag->conf->font_description, 1, true, "%s", button->text); } @@ -415,10 +415,10 @@ render_detailed(cairo_t *cairo, struct nag *nag, uint32_t y) nag->details.button_down.x = nag->details.x + nag->details.width; nag->details.button_down.y = - nag->details.button_up.y + nag->details.button_up.height; + nag->details.button_up.y + nag->details.button_up.height - border; nag->details.button_down.width = button_width; nag->details.button_down.height = - border_rect_height - nag->details.button_up.height; + border_rect_height - nag->details.button_up.height + border; render_details_scroll_button(cairo, nag, &nag->details.button_down); }