From 879b880a6ac52eb5b5b43aa75a92ac172cc278a5 Mon Sep 17 00:00:00 2001 From: Isaac Freund Date: Sun, 12 Feb 2023 18:56:57 +0100 Subject: [PATCH] XdgPopup: reimplement using the scene graph xdg-shell version 3 is now implemented, supporting popup repositioning. --- river/LayerSurface.zig | 36 ++++++++++++-- river/Output.zig | 7 ++- river/Seat.zig | 6 ++- river/Server.zig | 2 +- river/View.zig | 17 ++++++- river/XdgPopup.zig | 105 +++++++++++++++++++++++++++++++++++++++++ river/XdgToplevel.zig | 22 +++++++++ 7 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 river/XdgPopup.zig diff --git a/river/LayerSurface.zig b/river/LayerSurface.zig index 296eb9f..e0a273a 100644 --- a/river/LayerSurface.zig +++ b/river/LayerSurface.zig @@ -27,37 +27,42 @@ const util = @import("util.zig"); const Output = @import("Output.zig"); const SceneNodeData = @import("SceneNodeData.zig"); +const XdgPopup = @import("XdgPopup.zig"); const log = std.log.scoped(.layer_shell); output: *Output, scene_layer_surface: *wlr.SceneLayerSurfaceV1, +popup_tree: *wlr.SceneTree, destroy: wl.Listener(*wlr.LayerSurfaceV1) = wl.Listener(*wlr.LayerSurfaceV1).init(handleDestroy), map: wl.Listener(*wlr.LayerSurfaceV1) = wl.Listener(*wlr.LayerSurfaceV1).init(handleMap), unmap: wl.Listener(*wlr.LayerSurfaceV1) = wl.Listener(*wlr.LayerSurfaceV1).init(handleUnmap), commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit), +new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(handleNewPopup), pub fn create(wlr_layer_surface: *wlr.LayerSurfaceV1) error{OutOfMemory}!void { const output = @intToPtr(*Output, wlr_layer_surface.output.?.data); const layer_surface = try util.gpa.create(LayerSurface); errdefer util.gpa.destroy(layer_surface); - const tree = output.layerSurfaceTree(wlr_layer_surface.current.layer); - const scene_layer_surface = try tree.createSceneLayerSurfaceV1(wlr_layer_surface); - - try SceneNodeData.attach(&scene_layer_surface.tree.node, .{ .layer_surface = layer_surface }); + const layer_tree = output.layerSurfaceTree(wlr_layer_surface.current.layer); layer_surface.* = .{ .output = output, - .scene_layer_surface = scene_layer_surface, + .scene_layer_surface = try layer_tree.createSceneLayerSurfaceV1(wlr_layer_surface), + .popup_tree = try output.layers.popups.createSceneTree(), }; wlr_layer_surface.data = @ptrToInt(layer_surface); + try SceneNodeData.attach(&layer_surface.scene_layer_surface.tree.node, .{ .layer_surface = layer_surface }); + try SceneNodeData.attach(&layer_surface.popup_tree.node, .{ .layer_surface = layer_surface }); + wlr_layer_surface.events.destroy.add(&layer_surface.destroy); wlr_layer_surface.events.map.add(&layer_surface.map); wlr_layer_surface.events.unmap.add(&layer_surface.unmap); wlr_layer_surface.surface.events.commit.add(&layer_surface.commit); + wlr_layer_surface.events.new_popup.add(&layer_surface.new_popup); // wlroots only informs us of the new surface after the first commit, // so our listener does not get called for this first commit. However, @@ -65,6 +70,11 @@ pub fn create(wlr_layer_surface: *wlr.LayerSurfaceV1) error{OutOfMemory}!void { handleCommit(&layer_surface.commit, wlr_layer_surface.surface); } +pub fn destroyPopups(layer_surface: *LayerSurface) void { + var it = layer_surface.scene_layer_surface.layer_surface.popups.safeIterator(.forward); + while (it.next()) |wlr_xdg_popup| wlr_xdg_popup.destroy(); +} + fn handleDestroy(listener: *wl.Listener(*wlr.LayerSurfaceV1), wlr_layer_surface: *wlr.LayerSurfaceV1) void { const layer_surface = @fieldParentPtr(LayerSurface, "destroy", listener); @@ -75,6 +85,8 @@ fn handleDestroy(listener: *wl.Listener(*wlr.LayerSurfaceV1), wlr_layer_surface: layer_surface.unmap.link.remove(); layer_surface.commit.link.remove(); + layer_surface.destroyPopups(); + util.gpa.destroy(layer_surface); } @@ -163,3 +175,17 @@ fn handleKeyboardInteractiveExclusive(output: *Output) void { } } } + +fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.XdgPopup) void { + const layer_surface = @fieldParentPtr(LayerSurface, "new_popup", listener); + + XdgPopup.create( + wlr_xdg_popup, + layer_surface.popup_tree, + layer_surface.popup_tree, + &layer_surface.output, + ) catch { + wlr_xdg_popup.resource.postNoMemory(); + return; + }; +} diff --git a/river/Output.zig b/river/Output.zig index 48f16cd..036495c 100644 --- a/river/Output.zig +++ b/river/Output.zig @@ -329,7 +329,12 @@ pub fn arrangeLayers(self: *Self) void { while (it.next()) |node| { assert(node.type == .tree); if (@intToPtr(?*SceneNodeData, node.data)) |node_data| { - node_data.data.layer_surface.scene_layer_surface.configure(&full_box, &usable_box); + const layer_surface = node_data.data.layer_surface; + layer_surface.scene_layer_surface.configure(&full_box, &usable_box); + layer_surface.popup_tree.node.setPosition( + layer_surface.scene_layer_surface.tree.node.x, + layer_surface.scene_layer_surface.tree.node.y, + ); } } } diff --git a/river/Seat.zig b/river/Seat.zig index 9796bf7..d53532c 100644 --- a/river/Seat.zig +++ b/river/Seat.zig @@ -241,8 +241,12 @@ pub fn setFocusRaw(self: *Self, new_focus: FocusTarget) void { .view => |view| { view.pending.focus -= 1; if (view.pending.focus == 0) view.setActivated(false); + view.destroyPopups(); }, - .xwayland_override_redirect, .layer, .lock_surface, .none => {}, + .layer => |layer_surface| { + layer_surface.destroyPopups(); + }, + .xwayland_override_redirect, .lock_surface, .none => {}, } // Set the new focus diff --git a/river/Server.zig b/river/Server.zig index 6b2243e..a866f9b 100644 --- a/river/Server.zig +++ b/river/Server.zig @@ -100,7 +100,7 @@ pub fn init(self: *Self) !void { const compositor = try wlr.Compositor.create(self.wl_server, self.renderer); _ = try wlr.Subcompositor.create(self.wl_server); - self.xdg_shell = try wlr.XdgShell.create(self.wl_server, 2); + self.xdg_shell = try wlr.XdgShell.create(self.wl_server, 3); self.new_xdg_surface.setNotify(handleNewXdgSurface); self.xdg_shell.events.new_surface.add(&self.new_xdg_surface); diff --git a/river/View.zig b/river/View.zig index 7d38f1d..8cd96fe 100644 --- a/river/View.zig +++ b/river/View.zig @@ -84,6 +84,7 @@ borders: struct { top: *wlr.SceneRect, bottom: *wlr.SceneRect, }, +popup_tree: *wlr.SceneTree, /// This indicates that the view should be destroyed when the current /// transaction completes. See View.destroy() @@ -126,8 +127,6 @@ pub fn init(self: *Self, output: *Output, tree: *wlr.SceneTree, impl: Impl) erro break :blk if (tags != 0) tags else output.current.tags; }; - try SceneNodeData.attach(&tree.node, .{ .view = self }); - self.* = .{ .impl = impl, .output = output, @@ -138,9 +137,13 @@ pub fn init(self: *Self, output: *Output, tree: *wlr.SceneTree, impl: Impl) erro .top = try tree.createSceneRect(0, 0, &server.config.border_color_unfocused), .bottom = try tree.createSceneRect(0, 0, &server.config.border_color_unfocused), }, + .popup_tree = try output.layers.popups.createSceneTree(), .current = .{ .tags = initial_tags }, .pending = .{ .tags = initial_tags }, }; + + try SceneNodeData.attach(&self.tree.node, .{ .view = self }); + try SceneNodeData.attach(&self.popup_tree.node, .{ .view = self }); } /// If saved buffers of the view are currently in use by a transaction, @@ -211,6 +214,7 @@ pub fn updateCurrent(self: *Self) void { const box = &self.current.box; self.tree.node.setPosition(box.x, box.y); + self.popup_tree.node.setPosition(box.x, box.y); const border_width: c_int = config.border_width; self.borders.left.node.setPosition(-border_width, -border_width); @@ -361,6 +365,13 @@ pub fn close(self: Self) void { } } +pub fn destroyPopups(self: Self) void { + switch (self.impl) { + .xdg_toplevel => |xdg_toplevel| xdg_toplevel.destroyPopups(), + .xwayland_view => {}, + } +} + pub fn setActivated(self: Self, activated: bool) void { switch (self.impl) { .xdg_toplevel => |xdg_toplevel| xdg_toplevel.setActivated(activated), @@ -489,6 +500,7 @@ pub fn map(self: *Self) !void { log.debug("view '{?s}' mapped", .{self.getTitle()}); self.tree.node.setEnabled(true); + self.popup_tree.node.setEnabled(true); server.xdg_activation.events.request_activate.add(&self.request_activate); @@ -510,6 +522,7 @@ pub fn unmap(self: *Self) void { log.debug("view '{?s}' unmapped", .{self.getTitle()}); self.tree.node.setEnabled(false); + self.popup_tree.node.setEnabled(false); if (self.saved_buffers.items.len == 0) self.saveBuffers(); diff --git a/river/XdgPopup.zig b/river/XdgPopup.zig new file mode 100644 index 0000000..a4200ea --- /dev/null +++ b/river/XdgPopup.zig @@ -0,0 +1,105 @@ +// This file is part of river, a dynamic tiling wayland compositor. +// +// Copyright 2023 The River Developers +// +// 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, version 3. +// +// 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 XdgPopup = @This(); + +const std = @import("std"); +const wlr = @import("wlroots"); +const wl = @import("wayland").server.wl; + +const server = &@import("main.zig").server; +const util = @import("util.zig"); + +const Output = @import("Output.zig"); + +const log = std.log.scoped(.xdg_popup); + +wlr_xdg_popup: *wlr.XdgPopup, +/// This isn't terribly clean, but pointing to the output field of the parent +/// View or LayerSurface struct is ok in practice as all popups are destroyed +/// before their parent View or LayerSurface. +output: *const *Output, +/// The root of the surface tree, i.e. the View or LayerSurface popup_tree. +root: *wlr.SceneTree, + +tree: *wlr.SceneTree, + +destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy), +new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(handleNewPopup), +reposition: wl.Listener(void) = wl.Listener(void).init(handleReposition), + +pub fn create( + wlr_xdg_popup: *wlr.XdgPopup, + root: *wlr.SceneTree, + parent: *wlr.SceneTree, + output: *const *Output, +) error{OutOfMemory}!void { + const xdg_popup = try util.gpa.create(XdgPopup); + errdefer util.gpa.destroy(xdg_popup); + + xdg_popup.* = .{ + .wlr_xdg_popup = wlr_xdg_popup, + .output = output, + .root = root, + .tree = try parent.createSceneXdgSurface(wlr_xdg_popup.base), + }; + + wlr_xdg_popup.base.events.destroy.add(&xdg_popup.destroy); + wlr_xdg_popup.base.events.new_popup.add(&xdg_popup.new_popup); + wlr_xdg_popup.events.reposition.add(&xdg_popup.reposition); + + handleReposition(&xdg_popup.reposition); +} + +fn handleDestroy(listener: *wl.Listener(void)) void { + const xdg_popup = @fieldParentPtr(XdgPopup, "destroy", listener); + + xdg_popup.destroy.link.remove(); + xdg_popup.new_popup.link.remove(); + xdg_popup.reposition.link.remove(); + + util.gpa.destroy(xdg_popup); +} + +fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.XdgPopup) void { + const xdg_popup = @fieldParentPtr(XdgPopup, "new_popup", listener); + + XdgPopup.create( + wlr_xdg_popup, + xdg_popup.root, + xdg_popup.tree, + xdg_popup.output, + ) catch { + wlr_xdg_popup.resource.postNoMemory(); + return; + }; +} + +fn handleReposition(listener: *wl.Listener(void)) void { + const xdg_popup = @fieldParentPtr(XdgPopup, "reposition", listener); + + var box: wlr.Box = undefined; + server.root.output_layout.getBox(xdg_popup.output.*.wlr_output, &box); + + var root_lx: c_int = undefined; + var root_ly: c_int = undefined; + _ = xdg_popup.root.node.coords(&root_lx, &root_ly); + + box.x -= root_lx; + box.y -= root_ly; + + xdg_popup.wlr_xdg_popup.unconstrainFromBox(&box); +} diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig index a5aa128..ba63ee9 100644 --- a/river/XdgToplevel.zig +++ b/river/XdgToplevel.zig @@ -26,6 +26,7 @@ const util = @import("util.zig"); const Output = @import("Output.zig"); const Seat = @import("Seat.zig"); +const XdgPopup = @import("XdgPopup.zig"); const View = @import("View.zig"); const ViewStack = @import("view_stack.zig").ViewStack; @@ -44,6 +45,7 @@ acked_pending_serial: bool = false, destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy), map: wl.Listener(void) = wl.Listener(void).init(handleMap), unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap), +new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(handleNewPopup), // Listeners that are only active while the view is mapped ack_configure: wl.Listener(*wlr.XdgSurface.Configure) = @@ -78,6 +80,7 @@ pub fn create(output: *Output, xdg_toplevel: *wlr.XdgToplevel) error{OutOfMemory xdg_toplevel.base.events.destroy.add(&self.destroy); xdg_toplevel.base.events.map.add(&self.map); xdg_toplevel.base.events.unmap.add(&self.unmap); + xdg_toplevel.base.events.new_popup.add(&self.new_popup); } /// Returns true if a configure must be sent to ensure that the pending @@ -145,6 +148,11 @@ pub fn getConstraints(self: Self) View.Constraints { }; } +pub fn destroyPopups(self: Self) void { + var it = self.xdg_toplevel.base.popups.safeIterator(.forward); + while (it.next()) |wlr_xdg_popup| wlr_xdg_popup.destroy(); +} + fn handleDestroy(listener: *wl.Listener(void)) void { const self = @fieldParentPtr(Self, "destroy", listener); @@ -233,6 +241,20 @@ fn handleUnmap(listener: *wl.Listener(void)) void { self.view.unmap(); } +fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.XdgPopup) void { + const self = @fieldParentPtr(Self, "new_popup", listener); + + XdgPopup.create( + wlr_xdg_popup, + self.view.popup_tree, + self.view.popup_tree, + &self.view.output, + ) catch { + wlr_xdg_popup.resource.postNoMemory(); + return; + }; +} + fn handleAckConfigure( listener: *wl.Listener(*wlr.XdgSurface.Configure), acked_configure: *wlr.XdgSurface.Configure,