river: rework core data structures & transactions

This commit is contained in:
Isaac Freund 2023-02-24 19:28:37 +01:00
parent f5dc67cfc1
commit be4330288d
No known key found for this signature in database
GPG Key ID: 86DED400DDFD7A11
34 changed files with 1027 additions and 1543 deletions

View File

@ -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/

View File

@ -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/

View File

@ -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/

View File

@ -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 {

View File

@ -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

View File

@ -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,

View File

@ -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;
}

View File

@ -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);

View File

@ -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;

View File

@ -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();
}

View File

@ -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);
}

View File

@ -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 {

View File

@ -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);

View File

@ -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();

View File

@ -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);

View File

@ -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;

View File

@ -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),

View File

@ -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;
};

View File

@ -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();
}
}
}

View File

@ -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;

View File

@ -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);

View File

@ -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;
};

View File

@ -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();
}
}

View File

@ -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(

View File

@ -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;
},
}
}

View File

@ -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();
}

View File

@ -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 {

View File

@ -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;

View File

@ -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;
},
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -33,6 +33,6 @@ pub fn toggleFullscreen(
const view = seat.focused.view;
view.pending.fullscreen = !view.pending.fullscreen;
view.applyPending();
server.root.applyPending();
}
}

View File

@ -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;
}

View File

@ -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);
}
}