git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit2afcc3f25b36ba7dcad451bce8a1561ccdef0ac7
parentf67ec3b7a2
authorIsaac Freund <[email protected]>
date2026-01-15 13:35
protocol: add xkb_binding_v1.stop_repeat

 protocol/river-xkb-bindings-v1.xml | 15 +++++++++++++++
 river/KeyboardGroup.zig            | 11 +++++++++++
 river/Seat.zig                     |  6 ++++++
 river/XkbBinding.zig               | 15 ++++++++++++---
 4 files changed, 44 insertions(+), 3 deletions(-)

diff --git a/protocol/river-xkb-bindings-v1.xml b/protocol/river-xkb-bindings-v1.xml
index 888eeea..bc41dea 100644
--- a/protocol/river-xkb-bindings-v1.xml
+++ b/protocol/river-xkb-bindings-v1.xml
@@ -188,6 +188,21 @@
         input events is finite.
       </description>
     </event>
+
+    <event name="stop_repeat" since="2">
+      <description summary="repeating should be stopped">
+        This event indicates that repeating should be stopped for the binding if
+        the window manager has been repeating some action since the pressed
+        event.
+
+        This event is generally sent when some other (possible unbound) key is
+        pressed after the pressed event is sent and before the released event
+        is sent for this binding.
+
+        This event will be followed by a manage_start event after all other new
+        state has been sent by the server.
+      </description>
+    </event>
   </interface>
 
   <interface name="river_xkb_bindings_seat_v1" version="2">
diff --git a/river/KeyboardGroup.zig b/river/KeyboardGroup.zig
index 0a9f06b..f079ebc 100644
--- a/river/KeyboardGroup.zig
+++ b/river/KeyboardGroup.zig
@@ -176,6 +176,17 @@ fn handleKey(listener: *wl.Listener(*wlr.Keyboard.event.Key), event: *wlr.Keyboa
         return;
     };
 
+    {
+        var it = group.seat.keyboard_groups.iterator(.forward);
+        while (it.next()) |g| {
+            for (g.pressed.values()) |press| {
+                if (press.consumer != .binding) continue;
+                const binding = press.consumer.binding orelse continue;
+                binding.stopRepeat();
+            }
+        }
+    }
+
     // Every sent press event, to a regular client or the input method, should have
     // the corresponding release event sent to the same client.
     // Similarly, no press event means no release event.
diff --git a/river/Seat.zig b/river/Seat.zig
index 48c906e..2580a26 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -435,6 +435,12 @@ pub fn manageStart(seat: *Seat) void {
                         binding.sent_pressed = true;
                         binding.object.sendPressed();
                     },
+                    .stop_repeat => {
+                        assert(binding.sent_pressed);
+                        if (binding.object.getVersion() >= 2) {
+                            binding.object.sendStopRepeat();
+                        }
+                    },
                     .released => {
                         assert(binding.sent_pressed);
                         binding.sent_pressed = false;
diff --git a/river/XkbBinding.zig b/river/XkbBinding.zig
index 943924c..0ba1ebb 100644
--- a/river/XkbBinding.zig
+++ b/river/XkbBinding.zig
@@ -29,6 +29,7 @@ wm_scheduled: struct {
     state_change: enum {
         none,
         pressed,
+        stop_repeat,
         released,
     } = .none,
 } = .{},
@@ -136,18 +137,26 @@ fn handleRequest(
 
 pub fn pressed(binding: *XkbBinding) void {
     assert(!binding.sent_pressed);
-    // Input event processing should not continue after a press/release event
+    // Input event processing should not continue after a state_change
     // until that event is sent to the window manager in an update and acked.
     assert(binding.wm_scheduled.state_change == .none);
     binding.wm_scheduled.state_change = .pressed;
     server.wm.dirtyWindowing();
 }
 
-pub fn released(binding: *XkbBinding) void {
+pub fn stopRepeat(binding: *XkbBinding) void {
     assert(binding.sent_pressed);
-    // Input event processing should not continue after a press/release event
+    // Input event processing should not continue after a state change
     // until that event is sent to the window manager in an update and acked.
     assert(binding.wm_scheduled.state_change == .none);
+    binding.wm_scheduled.state_change = .stop_repeat;
+    server.wm.dirtyWindowing();
+}
+
+pub fn released(binding: *XkbBinding) void {
+    assert(binding.sent_pressed);
+    // stopRepeat() should always be called before released() by KeyboardGroup
+    assert(binding.wm_scheduled.state_change == .stop_repeat);
     binding.wm_scheduled.state_change = .released;
     server.wm.dirtyWindowing();
 }