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

commitaea9f3a2e41ca567c82ac909f50cd6552d5e8321
parent27218cab21
authorIsaac Freund <[email protected]>
date2025-07-02 10:09
rwm: add river_output_v1.wl_output

 protocol/river-window-management-v1.xml | 24 ++++++++++++++++++++++++
 river/Output.zig                        | 32 ++++++++++++++++++++++++++++++++
 rivercompat/Output.zig                  |  1 +
 3 files changed, 57 insertions(+)

diff --git a/protocol/river-window-management-v1.xml b/protocol/river-window-management-v1.xml
index d200375..86130ac 100644
--- a/protocol/river-window-management-v1.xml
+++ b/protocol/river-window-management-v1.xml
@@ -1133,6 +1133,30 @@
       </description>
     </event>
 
+    <event name="wl_output">
+      <description summary="corresponding wl_output">
+        The wl_output object corresponding to the river_output_v1.
+
+        This event is sent exactly once. The wl_output associated with a
+        river_output_v1 cannot change. It is guaranteed that there is a 1-to-1
+        mapping between wl_output and river_output_v1 objects.
+
+        Note that this event will only be sent after the client binds the
+        wl_output global. It is guaranteed that the corresponding wl_output is
+        advertised before the river_window_manager_v1.output event is sent.
+
+        The global_remove event for the corresponding wl_output may be sent
+        before the river_output_v1.remove event. This is due to the fact that
+        river_output_v1 state changes are synced to the river window management
+        update sequence while changes to globals are not.
+
+        Rationale: The window manager may need information provided by the
+        wl_output interface such as the name/description. It also may need the
+        wl_output object to start screencopy for example.
+      </description>
+      <arg name="wl_output" type="object" interface="wl_output"/>
+    </event>
+
     <event name="position">
       <description summary="output position">
         This event indicates the position of the output in the compositor's
diff --git a/river/Output.zig b/river/Output.zig
index 52f5b50..0c9215f 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -132,6 +132,7 @@ wlr_output: ?*wlr.Output,
 scene_output: ?*wlr.SceneOutput,
 
 object: ?*river.OutputV1 = null,
+sent_wl_output: bool = false,
 
 /// Tracks the currently presented frame on the output as it pertains to ext-session-lock.
 /// The output is initially considered blanked:
@@ -171,6 +172,7 @@ link_sent: wl.list.Link,
 current: State,
 
 destroy: wl.Listener(*wlr.Output) = .init(handleDestroy),
+bind: wl.Listener(*wlr.Output.event.Bind) = .init(handleBind),
 request_state: wl.Listener(*wlr.Output.event.RequestState) = .init(handleRequestState),
 frame: wl.Listener(*wlr.Output) = .init(handleFrame),
 present: wl.Listener(*wlr.Output.event.Present) = .init(handlePresent),
@@ -210,6 +212,7 @@ pub fn create(wlr_output: *wlr.Output) !void {
     output.link_sent.init();
 
     wlr_output.events.destroy.add(&output.destroy);
+    wlr_output.events.bind.add(&output.bind);
     wlr_output.events.request_state.add(&output.request_state);
     wlr_output.events.frame.add(&output.frame);
     wlr_output.events.present.add(&output.present);
@@ -230,6 +233,7 @@ fn handleDestroy(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) v
     output.link.remove();
 
     output.destroy.link.remove();
+    output.bind.link.remove();
     output.request_state.link.remove();
     output.frame.link.remove();
     output.present.link.remove();
@@ -243,6 +247,20 @@ fn handleDestroy(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) v
     server.wm.dirtyWindowing();
 }
 
+fn handleBind(listener: *wl.Listener(*wlr.Output.event.Bind), event: *wlr.Output.event.Bind) void {
+    const output: *Output = @fieldParentPtr("bind", listener);
+    // Guard against a client binding the same wl_output global more than once.
+    if (output.sent_wl_output) {
+        return;
+    }
+    if (output.object) |output_v1| {
+        if (output_v1.getClient() == event.resource.getClient()) {
+            output_v1.sendWlOutput(event.resource);
+            output.sent_wl_output = true;
+        }
+    }
+}
+
 pub fn updateWindowingStart(output: *Output) void {
     switch (output.scheduled.state) {
         .enabled, .disabled_soft => {
@@ -265,6 +283,20 @@ pub fn updateWindowingStart(output: *Output) void {
                 const pending = &output.scheduled;
                 const sent = &output.sent;
 
+                if (new) {
+                    const client = output_v1.getClient();
+                    var it = output.wlr_output.?.resources.iterator(.forward);
+                    while (it.next()) |wl_output| {
+                        if (client == wl_output.getClient()) {
+                            output_v1.sendWlOutput(wl_output);
+                            output.sent_wl_output = true;
+                            break;
+                        }
+                    } else {
+                        output.sent_wl_output = false;
+                    }
+                }
+
                 if (new or pending.width() != sent.width() or pending.height() != sent.height()) {
                     output_v1.sendDimensions(pending.width(), pending.height());
                 }
diff --git a/rivercompat/Output.zig b/rivercompat/Output.zig
index 7480e32..fe6d26d 100644
--- a/rivercompat/Output.zig
+++ b/rivercompat/Output.zig
@@ -64,6 +64,7 @@ fn handleEvent(output_v1: *river.OutputV1, event: river.OutputV1.Event, output:
     assert(output.output_v1 == output_v1);
     switch (event) {
         .removed => output.pending.removed = true,
+        .wl_output => {},
         .position => |args| {
             output.x = args.x;
             output.y = args.y;