river-layout: create and implement protocol

Replace the current layout mechanism based on passing args to a child
process and parsing it's stdout with a new wayland protocol. This much
more robust and allows for more featureful layout generators.

Co-authored-by: Isaac Freund <ifreund@ifreund.xyz>
This commit is contained in:
Leon Henrik Plickat
2020-10-02 15:53:08 +02:00
committed by Isaac Freund
parent df3e993013
commit f72656b72e
26 changed files with 1261 additions and 653 deletions

View File

@ -44,12 +44,6 @@ border_color_focused: [4]f32 = [_]f32{ 0.57647059, 0.63137255, 0.63137255, 1.0 }
/// Color of border of unfocused window in RGBA
border_color_unfocused: [4]f32 = [_]f32{ 0.34509804, 0.43137255, 0.45882353, 1.0 }, // Solarized base0
/// Amount of view padding in pixels
view_padding: u32 = 8,
/// Amount of padding arount the outer edge of the layout in pixels
outer_padding: u32 = 8,
/// Map of keymap mode name to mode id
mode_to_id: std.StringHashMap(usize),

View File

@ -533,8 +533,11 @@ pub fn enterMode(self: *Self, mode: @TagType(Mode), view: *View) void {
},
};
// Automatically float all views being moved by the pointer
if (!view.current.float) {
// Automatically float all views being moved by the pointer, if
// their dimensions are set by a layout client. 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) {
view.pending.float = true;
view.float_box = view.current.box;
view.applyPending();

197
river/Layout.zig Normal file
View File

@ -0,0 +1,197 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
// Copyright 2020 - 2021 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, either version 3 of the License, or
// (at your option) any later version.
//
// 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 Self = @This();
const std = @import("std");
const mem = std.mem;
const wlr = @import("wlroots");
const wayland = @import("wayland");
const wl = wayland.server.wl;
const river = wayland.server.river;
const util = @import("util.zig");
const Box = @import("Box.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);
layout: *river.LayoutV1,
namespace: []const u8,
output: *Output,
pub fn create(client: *wl.Client, version: u32, id: u32, output: *Output, namespace: []const u8) !void {
const layout = try river.LayoutV1.create(client, version, id);
if (namespaceInUse(namespace, output, client)) {
layout.sendNamespaceInUse();
layout.setHandler(?*c_void, handleRequestInert, null, null);
return;
}
const node = try util.gpa.create(std.TailQueue(Self).Node);
errdefer util.gpa.destroy(node);
node.data = .{
.layout = layout,
.namespace = try util.gpa.dupe(u8, namespace),
.output = output,
};
output.layouts.append(node);
layout.setHandler(*Self, handleRequest, handleDestroy, &node.data);
// If the namespace matches that of the output, set the layout as
// the active one of the output and arrange it.
if (output.layout_option.value.string) |current_layout| {
if (mem.eql(u8, namespace, mem.span(current_layout))) {
output.pending.layout = &node.data;
output.arrangeViews();
}
}
}
/// Returns true if the given namespace is already in use on the given output
/// or on another output by a different client.
fn namespaceInUse(namespace: []const u8, output: *Output, client: *wl.Client) bool {
var output_it = output.root.outputs.first;
while (output_it) |output_node| : (output_it = output_node.next) {
var layout_it = output_node.data.layouts.first;
if (output_node.data.wlr_output == output.wlr_output) {
// On this output, no other layout can have our namespace.
while (layout_it) |layout_node| : (layout_it = layout_node.next) {
if (mem.eql(u8, namespace, layout_node.data.namespace)) return true;
}
} else {
// Layouts on other outputs may share the namespace, if they come from the same client.
while (layout_it) |layout_node| : (layout_it = layout_node.next) {
if (mem.eql(u8, namespace, layout_node.data.namespace) and
client != layout_node.data.layout.getClient()) return true;
}
}
}
return false;
}
/// This exists to handle layouts that have been rendered inert (due to the
/// namespace already being in use) until the client destroys them.
fn handleRequestInert(layout: *river.LayoutV1, request: river.LayoutV1.Request, _: ?*c_void) void {
if (request == .destroy) layout.destroy();
}
/// Send a layout demand to the client
pub fn startLayoutDemand(self: *Self, views: u32) void {
log.debug(
"starting layout demand '{}' on output '{}'",
.{ self.namespace, self.output.wlr_output.name },
);
std.debug.assert(self.output.layout_demand == null);
self.output.layout_demand = LayoutDemand.init(self, views) catch {
log.err("failed starting layout demand", .{});
return;
};
const serial = self.output.layout_demand.?.serial;
// Then we let the client know that we require a layout
self.layout.sendLayoutDemand(
views,
self.output.usable_box.width,
self.output.usable_box.height,
self.output.pending.tags,
serial,
);
// And finally we advertise all visible views
var it = ViewStack(View).iter(self.output.views.first, .forward, self.output.pending.tags, Output.arrangeFilter);
while (it.next()) |view| {
self.layout.sendAdvertiseView(view.pending.tags, view.getAppId(), serial);
}
self.layout.sendAdvertiseDone(serial);
self.output.root.trackLayoutDemands();
}
fn handleRequest(layout: *river.LayoutV1, request: river.LayoutV1.Request, self: *Self) void {
switch (request) {
.destroy => layout.destroy(),
// Parameters of the layout changed. We only care about this, if the
// layout is currently in use, in which case we rearrange the output.
.parameters_changed => if (self == self.output.pending.layout) self.output.arrangeViews(),
// We receive this event when the client wants to push a view dimension proposal
// to the layout demand matching the serial.
.push_view_dimensions => |req| {
log.debug(
"layout '{}' on output '{}' pushed view dimensions: {} {} {} {}",
.{ self.namespace, self.output.wlr_output.name, req.x, req.y, req.width, req.height },
);
if (self.output.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.
if (layout_demand.serial != req.serial) return;
layout_demand.pushViewDimensions(self.output, req.x, req.y, req.width, req.height);
}
},
// We receive this event when the client wants to mark the proposed layout
// of the layout demand matching the serial as done.
.commit => |req| {
log.debug(
"layout '{}' on output '{}' commited",
.{ self.namespace, self.output.wlr_output.name },
);
if (self.output.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.
if (layout_demand.serial == req.serial) layout_demand.apply(self);
}
},
}
}
fn handleDestroy(layout: *river.LayoutV1, self: *Self) void {
log.debug(
"destroying layout '{}' on output '{}'",
.{ self.namespace, self.output.wlr_output.name },
);
// Remove layout from the list
const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self);
self.output.layouts.remove(node);
// If we are the currently active layout of an output, clean up. The output
// will always end up with no layout at this point, so we directly start the
// transaction.
if (self == self.output.pending.layout) {
self.output.pending.layout = null;
self.output.arrangeViews();
self.output.root.startTransaction();
}
util.gpa.free(self.namespace);
util.gpa.destroy(node);
}

139
river/LayoutDemand.zig Normal file
View File

@ -0,0 +1,139 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
// Copyright 2020 - 2021 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, either version 3 of the License, or
// (at your option) any later version.
//
// 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 Self = @This();
const std = @import("std");
const wlr = @import("wlroots");
const wayland = @import("wayland");
const wl = wayland.server.wl;
const zriver = wayland.server.zriver;
const util = @import("util.zig");
const Layout = @import("Layout.zig");
const Box = @import("Box.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);
const Error = error{ViewDimensionMismatch};
const timeout_ms = 1000;
serial: u32,
/// Number of views for which dimensions have not been pushed.
/// This will go negative if the client pushes too many dimensions.
views: i32,
/// Proposed view dimensions
view_boxen: []Box,
timeout_timer: *wl.EventSource,
pub fn init(layout: *Layout, views: u32) !Self {
const event_loop = layout.output.root.server.wl_server.getEventLoop();
const timeout_timer = try event_loop.addTimer(*Layout, handleTimeout, layout);
errdefer timeout_timer.remove();
try timeout_timer.timerUpdate(timeout_ms);
return Self{
.serial = layout.output.root.server.wl_server.nextSerial(),
.views = @intCast(i32, views),
.view_boxen = try util.gpa.alloc(Box, views),
.timeout_timer = timeout_timer,
};
}
pub fn deinit(self: *const Self) void {
self.timeout_timer.remove();
util.gpa.free(self.view_boxen);
}
/// Destroy the LayoutDemand on timeout.
/// All further responses to the event will simply be ignored.
fn handleTimeout(layout: *Layout) callconv(.C) c_int {
log.notice(
"layout demand for layout '{}' on output '{}' timed out",
.{ layout.namespace, layout.output.wlr_output.name },
);
layout.output.layout_demand.?.deinit();
layout.output.layout_demand = null;
layout.output.root.notifyLayoutDemandDone();
return 0;
}
/// Push a set of proposed view dimensions and position to the list
pub fn pushViewDimensions(self: *Self, output: *Output, x: i32, y: i32, width: u32, height: u32) void {
// The client pushed too many dimensions
if (self.views < 0) return;
// 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 = output.root.server.config.border_width;
self.view_boxen[self.view_boxen.len - @intCast(usize, self.views)] = .{
.x = x + output.usable_box.x + @intCast(i32, border_width),
.y = y + output.usable_box.y + @intCast(i32, border_width),
.width = if (width > 2 * border_width) width - 2 * border_width else width,
.height = if (height > 2 * border_width) height - 2 * border_width else height,
};
self.views -= 1;
}
/// Apply the proposed layout to the output
pub fn apply(self: *Self, layout: *Layout) void {
const output = layout.output;
// 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.root.notifyLayoutDemandDone();
}
// Check that the number of proposed dimensions is correct.
if (self.views != 0) {
log.err(
"proposed dimension count ({}) does not match view count ({}), aborting layout demand",
.{ -self.views + @intCast(i32, self.view_boxen.len), self.view_boxen.len },
);
layout.layout.postError(
.count_mismatch,
"number of proposed view dimensions must match number of views",
);
return;
}
// Apply proposed layout to views
var it = ViewStack(View).iter(output.views.first, .forward, output.pending.tags, Output.arrangeFilter);
var i: u32 = 0;
while (it.next()) |view| : (i += 1) {
if (view.pending.fullscreen) {
view.post_fullscreen_box = self.view_boxen[i];
} else {
view.pending.box = self.view_boxen[i];
}
view.applyConstraints();
}
std.debug.assert(i == self.view_boxen.len);
output.pending.layout = layout;
}

84
river/LayoutManager.zig Normal file
View File

@ -0,0 +1,84 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
// Copyright 2020 - 2021 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, either version 3 of the License, or
// (at your option) any later version.
//
// 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 Self = @This();
const std = @import("std");
const mem = std.mem;
const wlr = @import("wlroots");
const wayland = @import("wayland");
const wl = wayland.server.wl;
const river = wayland.server.river;
const util = @import("util.zig");
const Layout = @import("Layout.zig");
const Server = @import("Server.zig");
const Output = @import("Output.zig");
const log = std.log.scoped(.layout);
global: *wl.Global,
server_destroy: wl.Listener(*wl.Server) = wl.Listener(*wl.Server).init(handleServerDestroy),
pub fn init(self: *Self, server: *Server) !void {
self.* = .{
.global = try wl.Global.create(server.wl_server, river.LayoutManagerV1, 1, *Self, self, bind),
};
server.wl_server.addDestroyListener(&self.server_destroy);
}
fn handleServerDestroy(listener: *wl.Listener(*wl.Server), wl_server: *wl.Server) void {
const self = @fieldParentPtr(Self, "server_destroy", listener);
self.global.destroy();
}
fn bind(client: *wl.Client, self: *Self, version: u32, id: u32) callconv(.C) void {
const layout_manager = river.LayoutManagerV1.create(client, 1, id) catch {
client.postNoMemory();
log.crit("out of memory", .{});
return;
};
layout_manager.setHandler(*Self, handleRequest, null, self);
}
fn handleRequest(layout_manager: *river.LayoutManagerV1, request: river.LayoutManagerV1.Request, self: *Self) void {
switch (request) {
.destroy => layout_manager.destroy(),
.get_layout => |req| {
// Ignore if the output is inert
const wlr_output = wlr.Output.fromWlOutput(req.output) orelse return;
const output = @intToPtr(*Output, wlr_output.data);
log.debug("bind layout '{}' on output '{}'", .{ req.namespace, output.wlr_output.name });
Layout.create(
layout_manager.getClient(),
layout_manager.getVersion(),
req.id,
output,
mem.span(req.namespace),
) catch {
layout_manager.getClient().postNoMemory();
log.crit("out of memory", .{});
return;
};
},
}
}

View File

@ -36,6 +36,17 @@ pub const Value = union(enum) {
uint: u32,
fixed: wl.Fixed,
string: ?[*:0]const u8,
fn dupe(value: Value) !Value {
return switch (value) {
.string => |v| Value{ .string = if (v) |s| try util.gpa.dupeZ(u8, mem.span(s)) else null },
else => value,
};
}
fn deinit(value: *Value) void {
if (value.* == .string) if (value.string) |s| util.gpa.free(mem.span(s));
}
};
options_manager: *OptionsManager,
@ -43,24 +54,31 @@ link: wl.list.Link = undefined,
output: ?*Output,
key: [*:0]const u8,
value: Value = .unset,
value: Value,
/// Emitted whenever the value of the option changes.
update: wl.Signal(*Self) = undefined,
event: struct {
/// Emitted whenever the value of the option changes.
update: wl.Signal(*Self),
} = undefined,
handles: wl.list.Head(zriver.OptionHandleV1, null) = undefined,
pub fn create(options_manager: *OptionsManager, output: ?*Output, key: [*:0]const u8) !*Self {
/// Allocate a new option, duping the provided key and value
pub fn create(options_manager: *OptionsManager, output: ?*Output, key: [*:0]const u8, value: Value) !*Self {
const self = try util.gpa.create(Self);
errdefer util.gpa.destroy(self);
var owned_value = try value.dupe();
errdefer owned_value.deinit();
self.* = .{
.options_manager = options_manager,
.output = output,
.key = try util.gpa.dupeZ(u8, mem.span(key)),
.value = owned_value,
};
self.handles.init();
self.update.init();
self.event.update.init();
options_manager.options.append(self);
@ -83,31 +101,23 @@ pub fn set(self: *Self, value: Value) !void {
std.debug.assert(value != .unset);
if (self.value != .unset and meta.activeTag(value) != meta.activeTag(self.value)) return;
if (self.value == .unset and value == .string) {
self.value = .{
.string = if (value.string) |s| (try util.gpa.dupeZ(u8, mem.span(s))).ptr else null,
};
} else if (self.value == .string and
if (switch (self.value) {
.unset => true,
// TODO: std.mem needs a good way to compare optional sentinel pointers
(((self.value.string == null) != (value.string == null)) or
(self.value.string != null and value.string != null and
std.cstr.cmp(self.value.string.?, value.string.?) != 0)))
{
const owned_string = if (value.string) |s| (try util.gpa.dupeZ(u8, mem.span(s))).ptr else null;
if (self.value.string) |s| util.gpa.free(mem.span(s));
self.value.string = owned_string;
} else if (self.value == .unset or (self.value != .string and !std.meta.eql(self.value, value))) {
self.value = value;
} else {
// The value was not changed
return;
.string => ((self.value.string == null) != (value.string == null)) or
(self.value.string != null and value.string != null and
std.cstr.cmp(self.value.string.?, value.string.?) != 0),
else => !std.meta.eql(self.value, value),
}) {
self.value.deinit();
self.value = try value.dupe();
var it = self.handles.iterator(.forward);
while (it.next()) |handle| self.sendValue(handle);
// Call listeners, if any.
self.event.update.emit(self);
}
var it = self.handles.iterator(.forward);
while (it.next()) |handle| self.sendValue(handle);
// Call listeners, if any.
self.update.emit(self);
}
fn sendValue(self: Self, handle: *zriver.OptionHandleV1) void {

View File

@ -87,7 +87,7 @@ fn handleRequest(
break option;
}
} else
Option.create(self, output, req.key) catch {
Option.create(self, output, req.key, .unset) catch {
options_manager.getClient().postNoMemory();
return;
};

View File

@ -31,6 +31,8 @@ const util = @import("util.zig");
const Box = @import("Box.zig");
const LayerSurface = @import("LayerSurface.zig");
const Layout = @import("Layout.zig");
const LayoutDemand = @import("LayoutDemand.zig");
const Root = @import("Root.zig");
const View = @import("View.zig");
const ViewStack = @import("view_stack.zig").ViewStack;
@ -41,9 +43,18 @@ const Option = @import("Option.zig");
const State = struct {
/// A bit field of focused tags
tags: u32,
};
const log = std.log.scoped(.layout);
/// 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,
};
root: *Root,
wlr_output: *wlr.Output,
@ -63,16 +74,11 @@ views: ViewStack(View) = .{},
current: State = State{ .tags = 1 << 0 },
pending: State = State{ .tags = 1 << 0 },
/// Number of views in "main" section of the screen.
main_count: u32 = 1,
/// The currently active LayoutDemand
layout_demand: ?LayoutDemand = null,
/// Percentage of the total screen that the "main" section takes up.
main_factor: f64 = 0.6,
/// Current layout of the output. If it is "full", river will use the full
/// layout. Otherwise river assumes it contains a string which, when executed
/// with sh, will result in a layout.
layout: []const u8,
/// List of all layouts
layouts: std.TailQueue(Layout) = .{},
/// Determines where new views will be attached to the view stack.
attach_mode: AttachMode = .top,
@ -88,8 +94,11 @@ enable: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleEnable),
frame: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleFrame),
mode: wl.Listener(*wlr.Output) = wl.Listener(*wlr.Output).init(handleMode),
// Listeners for options
layout_option: *Option,
/// Listeners for options
output_title: wl.Listener(*Option) = wl.Listener(*Option).init(handleTitleChange),
layout_change: wl.Listener(*Option) = wl.Listener(*Option).init(handleLayoutChange),
pub fn init(self: *Self, root: *Root, wlr_output: *wlr.Output) !void {
// Some backends don't have modes. DRM+KMS does, and we need to set a mode
@ -103,14 +112,11 @@ pub fn init(self: *Self, root: *Root, wlr_output: *wlr.Output) !void {
try wlr_output.commit();
}
const layout = try std.mem.dupe(util.gpa, u8, "full");
errdefer util.gpa.free(layout);
self.* = .{
.root = root,
.wlr_output = wlr_output,
.layout = layout,
.usable_box = undefined,
.layout_option = undefined,
};
wlr_output.data = @ptrToInt(self);
@ -146,9 +152,22 @@ pub fn init(self: *Self, root: *Root, wlr_output: *wlr.Output) !void {
};
}
// Set the default title of this output
var buf: ["river - ".len + wlr_output.name.len + 1]u8 = undefined;
const default_title = fmt.bufPrintZ(&buf, "river - {}", .{mem.spanZ(&wlr_output.name)}) catch unreachable;
try self.defaultOption("output_title", .{ .string = default_title.ptr }, &self.output_title);
self.setTitle(default_title);
// Create all default output options
const options_manager = &root.server.options_manager;
self.layout_option = try Option.create(options_manager, self, "layout", .{ .string = null });
const title_option = try Option.create(options_manager, self, "output_title", .{ .string = default_title.ptr });
_ = try Option.create(options_manager, self, "main_amount", .{ .uint = 1 });
_ = try Option.create(options_manager, self, "main_factor", .{ .fixed = wl.Fixed.fromDouble(0.6) });
_ = try Option.create(options_manager, self, "view_padding", .{ .uint = 10 });
_ = try Option.create(options_manager, self, "outer_padding", .{ .uint = 10 });
self.layout_option.event.update.add(&self.layout_change);
title_option.event.update.add(&self.output_title);
}
pub fn getLayer(self: *Self, layer: zwlr.LayerShellV1.Layer) *std.TailQueue(LayerSurface) {
@ -160,157 +179,50 @@ pub fn sendViewTags(self: Self) void {
while (it) |node| : (it = node.next) node.data.sendViewTags();
}
/// The single build in layout, which makes all views use the maximum available
/// space.
fn layoutFull(self: *Self, visible_count: u32) void {
const border_width = self.root.server.config.border_width;
const view_padding = self.root.server.config.view_padding;
const outer_padding = self.root.server.config.outer_padding;
const xy_offset = outer_padding + border_width + view_padding;
var full_box: Box = .{
.x = self.usable_box.x + @intCast(i32, xy_offset),
.y = self.usable_box.y + @intCast(i32, xy_offset),
.width = self.usable_box.width - (2 * xy_offset),
.height = self.usable_box.height - (2 * xy_offset),
};
var it = ViewStack(View).iter(self.views.first, .forward, self.pending.tags, arrangeFilter);
while (it.next()) |view| {
view.pending.box = full_box;
view.applyConstraints();
}
}
const LayoutError = error{
BadExitCode,
WrongViewCount,
};
/// Parse 4 integers separated by spaces into a Box
fn parseBox(buffer: []const u8) !Box {
var it = std.mem.split(buffer, " ");
const box = Box{
.x = try std.fmt.parseInt(i32, it.next() orelse return error.NotEnoughArguments, 10),
.y = try std.fmt.parseInt(i32, it.next() orelse return error.NotEnoughArguments, 10),
.width = try std.fmt.parseInt(u32, it.next() orelse return error.NotEnoughArguments, 10),
.height = try std.fmt.parseInt(u32, it.next() orelse return error.NotEnoughArguments, 10),
};
if (it.next() != null) return error.TooManyArguments;
return box;
}
test "parse window configuration" {
const testing = @import("std").testing;
const box = try parseBox("5 10 100 200");
testing.expect(box.x == 5);
testing.expect(box.y == 10);
testing.expect(box.width == 100);
testing.expect(box.height == 200);
}
/// Execute an external layout function, parse its output and apply the layout
/// to the output.
fn layoutExternal(self: *Self, visible_count: u32) !void {
const config = self.root.server.config;
const xy_offset = @intCast(i32, config.border_width + config.outer_padding + config.view_padding);
const delta_size = (config.border_width + config.view_padding) * 2;
const layout_width = @intCast(u32, self.usable_box.width) - config.outer_padding * 2;
const layout_height = @intCast(u32, self.usable_box.height) - config.outer_padding * 2;
var arena = std.heap.ArenaAllocator.init(util.gpa);
defer arena.deinit();
// Assemble command
const layout_command = try std.fmt.allocPrint0(&arena.allocator, "{} {} {} {d} {} {}", .{
self.layout,
visible_count,
self.main_count,
self.main_factor,
layout_width,
layout_height,
});
const cmd = [_:null]?[*:0]const u8{ "/bin/sh", "-c", layout_command, null };
const stdout_pipe = try std.os.pipe();
const pid = try std.os.fork();
if (pid == 0) {
std.os.dup2(stdout_pipe[1], std.os.STDOUT_FILENO) catch c._exit(1);
std.os.close(stdout_pipe[0]);
std.os.close(stdout_pipe[1]);
std.os.execveZ("/bin/sh", &cmd, std.c.environ) catch c._exit(1);
}
std.os.close(stdout_pipe[1]);
const stdout = std.fs.File{ .handle = stdout_pipe[0] };
defer stdout.close();
// TODO abort after a timeout
const ret = std.os.waitpid(pid, 0);
if (!std.os.WIFEXITED(ret.status) or std.os.WEXITSTATUS(ret.status) != 0)
return LayoutError.BadExitCode;
const buffer = try stdout.inStream().readAllAlloc(&arena.allocator, 1024);
// Parse layout command output
var view_boxen = std.ArrayList(Box).init(&arena.allocator);
var parse_it = std.mem.split(buffer, "\n");
while (parse_it.next()) |token| {
if (std.mem.eql(u8, token, "")) break;
var box = try parseBox(token);
box.x += self.usable_box.x + xy_offset;
box.y += self.usable_box.y + xy_offset;
if (box.width > delta_size) box.width -= delta_size;
if (box.height > delta_size) box.height -= delta_size;
try view_boxen.append(box);
}
if (view_boxen.items.len != visible_count) return LayoutError.WrongViewCount;
// Apply window configuration to views
var i: u32 = 0;
var view_it = ViewStack(View).iter(self.views.first, .forward, self.pending.tags, arrangeFilter);
while (view_it.next()) |view| : (i += 1) {
view.pending.box = view_boxen.items[i];
view.applyConstraints();
}
}
fn arrangeFilter(view: *View, filter_tags: u32) bool {
pub fn arrangeFilter(view: *View, filter_tags: u32) bool {
return !view.destroying and !view.pending.float and
!view.pending.fullscreen and view.pending.tags & filter_tags != 0;
view.pending.tags & filter_tags != 0;
}
/// Arrange all views on the output for the current layout. Modifies only
/// pending state, the changes are not appplied until a transaction is started
/// and completed.
/// 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 == &self.root.noop_output) return;
// Count up views that will be arranged by the layout
var layout_count: u32 = 0;
var it = ViewStack(View).iter(self.views.first, .forward, self.pending.tags, arrangeFilter);
while (it.next() != null) layout_count += 1;
// If there is already an active layout demand, discard it.
if (self.layout_demand) |demand| {
demand.deinit();
self.layout_demand = null;
}
// If the usable area has a zero dimension, trying to arrange the layout
// would cause an underflow and is pointless anyway.
if (layout_count == 0 or self.usable_box.width == 0 or self.usable_box.height == 0) return;
// 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;
if (std.mem.eql(u8, self.layout, "full")) return layoutFull(self, layout_count);
// 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;
self.layoutExternal(layout_count) catch |err| {
switch (err) {
LayoutError.BadExitCode => log.err("layout command exited with non-zero return code", .{}),
LayoutError.WrongViewCount => log.err("mismatch between window configuration and visible window counts", .{}),
else => log.err("failed to use external layout: {}", .{err}),
}
log.err("falling back to internal layout", .{});
self.layoutFull(layout_count);
};
// 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
@ -547,9 +459,11 @@ fn handleDestroy(listener: *wl.Listener(*wlr.Output), wlr_output: *wlr.Output) v
self.frame.link.remove();
self.mode.link.remove();
// Cleanup the layout demand, if any
if (self.layout_demand) |demand| demand.deinit();
// Free all memory and clean up the wlr.Output
self.wlr_output.data = undefined;
util.gpa.free(self.layout);
const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self);
util.gpa.destroy(node);
@ -595,20 +509,20 @@ pub fn setTitle(self: *Self, title: [*:0]const u8) void {
}
}
/// Create an option for this output, attach a listener which is called when
/// the option changed and initialize with a default value. Note that the
/// listener is called once through this function.
fn defaultOption(
self: *Self,
key: [*:0]const u8,
value: Option.Value,
listener: *wl.Listener(*Option),
) !void {
const option = try Option.create(&self.root.server.options_manager, self, key);
option.update.add(listener);
try option.set(value);
}
fn handleTitleChange(listener: *wl.Listener(*Option), option: *Option) void {
if (option.value.string) |title| option.output.?.setTitle(title);
}
fn handleLayoutChange(listener: *wl.Listener(*Option), option: *Option) void {
// The user changed the layout namespace of this output. Try to find a
// matching layout.
const output = option.output.?;
output.pending.layout = if (option.value.string) |namespace| blk: {
var layout_it = output.layouts.first;
break :blk while (layout_it) |node| : (layout_it = node.next) {
if (mem.eql(u8, mem.span(namespace), node.data.namespace)) break &node.data;
} else null;
} else null;
output.arrangeViews();
output.root.startTransaction();
}

View File

@ -19,6 +19,7 @@ const Self = @This();
const build_options = @import("build_options");
const std = @import("std");
const assert = std.debug.assert;
const wlr = @import("wlroots");
const wl = @import("wayland").server.wl;
@ -76,10 +77,11 @@ xwayland_unmanaged_views: if (build_options.xwayland)
else
void = if (build_options.xwayland) .{},
/// 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,
@ -89,12 +91,16 @@ pub fn init(self: *Self, server: *Server) !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();
self.* = .{
.server = server,
.output_layout = output_layout,
.output_manager = try wlr.OutputManagerV1.create(server.wl_server),
.power_manager = try wlr.OutputPowerManagerV1.create(server.wl_server),
.transaction_timer = try self.server.wl_server.getEventLoop().addTimer(*Self, handleTimeout, self),
.transaction_timer = transaction_timer,
.noop_output = undefined,
};
@ -249,9 +255,33 @@ pub fn arrangeAll(self: *Self) void {
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;
}
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();
}
/// 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
self.pending_configures = 0;
@ -263,10 +293,7 @@ pub fn startTransaction(self: *Self) void {
while (view_it) |view_node| : (view_it = view_node.next) {
const view = &view_node.view;
if (view.destroying) {
if (view.saved_buffers.items.len == 0) view.saveBuffers();
continue;
}
if (view.destroying) continue;
if (view.shouldTrackConfigure()) {
// Clear the serial in case this transaction is interrupting a prior one.
@ -310,7 +337,7 @@ pub fn startTransaction(self: *Self) void {
}
}
fn handleTimeout(self: *Self) callconv(.C) c_int {
fn handleTransactionTimeout(self: *Self) callconv(.C) c_int {
std.log.scoped(.transaction).err("timeout occurred, some imperfect frames may be shown", .{});
self.pending_configures = 0;
@ -333,7 +360,7 @@ pub fn notifyConfigured(self: *Self) void {
/// 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 {
std.debug.assert(self.pending_configures == 0);
assert(self.pending_configures == 0);
// Iterate over all views of all outputs
var output_it = self.outputs.first;

View File

@ -31,6 +31,7 @@ const Control = @import("Control.zig");
const DecorationManager = @import("DecorationManager.zig");
const InputManager = @import("InputManager.zig");
const LayerSurface = @import("LayerSurface.zig");
const LayoutManager = @import("LayoutManager.zig");
const Output = @import("Output.zig");
const Root = @import("Root.zig");
const StatusManager = @import("StatusManager.zig");
@ -66,6 +67,7 @@ config: Config,
control: Control,
status_manager: StatusManager,
options_manager: OptionsManager,
layout_manager: LayoutManager,
pub fn init(self: *Self) !void {
self.wl_server = try wl.Server.create();
@ -119,6 +121,7 @@ pub fn init(self: *Self) !void {
try self.input_manager.init(self);
try self.control.init(self);
try self.status_manager.init(self);
try self.layout_manager.init(self);
// These all free themselves when the wl_server is destroyed
_ = try wlr.DataDeviceManager.create(self.wl_server);

View File

@ -117,6 +117,12 @@ saved_buffers: std.ArrayList(SavedBuffer),
/// view returns to floating mode.
float_box: Box = undefined,
/// While a view is in fullscreen, it is still arranged if a layout is active but
/// the resulting dimensions are stored here instead of being applied to the view's
/// state. This allows us to avoid an arrange when the view returns from fullscreen
/// and for more intuitive behavior if there is no active layout for the output.
post_fullscreen_box: Box = undefined,
/// The current opacity of this view
opacity: f32,
@ -194,19 +200,19 @@ pub fn applyPending(self: *Self) void {
if (self.current.float != self.pending.float)
arrange_output = true;
// If switching from float to something else save the dimensions
if ((self.current.float and !self.pending.float) or
(self.current.float and !self.current.fullscreen and self.pending.fullscreen))
// If switching from float to non-float, save the dimensions
if (self.current.float and !self.pending.float)
self.float_box = self.current.box;
// If switching from something else to float restore the dimensions
if ((!self.current.float and self.pending.float) or
(self.current.fullscreen and !self.pending.fullscreen and self.pending.float))
// If switching from non-float to float, apply the saved float dimensions
if (!self.current.float and self.pending.float)
self.pending.box = self.float_box;
// If switching to fullscreen set the dimensions to the full area of the output
// and turn the view fully opaque
if (!self.current.fullscreen and self.pending.fullscreen) {
self.post_fullscreen_box = self.current.box;
self.pending.target_opacity = 1.0;
const layout_box = self.output.root.output_layout.getBox(self.output.wlr_output).?;
self.pending.box = .{
@ -218,10 +224,7 @@ pub fn applyPending(self: *Self) void {
}
if (self.current.fullscreen and !self.pending.fullscreen) {
// If switching from fullscreen to layout, arrange the output to get
// assigned the proper size.
if (!self.pending.float)
arrange_output = true;
self.pending.box = self.post_fullscreen_box;
// Restore configured opacity
self.pending.target_opacity = if (self.pending.focus > 0)
@ -317,11 +320,15 @@ pub fn sendToOutput(self: *Self, destination_output: *Output) void {
self.output.sendViewTags();
destination_output.sendViewTags();
self.surface.?.sendLeave(self.output.wlr_output);
self.surface.?.sendEnter(destination_output.wlr_output);
if (self.surface) |surface| {
surface.sendLeave(self.output.wlr_output);
surface.sendEnter(destination_output.wlr_output);
self.foreign_toplevel_handle.?.outputLeave(self.output.wlr_output);
self.foreign_toplevel_handle.?.outputEnter(destination_output.wlr_output);
// Must be present if surface is non-null indicating that the view
// is mapped.
self.foreign_toplevel_handle.?.outputLeave(self.output.wlr_output);
self.foreign_toplevel_handle.?.outputEnter(destination_output.wlr_output);
}
self.output = destination_output;
}
@ -488,6 +495,7 @@ pub fn unmap(self: *Self) void {
log.debug("view '{}' unmapped", .{self.getTitle()});
self.destroying = true;
if (self.saved_buffers.items.len == 0) self.saveBuffers();
if (self.opacity_timer != null) {
self.killOpacityTimer();

View File

@ -182,6 +182,10 @@ fn handleMap(listener: *wl.Listener(*wlr.XdgSurface), xdg_surface: *wlr.XdgSurfa
view.float_box.y = std.math.max(0, @divTrunc(@intCast(i32, view.output.usable_box.height) -
@intCast(i32, view.float_box.height), 2));
// 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;
const state = &toplevel.current;
const has_fixed_size = state.min_width != 0 and state.min_height != 0 and
(state.min_width == state.max_width or state.min_height == state.max_height);
@ -296,14 +300,16 @@ fn handleRequestMove(
) void {
const self = @fieldParentPtr(Self, "request_move", listener);
const seat = @intToPtr(*Seat, event.seat.seat.data);
if (self.view.pending.float) seat.cursor.enterMode(.move, self.view);
if (self.view.pending.float or self.view.output.current.layout == null)
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) seat.cursor.enterMode(.resize, self.view);
if (self.view.pending.float or self.view.output.current.layout == null)
seat.cursor.enterMode(.resize, self.view);
}
/// Called when the client sets / updates its title

View File

@ -56,14 +56,10 @@ const str_to_impl_fn = [_]struct {
.{ .name = "focus-output", .impl = @import("command/focus_output.zig").focusOutput },
.{ .name = "focus-follows-cursor", .impl = @import("command/focus_follows_cursor.zig").focusFollowsCursor },
.{ .name = "focus-view", .impl = @import("command/focus_view.zig").focusView },
.{ .name = "layout", .impl = @import("command/layout.zig").layout },
.{ .name = "map", .impl = @import("command/map.zig").map },
.{ .name = "map-pointer", .impl = @import("command/map.zig").mapPointer },
.{ .name = "mod-main-count", .impl = @import("command/mod_main_count.zig").modMainCount },
.{ .name = "mod-main-factor", .impl = @import("command/mod_main_factor.zig").modMainFactor },
.{ .name = "move", .impl = @import("command/move.zig").move },
.{ .name = "opacity", .impl = @import("command/opacity.zig").opacity },
.{ .name = "outer-padding", .impl = @import("command/config.zig").outerPadding },
.{ .name = "resize", .impl = @import("command/move.zig").resize },
.{ .name = "send-to-output", .impl = @import("command/send_to_output.zig").sendToOutput },
.{ .name = "set-focused-tags", .impl = @import("command/tags.zig").setFocusedTags },
@ -79,7 +75,6 @@ const str_to_impl_fn = [_]struct {
.{ .name = "toggle-view-tags", .impl = @import("command/tags.zig").toggleViewTags },
.{ .name = "unmap", .impl = @import("command/map.zig").unmap },
.{ .name = "unmap-pointer", .impl = @import("command/map.zig").unmapPointer },
.{ .name = "view-padding", .impl = @import("command/config.zig").viewPadding },
.{ .name = "xcursor-theme", .impl = @import("command/xcursor_theme.zig").xcursorTheme },
.{ .name = "zoom", .impl = @import("command/zoom.zig").zoom },
};

View File

@ -35,36 +35,6 @@ pub fn borderWidth(
server.root.startTransaction();
}
pub fn viewPadding(
allocator: *std.mem.Allocator,
seat: *Seat,
args: []const []const u8,
out: *?[]const u8,
) Error!void {
if (args.len < 2) return Error.NotEnoughArguments;
if (args.len > 2) return Error.TooManyArguments;
const server = seat.input_manager.server;
server.config.view_padding = try std.fmt.parseInt(u32, args[1], 10);
server.root.arrangeAll();
server.root.startTransaction();
}
pub fn outerPadding(
allocator: *std.mem.Allocator,
seat: *Seat,
args: []const []const u8,
out: *?[]const u8,
) Error!void {
if (args.len < 2) return Error.NotEnoughArguments;
if (args.len > 2) return Error.TooManyArguments;
const server = seat.input_manager.server;
server.config.outer_padding = try std.fmt.parseInt(u32, args[1], 10);
server.root.arrangeAll();
server.root.startTransaction();
}
pub fn backgroundColor(
allocator: *std.mem.Allocator,
seat: *Seat,

View File

@ -1,38 +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, either version 3 of the License, or
// (at your option) any later version.
//
// 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 std = @import("std");
const util = @import("../util.zig");
const Error = @import("../command.zig").Error;
const Seat = @import("../Seat.zig");
pub fn layout(
allocator: *std.mem.Allocator,
seat: *Seat,
args: []const []const u8,
out: *?[]const u8,
) Error!void {
if (args.len < 2) return Error.NotEnoughArguments;
util.gpa.free(seat.focused_output.layout);
seat.focused_output.layout = try std.mem.join(util.gpa, " ", args[1..]);
seat.focused_output.arrangeViews();
seat.input_manager.server.root.startTransaction();
}

View File

@ -1,38 +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, either version 3 of the License, or
// (at your option) any later version.
//
// 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 std = @import("std");
const Error = @import("../command.zig").Error;
const Seat = @import("../Seat.zig");
/// Modify the number of main views
pub fn modMainCount(
allocator: *std.mem.Allocator,
seat: *Seat,
args: []const []const u8,
out: *?[]const u8,
) Error!void {
if (args.len < 2) return Error.NotEnoughArguments;
if (args.len > 2) return Error.TooManyArguments;
const delta = try std.fmt.parseInt(i32, args[1], 10);
const output = seat.focused_output;
output.main_count = @intCast(u32, std.math.max(0, @intCast(i32, output.main_count) + delta));
output.arrangeViews();
output.root.startTransaction();
}

View File

@ -1,41 +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, either version 3 of the License, or
// (at your option) any later version.
//
// 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 std = @import("std");
const Error = @import("../command.zig").Error;
const Seat = @import("../Seat.zig");
/// Modify the percent of the width of the screen that the main views occupy.
pub fn modMainFactor(
allocator: *std.mem.Allocator,
seat: *Seat,
args: []const []const u8,
out: *?[]const u8,
) Error!void {
if (args.len < 2) return Error.NotEnoughArguments;
if (args.len > 2) return Error.TooManyArguments;
const delta = try std.fmt.parseFloat(f64, args[1]);
const output = seat.focused_output;
const new_main_factor = std.math.min(std.math.max(output.main_factor + delta, 0.05), 0.95);
if (new_main_factor != output.main_factor) {
output.main_factor = new_main_factor;
output.arrangeViews();
output.root.startTransaction();
}
}

View File

@ -134,8 +134,13 @@ pub fn resize(
}
fn apply(view: *View) void {
// Set the view to floating but keep the position and dimensions
view.pending.float = true;
// Set the view to floating but keep the position and dimensions, if their
// dimensions are set by a layout client. If however the views are
// unarranged, leave them as non-floating so the next active layout can
// affect them.
if (view.output.current.layout != null)
view.pending.float = true;
view.float_box = view.pending.box;
view.applyPending();

View File

@ -33,6 +33,12 @@ pub fn toggleFloat(
if (seat.focused == .view) {
const view = seat.focused.view;
// 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.current.layout == null)
return;
// Don't float fullscreen views
if (view.pending.fullscreen) return;