From ac735719cfc086c54ff939786823b29de2c24c0a Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Fri, 8 May 2020 14:51:10 +0200 Subject: [PATCH] Implement initial Xwayland support --- README.md | 3 +- src/Cursor.zig | 11 +--- src/Output.zig | 9 +-- src/Server.zig | 43 +++++++++++- src/View.zig | 68 +++++++++++++++++-- src/VoidView.zig | 48 ++++++++++++++ src/XdgToplevel.zig | 34 ++++------ src/XwaylandView.zig | 151 +++++++++++++++++++++++++++++++++++++++++++ src/c.zig | 1 + 9 files changed, 324 insertions(+), 44 deletions(-) create mode 100644 src/VoidView.zig create mode 100644 src/XwaylandView.zig diff --git a/README.md b/README.md index 1fdef48..27ad1c7 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,8 @@ installed: - [wlroots](https://github.com/swaywm/wlroots) 0.10.1 - xkbcommon -Then simply use `zig build` to build and `zig build run` to run. +Then simply use `zig build` to build and `zig build run` to run. To +enable experimental Xwayland support use `-Dxwayland=true`. River can either be run nested in an X11/wayland session or directly from a tty using KMS/DRM. diff --git a/src/Cursor.zig b/src/Cursor.zig index 98e33a6..df5e4a4 100644 --- a/src/Cursor.zig +++ b/src/Cursor.zig @@ -386,16 +386,7 @@ fn viewSurfaceAt(output: Output, ox: f64, oy: f64, sx: *f64, sy: *f64, floating: if (view.floating != floating) { continue; } - const surface = switch (view.impl) { - .xdg_toplevel => |xdg_toplevel| c.wlr_xdg_surface_surface_at( - xdg_toplevel.wlr_xdg_surface, - ox - @intToFloat(f64, view.current_box.x), - oy - @intToFloat(f64, view.current_box.y), - sx, - sy, - ), - }; - if (surface) |found| { + if (view.surfaceAt(ox, oy, sx, sy)) |found| { return found; } } diff --git a/src/Output.zig b/src/Output.zig index a7921f0..a0ee749 100644 --- a/src/Output.zig +++ b/src/Output.zig @@ -144,10 +144,11 @@ pub fn deinit(self: *Self) void { } /// Add a new view to the output. arrangeViews() will be called by the view -/// when it is mapped. -pub fn addView(self: *Self, wlr_xdg_surface: *c.wlr_xdg_surface) void { - const node = self.root.server.allocator.create(ViewStack(View).Node) catch unreachable; - node.view.init_xdg_toplevel(self, self.current_focused_tags, wlr_xdg_surface); +/// when it is mapped. The surface argument must be a c.wlr_xdg_surface or +/// c.wlr_xwayland_surface (if xwayland is enabled) +pub fn addView(self: *Self, surface: var) !void { + const node = try self.root.server.allocator.create(ViewStack(View).Node); + node.view.init(self, self.current_focused_tags, surface); self.views.push(node); } diff --git a/src/Server.zig b/src/Server.zig index b255aff..da980dd 100644 --- a/src/Server.zig +++ b/src/Server.zig @@ -17,6 +17,7 @@ const Self = @This(); +const build_options = @import("build_options"); const std = @import("std"); const c = @import("c.zig"); @@ -40,6 +41,7 @@ wlr_renderer: *c.wlr_renderer, wlr_xdg_shell: *c.wlr_xdg_shell, wlr_layer_shell: *c.wlr_layer_shell_v1, +wlr_xwayland: if (build_options.xwayland) *c.wlr_xwayland else void, decoration_manager: DecorationManager, input_manager: InputManager, @@ -49,6 +51,7 @@ config: Config, listen_new_output: c.wl_listener, listen_new_xdg_surface: c.wl_listener, listen_new_layer_surface: c.wl_listener, +listen_new_xwayland_surface: c.wl_listener, pub fn init(self: *Self, allocator: *std.mem.Allocator) !void { self.allocator = allocator; @@ -84,12 +87,20 @@ pub fn init(self: *Self, allocator: *std.mem.Allocator) !void { c.wlr_renderer_init_wl_display(self.wlr_renderer, self.wl_display); // orelse // return error.CantInitWlDisplay; + const wlr_compositor = c.wlr_compositor_create(self.wl_display, self.wlr_renderer) orelse + return error.CantCreateWlrCompositor; + self.wlr_xdg_shell = c.wlr_xdg_shell_create(self.wl_display) orelse return error.CantCreateWlrXdgShell; self.wlr_layer_shell = c.wlr_layer_shell_v1_create(self.wl_display) orelse return error.CantCreateWlrLayerShell; + if (build_options.xwayland) { + self.wlr_xwayland = c.wlr_xwayland_create(self.wl_display, wlr_compositor, false) orelse + return error.CantCreateWlrXwayland; + } + try self.decoration_manager.init(self); try self.root.init(self); // Must be called after root is initialized @@ -97,8 +108,6 @@ pub fn init(self: *Self, allocator: *std.mem.Allocator) !void { try self.config.init(self.allocator); // These all free themselves when the wl_display is destroyed - _ = c.wlr_compositor_create(self.wl_display, self.wlr_renderer) orelse - return error.CantCreateWlrCompositor; _ = c.wlr_data_device_manager_create(self.wl_display) orelse return error.CantCreateWlrDataDeviceManager; _ = c.wlr_screencopy_manager_v1_create(self.wl_display) orelse @@ -115,11 +124,19 @@ 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); + + if (build_options.xwayland) { + self.listen_new_xwayland_surface.notify = handleNewXwaylandSurface; + c.wl_signal_add(&self.wlr_xwayland.events.new_surface, &self.listen_new_xwayland_surface); + } } /// Free allocated memory and clean up pub fn deinit(self: *Self) void { // Note: order is important here + if (build_options.xwayland) { + c.wlr_xwayland_destroy(self.wlr_xwayland); + } c.wl_display_destroy_clients(self.wl_display); c.wl_display_destroy(self.wl_display); self.input_manager.deinit(); @@ -142,6 +159,12 @@ pub fn start(self: Self) !void { if (c.setenv("WAYLAND_DISPLAY", socket, 1) == -1) { return error.CantSetEnv; } + + if (build_options.xwayland) { + if (c.setenv("DISPLAY", &self.wlr_xwayland.display_name, 1) == -1) { + return error.CantSetEnv; + } + } } /// Enter the wayland event loop and block until the compositor is exited @@ -169,7 +192,7 @@ fn handleNewXdgSurface(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) v Log.Debug.log("New xdg_toplevel", .{}); - self.input_manager.default_seat.focused_output.addView(wlr_xdg_surface); + self.input_manager.default_seat.focused_output.addView(wlr_xdg_surface) catch unreachable; } /// This event is raised when the layer_shell recieves a new surface from a client. @@ -219,3 +242,17 @@ fn handleNewLayerSurface(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) const output = @ptrCast(*Output, @alignCast(@alignOf(*Output), wlr_layer_surface.output.*.data)); output.addLayerSurface(wlr_layer_surface) catch unreachable; } + +fn handleNewXwaylandSurface(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { + const self = @fieldParentPtr(Self, "listen_new_xwayland_surface", listener.?); + const wlr_xwayland_surface = @ptrCast( + *c.wlr_xwayland_surface, + @alignCast(@alignOf(*c.wlr_xwayland_surface), data), + ); + + Log.Debug.log( + "New xwayland surface: title '{}', class '{}'", + .{ wlr_xwayland_surface.title, wlr_xwayland_surface.class }, + ); + self.input_manager.default_seat.focused_output.addView(wlr_xwayland_surface) catch unreachable; +} diff --git a/src/View.zig b/src/View.zig index 51e0b98..32924b4 100644 --- a/src/View.zig +++ b/src/View.zig @@ -17,6 +17,7 @@ const Self = @This(); +const build_options = @import("build_options"); const std = @import("std"); const c = @import("c.zig"); @@ -27,13 +28,18 @@ const Output = @import("Output.zig"); const Root = @import("Root.zig"); const ViewStack = @import("view_stack.zig").ViewStack; const XdgToplevel = @import("XdgToplevel.zig"); +const XwaylandView = if (build_options.xwayland) + @import("XwaylandView.zig") +else + @import("VoidView.zig"); -const ViewImpl = union(enum) { +const Impl = union(enum) { xdg_toplevel: XdgToplevel, + xwayland_view: XwaylandView, }; /// The implementation of this view -impl: ViewImpl, +impl: Impl, /// The output this view is currently associated with output: *Output, @@ -63,11 +69,11 @@ pending_serial: ?u32, // This is what we render while a transaction is in progress stashed_buffer: ?*c.wlr_buffer, -pub fn init_xdg_toplevel( +pub fn init( self: *Self, output: *Output, tags: u32, - wlr_xdg_surface: *c.wlr_xdg_surface, + surface: var, ) void { self.output = output; @@ -90,8 +96,13 @@ pub fn init_xdg_toplevel( self.stashed_buffer = null; - self.impl = .{ .xdg_toplevel = undefined }; - self.impl.xdg_toplevel.init(self, wlr_xdg_surface); + if (@TypeOf(surface) == *c.wlr_xdg_surface) { + self.impl = .{ .xdg_toplevel = undefined }; + self.impl.xdg_toplevel.init(self, surface); + } else if (build_options.xwayland and @TypeOf(surface) == *c.wlr_xwayland_surface) { + self.impl = .{ .xwayland_view = undefined }; + self.impl.xwayland_view.init(self, surface); + } else unreachable; } pub fn deinit(self: *Self) void { @@ -113,6 +124,7 @@ pub fn configure(self: Self) void { if (self.pending_box) |pending_box| { switch (self.impl) { .xdg_toplevel => |xdg_toplevel| xdg_toplevel.configure(pending_box), + .xwayland_view => |xwayland_view| xwayland_view.configure(pending_box), } } else { Log.Error.log("Configure called on a View with no pending box", .{}); @@ -148,6 +160,7 @@ pub fn setFocused(self: *Self, focused: bool) void { self.focused = focused; switch (self.impl) { .xdg_toplevel => |xdg_toplevel| xdg_toplevel.setActivated(focused), + .xwayland_view => |xwayland_view| xwayland_view.setActivated(focused), } } @@ -186,6 +199,7 @@ pub fn sendToOutput(self: *Self, destination_output: *Output) void { pub fn close(self: Self) void { switch (self.impl) { .xdg_toplevel => |xdg_toplevel| xdg_toplevel.close(), + .xwayland_view => |xwayland_view| xwayland_view.close(), } } @@ -196,5 +210,47 @@ pub fn forEachSurface( ) void { switch (self.impl) { .xdg_toplevel => |xdg_toplevel| xdg_toplevel.forEachSurface(iterator, user_data), + .xwayland_view => |xwayland_view| xwayland_view.forEachSurface(iterator, user_data), } } + +/// 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 switch (self.impl) { + .xdg_toplevel => |xdg_toplevel| xdg_toplevel.surfaceAt(ox, oy, sx, sy), + .xwayland_view => |xwayland_view| xwayland_view.surfaceAt(ox, oy, sx, sy), + }; +} + +/// Called by the impl when the surface is ready to be displayed +pub fn map(self: *Self) void { + const root = self.output.root; + + // Focus the newly mapped view. Note: if a seat is focusing a different output + // it will continue to do so. + var it = root.server.input_manager.seats.first; + while (it) |seat_node| : (it = seat_node.next) { + seat_node.data.focus(self); + } + + c.wlr_surface_send_enter(self.wlr_surface.?, self.output.wlr_output); + + root.arrange(); +} + +/// Called by the impl when the surface will no longer be displayed +pub fn unmap(self: *Self) void { + const root = self.output.root; + + self.wlr_surface = null; + + // Inform all seats that the view has been unmapped so they can handle focus + var it = root.server.input_manager.seats.first; + while (it) |node| : (it = node.next) { + const seat = &node.data; + seat.handleViewUnmap(self); + } + + root.arrange(); +} diff --git a/src/VoidView.zig b/src/VoidView.zig new file mode 100644 index 0000000..72c57a7 --- /dev/null +++ b/src/VoidView.zig @@ -0,0 +1,48 @@ +// 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"); + +pub fn configure(self: Self, pending_box: Box) void { + unreachable; +} + +pub fn setActivated(self: Self, activated: bool) void { + unreachable; +} + +pub fn close(self: Self) void { + unreachable; +} + +pub fn forEachSurface( + self: Self, + iterator: c.wlr_surface_iterator_func_t, + user_data: ?*c_void, +) void { + unreachable; +} + +pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*c.wlr_surface { + unreachable; +} diff --git a/src/XdgToplevel.zig b/src/XdgToplevel.zig index f1433e6..a9c2040 100644 --- a/src/XdgToplevel.zig +++ b/src/XdgToplevel.zig @@ -88,6 +88,18 @@ pub fn forEachSurface( c.wlr_xdg_surface_for_each_surface(self.wlr_xdg_surface, iterator, user_data); } +/// 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_xdg_surface_surface_at( + self.wlr_xdg_surface, + ox - @intToFloat(f64, self.view.current_box.x), + oy - @intToFloat(f64, self.view.current_box.y), + sx, + sy, + ); +} + /// Called when the xdg surface is destroyed fn handleDestroy(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { const self = @fieldParentPtr(Self, "listen_destroy", listener.?); @@ -151,16 +163,7 @@ fn handleMap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { view.setFloating(true); } - // Focus the newly mapped view. Note: if a seat is focusing a different output - // it will continue to do so. - var it = root.server.input_manager.seats.first; - while (it) |seat_node| : (it = seat_node.next) { - seat_node.data.focus(view); - } - - c.wlr_surface_send_enter(self.wlr_xdg_surface.surface, view.output.wlr_output); - - root.arrange(); + view.map(); } /// Called when the surface is unmapped and will no longer be displayed. @@ -168,16 +171,7 @@ fn handleUnmap(listener: ?*c.wl_listener, data: ?*c_void) callconv(.C) void { const self = @fieldParentPtr(Self, "listen_unmap", listener.?); const root = self.view.output.root; - self.view.wlr_surface = null; - - // Inform all seats that the view has been unmapped so they can handle focus - var it = root.server.input_manager.seats.first; - while (it) |node| : (it = node.next) { - const seat = &node.data; - seat.handleViewUnmap(self.view); - } - - root.arrange(); + self.view.unmap(); // Remove listeners that are only active while mapped c.wl_list_remove(&self.listen_commit.link); diff --git a/src/XwaylandView.zig b/src/XwaylandView.zig new file mode 100644 index 0000000..56beec8 --- /dev/null +++ b/src/XwaylandView.zig @@ -0,0 +1,151 @@ +// 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 View = @import("View.zig"); +const ViewStack = @import("view_stack.zig").ViewStack; +const XdgPopup = @import("XdgPopup.zig"); + +/// The view this xwayland view implements +view: *View, + +/// The corresponding wlroots object +wlr_xwayland_surface: *c.wlr_xwayland_surface, + +// Listeners that are always active over the view's lifetime +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, view: *View, wlr_xwayland_surface: *c.wlr_xwayland_surface) void { + self.view = view; + self.wlr_xwayland_surface = wlr_xwayland_surface; + wlr_xwayland_surface.data = self; + + // Add listeners that are active over the view's entire lifetime + self.listen_destroy.notify = handleDestroy; + c.wl_signal_add(&self.wlr_xwayland_surface.events.destroy, &self.listen_destroy); + + self.listen_map.notify = handleMap; + c.wl_signal_add(&self.wlr_xwayland_surface.events.map, &self.listen_map); + + self.listen_unmap.notify = handleUnmap; + c.wl_signal_add(&self.wlr_xwayland_surface.events.unmap, &self.listen_unmap); +} + +/// TODO: real output layout coords +pub fn configure(self: Self, pending_box: Box) void { + c.wlr_xwayland_surface_configure( + self.wlr_xwayland_surface, + 0, + 0, + @intCast(u16, pending_box.width), + @intCast(u16, pending_box.height), + ); +} + +/// Inform the xwayland surface that it has gained focus +pub fn setActivated(self: Self, activated: bool) void { + c.wlr_xwayland_surface_activate(self.wlr_xwayland_surface, activated); +} + +/// Close the view. This will lead to the unmap and destroy events being sent +pub fn close(self: Self) void { + c.wlr_xwayland_surface_close(self.wlr_xwayland_surface); +} + +/// Iterate over all surfaces of the xwayland view. +pub fn forEachSurface( + self: Self, + iterator: c.wlr_surface_iterator_func_t, + user_data: ?*c_void, +) void { + c.wlr_surface_for_each_surface(self.wlr_xwayland_surface.surface, iterator, user_data); +} + +/// 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, + ); +} + +/// 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.?); + const output = self.view.output; + + // 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); + + // Remove the view from the stack + const node = @fieldParentPtr(ViewStack(View).Node, "view", self.view); + output.views.remove(node); + output.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 view = self.view; + const root = view.output.root; + + // 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); + + view.wlr_surface = self.wlr_xwayland_surface.surface; + view.floating = false; + + view.natural_width = self.wlr_xwayland_surface.width; + view.natural_height = self.wlr_xwayland_surface.height; + + view.map(); +} + +/// 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.?); + + self.view.unmap(); + + // Remove listeners that are only active while mapped + c.wl_list_remove(&self.listen_commit.link); +} + +/// 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 for unexpected change in size and react as needed +} diff --git a/src/c.zig b/src/c.zig index c36f0a0..aae0095 100644 --- a/src/c.zig +++ b/src/c.zig @@ -40,6 +40,7 @@ pub usingnamespace @cImport({ @cInclude("wlr/types/wlr_xdg_decoration_v1.h"); @cInclude("wlr/types/wlr_xdg_output_v1.h"); @cInclude("wlr/types/wlr_xdg_shell.h"); + if (@import("build_options").xwayland) @cInclude("wlr/xwayland.h"); @cInclude("wlr/util/log.h"); @cInclude("xkbcommon/xkbcommon.h");