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:
		
				
					committed by
					
						 Isaac Freund
						Isaac Freund
					
				
			
			
				
	
			
			
			
						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| {}, | ||||
|     } | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user