2020-06-16 12:48:30 +00:00
|
|
|
// This file is part of river, a dynamic tiling wayland compositor.
|
|
|
|
//
|
2020-11-11 19:30:21 +00:00
|
|
|
// Copyright 2020 The River Developers
|
2020-06-16 12:48:30 +00:00
|
|
|
//
|
|
|
|
// 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/>.
|
2020-10-02 13:53:08 +00:00
|
|
|
//
|
|
|
|
|
|
|
|
//
|
|
|
|
// This is an implementation of the default "tiled" layout of dwm and the
|
|
|
|
// 3 other orientations thereof. This code is written with the left
|
|
|
|
// orientation in mind and then the input/output values are adjusted to apply
|
|
|
|
// the necessary transformations to derive the other 3.
|
|
|
|
//
|
|
|
|
// With 4 views and one main, the left layout looks something like this:
|
|
|
|
//
|
|
|
|
// +-----------------------+------------+
|
|
|
|
// | | |
|
|
|
|
// | | |
|
|
|
|
// | | |
|
|
|
|
// | +------------+
|
|
|
|
// | | |
|
|
|
|
// | | |
|
|
|
|
// | | |
|
|
|
|
// | +------------+
|
|
|
|
// | | |
|
|
|
|
// | | |
|
|
|
|
// | | |
|
|
|
|
// +-----------------------+------------+
|
|
|
|
//
|
2020-06-16 12:48:30 +00:00
|
|
|
|
|
|
|
const std = @import("std");
|
2020-10-02 13:53:08 +00:00
|
|
|
const wayland = @import("wayland");
|
|
|
|
const wl = wayland.client.wl;
|
|
|
|
const zriver = wayland.client.zriver;
|
|
|
|
const river = wayland.client.river;
|
|
|
|
|
|
|
|
const gpa = std.heap.c_allocator;
|
|
|
|
|
|
|
|
const Context = struct {
|
|
|
|
running: bool = true,
|
|
|
|
layout_manager: ?*river.LayoutManagerV1 = null,
|
|
|
|
options_manager: ?*zriver.OptionsManagerV1 = null,
|
|
|
|
outputs: std.TailQueue(Output) = .{},
|
|
|
|
|
|
|
|
pub fn addOutput(self: *Context, registry: *wl.Registry, name: u32) !void {
|
|
|
|
const output = try registry.bind(name, wl.Output, 3);
|
|
|
|
const node = try gpa.create(std.TailQueue(Output).Node);
|
|
|
|
node.data.init(self, output);
|
|
|
|
self.outputs.append(node);
|
|
|
|
}
|
2020-06-16 12:48:30 +00:00
|
|
|
|
2020-10-02 13:53:08 +00:00
|
|
|
pub fn destroyAllOutputs(self: *Context) void {
|
|
|
|
while (self.outputs.pop()) |node| {
|
|
|
|
node.data.deinit();
|
|
|
|
gpa.destroy(node);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn configureAllOutputs(self: *Context) void {
|
|
|
|
var it = self.outputs.first;
|
|
|
|
while (it) |node| : (it = node.next) {
|
|
|
|
node.data.configure(self);
|
|
|
|
}
|
|
|
|
}
|
2020-06-16 12:48:30 +00:00
|
|
|
};
|
|
|
|
|
2020-10-02 13:53:08 +00:00
|
|
|
const Option = struct {
|
|
|
|
pub const Value = union(enum) {
|
|
|
|
unset: void,
|
|
|
|
double: f64,
|
|
|
|
uint: u32,
|
|
|
|
};
|
|
|
|
|
|
|
|
handle: ?*zriver.OptionHandleV1 = null,
|
|
|
|
value: Value = .unset,
|
|
|
|
output: *Output = undefined,
|
|
|
|
|
|
|
|
pub fn init(self: *Option, output: *Output, comptime key: [*:0]const u8, initial: Value) !void {
|
|
|
|
self.* = .{
|
|
|
|
.value = initial,
|
|
|
|
.output = output,
|
|
|
|
.handle = try output.context.options_manager.?.getOptionHandle(
|
|
|
|
key,
|
|
|
|
output.output,
|
|
|
|
),
|
|
|
|
};
|
|
|
|
self.handle.?.setListener(*Option, optionListener, self) catch |err| {
|
|
|
|
self.handle.?.destroy();
|
|
|
|
self.handle = null;
|
|
|
|
return err;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn deinit(self: *Option) void {
|
|
|
|
if (self.handle) |handle| handle.destroy();
|
|
|
|
}
|
|
|
|
|
|
|
|
fn optionListener(handle: *zriver.OptionHandleV1, event: zriver.OptionHandleV1.Event, self: *Option) void {
|
|
|
|
switch (event) {
|
|
|
|
.unset => switch (self.value) {
|
|
|
|
.uint => handle.setUintValue(self.value.uint),
|
|
|
|
.double => handle.setFixedValue(wl.Fixed.fromDouble(self.value.double)),
|
|
|
|
else => unreachable,
|
|
|
|
},
|
|
|
|
.int_value => {},
|
|
|
|
.uint_value => |data| self.value = .{ .uint = data.value },
|
|
|
|
.fixed_value => |data| self.value = .{ .double = data.value.toDouble() },
|
|
|
|
.string_value => {},
|
|
|
|
}
|
|
|
|
if (self.output.top.layout) |layout| layout.parametersChanged();
|
|
|
|
if (self.output.right.layout) |layout| layout.parametersChanged();
|
|
|
|
if (self.output.bottom.layout) |layout| layout.parametersChanged();
|
|
|
|
if (self.output.left.layout) |layout| layout.parametersChanged();
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn getValueOrElse(self: *Option, comptime T: type, comptime otherwise: T) T {
|
|
|
|
switch (T) {
|
|
|
|
u32 => return if (self.value == .uint) self.value.uint else otherwise,
|
|
|
|
f64 => return if (self.value == .double) self.value.double else otherwise,
|
|
|
|
else => @compileError("Unsupported type for Option.getValueOrElse()"),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const Output = struct {
|
|
|
|
context: *Context,
|
|
|
|
output: *wl.Output,
|
|
|
|
|
|
|
|
top: Layout = undefined,
|
|
|
|
right: Layout = undefined,
|
|
|
|
bottom: Layout = undefined,
|
|
|
|
left: Layout = undefined,
|
|
|
|
|
|
|
|
main_amount: Option = .{},
|
|
|
|
main_factor: Option = .{},
|
|
|
|
view_padding: Option = .{},
|
|
|
|
outer_padding: Option = .{},
|
|
|
|
|
|
|
|
configured: bool = false,
|
|
|
|
|
|
|
|
pub fn init(self: *Output, context: *Context, wl_output: *wl.Output) void {
|
|
|
|
self.* = .{
|
|
|
|
.output = wl_output,
|
|
|
|
.context = context,
|
|
|
|
};
|
|
|
|
self.configure(context);
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn deinit(self: *Output) void {
|
|
|
|
self.output.release();
|
|
|
|
|
|
|
|
if (self.configured) {
|
|
|
|
self.top.deinit();
|
|
|
|
self.right.deinit();
|
|
|
|
self.bottom.deinit();
|
|
|
|
self.left.deinit();
|
|
|
|
|
|
|
|
self.main_amount.deinit();
|
|
|
|
self.main_factor.deinit();
|
|
|
|
self.view_padding.deinit();
|
|
|
|
self.outer_padding.deinit();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn configure(self: *Output, context: *Context) void {
|
|
|
|
if (self.configured) return;
|
|
|
|
if (context.layout_manager == null) return;
|
|
|
|
if (context.options_manager == null) return;
|
|
|
|
|
|
|
|
self.configured = true;
|
2020-06-16 15:06:24 +00:00
|
|
|
|
2020-10-02 13:53:08 +00:00
|
|
|
self.main_amount.init(self, "main_amount", .{ .uint = 1 }) catch {};
|
|
|
|
self.main_factor.init(self, "main_factor", .{ .double = 0.6 }) catch {};
|
|
|
|
self.view_padding.init(self, "view_padding", .{ .uint = 10 }) catch {};
|
|
|
|
self.outer_padding.init(self, "outer_padding", .{ .uint = 10 }) catch {};
|
|
|
|
|
|
|
|
self.top.init(self, .top) catch {};
|
|
|
|
self.right.init(self, .right) catch {};
|
|
|
|
self.bottom.init(self, .bottom) catch {};
|
|
|
|
self.left.init(self, .left) catch {};
|
|
|
|
}
|
|
|
|
};
|
2020-06-16 12:48:30 +00:00
|
|
|
|
2020-10-02 13:53:08 +00:00
|
|
|
const Layout = struct {
|
|
|
|
output: *Output,
|
|
|
|
layout: ?*river.LayoutV1,
|
|
|
|
orientation: Orientation,
|
2020-06-16 12:48:30 +00:00
|
|
|
|
2020-10-02 13:53:08 +00:00
|
|
|
const Orientation = enum {
|
|
|
|
top,
|
|
|
|
right,
|
|
|
|
bottom,
|
|
|
|
left,
|
2020-06-16 12:48:30 +00:00
|
|
|
};
|
2020-10-02 13:53:08 +00:00
|
|
|
|
|
|
|
pub fn init(self: *Layout, output: *Output, orientation: Orientation) !void {
|
|
|
|
self.output = output;
|
|
|
|
self.orientation = orientation;
|
|
|
|
self.layout = try output.context.layout_manager.?.getLayout(
|
|
|
|
self.output.output,
|
|
|
|
self.getNamespace(),
|
|
|
|
);
|
|
|
|
self.layout.?.setListener(*Layout, layoutListener, self) catch |err| {
|
|
|
|
self.layout.?.destroy();
|
|
|
|
self.layout = null;
|
|
|
|
return err;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
fn getNamespace(self: *Layout) [*:0]const u8 {
|
|
|
|
return switch (self.orientation) {
|
|
|
|
.top => "tile-top",
|
|
|
|
.right => "tile-right",
|
|
|
|
.bottom => "tile-bottom",
|
|
|
|
.left => "tile-left",
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn deinit(self: *Layout) void {
|
|
|
|
if (self.layout) |layout| {
|
|
|
|
layout.destroy();
|
|
|
|
self.layout = null;
|
2020-06-16 12:48:30 +00:00
|
|
|
}
|
2020-10-02 13:53:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn layoutListener(layout: *river.LayoutV1, event: river.LayoutV1.Event, self: *Layout) void {
|
|
|
|
switch (event) {
|
|
|
|
.namespace_in_use => {
|
|
|
|
std.debug.warn("{}: Namespace already in use.\n", .{self.getNamespace()});
|
|
|
|
self.deinit();
|
|
|
|
},
|
|
|
|
|
|
|
|
.layout_demand => |data| {
|
|
|
|
const main_amount = self.output.main_amount.getValueOrElse(u32, 1);
|
|
|
|
const main_factor = std.math.clamp(self.output.main_factor.getValueOrElse(f64, 0.6), 0.1, 0.9);
|
|
|
|
const view_padding = self.output.view_padding.getValueOrElse(u32, 0);
|
|
|
|
const outer_padding = self.output.outer_padding.getValueOrElse(u32, 0);
|
|
|
|
|
|
|
|
const secondary_count = if (data.view_count > main_amount)
|
|
|
|
data.view_count - main_amount
|
|
|
|
else
|
|
|
|
0;
|
|
|
|
|
|
|
|
const usable_width = if (self.orientation == .left or self.orientation == .right)
|
|
|
|
data.usable_width - (2 * outer_padding)
|
|
|
|
else
|
|
|
|
data.usable_height - (2 * outer_padding);
|
|
|
|
const usable_height = if (self.orientation == .left or self.orientation == .right)
|
|
|
|
data.usable_height - (2 * outer_padding)
|
|
|
|
else
|
|
|
|
data.usable_width - (2 * outer_padding);
|
|
|
|
|
|
|
|
// to make things pixel-perfect, we make the first main and first secondary
|
|
|
|
// view slightly larger if the height is not evenly divisible
|
|
|
|
var main_width: u32 = undefined;
|
|
|
|
var main_height: u32 = undefined;
|
|
|
|
var main_height_rem: u32 = undefined;
|
|
|
|
|
|
|
|
var secondary_width: u32 = undefined;
|
|
|
|
var secondary_height: u32 = undefined;
|
|
|
|
var secondary_height_rem: u32 = undefined;
|
|
|
|
|
|
|
|
if (main_amount > 0 and secondary_count > 0) {
|
|
|
|
main_width = @floatToInt(u32, main_factor * @intToFloat(f64, usable_width));
|
|
|
|
main_height = usable_height / main_amount;
|
|
|
|
main_height_rem = usable_height % main_amount;
|
|
|
|
|
|
|
|
secondary_width = usable_width - main_width;
|
|
|
|
secondary_height = usable_height / secondary_count;
|
|
|
|
secondary_height_rem = usable_height % secondary_count;
|
|
|
|
} else if (main_amount > 0) {
|
|
|
|
main_width = usable_width;
|
|
|
|
main_height = usable_height / main_amount;
|
|
|
|
main_height_rem = usable_height % main_amount;
|
|
|
|
} else if (secondary_width > 0) {
|
|
|
|
main_width = 0;
|
|
|
|
secondary_width = usable_width;
|
|
|
|
secondary_height = usable_height / secondary_count;
|
|
|
|
secondary_height_rem = usable_height % secondary_count;
|
|
|
|
}
|
2020-06-16 12:48:30 +00:00
|
|
|
|
2020-10-02 13:53:08 +00:00
|
|
|
var i: u32 = 0;
|
|
|
|
while (i < data.view_count) : (i += 1) {
|
|
|
|
var x: i32 = undefined;
|
|
|
|
var y: i32 = undefined;
|
|
|
|
var width: u32 = undefined;
|
|
|
|
var height: u32 = undefined;
|
|
|
|
|
|
|
|
if (i < main_amount) {
|
|
|
|
x = 0;
|
|
|
|
y = @intCast(i32, (i * main_height) + if (i > 0) main_height_rem else 0);
|
|
|
|
width = main_width;
|
|
|
|
height = main_height + if (i == 0) main_height_rem else 0;
|
|
|
|
} else {
|
|
|
|
x = @intCast(i32, main_width);
|
|
|
|
y = @intCast(i32, (i - main_amount) * secondary_height +
|
|
|
|
if (i > main_amount) secondary_height_rem else 0);
|
|
|
|
width = secondary_width;
|
|
|
|
height = secondary_height + if (i == main_amount) secondary_height_rem else 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
x += @intCast(i32, view_padding);
|
|
|
|
y += @intCast(i32, view_padding);
|
|
|
|
width -= 2 * view_padding;
|
|
|
|
height -= 2 * view_padding;
|
|
|
|
|
|
|
|
switch (self.orientation) {
|
|
|
|
.left => layout.pushViewDimensions(
|
|
|
|
data.serial,
|
|
|
|
x + @intCast(i32, outer_padding),
|
|
|
|
y + @intCast(i32, outer_padding),
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
),
|
|
|
|
.right => layout.pushViewDimensions(
|
|
|
|
data.serial,
|
|
|
|
@intCast(i32, usable_width - width) - x + @intCast(i32, outer_padding),
|
|
|
|
y + @intCast(i32, outer_padding),
|
|
|
|
width,
|
|
|
|
height,
|
|
|
|
),
|
|
|
|
.top => layout.pushViewDimensions(
|
|
|
|
data.serial,
|
|
|
|
y + @intCast(i32, outer_padding),
|
|
|
|
x + @intCast(i32, outer_padding),
|
|
|
|
height,
|
|
|
|
width,
|
|
|
|
),
|
|
|
|
.bottom => layout.pushViewDimensions(
|
|
|
|
data.serial,
|
|
|
|
y + @intCast(i32, outer_padding),
|
|
|
|
@intCast(i32, usable_width - width) - x + @intCast(i32, outer_padding),
|
|
|
|
height,
|
|
|
|
width,
|
|
|
|
),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
layout.commit(data.serial);
|
|
|
|
},
|
|
|
|
|
|
|
|
.advertise_view => {},
|
|
|
|
.advertise_done => {},
|
2020-06-16 12:48:30 +00:00
|
|
|
}
|
|
|
|
}
|
2020-10-02 13:53:08 +00:00
|
|
|
};
|
2020-06-16 12:48:30 +00:00
|
|
|
|
2020-10-02 13:53:08 +00:00
|
|
|
pub fn main() !void {
|
|
|
|
const display = wl.Display.connect(null) catch {
|
|
|
|
std.debug.warn("Unable to connect to Wayland server.\n", .{});
|
|
|
|
std.os.exit(1);
|
|
|
|
};
|
|
|
|
defer display.disconnect();
|
|
|
|
|
|
|
|
var context: Context = .{};
|
2020-06-16 15:06:24 +00:00
|
|
|
|
2020-10-02 13:53:08 +00:00
|
|
|
const registry = try display.getRegistry();
|
|
|
|
try registry.setListener(*Context, registryListener, &context);
|
|
|
|
_ = try display.roundtrip();
|
2020-06-16 15:06:24 +00:00
|
|
|
|
2020-10-02 13:53:08 +00:00
|
|
|
if (context.layout_manager == null) {
|
|
|
|
std.debug.warn("Wayland server does not support river_layout_unstable_v1.\n", .{});
|
|
|
|
std.os.exit(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (context.options_manager == null) {
|
|
|
|
std.debug.warn("Wayland server does not support river_options_unstable_v1.\n", .{});
|
|
|
|
std.os.exit(1);
|
|
|
|
}
|
|
|
|
|
|
|
|
context.configureAllOutputs();
|
|
|
|
defer context.destroyAllOutputs();
|
|
|
|
|
|
|
|
while (context.running) {
|
|
|
|
_ = try display.dispatch();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, context: *Context) void {
|
|
|
|
switch (event) {
|
|
|
|
.global => |global| {
|
|
|
|
if (std.cstr.cmp(global.interface, river.LayoutManagerV1.getInterface().name) == 0) {
|
|
|
|
context.layout_manager = registry.bind(global.name, river.LayoutManagerV1, 1) catch return;
|
|
|
|
} else if (std.cstr.cmp(global.interface, zriver.OptionsManagerV1.getInterface().name) == 0) {
|
|
|
|
context.options_manager = registry.bind(global.name, zriver.OptionsManagerV1, 1) catch return;
|
|
|
|
} else if (std.cstr.cmp(global.interface, wl.Output.getInterface().name) == 0) {
|
|
|
|
context.addOutput(registry, global.name) catch {
|
|
|
|
std.debug.warn("Failed to bind output.\n", .{});
|
|
|
|
context.running = false;
|
|
|
|
};
|
|
|
|
}
|
|
|
|
},
|
|
|
|
.global_remove => |global| {},
|
|
|
|
}
|
2020-06-16 15:06:24 +00:00
|
|
|
}
|