diff options
34 files changed, 1021 insertions, 1537 deletions
diff --git a/.builds/alpine.yml b/.builds/alpine.yml index 7249771..f6bd544 100644 --- a/.builds/alpine.yml +++ b/.builds/alpine.yml @@ -50,9 +50,6 @@ tasks: - build_xwayland: | cd river zig build -Dxwayland - - xwayland_test: | - cd river - zig build -Dxwayland test - fmt: | cd river zig fmt --check river/ diff --git a/.builds/archlinux.yml b/.builds/archlinux.yml index a1a9769..70653f5 100644 --- a/.builds/archlinux.yml +++ b/.builds/archlinux.yml @@ -48,9 +48,6 @@ tasks: - build_xwayland: | cd river zig build -Dxwayland - - xwayland_test: | - cd river - zig build -Dxwayland test - fmt: | cd river zig fmt --check river/ diff --git a/.builds/freebsd.yml b/.builds/freebsd.yml index ea807da..9f48f80 100644 --- a/.builds/freebsd.yml +++ b/.builds/freebsd.yml @@ -52,9 +52,6 @@ tasks: - build_xwayland: | cd river zig build -Dxwayland - - xwayland_test: | - cd river - zig build -Dxwayland test - fmt: | cd river zig fmt --check river/ @@ -123,7 +123,47 @@ pub fn build(b: *zbs.Builder) !void { river.setBuildMode(mode); river.addOptions("build_options", options); - addServerDeps(river, scanner); + const wayland = zbs.Pkg{ + .name = "wayland", + .source = .{ .generated = &scanner.result }, + }; + const xkbcommon = zbs.Pkg{ + .name = "xkbcommon", + .source = .{ .path = "deps/zig-xkbcommon/src/xkbcommon.zig" }, + }; + const pixman = zbs.Pkg{ + .name = "pixman", + .source = .{ .path = "deps/zig-pixman/pixman.zig" }, + }; + const wlroots = zbs.Pkg{ + .name = "wlroots", + .source = .{ .path = "deps/zig-wlroots/src/wlroots.zig" }, + .dependencies = &[_]zbs.Pkg{ wayland, xkbcommon, pixman }, + }; + + river.step.dependOn(&scanner.step); + + river.linkLibC(); + river.linkSystemLibrary("libevdev"); + river.linkSystemLibrary("libinput"); + + river.addPackage(wayland); + river.linkSystemLibrary("wayland-server"); + + river.addPackage(xkbcommon); + river.linkSystemLibrary("xkbcommon"); + + river.addPackage(pixman); + river.linkSystemLibrary("pixman-1"); + + river.addPackage(wlroots); + river.linkSystemLibrary("wlroots"); + + river.addPackagePath("flags", "common/flags.zig"); + river.addCSourceFile("river/wlroots_log_wrapper.c", &[_][]const u8{ "-std=c99", "-O2" }); + + // TODO: remove when zig issue #131 is implemented + scanner.addCSource(river); river.strip = strip; river.pie = pie; @@ -211,62 +251,6 @@ pub fn build(b: *zbs.Builder) !void { if (fish_completion) { b.installFile("completions/fish/riverctl.fish", "share/fish/vendor_completions.d/riverctl.fish"); } - - { - const river_test = b.addTest("river/test_main.zig"); - river_test.setTarget(target); - river_test.setBuildMode(mode); - river_test.addOptions("build_options", options); - - addServerDeps(river_test, scanner); - - const test_step = b.step("test", "Run the tests"); - test_step.dependOn(&river_test.step); - } -} - -fn addServerDeps(exe: *zbs.LibExeObjStep, scanner: *ScanProtocolsStep) void { - const wayland = zbs.Pkg{ - .name = "wayland", - .source = .{ .generated = &scanner.result }, - }; - const xkbcommon = zbs.Pkg{ - .name = "xkbcommon", - .source = .{ .path = "deps/zig-xkbcommon/src/xkbcommon.zig" }, - }; - const pixman = zbs.Pkg{ - .name = "pixman", - .source = .{ .path = "deps/zig-pixman/pixman.zig" }, - }; - const wlroots = zbs.Pkg{ - .name = "wlroots", - .source = .{ .path = "deps/zig-wlroots/src/wlroots.zig" }, - .dependencies = &[_]zbs.Pkg{ wayland, xkbcommon, pixman }, - }; - - exe.step.dependOn(&scanner.step); - - exe.linkLibC(); - exe.linkSystemLibrary("libevdev"); - exe.linkSystemLibrary("libinput"); - - exe.addPackage(wayland); - exe.linkSystemLibrary("wayland-server"); - - exe.addPackage(xkbcommon); - exe.linkSystemLibrary("xkbcommon"); - - exe.addPackage(pixman); - exe.linkSystemLibrary("pixman-1"); - - exe.addPackage(wlroots); - exe.linkSystemLibrary("wlroots"); - - exe.addPackagePath("flags", "common/flags.zig"); - exe.addCSourceFile("river/wlroots_log_wrapper.c", &[_][]const u8{ "-std=c99", "-O2" }); - - // TODO: remove when zig issue #131 is implemented - scanner.addCSource(exe); } const ScdocStep = struct { diff --git a/river/Config.zig b/river/Config.zig index f26136b..08bca49 100644 --- a/river/Config.zig +++ b/river/Config.zig @@ -24,9 +24,13 @@ const util = @import("util.zig"); const Server = @import("Server.zig"); const Mode = @import("Mode.zig"); -const AttachMode = @import("view_stack.zig").AttachMode; const View = @import("View.zig"); +pub const AttachMode = enum { + top, + bottom, +}; + pub const FocusFollowsCursorMode = enum { disabled, /// Only change focus on entering a surface diff --git a/river/Cursor.zig b/river/Cursor.zig index b710903..edc3351 100644 --- a/river/Cursor.zig +++ b/river/Cursor.zig @@ -38,7 +38,6 @@ const Output = @import("Output.zig"); const Root = @import("Root.zig"); const Seat = @import("Seat.zig"); const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig"); const Mode = union(enum) { @@ -253,17 +252,6 @@ pub fn setTheme(self: *Self, theme: ?[*:0]const u8, _size: ?u32) !void { } } -pub fn handleViewUnmap(self: *Self, view: *View) void { - if (switch (self.mode) { - .passthrough, .down => false, - .move => |data| data.view == view, - .resize => |data| data.view == view, - }) { - self.mode = .passthrough; - self.clearFocus(); - } -} - /// It seems that setCursorImage is actually fairly expensive to call repeatedly /// as it does no checks to see if the the given image is already set. Therefore, /// do that check here. @@ -346,7 +334,7 @@ fn handleButton(listener: *wl.Listener(*wlr.Pointer.event.Button), event: *wlr.P self.updateOutputFocus(self.wlr_cursor.x, self.wlr_cursor.y); } - server.root.startTransaction(); + server.root.applyPending(); } fn updateKeyboardFocus(self: Self, result: Root.AtResult) void { @@ -505,7 +493,7 @@ fn handleTouchDown( self.updateOutputFocus(lx, ly); } - server.root.startTransaction(); + server.root.applyPending(); } fn handleTouchMotion( @@ -558,7 +546,7 @@ fn handlePointerMapping(self: *Self, event: *wlr.Pointer.event.Button, view: *Vi // This is mildly inefficient as running the command may have already // started a transaction. However we need to start one after the Seat.focus() // call in the case where it didn't. - server.root.startTransaction(); + server.root.applyPending(); }, } break true; @@ -679,20 +667,17 @@ pub fn enterMode(self: *Self, mode: enum { move, resize }, view: *View) void { // their dimensions are set by a layout generator. If however the views // are unarranged, leave them as non-floating so the next active // layout can affect them. - if (!view.current.float and view.output.current.layout != null) { + if (!view.current.float and view.current.output.?.layout != null) { view.pending.float = true; view.float_box = view.current.box; - view.applyPending(); - } else { - // The View.applyPending() call in the other branch starts - // the transaction needed after the seat.focus() call above. - server.root.startTransaction(); } // Clear cursor focus, so that the surface does not receive events self.seat.wlr_seat.pointerNotifyClearFocus(); self.setImage(if (mode == .move) .move else .@"se-resize"); + + server.root.applyPending(); } /// Return from down/move/resize to passthrough @@ -756,7 +741,7 @@ fn processMotion(self: *Self, device: *wlr.InputDevice, time: u32, delta_x: f64, @intToFloat(f64, view.pending.box.x - view.current.box.x), @intToFloat(f64, view.pending.box.y - view.current.box.y), ); - view.applyPending(); + server.root.applyPending(); }, .resize => |*data| { dx += data.delta_x; @@ -769,24 +754,23 @@ fn processMotion(self: *Self, device: *wlr.InputDevice, time: u32, delta_x: f64, // Set width/height of view, clamp to view size constraints and output dimensions data.view.pending.box.width += @floatToInt(i32, dx); data.view.pending.box.height += @floatToInt(i32, dy); - data.view.applyConstraints(); + data.view.applyConstraints(&data.view.pending.box); var output_width: i32 = undefined; var output_height: i32 = undefined; - data.view.output.wlr_output.effectiveResolution(&output_width, &output_height); + data.view.current.output.?.wlr_output.effectiveResolution(&output_width, &output_height); const box = &data.view.pending.box; box.width = math.min(box.width, output_width - border_width - box.x); box.height = math.min(box.height, output_height - border_width - box.y); - data.view.applyPending(); - // Keep cursor locked to the original offset from the bottom right corner self.wlr_cursor.warpClosest( device, @intToFloat(f64, box.x + box.width - data.offset_x), @intToFloat(f64, box.y + box.height - data.offset_y), ); + server.root.applyPending(); }, } } @@ -806,16 +790,16 @@ pub fn checkFocusFollowsCursor(self: *Self) void { // geometry, we only want to move focus when the cursor // properly enters the window (the box that we draw borders around) var output_layout_box: wlr.Box = undefined; - server.root.output_layout.getBox(view.output.wlr_output, &output_layout_box); + server.root.output_layout.getBox(view.current.output.?.wlr_output, &output_layout_box); const cursor_ox = self.wlr_cursor.x - @intToFloat(f64, output_layout_box.x); const cursor_oy = self.wlr_cursor.y - @intToFloat(f64, output_layout_box.y); if ((self.seat.focused != .view or self.seat.focused.view != view) and view.current.box.containsPoint(cursor_ox, cursor_oy)) { - self.seat.focusOutput(view.output); + self.seat.focusOutput(view.current.output.?); self.seat.focus(view); self.last_focus_follows_cursor_target = view; - server.root.startTransaction(); + server.root.applyPending(); } }, .layer_surface, .lock_surface => {}, @@ -866,8 +850,9 @@ fn shouldPassthrough(self: Self) bool { assert(server.lock_manager.state != .locked); const target = if (self.mode == .resize) self.mode.resize.view else self.mode.move.view; // The target view is no longer visible, is part of the layout, or is fullscreen. - return target.current.tags & target.output.current.tags == 0 or - (!target.current.float and target.output.current.layout != null) or + return target.current.output == null or + target.current.tags & target.current.output.?.current.tags == 0 or + (!target.current.float and target.current.output.?.layout != null) or target.current.fullscreen; }, } @@ -896,10 +881,12 @@ fn passthrough(self: *Self, time: u32) void { fn warp(self: *Self) void { self.may_need_warp = false; - if (self.seat.focused_output == &server.root.noop_output) return; + + const focused_output = self.seat.focused_output orelse return; + // Warp pointer to center of the focused view/output (In layout coordinates) if enabled. var output_layout_box: wlr.Box = undefined; - server.root.output_layout.getBox(self.seat.focused_output.wlr_output, &output_layout_box); + server.root.output_layout.getBox(focused_output.wlr_output, &output_layout_box); const target_box = switch (server.config.warp_cursor) { .disabled => return, .@"on-output-change" => output_layout_box, @@ -921,7 +908,7 @@ fn warp(self: *Self) void { }; // Checking against the usable box here gives much better UX when, for example, // a status bar allows using the pointer to change tag/view focus. - const usable_box = self.seat.focused_output.usable_box; + const usable_box = focused_output.usable_box; const usable_layout_box = wlr.Box{ .x = output_layout_box.x + usable_box.x, .y = output_layout_box.y + usable_box.y, diff --git a/river/IdleInhibitorManager.zig b/river/IdleInhibitorManager.zig index 184dbdb..e3b198c 100644 --- a/river/IdleInhibitorManager.zig +++ b/river/IdleInhibitorManager.zig @@ -34,7 +34,7 @@ pub fn idleInhibitCheckActive(self: *Self) void { while (it) |node| : (it = node.next) { if (View.fromWlrSurface(node.data.inhibitor.surface)) |v| { // If view is visible, - if (v.current.tags & v.output.current.tags != 0) { + if (v.current.output != null and v.current.tags & v.current.output.?.current.tags != 0) { inhibited = true; break; } diff --git a/river/InputManager.zig b/river/InputManager.zig index df54c72..2992f78 100644 --- a/river/InputManager.zig +++ b/river/InputManager.zig @@ -107,11 +107,6 @@ pub fn inputAllowed(self: Self, wlr_surface: *wlr.Surface) bool { true; } -pub fn updateCursorState(self: Self) void { - var it = self.seats.first; - while (it) |node| : (it = node.next) node.data.cursor.updateState(); -} - fn handleNewInput(listener: *wl.Listener(*wlr.InputDevice), wlr_device: *wlr.InputDevice) void { const self = @fieldParentPtr(Self, "new_input", listener); diff --git a/river/LayerSurface.zig b/river/LayerSurface.zig index e0a273a..8ad75b5 100644 --- a/river/LayerSurface.zig +++ b/river/LayerSurface.zig @@ -97,7 +97,7 @@ fn handleMap(listener: *wl.Listener(*wlr.LayerSurfaceV1), wlr_layer_surface: *wl layer_surface.output.arrangeLayers(); handleKeyboardInteractiveExclusive(layer_surface.output); - server.root.startTransaction(); + server.root.applyPending(); } fn handleUnmap(listener: *wl.Listener(*wlr.LayerSurfaceV1), wlr_layer_surface: *wlr.LayerSurfaceV1) void { @@ -107,7 +107,7 @@ fn handleUnmap(listener: *wl.Listener(*wlr.LayerSurfaceV1), wlr_layer_surface: * layer_surface.output.arrangeLayers(); handleKeyboardInteractiveExclusive(layer_surface.output); - server.root.startTransaction(); + server.root.applyPending(); } fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void { @@ -123,10 +123,12 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void { } // If a surface is committed while it is not mapped, we must send a configure. + // TODO: this mapped check is not correct as it will be true in the commit + // that triggers the unmap as well. if (!wlr_layer_surface.mapped or @bitCast(u32, wlr_layer_surface.current.committed) != 0) { layer_surface.output.arrangeLayers(); handleKeyboardInteractiveExclusive(layer_surface.output); - server.root.startTransaction(); + server.root.applyPending(); } } @@ -183,7 +185,6 @@ fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.Xdg 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/Layout.zig b/river/Layout.zig index e5f6345..b6361ea 100644 --- a/river/Layout.zig +++ b/river/Layout.zig @@ -28,10 +28,8 @@ const river = wayland.server.river; const server = &@import("main.zig").server; const util = @import("util.zig"); -const Server = @import("Server.zig"); const Output = @import("Output.zig"); const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; const LayoutDemand = @import("LayoutDemand.zig"); const log = std.log.scoped(.layout); @@ -63,8 +61,8 @@ pub fn create(client: *wl.Client, version: u32, id: u32, output: *Output, namesp // If the namespace matches that of the output, set the layout as // the active one of the output and arrange it. if (mem.eql(u8, namespace, output.layoutNamespace())) { - output.pending.layout = &node.data; - output.arrangeViews(); + output.layout = &node.data; + server.root.applyPending(); } } @@ -103,8 +101,8 @@ pub fn startLayoutDemand(self: *Self, views: u32) void { .{ self.namespace, self.output.wlr_output.name }, ); - assert(self.output.layout_demand == null); - self.output.layout_demand = LayoutDemand.init(self, views) catch { + assert(self.output.inflight.layout_demand == null); + self.output.inflight.layout_demand = LayoutDemand.init(self, views) catch { log.err("failed starting layout demand", .{}); return; }; @@ -114,10 +112,10 @@ pub fn startLayoutDemand(self: *Self, views: u32) void { @intCast(u32, self.output.usable_box.width), @intCast(u32, self.output.usable_box.height), self.output.pending.tags, - self.output.layout_demand.?.serial, + self.output.inflight.layout_demand.?.serial, ); - server.root.trackLayoutDemands(); + server.root.inflight_layout_demands += 1; } fn handleRequest(layout: *river.LayoutV3, request: river.LayoutV3.Request, self: *Self) void { @@ -132,7 +130,7 @@ fn handleRequest(layout: *river.LayoutV3, request: river.LayoutV3.Request, self: .{ self.namespace, self.output.wlr_output.name, req.x, req.y, req.width, req.height }, ); - if (self.output.layout_demand) |*layout_demand| { + if (self.output.inflight.layout_demand) |*layout_demand| { // We can't raise a protocol error when the serial is old/wrong // because we do not keep track of old serials server-side. // Therefore, simply ignore requests with old/wrong serials. @@ -154,7 +152,7 @@ fn handleRequest(layout: *river.LayoutV3, request: river.LayoutV3.Request, self: .{ self.namespace, self.output.wlr_output.name }, ); - if (self.output.layout_demand) |*layout_demand| { + if (self.output.inflight.layout_demand) |*layout_demand| { // We can't raise a protocol error when the serial is old/wrong // because we do not keep track of old serials server-side. // Therefore, simply ignore requests with old/wrong serials. @@ -185,11 +183,11 @@ pub fn destroy(self: *Self) void { self.output.layouts.remove(node); // If we are the currently active layout of an output, clean up. - if (self.output.pending.layout == self) { - self.output.pending.layout = null; - if (self.output.layout_demand) |*layout_demand| { + if (self.output.layout == self) { + self.output.layout = null; + if (self.output.inflight.layout_demand) |*layout_demand| { layout_demand.deinit(); - self.output.layout_demand = null; + self.output.inflight.layout_demand = null; server.root.notifyLayoutDemandDone(); } diff --git a/river/LayoutDemand.zig b/river/LayoutDemand.zig index 10384ae..6bf637b 100644 --- a/river/LayoutDemand.zig +++ b/river/LayoutDemand.zig @@ -29,7 +29,6 @@ const Layout = @import("Layout.zig"); const Server = @import("Server.zig"); const Output = @import("Output.zig"); const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; const log = std.log.scoped(.layout); @@ -71,8 +70,8 @@ fn handleTimeout(layout: *Layout) c_int { "layout demand for layout '{s}' on output '{s}' timed out", .{ layout.namespace, layout.output.wlr_output.name }, ); - layout.output.layout_demand.?.deinit(); - layout.output.layout_demand = null; + layout.output.inflight.layout_demand.?.deinit(); + layout.output.inflight.layout_demand = null; server.root.notifyLayoutDemandDone(); @@ -104,8 +103,8 @@ pub fn apply(self: *Self, layout: *Layout) void { // Whether the layout demand succeeds or fails, we are done with it and // need to clean up defer { - output.layout_demand.?.deinit(); - output.layout_demand = null; + output.inflight.layout_demand.?.deinit(); + output.inflight.layout_demand = null; server.root.notifyLayoutDemandDone(); } @@ -122,24 +121,36 @@ pub fn apply(self: *Self, layout: *Layout) void { return; } - // Apply proposed layout to views - var it = ViewStack(View).iter(output.views.first, .forward, output.pending.tags, Output.arrangeFilter); + // Apply proposed layout to the inflight state of the target views + var it = output.inflight.wm_stack.iterator(.forward); var i: u32 = 0; - while (it.next()) |view| : (i += 1) { - const proposed = &self.view_boxen[i]; - - // Here we apply the offset to align the coords with the origin of the - // usable area and shrink the dimensions to accomodate the border size. - const border_width = if (view.draw_borders) server.config.border_width else 0; - view.pending.box = .{ - .x = proposed.x + output.usable_box.x + border_width, - .y = proposed.y + output.usable_box.y + border_width, - .width = proposed.width - 2 * border_width, - .height = proposed.height - 2 * border_width, - }; - - view.applyConstraints(); + while (it.next()) |view| { + if (!view.inflight.float and !view.inflight.fullscreen and + view.inflight.tags & output.inflight.tags != 0) + { + const proposed = &self.view_boxen[i]; + + // Here we apply the offset to align the coords with the origin of the + // usable area and shrink the dimensions to accommodate the border size. + const border_width = if (view.draw_borders) server.config.border_width else 0; + view.inflight.box = .{ + .x = proposed.x + output.usable_box.x + border_width, + .y = proposed.y + output.usable_box.y + border_width, + .width = proposed.width - 2 * border_width, + .height = proposed.height - 2 * border_width, + }; + + view.applyConstraints(&view.inflight.box); + + // State flowing "backwards" like this is pretty ugly, but I don't + // see a better way to sync this up right now. + if (!view.pending.float and !view.pending.fullscreen) { + view.pending.box = view.inflight.box; + } + + i += 1; + } } assert(i == self.view_boxen.len); - assert(output.pending.layout == layout); + assert(output.layout == layout); } diff --git a/river/Output.zig b/river/Output.zig index 2917bad..93b5a26 100644 --- a/river/Output.zig +++ b/river/Output.zig @@ -37,23 +37,6 @@ const LockSurface = @import("LockSurface.zig"); const OutputStatus = @import("OutputStatus.zig"); const SceneNodeData = @import("SceneNodeData.zig"); const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; - -const State = struct { - /// A bit field of focused tags - tags: u32, - - /// Active layout, or null if views are un-arranged. - /// - /// If null, views which are manually moved or resized (with the pointer or - /// or command) will not be automatically set to floating. Everything is - /// already floating, so this would be an unexpected change of a views state - /// the user will only notice once a layout affects the views. So instead we - /// "snap back" all manually moved views the next time a layout is active. - /// This is similar to dwms behvaviour. Note that this of course does not - /// affect already floating views. - layout: ?*Layout = null, -}; wlr_output: *wlr.Output, @@ -87,9 +70,6 @@ layers: struct { popups: *wlr.SceneTree, }, -/// The top of the stack is the "most important" view. -views: ViewStack(View) = .{}, - /// Tracks the currently presented frame on the output as it pertains to ext-session-lock. /// The output is initially considered blanked: /// If using the DRM backend it will be blanked with the initial modeset. @@ -109,16 +89,66 @@ lock_render_state: enum { lock_surface, } = .blanked, -/// The double-buffered state of the output. -current: State = State{ .tags = 1 << 0 }, -pending: State = State{ .tags = 1 << 0 }, +/// The state of the output that is directly acted upon/modified through user input. +/// +/// Pending state will be copied to the pending state and communicated to clients +/// to be applied as a single atomic transaction across all clients as soon as any +/// in progress transaction has been completed. +/// +/// On completion of a transaction +/// Any time pending state is modified Root.dirty must be set. +/// +pending: struct { + /// A bit field of focused tags + tags: u32 = 1 << 0, + /// The stack of views in focus/rendering order. + /// + /// This contains views that aren't currently visible because they do not + /// match the tags of the output. + /// + /// This list is used to update the rendering order of nodes in the scene + /// graph when the pending state is committed. + focus_stack: wl.list.Head(View, .pending_focus_stack_link), + /// The stack of views acted upon by window management commands such + /// as focus-view, zoom, etc. + /// + /// This contains views that aren't currently visible because they do not + /// match the tags of the output. This means that a filtered version of the + /// list must be used for window management commands. + /// + /// This includes both floating/fullscreen views and those arranged in the layout. + wm_stack: wl.list.Head(View, .pending_wm_stack_link), +}, + +/// The state most recently sent to the layout generator and clients. +/// This state is immutable until all clients have replied and the transaction +/// is completed, at which point this inflight state is copied to current. +inflight: struct { + /// A bit field of focused tags + tags: u32 = 1 << 0, + /// See pending.focus_stack + focus_stack: wl.list.Head(View, .inflight_focus_stack_link), + /// See pending.wm_stack + wm_stack: wl.list.Head(View, .inflight_wm_stack_link), + /// The view to be made fullscreen, if any. + fullscreen: ?*View = null, + layout_demand: ?LayoutDemand = null, +}, + +/// The current state represented by the scene graph. +/// There is no need to have a current focus_stack/wm_stack copy as this +/// information is transferred from the inflight state to the scene graph +/// as an inflight transaction completes. +current: struct { + /// A bit field of focused tags + tags: u32 = 1 << 0, + /// The currently fullscreen view, if any. + fullscreen: ?*View = null, +} = .{}, /// Remembered version of tags (from last run) previous_tags: u32 = 1 << 0, -/// The currently active LayoutDemand -layout_demand: ?LayoutDemand = null, - /// List of all layouts layouts: std.TailQueue(Layout) = .{}, @@ -130,6 +160,17 @@ layout_namespace: ?[]const u8 = null, /// The last set layout name. layout_name: ?[:0]const u8 = null, +/// Active layout, or null if views are un-arranged. +/// +/// If null, views which are manually moved or resized (with the pointer or +/// or command) will not be automatically set to floating. Everything is +/// already floating, so this would be an unexpected change of a views state +/// the user will only notice once a layout affects the views. So instead we +/// "snap back" all manually moved views the next time a layout is active. +/// This is similar to dwms behvaviour. Note that this of course does not +/// affect already floating views. +layout: ?*Layout = null, + /// List of status tracking objects relaying changes to this output to clients. status_trackers: std.SinglyLinkedList(OutputStatus) = .{}, @@ -189,6 +230,14 @@ pub fn create(wlr_output: *wlr.Output) !void { .overlay = try normal_content.createSceneTree(), .popups = try normal_content.createSceneTree(), }, + .pending = .{ + .focus_stack = undefined, + .wm_stack = undefined, + }, + .inflight = .{ + .focus_stack = undefined, + .wm_stack = undefined, + }, .usable_box = .{ .x = 0, .y = 0, @@ -198,6 +247,11 @@ pub fn create(wlr_output: *wlr.Output) !void { }; wlr_output.data = @ptrToInt(self); + self.pending.focus_stack.init(); + self.pending.wm_stack.init(); + self.inflight.focus_stack.init(); + self.inflight.wm_stack.init(); + _ = try self.layers.fullscreen.createSceneRect(width, height, &[_]f32{ 0, 0, 0, 1.0 }); self.layers.fullscreen.node.setEnabled(false); @@ -240,16 +294,18 @@ pub fn sendViewTags(self: Self) void { while (it) |node| : (it = node.next) node.data.sendViewTags(); } -pub fn sendUrgentTags(self: Self) void { +pub fn sendUrgentTags(output: *Self) void { var urgent_tags: u32 = 0; - - var view_it = self.views.first; - while (view_it) |node| : (view_it = node.next) { - if (node.view.current.urgent) urgent_tags |= node.view.current.tags; + { + var it = output.inflight.wm_stack.iterator(.forward); + while (it.next()) |view| { + if (view.current.urgent) urgent_tags |= view.current.tags; + } + } + { + var it = output.status_trackers.first; + while (it) |node| : (it = node.next) node.data.sendUrgentTags(urgent_tags); } - - var it = self.status_trackers.first; - while (it) |node| : (it = node.next) node.data.sendUrgentTags(urgent_tags); } pub fn sendLayoutName(self: Self) void { @@ -264,52 +320,6 @@ pub fn sendLayoutNameClear(self: Self) void { while (it) |node| : (it = node.next) node.data.sendLayoutNameClear(); } -pub fn arrangeFilter(view: *View, filter_tags: u32) bool { - return view.tree.node.enabled and !view.pending.float and !view.pending.fullscreen and - view.pending.tags & filter_tags != 0; -} - -/// Start a layout demand with the currently active (pending) layout. -/// Note that this function does /not/ decide which layout shall be active. That -/// is done in two places: 1) When the user changed the layout namespace option -/// of this output and 2) when a new layout is added. -/// -/// If no layout is active, all views will simply retain their current -/// dimensions. So without any active layouts, river will function like a simple -/// floating WM. -/// -/// The changes of view dimensions are async. Therefore all transactions are -/// blocked until the layout demand has either finished or was aborted. Both -/// cases will start a transaction. -pub fn arrangeViews(self: *Self) void { - if (self == &server.root.noop_output) return; - - // If there is already an active layout demand, discard it. - if (self.layout_demand) |demand| { - demand.deinit(); - self.layout_demand = null; - } - - // We only need to do something if there is an active layout. - if (self.pending.layout) |layout| { - // If the usable area has a zero dimension, trying to arrange the layout - // would cause an underflow and is pointless anyway. - if (self.usable_box.width == 0 or self.usable_box.height == 0) return; - - // How many views will be part of the layout? - var views: u32 = 0; - var view_it = ViewStack(View).iter(self.views.first, .forward, self.pending.tags, arrangeFilter); - while (view_it.next() != null) views += 1; - - // No need to arrange an empty output. - if (views == 0) return; - - // Note that this is async. A layout demand will start a transaction - // once its done. - layout.startLayoutDemand(views); - } -} - /// Arrange all layer surfaces of this output and adjust the usable area. /// Will arrange views as well if the usable area changes. pub fn arrangeLayers(self: *Self) void { @@ -340,45 +350,43 @@ pub fn arrangeLayers(self: *Self) void { } } - // If the the usable_box has changed, we need to rearrange the output - if (!std.meta.eql(self.usable_box, usable_box)) { - self.usable_box = usable_box; - self.arrangeViews(); - } + self.usable_box = usable_box; } fn handleDestroy(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void { - const self = @fieldParentPtr(Self, "destroy", listener); + const output = @fieldParentPtr(Self, "destroy", listener); - std.log.scoped(.server).debug("output '{s}' destroyed", .{self.wlr_output.name}); + std.log.scoped(.server).debug("output '{s}' destroyed", .{output.wlr_output.name}); // Remove the destroyed output from root if it wasn't already removed - server.root.removeOutput(self); - assert(self.views.first == null and self.views.last == null); - assert(self.layouts.len == 0); + server.root.removeOutput(output); + + assert(output.pending.focus_stack.empty()); + assert(output.pending.wm_stack.empty()); + assert(output.inflight.focus_stack.empty()); + assert(output.inflight.wm_stack.empty()); + assert(output.inflight.layout_demand == null); + assert(output.layouts.len == 0); var it = server.root.all_outputs.first; while (it) |all_node| : (it = all_node.next) { - if (all_node.data == self) { + if (all_node.data == output) { server.root.all_outputs.remove(all_node); break; } } - // Remove all listeners - self.destroy.link.remove(); - self.enable.link.remove(); - self.frame.link.remove(); - self.mode.link.remove(); - self.present.link.remove(); + output.destroy.link.remove(); + output.enable.link.remove(); + output.frame.link.remove(); + output.mode.link.remove(); + output.present.link.remove(); - // Free all memory and clean up the wlr.Output - if (self.layout_demand) |demand| demand.deinit(); - if (self.layout_namespace) |namespace| util.gpa.free(namespace); + if (output.layout_namespace) |namespace| util.gpa.free(namespace); - self.wlr_output.data = undefined; + output.wlr_output.data = 0; - const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self); + const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", output); util.gpa.destroy(node); } @@ -429,8 +437,7 @@ fn handleMode(listener: *wl.Listener(*wlr.Output), _: *wlr.Output) void { background_color_rect.setSize(width, height); } - self.arrangeLayers(); - server.root.startTransaction(); + server.root.applyPending(); } fn handlePresent( @@ -475,11 +482,10 @@ pub fn handleLayoutNamespaceChange(self: *Self) void { // The user changed the layout namespace of this output. Try to find a // matching layout. var it = self.layouts.first; - self.pending.layout = while (it) |node| : (it = node.next) { + self.layout = while (it) |node| : (it = node.next) { if (mem.eql(u8, self.layoutNamespace(), node.data.namespace)) break &node.data; } else null; - self.arrangeViews(); - server.root.startTransaction(); + server.root.applyPending(); } pub fn layoutNamespace(self: Self) []const u8 { diff --git a/river/OutputStatus.zig b/river/OutputStatus.zig index 8a22128..ab39458 100644 --- a/river/OutputStatus.zig +++ b/river/OutputStatus.zig @@ -25,7 +25,6 @@ const util = @import("util.zig"); const Output = @import("Output.zig"); const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; const log = std.log.scoped(.river_status); @@ -41,12 +40,7 @@ pub fn init(self: *Self, output: *Output, output_status: *zriver.OutputStatusV1) self.sendViewTags(); self.sendFocusedTags(output.current.tags); - var urgent_tags: u32 = 0; - var view_it = self.output.views.first; - while (view_it) |node| : (view_it = node.next) { - if (node.view.current.urgent) urgent_tags |= node.view.current.tags; - } - self.sendUrgentTags(urgent_tags); + output.sendUrgentTags(); if (output.layout_name) |name| { self.sendLayoutName(name); @@ -75,14 +69,15 @@ pub fn sendViewTags(self: Self) void { var view_tags = std.ArrayList(u32).init(util.gpa); defer view_tags.deinit(); - var it = self.output.views.first; - while (it) |node| : (it = node.next) { - if (!node.view.tree.node.enabled) continue; - view_tags.append(node.view.current.tags) catch { - self.output_status.postNoMemory(); - log.err("out of memory", .{}); - return; - }; + { + var it = self.output.inflight.wm_stack.iterator(.forward); + while (it.next()) |view| { + view_tags.append(view.current.tags) catch { + self.output_status.postNoMemory(); + log.err("out of memory", .{}); + return; + }; + } } var wl_array = wl.Array.fromArrayList(u32, view_tags); diff --git a/river/Root.zig b/river/Root.zig index 2e7d705..819397c 100644 --- a/river/Root.zig +++ b/river/Root.zig @@ -33,7 +33,6 @@ const LockSurface = @import("LockSurface.zig"); const Output = @import("Output.zig"); const SceneNodeData = @import("SceneNodeData.zig"); const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig"); scene: *wlr.Scene, @@ -53,6 +52,25 @@ layers: struct { xwayland_override_redirect: if (build_options.xwayland) *wlr.SceneTree else void, }, +/// This is kind of like an imaginary output where views start and end their life. +/// It is also used to store views and tags when no actual outputs are available. +hidden: struct { + /// This tree is always disabled. + tree: *wlr.SceneTree, + + tags: u32 = 1 << 0, + + pending: struct { + focus_stack: wl.list.Head(View, .pending_focus_stack_link), + wm_stack: wl.list.Head(View, .pending_wm_stack_link), + }, + + inflight: struct { + focus_stack: wl.list.Head(View, .inflight_focus_stack_link), + wm_stack: wl.list.Head(View, .inflight_wm_stack_link), + }, +}, + new_output: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleNewOutput), output_layout: *wlr.OutputLayout, @@ -74,17 +92,14 @@ all_outputs: std.TailQueue(*Output) = .{}, /// A list of all active outputs. See Output.active outputs: std.TailQueue(Output) = .{}, -/// This output is used internally when no real outputs are available. -/// It is not advertised to clients. -noop_output: Output = undefined, - -/// Number of layout demands pending before the transaction may be started. -pending_layout_demands: u32 = 0, -/// Number of pending configures sent in the current transaction. -/// A value of 0 means there is no current transaction. -pending_configures: u32 = 0, -/// Handles timeout of transactions -transaction_timer: *wl.EventSource, +/// Number of layout demands before sending configures to clients. +inflight_layout_demands: u32 = 0, +/// Number of inflight configures sent in the current transaction. +inflight_configures: u32 = 0, +transaction_timeout: *wl.EventSource, +/// Set to true if applyPending() is called while a transaction is inflight. +/// If true when a transaction completes will cause applyPending() to be called again. +pending_state_dirty: bool = false, pub fn init(self: *Self) !void { const output_layout = try wlr.OutputLayout.create(); @@ -95,6 +110,8 @@ pub fn init(self: *Self) !void { const interactive_content = try scene.tree.createSceneTree(); const drag_icons = try scene.tree.createSceneTree(); + const hidden_tree = try scene.tree.createSceneTree(); + hidden_tree.node.setEnabled(false); const outputs = try interactive_content.createSceneTree(); const xwayland_override_redirect = if (build_options.xwayland) try interactive_content.createSceneTree(); @@ -104,13 +121,8 @@ pub fn init(self: *Self) !void { _ = try wlr.XdgOutputManagerV1.create(server.wl_server, output_layout); const event_loop = server.wl_server.getEventLoop(); - const transaction_timer = try event_loop.addTimer(*Self, handleTransactionTimeout, self); - errdefer transaction_timer.remove(); - - // TODO get rid of this hack somehow - const noop_wlr_output = try server.headless_backend.headlessAddOutput(1920, 1080); - const noop_tree = try outputs.createSceneTree(); - noop_tree.node.setEnabled(false); + const transaction_timeout = try event_loop.addTimer(*Self, handleTransactionTimeout, self); + errdefer transaction_timeout.remove(); self.* = .{ .scene = scene, @@ -120,33 +132,26 @@ pub fn init(self: *Self) !void { .outputs = outputs, .xwayland_override_redirect = xwayland_override_redirect, }, + .hidden = .{ + .tree = hidden_tree, + .pending = .{ + .focus_stack = undefined, + .wm_stack = undefined, + }, + .inflight = .{ + .focus_stack = undefined, + .wm_stack = undefined, + }, + }, .output_layout = output_layout, .output_manager = try wlr.OutputManagerV1.create(server.wl_server), .power_manager = try wlr.OutputPowerManagerV1.create(server.wl_server), - .transaction_timer = transaction_timer, - .noop_output = .{ - .wlr_output = noop_wlr_output, - .tree = noop_tree, - .normal_content = try noop_tree.createSceneTree(), - .locked_content = try noop_tree.createSceneTree(), - .layers = .{ - .background_color_rect = try noop_tree.createSceneRect( - 0, - 0, - &server.config.background_color, - ), - .background = try noop_tree.createSceneTree(), - .bottom = try noop_tree.createSceneTree(), - .views = try noop_tree.createSceneTree(), - .top = try noop_tree.createSceneTree(), - .fullscreen = try noop_tree.createSceneTree(), - .overlay = try noop_tree.createSceneTree(), - .popups = try noop_tree.createSceneTree(), - }, - .usable_box = .{ .x = 0, .y = 0, .width = 0, .height = 0 }, - }, + .transaction_timeout = transaction_timeout, }; - noop_wlr_output.data = @ptrToInt(&self.noop_output); + self.hidden.pending.focus_stack.init(); + self.hidden.pending.wm_stack.init(); + self.hidden.inflight.focus_stack.init(); + self.hidden.inflight.wm_stack.init(); server.backend.events.new_output.add(&self.new_output); self.output_manager.events.apply.add(&self.manager_apply); @@ -158,7 +163,7 @@ pub fn init(self: *Self) !void { pub fn deinit(self: *Self) void { self.scene.tree.node.destroy(); self.output_layout.destroy(); - self.transaction_timer.remove(); + self.transaction_timeout.remove(); } pub const AtResult = struct { @@ -190,28 +195,23 @@ pub fn at(self: Self, lx: f64, ly: f64) ?AtResult { break :blk null; }; - { - var it: ?*wlr.SceneNode = node_at; - while (it) |node| : (it = node.parent) { - if (@intToPtr(?*SceneNodeData, node.data)) |scene_node_data| { - return .{ - .surface = surface, - .sx = sx, - .sy = sy, - .node = switch (scene_node_data.data) { - .view => |view| .{ .view = view }, - .layer_surface => |layer_surface| .{ .layer_surface = layer_surface }, - .lock_surface => |lock_surface| .{ .lock_surface = lock_surface }, - .xwayland_override_redirect => |xwayland_override_redirect| .{ - .xwayland_override_redirect = xwayland_override_redirect, - }, - }, - }; - } - } + if (SceneNodeData.get(node_at)) |scene_node_data| { + return .{ + .surface = surface, + .sx = sx, + .sy = sy, + .node = switch (scene_node_data.data) { + .view => |view| .{ .view = view }, + .layer_surface => |layer_surface| .{ .layer_surface = layer_surface }, + .lock_surface => |lock_surface| .{ .lock_surface = lock_surface }, + .xwayland_override_redirect => |xwayland_override_redirect| .{ + .xwayland_override_redirect = xwayland_override_redirect, + }, + }, + }; + } else { + return null; } - - return null; } fn handleNewOutput(_: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void { @@ -230,34 +230,50 @@ fn handleNewOutput(_: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) void { /// Remove the output from self.outputs and evacuate views if it is a member of /// the list. The node is not freed -pub fn removeOutput(self: *Self, output: *Output) void { - const node = @fieldParentPtr(std.TailQueue(Output).Node, "data", output); +pub fn removeOutput(root: *Self, output: *Output) void { + { + const node = @fieldParentPtr(std.TailQueue(Output).Node, "data", output); - // If the node has already been removed, do nothing - var output_it = self.outputs.first; - while (output_it) |n| : (output_it = n.next) { - if (n == node) break; - } else return; + // If the node has already been removed, do nothing + var output_it = root.outputs.first; + while (output_it) |n| : (output_it = n.next) { + if (n == node) break; + } else return; - self.outputs.remove(node); + root.outputs.remove(node); + } - // Use the first output in the list as fallback. If the last real output - // is being removed, use the noop output. - const fallback_output = blk: { - if (self.outputs.first) |output_node| { - break :blk &output_node.data; - } else { - // Store the focused output tags if we are hotplugged down to - // 0 real outputs so they can be restored on gaining a new output. - self.noop_output.current.tags = output.current.tags; - break :blk &self.noop_output; - } - }; + if (output.inflight.layout_demand) |layout_demand| { + layout_demand.deinit(); + output.inflight.layout_demand = null; + } + while (output.layouts.first) |node| node.data.destroy(); - // Move all views from the destroyed output to the fallback one - while (output.views.last) |view_node| { - const view = &view_node.view; - view.sendToOutput(fallback_output); + { + var it = output.inflight.focus_stack.iterator(.forward); + while (it.next()) |view| { + view.inflight.output = null; + view.current.output = null; + view.tree.node.reparent(root.hidden.tree); + view.popup_tree.node.reparent(root.hidden.tree); + } + root.hidden.inflight.focus_stack.prependList(&output.inflight.focus_stack); + root.hidden.inflight.wm_stack.prependList(&output.inflight.wm_stack); + } + // Use the first output in the list as fallback. If the last real output + // is being removed store the views in Root.hidden. + const fallback_output = if (root.outputs.first) |node| &node.data else null; + if (fallback_output) |fallback| { + var it = output.pending.focus_stack.safeIterator(.reverse); + while (it.next()) |view| view.setPendingOutput(fallback); + } else { + var it = output.pending.focus_stack.iterator(.forward); + while (it.next()) |view| view.pending.output = null; + root.hidden.pending.focus_stack.prependList(&output.pending.focus_stack); + root.hidden.pending.wm_stack.prependList(&output.pending.wm_stack); + // Store the focused output tags if we are hotplugged down to + // 0 real outputs so they can be restored on gaining a new output. + root.hidden.tags = output.pending.tags; } // Close all layer surfaces on the removed output @@ -282,211 +298,320 @@ pub fn removeOutput(self: *Self, output: *Output) void { } } - // Destroy all layouts of the output - while (output.layouts.first) |layout_node| layout_node.data.destroy(); - while (output.status_trackers.first) |status_node| status_node.data.destroy(); - // Arrange the root in case evacuated views affect the layout - fallback_output.arrangeViews(); - self.startTransaction(); + root.applyPending(); } /// Add the output to self.outputs and the output layout if it has not /// already been added. -pub fn addOutput(self: *Self, output: *Output) void { +pub fn addOutput(root: *Self, output: *Output) void { const node = @fieldParentPtr(std.TailQueue(Output).Node, "data", output); // If we have already added the output, do nothing and return - var output_it = self.outputs.first; + var output_it = root.outputs.first; while (output_it) |n| : (output_it = n.next) if (n == node) return; - self.outputs.append(node); + root.outputs.append(node); - // This aarranges outputs from left-to-right in the order they appear. The + // This arranges outputs from left-to-right in the order they appear. The // wlr-output-management protocol may be used to modify this arrangement. // This also creates a wl_output global which is advertised to clients. - self.output_layout.addAuto(output.wlr_output); + root.output_layout.addAuto(output.wlr_output); - const layout_output = self.output_layout.get(output.wlr_output).?; + const layout_output = root.output_layout.get(output.wlr_output).?; output.tree.node.setEnabled(true); output.tree.node.setPosition(layout_output.x, layout_output.y); - // If we previously had no real outputs, move focus from the noop output - // to the new one. - if (self.outputs.len == 1) { - // Restore the focused tags of the last output to be removed - output.pending.tags = self.noop_output.current.tags; - output.current.tags = self.noop_output.current.tags; + // If we previously had no outputs move all views to the new output and focus it. + if (root.outputs.len == 1) { + output.pending.tags = root.hidden.tags; + { + var it = root.hidden.pending.focus_stack.safeIterator(.reverse); + while (it.next()) |view| view.setPendingOutput(output); + assert(root.hidden.pending.focus_stack.empty()); + assert(root.hidden.pending.wm_stack.empty()); + assert(root.hidden.inflight.focus_stack.empty()); + assert(root.hidden.inflight.wm_stack.empty()); + } + { + // Focus the new output with all seats + var it = server.input_manager.seats.first; + while (it) |seat_node| : (it = seat_node.next) { + const seat = &seat_node.data; + seat.focusOutput(output); + seat.focus(null); + } + } + root.applyPending(); + } +} - // Move all views from noop output to the new output - while (self.noop_output.views.last) |n| n.view.sendToOutput(output); +/// Trigger asynchronous application of pending state for all outputs and views. +/// Changes will not be applied to the scene graph until the layout generator +/// generates a new layout for all outputs and all affected clients ack a +/// configure and commit a new buffer. +pub fn applyPending(root: *Self) void { + // If there is already a transaction inflight, wait until it completes. + if (root.inflight_layout_demands > 0 or root.inflight_configures > 0) { + root.pending_state_dirty = true; + return; + } + root.pending_state_dirty = false; - // Focus the new output with all seats - var it = server.input_manager.seats.first; - while (it) |seat_node| : (it = seat_node.next) { - const seat = &seat_node.data; - seat.focusOutput(output); - seat.focus(null); + { + var it = root.hidden.pending.focus_stack.iterator(.forward); + while (it.next()) |view| { + assert(view.pending.output == null); + view.inflight.output = null; + view.inflight_focus_stack_link.remove(); + root.hidden.inflight.focus_stack.append(view); } } -} -/// Arrange all views on all outputs -pub fn arrangeAll(self: *Self) void { - var it = self.outputs.first; - while (it) |node| : (it = node.next) node.data.arrangeViews(); -} + { + var it = root.hidden.pending.wm_stack.iterator(.forward); + while (it.next()) |view| { + view.inflight_wm_stack_link.remove(); + root.hidden.inflight.wm_stack.append(view); + } + } -/// Record the number of currently pending layout demands so that a transaction -/// can be started once all are either complete or have timed out. -pub fn trackLayoutDemands(self: *Self) void { - self.pending_layout_demands = 0; + var output_it = root.outputs.first; + while (output_it) |node| : (output_it = node.next) { + const output = &node.data; - var it = self.outputs.first; - while (it) |node| : (it = node.next) { - if (node.data.layout_demand != null) self.pending_layout_demands += 1; + if (output.inflight.fullscreen) |view| { + if (!view.pending.fullscreen or view.pending.tags & output.pending.tags == 0) { + output.inflight.fullscreen = null; + + view.setFullscreen(false); + view.pending.box = view.post_fullscreen_box; + } + } + + // Iterate the focus stack in order to ensure the currently focused/most + // recently focused view that requests fullscreen is given fullscreen. + { + var it = output.pending.focus_stack.iterator(.forward); + while (it.next()) |view| { + assert(view.pending.output == output); + + if (view.current.float and !view.pending.float) { + // If switching from float to non-float, save the dimensions. + view.float_box = view.current.box; + } else if (!view.current.float and view.pending.float) { + // If switching from non-float to float, apply the saved float dimensions. + view.pending.box = view.float_box; + } + + if (output.inflight.fullscreen == null) { + if (view.pending.fullscreen and view.pending.tags & output.pending.tags != 0) { + output.inflight.fullscreen = view; + + view.setFullscreen(true); + view.post_fullscreen_box = view.pending.box; + view.pending.box = .{ + .x = 0, + .y = 0, + .width = undefined, + .height = undefined, + }; + output.wlr_output.effectiveResolution( + &view.pending.box.width, + &view.pending.box.height, + ); + } + } + + view.inflight_focus_stack_link.remove(); + output.inflight.focus_stack.append(view); + + view.inflight = view.pending; + } + } + + { + var it = output.pending.wm_stack.iterator(.forward); + while (it.next()) |view| { + view.inflight_wm_stack_link.remove(); + output.inflight.wm_stack.append(view); + } + } + + output.inflight.tags = output.pending.tags; + + assert(output.inflight.layout_demand == null); + if (output.layout) |layout| { + var layout_count: u32 = 0; + { + var it = output.inflight.wm_stack.iterator(.forward); + while (it.next()) |view| { + if (!view.inflight.float and !view.inflight.fullscreen and + view.inflight.tags & output.inflight.tags != 0) + { + layout_count += 1; + } + } + } + + if (layout_count > 0) { + // TODO don't do this if the count has not changed + layout.startLayoutDemand(layout_count); + } + } + } + + if (root.inflight_layout_demands == 0) { + root.sendConfigures(); } - assert(self.pending_layout_demands > 0); } /// This function is used to inform the transaction system that a layout demand /// has either been completed or timed out. If it was the last pending layout /// demand in the current sequence, a transaction is started. -pub fn notifyLayoutDemandDone(self: *Self) void { - self.pending_layout_demands -= 1; - if (self.pending_layout_demands == 0) self.startTransaction(); +pub fn notifyLayoutDemandDone(root: *Self) void { + root.inflight_layout_demands -= 1; + if (root.inflight_layout_demands == 0) { + root.sendConfigures(); + } } -/// Initiate an atomic change to the layout. This change will not be -/// applied until all affected clients ack a configure and commit a buffer. -pub fn startTransaction(self: *Self) void { - // If one or more layout demands are currently in progress, postpone - // transactions until they complete. Every frame must be perfect. - if (self.pending_layout_demands > 0) return; - - // If a new transaction is started while another is in progress, we need - // to reset the pending count to 0 and clear serials from the views - const preempting = self.pending_configures > 0; - self.pending_configures = 0; +fn sendConfigures(root: *Self) void { + assert(root.inflight_layout_demands == 0); + assert(root.inflight_configures == 0); // Iterate over all views of all outputs - var output_it = self.outputs.first; + var output_it = root.outputs.first; while (output_it) |output_node| : (output_it = output_node.next) { - var view_it = output_node.data.views.first; - while (view_it) |view_node| : (view_it = view_node.next) { - const view = &view_node.view; - - if (!view.tree.node.enabled) continue; - - if (view.shouldTrackConfigure()) { - // Clear the serial in case this transaction is interrupting a prior one. - view.pending_serial = null; + const output = &output_node.data; - if (view.needsConfigure()) { - view.configure(); - self.pending_configures += 1; + var focus_stack_it = output.inflight.focus_stack.iterator(.forward); + while (focus_stack_it.next()) |view| { + if (view.needsConfigure()) { + view.configure(); - // Send a frame done that the client will commit a new frame - // with the dimensions we sent in the configure. Normally this - // event would be sent in the render function. + // We don't give a damn about frame perfection for xwayland views + if (!build_options.xwayland or view.impl != .xwayland_view) { + root.inflight_configures += 1; + view.saveSurfaceTree(); view.sendFrameDone(); } - - // If the saved surface tree is enabled, then this transaction is interrupting - // a previous transaction and we should keep the old surface tree. - if (!view.saved_surface_tree.node.enabled) view.saveSurfaceTree(); - } else { - if (view.needsConfigure()) view.configure(); } } } - if (self.pending_configures > 0) { + if (root.inflight_configures > 0) { std.log.scoped(.transaction).debug("started transaction with {} pending configure(s)", .{ - self.pending_configures, + root.inflight_configures, }); - // Timeout the transaction after 200ms. If we are preempting an - // already in progress transaction, don't extend the timeout. - if (!preempting) { - self.transaction_timer.timerUpdate(200) catch { - std.log.scoped(.transaction).err("failed to update timer", .{}); - self.commitTransaction(); - }; - } + root.transaction_timeout.timerUpdate(200) catch { + std.log.scoped(.transaction).err("failed to update timer", .{}); + root.commitTransaction(); + }; } else { - // No views need configures, clear the current timer in case we are - // interrupting another transaction and commit. - self.transaction_timer.timerUpdate(0) catch std.log.scoped(.transaction).err("error disarming timer", .{}); - self.commitTransaction(); + root.commitTransaction(); } } fn handleTransactionTimeout(self: *Self) c_int { + assert(self.inflight_layout_demands == 0); + std.log.scoped(.transaction).err("timeout occurred, some imperfect frames may be shown", .{}); - self.pending_configures = 0; + self.inflight_configures = 0; self.commitTransaction(); return 0; } pub fn notifyConfigured(self: *Self) void { - self.pending_configures -= 1; - if (self.pending_configures == 0) { + assert(self.inflight_layout_demands == 0); + + self.inflight_configures -= 1; + if (self.inflight_configures == 0) { // Disarm the timer, as we didn't timeout - self.transaction_timer.timerUpdate(0) catch std.log.scoped(.transaction).err("error disarming timer", .{}); + self.transaction_timeout.timerUpdate(0) catch std.log.scoped(.transaction).err("error disarming timer", .{}); self.commitTransaction(); } } -/// Apply the pending state and drop stashed buffers. This means that +/// Apply the inflight state and drop stashed buffers. This means that /// the next frame drawn will be the post-transaction state of the /// layout. Should only be called after all clients have configured for /// the new layout. If called early imperfect frames may be drawn. -fn commitTransaction(self: *Self) void { - assert(self.pending_configures == 0); +fn commitTransaction(root: *Self) void { + assert(root.inflight_layout_demands == 0); + assert(root.inflight_configures == 0); - // Iterate over all views of all outputs - var output_it = self.outputs.first; + { + var it = root.hidden.inflight.focus_stack.safeIterator(.forward); + while (it.next()) |view| { + assert(view.inflight.output == null); + view.current.output = null; + + view.tree.node.reparent(root.hidden.tree); + view.popup_tree.node.reparent(root.hidden.tree); + + view.updateCurrent(); + } + } + + var output_it = root.outputs.first; while (output_it) |output_node| : (output_it = output_node.next) { const output = &output_node.data; - // Apply pending state of the output - if (output.pending.tags != output.current.tags) { + if (output.inflight.tags != output.current.tags) { std.log.scoped(.output).debug( "changing current focus: {b:0>10} to {b:0>10}", - .{ output.current.tags, output.pending.tags }, + .{ output.current.tags, output.inflight.tags }, ); + + output.current.tags = output.pending.tags; + var it = output.status_trackers.first; - while (it) |node| : (it = node.next) node.data.sendFocusedTags(output.pending.tags); + while (it) |node| : (it = node.next) node.data.sendFocusedTags(output.current.tags); + } + + if (output.inflight.fullscreen != output.current.fullscreen) { + if (output.current.fullscreen) |view| { + if (view.inflight.output) |new_output| { + view.tree.node.reparent(new_output.layers.views); + } else { + view.tree.node.reparent(root.hidden.tree); + } + } + if (output.inflight.fullscreen) |view| { + assert(view.inflight.output == output); + view.tree.node.reparent(output.layers.fullscreen); + } + output.current.fullscreen = output.inflight.fullscreen; + output.layers.fullscreen.node.setEnabled(output.current.fullscreen != null); } - output.current = output.pending; var view_tags_changed = false; var urgent_tags_dirty = false; - var view_it = output.views.first; - while (view_it) |view_node| { - const view = &view_node.view; - view_it = view_node.next; + var focus_stack_it = output.inflight.focus_stack.iterator(.forward); + while (focus_stack_it.next()) |view| { + assert(view.inflight.output == output); - if (!view.tree.node.enabled) { - view.dropSavedSurfaceTree(); - view.output.views.remove(view_node); - if (view.destroying) view.destroy(); - continue; - } - assert(!view.destroying); + view.inflight_serial = null; - if (view.pending_serial != null and !view.shouldTrackConfigure()) continue; + if (view.inflight.tags != view.current.tags) view_tags_changed = true; + if (view.inflight.urgent != view.current.urgent) urgent_tags_dirty = true; + if (view.inflight.urgent and view_tags_changed) urgent_tags_dirty = true; - // Apply pending state of the view - view.pending_serial = null; - if (view.pending.tags != view.current.tags) view_tags_changed = true; - if (view.pending.urgent != view.current.urgent) urgent_tags_dirty = true; - if (view.pending.urgent and view_tags_changed) urgent_tags_dirty = true; + if (view.current.output != output) { + view.tree.node.reparent(output.layers.views); + view.popup_tree.node.reparent(output.layers.popups); + } + const enabled = view.current.tags & output.current.tags != 0; + view.tree.node.setEnabled(enabled); + view.popup_tree.node.setEnabled(enabled); + // TODO this approach for syncing the order will likely cause over-damaging. + view.tree.node.lowerToBottom(); view.updateCurrent(); } @@ -494,8 +619,25 @@ fn commitTransaction(self: *Self) void { if (view_tags_changed) output.sendViewTags(); if (urgent_tags_dirty) output.sendUrgentTags(); } - server.input_manager.updateCursorState(); + + { + var it = server.input_manager.seats.first; + while (it) |node| : (it = node.next) node.data.cursor.updateState(); + } + + { + // This must be done after updating cursor state in case the view was the target of move/resize. + var it = root.hidden.inflight.focus_stack.safeIterator(.forward); + while (it.next()) |view| { + if (view.destroying) view.destroy(); + } + } + server.idle_inhibitor_manager.idleInhibitCheckActive(); + + if (root.pending_state_dirty) { + root.applyPending(); + } } /// Send the new output configuration to all wlr-output-manager clients @@ -582,7 +724,7 @@ fn processOutputConfig( } } - if (action == .apply) self.startTransaction(); + if (action == .apply) self.applyPending(); if (success) { config.sendSucceeded(); diff --git a/river/SceneNodeData.zig b/river/SceneNodeData.zig index d855ca5..f79bc59 100644 --- a/river/SceneNodeData.zig +++ b/river/SceneNodeData.zig @@ -50,6 +50,16 @@ pub fn attach(node: *wlr.SceneNode, data: Data) error{OutOfMemory}!void { node.events.destroy.add(&scene_node_data.destroy); } +pub fn get(node: *wlr.SceneNode) ?*SceneNodeData { + var it: ?*wlr.SceneNode = node; + while (it) |n| : (it = n.parent) { + if (@intToPtr(?*SceneNodeData, n.data)) |scene_node_data| { + return scene_node_data; + } + } + return null; +} + fn handleDestroy(listener: *wl.Listener(void)) void { const scene_node_data = @fieldParentPtr(SceneNodeData, "destroy", listener); diff --git a/river/Seat.zig b/river/Seat.zig index 3dd6883..82b9cd8 100644 --- a/river/Seat.zig +++ b/river/Seat.zig @@ -41,7 +41,6 @@ const Output = @import("Output.zig"); const SeatStatus = @import("SeatStatus.zig"); const Switch = @import("Switch.zig"); const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig"); const log = std.log.scoped(.seat); @@ -73,16 +72,11 @@ repeating_mapping: ?*const Mapping = null, keyboard_groups: std.TailQueue(KeyboardGroup) = .{}, -/// Currently focused output, may be the noop output if no real output -/// is currently available for focus. -focused_output: *Output, +/// Currently focused output. Null only when there are no outputs at all. +focused_output: ?*Output = null, focused: FocusTarget = .none, -/// Stack of views in most recently focused order -/// If there is a currently focused view, it is on top. -focus_stack: ViewStack(*View) = .{}, - /// List of status tracking objects relaying changes to this seat to clients. status_trackers: std.SinglyLinkedList(SeatStatus) = .{}, @@ -110,7 +104,6 @@ pub fn init(self: *Self, name: [*:0]const u8) !void { self.* = .{ // This will be automatically destroyed when the display is destroyed .wlr_seat = try wlr.Seat.create(server.wl_server, name), - .focused_output = &server.root.noop_output, .mapping_repeat_timer = mapping_repeat_timer, }; self.wlr_seat.data = @ptrToInt(self); @@ -136,11 +129,6 @@ pub fn deinit(self: *Self) void { node.data.destroy(); } - while (self.focus_stack.first) |node| { - self.focus_stack.remove(node); - util.gpa.destroy(node); - } - self.request_set_selection.link.remove(); self.request_start_drag.link.remove(); self.start_drag.link.remove(); @@ -149,14 +137,17 @@ pub fn deinit(self: *Self) void { } /// Set the current focus. If a visible view is passed it will be focused. -/// If null is passed, the first visible view in the focus stack will be focused. +/// If null is passed, the top view in the stack of the focused output will be focused. pub fn focus(self: *Self, _target: ?*View) void { var target = _target; - // Views may not recieve focus while locked. + // Don't change focus if there are no outputs. + if (self.focused_output == null) return; + + // Views may not receive focus while locked. if (server.lock_manager.state != .unlocked) return; - // While a layer surface is exclusively focused, views may not recieve focus + // While a layer surface is exclusively focused, views may not receive focus if (self.focused == .layer) { const wlr_layer_surface = self.focused.layer.scene_layer_surface.layer_surface; if (wlr_layer_surface.current.keyboard_interactive == .exclusive and @@ -167,58 +158,47 @@ pub fn focus(self: *Self, _target: ?*View) void { } if (target) |view| { - // If the view is not currently visible, behave as if null was passed - if (view.pending.tags & view.output.pending.tags == 0) { + if (view.pending.tags & view.pending.output.?.pending.tags == 0) { + // If the view is not currently visible, behave as if null was passed target = null; - } else { + } else if (view.pending.output.? != self.focused_output.?) { // If the view is not on the currently focused output, focus it - if (view.output != self.focused_output) self.focusOutput(view.output); + self.focusOutput(view.pending.output.?); } } - // If the target view is not fullscreen or null, then a fullscreen view - // will grab focus if visible. - if (if (target) |v| !v.pending.fullscreen else true) { - const tags = self.focused_output.pending.tags; - var it = ViewStack(*View).iter(self.focus_stack.first, .forward, tags, pendingFilter); - target = while (it.next()) |view| { - if (view.output == self.focused_output and view.pending.fullscreen) break view; - } else target; + { + var it = self.focused_output.?.pending.focus_stack.iterator(.forward); + while (it.next()) |view| { + if (view.pending.fullscreen and + view.pending.tags & self.focused_output.?.pending.tags != 0) + { + target = view; + break; + } + } } + // If null, set the target to the first currently visible view in the focus stack if any if (target == null) { - // Set view to the first currently visible view in the focus stack if any - const tags = self.focused_output.pending.tags; - var it = ViewStack(*View).iter(self.focus_stack.first, .forward, tags, pendingFilter); + var it = self.focused_output.?.pending.focus_stack.iterator(.forward); target = while (it.next()) |view| { - if (view.output == self.focused_output) break view; + if (view.pending.tags & self.focused_output.?.pending.tags != 0) { + break view; + } } else null; } // Focus the target view or clear the focus if target is null if (target) |view| { - // Find the node for this view in the focus stack and move it to the top. - var it = self.focus_stack.first; - while (it) |node| : (it = node.next) { - if (node.view == view) { - self.focus_stack.remove(node); - self.focus_stack.push(node); - break; - } - } else { - // A node is added when new Views are mapped in Seat.handleViewMap() - unreachable; - } + view.pending_focus_stack_link.remove(); + self.focused_output.?.pending.focus_stack.prepend(view); self.setFocusRaw(.{ .view = view }); } else { self.setFocusRaw(.{ .none = {} }); } } -fn pendingFilter(view: *View, filter_tags: u32) bool { - return view.tree.node.enabled and view.pending.tags & filter_tags != 0; -} - /// Switch focus to the target, handling unfocus and input inhibition /// properly. This should only be called directly if dealing with layers or /// override redirect xwayland views. @@ -252,7 +232,7 @@ pub fn setFocusRaw(self: *Self, new_focus: FocusTarget) void { switch (new_focus) { .view => |target_view| { assert(server.lock_manager.state != .locked); - assert(self.focused_output == target_view.output); + assert(self.focused_output == target_view.pending.output); if (target_view.pending.focus == 0) target_view.setActivated(true); target_view.pending.focus += 1; target_view.pending.urgent = false; @@ -314,48 +294,26 @@ fn keyboardNotifyEnter(self: *Self, wlr_surface: *wlr.Surface) void { } /// Focus the given output, notifying any listening clients of the change. -pub fn focusOutput(self: *Self, output: *Output) void { +pub fn focusOutput(self: *Self, output: ?*Output) void { if (self.focused_output == output) return; - var it = self.status_trackers.first; - while (it) |node| : (it = node.next) node.data.sendOutput(.unfocused); + if (self.focused_output) |old| { + var it = self.status_trackers.first; + while (it) |node| : (it = node.next) node.data.sendOutput(old, .unfocused); + } self.focused_output = output; - it = self.status_trackers.first; - while (it) |node| : (it = node.next) node.data.sendOutput(.focused); + if (self.focused_output) |new| { + var it = self.status_trackers.first; + while (it) |node| : (it = node.next) node.data.sendOutput(new, .focused); + } } pub fn handleActivity(self: Self) void { server.input_manager.idle_notifier.notifyActivity(self.wlr_seat); } -pub fn handleViewMap(self: *Self, view: *View) !void { - const new_focus_node = try util.gpa.create(ViewStack(*View).Node); - new_focus_node.view = view; - self.focus_stack.append(new_focus_node); - self.focus(view); -} - -/// Handle the unmapping of a view, removing it from the focus stack and -/// setting the focus if needed. -pub fn handleViewUnmap(self: *Self, view: *View) void { - // Remove the node from the focus stack and destroy it. - var it = self.focus_stack.first; - while (it) |node| : (it = node.next) { - if (node.view == view) { - self.focus_stack.remove(node); - util.gpa.destroy(node); - break; - } - } - - self.cursor.handleViewUnmap(view); - - // If the unmapped view is focused, choose a new focus - if (self.focused == .view and self.focused.view == view) self.focus(null); -} - pub fn enterMode(self: *Self, mode_id: u32) void { self.mode_id = mode_id; diff --git a/river/SeatStatus.zig b/river/SeatStatus.zig index f6fe5dd..59c4531 100644 --- a/river/SeatStatus.zig +++ b/river/SeatStatus.zig @@ -38,7 +38,7 @@ pub fn init(self: *Self, seat: *Seat, seat_status: *zriver.SeatStatusV1) void { // Send all info once on bind self.sendMode(server.config.modes.items[seat.mode_id].name); - self.sendOutput(.focused); + if (seat.focused_output) |output| self.sendOutput(output, .focused); self.sendFocusedView(); } @@ -54,9 +54,9 @@ fn handleDestroy(_: *zriver.SeatStatusV1, self: *Self) void { util.gpa.destroy(node); } -pub fn sendOutput(self: Self, state: enum { focused, unfocused }) void { +pub fn sendOutput(self: Self, output: *Output, state: enum { focused, unfocused }) void { const client = self.seat_status.getClient(); - var it = self.seat.focused_output.wlr_output.resources.iterator(.forward); + var it = output.wlr_output.resources.iterator(.forward); while (it.next()) |wl_output| { if (wl_output.getClient() == client) switch (state) { .focused => self.seat_status.sendFocusedOutput(wl_output), diff --git a/river/Server.zig b/river/Server.zig index 44706cc..291567e 100644 --- a/river/Server.zig +++ b/river/Server.zig @@ -180,9 +180,7 @@ fn terminate(_: c_int, wl_server: *wl.Server) c_int { return 0; } -fn handleNewXdgSurface(listener: *wl.Listener(*wlr.XdgSurface), xdg_surface: *wlr.XdgSurface) void { - const self = @fieldParentPtr(Self, "new_xdg_surface", listener); - +fn handleNewXdgSurface(_: *wl.Listener(*wlr.XdgSurface), xdg_surface: *wlr.XdgSurface) void { if (xdg_surface.role == .popup) { log.debug("new xdg_popup", .{}); return; @@ -190,8 +188,7 @@ fn handleNewXdgSurface(listener: *wl.Listener(*wlr.XdgSurface), xdg_surface: *wl log.debug("new xdg_toplevel", .{}); - const output = self.input_manager.defaultSeat().focused_output; - XdgToplevel.create(output, xdg_surface.role_data.toplevel) catch { + XdgToplevel.create(xdg_surface.role_data.toplevel) catch { log.err("out of memory", .{}); xdg_surface.resource.postNoMemory(); return; @@ -220,12 +217,11 @@ fn handleNewLayerSurface(listener: *wl.Listener(*wlr.LayerSurfaceV1), wlr_layer_ // If the new layer surface does not have an output assigned to it, use the // first output or close the surface if none are available. if (wlr_layer_surface.output == null) { - const output = self.input_manager.defaultSeat().focused_output; - if (output == &self.root.noop_output) { + const output = self.input_manager.defaultSeat().focused_output orelse { log.err("no output available for layer surface '{s}'", .{wlr_layer_surface.namespace}); wlr_layer_surface.destroy(); return; - } + }; log.debug("new layer surface had null output, assigning it to output '{s}'", .{output.wlr_output.name}); wlr_layer_surface.output = output.wlr_output; @@ -237,9 +233,7 @@ fn handleNewLayerSurface(listener: *wl.Listener(*wlr.LayerSurfaceV1), wlr_layer_ }; } -fn handleNewXwaylandSurface(listener: *wl.Listener(*wlr.XwaylandSurface), xwayland_surface: *wlr.XwaylandSurface) void { - const self = @fieldParentPtr(Self, "new_xwayland_surface", listener); - +fn handleNewXwaylandSurface(_: *wl.Listener(*wlr.XwaylandSurface), xwayland_surface: *wlr.XwaylandSurface) void { log.debug( "new xwayland surface: title='{?s}', class='{?s}', override redirect={}", .{ xwayland_surface.title, xwayland_surface.class, xwayland_surface.override_redirect }, @@ -251,8 +245,7 @@ fn handleNewXwaylandSurface(listener: *wl.Listener(*wlr.XwaylandSurface), xwayla return; }; } else { - const output = self.input_manager.defaultSeat().focused_output; - _ = XwaylandView.create(output, xwayland_surface) catch { + _ = XwaylandView.create(xwayland_surface) catch { log.err("out of memory", .{}); return; }; diff --git a/river/View.zig b/river/View.zig index 8fb07cd..4182a04 100644 --- a/river/View.zig +++ b/river/View.zig @@ -30,7 +30,6 @@ const util = @import("util.zig"); const Output = @import("Output.zig"); const SceneNodeData = @import("SceneNodeData.zig"); const Seat = @import("Seat.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; const XdgToplevel = @import("XdgToplevel.zig"); const XwaylandView = @import("XwaylandView.zig"); @@ -49,11 +48,16 @@ const Impl = union(enum) { }; const State = struct { + /// The output the view is currently assigned to. + /// May be null if there are no outputs or for newly created views. + /// Must be set using setPendingOutput() + output: ?*Output = null, + /// The output-relative coordinates of the view and dimensions requested by river. - box: wlr.Box = wlr.Box{ .x = 0, .y = 0, .width = 0, .height = 0 }, + box: wlr.Box = .{ .x = 0, .y = 0, .width = 0, .height = 0 }, /// The tags of the view, as a bitmask - tags: u32, + tags: u32 = 0, /// Number of seats currently focusing the view focus: u32 = 0, @@ -66,9 +70,6 @@ const State = struct { /// The implementation of this view impl: Impl, -/// The output this view is currently associated with -output: *Output, - tree: *wlr.SceneTree, surface_tree: *wlr.SceneTree, saved_surface_tree: *wlr.SceneTree, @@ -84,12 +85,18 @@ popup_tree: *wlr.SceneTree, /// transaction completes. See View.destroy() destroying: bool = false, -/// The double-buffered state of the view -current: State, -pending: State, +pending: State = .{}, +pending_focus_stack_link: wl.list.Link, +pending_wm_stack_link: wl.list.Link, + +inflight: State = .{}, +inflight_focus_stack_link: wl.list.Link, +inflight_wm_stack_link: wl.list.Link, -/// The serial sent with the currently pending configure event -pending_serial: ?u32 = null, +current: State = .{}, + +/// The serial sent with the currently inflight configure event +inflight_serial: ?u32 = null, /// The floating dimensions the view, saved so that they can be restored if the /// view returns to floating mode. @@ -104,25 +111,18 @@ draw_borders: bool = true, request_activate: wl.Listener(*wlr.XdgActivationV1.event.RequestActivate) = wl.Listener(*wlr.XdgActivationV1.event.RequestActivate).init(handleRequestActivate), -pub fn create(output: *Output, impl: Impl) error{OutOfMemory}!*Self { - const node = try util.gpa.create(ViewStack(Self).Node); - errdefer util.gpa.destroy(node); - const self = &node.view; - - const initial_tags = blk: { - const tags = output.current.tags & server.config.spawn_tagmask; - break :blk if (tags != 0) tags else output.current.tags; - }; +pub fn create(impl: Impl) error{OutOfMemory}!*Self { + const view = try util.gpa.create(Self); + errdefer util.gpa.destroy(view); - const tree = try output.layers.views.createSceneTree(); + const tree = try server.root.hidden.tree.createSceneTree(); errdefer tree.node.destroy(); - const popup_tree = try output.layers.popups.createSceneTree(); + const popup_tree = try server.root.hidden.tree.createSceneTree(); errdefer popup_tree.node.destroy(); - self.* = .{ + view.* = .{ .impl = impl, - .output = output, .tree = tree, .surface_tree = try tree.createSceneTree(), .saved_surface_tree = try tree.createSceneTree(), @@ -133,105 +133,82 @@ pub fn create(output: *Output, impl: Impl) error{OutOfMemory}!*Self { .bottom = try tree.createSceneRect(0, 0, &server.config.border_color_unfocused), }, .popup_tree = popup_tree, - .current = .{ .tags = initial_tags }, - .pending = .{ .tags = initial_tags }, + + .pending_wm_stack_link = undefined, + .pending_focus_stack_link = undefined, + .inflight_wm_stack_link = undefined, + .inflight_focus_stack_link = undefined, }; - self.saved_surface_tree.node.setEnabled(false); + server.root.hidden.pending.focus_stack.prepend(view); + server.root.hidden.pending.wm_stack.prepend(view); + server.root.hidden.inflight.focus_stack.prepend(view); + server.root.hidden.inflight.wm_stack.prepend(view); + + view.tree.node.setEnabled(false); + view.popup_tree.node.setEnabled(false); + view.saved_surface_tree.node.setEnabled(false); - try SceneNodeData.attach(&self.tree.node, .{ .view = self }); - try SceneNodeData.attach(&self.popup_tree.node, .{ .view = self }); + try SceneNodeData.attach(&view.tree.node, .{ .view = view }); + try SceneNodeData.attach(&view.popup_tree.node, .{ .view = view }); - return self; + return view; } /// If saved buffers of the view are currently in use by a transaction, /// mark this view for destruction when the transaction completes. Otherwise /// destroy immediately. -pub fn destroy(self: *Self) void { - self.destroying = true; +pub fn destroy(view: *Self) void { + view.destroying = true; // If there are still saved buffers, then this view needs to be kept // around until the current transaction completes. This function will be // called again in Root.commitTransaction() - if (!self.saved_surface_tree.node.enabled) { - self.tree.node.destroy(); - self.popup_tree.node.destroy(); + if (!view.saved_surface_tree.node.enabled) { + view.tree.node.destroy(); + view.popup_tree.node.destroy(); - const node = @fieldParentPtr(ViewStack(Self).Node, "view", self); - util.gpa.destroy(node); - } -} + view.pending_focus_stack_link.remove(); + view.pending_wm_stack_link.remove(); + view.inflight_focus_stack_link.remove(); + view.inflight_wm_stack_link.remove(); -/// Handle changes to pending state and start a transaction to apply them -pub fn applyPending(self: *Self) void { - if (self.current.float and !self.pending.float) { - // If switching from float to non-float, save the dimensions. - self.float_box = self.current.box; - } else if (!self.current.float and self.pending.float) { - // If switching from non-float to float, apply the saved float dimensions. - self.pending.box = self.float_box; + util.gpa.destroy(view); } - - if (!self.lastSetFullscreenState() and self.pending.fullscreen) { - // If switching to fullscreen, set the dimensions to the full area of the output - self.setFullscreen(true); - self.post_fullscreen_box = self.current.box; - - self.pending.box = .{ - .x = 0, - .y = 0, - .width = undefined, - .height = undefined, - }; - self.output.wlr_output.effectiveResolution(&self.pending.box.width, &self.pending.box.height); - } else if (self.lastSetFullscreenState() and !self.pending.fullscreen) { - self.setFullscreen(false); - self.pending.box = self.post_fullscreen_box; - } - - // We always need to arrange the output, as there could already be a - // transaction in progress. If we were able to check against the state - // that was pending when that transaction was started, we could in some - // cases avoid the arrangeViews() call here, but we don't store that - // information and it's simpler to always arrange anyways. - self.output.arrangeViews(); - - server.root.startTransaction(); } -pub fn updateCurrent(self: *Self) void { +pub fn updateCurrent(view: *Self) void { const config = &server.config; - self.current = self.pending; - if (self.saved_surface_tree.node.enabled) self.dropSavedSurfaceTree(); + view.current = view.inflight; + view.dropSavedSurfaceTree(); const color = blk: { - if (self.current.urgent) break :blk &config.border_color_urgent; - if (self.current.focus != 0) break :blk &config.border_color_focused; + if (view.current.urgent) break :blk &config.border_color_urgent; + if (view.current.focus != 0) break :blk &config.border_color_focused; break :blk &config.border_color_unfocused; }; - const box = &self.current.box; - self.tree.node.setPosition(box.x, box.y); - self.popup_tree.node.setPosition(box.x, box.y); + const box = &view.current.box; + view.tree.node.setPosition(box.x, box.y); + view.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); - self.borders.left.setSize(border_width, box.height + 2 * border_width); - self.borders.left.setColor(color); + view.borders.left.node.setPosition(-border_width, -border_width); + view.borders.left.setSize(border_width, box.height + 2 * border_width); + view.borders.left.setColor(color); - self.borders.right.node.setPosition(box.width, -border_width); - self.borders.right.setSize(border_width, box.height + 2 * border_width); - self.borders.right.setColor(color); + view.borders.right.node.setPosition(box.width, -border_width); + view.borders.right.setSize(border_width, box.height + 2 * border_width); + view.borders.right.setColor(color); - self.borders.top.node.setPosition(0, -border_width); - self.borders.top.setSize(box.width, border_width); - self.borders.top.setColor(color); + view.borders.top.node.setPosition(0, -border_width); + view.borders.top.setSize(box.width, border_width); + view.borders.top.setColor(color); - self.borders.bottom.node.setPosition(0, box.height); - self.borders.bottom.setSize(box.width, border_width); - self.borders.bottom.setColor(color); + view.borders.bottom.node.setPosition(0, box.height); + view.borders.bottom.setSize(box.width, border_width); + view.borders.bottom.setColor(color); } pub fn needsConfigure(self: Self) bool { @@ -252,13 +229,6 @@ pub fn configure(self: *Self) void { } } -fn lastSetFullscreenState(self: Self) bool { - return switch (self.impl) { - .xdg_toplevel => |xdg_toplevel| xdg_toplevel.lastSetFullscreenState(), - .xwayland_view => |xwayland_view| xwayland_view.lastSetFullscreenState(), - }; -} - pub fn rootSurface(self: Self) *wlr.Surface { assert(!self.destroying); return switch (self.impl) { @@ -275,7 +245,7 @@ pub fn sendFrameDone(self: Self) void { } pub fn dropSavedSurfaceTree(self: *Self) void { - assert(self.saved_surface_tree.node.enabled); + if (!self.saved_surface_tree.node.enabled) return; var it = self.saved_surface_tree.children.safeIterator(.forward); while (it.next()) |node| node.destroy(); @@ -310,52 +280,28 @@ fn saveSurfaceTreeIter( saved.setTransform(buffer.transform); } -/// Move a view from one output to another, sending the required enter/leave -/// events. -pub fn sendToOutput(self: *Self, destination_output: *Output) void { - const node = @fieldParentPtr(ViewStack(Self).Node, "view", self); +pub fn setPendingOutput(view: *Self, output: *Output) void { + view.pending.output = output; + view.pending_wm_stack_link.remove(); + view.pending_focus_stack_link.remove(); - self.output.views.remove(node); - destination_output.views.attach(node, server.config.attach_mode); - - self.output.sendViewTags(); - destination_output.sendViewTags(); - - if (self.pending.urgent) { - self.output.sendUrgentTags(); - destination_output.sendUrgentTags(); + switch (server.config.attach_mode) { + .top => output.pending.wm_stack.prepend(view), + .bottom => output.pending.wm_stack.append(view), } + output.pending.focus_stack.prepend(view); - self.output = destination_output; - - var output_width: i32 = undefined; - var output_height: i32 = undefined; - destination_output.wlr_output.effectiveResolution(&output_width, &output_height); - - if (self.pending.float) { - // Adapt dimensions of view to new output. Only necessary when floating, - // because for tiled views the output will be rearranged, taking care - // of this. - if (self.pending.fullscreen) self.pending.box = self.post_fullscreen_box; - const border_width = if (self.draw_borders) server.config.border_width else 0; - self.pending.box.width = math.min(self.pending.box.width, output_width - (2 * border_width)); - self.pending.box.height = math.min(self.pending.box.height, output_height - (2 * border_width)); - - // Adjust position of view so that it is fully inside the target output. - self.move(0, 0); - } + // Adapt the floating position/dimensions of the view to the new output. + if (view.pending.float) { + var output_width: i32 = undefined; + var output_height: i32 = undefined; + output.wlr_output.effectiveResolution(&output_width, &output_height); - if (self.pending.fullscreen) { - // If the view is floating, we need to set the post_fullscreen_box, as - // that is still set for the previous output. - if (self.pending.float) self.post_fullscreen_box = self.pending.box; + const border_width = if (view.draw_borders) server.config.border_width else 0; + view.pending.box.width = math.min(view.pending.box.width, output_width - (2 * border_width)); + view.pending.box.height = math.min(view.pending.box.height, output_height - (2 * border_width)); - self.pending.box = .{ - .x = 0, - .y = 0, - .width = output_width, - .height = output_height, - }; + view.move(0, 0); } } @@ -380,7 +326,7 @@ pub fn setActivated(self: Self, activated: bool) void { } } -fn setFullscreen(self: *Self, fullscreen: bool) void { +pub fn setFullscreen(self: *Self, fullscreen: bool) void { switch (self.impl) { .xdg_toplevel => |xdg_toplevel| xdg_toplevel.setFullscreen(fullscreen), .xwayland_view => |*xwayland_view| { @@ -431,10 +377,9 @@ pub fn getAppId(self: Self) ?[*:0]const u8 { }; } -/// Clamp the width/height of the pending state to the constraints of the view -pub fn applyConstraints(self: *Self) void { +/// Clamp the width/height of the box to the constraints of the view +pub fn applyConstraints(self: *Self, box: *wlr.Box) void { const constraints = self.getConstraints(); - const box = &self.pending.box; box.width = math.clamp(box.width, constraints.min_width, constraints.max_width); box.height = math.clamp(box.height, constraints.min_height, constraints.max_height); } @@ -451,9 +396,12 @@ pub fn getConstraints(self: Self) Constraints { /// bounds of the output. pub fn move(self: *Self, delta_x: i32, delta_y: i32) void { const border_width = if (self.draw_borders) server.config.border_width else 0; - var output_width: i32 = undefined; - var output_height: i32 = undefined; - self.output.wlr_output.effectiveResolution(&output_width, &output_height); + + var output_width: i32 = math.maxInt(i32); + var output_height: i32 = math.maxInt(i32); + if (self.pending.output) |output| { + output.wlr_output.effectiveResolution(&output_width, &output_height); + } const max_x = output_width - self.pending.box.width - border_width; self.pending.box.x += delta_x; @@ -483,62 +431,60 @@ pub fn fromWlrSurface(surface: *wlr.Surface) ?*Self { return null; } -pub fn shouldTrackConfigure(self: Self) bool { - // We don't give a damn about frame perfection for xwayland views - if (build_options.xwayland and self.impl == .xwayland_view) return false; - - // There are exactly three cases in which we do not track configures - // 1. the view was and remains floating - // 2. the view is changing from float/layout to fullscreen - // 3. the view is changing from fullscreen to float - return !((self.pending.float and self.current.float) or - (self.pending.fullscreen and !self.current.fullscreen) or - (self.pending.float and !self.pending.fullscreen and self.current.fullscreen)); -} - /// Called by the impl when the surface is ready to be displayed -pub fn map(self: *Self) !void { - log.debug("view '{?s}' mapped", .{self.getTitle()}); +pub fn map(view: *Self) !void { + log.debug("view '{?s}' mapped", .{view.getTitle()}); - self.tree.node.setEnabled(true); - self.popup_tree.node.setEnabled(true); + server.xdg_activation.events.request_activate.add(&view.request_activate); - server.xdg_activation.events.request_activate.add(&self.request_activate); + if (server.input_manager.defaultSeat().focused_output) |output| { + // Center the initial pending box on the output + view.pending.box.x = @divTrunc(math.max(0, output.usable_box.width - view.pending.box.width), 2); + view.pending.box.y = @divTrunc(math.max(0, output.usable_box.height - view.pending.box.height), 2); - // Add the view to the stack of its output - const node = @fieldParentPtr(ViewStack(Self).Node, "view", self); - self.output.views.attach(node, server.config.attach_mode); + view.pending.tags = blk: { + const tags = output.pending.tags & server.config.spawn_tagmask; + break :blk if (tags != 0) tags else output.pending.tags; + }; - // Inform all seats that the view has been mapped so they can handle focus - var it = server.input_manager.seats.first; - while (it) |seat_node| : (it = seat_node.next) try seat_node.data.handleViewMap(self); + view.setPendingOutput(output); - self.output.sendViewTags(); + var it = server.input_manager.seats.first; + while (it) |seat_node| : (it = seat_node.next) seat_node.data.focus(view); + } - self.applyPending(); + view.float_box = view.pending.box; + + server.root.applyPending(); } /// Called by the impl when the surface will no longer be displayed -pub fn unmap(self: *Self) void { - log.debug("view '{?s}' unmapped", .{self.getTitle()}); - - self.tree.node.setEnabled(false); - self.popup_tree.node.setEnabled(false); +pub fn unmap(view: *Self) void { + log.debug("view '{?s}' unmapped", .{view.getTitle()}); - if (!self.saved_surface_tree.node.enabled) self.saveSurfaceTree(); + if (!view.saved_surface_tree.node.enabled) view.saveSurfaceTree(); - // Inform all seats that the view has been unmapped so they can handle focus - var it = server.input_manager.seats.first; - while (it) |seat_node| : (it = seat_node.next) seat_node.data.handleViewUnmap(self); - - self.request_activate.link.remove(); + { + view.pending.output = null; + view.pending_focus_stack_link.remove(); + view.pending_wm_stack_link.remove(); + server.root.hidden.pending.focus_stack.prepend(view); + server.root.hidden.pending.wm_stack.prepend(view); + } - self.output.sendViewTags(); + { + var it = server.input_manager.seats.first; + while (it) |node| : (it = node.next) { + const seat = &node.data; + if (seat.focused == .view and seat.focused.view == view) { + seat.focus(null); + } + } + } - // Still need to arrange if fullscreened from the layout - if (!self.current.float) self.output.arrangeViews(); + view.request_activate.link.remove(); - server.root.startTransaction(); + server.root.applyPending(); } pub fn notifyTitle(self: *const Self) void { @@ -565,7 +511,7 @@ fn handleRequestActivate( if (fromWlrSurface(event.surface)) |view| { if (view.current.focus == 0) { view.pending.urgent = true; - server.root.startTransaction(); + server.root.applyPending(); } } } diff --git a/river/XdgPopup.zig b/river/XdgPopup.zig index a4200ea..2bb6d42 100644 --- a/river/XdgPopup.zig +++ b/river/XdgPopup.zig @@ -24,14 +24,11 @@ const server = &@import("main.zig").server; const util = @import("util.zig"); const Output = @import("Output.zig"); +const SceneNodeData = @import("SceneNodeData.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, @@ -41,18 +38,17 @@ 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), +// TODO check if popup is set_reactive and reposition on parent movement. 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), }; @@ -81,7 +77,6 @@ fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.Xdg wlr_xdg_popup, xdg_popup.root, xdg_popup.tree, - xdg_popup.output, ) catch { wlr_xdg_popup.resource.postNoMemory(); return; @@ -91,8 +86,14 @@ fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.Xdg fn handleReposition(listener: *wl.Listener(void)) void { const xdg_popup = @fieldParentPtr(XdgPopup, "reposition", listener); + const output = switch (SceneNodeData.get(&xdg_popup.root.node).?.data) { + .view => |view| view.current.output orelse return, + .layer_surface => |layer_surface| layer_surface.output, + else => unreachable, + }; + var box: wlr.Box = undefined; - server.root.output_layout.getBox(xdg_popup.output.*.wlr_output, &box); + server.root.output_layout.getBox(output.wlr_output, &box); var root_lx: c_int = undefined; var root_ly: c_int = undefined; diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig index a15a3d1..2435486 100644 --- a/river/XdgToplevel.zig +++ b/river/XdgToplevel.zig @@ -28,7 +28,6 @@ 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; const log = std.log.scoped(.xdg_shell); @@ -39,8 +38,8 @@ xdg_toplevel: *wlr.XdgToplevel, geometry: wlr.Box, -/// Set to true when the client acks the configure with serial View.pending_serial. -acked_pending_serial: bool = false, +/// Set to true when the client acks the configure with serial View.inflight_serial. +acked_inflight_serial: bool = false, // Listeners that are always active over the view's lifetime destroy: wl.Listener(void) = wl.Listener(void).init(handleDestroy), @@ -53,15 +52,11 @@ ack_configure: wl.Listener(*wlr.XdgSurface.Configure) = wl.Listener(*wlr.XdgSurface.Configure).init(handleAckConfigure), commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit), request_fullscreen: wl.Listener(void) = wl.Listener(void).init(handleRequestFullscreen), -request_move: wl.Listener(*wlr.XdgToplevel.event.Move) = - wl.Listener(*wlr.XdgToplevel.event.Move).init(handleRequestMove), -request_resize: wl.Listener(*wlr.XdgToplevel.event.Resize) = - wl.Listener(*wlr.XdgToplevel.event.Resize).init(handleRequestResize), set_title: wl.Listener(void) = wl.Listener(void).init(handleSetTitle), set_app_id: wl.Listener(void) = wl.Listener(void).init(handleSetAppId), -pub fn create(output: *Output, xdg_toplevel: *wlr.XdgToplevel) error{OutOfMemory}!void { - const view = try View.create(output, .{ .xdg_toplevel = .{ +pub fn create(xdg_toplevel: *wlr.XdgToplevel) error{OutOfMemory}!void { + const view = try View.create(.{ .xdg_toplevel = .{ .view = undefined, .xdg_toplevel = xdg_toplevel, .geometry = undefined, @@ -85,27 +80,23 @@ pub fn create(output: *Output, xdg_toplevel: *wlr.XdgToplevel) error{OutOfMemory _ = xdg_toplevel.setWmCapabilities(.{ .fullscreen = true }); } -/// Returns true if a configure must be sent to ensure that the pending +/// Returns true if a configure must be sent to ensure that the inflight /// dimensions are applied. pub fn needsConfigure(self: Self) bool { - const scheduled = &self.xdg_toplevel.scheduled; - const state = &self.view.pending; + const view = self.view; // We avoid a special case for newly mapped views which we have not yet - // configured by setting scheduled.width/height to the initial width/height + // configured by setting the current width/height to the initial width/height // of the view in handleMap(). - return state.box.width != scheduled.width or state.box.height != scheduled.height; + return view.inflight.box.width != view.current.box.width or + view.inflight.box.height != view.current.box.height; } -/// Send a configure event, applying the pending state of the view. +/// Send a configure event, applying the inflight state of the view. pub fn configure(self: *Self) void { - const state = &self.view.pending; - self.view.pending_serial = self.xdg_toplevel.setSize(state.box.width, state.box.height); - self.acked_pending_serial = false; -} - -pub fn lastSetFullscreenState(self: Self) bool { - return self.xdg_toplevel.scheduled.fullscreen; + const state = &self.view.inflight; + self.view.inflight_serial = self.xdg_toplevel.setSize(state.box.width, state.box.height); + self.acked_inflight_serial = false; } pub fn rootSurface(self: Self) *wlr.Surface { @@ -174,31 +165,20 @@ fn handleMap(listener: *wl.Listener(void)) void { self.xdg_toplevel.base.events.ack_configure.add(&self.ack_configure); self.xdg_toplevel.base.surface.events.commit.add(&self.commit); self.xdg_toplevel.events.request_fullscreen.add(&self.request_fullscreen); - self.xdg_toplevel.events.request_move.add(&self.request_move); - self.xdg_toplevel.events.request_resize.add(&self.request_resize); self.xdg_toplevel.events.set_title.add(&self.set_title); self.xdg_toplevel.events.set_app_id.add(&self.set_app_id); - // Use the view's initial size centered on the output as the default - // floating dimensions - var initial_box: wlr.Box = undefined; - self.xdg_toplevel.base.getGeometry(&initial_box); + var geometry: wlr.Box = undefined; + self.xdg_toplevel.base.getGeometry(&geometry); - view.float_box = .{ - .x = @divTrunc(math.max(0, view.output.usable_box.width - initial_box.width), 2), - .y = @divTrunc(math.max(0, view.output.usable_box.height - initial_box.height), 2), - .width = initial_box.width, - .height = initial_box.height, + view.pending.box = .{ + .x = 0, + .y = 0, + .width = geometry.width, + .height = geometry.height, }; - - // We initialize these to avoid special-casing newly mapped views in - // the check preformed in needsConfigure(). - self.xdg_toplevel.scheduled.width = initial_box.width; - self.xdg_toplevel.scheduled.height = initial_box.height; - - // Also use the view's "natural" size as the initial regular dimensions, - // for the case that it does not get arranged by a lyaout. - view.pending.box = view.float_box; + view.inflight.box = view.pending.box; + view.current.box = view.pending.box; const state = &self.xdg_toplevel.current; const has_fixed_size = state.min_width != 0 and state.min_height != 0 and @@ -235,8 +215,6 @@ fn handleUnmap(listener: *wl.Listener(void)) void { self.ack_configure.link.remove(); self.commit.link.remove(); self.request_fullscreen.link.remove(); - self.request_move.link.remove(); - self.request_resize.link.remove(); self.set_title.link.remove(); self.set_app_id.link.remove(); @@ -246,12 +224,7 @@ fn handleUnmap(listener: *wl.Listener(void)) void { 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 { + XdgPopup.create(wlr_xdg_popup, self.view.popup_tree, self.view.popup_tree) catch { wlr_xdg_popup.resource.postNoMemory(); return; }; @@ -262,9 +235,9 @@ fn handleAckConfigure( acked_configure: *wlr.XdgSurface.Configure, ) void { const self = @fieldParentPtr(Self, "ack_configure", listener); - if (self.view.pending_serial) |serial| { + if (self.view.inflight_serial) |serial| { if (serial == acked_configure.serial) { - self.acked_pending_serial = true; + self.acked_inflight_serial = true; } } } @@ -279,27 +252,10 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void { const size_changed = !std.meta.eql(self.geometry, new_geometry); self.geometry = new_geometry; - // If we have sent a configure changing the size - if (view.pending_serial != null) { - if (self.acked_pending_serial) { - // If this commit is in response to our configure and the - // transaction code is tracking this configure, notify it. - // Otherwise, apply the pending state immediately. - view.pending_serial = null; - if (view.shouldTrackConfigure()) { - server.root.notifyConfigured(); - } else { - const self_tags_changed = view.pending.tags != view.current.tags; - const urgent_tags_dirty = view.pending.urgent != view.current.urgent or - (view.pending.urgent and self_tags_changed); - - view.updateCurrent(); - - if (self_tags_changed) view.output.sendViewTags(); - if (urgent_tags_dirty) view.output.sendUrgentTags(); - - server.input_manager.updateCursorState(); - } + if (view.inflight_serial != null) { + if (self.acked_inflight_serial) { + view.inflight_serial = null; + server.root.notifyConfigured(); } else { // If the client has not yet acked our configure, we need to send a // frame done event so that it commits another buffer. These @@ -307,12 +263,16 @@ fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void { // stashed buffer from when the transaction started. view.sendFrameDone(); } - } else if ((self.view.pending.float or self.view.output.pending.layout == null) and size_changed) { + } else if (size_changed and !view.current.fullscreen and + (view.current.float or view.current.output == null or view.current.output.?.layout == null)) + { // If the client has decided to resize itself and the view is floating, // then respect that resize. + view.current.box.width = new_geometry.width; + view.current.box.height = new_geometry.height; view.pending.box.width = new_geometry.width; view.pending.box.height = new_geometry.height; - view.applyPending(); + server.root.applyPending(); } } @@ -322,30 +282,10 @@ fn handleRequestFullscreen(listener: *wl.Listener(void)) void { const self = @fieldParentPtr(Self, "request_fullscreen", listener); if (self.view.pending.fullscreen != self.xdg_toplevel.requested.fullscreen) { self.view.pending.fullscreen = self.xdg_toplevel.requested.fullscreen; - self.view.applyPending(); + server.root.applyPending(); } } -/// Called when the client asks to be moved via the cursor, for example when the -/// user drags CSD titlebars. -fn handleRequestMove( - listener: *wl.Listener(*wlr.XdgToplevel.event.Move), - event: *wlr.XdgToplevel.event.Move, -) void { - const self = @fieldParentPtr(Self, "request_move", listener); - const seat = @intToPtr(*Seat, event.seat.seat.data); - if ((self.view.pending.float or self.view.output.pending.layout == null) and !self.view.pending.fullscreen) - seat.cursor.enterMode(.move, self.view); -} - -/// Called when the client asks to be resized via the cursor. -fn handleRequestResize(listener: *wl.Listener(*wlr.XdgToplevel.event.Resize), event: *wlr.XdgToplevel.event.Resize) void { - const self = @fieldParentPtr(Self, "request_resize", listener); - const seat = @intToPtr(*Seat, event.seat.seat.data); - if ((self.view.pending.float or self.view.output.pending.layout == null) and !self.view.pending.fullscreen) - seat.cursor.enterMode(.resize, self.view); -} - /// Called when the client sets / updates its title fn handleSetTitle(listener: *wl.Listener(void)) void { const self = @fieldParentPtr(Self, "set_title", listener); diff --git a/river/XwaylandOverrideRedirect.zig b/river/XwaylandOverrideRedirect.zig index 5760879..e87ce77 100644 --- a/river/XwaylandOverrideRedirect.zig +++ b/river/XwaylandOverrideRedirect.zig @@ -27,7 +27,6 @@ const util = @import("util.zig"); const SceneNodeData = @import("SceneNodeData.zig"); const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; const XwaylandView = @import("XwaylandView.zig"); const log = std.log.scoped(.xwayland); @@ -151,7 +150,7 @@ fn handleUnmap(listener: *wl.Listener(*wlr.XwaylandSurface), _: *wlr.XwaylandSur } } - server.root.startTransaction(); + server.root.applyPending(); } fn handleSetGeometry(listener: *wl.Listener(*wlr.XwaylandSurface), _: *wlr.XwaylandSurface) void { @@ -173,8 +172,7 @@ fn handleSetOverrideRedirect( if (xwayland_surface.mapped) handleUnmap(&self.unmap, xwayland_surface); handleDestroy(&self.destroy, xwayland_surface); - const output = server.input_manager.defaultSeat().focused_output; - XwaylandView.create(output, xwayland_surface) catch { + XwaylandView.create(xwayland_surface) catch { log.err("out of memory", .{}); return; }; diff --git a/river/XwaylandView.zig b/river/XwaylandView.zig index e17ac0a..376d7a2 100644 --- a/river/XwaylandView.zig +++ b/river/XwaylandView.zig @@ -28,7 +28,6 @@ const util = @import("util.zig"); const Output = @import("Output.zig"); const View = @import("View.zig"); -const ViewStack = @import("view_stack.zig").ViewStack; const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig"); const log = std.log.scoped(.xwayland); @@ -40,11 +39,6 @@ xwayland_surface: *wlr.XwaylandSurface, /// Created on map and destroyed on unmap surface_tree: ?*wlr.SceneTree = null, -/// The wlroots Xwayland implementation overwrites xwayland_surface.fullscreen -/// immediately when the client requests it, so we track this state here to be -/// able to match the XdgToplevel API. -last_set_fullscreen_state: bool = false, - // Listeners that are always active over the view's lifetime destroy: wl.Listener(*wlr.XwaylandSurface) = wl.Listener(*wlr.XwaylandSurface).init(handleDestroy), map: wl.Listener(*wlr.XwaylandSurface) = wl.Listener(*wlr.XwaylandSurface).init(handleMap), @@ -62,8 +56,8 @@ request_fullscreen: wl.Listener(*wlr.XwaylandSurface) = request_minimize: wl.Listener(*wlr.XwaylandSurface.event.Minimize) = wl.Listener(*wlr.XwaylandSurface.event.Minimize).init(handleRequestMinimize), -pub fn create(output: *Output, xwayland_surface: *wlr.XwaylandSurface) error{OutOfMemory}!void { - const view = try View.create(output, .{ .xwayland_view = .{ +pub fn create(xwayland_surface: *wlr.XwaylandSurface) error{OutOfMemory}!void { + const view = try View.create(.{ .xwayland_view = .{ .view = undefined, .xwayland_surface = xwayland_surface, } }); @@ -86,23 +80,23 @@ pub fn create(output: *Output, xwayland_surface: *wlr.XwaylandSurface) error{Out } pub fn needsConfigure(self: Self) bool { - const output = self.view.output; + const output = self.view.inflight.output orelse return false; var output_box: wlr.Box = undefined; server.root.output_layout.getBox(output.wlr_output, &output_box); - return self.xwayland_surface.x != self.view.pending.box.x + output_box.x or - self.xwayland_surface.y != self.view.pending.box.y + output_box.y or - self.xwayland_surface.width != self.view.pending.box.width or - self.xwayland_surface.height != self.view.pending.box.height; + + const state = &self.view.inflight; + return self.xwayland_surface.x != state.box.x + output_box.x or + self.xwayland_surface.y != state.box.y + output_box.y or + self.xwayland_surface.width != state.box.width or + self.xwayland_surface.height != state.box.height; } -/// Apply pending state. Note: we don't set View.serial as -/// shouldTrackConfigure() is always false for xwayland views. pub fn configure(self: Self) void { - const output = self.view.output; + const output = self.view.inflight.output orelse return; var output_box: wlr.Box = undefined; server.root.output_layout.getBox(output.wlr_output, &output_box); - const state = &self.view.pending; + const state = &self.view.inflight; self.xwayland_surface.configure( @intCast(i16, state.box.x + output_box.x), @intCast(i16, state.box.y + output_box.y), @@ -111,10 +105,6 @@ pub fn configure(self: Self) void { ); } -pub fn lastSetFullscreenState(self: Self) bool { - return self.last_set_fullscreen_state; -} - pub fn rootSurface(self: Self) *wlr.Surface { // TODO This is probably not OK, understand when xwayland surfaces can be null. return self.xwayland_surface.surface.?; @@ -135,7 +125,6 @@ pub fn setActivated(self: Self, activated: bool) void { } pub fn setFullscreen(self: *Self, fullscreen: bool) void { - self.last_set_fullscreen_state = fullscreen; self.xwayland_surface.setFullscreen(fullscreen); } @@ -196,14 +185,14 @@ pub fn handleMap(listener: *wl.Listener(*wlr.XwaylandSurface), xwayland_surface: return; }; - // Use the view's "natural" size centered on the output as the default - // floating dimensions - view.float_box = .{ - .x = @divTrunc(math.max(0, view.output.usable_box.width - self.xwayland_surface.width), 2), - .y = @divTrunc(math.max(0, view.output.usable_box.height - self.xwayland_surface.height), 2), + view.pending.box = .{ + .x = 0, + .y = 0, .width = self.xwayland_surface.width, .height = self.xwayland_surface.height, }; + view.inflight.box = view.pending.box; + view.current.box = view.pending.box; const has_fixed_size = if (self.xwayland_surface.size_hints) |size_hints| size_hints.min_width != 0 and size_hints.min_height != 0 and @@ -294,7 +283,7 @@ fn handleRequestFullscreen(listener: *wl.Listener(*wlr.XwaylandSurface), xwaylan const self = @fieldParentPtr(Self, "request_fullscreen", listener); if (self.view.pending.fullscreen != xwayland_surface.fullscreen) { self.view.pending.fullscreen = xwayland_surface.fullscreen; - self.view.applyPending(); + server.root.applyPending(); } } diff --git a/river/command/config.zig b/river/command/config.zig index c657a5f..1a5543a 100644 --- a/river/command/config.zig +++ b/river/command/config.zig @@ -33,8 +33,7 @@ pub fn borderWidth( if (args.len > 2) return Error.TooManyArguments; server.config.border_width = try fmt.parseInt(u31, args[1], 10); - server.root.arrangeAll(); - server.root.startTransaction(); + server.root.applyPending(); } pub fn backgroundColor( @@ -62,7 +61,7 @@ pub fn borderColorFocused( if (args.len > 2) return Error.TooManyArguments; server.config.border_color_focused = try parseRgba(args[1]); - server.root.startTransaction(); + server.root.applyPending(); } pub fn borderColorUnfocused( @@ -74,7 +73,7 @@ pub fn borderColorUnfocused( if (args.len > 2) return Error.TooManyArguments; server.config.border_color_unfocused = try parseRgba(args[1]); - server.root.startTransaction(); + server.root.applyPending(); } pub fn borderColorUrgent( @@ -86,7 +85,7 @@ pub fn borderColorUrgent( if (args.len > 2) return Error.TooManyArguments; server.config.border_color_urgent = try parseRgba(args[1]); - server.root.startTransaction(); + server.root.applyPending(); } pub fn setCursorWarp( diff --git a/river/command/focus_view.zig b/river/command/focus_view.zig index 4840f83..9c48525 100644 --- a/river/command/focus_view.zig +++ b/river/command/focus_view.zig @@ -15,14 +15,15 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. const std = @import("std"); +const assert = std.debug.assert; const server = &@import("../main.zig").server; const Direction = @import("../command.zig").Direction; const Error = @import("../command.zig").Error; +const Output = @import("../Output.zig"); const Seat = @import("../Seat.zig"); const View = @import("../View.zig"); -const ViewStack = @import("../view_stack.zig").ViewStack; /// Focus either the next or the previous visible view, depending on the enum /// passed. Does nothing if there are 1 or 0 views in the stack. @@ -35,40 +36,45 @@ pub fn focusView( if (args.len > 2) return Error.TooManyArguments; const direction = std.meta.stringToEnum(Direction, args[1]) orelse return Error.InvalidDirection; - const output = seat.focused_output; + const output = seat.focused_output orelse return; - if (seat.focused == .view) { - // If the focused view is fullscreen, do nothing - if (seat.focused.view.current.fullscreen) return; + if (seat.focused != .view) return; + if (seat.focused.view.pending.fullscreen) return; - // If there is a currently focused view, focus the next visible view in the stack. - const focused_node = @fieldParentPtr(ViewStack(View).Node, "view", seat.focused.view); - var it = switch (direction) { - .next => ViewStack(View).iter(focused_node, .forward, output.pending.tags, filter), - .previous => ViewStack(View).iter(focused_node, .reverse, output.pending.tags, filter), - }; - - // Skip past the focused node - _ = it.next(); - // Focus the next visible node if there is one - if (it.next()) |view| { - seat.focus(view); - server.root.startTransaction(); - return; - } + if (focusViewTarget(seat, output, direction)) |target| { + assert(!target.pending.fullscreen); + seat.focus(target); + server.root.applyPending(); } +} - // There is either no currently focused view or the last visible view in the - // stack is focused and we need to wrap. - var it = switch (direction) { - .next => ViewStack(View).iter(output.views.first, .forward, output.pending.tags, filter), - .previous => ViewStack(View).iter(output.views.last, .reverse, output.pending.tags, filter), - }; +fn focusViewTarget(seat: *Seat, output: *Output, direction: Direction) ?*View { + switch (direction) { + inline else => |dir| { + const it_dir = comptime switch (dir) { + .next => .forward, + .previous => .reverse, + }; + var it = output.pending.wm_stack.iterator(it_dir); + while (it.next()) |view| { + if (view == seat.focused.view) break; + } else { + unreachable; + } - seat.focus(it.next()); - server.root.startTransaction(); -} + // Return the next view in the stack matching the tags if any. + while (it.next()) |view| { + if (output.pending.tags & view.pending.tags != 0) return view; + } -fn filter(view: *View, filter_tags: u32) bool { - return view.tree.node.enabled and view.pending.tags & filter_tags != 0; + // Wrap and return the first view in the stack matching the tags if + // any is found before completing the loop back to the focused view. + while (it.next()) |view| { + if (view == seat.focused.view) return null; + if (output.pending.tags & view.pending.tags != 0) return view; + } + + unreachable; + }, + } } diff --git a/river/command/layout.zig b/river/command/layout.zig index e0967c9..133a332 100644 --- a/river/command/layout.zig +++ b/river/command/layout.zig @@ -32,7 +32,7 @@ pub fn outputLayout( if (args.len < 2) return Error.NotEnoughArguments; if (args.len > 2) return Error.TooManyArguments; - const output = seat.focused_output; + const output = seat.focused_output orelse return; const old_layout_namespace = output.layout_namespace; output.layout_namespace = try util.gpa.dupe(u8, args[1]); if (old_layout_namespace) |old| util.gpa.free(old); @@ -69,7 +69,7 @@ pub fn sendLayoutCmd( if (args.len < 3) return Error.NotEnoughArguments; if (args.len > 3) return Error.TooManyArguments; - const output = seat.focused_output; + const output = seat.focused_output orelse return; const target_namespace = args[1]; var it = output.layouts.first; @@ -82,5 +82,5 @@ pub fn sendLayoutCmd( layout.layout.sendUserCommandTags(output.pending.tags); } layout.layout.sendUserCommand(args[2]); - if (layout == output.current.layout) output.arrangeViews(); + if (layout == output.layout) server.root.applyPending(); } diff --git a/river/command/move.zig b/river/command/move.zig index 6f22972..73e6b54 100644 --- a/river/command/move.zig +++ b/river/command/move.zig @@ -60,10 +60,11 @@ pub fn snap( return Error.InvalidPhysicalDirection; const view = getView(seat) orelse return; + const output = view.pending.output orelse return; const border_width = server.config.border_width; var output_width: i32 = undefined; var output_height: i32 = undefined; - view.output.wlr_output.effectiveResolution(&output_width, &output_height); + output.wlr_output.effectiveResolution(&output_width, &output_height); switch (direction) { .up => view.pending.box.y = border_width, .down => view.pending.box.y = output_height - view.pending.box.height - border_width, @@ -87,14 +88,16 @@ pub fn resize( return Error.InvalidOrientation; const view = getView(seat) orelse return; - var output_width: i32 = undefined; - var output_height: i32 = undefined; - view.output.wlr_output.effectiveResolution(&output_width, &output_height); + var output_width: c_int = math.maxInt(c_int); + var output_height: c_int = math.maxInt(c_int); + if (view.pending.output) |output| { + output.wlr_output.effectiveResolution(&output_width, &output_height); + } switch (orientation) { .horizontal => { const prev_width = view.pending.box.width; view.pending.box.width += delta; - view.applyConstraints(); + view.applyConstraints(&view.pending.box); // Get width difference after applying view constraints, so that the // move reflects the actual size difference, but before applying the // output size constraints, to allow growing a view even if it is @@ -110,7 +113,7 @@ pub fn resize( .vertical => { const prev_height = view.pending.box.height; view.pending.box.height += delta; - view.applyConstraints(); + view.applyConstraints(&view.pending.box); const diff_height = prev_height - view.pending.box.height; // Do not grow bigger than the output view.pending.box.height = math.min( @@ -129,12 +132,11 @@ fn apply(view: *View) void { // dimensions are set by a layout generator. If however the views are // unarranged, leave them as non-floating so the next active layout can // affect them. - if (view.output.pending.layout != null) + if (view.pending.output == null or view.pending.output.?.layout != null) { view.pending.float = true; + } - view.float_box = view.pending.box; - - view.applyPending(); + server.root.applyPending(); } fn getView(seat: *Seat) ?*View { diff --git a/river/command/output.zig b/river/command/output.zig index da7f921..dc72323 100644 --- a/river/command/output.zig +++ b/river/command/output.zig @@ -37,14 +37,14 @@ pub fn focusOutput( if (args.len > 2) return Error.TooManyArguments; // If the noop output is focused, there are no other outputs to switch to - if (seat.focused_output == &server.root.noop_output) { + if (seat.focused_output == null) { assert(server.root.outputs.len == 0); return; } seat.focusOutput((try getOutput(seat, args[1])) orelse return); seat.focus(null); - server.root.startTransaction(); + server.root.applyPending(); } pub fn sendToOutput( @@ -56,22 +56,21 @@ pub fn sendToOutput( if (args.len > 2) return Error.TooManyArguments; // If the noop output is focused, there is nowhere to send the view - if (seat.focused_output == &server.root.noop_output) { + if (seat.focused_output == null) { assert(server.root.outputs.len == 0); return; } if (seat.focused == .view) { const destination_output = (try getOutput(seat, args[1])) orelse return; + // If the view is already on destination_output, do nothing - if (seat.focused.view.output == destination_output) return; - seat.focused.view.sendToOutput(destination_output); + if (seat.focused.view.pending.output == destination_output) return; + seat.focused.view.setPendingOutput(destination_output); // Handle the change and focus whatever's next in the focus stack seat.focus(null); - seat.focused_output.arrangeViews(); - destination_output.arrangeViews(); - server.root.startTransaction(); + server.root.applyPending(); } } @@ -80,19 +79,19 @@ pub fn sendToOutput( fn getOutput(seat: *Seat, str: []const u8) !?*Output { if (std.meta.stringToEnum(Direction, str)) |direction| { // Logical direction // Return the next/prev output in the list if there is one, else wrap - const focused_node = @fieldParentPtr(std.TailQueue(Output).Node, "data", seat.focused_output); + const focused_node = @fieldParentPtr(std.TailQueue(Output).Node, "data", seat.focused_output.?); return switch (direction) { .next => if (focused_node.next) |node| &node.data else &server.root.outputs.first.?.data, .previous => if (focused_node.prev) |node| &node.data else &server.root.outputs.last.?.data, }; } else if (std.meta.stringToEnum(wlr.OutputLayout.Direction, str)) |direction| { // Spacial direction var focus_box: wlr.Box = undefined; - server.root.output_layout.getBox(seat.focused_output.wlr_output, &focus_box); + server.root.output_layout.getBox(seat.focused_output.?.wlr_output, &focus_box); if (focus_box.empty()) return null; const wlr_output = server.root.output_layout.adjacentOutput( direction, - seat.focused_output.wlr_output, + seat.focused_output.?.wlr_output, @intToFloat(f64, focus_box.x + @divTrunc(focus_box.width, 2)), @intToFloat(f64, focus_box.y + @divTrunc(focus_box.height, 2)), ) orelse return null; diff --git a/river/command/swap.zig b/river/command/swap.zig index ed3118f..032bdaf 100644 --- a/river/command/swap.zig +++ b/river/command/swap.zig @@ -15,14 +15,15 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. const std = @import("std"); +const assert = std.debug.assert; const server = &@import("../main.zig").server; -const Error = @import("../command.zig").Error; const Direction = @import("../command.zig").Direction; +const Error = @import("../command.zig").Error; +const Output = @import("../Output.zig"); const Seat = @import("../Seat.zig"); const View = @import("../View.zig"); -const ViewStack = @import("../view_stack.zig").ViewStack; /// Swap the currently focused view with either the view higher or lower in the visible stack pub fn swap( @@ -33,51 +34,47 @@ pub fn swap( if (args.len < 2) return Error.NotEnoughArguments; if (args.len > 2) return Error.TooManyArguments; - if (seat.focused != .view) - return; + const direction = std.meta.stringToEnum(Direction, args[1]) orelse return Error.InvalidDirection; + const output = seat.focused_output orelse return; - // Filter out everything that is not part of the current layout + if (seat.focused != .view) return; if (seat.focused.view.pending.float or seat.focused.view.pending.fullscreen) return; - const direction = std.meta.stringToEnum(Direction, args[1]) orelse return Error.InvalidDirection; + if (swapTarget(seat, output, direction)) |target| { + assert(!target.pending.float); + assert(!target.pending.fullscreen); + seat.focused.view.pending_wm_stack_link.swapWith(&target.pending_wm_stack_link); + server.root.applyPending(); + } +} - const focused_node = @fieldParentPtr(ViewStack(View).Node, "view", seat.focused.view); - const output = seat.focused_output; - var it = ViewStack(View).iter( - focused_node, - if (direction == .next) .forward else .reverse, - output.pending.tags, - filter, - ); - var it_wrap = ViewStack(View).iter( - if (direction == .next) output.views.first else output.views.last, - if (direction == .next) .forward else .reverse, - output.pending.tags, - filter, - ); +fn swapTarget(seat: *Seat, output: *Output, direction: Direction) ?*View { + switch (direction) { + inline else => |dir| { + const it_dir = comptime switch (dir) { + .next => .forward, + .previous => .reverse, + }; + var it = output.pending.wm_stack.iterator(it_dir); + while (it.next()) |view| { + if (view == seat.focused.view) break; + } else { + unreachable; + } - // skip the first node which is focused_node - _ = it.next().?; + // Return the next view in the stack matching the tags if any. + while (it.next()) |view| { + if (output.pending.tags & view.pending.tags != 0 and !view.pending.float) return view; + } - const to_swap = @fieldParentPtr( - ViewStack(View).Node, - "view", - // Wrap around if needed - if (it.next()) |next| next else it_wrap.next().?, - ); + // Wrap and return the first view in the stack matching the tags if + // any is found before completing the loop back to the focused view. + while (it.next()) |view| { + if (view == seat.focused.view) return null; + if (output.pending.tags & view.pending.tags != 0 and !view.pending.float) return view; + } - // Dont swap when only the focused view is part of the layout - if (focused_node == to_swap) { - return; + unreachable; + }, } - - output.views.swap(focused_node, to_swap); - - output.arrangeViews(); - server.root.startTransaction(); -} - -fn filter(view: *View, filter_tags: u32) bool { - return view.tree.node.enabled and !view.pending.float and - !view.pending.fullscreen and view.pending.tags & filter_tags != 0; } diff --git a/river/command/tags.zig b/river/command/tags.zig index 460a736..3eac000 100644 --- a/river/command/tags.zig +++ b/river/command/tags.zig @@ -30,12 +30,12 @@ pub fn setFocusedTags( out: *?[]const u8, ) Error!void { const tags = try parseTags(args, out); - if (seat.focused_output.pending.tags != tags) { - seat.focused_output.previous_tags = seat.focused_output.pending.tags; - seat.focused_output.pending.tags = tags; - seat.focused_output.arrangeViews(); + const output = seat.focused_output orelse return; + if (output.pending.tags != tags) { + output.previous_tags = output.pending.tags; + output.pending.tags = tags; seat.focus(null); - server.root.startTransaction(); + server.root.applyPending(); } } @@ -59,7 +59,7 @@ pub fn setViewTags( const view = seat.focused.view; view.pending.tags = tags; seat.focus(null); - view.applyPending(); + server.root.applyPending(); } } @@ -70,14 +70,13 @@ pub fn toggleFocusedTags( out: *?[]const u8, ) Error!void { const tags = try parseTags(args, out); - const output = seat.focused_output; + const output = seat.focused_output orelse return; const new_focused_tags = output.pending.tags ^ tags; if (new_focused_tags != 0) { output.previous_tags = output.pending.tags; output.pending.tags = new_focused_tags; - output.arrangeViews(); seat.focus(null); - server.root.startTransaction(); + server.root.applyPending(); } } @@ -94,7 +93,7 @@ pub fn toggleViewTags( const view = seat.focused.view; view.pending.tags = new_tags; seat.focus(null); - view.applyPending(); + server.root.applyPending(); } } } @@ -106,13 +105,13 @@ pub fn focusPreviousTags( _: *?[]const u8, ) Error!void { if (args.len > 1) return error.TooManyArguments; - const previous_tags = seat.focused_output.previous_tags; - if (seat.focused_output.pending.tags != previous_tags) { - seat.focused_output.previous_tags = seat.focused_output.pending.tags; - seat.focused_output.pending.tags = previous_tags; - seat.focused_output.arrangeViews(); + const output = seat.focused_output orelse return; + const previous_tags = output.previous_tags; + if (output.pending.tags != previous_tags) { + output.previous_tags = output.pending.tags; + output.pending.tags = previous_tags; seat.focus(null); - server.root.startTransaction(); + server.root.applyPending(); } } @@ -123,12 +122,13 @@ pub fn sendToPreviousTags( _: *?[]const u8, ) Error!void { if (args.len > 1) return error.TooManyArguments; - const previous_tags = seat.focused_output.previous_tags; + + const output = seat.focused_output orelse return; if (seat.focused == .view) { const view = seat.focused.view; - view.pending.tags = previous_tags; + view.pending.tags = output.previous_tags; seat.focus(null); - view.applyPending(); + server.root.applyPending(); } } diff --git a/river/command/toggle_float.zig b/river/command/toggle_float.zig index 41418c1..5185ae4 100644 --- a/river/command/toggle_float.zig +++ b/river/command/toggle_float.zig @@ -36,13 +36,12 @@ pub fn toggleFloat( // If views are unarranged, don't allow changing the views float status. // It would just lead to confusing because this state would not be // visible immediately, only after a layout is connected. - if (view.output.pending.layout == null) - return; + if (view.pending.output == null or view.pending.output.?.layout == null) return; // Don't float fullscreen views if (view.pending.fullscreen) return; view.pending.float = !view.pending.float; - view.applyPending(); + server.root.applyPending(); } } diff --git a/river/command/toggle_fullscreen.zig b/river/command/toggle_fullscreen.zig index 3f2d5f9..5dfef41 100644 --- a/river/command/toggle_fullscreen.zig +++ b/river/command/toggle_fullscreen.zig @@ -33,6 +33,6 @@ pub fn toggleFullscreen( const view = seat.focused.view; view.pending.fullscreen = !view.pending.fullscreen; - view.applyPending(); + server.root.applyPending(); } } diff --git a/river/command/zoom.zig b/river/command/zoom.zig index 853aeb0..0a16eb6 100644 --- a/river/command/zoom.zig +++ b/river/command/zoom.zig @@ -15,13 +15,13 @@ // along with this program. If not, see <https://www.gnu.org/licenses/>. const std = @import("std"); +const assert = std.debug.assert; const server = &@import("../main.zig").server; const Error = @import("../command.zig").Error; const Seat = @import("../Seat.zig"); const View = @import("../View.zig"); -const ViewStack = @import("../view_stack.zig").ViewStack; /// Bump the focused view to the top of the stack. If the view on the top of /// the stack is focused, bump the second view to the top. @@ -32,33 +32,50 @@ pub fn zoom( ) Error!void { if (args.len > 1) return Error.TooManyArguments; - if (seat.focused == .view) { - // Only zoom views that are part of the layout - if (seat.focused.view.pending.float or seat.focused.view.pending.fullscreen) return; + if (seat.focused != .view) return; + if (seat.focused.view.pending.float or seat.focused.view.pending.fullscreen) return; - // If the first view that is part of the layout is focused, zoom - // the next view in the layout. Otherwise zoom the focused view. - const output = seat.focused_output; - var it = ViewStack(View).iter(output.views.first, .forward, output.pending.tags, filter); - const layout_first = @fieldParentPtr(ViewStack(View).Node, "view", it.next().?); + const output = seat.focused_output orelse return; - const focused_node = @fieldParentPtr(ViewStack(View).Node, "view", seat.focused.view); - const zoom_node = if (focused_node == layout_first) - if (it.next()) |view| @fieldParentPtr(ViewStack(View).Node, "view", view) else null - else - focused_node; + const layout_first = blk: { + var it = output.pending.wm_stack.iterator(.forward); + while (it.next()) |view| { + if (view.pending.tags & output.pending.tags != 0 and !view.pending.float) break :blk view; + } else { + // If we are focusing a view that is not fullscreen or floating + // it must be visible and in the layout. + unreachable; + } + }; + + // If the first view that is part of the layout is focused, zoom + // the next view in the layout if any. Otherwise zoom the focused view. + const zoom_target = blk: { + if (seat.focused.view == layout_first) { + var it = output.pending.wm_stack.iterator(.forward); + while (it.next()) |view| { + if (view == seat.focused.view) break; + } else { + unreachable; + } - if (zoom_node) |to_bump| { - output.views.remove(to_bump); - output.views.push(to_bump); - seat.focus(&to_bump.view); - output.arrangeViews(); - server.root.startTransaction(); + while (it.next()) |view| { + if (view.pending.tags & output.pending.tags != 0 and !view.pending.float) break :blk view; + } else { + break :blk null; + } + } else { + break :blk seat.focused.view; } - } -} + }; + + if (zoom_target) |target| { + assert(!target.pending.float); + assert(!target.pending.fullscreen); -fn filter(view: *View, filter_tags: u32) bool { - return view.tree.node.enabled and !view.pending.float and - !view.pending.fullscreen and view.pending.tags & filter_tags != 0; + target.pending_wm_stack_link.remove(); + output.pending.wm_stack.prepend(target); + seat.focus(target); + server.root.applyPending(); + } } diff --git a/river/view_stack.zig b/river/view_stack.zig deleted file mode 100644 index 4c50ac1..0000000 --- a/river/view_stack.zig +++ /dev/null @@ -1,484 +0,0 @@ -// This file is part of river, a dynamic tiling wayland compositor. -// -// Copyright 2020 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 View = @import("View.zig"); - -pub const AttachMode = enum { - top, - bottom, -}; - -/// A specialized doubly-linked stack that allows for filtered iteration -/// over the nodes. T must be View or *View. -pub fn ViewStack(comptime T: type) type { - if (!(T == View or T == *View)) { - @compileError("ViewStack: T must be View or *View"); - } - return struct { - const Self = @This(); - - pub const Node = struct { - /// Previous/next nodes in the stack - prev: ?*Node, - next: ?*Node, - - /// The view stored in this node - view: T, - }; - - /// Top/bottom nodes in the stack - first: ?*Node = null, - last: ?*Node = null, - - /// Add a node to the top of the stack. - pub fn push(self: *Self, new_node: *Node) void { - // Set the prev/next pointers of the new node - new_node.prev = null; - new_node.next = self.first; - - if (self.first) |first| { - // If the list is not empty, set the prev pointer of the current - // first node to the new node. - first.prev = new_node; - } else { - // If the list is empty set the last pointer to the new node. - self.last = new_node; - } - - // Set the first pointer to the new node - self.first = new_node; - } - - /// Add a node to the bottom of the stack. - pub fn append(self: *Self, new_node: *Node) void { - // Set the prev/next pointers of the new node - new_node.prev = self.last; - new_node.next = null; - - if (self.last) |last| { - // If the list is not empty, set the next pointer of the current - // first node to the new node. - last.next = new_node; - } else { - // If the list is empty set the first pointer to the new node. - self.first = new_node; - } - - // Set the last pointer to the new node - self.last = new_node; - } - - /// Attach a node into the viewstack based on the attach mode - pub fn attach(self: *Self, new_node: *Node, mode: AttachMode) void { - switch (mode) { - .top => self.push(new_node), - .bottom => self.append(new_node), - } - } - - /// Remove a node from the view stack. This removes it from the stack of - /// all views as well as the stack of visible ones. - pub fn remove(self: *Self, target_node: *Node) void { - // Set the previous node/list head to the next pointer - if (target_node.prev) |prev_node| { - prev_node.next = target_node.next; - } else { - self.first = target_node.next; - } - - // Set the next node/list tail to the previous pointer - if (target_node.next) |next_node| { - next_node.prev = target_node.prev; - } else { - self.last = target_node.prev; - } - } - - /// Swap the nodes a and b. - /// pointers to Node.T will point to the same data as before - pub fn swap(self: *Self, a: *Node, b: *Node) void { - // Set self.first and self.last - const first = self.first; - const last = self.last; - if (a == first) { - self.first = b; - } else if (a == last) { - self.last = b; - } - - if (b == first) { - self.first = a; - } else if (b == last) { - self.last = a; - } - - // This is so complicated to make sure everything works when a and b are neighbors - const a_next = if (b.next == a) b else b.next; - const a_prev = if (b.prev == a) b else b.prev; - const b_next = if (a.next == b) a else a.next; - const b_prev = if (a.prev == b) a else a.prev; - - a.next = a_next; - a.prev = a_prev; - b.next = b_next; - b.prev = b_prev; - - // Update all neighbors - if (a.next) |next| { - next.prev = a; - } - if (a.prev) |prev| { - prev.next = a; - } - if (b.next) |next| { - next.prev = b; - } - if (b.prev) |prev| { - prev.next = b; - } - } - - const Direction = enum { - forward, - reverse, - }; - - fn Iter(comptime Context: type) type { - return struct { - it: ?*Node, - dir: Direction, - context: Context, - filter: *const fn (*View, Context) bool, - - /// Returns the next node in iteration order which passes the - /// filter, or null if done. - pub fn next(self: *@This()) ?*View { - return while (self.it) |node| : (self.it = if (self.dir == .forward) node.next else node.prev) { - const view = if (T == View) &node.view else node.view; - if (self.filter(view, self.context)) { - self.it = if (self.dir == .forward) node.next else node.prev; - break view; - } - } else null; - } - }; - } - - /// Return a filtered iterator over the stack given a start node, - /// iteration direction, and filter function. Views for which the - /// filter function returns false will be skipped. - pub fn iter( - start: ?*Node, - dir: Direction, - context: anytype, - filter: *const fn (*View, @TypeOf(context)) bool, - ) Iter(@TypeOf(context)) { - return .{ .it = start, .dir = dir, .context = context, .filter = filter }; - } - }; -} - -test "push/remove (*View)" { - const testing = @import("std").testing; - - const allocator = testing.allocator; - - var views = ViewStack(*View){}; - - const one = try allocator.create(ViewStack(*View).Node); - defer allocator.destroy(one); - const two = try allocator.create(ViewStack(*View).Node); - defer allocator.destroy(two); - const three = try allocator.create(ViewStack(*View).Node); - defer allocator.destroy(three); - const four = try allocator.create(ViewStack(*View).Node); - defer allocator.destroy(four); - const five = try allocator.create(ViewStack(*View).Node); - defer allocator.destroy(five); - - views.push(three); // {3} - views.push(one); // {1, 3} - views.push(four); // {4, 1, 3} - views.push(five); // {5, 4, 1, 3} - views.push(two); // {2, 5, 4, 1, 3} - - // Simple insertion - { - var it = views.first; - try testing.expect(it == two); - it = it.?.next; - try testing.expect(it == five); - it = it.?.next; - try testing.expect(it == four); - it = it.?.next; - try testing.expect(it == one); - it = it.?.next; - try testing.expect(it == three); - it = it.?.next; - - try testing.expect(it == null); - - try testing.expect(views.first == two); - try testing.expect(views.last == three); - } - - // Removal of first - views.remove(two); - { - var it = views.first; - try testing.expect(it == five); - it = it.?.next; - try testing.expect(it == four); - it = it.?.next; - try testing.expect(it == one); - it = it.?.next; - try testing.expect(it == three); - it = it.?.next; - - try testing.expect(it == null); - - try testing.expect(views.first == five); - try testing.expect(views.last == three); - } - - // Removal of last - views.remove(three); - { - var it = views.first; - try testing.expect(it == five); - it = it.?.next; - try testing.expect(it == four); - it = it.?.next; - try testing.expect(it == one); - it = it.?.next; - - try testing.expect(it == null); - - try testing.expect(views.first == five); - try testing.expect(views.last == one); - } - - // Remove from middle - views.remove(four); - { - var it = views.first; - try testing.expect(it == five); - it = it.?.next; - try testing.expect(it == one); - it = it.?.next; - - try testing.expect(it == null); - - try testing.expect(views.first == five); - try testing.expect(views.last == one); - } - - // Reinsertion - views.push(two); - views.push(three); - views.push(four); - { - var it = views.first; - try testing.expect(it == four); - it = it.?.next; - try testing.expect(it == three); - it = it.?.next; - try testing.expect(it == two); - it = it.?.next; - try testing.expect(it == five); - it = it.?.next; - try testing.expect(it == one); - it = it.?.next; - - try testing.expect(it == null); - - try testing.expect(views.first == four); - try testing.expect(views.last == one); - } - - // Clear - views.remove(four); - views.remove(two); - views.remove(three); - views.remove(one); - views.remove(five); - - try testing.expect(views.first == null); - try testing.expect(views.last == null); -} - -test "iteration (View)" { - const std = @import("std"); - const testing = std.testing; - - const allocator = testing.allocator; - - const filters = struct { - fn all(_: *View, _: void) bool { - return true; - } - - fn none(_: *View, _: void) bool { - return false; - } - - fn current(view: *View, filter_tags: u32) bool { - return view.current.tags & filter_tags != 0; - } - }; - - var views = ViewStack(View){}; - - const one_a_pb = try allocator.create(ViewStack(View).Node); - defer allocator.destroy(one_a_pb); - one_a_pb.view.current.tags = 1 << 0; - one_a_pb.view.pending.tags = 1 << 1; - - const two_a = try allocator.create(ViewStack(View).Node); - defer allocator.destroy(two_a); - two_a.view.current.tags = 1 << 0; - two_a.view.pending.tags = 1 << 0; - - const three_b_pa = try allocator.create(ViewStack(View).Node); - defer allocator.destroy(three_b_pa); - three_b_pa.view.current.tags = 1 << 1; - three_b_pa.view.pending.tags = 1 << 0; - - const four_b = try allocator.create(ViewStack(View).Node); - defer allocator.destroy(four_b); - four_b.view.current.tags = 1 << 1; - four_b.view.pending.tags = 1 << 1; - - const five_b = try allocator.create(ViewStack(View).Node); - defer allocator.destroy(five_b); - five_b.view.current.tags = 1 << 1; - five_b.view.pending.tags = 1 << 1; - - views.push(three_b_pa); // {3} - views.push(one_a_pb); // {1, 3} - views.push(four_b); // {4, 1, 3} - views.push(five_b); // {5, 4, 1, 3} - views.push(two_a); // {2, 5, 4, 1, 3} - - // Iteration over all views - { - var it = ViewStack(View).iter(views.first, .forward, {}, filters.all); - try testing.expect(it.next() == &two_a.view); - try testing.expect(it.next() == &five_b.view); - try testing.expect(it.next() == &four_b.view); - try testing.expect(it.next() == &one_a_pb.view); - try testing.expect(it.next() == &three_b_pa.view); - try testing.expect(it.next() == null); - } - - // Iteration over no views - { - var it = ViewStack(View).iter(views.first, .forward, {}, filters.none); - try testing.expect(it.next() == null); - } - - // Iteration over 'a' tags - { - var it = ViewStack(View).iter(views.first, .forward, @as(u32, 1 << 0), filters.current); - try testing.expect(it.next() == &two_a.view); - try testing.expect(it.next() == &one_a_pb.view); - try testing.expect(it.next() == null); - } - - // Iteration over 'b' tags - { - var it = ViewStack(View).iter(views.first, .forward, @as(u32, 1 << 1), filters.current); - try testing.expect(it.next() == &five_b.view); - try testing.expect(it.next() == &four_b.view); - try testing.expect(it.next() == &three_b_pa.view); - try testing.expect(it.next() == null); - } - - // Reverse iteration over all views - { - var it = ViewStack(View).iter(views.last, .reverse, {}, filters.all); - try testing.expect(it.next() == &three_b_pa.view); - try testing.expect(it.next() == &one_a_pb.view); - try testing.expect(it.next() == &four_b.view); - try testing.expect(it.next() == &five_b.view); - try testing.expect(it.next() == &two_a.view); - try testing.expect(it.next() == null); - } - - // Reverse iteration over no views - { - var it = ViewStack(View).iter(views.last, .reverse, {}, filters.none); - try testing.expect(it.next() == null); - } - - // Reverse iteration over 'a' tags - { - var it = ViewStack(View).iter(views.last, .reverse, @as(u32, 1 << 0), filters.current); - try testing.expect(it.next() == &one_a_pb.view); - try testing.expect(it.next() == &two_a.view); - try testing.expect(it.next() == null); - } - - // Reverse iteration over 'b' tags - { - var it = ViewStack(View).iter(views.last, .reverse, @as(u32, 1 << 1), filters.current); - try testing.expect(it.next() == &three_b_pa.view); - try testing.expect(it.next() == &four_b.view); - try testing.expect(it.next() == &five_b.view); - try testing.expect(it.next() == null); - } - - // Swap, then iterate - { - var view_a = views.first orelse unreachable; - var view_b = view_a.next orelse unreachable; - ViewStack(View).swap(&views, view_a, view_b); // {2, 5, 4, 1, 3} -> {5, 2, 4, 1, 3} - - view_a = views.last orelse unreachable; - view_b = view_a.prev orelse unreachable; - ViewStack(View).swap(&views, view_a, view_b); // {5, 2, 4, 1, 3} -> {5, 2, 4, 3, 1} - - view_a = views.last orelse unreachable; - view_b = views.first orelse unreachable; - ViewStack(View).swap(&views, view_a, view_b); // {5, 2, 4, 3, 1} -> {1, 2, 4, 3, 5} - - view_a = views.first orelse unreachable; - view_b = views.last orelse unreachable; - ViewStack(View).swap(&views, view_a, view_b); // {1, 2, 4, 3, 5} -> {5, 2, 4, 3, 1} - - view_a = views.first orelse unreachable; - view_a = view_a.next orelse unreachable; - view_b = view_a.next orelse unreachable; - view_b = view_b.next orelse unreachable; - ViewStack(View).swap(&views, view_a, view_b); // {5, 2, 4, 3, 1} -> {5, 3, 4, 2, 1} - - var it = ViewStack(View).iter(views.first, .forward, {}, filters.all); - try testing.expect(it.next() == &five_b.view); - try testing.expect(it.next() == &three_b_pa.view); - try testing.expect(it.next() == &four_b.view); - try testing.expect(it.next() == &two_a.view); - try testing.expect(it.next() == &one_a_pb.view); - try testing.expect(it.next() == null); - - it = ViewStack(View).iter(views.last, .reverse, {}, filters.all); - try testing.expect(it.next() == &one_a_pb.view); - try testing.expect(it.next() == &two_a.view); - try testing.expect(it.next() == &four_b.view); - try testing.expect(it.next() == &three_b_pa.view); - try testing.expect(it.next() == &five_b.view); - try testing.expect(it.next() == null); - } -} |
