aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.zig10
-rw-r--r--doc/river-layouts.7.scd86
-rw-r--r--doc/riverctl.1.scd53
-rw-r--r--doc/rivertile.1.scd32
-rwxr-xr-xexample/init32
-rw-r--r--protocol/river-layout-v1.xml201
-rw-r--r--river/Config.zig6
-rw-r--r--river/Cursor.zig7
-rw-r--r--river/Layout.zig197
-rw-r--r--river/LayoutDemand.zig139
-rw-r--r--river/LayoutManager.zig84
-rw-r--r--river/Option.zig66
-rw-r--r--river/OptionsManager.zig2
-rw-r--r--river/Output.zig262
-rw-r--r--river/Root.zig43
-rw-r--r--river/Server.zig3
-rw-r--r--river/View.zig36
-rw-r--r--river/XdgToplevel.zig10
-rw-r--r--river/command.zig5
-rw-r--r--river/command/config.zig30
-rw-r--r--river/command/layout.zig38
-rw-r--r--river/command/mod_main_count.zig38
-rw-r--r--river/command/mod_main_factor.zig41
-rw-r--r--river/command/move.zig9
-rw-r--r--river/command/toggle_float.zig6
-rw-r--r--rivertile/main.zig482
26 files changed, 1263 insertions, 655 deletions
diff --git a/build.zig b/build.zig
index 33a7f8a..ec4c502 100644
--- a/build.zig
+++ b/build.zig
@@ -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,
diff --git a/doc/river-layouts.7.scd b/doc/river-layouts.7.scd
deleted file mode 100644
index 6b76846..0000000
--- a/doc/river-layouts.7.scd
+++ /dev/null
@@ -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)
diff --git a/doc/riverctl.1.scd b/doc/riverctl.1.scd
index 46dd3a1..0d17e44 100644
--- a/doc/riverctl.1.scd
+++ b/doc/riverctl.1.scd
@@ -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)
diff --git a/doc/rivertile.1.scd b/doc/rivertile.1.scd
index f77c177..cbf8f32 100644
--- a/doc/rivertile.1.scd
+++ b/doc/rivertile.1.scd
@@ -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)
+
diff --git a/example/init b/example/init
index 84c0e05..4cfca78 100755
--- a/example/init
+++ b/example/init
@@ -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"
diff --git a/protocol/river-layout-v1.xml b/protocol/river-layout-v1.xml
new file mode 100644
index 0000000..2ddcea3
--- /dev/null
+++ b/protocol/river-layout-v1.xml
@@ -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>
diff --git a/river/Config.zig b/river/Config.zig
index 24b8690..8e8abba 100644
--- a/river/Config.zig
+++ b/river/Config.zig
@@ -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),
diff --git a/river/Cursor.zig b/river/Cursor.zig
index 081da03..cdf1516 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -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();
diff --git a/river/Layout.zig b/river/Layout.zig
new file mode 100644
index 0000000..887f630
--- /dev/null
+++ b/river/Layout.zig
@@ -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);
+}
diff --git a/river/LayoutDemand.zig b/river/LayoutDemand.zig
new file mode 100644
index 0000000..4bc9426
--- /dev/null
+++ b/river/LayoutDemand.zig
@@ -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;
+}
diff --git a/river/LayoutManager.zig b/river/LayoutManager.zig
new file mode 100644
index 0000000..905954d
--- /dev/null
+++ b/river/LayoutManager.zig
@@ -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;
+ };
+ },
+ }
+}
diff --git a/river/Option.zig b/river/Option.zig
index 130f01a..a7e0d37 100644
--- a/river/Option.zig
+++ b/river/Option.zig
@@ -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 {
diff --git a/river/OptionsManager.zig b/river/OptionsManager.zig
index 9d895dd..063f6e1 100644
--- a/river/OptionsManager.zig
+++ b/river/OptionsManager.zig
@@ -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;
};
diff --git a/river/Output.zig b/river/Output.zig
index 166b3c0..bf894be 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -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,
-
-/// Percentage of the total screen that the "main" section takes up.
-main_factor: f64 = 0.6,
+/// The currently active LayoutDemand
+layout_demand: ?LayoutDemand = null,
-/// 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();
+}
diff --git a/river/Root.zig b/river/Root.zig
index e05495f..ae5b5b4 100644
--- a/river/Root.zig
+++ b/river/Root.zig
@@ -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;
diff --git a/river/Server.zig b/river/Server.zig
index 5cc612b..7a7ce32 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -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);
diff --git a/river/View.zig b/river/View.zig
index 91f7e1d..0606cc3 100644
--- a/river/View.zig
+++ b/river/View.zig
@@ -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();
diff --git a/river/XdgToplevel.zig b/river/XdgToplevel.zig
index 11389a8..c8b278f 100644
--- a/river/XdgToplevel.zig
+++ b/river/XdgToplevel.zig
@@ -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
diff --git a/river/command.zig b/river/command.zig
index 88acdbc..c0cf51b 100644
--- a/river/command.zig
+++ b/river/command.zig
@@ -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 },
};
diff --git a/river/command/config.zig b/river/command/config.zig
index e889d05..d881c2c 100644
--- a/river/command/config.zig
+++ b/river/command/config.zig
@@ -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,
diff --git a/river/command/layout.zig b/river/command/layout.zig
deleted file mode 100644
index 4ca8f3a..0000000
--- a/river/command/layout.zig
+++ /dev/null
@@ -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();
-}
diff --git a/river/command/mod_main_count.zig b/river/command/mod_main_count.zig
deleted file mode 100644
index 68ce80b..0000000
--- a/river/command/mod_main_count.zig
+++ /dev/null
@@ -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();
-}
diff --git a/river/command/mod_main_factor.zig b/river/command/mod_main_factor.zig
deleted file mode 100644
index dd5fa9d..0000000
--- a/river/command/mod_main_factor.zig
+++ /dev/null
@@ -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();
- }
-}
diff --git a/river/command/move.zig b/river/command/move.zig
index 173985b..c089f1f 100644
--- a/river/command/move.zig
+++ b/river/command/move.zig
@@ -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();
diff --git a/river/command/toggle_float.zig b/river/command/toggle_float.zig
index 513da11..f8a7b22 100644
--- a/river/command/toggle_float.zig
+++ b/river/command/toggle_float.zig
@@ -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;
diff --git a/rivertile/main.zig b/rivertile/main.zig
index d1149b1..c020101 100644
--- a/rivertile/main.zig
+++ b/rivertile/main.zig
@@ -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 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);
+ }
-const Orientation = enum {
- left,
- right,
- top,
- bottom,
+ 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();
+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;
- // first arg must be left, right, top, or bottom
- const main_location = std.meta.stringToEnum(Orientation, std.mem.spanZ(args[1])) orelse
- printUsageAndExit();
+ 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 {};
+ }
+};
- // 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 Layout = struct {
+ output: *Output,
+ layout: ?*river.LayoutV1,
+ orientation: Orientation,
- const width_arg: u32 = switch (main_location) {
- .left, .right => 5,
- .top, .bottom => 6,
+ const Orientation = enum {
+ top,
+ right,
+ bottom,
+ left,
};
- 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);
-
- 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;
- }
-
- // Buffering the output makes things faster
- var stdout_buf = std.io.bufferedOutStream(std.io.getStdOut().outStream());
- const stdout = stdout_buf.outStream();
-
- 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;
+
+ 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;
+ }
- 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 }),
+ 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 => {},
}
}
+};
- try stdout_buf.flush();
-}
+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 = .{};
-fn printUsageAndExit() noreturn {
- const usage: []const u8 =
- \\Usage: rivertile left|right|top|bottom [args passed by river]
- \\
- ;
+ const registry = try display.getRegistry();
+ try registry.setListener(*Context, registryListener, &context);
+ _ = try display.roundtrip();
- std.debug.warn(usage, .{});
- std.os.exit(1);
+ 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| {},
+ }
}