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

commit7d0e613f40d4c713ef7a32a251e35423119e03f0
parent8b292e8dd6
authorIsaac Freund <[email protected]>
date2026-03-30 10:53
ext-image-capture-source: support toplevels

 river/LayerSurface.zig   |  1 +
 river/Server.zig         | 27 +++++++++++++++++++++++++++
 river/Window.zig         |  8 ++++++++
 river/XdgPopup.zig       |  6 ++++++
 river/XdgToplevel.zig    |  8 +++++++-
 river/XwaylandWindow.zig |  6 ++++++
 6 files changed, 55 insertions(+), 1 deletion(-)

diff --git a/river/LayerSurface.zig b/river/LayerSurface.zig
index e8557f5..9d29b12 100644
--- a/river/LayerSurface.zig
+++ b/river/LayerSurface.zig
@@ -173,6 +173,7 @@ fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.Xdg
         wlr_xdg_popup,
         layer_surface.popup_tree,
         layer_surface.popup_tree,
+        null,
     ) catch {
         wlr_xdg_popup.resource.postNoMemory();
         return;
diff --git a/river/Server.zig b/river/Server.zig
index 44c30fc..89ee50f 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -25,6 +25,7 @@ const Scene = @import("Scene.zig");
 const SceneNodeData = @import("SceneNodeData.zig");
 const Seat = @import("Seat.zig");
 const TabletTool = @import("TabletTool.zig");
+const Window = @import("Window.zig");
 const WindowManager = @import("WindowManager.zig");
 const XkbBindings = @import("XkbBindings.zig");
 const LayerShell = @import("LayerShell.zig");
@@ -84,6 +85,7 @@ image_copy_capture_manager: *wlr.ExtImageCopyCaptureManagerV1,
 output_image_capture_source_manager: *wlr.ExtOutputImageCaptureSourceManagerV1,
 
 foreign_toplevel_list: *wlr.ExtForeignToplevelListV1,
+toplevel_capture_source_manager: *wlr.ExtForeignToplevelImageCaptureSourceManagerV1,
 
 tearing_control_manager: *wlr.TearingControlManagerV1,
 
@@ -107,6 +109,7 @@ new_xdg_toplevel: wl.Listener(*wlr.XdgToplevel) = .init(handleNewXdgToplevel),
 new_toplevel_decoration: wl.Listener(*wlr.XdgToplevelDecorationV1) = .init(handleNewToplevelDecoration),
 request_activate: wl.Listener(*wlr.XdgActivationV1.event.RequestActivate) = .init(handleRequestActivate),
 request_set_cursor_shape: wl.Listener(*wlr.CursorShapeManagerV1.event.RequestSetShape) = .init(handleRequestSetCursorShape),
+toplevel_capture_request: wl.Listener(*wlr.ExtForeignToplevelImageCaptureSourceManagerV1.Request) = .init(handleToplevelCaptureRequest),
 
 pub fn init(server: *Server, runtime_xwayland: bool) !void {
     // We intentionally don't try to prevent memory leaks on error in this function
@@ -164,6 +167,7 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void {
         .output_image_capture_source_manager = try wlr.ExtOutputImageCaptureSourceManagerV1.create(wl_server, 1),
 
         .foreign_toplevel_list = try wlr.ExtForeignToplevelListV1.create(wl_server, 1),
+        .toplevel_capture_source_manager = try wlr.ExtForeignToplevelImageCaptureSourceManagerV1.create(wl_server, 1),
 
         .tearing_control_manager = try wlr.TearingControlManagerV1.create(wl_server, 1),
 
@@ -227,6 +231,7 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void {
     server.xdg_decoration_manager.events.new_toplevel_decoration.add(&server.new_toplevel_decoration);
     server.xdg_activation.events.request_activate.add(&server.request_activate);
     server.cursor_shape_manager.events.request_set_shape.add(&server.request_set_cursor_shape);
+    server.toplevel_capture_source_manager.events.new_request.add(&server.toplevel_capture_request);
 
     wl_server.setGlobalFilter(*Server, globalFilter, server);
 }
@@ -241,6 +246,7 @@ pub fn deinit(server: *Server) void {
     server.new_toplevel_decoration.link.remove();
     server.request_activate.link.remove();
     server.request_set_cursor_shape.link.remove();
+    server.toplevel_capture_request.link.remove();
 
     server.input_manager.new_input.link.remove();
     server.om.new_output.link.remove();
@@ -374,6 +380,7 @@ fn blocklist(server: *Server, global: *const wl.Global) bool {
         global == server.image_copy_capture_manager.global or
         global == server.output_image_capture_source_manager.global or
         global == server.foreign_toplevel_list.global or
+        global == server.toplevel_capture_source_manager.global or
         global == server.export_dmabuf_manager.global or
         global == server.data_control_manager.global or
         global == server.wlr_data_control_manager.global or
@@ -538,3 +545,23 @@ fn handleRequestSetCursorShape(
         }
     }
 }
+
+fn handleToplevelCaptureRequest(
+    listener: *wl.Listener(*wlr.ExtForeignToplevelImageCaptureSourceManagerV1.Request),
+    request: *wlr.ExtForeignToplevelImageCaptureSourceManagerV1.Request,
+) void {
+    const server: *Server = @fieldParentPtr("toplevel_capture_request", listener);
+    const window = @as(?*Window, @ptrCast(@alignCast(request.toplevel_handle.data))) orelse return;
+
+    const capture_source = window.capture_source orelse wlr.ExtImageCaptureSourceV1.createWithSceneNode(
+        &window.capture_scene.tree.node,
+        server.wl_server.getEventLoop(),
+        server.allocator,
+        server.renderer,
+    ) catch {
+        log.err("failed to create ext image capture source", .{});
+        return;
+    };
+
+    _ = request.accept(capture_source);
+}
diff --git a/river/Window.zig b/river/Window.zig
index 31e1f7a..9b05f45 100644
--- a/river/Window.zig
+++ b/river/Window.zig
@@ -201,6 +201,9 @@ decorations_above_tree: *wlr.SceneTree,
 
 popup_tree: *wlr.SceneTree,
 
+capture_scene: *wlr.Scene,
+capture_source: ?*wlr.ExtImageCaptureSourceV1 = null,
+
 /// State to be sent to the wm in the next manage sequence.
 wm_scheduled: struct {
     dimensions_hint: DimensionsHint = .{},
@@ -297,6 +300,7 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
         .decorations_above = undefined,
         .decorations_above_tree = try tree.createSceneTree(),
         .popup_tree = popup_tree,
+        .capture_scene = try wlr.Scene.create(),
     };
 
     window.node.init(.window);
@@ -308,6 +312,8 @@ pub fn create(impl: Impl) error{OutOfMemory}!*Window {
     window.popup_tree.node.setEnabled(false);
     window.fullscreen_background.node.setEnabled(false);
 
+    window.capture_scene.restack_xwayland_surfaces = false;
+
     try SceneNodeData.attach(&window.tree.node, .{ .window = window });
     try SceneNodeData.attach(&window.popup_tree.node, .{ .window = window });
 
@@ -345,6 +351,7 @@ pub fn destroy(window: *Window) void {
 
     window.tree.node.destroy();
     window.popup_tree.node.destroy();
+    window.capture_scene.tree.node.destroy();
 
     window.node.deinit();
 
@@ -417,6 +424,7 @@ pub fn manageStart(window: *Window) void {
                         .app_id = window.getAppId(),
                     })) |handle| {
                         window.foreign_toplevel_handle = handle;
+                        handle.data = window;
                     } else |_| {
                         log.err("failed to create ext foreign toplevel handle", .{});
                     }
diff --git a/river/XdgPopup.zig b/river/XdgPopup.zig
index ebbc509..c072d9a 100644
--- a/river/XdgPopup.zig
+++ b/river/XdgPopup.zig
@@ -20,6 +20,7 @@ wlr_popup: *wlr.XdgPopup,
 root: *wlr.SceneTree,
 
 tree: *wlr.SceneTree,
+capture_tree: ?*wlr.SceneTree = null,
 
 destroy: wl.Listener(void) = .init(handleDestroy),
 commit: wl.Listener(*wlr.Surface) = .init(handleCommit),
@@ -31,6 +32,7 @@ pub fn create(
     wlr_popup: *wlr.XdgPopup,
     root: *wlr.SceneTree,
     parent: *wlr.SceneTree,
+    capture_parent: ?*wlr.SceneTree,
 ) error{OutOfMemory}!void {
     const xdg_popup = try util.gpa.create(XdgPopup);
     errdefer util.gpa.destroy(xdg_popup);
@@ -40,6 +42,9 @@ pub fn create(
         .root = root,
         .tree = try parent.createSceneXdgSurface(wlr_popup.base),
     };
+    if (capture_parent) |p| {
+        xdg_popup.capture_tree = try p.createSceneXdgSurface(wlr_popup.base);
+    }
 
     wlr_popup.events.destroy.add(&xdg_popup.destroy);
     wlr_popup.base.surface.events.commit.add(&xdg_popup.commit);
@@ -73,6 +78,7 @@ fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_popup: *wlr.XdgPopu
         wlr_popup,
         xdg_popup.root,
         xdg_popup.tree,
+        xdg_popup.capture_tree,
     ) catch {
         wlr_popup.resource.postNoMemory();
         return;
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index 11cb497..2971062 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -84,6 +84,7 @@ pub fn create(wlr_toplevel: *wlr.XdgToplevel) error{OutOfMemory}!void {
     errdefer toplevel.unmap.link.remove();
 
     _ = try window.surfaces.tree.createSceneXdgSurface(wlr_toplevel.base);
+    _ = try window.capture_scene.tree.createSceneXdgSurface(wlr_toplevel.base);
 
     toplevel.window = window;
 
@@ -288,7 +289,12 @@ fn handleUnmap(listener: *wl.Listener(void)) void {
 fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.XdgPopup) void {
     const toplevel: *XdgToplevel = @fieldParentPtr("new_popup", listener);
 
-    XdgPopup.create(wlr_xdg_popup, toplevel.window.popup_tree, toplevel.window.popup_tree) catch {
+    XdgPopup.create(
+        wlr_xdg_popup,
+        toplevel.window.popup_tree,
+        toplevel.window.popup_tree,
+        &toplevel.window.capture_scene.tree,
+    ) catch {
         wlr_xdg_popup.resource.postNoMemory();
         return;
     };
diff --git a/river/XwaylandWindow.zig b/river/XwaylandWindow.zig
index 5f42c73..33bca48 100644
--- a/river/XwaylandWindow.zig
+++ b/river/XwaylandWindow.zig
@@ -191,6 +191,12 @@ pub fn handleMap(listener: *wl.Listener(void)) void {
     };
     surface.data = &window.tree.node;
 
+    _ = window.capture_scene.tree.createSceneSurface(surface) catch {
+        log.err("out of memory", .{});
+        surface.resource.getClient().postNoMemory();
+        return;
+    };
+
     // TODO(wlroots) update the dimensions_hint if the size hints change
     // https://gitlab.freedesktop.org/wlroots/wlroots/-/merge_requests/5238
     if (xwindow.xsurface.size_hints) |size_hints| {