river: rework core data structures & transactions
This commit is contained in:
parent
f5dc67cfc1
commit
be4330288d
@ -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/
|
||||
|
@ -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/
|
||||
|
@ -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/
|
||||
|
98
build.zig
98
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 {
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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;
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
|
@ -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];
|
||||
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 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,
|
||||
};
|
||||
// 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.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);
|
||||
}
|
||||
|
220
river/Output.zig
220
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 {
|
||||
|
@ -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);
|
||||
|
570
river/Root.zig
570
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();
|
||||
|
||||
{
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// 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);
|
||||
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();
|
||||
}
|
||||
|
||||
/// 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 it = self.outputs.first;
|
||||
while (it) |node| : (it = node.next) {
|
||||
if (node.data.layout_demand != null) self.pending_layout_demands += 1;
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
var output_it = root.outputs.first;
|
||||
while (output_it) |node| : (output_it = node.next) {
|
||||
const output = &node.data;
|
||||
|
||||
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;
|
||||
const output = &output_node.data;
|
||||
|
||||
if (!view.tree.node.enabled) continue;
|
||||
var focus_stack_it = output.inflight.focus_stack.iterator(.forward);
|
||||
while (focus_stack_it.next()) |view| {
|
||||
if (view.needsConfigure()) {
|
||||
view.configure();
|
||||
|
||||
if (view.shouldTrackConfigure()) {
|
||||
// Clear the serial in case this transaction is interrupting a prior one.
|
||||
view.pending_serial = null;
|
||||
|
||||
if (view.needsConfigure()) {
|
||||
view.configure();
|
||||
self.pending_configures += 1;
|
||||
|
||||
// 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;
|
||||
view.inflight_serial = null;
|
||||
|
||||
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;
|
||||
|
||||
if (view.current.output != output) {
|
||||
view.tree.node.reparent(output.layers.views);
|
||||
view.popup_tree.node.reparent(output.layers.popups);
|
||||
}
|
||||
assert(!view.destroying);
|
||||
|
||||
if (view.pending_serial != null and !view.shouldTrackConfigure()) continue;
|
||||
|
||||
// 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;
|
||||
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();
|
||||
|
@ -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);
|
||||
|
||||
|
120
river/Seat.zig
120
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;
|
||||
|
||||
|
@ -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),
|
||||
|
@ -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;
|
||||
};
|
||||
|
330
river/View.zig
330
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,
|
||||
|
||||
/// The serial sent with the currently pending configure event
|
||||
pending_serial: ?u32 = null,
|
||||
inflight: State = .{},
|
||||
inflight_focus_stack_link: wl.list.Link,
|
||||
inflight_wm_stack_link: wl.list.Link,
|
||||
|
||||
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;
|
||||
pub fn create(impl: Impl) error{OutOfMemory}!*Self {
|
||||
const view = try util.gpa.create(Self);
|
||||
errdefer util.gpa.destroy(view);
|
||||
|
||||
const initial_tags = blk: {
|
||||
const tags = output.current.tags & server.config.spawn_tagmask;
|
||||
break :blk if (tags != 0) tags else output.current.tags;
|
||||
};
|
||||
|
||||
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);
|
||||
|
||||
try SceneNodeData.attach(&self.tree.node, .{ .view = self });
|
||||
try SceneNodeData.attach(&self.popup_tree.node, .{ .view = self });
|
||||
view.tree.node.setEnabled(false);
|
||||
view.popup_tree.node.setEnabled(false);
|
||||
view.saved_surface_tree.node.setEnabled(false);
|
||||
|
||||
return self;
|
||||
try SceneNodeData.attach(&view.tree.node, .{ .view = view });
|
||||
try SceneNodeData.attach(&view.popup_tree.node, .{ .view = view });
|
||||
|
||||
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();
|
||||
|
||||
util.gpa.destroy(view);
|
||||
}
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
// 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);
|
||||
|
||||
var output_width: i32 = undefined;
|
||||
var output_height: i32 = undefined;
|
||||
destination_output.wlr_output.effectiveResolution(&output_width, &output_height);
|
||||
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));
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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()});
|
||||
pub fn unmap(view: *Self) void {
|
||||
log.debug("view '{?s}' unmapped", .{view.getTitle()});
|
||||
|
||||
self.tree.node.setEnabled(false);
|
||||
self.popup_tree.node.setEnabled(false);
|
||||
if (!view.saved_surface_tree.node.enabled) view.saveSurfaceTree();
|
||||
|
||||
if (!self.saved_surface_tree.node.enabled) self.saveSurfaceTree();
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
// 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);
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.request_activate.link.remove();
|
||||
view.request_activate.link.remove();
|
||||
|
||||
self.output.sendViewTags();
|
||||
|
||||
// Still need to arrange if fullscreened from the layout
|
||||
if (!self.current.float) self.output.arrangeViews();
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
};
|
||||
|
||||
seat.focus(it.next());
|
||||
server.root.startTransaction();
|
||||
}
|
||||
|
||||
fn filter(view: *View, filter_tags: u32) bool {
|
||||
return view.tree.node.enabled and view.pending.tags & filter_tags != 0;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
// skip the first node which is focused_node
|
||||
_ = it.next().?;
|
||||
|
||||
const to_swap = @fieldParentPtr(
|
||||
ViewStack(View).Node,
|
||||
"view",
|
||||
// Wrap around if needed
|
||||
if (it.next()) |next| next else it_wrap.next().?,
|
||||
);
|
||||
|
||||
// Dont swap when only the focused view is part of the layout
|
||||
if (focused_node == to_swap) {
|
||||
return;
|
||||
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();
|
||||
}
|
||||
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
unreachable;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -33,6 +33,6 @@ pub fn toggleFullscreen(
|
||||
const view = seat.focused.view;
|
||||
|
||||
view.pending.fullscreen = !view.pending.fullscreen;
|
||||
view.applyPending();
|
||||
server.root.applyPending();
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
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();
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
target.pending_wm_stack_link.remove();
|
||||
output.pending.wm_stack.prepend(target);
|
||||
seat.focus(target);
|
||||
server.root.applyPending();
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user