river-layout: create and implement protocol
Replace the current layout mechanism based on passing args to a child process and parsing it's stdout with a new wayland protocol. This much more robust and allows for more featureful layout generators. Co-authored-by: Isaac Freund <ifreund@ifreund.xyz>
This commit is contained in:
parent
df3e993013
commit
f72656b72e
10
build.zig
10
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,
|
||||
|
@ -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)
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
32
example/init
32
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"
|
||||
|
201
protocol/river-layout-v1.xml
Normal file
201
protocol/river-layout-v1.xml
Normal file
@ -0,0 +1,201 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="river_layout_v1">
|
||||
<copyright>
|
||||
Copyright 2020-2021 The River Developers
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<description summary="let clients propose view positions and dimensions">
|
||||
This protocol specifies a way for clients to propose arbitrary positions and
|
||||
dimensions for a set of views on a specific output of a compositor through
|
||||
the river_layout_v1 object.
|
||||
|
||||
This set of views is logically structured as a simple list. Views
|
||||
in this list cannot be individually addressed, instead the order of
|
||||
requests/events is significant.
|
||||
|
||||
The entire set of proposed positions and dimensions for the views in the
|
||||
list are called a layout. Due to their list heritage, layouts are also
|
||||
logically strictly linear; Any complex underlying data structure a client
|
||||
may use when generating the layout is lost in transmission. This is an
|
||||
intentional limitation.
|
||||
|
||||
Note that the client may need to handle multiple layout demands per
|
||||
river_layout_v1 object simultaneously.
|
||||
|
||||
Warning! The protocol described in this file is currently in the testing
|
||||
phase. Backward compatible changes may be added together with the
|
||||
corresponding interface version bump. Backward incompatible changes can
|
||||
only be done by creating a new major version of the extension.
|
||||
</description>
|
||||
|
||||
<interface name="river_layout_manager_v1" version="1">
|
||||
<description summary="manage river layout objects">
|
||||
A global factory for river_layout_v1 objects.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the river_layout_manager object">
|
||||
This request indicates that the client will not use the
|
||||
river_layout_manager object any more. Objects that have been created
|
||||
through this instance are not affected.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="get_layout">
|
||||
<description summary="create a river_layout_v1 object">
|
||||
This creates a new river_layout_v1 object for the given wl_output.
|
||||
|
||||
All layout related communication is done through this interface.
|
||||
|
||||
The namespace is used by the compositor to decide which river_layout_v1
|
||||
object will receive layout demands for the output.
|
||||
|
||||
The namespace is required to be be unique per-output. Furthermore,
|
||||
two separate clients may not share a namespace on separate outputs. If
|
||||
these conditions are not upheld, the the namespace_in_use event will
|
||||
be sent directly after creation of the river_layout_v1 object.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="river_layout_v1"/>
|
||||
<arg name="output" type="object" interface="wl_output"/>
|
||||
<arg name="namespace" type="string" summary="namespace of the layout object"/>
|
||||
</request>
|
||||
</interface>
|
||||
|
||||
<interface name="river_layout_v1" version="1">
|
||||
<description summary="receive and respond to layout demands">
|
||||
This interface allows clients to receive layout demands from the
|
||||
compositor for a specific output and subsequently propose positions and
|
||||
dimensions of individual views.
|
||||
</description>
|
||||
|
||||
<enum name="error">
|
||||
<entry name="count_mismatch" value="0" summary="number of
|
||||
proposed dimensions does not match number of views in layout"/>
|
||||
<entry name="already_committed" value="1" summary="the layout demand with
|
||||
the provided serial was already committed"/>
|
||||
</enum>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the river_layout_v1 object">
|
||||
This request indicates that the client will not use the river_layout_v1
|
||||
object any more.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="namespace_in_use">
|
||||
<description summary="the requested namespace is already in use">
|
||||
After this event is sent, all requests aside from the destroy event
|
||||
will be ignored by the server. If the client wishes to try again with
|
||||
a different namespace they must create a new river_layout_v1 object.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="layout_demand">
|
||||
<description summary="the compositor requires a layout">
|
||||
The compositor sends this event to inform the client that it requires a
|
||||
layout for a set of views.
|
||||
|
||||
The usable width and height height indicate the space in which the
|
||||
client can safely position views without interfering with desktop
|
||||
widgets such as panels.
|
||||
|
||||
The serial of this event is used to identify subsequent events and
|
||||
request as belonging to this layout demand. Beware that the client
|
||||
might need to handle multiple layout demands at the same time.
|
||||
|
||||
The server will ignore responses to all but the most recent
|
||||
layout demand. Thus, clients are only required to respond to the most
|
||||
recent layout_demand received. If a newer layout_demand is received
|
||||
before the client has finished responding to an old demand, the client
|
||||
may abort work on the old demand as any further work would be wasted.
|
||||
</description>
|
||||
<arg name="view_count" type="uint" summary="number of views in the layout"/>
|
||||
<arg name="usable_width" type="uint" summary="width of the usable area"/>
|
||||
<arg name="usable_height" type="uint" summary="height of the usable area"/>
|
||||
<arg name="tags" type="uint" summary="tags of the output, 32-bit bitfield"/>
|
||||
<arg name="serial" type="uint" summary="serial of the layout demand"/>
|
||||
</event>
|
||||
|
||||
<event name="advertise_view">
|
||||
<description summary="make layout client aware of view">
|
||||
This event is sent by the server as part of the layout demand with
|
||||
matching serial. It provides additional information about one of
|
||||
the views to be arranged.
|
||||
|
||||
Every view part of the layout demand is advertised exactly once,
|
||||
in the order of the view list.
|
||||
</description>
|
||||
<arg name="tags" type="uint" summary="tags of the view, 32-bit bitfield"/>
|
||||
<arg name="app_id" type="string" summary="view app-id" allow-null="true"/>
|
||||
<arg name="serial" type="uint" summary="serial of the layout demand"/>
|
||||
</event>
|
||||
|
||||
<event name="advertise_done">
|
||||
<description summary="all views have been advertised">
|
||||
This event is sent by the server as the last event of the layout
|
||||
demand with matching serial, after all advertise_view events.
|
||||
</description>
|
||||
<arg name="serial" type="uint" summary="serial of the layout demand"/>
|
||||
</event>
|
||||
|
||||
<request name="push_view_dimensions">
|
||||
<description summary="propose dimensions of the next view">
|
||||
This request proposes a size and position of a view in the layout demand
|
||||
with matching serial.
|
||||
|
||||
Pushed view dimensions apply to the views in the same order they were
|
||||
advertised. That is, the first push_view_dimensions request applies
|
||||
to the first view advertised, the second to the second, and so on.
|
||||
|
||||
A client must propose position and dimensions for the entire set of
|
||||
views. Proposing too many or too few view dimensions is a protocol error.
|
||||
|
||||
This request may be sent before the corresponding view has been
|
||||
advertised.
|
||||
|
||||
The x and y coordinates are relative to the usable area of the output,
|
||||
with (0,0) as the top left corner.
|
||||
</description>
|
||||
<arg name="serial" type="uint" summary="serial of layout demand"/>
|
||||
<arg name="x" type="int" summary="x coordinate of view"/>
|
||||
<arg name="y" type="int" summary="y coordinate of view"/>
|
||||
<arg name="width" type="uint" summary="width of view"/>
|
||||
<arg name="height" type="uint" summary="height of view"/>
|
||||
</request>
|
||||
|
||||
<request name="commit">
|
||||
<description summary="commit a layout">
|
||||
This request indicates that the client is done pushing dimensions
|
||||
and the compositor may apply the layout. This completes the layout
|
||||
demand with matching serial, any other requests sent with the serial
|
||||
are a protocol error.
|
||||
|
||||
The compositor is free to use this proposed layout however it chooses,
|
||||
including ignoring it.
|
||||
</description>
|
||||
<arg name="serial" type="uint" summary="serial of layout demand"/>
|
||||
</request>
|
||||
|
||||
<request name="parameters_changed">
|
||||
<description summary="parameters of layout have changed">
|
||||
The client may use this request to inform the compositor that one or
|
||||
muliple of the parameters it uses to generate layouts have changed.
|
||||
|
||||
If the client is responsible for the current view layout, the compositor
|
||||
may decide to send a new layout demand to update the layout.
|
||||
</description>
|
||||
</request>
|
||||
</interface>
|
||||
</protocol>
|
@ -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),
|
||||
|
||||
|
@ -533,8 +533,11 @@ pub fn enterMode(self: *Self, mode: @TagType(Mode), view: *View) void {
|
||||
},
|
||||
};
|
||||
|
||||
// Automatically float all views being moved by the pointer
|
||||
if (!view.current.float) {
|
||||
// Automatically float all views being moved by the pointer, if
|
||||
// their dimensions are set by a layout client. If however the views
|
||||
// are unarranged, leave them as non-floating so the next active
|
||||
// layout can affect them.
|
||||
if (!view.current.float and view.output.current.layout != null) {
|
||||
view.pending.float = true;
|
||||
view.float_box = view.current.box;
|
||||
view.applyPending();
|
||||
|
197
river/Layout.zig
Normal file
197
river/Layout.zig
Normal file
@ -0,0 +1,197 @@
|
||||
// This file is part of river, a dynamic tiling wayland compositor.
|
||||
//
|
||||
// Copyright 2020 - 2021 The River Developers
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const wlr = @import("wlroots");
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.server.wl;
|
||||
const river = wayland.server.river;
|
||||
|
||||
const util = @import("util.zig");
|
||||
|
||||
const Box = @import("Box.zig");
|
||||
const Server = @import("Server.zig");
|
||||
const Output = @import("Output.zig");
|
||||
const View = @import("View.zig");
|
||||
const ViewStack = @import("view_stack.zig").ViewStack;
|
||||
const LayoutDemand = @import("LayoutDemand.zig");
|
||||
|
||||
const log = std.log.scoped(.layout);
|
||||
|
||||
layout: *river.LayoutV1,
|
||||
namespace: []const u8,
|
||||
output: *Output,
|
||||
|
||||
pub fn create(client: *wl.Client, version: u32, id: u32, output: *Output, namespace: []const u8) !void {
|
||||
const layout = try river.LayoutV1.create(client, version, id);
|
||||
|
||||
if (namespaceInUse(namespace, output, client)) {
|
||||
layout.sendNamespaceInUse();
|
||||
layout.setHandler(?*c_void, handleRequestInert, null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
const node = try util.gpa.create(std.TailQueue(Self).Node);
|
||||
errdefer util.gpa.destroy(node);
|
||||
node.data = .{
|
||||
.layout = layout,
|
||||
.namespace = try util.gpa.dupe(u8, namespace),
|
||||
.output = output,
|
||||
};
|
||||
output.layouts.append(node);
|
||||
|
||||
layout.setHandler(*Self, handleRequest, handleDestroy, &node.data);
|
||||
|
||||
// If the namespace matches that of the output, set the layout as
|
||||
// the active one of the output and arrange it.
|
||||
if (output.layout_option.value.string) |current_layout| {
|
||||
if (mem.eql(u8, namespace, mem.span(current_layout))) {
|
||||
output.pending.layout = &node.data;
|
||||
output.arrangeViews();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the given namespace is already in use on the given output
|
||||
/// or on another output by a different client.
|
||||
fn namespaceInUse(namespace: []const u8, output: *Output, client: *wl.Client) bool {
|
||||
var output_it = output.root.outputs.first;
|
||||
while (output_it) |output_node| : (output_it = output_node.next) {
|
||||
var layout_it = output_node.data.layouts.first;
|
||||
if (output_node.data.wlr_output == output.wlr_output) {
|
||||
// On this output, no other layout can have our namespace.
|
||||
while (layout_it) |layout_node| : (layout_it = layout_node.next) {
|
||||
if (mem.eql(u8, namespace, layout_node.data.namespace)) return true;
|
||||
}
|
||||
} else {
|
||||
// Layouts on other outputs may share the namespace, if they come from the same client.
|
||||
while (layout_it) |layout_node| : (layout_it = layout_node.next) {
|
||||
if (mem.eql(u8, namespace, layout_node.data.namespace) and
|
||||
client != layout_node.data.layout.getClient()) return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/// This exists to handle layouts that have been rendered inert (due to the
|
||||
/// namespace already being in use) until the client destroys them.
|
||||
fn handleRequestInert(layout: *river.LayoutV1, request: river.LayoutV1.Request, _: ?*c_void) void {
|
||||
if (request == .destroy) layout.destroy();
|
||||
}
|
||||
|
||||
/// Send a layout demand to the client
|
||||
pub fn startLayoutDemand(self: *Self, views: u32) void {
|
||||
log.debug(
|
||||
"starting layout demand '{}' on output '{}'",
|
||||
.{ self.namespace, self.output.wlr_output.name },
|
||||
);
|
||||
|
||||
std.debug.assert(self.output.layout_demand == null);
|
||||
self.output.layout_demand = LayoutDemand.init(self, views) catch {
|
||||
log.err("failed starting layout demand", .{});
|
||||
return;
|
||||
};
|
||||
const serial = self.output.layout_demand.?.serial;
|
||||
|
||||
// Then we let the client know that we require a layout
|
||||
self.layout.sendLayoutDemand(
|
||||
views,
|
||||
self.output.usable_box.width,
|
||||
self.output.usable_box.height,
|
||||
self.output.pending.tags,
|
||||
serial,
|
||||
);
|
||||
|
||||
// And finally we advertise all visible views
|
||||
var it = ViewStack(View).iter(self.output.views.first, .forward, self.output.pending.tags, Output.arrangeFilter);
|
||||
while (it.next()) |view| {
|
||||
self.layout.sendAdvertiseView(view.pending.tags, view.getAppId(), serial);
|
||||
}
|
||||
self.layout.sendAdvertiseDone(serial);
|
||||
|
||||
self.output.root.trackLayoutDemands();
|
||||
}
|
||||
|
||||
fn handleRequest(layout: *river.LayoutV1, request: river.LayoutV1.Request, self: *Self) void {
|
||||
switch (request) {
|
||||
.destroy => layout.destroy(),
|
||||
|
||||
// Parameters of the layout changed. We only care about this, if the
|
||||
// layout is currently in use, in which case we rearrange the output.
|
||||
.parameters_changed => if (self == self.output.pending.layout) self.output.arrangeViews(),
|
||||
|
||||
// We receive this event when the client wants to push a view dimension proposal
|
||||
// to the layout demand matching the serial.
|
||||
.push_view_dimensions => |req| {
|
||||
log.debug(
|
||||
"layout '{}' on output '{}' pushed view dimensions: {} {} {} {}",
|
||||
.{ self.namespace, self.output.wlr_output.name, req.x, req.y, req.width, req.height },
|
||||
);
|
||||
|
||||
if (self.output.layout_demand) |*layout_demand| {
|
||||
// We can't raise a protocol error when the serial is old/wrong
|
||||
// because we do not keep track of old serials server-side.
|
||||
// Therefore, simply ignore requests with old/wrong serials.
|
||||
if (layout_demand.serial != req.serial) return;
|
||||
layout_demand.pushViewDimensions(self.output, req.x, req.y, req.width, req.height);
|
||||
}
|
||||
},
|
||||
|
||||
// We receive this event when the client wants to mark the proposed layout
|
||||
// of the layout demand matching the serial as done.
|
||||
.commit => |req| {
|
||||
log.debug(
|
||||
"layout '{}' on output '{}' commited",
|
||||
.{ self.namespace, self.output.wlr_output.name },
|
||||
);
|
||||
|
||||
if (self.output.layout_demand) |*layout_demand| {
|
||||
// We can't raise a protocol error when the serial is old/wrong
|
||||
// because we do not keep track of old serials server-side.
|
||||
// Therefore, simply ignore requests with old/wrong serials.
|
||||
if (layout_demand.serial == req.serial) layout_demand.apply(self);
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn handleDestroy(layout: *river.LayoutV1, self: *Self) void {
|
||||
log.debug(
|
||||
"destroying layout '{}' on output '{}'",
|
||||
.{ self.namespace, self.output.wlr_output.name },
|
||||
);
|
||||
|
||||
// Remove layout from the list
|
||||
const node = @fieldParentPtr(std.TailQueue(Self).Node, "data", self);
|
||||
self.output.layouts.remove(node);
|
||||
|
||||
// If we are the currently active layout of an output, clean up. The output
|
||||
// will always end up with no layout at this point, so we directly start the
|
||||
// transaction.
|
||||
if (self == self.output.pending.layout) {
|
||||
self.output.pending.layout = null;
|
||||
self.output.arrangeViews();
|
||||
self.output.root.startTransaction();
|
||||
}
|
||||
|
||||
util.gpa.free(self.namespace);
|
||||
util.gpa.destroy(node);
|
||||
}
|
139
river/LayoutDemand.zig
Normal file
139
river/LayoutDemand.zig
Normal file
@ -0,0 +1,139 @@
|
||||
// This file is part of river, a dynamic tiling wayland compositor.
|
||||
//
|
||||
// Copyright 2020 - 2021 The River Developers
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const wlr = @import("wlroots");
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.server.wl;
|
||||
const zriver = wayland.server.zriver;
|
||||
|
||||
const util = @import("util.zig");
|
||||
|
||||
const Layout = @import("Layout.zig");
|
||||
const Box = @import("Box.zig");
|
||||
const Server = @import("Server.zig");
|
||||
const Output = @import("Output.zig");
|
||||
const View = @import("View.zig");
|
||||
const ViewStack = @import("view_stack.zig").ViewStack;
|
||||
|
||||
const log = std.log.scoped(.layout);
|
||||
|
||||
const Error = error{ViewDimensionMismatch};
|
||||
|
||||
const timeout_ms = 1000;
|
||||
|
||||
serial: u32,
|
||||
/// Number of views for which dimensions have not been pushed.
|
||||
/// This will go negative if the client pushes too many dimensions.
|
||||
views: i32,
|
||||
/// Proposed view dimensions
|
||||
view_boxen: []Box,
|
||||
timeout_timer: *wl.EventSource,
|
||||
|
||||
pub fn init(layout: *Layout, views: u32) !Self {
|
||||
const event_loop = layout.output.root.server.wl_server.getEventLoop();
|
||||
const timeout_timer = try event_loop.addTimer(*Layout, handleTimeout, layout);
|
||||
errdefer timeout_timer.remove();
|
||||
try timeout_timer.timerUpdate(timeout_ms);
|
||||
|
||||
return Self{
|
||||
.serial = layout.output.root.server.wl_server.nextSerial(),
|
||||
.views = @intCast(i32, views),
|
||||
.view_boxen = try util.gpa.alloc(Box, views),
|
||||
.timeout_timer = timeout_timer,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *const Self) void {
|
||||
self.timeout_timer.remove();
|
||||
util.gpa.free(self.view_boxen);
|
||||
}
|
||||
|
||||
/// Destroy the LayoutDemand on timeout.
|
||||
/// All further responses to the event will simply be ignored.
|
||||
fn handleTimeout(layout: *Layout) callconv(.C) c_int {
|
||||
log.notice(
|
||||
"layout demand for layout '{}' on output '{}' timed out",
|
||||
.{ layout.namespace, layout.output.wlr_output.name },
|
||||
);
|
||||
layout.output.layout_demand.?.deinit();
|
||||
layout.output.layout_demand = null;
|
||||
|
||||
layout.output.root.notifyLayoutDemandDone();
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Push a set of proposed view dimensions and position to the list
|
||||
pub fn pushViewDimensions(self: *Self, output: *Output, x: i32, y: i32, width: u32, height: u32) void {
|
||||
// The client pushed too many dimensions
|
||||
if (self.views < 0) return;
|
||||
|
||||
// Here we apply the offset to align the coords with the origin of the
|
||||
// usable area and shrink the dimensions to accomodate the border size.
|
||||
const border_width = output.root.server.config.border_width;
|
||||
self.view_boxen[self.view_boxen.len - @intCast(usize, self.views)] = .{
|
||||
.x = x + output.usable_box.x + @intCast(i32, border_width),
|
||||
.y = y + output.usable_box.y + @intCast(i32, border_width),
|
||||
.width = if (width > 2 * border_width) width - 2 * border_width else width,
|
||||
.height = if (height > 2 * border_width) height - 2 * border_width else height,
|
||||
};
|
||||
|
||||
self.views -= 1;
|
||||
}
|
||||
|
||||
/// Apply the proposed layout to the output
|
||||
pub fn apply(self: *Self, layout: *Layout) void {
|
||||
const output = layout.output;
|
||||
|
||||
// Whether the layout demand succeeds or fails, we are done with it and
|
||||
// need to clean up
|
||||
defer {
|
||||
output.layout_demand.?.deinit();
|
||||
output.layout_demand = null;
|
||||
output.root.notifyLayoutDemandDone();
|
||||
}
|
||||
|
||||
// Check that the number of proposed dimensions is correct.
|
||||
if (self.views != 0) {
|
||||
log.err(
|
||||
"proposed dimension count ({}) does not match view count ({}), aborting layout demand",
|
||||
.{ -self.views + @intCast(i32, self.view_boxen.len), self.view_boxen.len },
|
||||
);
|
||||
layout.layout.postError(
|
||||
.count_mismatch,
|
||||
"number of proposed view dimensions must match number of views",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply proposed layout to views
|
||||
var it = ViewStack(View).iter(output.views.first, .forward, output.pending.tags, Output.arrangeFilter);
|
||||
var i: u32 = 0;
|
||||
while (it.next()) |view| : (i += 1) {
|
||||
if (view.pending.fullscreen) {
|
||||
view.post_fullscreen_box = self.view_boxen[i];
|
||||
} else {
|
||||
view.pending.box = self.view_boxen[i];
|
||||
}
|
||||
view.applyConstraints();
|
||||
}
|
||||
std.debug.assert(i == self.view_boxen.len);
|
||||
output.pending.layout = layout;
|
||||
}
|
84
river/LayoutManager.zig
Normal file
84
river/LayoutManager.zig
Normal file
@ -0,0 +1,84 @@
|
||||
// This file is part of river, a dynamic tiling wayland compositor.
|
||||
//
|
||||
// Copyright 2020 - 2021 The River Developers
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
const Self = @This();
|
||||
|
||||
const std = @import("std");
|
||||
const mem = std.mem;
|
||||
const wlr = @import("wlroots");
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.server.wl;
|
||||
const river = wayland.server.river;
|
||||
|
||||
const util = @import("util.zig");
|
||||
|
||||
const Layout = @import("Layout.zig");
|
||||
const Server = @import("Server.zig");
|
||||
const Output = @import("Output.zig");
|
||||
|
||||
const log = std.log.scoped(.layout);
|
||||
|
||||
global: *wl.Global,
|
||||
server_destroy: wl.Listener(*wl.Server) = wl.Listener(*wl.Server).init(handleServerDestroy),
|
||||
|
||||
pub fn init(self: *Self, server: *Server) !void {
|
||||
self.* = .{
|
||||
.global = try wl.Global.create(server.wl_server, river.LayoutManagerV1, 1, *Self, self, bind),
|
||||
};
|
||||
|
||||
server.wl_server.addDestroyListener(&self.server_destroy);
|
||||
}
|
||||
|
||||
fn handleServerDestroy(listener: *wl.Listener(*wl.Server), wl_server: *wl.Server) void {
|
||||
const self = @fieldParentPtr(Self, "server_destroy", listener);
|
||||
self.global.destroy();
|
||||
}
|
||||
|
||||
fn bind(client: *wl.Client, self: *Self, version: u32, id: u32) callconv(.C) void {
|
||||
const layout_manager = river.LayoutManagerV1.create(client, 1, id) catch {
|
||||
client.postNoMemory();
|
||||
log.crit("out of memory", .{});
|
||||
return;
|
||||
};
|
||||
layout_manager.setHandler(*Self, handleRequest, null, self);
|
||||
}
|
||||
|
||||
fn handleRequest(layout_manager: *river.LayoutManagerV1, request: river.LayoutManagerV1.Request, self: *Self) void {
|
||||
switch (request) {
|
||||
.destroy => layout_manager.destroy(),
|
||||
|
||||
.get_layout => |req| {
|
||||
// Ignore if the output is inert
|
||||
const wlr_output = wlr.Output.fromWlOutput(req.output) orelse return;
|
||||
const output = @intToPtr(*Output, wlr_output.data);
|
||||
|
||||
log.debug("bind layout '{}' on output '{}'", .{ req.namespace, output.wlr_output.name });
|
||||
|
||||
Layout.create(
|
||||
layout_manager.getClient(),
|
||||
layout_manager.getVersion(),
|
||||
req.id,
|
||||
output,
|
||||
mem.span(req.namespace),
|
||||
) catch {
|
||||
layout_manager.getClient().postNoMemory();
|
||||
log.crit("out of memory", .{});
|
||||
return;
|
||||
};
|
||||
},
|
||||
}
|
||||
}
|
@ -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 {
|
||||
|
@ -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;
|
||||
};
|
||||
|
262
river/Output.zig
262
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();
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
|
@ -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 },
|
||||
};
|
||||
|
@ -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,
|
||||
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
@ -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;
|
||||
|
||||
|
@ -14,129 +14,393 @@
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
|
||||
//
|
||||
// This is an implementation of the default "tiled" layout of dwm and the
|
||||
// 3 other orientations thereof. This code is written with the left
|
||||
// orientation in mind and then the input/output values are adjusted to apply
|
||||
// the necessary transformations to derive the other 3.
|
||||
//
|
||||
// With 4 views and one main, the left layout looks something like this:
|
||||
//
|
||||
// +-----------------------+------------+
|
||||
// | | |
|
||||
// | | |
|
||||
// | | |
|
||||
// | +------------+
|
||||
// | | |
|
||||
// | | |
|
||||
// | | |
|
||||
// | +------------+
|
||||
// | | |
|
||||
// | | |
|
||||
// | | |
|
||||
// +-----------------------+------------+
|
||||
//
|
||||
|
||||
const std = @import("std");
|
||||
const wayland = @import("wayland");
|
||||
const wl = wayland.client.wl;
|
||||
const zriver = wayland.client.zriver;
|
||||
const river = wayland.client.river;
|
||||
|
||||
const Orientation = enum {
|
||||
left,
|
||||
right,
|
||||
top,
|
||||
bottom,
|
||||
const gpa = std.heap.c_allocator;
|
||||
|
||||
const Context = struct {
|
||||
running: bool = true,
|
||||
layout_manager: ?*river.LayoutManagerV1 = null,
|
||||
options_manager: ?*zriver.OptionsManagerV1 = null,
|
||||
outputs: std.TailQueue(Output) = .{},
|
||||
|
||||
pub fn addOutput(self: *Context, registry: *wl.Registry, name: u32) !void {
|
||||
const output = try registry.bind(name, wl.Output, 3);
|
||||
const node = try gpa.create(std.TailQueue(Output).Node);
|
||||
node.data.init(self, output);
|
||||
self.outputs.append(node);
|
||||
}
|
||||
|
||||
pub fn destroyAllOutputs(self: *Context) void {
|
||||
while (self.outputs.pop()) |node| {
|
||||
node.data.deinit();
|
||||
gpa.destroy(node);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configureAllOutputs(self: *Context) void {
|
||||
var it = self.outputs.first;
|
||||
while (it) |node| : (it = node.next) {
|
||||
node.data.configure(self);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/// This is an implementation of the default "tiled" layout of dwm and the
|
||||
/// 3 other orientations thereof. This code is written with the left
|
||||
/// orientation in mind and then the input/output values are adjusted to apply
|
||||
/// the necessary transformations to derive the other 3.
|
||||
///
|
||||
/// With 4 views and one main view, the left layout looks something like this:
|
||||
///
|
||||
/// +-----------------------+------------+
|
||||
/// | | |
|
||||
/// | | |
|
||||
/// | | |
|
||||
/// | +------------+
|
||||
/// | | |
|
||||
/// | | |
|
||||
/// | | |
|
||||
/// | +------------+
|
||||
/// | | |
|
||||
/// | | |
|
||||
/// | | |
|
||||
/// +-----------------------+------------+
|
||||
pub fn main() !void {
|
||||
const args = std.os.argv;
|
||||
if (args.len != 7) printUsageAndExit();
|
||||
|
||||
// first arg must be left, right, top, or bottom
|
||||
const main_location = std.meta.stringToEnum(Orientation, std.mem.spanZ(args[1])) orelse
|
||||
printUsageAndExit();
|
||||
|
||||
// the other 5 are passed by river and described in river-layouts(7)
|
||||
const num_views = try std.fmt.parseInt(u32, std.mem.spanZ(args[2]), 10);
|
||||
const main_count = try std.fmt.parseInt(u32, std.mem.spanZ(args[3]), 10);
|
||||
const main_factor = try std.fmt.parseFloat(f64, std.mem.spanZ(args[4]));
|
||||
|
||||
const width_arg: u32 = switch (main_location) {
|
||||
.left, .right => 5,
|
||||
.top, .bottom => 6,
|
||||
const Option = struct {
|
||||
pub const Value = union(enum) {
|
||||
unset: void,
|
||||
double: f64,
|
||||
uint: u32,
|
||||
};
|
||||
const height_arg: u32 = if (width_arg == 5) 6 else 5;
|
||||
|
||||
const output_width = try std.fmt.parseInt(u32, std.mem.spanZ(args[width_arg]), 10);
|
||||
const output_height = try std.fmt.parseInt(u32, std.mem.spanZ(args[height_arg]), 10);
|
||||
handle: ?*zriver.OptionHandleV1 = null,
|
||||
value: Value = .unset,
|
||||
output: *Output = undefined,
|
||||
|
||||
const secondary_count = if (num_views > main_count) num_views - main_count else 0;
|
||||
|
||||
// to make things pixel-perfect, we make the first main and first secondary
|
||||
// view slightly larger if the height is not evenly divisible
|
||||
var main_width: u32 = undefined;
|
||||
var main_height: u32 = undefined;
|
||||
var main_height_rem: u32 = undefined;
|
||||
|
||||
var secondary_width: u32 = undefined;
|
||||
var secondary_height: u32 = undefined;
|
||||
var secondary_height_rem: u32 = undefined;
|
||||
|
||||
if (main_count > 0 and secondary_count > 0) {
|
||||
main_width = @floatToInt(u32, main_factor * @intToFloat(f64, output_width));
|
||||
main_height = output_height / main_count;
|
||||
main_height_rem = output_height % main_count;
|
||||
|
||||
secondary_width = output_width - main_width;
|
||||
secondary_height = output_height / secondary_count;
|
||||
secondary_height_rem = output_height % secondary_count;
|
||||
} else if (main_count > 0) {
|
||||
main_width = output_width;
|
||||
main_height = output_height / main_count;
|
||||
main_height_rem = output_height % main_count;
|
||||
} else if (secondary_width > 0) {
|
||||
main_width = 0;
|
||||
secondary_width = output_width;
|
||||
secondary_height = output_height / secondary_count;
|
||||
secondary_height_rem = output_height % secondary_count;
|
||||
pub fn init(self: *Option, output: *Output, comptime key: [*:0]const u8, initial: Value) !void {
|
||||
self.* = .{
|
||||
.value = initial,
|
||||
.output = output,
|
||||
.handle = try output.context.options_manager.?.getOptionHandle(
|
||||
key,
|
||||
output.output,
|
||||
),
|
||||
};
|
||||
self.handle.?.setListener(*Option, optionListener, self) catch |err| {
|
||||
self.handle.?.destroy();
|
||||
self.handle = null;
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
// Buffering the output makes things faster
|
||||
var stdout_buf = std.io.bufferedOutStream(std.io.getStdOut().outStream());
|
||||
const stdout = stdout_buf.outStream();
|
||||
pub fn deinit(self: *Option) void {
|
||||
if (self.handle) |handle| handle.destroy();
|
||||
}
|
||||
|
||||
var i: u32 = 0;
|
||||
while (i < num_views) : (i += 1) {
|
||||
var x: u32 = undefined;
|
||||
var y: u32 = undefined;
|
||||
var width: u32 = undefined;
|
||||
var height: u32 = undefined;
|
||||
|
||||
if (i < main_count) {
|
||||
x = 0;
|
||||
y = i * main_height + if (i > 0) main_height_rem else 0;
|
||||
width = main_width;
|
||||
height = main_height + if (i == 0) main_height_rem else 0;
|
||||
} else {
|
||||
x = main_width;
|
||||
y = (i - main_count) * secondary_height + if (i > main_count) secondary_height_rem else 0;
|
||||
width = secondary_width;
|
||||
height = secondary_height + if (i == main_count) secondary_height_rem else 0;
|
||||
fn optionListener(handle: *zriver.OptionHandleV1, event: zriver.OptionHandleV1.Event, self: *Option) void {
|
||||
switch (event) {
|
||||
.unset => switch (self.value) {
|
||||
.uint => handle.setUintValue(self.value.uint),
|
||||
.double => handle.setFixedValue(wl.Fixed.fromDouble(self.value.double)),
|
||||
else => unreachable,
|
||||
},
|
||||
.int_value => {},
|
||||
.uint_value => |data| self.value = .{ .uint = data.value },
|
||||
.fixed_value => |data| self.value = .{ .double = data.value.toDouble() },
|
||||
.string_value => {},
|
||||
}
|
||||
if (self.output.top.layout) |layout| layout.parametersChanged();
|
||||
if (self.output.right.layout) |layout| layout.parametersChanged();
|
||||
if (self.output.bottom.layout) |layout| layout.parametersChanged();
|
||||
if (self.output.left.layout) |layout| layout.parametersChanged();
|
||||
}
|
||||
|
||||
switch (main_location) {
|
||||
.left => try stdout.print("{} {} {} {}\n", .{ x, y, width, height }),
|
||||
.right => try stdout.print("{} {} {} {}\n", .{ output_width - x - width, y, width, height }),
|
||||
.top => try stdout.print("{} {} {} {}\n", .{ y, x, height, width }),
|
||||
.bottom => try stdout.print("{} {} {} {}\n", .{ y, output_width - x - width, height, width }),
|
||||
pub fn getValueOrElse(self: *Option, comptime T: type, comptime otherwise: T) T {
|
||||
switch (T) {
|
||||
u32 => return if (self.value == .uint) self.value.uint else otherwise,
|
||||
f64 => return if (self.value == .double) self.value.double else otherwise,
|
||||
else => @compileError("Unsupported type for Option.getValueOrElse()"),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const Output = struct {
|
||||
context: *Context,
|
||||
output: *wl.Output,
|
||||
|
||||
top: Layout = undefined,
|
||||
right: Layout = undefined,
|
||||
bottom: Layout = undefined,
|
||||
left: Layout = undefined,
|
||||
|
||||
main_amount: Option = .{},
|
||||
main_factor: Option = .{},
|
||||
view_padding: Option = .{},
|
||||
outer_padding: Option = .{},
|
||||
|
||||
configured: bool = false,
|
||||
|
||||
pub fn init(self: *Output, context: *Context, wl_output: *wl.Output) void {
|
||||
self.* = .{
|
||||
.output = wl_output,
|
||||
.context = context,
|
||||
};
|
||||
self.configure(context);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Output) void {
|
||||
self.output.release();
|
||||
|
||||
if (self.configured) {
|
||||
self.top.deinit();
|
||||
self.right.deinit();
|
||||
self.bottom.deinit();
|
||||
self.left.deinit();
|
||||
|
||||
self.main_amount.deinit();
|
||||
self.main_factor.deinit();
|
||||
self.view_padding.deinit();
|
||||
self.outer_padding.deinit();
|
||||
}
|
||||
}
|
||||
|
||||
try stdout_buf.flush();
|
||||
pub fn configure(self: *Output, context: *Context) void {
|
||||
if (self.configured) return;
|
||||
if (context.layout_manager == null) return;
|
||||
if (context.options_manager == null) return;
|
||||
|
||||
self.configured = true;
|
||||
|
||||
self.main_amount.init(self, "main_amount", .{ .uint = 1 }) catch {};
|
||||
self.main_factor.init(self, "main_factor", .{ .double = 0.6 }) catch {};
|
||||
self.view_padding.init(self, "view_padding", .{ .uint = 10 }) catch {};
|
||||
self.outer_padding.init(self, "outer_padding", .{ .uint = 10 }) catch {};
|
||||
|
||||
self.top.init(self, .top) catch {};
|
||||
self.right.init(self, .right) catch {};
|
||||
self.bottom.init(self, .bottom) catch {};
|
||||
self.left.init(self, .left) catch {};
|
||||
}
|
||||
};
|
||||
|
||||
const Layout = struct {
|
||||
output: *Output,
|
||||
layout: ?*river.LayoutV1,
|
||||
orientation: Orientation,
|
||||
|
||||
const Orientation = enum {
|
||||
top,
|
||||
right,
|
||||
bottom,
|
||||
left,
|
||||
};
|
||||
|
||||
pub fn init(self: *Layout, output: *Output, orientation: Orientation) !void {
|
||||
self.output = output;
|
||||
self.orientation = orientation;
|
||||
self.layout = try output.context.layout_manager.?.getLayout(
|
||||
self.output.output,
|
||||
self.getNamespace(),
|
||||
);
|
||||
self.layout.?.setListener(*Layout, layoutListener, self) catch |err| {
|
||||
self.layout.?.destroy();
|
||||
self.layout = null;
|
||||
return err;
|
||||
};
|
||||
}
|
||||
|
||||
fn getNamespace(self: *Layout) [*:0]const u8 {
|
||||
return switch (self.orientation) {
|
||||
.top => "tile-top",
|
||||
.right => "tile-right",
|
||||
.bottom => "tile-bottom",
|
||||
.left => "tile-left",
|
||||
};
|
||||
}
|
||||
|
||||
pub fn deinit(self: *Layout) void {
|
||||
if (self.layout) |layout| {
|
||||
layout.destroy();
|
||||
self.layout = null;
|
||||
}
|
||||
}
|
||||
|
||||
fn layoutListener(layout: *river.LayoutV1, event: river.LayoutV1.Event, self: *Layout) void {
|
||||
switch (event) {
|
||||
.namespace_in_use => {
|
||||
std.debug.warn("{}: Namespace already in use.\n", .{self.getNamespace()});
|
||||
self.deinit();
|
||||
},
|
||||
|
||||
.layout_demand => |data| {
|
||||
const main_amount = self.output.main_amount.getValueOrElse(u32, 1);
|
||||
const main_factor = std.math.clamp(self.output.main_factor.getValueOrElse(f64, 0.6), 0.1, 0.9);
|
||||
const view_padding = self.output.view_padding.getValueOrElse(u32, 0);
|
||||
const outer_padding = self.output.outer_padding.getValueOrElse(u32, 0);
|
||||
|
||||
const secondary_count = if (data.view_count > main_amount)
|
||||
data.view_count - main_amount
|
||||
else
|
||||
0;
|
||||
|
||||
const usable_width = if (self.orientation == .left or self.orientation == .right)
|
||||
data.usable_width - (2 * outer_padding)
|
||||
else
|
||||
data.usable_height - (2 * outer_padding);
|
||||
const usable_height = if (self.orientation == .left or self.orientation == .right)
|
||||
data.usable_height - (2 * outer_padding)
|
||||
else
|
||||
data.usable_width - (2 * outer_padding);
|
||||
|
||||
// to make things pixel-perfect, we make the first main and first secondary
|
||||
// view slightly larger if the height is not evenly divisible
|
||||
var main_width: u32 = undefined;
|
||||
var main_height: u32 = undefined;
|
||||
var main_height_rem: u32 = undefined;
|
||||
|
||||
var secondary_width: u32 = undefined;
|
||||
var secondary_height: u32 = undefined;
|
||||
var secondary_height_rem: u32 = undefined;
|
||||
|
||||
if (main_amount > 0 and secondary_count > 0) {
|
||||
main_width = @floatToInt(u32, main_factor * @intToFloat(f64, usable_width));
|
||||
main_height = usable_height / main_amount;
|
||||
main_height_rem = usable_height % main_amount;
|
||||
|
||||
secondary_width = usable_width - main_width;
|
||||
secondary_height = usable_height / secondary_count;
|
||||
secondary_height_rem = usable_height % secondary_count;
|
||||
} else if (main_amount > 0) {
|
||||
main_width = usable_width;
|
||||
main_height = usable_height / main_amount;
|
||||
main_height_rem = usable_height % main_amount;
|
||||
} else if (secondary_width > 0) {
|
||||
main_width = 0;
|
||||
secondary_width = usable_width;
|
||||
secondary_height = usable_height / secondary_count;
|
||||
secondary_height_rem = usable_height % secondary_count;
|
||||
}
|
||||
|
||||
var i: u32 = 0;
|
||||
while (i < data.view_count) : (i += 1) {
|
||||
var x: i32 = undefined;
|
||||
var y: i32 = undefined;
|
||||
var width: u32 = undefined;
|
||||
var height: u32 = undefined;
|
||||
|
||||
if (i < main_amount) {
|
||||
x = 0;
|
||||
y = @intCast(i32, (i * main_height) + if (i > 0) main_height_rem else 0);
|
||||
width = main_width;
|
||||
height = main_height + if (i == 0) main_height_rem else 0;
|
||||
} else {
|
||||
x = @intCast(i32, main_width);
|
||||
y = @intCast(i32, (i - main_amount) * secondary_height +
|
||||
if (i > main_amount) secondary_height_rem else 0);
|
||||
width = secondary_width;
|
||||
height = secondary_height + if (i == main_amount) secondary_height_rem else 0;
|
||||
}
|
||||
|
||||
x += @intCast(i32, view_padding);
|
||||
y += @intCast(i32, view_padding);
|
||||
width -= 2 * view_padding;
|
||||
height -= 2 * view_padding;
|
||||
|
||||
switch (self.orientation) {
|
||||
.left => layout.pushViewDimensions(
|
||||
data.serial,
|
||||
x + @intCast(i32, outer_padding),
|
||||
y + @intCast(i32, outer_padding),
|
||||
width,
|
||||
height,
|
||||
),
|
||||
.right => layout.pushViewDimensions(
|
||||
data.serial,
|
||||
@intCast(i32, usable_width - width) - x + @intCast(i32, outer_padding),
|
||||
y + @intCast(i32, outer_padding),
|
||||
width,
|
||||
height,
|
||||
),
|
||||
.top => layout.pushViewDimensions(
|
||||
data.serial,
|
||||
y + @intCast(i32, outer_padding),
|
||||
x + @intCast(i32, outer_padding),
|
||||
height,
|
||||
width,
|
||||
),
|
||||
.bottom => layout.pushViewDimensions(
|
||||
data.serial,
|
||||
y + @intCast(i32, outer_padding),
|
||||
@intCast(i32, usable_width - width) - x + @intCast(i32, outer_padding),
|
||||
height,
|
||||
width,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
layout.commit(data.serial);
|
||||
},
|
||||
|
||||
.advertise_view => {},
|
||||
.advertise_done => {},
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn main() !void {
|
||||
const display = wl.Display.connect(null) catch {
|
||||
std.debug.warn("Unable to connect to Wayland server.\n", .{});
|
||||
std.os.exit(1);
|
||||
};
|
||||
defer display.disconnect();
|
||||
|
||||
var context: Context = .{};
|
||||
|
||||
const registry = try display.getRegistry();
|
||||
try registry.setListener(*Context, registryListener, &context);
|
||||
_ = try display.roundtrip();
|
||||
|
||||
if (context.layout_manager == null) {
|
||||
std.debug.warn("Wayland server does not support river_layout_unstable_v1.\n", .{});
|
||||
std.os.exit(1);
|
||||
}
|
||||
|
||||
if (context.options_manager == null) {
|
||||
std.debug.warn("Wayland server does not support river_options_unstable_v1.\n", .{});
|
||||
std.os.exit(1);
|
||||
}
|
||||
|
||||
context.configureAllOutputs();
|
||||
defer context.destroyAllOutputs();
|
||||
|
||||
while (context.running) {
|
||||
_ = try display.dispatch();
|
||||
}
|
||||
}
|
||||
|
||||
fn printUsageAndExit() noreturn {
|
||||
const usage: []const u8 =
|
||||
\\Usage: rivertile left|right|top|bottom [args passed by river]
|
||||
\\
|
||||
;
|
||||
|
||||
std.debug.warn(usage, .{});
|
||||
std.os.exit(1);
|
||||
fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, context: *Context) void {
|
||||
switch (event) {
|
||||
.global => |global| {
|
||||
if (std.cstr.cmp(global.interface, river.LayoutManagerV1.getInterface().name) == 0) {
|
||||
context.layout_manager = registry.bind(global.name, river.LayoutManagerV1, 1) catch return;
|
||||
} else if (std.cstr.cmp(global.interface, zriver.OptionsManagerV1.getInterface().name) == 0) {
|
||||
context.options_manager = registry.bind(global.name, zriver.OptionsManagerV1, 1) catch return;
|
||||
} else if (std.cstr.cmp(global.interface, wl.Output.getInterface().name) == 0) {
|
||||
context.addOutput(registry, global.name) catch {
|
||||
std.debug.warn("Failed to bind output.\n", .{});
|
||||
context.running = false;
|
||||
};
|
||||
}
|
||||
},
|
||||
.global_remove => |global| {},
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user