From fe759d2d8a050c33cd7e6cf44202ec14ea1d30e1 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Wed, 25 Jun 2025 23:21:01 +0200 Subject: [PATCH 1/7] Keyboard: don't add virtual keyboards to group Also don't set their keymap, the client handles that. This is the first step towards fixing a regression with fcitx5. --- river/InputManager.zig | 6 +++--- river/Keyboard.zig | 14 ++++++++------ river/Seat.zig | 10 +++++----- 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/river/InputManager.zig b/river/InputManager.zig index d0ab7c6..fe99da8 100644 --- a/river/InputManager.zig +++ b/river/InputManager.zig @@ -160,7 +160,7 @@ pub fn reconfigureDevices(input_manager: *InputManager) void { fn handleNewInput(listener: *wl.Listener(*wlr.InputDevice), wlr_device: *wlr.InputDevice) void { const input_manager: *InputManager = @fieldParentPtr("new_input", listener); - input_manager.defaultSeat().addDevice(wlr_device); + input_manager.defaultSeat().addDevice(wlr_device, false); } fn handleNewVirtualPointer( @@ -178,7 +178,7 @@ fn handleNewVirtualPointer( log.debug("Ignoring output suggestion from virtual pointer", .{}); } - input_manager.defaultSeat().addDevice(&event.new_pointer.pointer.base); + input_manager.defaultSeat().addDevice(&event.new_pointer.pointer.base, true); } fn handleNewVirtualKeyboard( @@ -186,7 +186,7 @@ fn handleNewVirtualKeyboard( virtual_keyboard: *wlr.VirtualKeyboardV1, ) void { const seat: *Seat = @alignCast(@ptrCast(virtual_keyboard.seat.data)); - seat.addDevice(&virtual_keyboard.keyboard.base); + seat.addDevice(&virtual_keyboard.keyboard.base, true); } fn handleNewConstraint( diff --git a/river/Keyboard.zig b/river/Keyboard.zig index 0fffefa..9f2b92b 100644 --- a/river/Keyboard.zig +++ b/river/Keyboard.zig @@ -88,7 +88,7 @@ pressed: Pressed = .{}, key: wl.Listener(*wlr.Keyboard.event.Key) = wl.Listener(*wlr.Keyboard.event.Key).init(handleKey), modifiers: wl.Listener(*wlr.Keyboard) = wl.Listener(*wlr.Keyboard).init(handleModifiers), -pub fn init(keyboard: *Keyboard, seat: *Seat, wlr_device: *wlr.InputDevice) !void { +pub fn init(keyboard: *Keyboard, seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) !void { keyboard.* = .{ .device = undefined, }; @@ -98,12 +98,14 @@ pub fn init(keyboard: *Keyboard, seat: *Seat, wlr_device: *wlr.InputDevice) !voi const wlr_keyboard = keyboard.device.wlr_device.toKeyboard(); wlr_keyboard.data = keyboard; - // wlroots will log a more detailed error if this fails. - if (!wlr_keyboard.setKeymap(server.config.keymap)) return error.OutOfMemory; + if (!virtual) { + // wlroots will log a more detailed error if this fails. + if (!wlr_keyboard.setKeymap(server.config.keymap)) return error.OutOfMemory; - if (wlr.KeyboardGroup.fromKeyboard(wlr_keyboard) == null) { - // wlroots will log an error on failure - _ = seat.keyboard_group.addKeyboard(wlr_keyboard); + if (wlr.KeyboardGroup.fromKeyboard(wlr_keyboard) == null) { + // wlroots will log an error on failure + _ = seat.keyboard_group.addKeyboard(wlr_keyboard); + } } wlr_keyboard.setRepeatInfo(server.config.repeat_rate, server.config.repeat_delay); diff --git a/river/Seat.zig b/river/Seat.zig index 626eb6b..32b2186 100644 --- a/river/Seat.zig +++ b/river/Seat.zig @@ -127,7 +127,7 @@ pub fn init(seat: *Seat, name: [*:0]const u8) !void { try seat.cursor.init(seat); seat.relay.init(); - try seat.tryAddDevice(&seat.keyboard_group.keyboard.base); + try seat.tryAddDevice(&seat.keyboard_group.keyboard.base, false); seat.wlr_seat.events.request_set_selection.add(&seat.request_set_selection); seat.wlr_seat.events.request_start_drag.add(&seat.request_start_drag); @@ -480,19 +480,19 @@ fn handleMappingRepeatTimeout(seat: *Seat) c_int { return 0; } -pub fn addDevice(seat: *Seat, wlr_device: *wlr.InputDevice) void { - seat.tryAddDevice(wlr_device) catch |err| switch (err) { +pub fn addDevice(seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) void { + seat.tryAddDevice(wlr_device, virtual) catch |err| switch (err) { error.OutOfMemory => log.err("out of memory", .{}), }; } -fn tryAddDevice(seat: *Seat, wlr_device: *wlr.InputDevice) !void { +fn tryAddDevice(seat: *Seat, wlr_device: *wlr.InputDevice, virtual: bool) !void { switch (wlr_device.type) { .keyboard => { const keyboard = try util.gpa.create(Keyboard); errdefer util.gpa.destroy(keyboard); - try keyboard.init(seat, wlr_device); + try keyboard.init(seat, wlr_device, virtual); seat.wlr_seat.setKeyboard(keyboard.device.wlr_device.toKeyboard()); if (seat.wlr_seat.keyboard_state.focused_surface) |wlr_surface| { From d08c170571722348eca3c665612704c6e7e3d800 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Mon, 30 Jun 2025 12:44:31 +0200 Subject: [PATCH 2/7] Seat: ignore virtual keyboards until keymap set The wlr-virtual-keyboard-v1 protocol fails to make keyboard creation atomic. There is nothing a client can do with a virtual keyboard that has no keymap except for destroy it, so we can completely ignore them until a keymap is set. This fixes a regression with fcitx caused by river sending the null keymap of a new virtual keyboard to fcitx. --- river/InputManager.zig | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/river/InputManager.zig b/river/InputManager.zig index fe99da8..90497b9 100644 --- a/river/InputManager.zig +++ b/river/InputManager.zig @@ -185,10 +185,46 @@ fn handleNewVirtualKeyboard( _: *wl.Listener(*wlr.VirtualKeyboardV1), virtual_keyboard: *wlr.VirtualKeyboardV1, ) void { - const seat: *Seat = @alignCast(@ptrCast(virtual_keyboard.seat.data)); - seat.addDevice(&virtual_keyboard.keyboard.base, true); + const no_keymap = util.gpa.create(NoKeymapVirtKeyboard) catch { + log.err("out of memory", .{}); + return; + }; + errdefer util.gpa.destroy(no_keymap); + + no_keymap.* = .{ + .virtual_keyboard = virtual_keyboard, + }; + virtual_keyboard.keyboard.base.events.destroy.add(&no_keymap.destroy); + virtual_keyboard.keyboard.events.keymap.add(&no_keymap.keymap); } +/// Ignore virtual keyboards completely until the client sets a keymap +/// Yes, wlroots should probably do this for us. +const NoKeymapVirtKeyboard = struct { + virtual_keyboard: *wlr.VirtualKeyboardV1, + destroy: wl.Listener(*wlr.InputDevice) = .init(handleDestroy), + keymap: wl.Listener(*wlr.Keyboard) = .init(handleKeymap), + + fn handleDestroy(listener: *wl.Listener(*wlr.InputDevice), _: *wlr.InputDevice) void { + const no_keymap: *NoKeymapVirtKeyboard = @fieldParentPtr("destroy", listener); + + no_keymap.destroy.link.remove(); + no_keymap.keymap.link.remove(); + + util.gpa.destroy(no_keymap); + } + + fn handleKeymap(listener: *wl.Listener(*wlr.Keyboard), _: *wlr.Keyboard) void { + const no_keymap: *NoKeymapVirtKeyboard = @fieldParentPtr("keymap", listener); + const virtual_keyboard = no_keymap.virtual_keyboard; + + handleDestroy(&no_keymap.destroy, &virtual_keyboard.keyboard.base); + + const seat: *Seat = @alignCast(@ptrCast(virtual_keyboard.seat.data)); + seat.addDevice(&virtual_keyboard.keyboard.base, true); + } +}; + fn handleNewConstraint( _: *wl.Listener(*wlr.PointerConstraintV1), wlr_constraint: *wlr.PointerConstraintV1, From 1db51285c0a02d7716ce1658bcbf743dc49fb43c Mon Sep 17 00:00:00 2001 From: Peter Kaplan Date: Mon, 30 Jun 2025 14:19:05 +0200 Subject: [PATCH 3/7] ext-foreign-toplevel-list-v1: implement protocol --- build.zig.zon | 4 ++-- river/Server.zig | 5 +++++ river/View.zig | 30 ++++++++++++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index cc8d63e..df0d375 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -17,8 +17,8 @@ .hash = "wayland-0.3.0-lQa1kjPIAQDmhGYpY-zxiRzQJFHQ2VqhJkQLbKKdt5wl", }, .wlroots = .{ - .url = "https://codeberg.org/ifreund/zig-wlroots/archive/v0.19.1.tar.gz", - .hash = "wlroots-0.19.1-jmOlcs7dAwCajnVWlQZIc-ySYjRlbLxy0F5FvTQqYA3P", + .url = "https://codeberg.org/ifreund/zig-wlroots/archive/bb222027d3ab96cca4d217c5b3bab616edc14632.tar.gz", + .hash = "wlroots-0.19.1-jmOlcsnnAwA88XV5BSNEhufSaNzlSmKNcvhkFSv1nGJa", }, .xkbcommon = .{ .url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.3.0.tar.gz", diff --git a/river/Server.zig b/river/Server.zig index 58c7eb6..35f50c5 100644 --- a/river/Server.zig +++ b/river/Server.zig @@ -85,6 +85,8 @@ screencopy_manager: *wlr.ScreencopyManagerV1, foreign_toplevel_manager: *wlr.ForeignToplevelManagerV1, +foreign_toplevel_list: *wlr.ExtForeignToplevelListV1, + tearing_control_manager: *wlr.TearingControlManagerV1, alpha_modifier: *wlr.AlphaModifierV1, @@ -164,6 +166,8 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void { .foreign_toplevel_manager = try wlr.ForeignToplevelManagerV1.create(wl_server), + .foreign_toplevel_list = try wlr.ExtForeignToplevelListV1.create(wl_server, 1), + .tearing_control_manager = try wlr.TearingControlManagerV1.create(wl_server, 1), .alpha_modifier = try wlr.AlphaModifierV1.create(wl_server), @@ -344,6 +348,7 @@ fn blocklist(server: *Server, global: *const wl.Global) bool { return global == server.security_context_manager.global or global == server.layer_shell.global or global == server.foreign_toplevel_manager.global or + global == server.foreign_toplevel_list.global or global == server.screencopy_manager.global or global == server.export_dmabuf_manager.global or global == server.data_control_manager.global or diff --git a/river/View.zig b/river/View.zig index 3ba48f5..9ce576f 100644 --- a/river/View.zig +++ b/river/View.zig @@ -181,6 +181,8 @@ post_fullscreen_box: wlr.Box = undefined, foreign_toplevel_handle: ForeignToplevelHandle = .{}, +ext_foreign_toplevel_handle: ?*wlr.ExtForeignToplevelHandleV1 = null, + /// Connector name of the output this view occupied before an evacuation. output_before_evac: ?[]const u8 = null, @@ -656,6 +658,15 @@ pub fn map(view: *View) !void { assert(!view.mapped and !view.destroying); view.mapped = true; + if (wlr.ExtForeignToplevelHandleV1.create(server.foreign_toplevel_list, &.{ + .title = view.getTitle(), + .app_id = view.getAppId(), + })) |handle| { + view.ext_foreign_toplevel_handle = handle; + } else |_| { + log.err("failed to create ext foreign toplevel handle", .{}); + } + view.foreign_toplevel_handle.map(); if (server.config.rules.float.match(view)) |float| { @@ -745,6 +756,10 @@ pub fn unmap(view: *View) void { assert(view.mapped and !view.destroying); view.mapped = false; + if (view.ext_foreign_toplevel_handle) |handle| { + handle.destroy(); + view.ext_foreign_toplevel_handle = null; + } view.foreign_toplevel_handle.unmap(); server.root.applyPending(); @@ -754,6 +769,14 @@ pub fn notifyTitle(view: *const View) void { if (view.foreign_toplevel_handle.wlr_handle) |wlr_handle| { if (view.getTitle()) |title| wlr_handle.setTitle(title); } + + if (view.ext_foreign_toplevel_handle) |handle| { + handle.updateState(&.{ + .title = view.getTitle(), + .app_id = view.getAppId(), + }); + } + // Send title to all status listeners attached to a seat which focuses this view var seat_it = server.input_manager.seats.first; while (seat_it) |seat_node| : (seat_it = seat_node.next) { @@ -770,4 +793,11 @@ pub fn notifyAppId(view: View) void { if (view.foreign_toplevel_handle.wlr_handle) |wlr_handle| { if (view.getAppId()) |app_id| wlr_handle.setAppId(app_id); } + + if (view.ext_foreign_toplevel_handle) |handle| { + handle.updateState(&.{ + .title = view.getTitle(), + .app_id = view.getAppId(), + }); + } } From d2520301cdee5a85a064130afc15d80f99c274ec Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Tue, 1 Jul 2025 09:36:30 +0200 Subject: [PATCH 4/7] build: switch to zig-wlroots tag --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index df0d375..da31a63 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -17,8 +17,8 @@ .hash = "wayland-0.3.0-lQa1kjPIAQDmhGYpY-zxiRzQJFHQ2VqhJkQLbKKdt5wl", }, .wlroots = .{ - .url = "https://codeberg.org/ifreund/zig-wlroots/archive/bb222027d3ab96cca4d217c5b3bab616edc14632.tar.gz", - .hash = "wlroots-0.19.1-jmOlcsnnAwA88XV5BSNEhufSaNzlSmKNcvhkFSv1nGJa", + .url = "https://codeberg.org/ifreund/zig-wlroots/archive/v0.19.2.tar.gz", + .hash = "wlroots-0.19.2-jmOlcsnnAwDFAeOamkUaxyHNhKngH4Ai5rrLSVbqA8LW", }, .xkbcommon = .{ .url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.3.0.tar.gz", From 0898a06a967516ecade18b0213d33c2d301c8686 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Tue, 1 Jul 2025 09:36:57 +0200 Subject: [PATCH 5/7] build: bump version to 0.3.11 --- build.zig.zon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig.zon b/build.zig.zon index da31a63..e4ed983 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,7 +5,7 @@ // When a release is tagged, the "-dev" suffix should be removed for the // commit that gets tagged. Directly after the tagged commit, the version // should be bumped and the "-dev" suffix added. - .version = "0.3.11-dev", + .version = "0.3.11", .paths = .{""}, .dependencies = .{ .pixman = .{ From b8ec02796f7bb5149f311602fda01afa0f57a917 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Tue, 1 Jul 2025 09:40:25 +0200 Subject: [PATCH 6/7] build: bump version to 0.3.12-dev --- build.zig.zon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.zig.zon b/build.zig.zon index e4ed983..e61c988 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,7 +5,7 @@ // When a release is tagged, the "-dev" suffix should be removed for the // commit that gets tagged. Directly after the tagged commit, the version // should be bumped and the "-dev" suffix added. - .version = "0.3.11", + .version = "0.3.12-dev", .paths = .{""}, .dependencies = .{ .pixman = .{ From 63542fdf3e0815e1ededa781e2c0195fc9f79e42 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Thu, 10 Jul 2025 14:55:38 +0200 Subject: [PATCH 7/7] build: bump zig-wlroots version This version includes a fix for a regression introduced with the wlroots 0.19 upgrade. --- build.zig.zon | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.zig.zon b/build.zig.zon index e61c988..109eb1b 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -17,8 +17,8 @@ .hash = "wayland-0.3.0-lQa1kjPIAQDmhGYpY-zxiRzQJFHQ2VqhJkQLbKKdt5wl", }, .wlroots = .{ - .url = "https://codeberg.org/ifreund/zig-wlroots/archive/v0.19.2.tar.gz", - .hash = "wlroots-0.19.2-jmOlcsnnAwDFAeOamkUaxyHNhKngH4Ai5rrLSVbqA8LW", + .url = "https://codeberg.org/ifreund/zig-wlroots/archive/f92ba27133ecf702d85c9d3894f98a336389bbd9.tar.gz", + .hash = "wlroots-0.19.3-dev-jmOlcr7_AwClfjFwW8oOkWoqAbt9oPLqgdvfFYEXqlOF", }, .xkbcommon = .{ .url = "https://codeberg.org/ifreund/zig-xkbcommon/archive/v0.3.0.tar.gz",