From f72656b72e5822481a4bc15b75d19b61b1bb7bc8 Mon Sep 17 00:00:00 2001 From: Leon Henrik Plickat Date: Fri, 2 Oct 2020 15:53:08 +0200 Subject: [PATCH] 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 --- build.zig | 10 +- doc/river-layouts.7.scd | 86 ------ doc/riverctl.1.scd | 53 ++-- doc/rivertile.1.scd | 32 +- example/init | 32 +- protocol/river-layout-v1.xml | 201 +++++++++++++ river/Config.zig | 6 - river/Cursor.zig | 7 +- river/Layout.zig | 197 ++++++++++++ river/LayoutDemand.zig | 139 +++++++++ river/LayoutManager.zig | 84 ++++++ river/Option.zig | 66 +++-- river/OptionsManager.zig | 2 +- river/Output.zig | 262 ++++++---------- river/Root.zig | 43 ++- river/Server.zig | 3 + river/View.zig | 36 ++- river/XdgToplevel.zig | 10 +- river/command.zig | 5 - river/command/config.zig | 30 -- river/command/layout.zig | 38 --- river/command/mod_main_count.zig | 38 --- river/command/mod_main_factor.zig | 41 --- river/command/move.zig | 9 +- river/command/toggle_float.zig | 6 + rivertile/main.zig | 478 +++++++++++++++++++++++------- 26 files changed, 1261 insertions(+), 653 deletions(-) delete mode 100644 doc/river-layouts.7.scd create mode 100644 protocol/river-layout-v1.xml create mode 100644 river/Layout.zig create mode 100644 river/LayoutDemand.zig create mode 100644 river/LayoutManager.zig delete mode 100644 river/command/layout.zig delete mode 100644 river/command/mod_main_count.zig delete mode 100644 river/command/mod_main_factor.zig 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 who is assisted by open -source contributors. For more information about river's development, see -. - -# 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 @@ + + + + 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. + + + + 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. + + + + + A global factory for river_layout_v1 objects. + + + + + 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. + + + + + + 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. + + + + + + + + + + This interface allows clients to receive layout demands from the + compositor for a specific output and subsequently propose positions and + dimensions of individual views. + + + + + + + + + + This request indicates that the client will not use the river_layout_v1 + object any more. + + + + + + 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. + + + + + + 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. + + + + + + + + + + + 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. + + + + + + + + + This event is sent by the server as the last event of the layout + demand with matching serial, after all advertise_view events. + + + + + + + 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. + + + + + + + + + + + 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. + + + + + + + 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. + + + + 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 . + +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 . + +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 . + +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, +/// 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(); +} 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 . - -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 . - -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 . - -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 . +// + +// +// 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| {}, + } }