From 760c88b094e4d07be87a0096aa047768463937e5 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Thu, 24 Dec 2020 01:59:30 +0100 Subject: [PATCH] foreign-toplevel-management: basic implementation We do no yet set the parent of toplevels. We also only honor activate requests if the target view is already visible on the focused output. --- deps/zig-wayland | 2 +- deps/zig-wlroots | 2 +- river/SeatStatus.zig | 5 +- river/Server.zig | 6 ++- river/View.zig | 114 +++++++++++++++++++++++++++++++++++++++-- river/VoidView.zig | 6 ++- river/XdgToplevel.zig | 22 +++----- river/XwaylandView.zig | 11 ++-- 8 files changed, 142 insertions(+), 26 deletions(-) diff --git a/deps/zig-wayland b/deps/zig-wayland index 52326e7..d693b37 160000 --- a/deps/zig-wayland +++ b/deps/zig-wayland @@ -1 +1 @@ -Subproject commit 52326e7ee09d7acb6b55855f7a697af083ae973a +Subproject commit d693b3704ee73762c71a68d634ef1a538c3307bd diff --git a/deps/zig-wlroots b/deps/zig-wlroots index 16d9039..10ddf02 160000 --- a/deps/zig-wlroots +++ b/deps/zig-wlroots @@ -1 +1 @@ -Subproject commit 16d9039b5c345b2cc26118032261df9782e24946 +Subproject commit 10ddf02b8aff37dabd07cee8695366917527676e diff --git a/river/SeatStatus.zig b/river/SeatStatus.zig index a6c0d46..c83a9db 100644 --- a/river/SeatStatus.zig +++ b/river/SeatStatus.zig @@ -65,6 +65,9 @@ pub fn sendOutput(self: Self, state: enum { focused, unfocused }) void { } pub fn sendFocusedView(self: Self) void { - const title: [*:0]const u8 = if (self.seat.focused == .view) self.seat.focused.view.getTitle() else ""; + const title: [*:0]const u8 = if (self.seat.focused == .view) + self.seat.focused.view.getTitle() orelse "" + else + ""; self.seat_status.sendFocusedView(title); } diff --git a/river/Server.zig b/river/Server.zig index 1d91061..b4e5a20 100644 --- a/river/Server.zig +++ b/river/Server.zig @@ -56,6 +56,8 @@ new_layer_surface: wl.Listener(*wlr.LayerSurfaceV1), xwayland: if (build_options.xwayland) *wlr.Xwayland else void, new_xwayland_surface: if (build_options.xwayland) wl.Listener(*wlr.XwaylandSurface) else void, +foreign_toplevel_manager: *wlr.ForeignToplevelManagerV1, + decoration_manager: DecorationManager, input_manager: InputManager, output_manager: OutputManager, @@ -104,6 +106,8 @@ pub fn init(self: *Self) !void { self.xwayland.events.new_surface.add(&self.new_xwayland_surface); } + self.foreign_toplevel_manager = try wlr.ForeignToplevelManagerV1.create(self.wl_server); + _ = try wlr.PrimarySelectionDeviceManagerV1.create(self.wl_server); self.config = try Config.init(); @@ -135,8 +139,8 @@ pub fn deinit(self: *Self) void { self.root.deinit(); - self.wl_server.destroy(); self.noop_backend.destroy(); + self.wl_server.destroy(); self.input_manager.deinit(); self.config.deinit(); diff --git a/river/View.zig b/river/View.zig index ddde6a4..3003f88 100644 --- a/river/View.zig +++ b/river/View.zig @@ -29,6 +29,7 @@ const util = @import("util.zig"); const Box = @import("Box.zig"); const Output = @import("Output.zig"); const Root = @import("Root.zig"); +const Seat = @import("Seat.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"); @@ -84,8 +85,8 @@ impl: Impl = undefined, /// The output this view is currently associated with output: *Output, -/// This is from the point where the view is mapped until the surface -/// is destroyed by wlroots. +/// This is non-null from the point where the view is mapped until the +/// surface is destroyed by wlroots. surface: ?*wlr.Surface = null, /// This View struct outlasts the wlroots object it wraps. This bool is set to @@ -122,6 +123,12 @@ opacity_timer: ?*wl.EventSource = null, draw_borders: bool = true, +/// This is created when the view is mapped and destroyed with the view +foreign_toplevel_handle: ?*wlr.ForeignToplevelHandleV1 = null, +foreign_activate: wl.Listener(*wlr.ForeignToplevelHandleV1.event.Activated) = undefined, +foreign_fullscreen: wl.Listener(*wlr.ForeignToplevelHandleV1.event.Fullscreen) = undefined, +foreign_close: wl.Listener(*wlr.ForeignToplevelHandleV1) = undefined, + pub fn init(self: *Self, output: *Output, tags: u32, surface: anytype) void { self.* = .{ .output = output, @@ -150,10 +157,19 @@ pub fn init(self: *Self, output: *Output, tags: u32, surface: anytype) void { pub fn destroy(self: *Self) void { self.dropSavedBuffers(); self.saved_buffers.deinit(); + + if (self.foreign_toplevel_handle) |handle| { + self.foreign_activate.link.remove(); + self.foreign_fullscreen.link.remove(); + self.foreign_close.link.remove(); + handle.destroy(); + } + switch (self.impl) { .xdg_toplevel => |*xdg_toplevel| xdg_toplevel.deinit(), .xwayland_view => |*xwayland_view| xwayland_view.deinit(), } + const node = @fieldParentPtr(ViewStack(Self).Node, "view", self); self.output.views.remove(node); util.gpa.destroy(node); @@ -220,6 +236,10 @@ pub fn needsConfigure(self: Self) bool { } pub fn configure(self: Self) void { + if (self.foreign_toplevel_handle) |handle| { + handle.setActivated(self.pending.focus != 0); + handle.setFullscreen(self.pending.fullscreen); + } switch (self.impl) { .xdg_toplevel => |xdg_toplevel| xdg_toplevel.configure(), .xwayland_view => |xwayland_view| xwayland_view.configure(), @@ -293,6 +313,9 @@ pub fn sendToOutput(self: *Self, destination_output: *Output) void { self.surface.?.sendLeave(self.output.wlr_output); self.surface.?.sendEnter(destination_output.wlr_output); + self.foreign_toplevel_handle.?.outputLeave(self.output.wlr_output); + self.foreign_toplevel_handle.?.outputEnter(destination_output.wlr_output); + self.output = destination_output; } @@ -324,14 +347,23 @@ pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*wlr.Surface }; } -/// Return the current title of the view. May be an empty string. -pub fn getTitle(self: Self) [*:0]const u8 { +/// Return the current title of the view if any. +pub fn getTitle(self: Self) ?[*:0]const u8 { return switch (self.impl) { .xdg_toplevel => |xdg_toplevel| xdg_toplevel.getTitle(), .xwayland_view => |xwayland_view| xwayland_view.getTitle(), }; } +/// Return the current app_id of the view if any. +pub fn getAppId(self: Self) ?[*:0]const u8 { + return switch (self.impl) { + .xdg_toplevel => |xdg_toplevel| xdg_toplevel.getAppId(), + // X11 clients don't have an app_id but the class serves a similar role + .xwayland_view => |xwayland_view| xwayland_view.getClass(), + }; +} + /// Clamp the width/height of the pending state to the constraints of the view pub fn applyConstraints(self: *Self) void { const constraints = self.getConstraints(); @@ -383,6 +415,28 @@ pub fn map(self: *Self) void { log.debug(.server, "view '{}' mapped", .{self.getTitle()}); + if (self.foreign_toplevel_handle == null) { + self.foreign_toplevel_handle = wlr.ForeignToplevelHandleV1.create( + root.server.foreign_toplevel_manager, + ) catch { + log.crit(.server, "out of memory", .{}); + self.surface.?.resource.getClient().postNoMemory(); + return; + }; + + self.foreign_activate.setNotify(handleForeignActivate); + self.foreign_toplevel_handle.?.events.request_activate.add(&self.foreign_activate); + + self.foreign_fullscreen.setNotify(handleForeignFullscreen); + self.foreign_toplevel_handle.?.events.request_fullscreen.add(&self.foreign_fullscreen); + + self.foreign_close.setNotify(handleForeignClose); + self.foreign_toplevel_handle.?.events.request_close.add(&self.foreign_close); + + if (self.getTitle()) |s| self.foreign_toplevel_handle.?.setTitle(s); + if (self.getAppId()) |s| self.foreign_toplevel_handle.?.setAppId(s); + } + // Add the view to the stack of its output const node = @fieldParentPtr(ViewStack(Self).Node, "view", self); self.output.views.attach(node, self.output.attach_mode); @@ -428,6 +482,28 @@ pub fn unmap(self: *Self) void { root.startTransaction(); } +pub fn notifyTitle(self: Self) void { + if (self.foreign_toplevel_handle) |handle| { + if (self.getTitle()) |s| handle.setTitle(s); + } + // Send title to all status listeners attached to a seat which focuses this view + var seat_it = self.output.root.server.input_manager.seats.first; + while (seat_it) |seat_node| : (seat_it = seat_node.next) { + if (seat_node.data.focused == .view and seat_node.data.focused.view == self) { + var client_it = seat_node.data.status_trackers.first; + while (client_it) |client_node| : (client_it = client_node.next) { + client_node.data.sendFocusedView(); + } + } + } +} + +pub fn notifyAppId(self: Self) void { + if (self.foreign_toplevel_handle) |handle| { + if (self.getAppId()) |s| handle.setAppId(s); + } +} + /// Change the opacity of a view by config.view_opacity_delta. /// If the target opacity was reached, return true. fn incrementOpacity(self: *Self) bool { @@ -491,3 +567,33 @@ pub fn commitOpacityTransition(self: *Self) void { self.attachOpacityTimer(); } } + +/// Only honors the request if the view is already visible on the seat's +/// currently focused output. TODO: consider allowing this request to switch +/// output/tag focus. +fn handleForeignActivate( + listener: *wl.Listener(*wlr.ForeignToplevelHandleV1.event.Activated), + event: *wlr.ForeignToplevelHandleV1.event.Activated, +) void { + const self = @fieldParentPtr(Self, "foreign_activate", listener); + const seat = @intToPtr(*Seat, event.seat.data); + seat.focus(self); + self.output.root.startTransaction(); +} + +fn handleForeignFullscreen( + listener: *wl.Listener(*wlr.ForeignToplevelHandleV1.event.Fullscreen), + event: *wlr.ForeignToplevelHandleV1.event.Fullscreen, +) void { + const self = @fieldParentPtr(Self, "foreign_fullscreen", listener); + self.pending.fullscreen = event.fullscreen; + self.applyPending(); +} + +fn handleForeignClose( + listener: *wl.Listener(*wlr.ForeignToplevelHandleV1), + event: *wlr.ForeignToplevelHandleV1, +) void { + const self = @fieldParentPtr(Self, "foreign_close", listener); + self.close(); +} diff --git a/river/VoidView.zig b/river/VoidView.zig index ff29d99..21192ca 100644 --- a/river/VoidView.zig +++ b/river/VoidView.zig @@ -52,7 +52,11 @@ pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*wlr.Surface unreachable; } -pub fn getTitle(self: Self) [*:0]const u8 { +pub fn getTitle(self: Self) ?[*:0]const u8 { + unreachable; +} + +pub fn getClass(self: Self) ?[*:0]const u8 { unreachable; } diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig index 8017be9..689a4aa 100644 --- a/river/XdgToplevel.zig +++ b/river/XdgToplevel.zig @@ -123,9 +123,14 @@ pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*wlr.Surface ); } -/// Return the current title of the toplevel. May be an empty string. -pub fn getTitle(self: Self) [*:0]const u8 { - return self.xdg_surface.role_data.toplevel.title orelse "NULL"; +/// Return the current title of the toplevel if any. +pub fn getTitle(self: Self) ?[*:0]const u8 { + return self.xdg_surface.role_data.toplevel.title; +} + +/// Return the current app_id of the toplevel if any . +pub fn getAppId(self: Self) ?[*:0]const u8 { + return self.xdg_surface.role_data.toplevel.app_id; } /// Return bounds on the dimensions of the toplevel. @@ -310,15 +315,4 @@ fn handleRequestResize(listener: *wl.Listener(*wlr.XdgToplevel.event.Resize), ev /// Called when the client sets / updates its title fn handleSetTitle(listener: *wl.Listener(*wlr.XdgSurface), xdg_surface: *wlr.XdgSurface) void { const self = @fieldParentPtr(Self, "set_title", listener); - - // Send title to all status listeners attached to a seat which focuses this view - var seat_it = self.view.output.root.server.input_manager.seats.first; - while (seat_it) |seat_node| : (seat_it = seat_node.next) { - if (seat_node.data.focused == .view and seat_node.data.focused.view == self.view) { - var client_it = seat_node.data.status_trackers.first; - while (client_it) |client_node| : (client_it = client_node.next) { - client_node.data.sendFocusedView(); - } - } - } } diff --git a/river/XwaylandView.zig b/river/XwaylandView.zig index b118b82..f3f0474 100644 --- a/river/XwaylandView.zig +++ b/river/XwaylandView.zig @@ -121,9 +121,14 @@ pub fn surfaceAt(self: Self, ox: f64, oy: f64, sx: *f64, sy: *f64) ?*wlr.Surface ); } -/// Get the current title of the xwayland surface. May be an empty string -pub fn getTitle(self: Self) [*:0]const u8 { - return self.xwayland_surface.title orelse ""; +/// Get the current title of the xwayland surface if any. +pub fn getTitle(self: Self) ?[*:0]const u8 { + return self.xwayland_surface.title; +} + +/// Get the current class of the xwayland surface if any. +pub fn getClass(self: Self) ?[*:0]const u8 { + return self.xwayland_surface.class; } /// Return bounds on the dimensions of the view