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

@ -66,6 +66,7 @@ pub fn build(b: *zbs.Builder) !void {
scanner.addProtocolPath("protocol/river-control-unstable-v1.xml");
scanner.addProtocolPath("protocol/river-options-unstable-v1.xml");
scanner.addProtocolPath("protocol/river-status-unstable-v1.xml");
scanner.addProtocolPath("protocol/river-layout-v1.xml");
scanner.addProtocolPath("protocol/wlr-layer-shell-unstable-v1.xml");
scanner.addProtocolPath("protocol/wlr-output-power-management-unstable-v1.xml");
@ -100,6 +101,14 @@ pub fn build(b: *zbs.Builder) !void {
const rivertile = b.addExecutable("rivertile", "rivertile/main.zig");
rivertile.setTarget(target);
rivertile.setBuildMode(mode);
rivertile.step.dependOn(&scanner.step);
rivertile.addPackage(scanner.getPkg());
rivertile.linkLibC();
rivertile.linkSystemLibrary("wayland-client");
scanner.addCSource(rivertile);
rivertile.install();
}
@ -195,7 +204,6 @@ const ScdocStep = struct {
"doc/river.1.scd",
"doc/riverctl.1.scd",
"doc/rivertile.1.scd",
"doc/river-layouts.7.scd",
};
builder: *zbs.Builder,

View file

@ -1,86 +0,0 @@
RIVER-LAYOUTS(7) "github.com/ifreund/river"
# NAME
river-layouts - Details on layout generators for river
# DESCRIPTION
River can use external window management layouts. To get such a layout, river
will run an executable and parse its output. This document outlines how such a
layout generator interacts with river.
# INPUT
When running the executable, river will provide it with five parameters which
are appended to the end of the command in the following order:
. The amount of visible clients (integer)
. The amount of views dedicated as main (integer)
. The screen size multiplier for the main area (float between 0.0 and 1.0)
. The useable width of the output (integer)
. The useable height of the output (integer)
A layout generator may choose to ignore any of these values except
for the first one.
# OUTPUT
River expects four integer values for each window: The x position, the y
position, the width and the height. These must be separated by spaces. A window
configuration having fewer or more than four values is an error and will cause
river to fall back the full layout.
A layout generator needs to output position and size for every visible window.
The window configurations are separated by a newline. Too few or too many
outputted window configurations is an error and will cause river to fall back
to the full layout.
River will apply the position and dimensions in the order they are outputted to
the visible windows in the stack from top to bottom.
The output of a layout generator is not required to remain the same when called
with identical parameters. Layouts are allowed to also depend on external
factors or be completely random.
# WINDOW DIMENSIONS and POSITION
Layout generators are not supposed to include padding or leave space for window
borders. The window dimensions will be shrunk by river to make space for these.
River enforces a minimal window width and height of 50.
Layout generators operate on a special coordinate grid from 0 to the maximum
useable width or height of an output with the coordinate 0-0 being positioned
at the top-left corner of the useable area of an output. While layout
generators are free to place windows everywhere (including coordinates below
zero or above the maximum width or height of an output), beware that the
relative positioning of this grid on the screen can not be expected to remain
constant. River applies an offset to window positions, depending on outer
padding and the presence of desktop widgets like bars. Layout generators can
therefore not position windows at exact screen coordinates.
Layout generators are not required to make use of the entire available space.
Windows may overlap.
# EXAMPLE
Below is an example output of a layout generator for four visible windows. In
this example layout all four windows have a size of 500 by 500 and are arranged
in a grid.
```
0 0 500 500
500 0 500 500
0 500 500 500
500 500 500 500
```
# AUTHORS
Maintained by Isaac Freund <ifreund@ifreund.xyz> who is assisted by open
source contributors. For more information about river's development, see
<https://github.com/ifreund/river>.
# SEE ALSO
*river*(1), *riverctl*(1), *rivertile*(1)

View file

@ -38,29 +38,6 @@ over the Wayland protocol.
*focus-view* *next*|*previous*
Focus the next or previous view in the stack.
*layout* *full*|_command_
Provide a command which river will use for generating the layout
of non-floating windows on the currently focused output. See
*river-layouts*(7) for details on the expected formatting of the
output of layout commands. Alternatively, “full” can be given
instead of a command to cause river to use its single internal layout,
in which windows span the entire width and height of the output.
*mod-main-count* _integer_
Increase or decrease the number of "main" views which is relayed to the
layout generator. _integer_ can be positive or negative. Exactly how
"main" views are display, or if they are even displayed differently
from other views, is left to the layout generator.
*mod-main-factor* _float_
Increase or decrease the "main factor" relayed to layout
generators. _float_ is a positive or negative floating point number
(such as 0.05). This value is added to the current main factor which
is then clamped to the range [0.0, 1.0]. The layout generator is
free to interpret this value as it sees fit, or ignore it entirely.
*rivertile*(1) uses this to determine what percentage of the screen
the "main" area will occupy.
*move* *up*|*down*|*left*|*right* _delta_
Move the focused view in the specified direction by _delta_ logical
pixels. The view will be set to floating.
@ -264,16 +241,10 @@ A complete list may be found in _/usr/include/linux/input-event-codes.h_
Setting _step-size_ to 1.0 disables transitions fully regardless of
the value of _delta-t_.
*outer-padding* _pixels_
Set the padding around the edge of the screen to _pixels_.
*set-repeat* _rate_ _delay_
Set the keyboard repeat rate to _rate_ key repeats per second and
repeat delay to _delay_ milliseconds.
*view-padding* _pixels_
Set the padding around the edge of each view to _pixels_.
*xcursor-theme* _theme_name_ [_size_]
Set the xcursor theme to _theme_name_ and optionally set the _size_.
The theme of the default seat determines the default for Xwayland
@ -309,6 +280,28 @@ River declares certain default options for all outputs.
Changing this option changes the title of the wayland and X11 backend
outputs.
*layout* (string)
The layout namespace used to determine which layout should arrange this
output. If set to null or no layout with this namespace exists for this
output, the output will enter floating mode. Defaults to null.
*main_amount* (uint, optional hint for layouts)
An arbitrary positive integer indicating the amount of main views. Defaults
to 1.
*main_factor* (float, optional hint for layouts)
A floating point numger indicating the relative size of the area reserved
for main views. Note that layouts commonly expect values between 0.1 and 0.9.
Defaults to 0.6.
*view_padding* (uint, optional hint for layouts)
A positive integer indicating the padding in of pixels between / around
views. Defaults to 10.
*outer_padding* (uint, optional hint for layouts)
A positive integer indicating the padding in of pixels around the layut.
Defaults to 10.
# EXAMPLES
Bind bemenu-run to Super+P in normal mode:
@ -325,4 +318,4 @@ source contributors. For more information about river's development, see
# SEE ALSO
*river*(1), *river-layouts*(7), *rivertile*(1)
*river*(1), *rivertile*(1)

View file

@ -6,32 +6,19 @@ rivertile - Tiled layout generator for river
# SYNOPSIS
*rivertile* *left*|*right*|*top*|*bottom* [args passed by river]
*rivertile*
# DESCRIPTION
*rivertile* is a layout generator for river. It produces tiled layouts with
split main/secondary stacks in four configurable orientations.
*rivertile* is a layout client for river. It provides four tiled layouts per
output with split main/secondary stacks with the main area in different
positions.
# OPTIONS
The namespaces of the four layouts are "tile-top", "tile-right", "tile-bottom"
and "tile-left", corresponding to the position of the main area.
*left*
Place the main stack on the left side of the output.
*right*
Place the main stack on the right side of the output.
*top*
Place the main stack at the top of the output.
*bottom*
Place the main stack at the bottom of the output.
# EXAMPLE
Set river's layout to *rivertile*'s *left* layout using riverctl
riverctl layout rivertile left
*rivertile* uses the *main_amount*, *main_factor*, *view_padding* and
*outer_padding* options.
# AUTHORS
@ -41,4 +28,5 @@ source contributors. For more information about river's development, see
# SEE ALSO
*river-layouts*(7), *river*(1), *riverctl*(1)
*river*(1), *riverctl*(1)

View file

@ -39,16 +39,6 @@ riverctl map normal $mod+Shift Comma send-to-output previous
# Mod+Return to bump the focused view to the top of the layout stack
riverctl map normal $mod Return zoom
# Mod+H and Mod+L to decrease/increase the main factor by 5%
# If using rivertile(1) this determines the width of the main stack.
riverctl map normal $mod H mod-main-factor -0.05
riverctl map normal $mod L mod-main-factor +0.05
# Mod+Shift+H and Mod+Shift+L to increment/decrement the number of
# main views in the layout
riverctl map normal $mod+Shift H mod-main-count +1
riverctl map normal $mod+Shift L mod-main-count -1
# Mod+Alt+{H,J,K,L} to move views
riverctl map normal $mod+Mod1 H move left 100
riverctl map normal $mod+Mod1 J move down 100
@ -103,13 +93,10 @@ riverctl map normal $mod Space toggle-float
riverctl map normal $mod F toggle-fullscreen
# Mod+{Up,Right,Down,Left} to change layout orientation
riverctl map normal $mod Up layout rivertile top
riverctl map normal $mod Right layout rivertile right
riverctl map normal $mod Down layout rivertile bottom
riverctl map normal $mod Left layout rivertile left
# Mod+S to change to Full layout
riverctl map normal $mod S layout full
riverctl map normal $mod Up spawn riverctl set-option -focused-output layout tile-up
riverctl map normal $mod Right spawn riverctl set-option -focused-output layout tile-right
riverctl map normal $mod Down spawn riverctl set-option -focused-output layout tile-down
riverctl map normal $mod Left spawn riverctl set-option -focused-output layout tile-left
# Declare a passthrough mode. This mode has only a single mapping to return to
# normal mode. This makes it useful for testing a nested wayland compositor
@ -148,7 +135,16 @@ done
riverctl set-repeat 50 300
# Set the layout on startup
riverctl layout rivertile left
riverctl spawn rivertile
riverctl set-option -focused-output layout tile-left
# Mod+Alt+{1..9} to set main amount
# Mod+Alt+Ctrl+{1..9} to set main factor
#for i in $(seq 1 9)
#do
# riverctl map normal $mod+mod1 spawn riverctl set-option -focused-output main_amount "${i}"
# riverctl map normal $mod+Control+mod1 spawn riverctl set-option -focused-output main_factor "0.${i}"
#done
# Set app-ids of views which should float
riverctl float-filter-add "float"

View file

@ -0,0 +1,201 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="river_layout_v1">
<copyright>
Copyright 2020-2021 The River Developers
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
</copyright>
<description summary="let clients propose view positions and dimensions">
This protocol specifies a way for clients to propose arbitrary positions and
dimensions for a set of views on a specific output of a compositor through
the river_layout_v1 object.
This set of views is logically structured as a simple list. Views
in this list cannot be individually addressed, instead the order of
requests/events is significant.
The entire set of proposed positions and dimensions for the views in the
list are called a layout. Due to their list heritage, layouts are also
logically strictly linear; Any complex underlying data structure a client
may use when generating the layout is lost in transmission. This is an
intentional limitation.
Note that the client may need to handle multiple layout demands per
river_layout_v1 object simultaneously.
Warning! The protocol described in this file is currently in the testing
phase. Backward compatible changes may be added together with the
corresponding interface version bump. Backward incompatible changes can
only be done by creating a new major version of the extension.
</description>
<interface name="river_layout_manager_v1" version="1">
<description summary="manage river layout objects">
A global factory for river_layout_v1 objects.
</description>
<request name="destroy" type="destructor">
<description summary="destroy the river_layout_manager object">
This request indicates that the client will not use the
river_layout_manager object any more. Objects that have been created
through this instance are not affected.
</description>
</request>
<request name="get_layout">
<description summary="create a river_layout_v1 object">
This creates a new river_layout_v1 object for the given wl_output.
All layout related communication is done through this interface.
The namespace is used by the compositor to decide which river_layout_v1
object will receive layout demands for the output.
The namespace is required to be be unique per-output. Furthermore,
two separate clients may not share a namespace on separate outputs. If
these conditions are not upheld, the the namespace_in_use event will
be sent directly after creation of the river_layout_v1 object.
</description>
<arg name="id" type="new_id" interface="river_layout_v1"/>
<arg name="output" type="object" interface="wl_output"/>
<arg name="namespace" type="string" summary="namespace of the layout object"/>
</request>
</interface>
<interface name="river_layout_v1" version="1">
<description summary="receive and respond to layout demands">
This interface allows clients to receive layout demands from the
compositor for a specific output and subsequently propose positions and
dimensions of individual views.
</description>
<enum name="error">
<entry name="count_mismatch" value="0" summary="number of
proposed dimensions does not match number of views in layout"/>
<entry name="already_committed" value="1" summary="the layout demand with
the provided serial was already committed"/>
</enum>
<request name="destroy" type="destructor">
<description summary="destroy the river_layout_v1 object">
This request indicates that the client will not use the river_layout_v1
object any more.
</description>
</request>
<event name="namespace_in_use">
<description summary="the requested namespace is already in use">
After this event is sent, all requests aside from the destroy event
will be ignored by the server. If the client wishes to try again with
a different namespace they must create a new river_layout_v1 object.
</description>
</event>
<event name="layout_demand">
<description summary="the compositor requires a layout">
The compositor sends this event to inform the client that it requires a
layout for a set of views.
The usable width and height height indicate the space in which the
client can safely position views without interfering with desktop
widgets such as panels.
The serial of this event is used to identify subsequent events and
request as belonging to this layout demand. Beware that the client
might need to handle multiple layout demands at the same time.
The server will ignore responses to all but the most recent
layout demand. Thus, clients are only required to respond to the most
recent layout_demand received. If a newer layout_demand is received
before the client has finished responding to an old demand, the client
may abort work on the old demand as any further work would be wasted.
</description>
<arg name="view_count" type="uint" summary="number of views in the layout"/>
<arg name="usable_width" type="uint" summary="width of the usable area"/>
<arg name="usable_height" type="uint" summary="height of the usable area"/>
<arg name="tags" type="uint" summary="tags of the output, 32-bit bitfield"/>
<arg name="serial" type="uint" summary="serial of the layout demand"/>
</event>
<event name="advertise_view">
<description summary="make layout client aware of view">
This event is sent by the server as part of the layout demand with
matching serial. It provides additional information about one of
the views to be arranged.
Every view part of the layout demand is advertised exactly once,
in the order of the view list.
</description>
<arg name="tags" type="uint" summary="tags of the view, 32-bit bitfield"/>
<arg name="app_id" type="string" summary="view app-id" allow-null="true"/>
<arg name="serial" type="uint" summary="serial of the layout demand"/>
</event>
<event name="advertise_done">
<description summary="all views have been advertised">
This event is sent by the server as the last event of the layout
demand with matching serial, after all advertise_view events.
</description>
<arg name="serial" type="uint" summary="serial of the layout demand"/>
</event>
<request name="push_view_dimensions">
<description summary="propose dimensions of the next view">
This request proposes a size and position of a view in the layout demand
with matching serial.
Pushed view dimensions apply to the views in the same order they were
advertised. That is, the first push_view_dimensions request applies
to the first view advertised, the second to the second, and so on.
A client must propose position and dimensions for the entire set of
views. Proposing too many or too few view dimensions is a protocol error.
This request may be sent before the corresponding view has been
advertised.
The x and y coordinates are relative to the usable area of the output,
with (0,0) as the top left corner.
</description>
<arg name="serial" type="uint" summary="serial of layout demand"/>
<arg name="x" type="int" summary="x coordinate of view"/>
<arg name="y" type="int" summary="y coordinate of view"/>
<arg name="width" type="uint" summary="width of view"/>
<arg name="height" type="uint" summary="height of view"/>
</request>
<request name="commit">
<description summary="commit a layout">
This request indicates that the client is done pushing dimensions
and the compositor may apply the layout. This completes the layout
demand with matching serial, any other requests sent with the serial
are a protocol error.
The compositor is free to use this proposed layout however it chooses,
including ignoring it.
</description>
<arg name="serial" type="uint" summary="serial of layout demand"/>
</request>
<request name="parameters_changed">
<description summary="parameters of layout have changed">
The client may use this request to inform the compositor that one or
muliple of the parameters it uses to generate layouts have changed.
If the client is responsible for the current view layout, the compositor
may decide to send a new layout demand to update the layout.
</description>
</request>
</interface>
</protocol>

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;

View file

@ -14,129 +14,393 @@
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//
//
// 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:
//
// +-----------------------+------------+
// | | |
// | | |
// | | |
// | +------------+
// | | |
// | | |
// | | |
// | +------------+
// | | |
// | | |
// | | |
// +-----------------------+------------+
//
const std = @import("std");
const wayland = @import("wayland");
const wl = wayland.client.wl;
const zriver = wayland.client.zriver;
const river = wayland.client.river;
const Orientation = enum {
left,
right,
top,
bottom,
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);
}
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);
}
}
};
/// 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 view, the left layout looks something like this:
///
/// +-----------------------+------------+
/// | | |
/// | | |
/// | | |
/// | +------------+
/// | | |
/// | | |
/// | | |
/// | +------------+
/// | | |
/// | | |
/// | | |
/// +-----------------------+------------+
pub fn main() !void {
const args = std.os.argv;
if (args.len != 7) printUsageAndExit();
// first arg must be left, right, top, or bottom
const main_location = std.meta.stringToEnum(Orientation, std.mem.spanZ(args[1])) orelse
printUsageAndExit();
// the other 5 are passed by river and described in river-layouts(7)
const num_views = try std.fmt.parseInt(u32, std.mem.spanZ(args[2]), 10);
const main_count = try std.fmt.parseInt(u32, std.mem.spanZ(args[3]), 10);
const main_factor = try std.fmt.parseFloat(f64, std.mem.spanZ(args[4]));
const width_arg: u32 = switch (main_location) {
.left, .right => 5,
.top, .bottom => 6,
const Option = struct {
pub const Value = union(enum) {
unset: void,
double: f64,
uint: u32,
};
const height_arg: u32 = if (width_arg == 5) 6 else 5;
const output_width = try std.fmt.parseInt(u32, std.mem.spanZ(args[width_arg]), 10);
const output_height = try std.fmt.parseInt(u32, std.mem.spanZ(args[height_arg]), 10);
handle: ?*zriver.OptionHandleV1 = null,
value: Value = .unset,
output: *Output = undefined,
const secondary_count = if (num_views > main_count) num_views - main_count else 0;
// 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_count > 0 and secondary_count > 0) {
main_width = @floatToInt(u32, main_factor * @intToFloat(f64, output_width));
main_height = output_height / main_count;
main_height_rem = output_height % main_count;
secondary_width = output_width - main_width;
secondary_height = output_height / secondary_count;
secondary_height_rem = output_height % secondary_count;
} else if (main_count > 0) {
main_width = output_width;
main_height = output_height / main_count;
main_height_rem = output_height % main_count;
} else if (secondary_width > 0) {
main_width = 0;
secondary_width = output_width;
secondary_height = output_height / secondary_count;
secondary_height_rem = output_height % secondary_count;
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;
};
}
// Buffering the output makes things faster
var stdout_buf = std.io.bufferedOutStream(std.io.getStdOut().outStream());
const stdout = stdout_buf.outStream();
pub fn deinit(self: *Option) void {
if (self.handle) |handle| handle.destroy();
}
var i: u32 = 0;
while (i < num_views) : (i += 1) {
var x: u32 = undefined;
var y: u32 = undefined;
var width: u32 = undefined;
var height: u32 = undefined;
if (i < main_count) {
x = 0;
y = 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 = main_width;
y = (i - main_count) * secondary_height + if (i > main_count) secondary_height_rem else 0;
width = secondary_width;
height = secondary_height + if (i == main_count) secondary_height_rem else 0;
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();
}
switch (main_location) {
.left => try stdout.print("{} {} {} {}\n", .{ x, y, width, height }),
.right => try stdout.print("{} {} {} {}\n", .{ output_width - x - width, y, width, height }),
.top => try stdout.print("{} {} {} {}\n", .{ y, x, height, width }),
.bottom => try stdout.print("{} {} {} {}\n", .{ y, output_width - x - width, height, width }),
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();
}
}
try stdout_buf.flush();
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;
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 {};
}
};
const Layout = struct {
output: *Output,
layout: ?*river.LayoutV1,
orientation: Orientation,
const Orientation = enum {
top,
right,
bottom,
left,
};
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;
}
}
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;
}
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 => {},
}
}
};
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 = .{};
const registry = try display.getRegistry();
try registry.setListener(*Context, registryListener, &context);
_ = try display.roundtrip();
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 printUsageAndExit() noreturn {
const usage: []const u8 =
\\Usage: rivertile left|right|top|bottom [args passed by river]
\\
;
std.debug.warn(usage, .{});
std.os.exit(1);
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| {},
}
}