From b2f172e91b2d75b0b2af02621ab302afce3a94d6 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Mon, 11 May 2020 13:46:29 +0200 Subject: [PATCH] Implement xwayland unmanaged windows --- src/Root.zig | 10 +++ src/Server.zig | 12 +++- src/XwaylandUnmanaged.zig | 136 ++++++++++++++++++++++++++++++++++++++ src/render.zig | 116 ++++++++++++++------------------ 4 files changed, 207 insertions(+), 67 deletions(-) create mode 100644 src/XwaylandUnmanaged.zig diff --git a/src/Root.zig b/src/Root.zig index 8483969..f87d7ae 100644 --- a/src/Root.zig +++ b/src/Root.zig @@ -18,6 +18,7 @@ const Self = @This(); const std = @import("std"); +const build_options = @import("build_options"); const c = @import("c.zig"); @@ -26,6 +27,7 @@ const Output = @import("Output.zig"); const Server = @import("Server.zig"); const View = @import("View.zig"); const ViewStack = @import("view_stack.zig").ViewStack; +const XwaylandUnmanaged = @import("XwaylandUnmanaged.zig"); /// Responsible for all windowing operations server: *Server, @@ -37,6 +39,10 @@ outputs: std.TailQueue(Output), /// It is not advertised to clients. noop_output: Output, +/// This list stores all unmanaged Xwayland windows. This needs to be in root +/// since X is like the wild west and who knows where these things will go. +xwayland_unmanaged_views: if (build_options.xwayland) std.TailQueue(XwaylandUnmanaged) else void, + /// Number of pending configures sent in the current transaction. /// A value of 0 means there is no current transaction. pending_configures: u32, @@ -59,6 +65,10 @@ pub fn init(self: *Self, server: *Server) !void { return error.CantAddNoopOutput; try self.noop_output.init(self, noop_wlr_output); + if (build_options.xwayland) { + self.xwayland_unmanaged_views = std.TailQueue(XwaylandUnmanaged).init(); + } + self.pending_configures = 0; self.transaction_timer = null; diff --git a/src/Server.zig b/src/Server.zig index b617b04..944f297 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -30,6 +30,7 @@ const Output = @import("Output.zig"); const Root = @import("Root.zig"); const View = @import("View.zig"); const ViewStack = @import("view_stack.zig").ViewStack; +const XwaylandUnmanaged = @import("XwaylandUnmanaged.zig"); allocator: *std.mem.Allocator, @@ -105,7 +106,7 @@ pub fn init(self: *Self, allocator: *std.mem.Allocator) !void { self.listen_new_layer_surface.notify = handleNewLayerSurface; c.wl_signal_add(&self.wlr_layer_shell.events.new_surface, &self.listen_new_layer_surface); - // Set up xwayland if built with suport + // Set up xwayland if built with support if (build_options.xwayland) { self.wlr_xwayland = c.wlr_xwayland_create(self.wl_display, wlr_compositor, false) orelse return error.CantCreateWlrXwayland; @@ -247,6 +248,15 @@ fn handleNewXwaylandSurface(listener: ?*c.wl_listener, data: ?*c_void) callconv( @alignCast(@alignOf(*c.wlr_xwayland_surface), data), ); + if (wlr_xwayland_surface.override_redirect) { + Log.Debug.log("New unmanaged xwayland surface", .{}); + // The unmanged surface will add itself to the list of unmanaged views + // in Root when it is mapped. + const node = self.allocator.create(std.TailQueue(XwaylandUnmanaged).Node) catch unreachable; + node.data.init(&self.root, wlr_xwayland_surface); + return; + } + Log.Debug.log( "New xwayland surface: title '{}', class '{}'", .{ wlr_xwayland_surface.title, wlr_xwayland_surface.class }, diff --git a/src/XwaylandUnmanaged.zig b/src/XwaylandUnmanaged.zig new file mode 100644 index 0000000..fe33e73 --- /dev/null +++ b/src/XwaylandUnmanaged.zig @@ -0,0 +1,136 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2020 Isaac Freund +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +const Self = @This(); + +const std = @import("std"); + +const c = @import("c.zig"); + +const Box = @import("Box.zig"); +const Log = @import("log.zig").Log; +const Root = @import("Root.zig"); + +root: *Root, + +/// The corresponding wlroots object +wlr_xwayland_surface: *c.wlr_xwayland_surface, + +// Listeners that are always active over the view's lifetime +liseten_request_configure: c.wl_listener, +listen_destroy: c.wl_listener, +listen_map: c.wl_listener, +listen_unmap: c.wl_listener, + +// Listeners that are only active while the view is mapped +listen_commit: c.wl_listener, + +pub fn init(self: *Self, root: *Root, wlr_xwayland_surface: *c.wlr_xwayland_surface) void { + self.root = root; + self.wlr_xwayland_surface = wlr_xwayland_surface; + + // Add listeners that are active over the view's entire lifetime + self.liseten_request_configure.notify = handleRequestConfigure; + c.wl_signal_add(&wlr_xwayland_surface.events.request_configure, &self.liseten_request_configure); + + self.listen_destroy.notify = handleDestroy; + c.wl_signal_add(&wlr_xwayland_surface.events.destroy, &self.listen_destroy); + + self.listen_map.notify = handleMap; + c.wl_signal_add(&wlr_xwayland_surface.events.map, &self.listen_map); + + self.listen_unmap.notify = handleUnmap; + c.wl_signal_add(&wlr_xwayland_surface.events.unmap, &self.listen_unmap); +} + +/// Return the surface at output coordinates ox, oy and set sx, sy to the +/// corresponding surface-relative coordinates, if there is a surface. +pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surface { + return c.wlr_surface_surface_at( + self.wlr_xwayland_surface.surface, + ox - @intToFloat(f64, self.view.current_box.x), + oy - @intToFloat(f64, self.view.current_box.y), + sx, + sy, + ); +} + +fn handleRequestConfigure(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "liseten_request_configure", listener.?); + const wlr_xwayland_surface_configure_event = @ptrCast( + *c.wlr_xwayland_surface_configure_event, + @alignCast(@alignOf(*c.wlr_xwayland_surface_configure_event), data), + ); + c.wlr_xwayland_surface_configure( + self.wlr_xwayland_surface, + wlr_xwayland_surface_configure_event.x, + wlr_xwayland_surface_configure_event.y, + wlr_xwayland_surface_configure_event.width, + wlr_xwayland_surface_configure_event.height, + ); +} + +/// Called when the xwayland surface is destroyed +fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_destroy", listener.?); + + // Remove listeners that are active for the entire lifetime of the view + c.wl_list_remove(&self.listen_destroy.link); + c.wl_list_remove(&self.listen_map.link); + c.wl_list_remove(&self.listen_unmap.link); + + // Deallocate the node + const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + self.root.server.allocator.destroy(node); +} + +/// Called when the xwayland surface is mapped, or ready to display on-screen. +fn handleMap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_map", listener.?); + const root = self.root; + + // Add self to the list of unmanaged views in the root + const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + root.xwayland_unmanaged_views.prepend(node); + + // Add listeners that are only active while mapped + self.listen_commit.notify = handleCommit; + c.wl_signal_add(&self.wlr_xwayland_surface.surface.*.events.commit, &self.listen_commit); + + // TODO: handle keyboard focus + // if (wlr_xwayland_or_surface_wants_focus(self.wlr_xwayland_surface)) { ... +} + +/// Called when the surface is unmapped and will no longer be displayed. +fn handleUnmap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_unmap", listener.?); + + // Remove self from the list of unmanged views in the root + const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + self.root.xwayland_unmanaged_views.remove(node); + + // Remove listeners that are only active while mapped + c.wl_list_remove(&self.listen_commit.link); + + // TODO: return focus +} + +/// Called when the surface is comitted +fn handleCommit(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_commit", listener.?); + // TODO: check if the surface has moved for damage tracking +} diff --git a/src/render.zig b/src/render.zig index 9f1dc39..7341e18 100644 --- a/src/render.zig +++ b/src/render.zig @@ -15,6 +15,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +const build_options = @import("build_options"); const std = @import("std"); const c = @import("c.zig"); @@ -26,15 +27,13 @@ const Server = @import("Server.zig"); const View = @import("View.zig"); const ViewStack = @import("view_stack.zig").ViewStack; -const ViewRenderData = struct { +const SurfaceRenderData = struct { output: *const Output, - view: *View, - when: *c.timespec, -}; -const LayerSurfaceRenderData = struct { - output: *const Output, - layer_surface: *LayerSurface, + /// In output layout coordinates relative to the output + output_x: i32, + output_y: i32, + when: *c.timespec, }; @@ -94,6 +93,11 @@ pub fn renderOutput(output: *Output) void { renderBorders(output.*, view, &now); } + // Render xwayland unmanged views + if (build_options.xwayland) { + renderXwaylandUnmanaged(output.*, &now); + } + renderLayer(output.*, output.layers[c.ZWLR_LAYER_SHELL_V1_LAYER_TOP], &now); renderLayer(output.*, output.layers[c.ZWLR_LAYER_SHELL_V1_LAYER_OVERLAY], &now); @@ -120,67 +124,20 @@ fn renderLayer(output: Output, layer: std.TailQueue(LayerSurface), now: *c.times if (!layer_surface.mapped) { continue; } - var rdata = LayerSurfaceRenderData{ + var rdata = SurfaceRenderData{ .output = &output, - .layer_surface = layer_surface, + .output_x = layer_surface.box.x, + .output_y = layer_surface.box.y, .when = now, }; c.wlr_layer_surface_v1_for_each_surface( layer_surface.wlr_layer_surface, - renderLayerSurface, + renderSurface, &rdata, ); } } -/// This function is called for every layer surface and popup that needs to be rendered. -/// TODO: refactor this to reduce code duplication -fn renderLayerSurface(_surface: ?*c.wlr_surface, sx: c_int, sy: c_int, data: ?*c_void) callconv(.C) void { - // wlroots says this will never be null - const surface = _surface.?; - // This function is called for every surface that needs to be rendered. - const rdata = @ptrCast(*LayerSurfaceRenderData, @alignCast(@alignOf(LayerSurfaceRenderData), data)); - const layer_surface = rdata.layer_surface; - const output = rdata.output; - const wlr_output = output.wlr_output; - - // We first obtain a wlr_texture, which is a GPU resource. wlroots - // automatically handles negotiating these with the client. The underlying - // resource could be an opaque handle passed from the client, or the client - // could have sent a pixel buffer which we copied to the GPU, or a few other - // means. You don't have to worry about this, wlroots takes care of it. - const texture = c.wlr_surface_get_texture(surface); - if (texture == null) { - return; - } - - var box = c.wlr_box{ - .x = layer_surface.box.x + sx, - .y = layer_surface.box.y + sy, - .width = surface.current.width, - .height = surface.current.height, - }; - - // Scale the box to the output's current scaling factor - scaleBox(&box, wlr_output.scale); - - // wlr_matrix_project_box is a helper which takes a box with a desired - // x, y coordinates, width and height, and an output geometry, then - // prepares an orthographic projection and multiplies the necessary - // transforms to produce a model-view-projection matrix. - var matrix: [9]f32 = undefined; - const transform = c.wlr_output_transform_invert(surface.current.transform); - c.wlr_matrix_project_box(&matrix, &box, transform, 0.0, &wlr_output.transform_matrix); - - // This takes our matrix, the texture, and an alpha, and performs the actual - // rendering on the GPU. - _ = c.wlr_render_texture_with_matrix(output.getRenderer(), texture, &matrix, 1.0); - - // This lets the client know that we've displayed that frame and it can - // prepare another one now if it likes. - c.wlr_surface_send_frame_done(surface, rdata.when); -} - fn renderView(output: Output, view: *View, now: *c.timespec) void { // If we have a stashed buffer, we are in the middle of a transaction // and need to render that buffer until the transaction is complete. @@ -215,9 +172,10 @@ fn renderView(output: Output, view: *View, now: *c.timespec) void { } else { // Since there is no stashed buffer, we are not in the middle of // a transaction and may simply render each toplevel surface. - var rdata = ViewRenderData{ + var rdata = SurfaceRenderData{ .output = &output, - .view = view, + .output_x = view.current_box.x, + .output_y = view.current_box.y, .when = now, }; @@ -225,12 +183,38 @@ fn renderView(output: Output, view: *View, now: *c.timespec) void { } } -/// This function is called for every toplevel and popup surface that needs to be rendered. -fn renderSurface(_surface: ?*c.wlr_surface, sx: c_int, sy: c_int, data: ?*c_void) callconv(.C) void { +/// Render all xwayland unmanaged windows that appear on the output +fn renderXwaylandUnmanaged(output: Output, now: *c.timespec) void { + const root = output.root; + const output_box: *c.wlr_box = c.wlr_output_layout_get_box( + root.wlr_output_layout, + output.wlr_output, + ); + + var it = output.root.xwayland_unmanaged_views.first; + while (it) |node| : (it = node.next) { + const wlr_xwayland_surface = node.data.wlr_xwayland_surface; + + var rdata = SurfaceRenderData{ + .output = &output, + .output_x = wlr_xwayland_surface.x - output_box.x, + .output_y = wlr_xwayland_surface.y - output_box.y, + .when = now, + }; + c.wlr_surface_for_each_surface(wlr_xwayland_surface.surface, renderSurface, &rdata); + } +} + +/// This function is passed to wlroots to render each surface during iteration +fn renderSurface( + _surface: ?*c.wlr_surface, + surface_x: c_int, + surface_y: c_int, + data: ?*c_void, +) callconv(.C) void { // wlroots says this will never be null const surface = _surface.?; - const rdata = @ptrCast(*ViewRenderData, @alignCast(@alignOf(ViewRenderData), data)); - const view = rdata.view; + const rdata = @ptrCast(*SurfaceRenderData, @alignCast(@alignOf(SurfaceRenderData), data)); const output = rdata.output; const wlr_output = output.wlr_output; @@ -245,8 +229,8 @@ fn renderSurface(_surface: ?*c.wlr_surface, sx: c_int, sy: c_int, data: ?*c_void } var box = c.wlr_box{ - .x = view.current_box.x + sx, - .y = view.current_box.y + sy, + .x = rdata.output_x + surface_x, + .y = rdata.output_y + surface_y, .width = surface.current.width, .height = surface.current.height, };