aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorIsaac Freund <mail@isaacfreund.com>2023-02-24 19:28:37 +0100
committerIsaac Freund <mail@isaacfreund.com>2023-02-28 18:28:17 +0100
commitbe4330288d199e1d2c8024c051eb6c08f58a87db (patch)
tree8ceae64e14bcc469f0401bab0f8c083c9e4ec824
parentf5dc67cfc1fad5b24b9e2d611a99eb2f3bba776c (diff)
downloadriver-be4330288d199e1d2c8024c051eb6c08f58a87db.tar.gz
river-be4330288d199e1d2c8024c051eb6c08f58a87db.tar.xz
river: rework core data structures & transactions
-rw-r--r--.builds/alpine.yml3
-rw-r--r--.builds/archlinux.yml3
-rw-r--r--.builds/freebsd.yml3
-rw-r--r--build.zig98
-rw-r--r--river/Config.zig6
-rw-r--r--river/Cursor.zig55
-rw-r--r--river/IdleInhibitorManager.zig2
-rw-r--r--river/InputManager.zig5
-rw-r--r--river/LayerSurface.zig9
-rw-r--r--river/Layout.zig26
-rw-r--r--river/LayoutDemand.zig55
-rw-r--r--river/Output.zig220
-rw-r--r--river/OutputStatus.zig25
-rw-r--r--river/Root.zig564
-rw-r--r--river/SceneNodeData.zig10
-rw-r--r--river/Seat.zig120
-rw-r--r--river/SeatStatus.zig6
-rw-r--r--river/Server.zig19
-rw-r--r--river/View.zig328
-rw-r--r--river/XdgPopup.zig17
-rw-r--r--river/XdgToplevel.zig132
-rw-r--r--river/XwaylandOverrideRedirect.zig6
-rw-r--r--river/XwaylandView.zig45
-rw-r--r--river/command/config.zig9
-rw-r--r--river/command/focus_view.zig68
-rw-r--r--river/command/layout.zig6
-rw-r--r--river/command/move.zig22
-rw-r--r--river/command/output.zig21
-rw-r--r--river/command/swap.zig79
-rw-r--r--river/command/tags.zig38
-rw-r--r--river/command/toggle_float.zig5
-rw-r--r--river/command/toggle_fullscreen.zig2
-rw-r--r--river/command/zoom.zig67
-rw-r--r--river/view_stack.zig484
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/
diff --git a/build.zig b/build.zig
index 4b696b5..e72d1ff 100644
--- a/build.zig
+++ b/build.zig
@@ -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);
- }
-}