XdgPopup: reimplement using the scene graph
xdg-shell version 3 is now implemented, supporting popup repositioning.
This commit is contained in:
		| @ -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; | ||||
|     }; | ||||
| } | ||||
|  | ||||
| @ -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, | ||||
|                 ); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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); | ||||
|  | ||||
|  | ||||
| @ -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(); | ||||
|  | ||||
|  | ||||
							
								
								
									
										105
									
								
								river/XdgPopup.zig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								river/XdgPopup.zig
									
									
									
									
									
										Normal file
									
								
							| @ -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 <https://www.gnu.org/licenses/>. | ||||
|  | ||||
| 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); | ||||
| } | ||||
| @ -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, | ||||
|  | ||||
		Reference in New Issue
	
	Block a user