Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
layer-shell: implement protocol
This commit adds a new river-layer-shell-v1 protocol that exposes the
necessary window management information from wlr-layer-shell-v1 to the
window manager.
This is an optional extension for window managers, a window manager does
not have to support layer shell. If the window manager chooses not to
support layer shell, all layer surfaces are closed immediately.
build.zig | 5 +
protocol/river-layer-shell-v1.xml | 182 ++++++++++++++
protocol/wlr-layer-shell-unstable-v1.xml | 407 +++++++++++++++++++++++++++++++
river/Cursor.zig | 27 +-
river/IdleInhibitManager.zig | 2 +-
river/InputPopup.zig | 1 +
river/LayerShell.zig | 206 ++++++++++++++++
river/LayerShellOutput.zig | 190 +++++++++++++++
river/LayerShellSeat.zig | 109 +++++++++
river/LayerSurface.zig | 188 ++++++++++++++
river/Output.zig | 28 ++-
river/Scene.zig | 11 +
river/SceneNodeData.zig | 2 +
river/Seat.zig | 78 ++++--
river/Server.zig | 7 +
15 files changed, 1401 insertions(+), 42 deletions(-)
diff --git a/build.zig b/build.zig
index 3b56e34..9c03e39 100644
--- a/build.zig
+++ b/build.zig
@@ -78,6 +78,9 @@ pub fn build(b: *Build) !void {
scanner.addCustomProtocol(b.path("protocol/river-window-management-v1.xml"));
scanner.addCustomProtocol(b.path("protocol/river-xkb-bindings-v1.xml"));
+ scanner.addCustomProtocol(b.path("protocol/river-layer-shell-v1.xml"));
+
+ scanner.addCustomProtocol(b.path("protocol/wlr-layer-shell-unstable-v1.xml"));
scanner.addCustomProtocol(b.path("protocol/wlr-output-power-management-unstable-v1.xml"));
// Some of these versions may be out of date with what wlroots implements.
@@ -104,8 +107,10 @@ pub fn build(b: *Build) !void {
scanner.generate("river_window_manager_v1", 1);
scanner.generate("river_xkb_bindings_v1", 1);
+ scanner.generate("river_layer_shell_v1", 1);
scanner.generate("zwlr_output_power_manager_v1", 1);
+ scanner.generate("zwlr_layer_shell_v1", 4);
const wayland = b.createModule(.{ .root_source_file = scanner.result });
diff --git a/protocol/river-layer-shell-v1.xml b/protocol/river-layer-shell-v1.xml
new file mode 100644
index 0000000..8959131
--- /dev/null
+++ b/protocol/river-layer-shell-v1.xml
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="river_layer_shell_v1">
+ <copyright>
+ Copyright 2025 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="layer shell support for river">
+ This protocol allows the river-window-management-v1 window manager to
+ support the wlr-layer-shell-v1 protocol.
+
+ The key words "must", "must not", "required", "shall", "shall not",
+ "should", "should not", "recommended", "may", and "optional" in this
+ document are to be interpreted as described in IETF RFC 2119.
+
+ 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_layer_shell_v1" version="1">
+ <description summary="river layer shell global interface">
+ This global interface should only be advertised to the client if the
+ river_window_manager_v1 global is also advertised. Binding this interface
+ indicates that the window manager supports layer shell.
+
+ If the window manager does not bind this interface, the compositor should
+ not allow clients to map layer surfaces. This can be achieved by
+ closing layer surfaces immediately.
+ </description>
+
+ <enum name="error">
+ <entry name="object_already_created" value="0"
+ sumary="the layer_shell_output/seat object was already created."/>
+ </enum>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the river_layer_shell_v1 object">
+ This request indicates that the client will no longer use the
+ river_layer_shell_v1 object.
+ </description>
+ </request>
+
+ <request name="get_output">
+ <description summary="get layer shell output state">
+ It is a protocol error to make this request more than once for a given
+ river_output_v1 object.
+ </description>
+ <arg name="id" type="new_id" interface="river_layer_shell_output_v1"/>
+ <arg name="output" type="object" interface="river_output_v1"/>
+ </request>
+
+ <request name="get_seat">
+ <description summary="get layer shell seat state">
+ It is a protocol error to make this request more than once for a given
+ river_seat_v1 object.
+ </description>
+ <arg name="id" type="new_id" interface="river_layer_shell_seat_v1"/>
+ <arg name="seat" type="object" interface="river_seat_v1"/>
+ </request>
+ </interface>
+
+ <interface name="river_layer_shell_output_v1" version="1">
+ <description summary="layer shell output state">
+ The lifetime of this object is tied to the corresponding river_output_v1.
+ This object is made inert when the river_output_v1.removed event is sent
+ and should be destroyed.
+ </description>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the object">
+ This request indicates that the client will no longer use the
+ river_layer_shell_output_v1 object and that it may be safely destroyed.
+
+ This request should be made after the river_output_v1.removed event is
+ received to complete destruction of the output.
+ </description>
+ </request>
+
+ <event name="non_exclusive_area">
+ <description summary="area left after subtracting exclusive zones">
+ This event indicates the area of the output remaining after subtracting
+ the exclusive zones of layer surfaces. Exclusive zones are a hint, the
+ window manager is free to ignore this area hint if it wishes.
+
+ This event will be followed by a manage_start event after all other new
+ state has been sent by the server.
+ </description>
+ <arg name="x" type="int"/>
+ <arg name="y" type="int"/>
+ <arg name="width" type="int"/>
+ <arg name="height" type="int"/>
+ </event>
+
+ <request name="set_default">
+ <description summary="Set default output for layer surfaces">
+ Mark this output as the default for new layer surfaces which do not
+ request a specific output themselves. This request overrides any
+ previous set_default request on any river_layer_shell_output_v1 object.
+
+ If no set_default request is made or if the default output is destroyed,
+ the default output is undefined until the next set_default request.
+
+ This request modifies window management state and may only be made as
+ part of a manage sequence, see the river_window_manager_v1 description.
+ </description>
+ </request>
+ </interface>
+
+ <interface name="river_layer_shell_seat_v1" version="1">
+ <description summary="layer shell seat state">
+ The lifetime of this object is tied to the corresponding river_seat_v1.
+ This object is made inert when the river_seat_v1.removed event is sent and
+ should be destroyed.
+ </description>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the object">
+ This request indicates that the client will no longer use the
+ river_layer_shell_seat_v1 object and that it may be safely destroyed.
+
+ This request should be made after the river_seat_v1.removed event is
+ received to complete destruction of the seat.
+ </description>
+ </request>
+
+ <event name="focus_exclusive">
+ <description summary="">
+ A layer shell surface will be given exclusive keyboard focus at the end
+ of the manage sequence in which this event is sent. The window manager
+ may want to update window decorations or similar to indicate that no
+ window is focused.
+
+ Until the focus_non_exclusive or focus_none event is sent, all window
+ manager requests to change focus are ignored.
+
+ This event will be followed by a manage_start event after all other new
+ state has been sent by the server.
+ </description>
+ </event>
+
+ <event name="focus_non_exclusive">
+ <description summary="">
+ A layer shell surface will be given non-exclusive keyboard focus at the
+ end of the manage sequence in which this event is sent. The window
+ manager may want to update window decorations or similar to indicate
+ that no window is focused.
+
+ The window manager continues to control focus and may choose to focus a
+ different window/shell surface at any time. If the window manager sets
+ focus during the same manage sequence in which this event is sent, the
+ layer surface will not be focused.
+
+ This event will be followed by a manage_start event after all other new
+ state has been sent by the server.
+ </description>
+ </event>
+
+ <event name="focus_none">
+ <description summary="">
+ No layer shell surface will have keyboard focus at the end of the manage
+ sequence in which this event is sent. The window manager may want to
+ return focus to whichever window last had focus, for example.
+
+ This event will be followed by a manage_start event after all other new
+ state has been sent by the server.
+ </description>
+ </event>
+ </interface>
+</protocol>
diff --git a/protocol/wlr-layer-shell-unstable-v1.xml b/protocol/wlr-layer-shell-unstable-v1.xml
new file mode 100644
index 0000000..e9f27e4
--- /dev/null
+++ b/protocol/wlr-layer-shell-unstable-v1.xml
@@ -0,0 +1,407 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<protocol name="wlr_layer_shell_unstable_v1">
+ <copyright>
+ Copyright © 2017 Drew DeVault
+
+ Permission to use, copy, modify, distribute, and sell this
+ software and its documentation for any purpose is hereby granted
+ without fee, provided that the above copyright notice appear in
+ all copies and that both that copyright notice and this permission
+ notice appear in supporting documentation, and that the name of
+ the copyright holders not be used in advertising or publicity
+ pertaining to distribution of the software without specific,
+ written prior permission. The copyright holders make no
+ representations about the suitability of this software for any
+ purpose. It is provided "as is" without express or implied
+ warranty.
+
+ THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS
+ SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND
+ FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY
+ SPECIAL, 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>
+
+ <interface name="zwlr_layer_shell_v1" version="5">
+ <description summary="create surfaces that are layers of the desktop">
+ Clients can use this interface to assign the surface_layer role to
+ wl_surfaces. Such surfaces are assigned to a "layer" of the output and
+ rendered with a defined z-depth respective to each other. They may also be
+ anchored to the edges and corners of a screen and specify input handling
+ semantics. This interface should be suitable for the implementation of
+ many desktop shell components, and a broad number of other applications
+ that interact with the desktop.
+ </description>
+
+ <request name="get_layer_surface">
+ <description summary="create a layer_surface from a surface">
+ Create a layer surface for an existing surface. This assigns the role of
+ layer_surface, or raises a protocol error if another role is already
+ assigned.
+
+ Creating a layer surface from a wl_surface which has a buffer attached
+ or committed is a client error, and any attempts by a client to attach
+ or manipulate a buffer prior to the first layer_surface.configure call
+ must also be treated as errors.
+
+ After creating a layer_surface object and setting it up, the client
+ must perform an initial commit without any buffer attached.
+ The compositor will reply with a layer_surface.configure event.
+ The client must acknowledge it and is then allowed to attach a buffer
+ to map the surface.
+
+ You may pass NULL for output to allow the compositor to decide which
+ output to use. Generally this will be the one that the user most
+ recently interacted with.
+
+ Clients can specify a namespace that defines the purpose of the layer
+ surface.
+ </description>
+ <arg name="id" type="new_id" interface="zwlr_layer_surface_v1"/>
+ <arg name="surface" type="object" interface="wl_surface"/>
+ <arg name="output" type="object" interface="wl_output" allow-null="true"/>
+ <arg name="layer" type="uint" enum="layer" summary="layer to add this surface to"/>
+ <arg name="namespace" type="string" summary="namespace for the layer surface"/>
+ </request>
+
+ <enum name="error">
+ <entry name="role" value="0" summary="wl_surface has another role"/>
+ <entry name="invalid_layer" value="1" summary="layer value is invalid"/>
+ <entry name="already_constructed" value="2" summary="wl_surface has a buffer attached or committed"/>
+ </enum>
+
+ <enum name="layer">
+ <description summary="available layers for surfaces">
+ These values indicate which layers a surface can be rendered in. They
+ are ordered by z depth, bottom-most first. Traditional shell surfaces
+ will typically be rendered between the bottom and top layers.
+ Fullscreen shell surfaces are typically rendered at the top layer.
+ Multiple surfaces can share a single layer, and ordering within a
+ single layer is undefined.
+ </description>
+
+ <entry name="background" value="0"/>
+ <entry name="bottom" value="1"/>
+ <entry name="top" value="2"/>
+ <entry name="overlay" value="3"/>
+ </enum>
+
+ <!-- Version 3 additions -->
+
+ <request name="destroy" type="destructor" since="3">
+ <description summary="destroy the layer_shell object">
+ This request indicates that the client will not use the layer_shell
+ object any more. Objects that have been created through this instance
+ are not affected.
+ </description>
+ </request>
+ </interface>
+
+ <interface name="zwlr_layer_surface_v1" version="5">
+ <description summary="layer metadata interface">
+ An interface that may be implemented by a wl_surface, for surfaces that
+ are designed to be rendered as a layer of a stacked desktop-like
+ environment.
+
+ Layer surface state (layer, size, anchor, exclusive zone,
+ margin, interactivity) is double-buffered, and will be applied at the
+ time wl_surface.commit of the corresponding wl_surface is called.
+
+ Attaching a null buffer to a layer surface unmaps it.
+
+ Unmapping a layer_surface means that the surface cannot be shown by the
+ compositor until it is explicitly mapped again. The layer_surface
+ returns to the state it had right after layer_shell.get_layer_surface.
+ The client can re-map the surface by performing a commit without any
+ buffer attached, waiting for a configure event and handling it as usual.
+ </description>
+
+ <request name="set_size">
+ <description summary="sets the size of the surface">
+ Sets the size of the surface in surface-local coordinates. The
+ compositor will display the surface centered with respect to its
+ anchors.
+
+ If you pass 0 for either value, the compositor will assign it and
+ inform you of the assignment in the configure event. You must set your
+ anchor to opposite edges in the dimensions you omit; not doing so is a
+ protocol error. Both values are 0 by default.
+
+ Size is double-buffered, see wl_surface.commit.
+ </description>
+ <arg name="width" type="uint"/>
+ <arg name="height" type="uint"/>
+ </request>
+
+ <request name="set_anchor">
+ <description summary="configures the anchor point of the surface">
+ Requests that the compositor anchor the surface to the specified edges
+ and corners. If two orthogonal edges are specified (e.g. 'top' and
+ 'left'), then the anchor point will be the intersection of the edges
+ (e.g. the top left corner of the output); otherwise the anchor point
+ will be centered on that edge, or in the center if none is specified.
+
+ Anchor is double-buffered, see wl_surface.commit.
+ </description>
+ <arg name="anchor" type="uint" enum="anchor"/>
+ </request>
+
+ <request name="set_exclusive_zone">
+ <description summary="configures the exclusive geometry of this surface">
+ Requests that the compositor avoids occluding an area with other
+ surfaces. The compositor's use of this information is
+ implementation-dependent - do not assume that this region will not
+ actually be occluded.
+
+ A positive value is only meaningful if the surface is anchored to one
+ edge or an edge and both perpendicular edges. If the surface is not
+ anchored, anchored to only two perpendicular edges (a corner), anchored
+ to only two parallel edges or anchored to all edges, a positive value
+ will be treated the same as zero.
+
+ A positive zone is the distance from the edge in surface-local
+ coordinates to consider exclusive.
+
+ Surfaces that do not wish to have an exclusive zone may instead specify
+ how they should interact with surfaces that do. If set to zero, the
+ surface indicates that it would like to be moved to avoid occluding
+ surfaces with a positive exclusive zone. If set to -1, the surface
+ indicates that it would not like to be moved to accommodate for other
+ surfaces, and the compositor should extend it all the way to the edges
+ it is anchored to.
+
+ For example, a panel might set its exclusive zone to 10, so that
+ maximized shell surfaces are not shown on top of it. A notification
+ might set its exclusive zone to 0, so that it is moved to avoid
+ occluding the panel, but shell surfaces are shown underneath it. A
+ wallpaper or lock screen might set their exclusive zone to -1, so that
+ they stretch below or over the panel.
+
+ The default value is 0.
+
+ Exclusive zone is double-buffered, see wl_surface.commit.
+ </description>
+ <arg name="zone" type="int"/>
+ </request>
+
+ <request name="set_margin">
+ <description summary="sets a margin from the anchor point">
+ Requests that the surface be placed some distance away from the anchor
+ point on the output, in surface-local coordinates. Setting this value
+ for edges you are not anchored to has no effect.
+
+ The exclusive zone includes the margin.
+
+ Margin is double-buffered, see wl_surface.commit.
+ </description>
+ <arg name="top" type="int"/>
+ <arg name="right" type="int"/>
+ <arg name="bottom" type="int"/>
+ <arg name="left" type="int"/>
+ </request>
+
+ <enum name="keyboard_interactivity">
+ <description summary="types of keyboard interaction possible for a layer shell surface">
+ Types of keyboard interaction possible for layer shell surfaces. The
+ rationale for this is twofold: (1) some applications are not interested
+ in keyboard events and not allowing them to be focused can improve the
+ desktop experience; (2) some applications will want to take exclusive
+ keyboard focus.
+ </description>
+
+ <entry name="none" value="0">
+ <description summary="no keyboard focus is possible">
+ This value indicates that this surface is not interested in keyboard
+ events and the compositor should never assign it the keyboard focus.
+
+ This is the default value, set for newly created layer shell surfaces.
+
+ This is useful for e.g. desktop widgets that display information or
+ only have interaction with non-keyboard input devices.
+ </description>
+ </entry>
+ <entry name="exclusive" value="1">
+ <description summary="request exclusive keyboard focus">
+ Request exclusive keyboard focus if this surface is above the shell surface layer.
+
+ For the top and overlay layers, the seat will always give
+ exclusive keyboard focus to the top-most layer which has keyboard
+ interactivity set to exclusive. If this layer contains multiple
+ surfaces with keyboard interactivity set to exclusive, the compositor
+ determines the one receiving keyboard events in an implementation-
+ defined manner. In this case, no guarantee is made when this surface
+ will receive keyboard focus (if ever).
+
+ For the bottom and background layers, the compositor is allowed to use
+ normal focus semantics.
+
+ This setting is mainly intended for applications that need to ensure
+ they receive all keyboard events, such as a lock screen or a password
+ prompt.
+ </description>
+ </entry>
+ <entry name="on_demand" value="2" since="4">
+ <description summary="request regular keyboard focus semantics">
+ This requests the compositor to allow this surface to be focused and
+ unfocused by the user in an implementation-defined manner. The user
+ should be able to unfocus this surface even regardless of the layer
+ it is on.
+
+ Typically, the compositor will want to use its normal mechanism to
+ manage keyboard focus between layer shell surfaces with this setting
+ and regular toplevels on the desktop layer (e.g. click to focus).
+ Nevertheless, it is possible for a compositor to require a special
+ interaction to focus or unfocus layer shell surfaces (e.g. requiring
+ a click even if focus follows the mouse normally, or providing a
+ keybinding to switch focus between layers).
+
+ This setting is mainly intended for desktop shell components (e.g.
+ panels) that allow keyboard interaction. Using this option can allow
+ implementing a desktop shell that can be fully usable without the
+ mouse.
+ </description>
+ </entry>
+ </enum>
+
+ <request name="set_keyboard_interactivity">
+ <description summary="requests keyboard events">
+ Set how keyboard events are delivered to this surface. By default,
+ layer shell surfaces do not receive keyboard events; this request can
+ be used to change this.
+
+ This setting is inherited by child surfaces set by the get_popup
+ request.
+
+ Layer surfaces receive pointer, touch, and tablet events normally. If
+ you do not want to receive them, set the input region on your surface
+ to an empty region.
+
+ Keyboard interactivity is double-buffered, see wl_surface.commit.
+ </description>
+ <arg name="keyboard_interactivity" type="uint" enum="keyboard_interactivity"/>
+ </request>
+
+ <request name="get_popup">
+ <description summary="assign this layer_surface as an xdg_popup parent">
+ This assigns an xdg_popup's parent to this layer_surface. This popup
+ should have been created via xdg_surface::get_popup with the parent set
+ to NULL, and this request must be invoked before committing the popup's
+ initial state.
+
+ See the documentation of xdg_popup for more details about what an
+ xdg_popup is and how it is used.
+ </description>
+ <arg name="popup" type="object" interface="xdg_popup"/>
+ </request>
+
+ <request name="ack_configure">
+ <description summary="ack a configure event">
+ When a configure event is received, if a client commits the
+ surface in response to the configure event, then the client
+ must make an ack_configure request sometime before the commit
+ request, passing along the serial of the configure event.
+
+ If the client receives multiple configure events before it
+ can respond to one, it only has to ack the last configure event.
+
+ A client is not required to commit immediately after sending
+ an ack_configure request - it may even ack_configure several times
+ before its next surface commit.
+
+ A client may send multiple ack_configure requests before committing, but
+ only the last request sent before a commit indicates which configure
+ event the client really is responding to.
+ </description>
+ <arg name="serial" type="uint" summary="the serial from the configure event"/>
+ </request>
+
+ <request name="destroy" type="destructor">
+ <description summary="destroy the layer_surface">
+ This request destroys the layer surface.
+ </description>
+ </request>
+
+ <event name="configure">
+ <description summary="suggest a surface change">
+ The configure event asks the client to resize its surface.
+
+ Clients should arrange their surface for the new states, and then send
+ an ack_configure request with the serial sent in this configure event at
+ some point before committing the new surface.
+
+ The client is free to dismiss all but the last configure event it
+ received.
+
+ The width and height arguments specify the size of the window in
+ surface-local coordinates.
+
+ The size is a hint, in the sense that the client is free to ignore it if
+ it doesn't resize, pick a smaller size (to satisfy aspect ratio or
+ resize in steps of NxM pixels). If the client picks a smaller size and
+ is anchored to two opposite anchors (e.g. 'top' and 'bottom'), the
+ surface will be centered on this axis.
+
+ If the width or height arguments are zero, it means the client should
+ decide its own window dimension.
+ </description>
+ <arg name="serial" type="uint"/>
+ <arg name="width" type="uint"/>
+ <arg name="height" type="uint"/>
+ </event>
+
+ <event name="closed">
+ <description summary="surface should be closed">
+ The closed event is sent by the compositor when the surface will no
+ longer be shown. The output may have been destroyed or the user may
+ have asked for it to be removed. Further changes to the surface will be
+ ignored. The client should destroy the resource after receiving this
+ event, and create a new surface if they so choose.
+ </description>
+ </event>
+
+ <enum name="error">
+ <entry name="invalid_surface_state" value="0" summary="provided surface state is invalid"/>
+ <entry name="invalid_size" value="1" summary="size is invalid"/>
+ <entry name="invalid_anchor" value="2" summary="anchor bitfield is invalid"/>
+ <entry name="invalid_keyboard_interactivity" value="3" summary="keyboard interactivity is invalid"/>
+ <entry name="invalid_exclusive_edge" value="4" summary="exclusive edge is invalid given the surface anchors"/>
+ </enum>
+
+ <enum name="anchor" bitfield="true">
+ <entry name="top" value="1" summary="the top edge of the anchor rectangle"/>
+ <entry name="bottom" value="2" summary="the bottom edge of the anchor rectangle"/>
+ <entry name="left" value="4" summary="the left edge of the anchor rectangle"/>
+ <entry name="right" value="8" summary="the right edge of the anchor rectangle"/>
+ </enum>
+
+ <!-- Version 2 additions -->
+
+ <request name="set_layer" since="2">
+ <description summary="change the layer of the surface">
+ Change the layer that the surface is rendered on.
+
+ Layer is double-buffered, see wl_surface.commit.
+ </description>
+ <arg name="layer" type="uint" enum="zwlr_layer_shell_v1.layer" summary="layer to move this surface to"/>
+ </request>
+
+ <!-- Version 5 additions -->
+
+ <request name="set_exclusive_edge" since="5">
+ <description summary="set the edge the exclusive zone will be applied to">
+ Requests an edge for the exclusive zone to apply. The exclusive
+ edge will be automatically deduced from anchor points when possible,
+ but when the surface is anchored to a corner, it will be necessary
+ to set it explicitly to disambiguate, as it is not possible to deduce
+ which one of the two corner edges should be used.
+
+ The edge must be one the surface is anchored to, otherwise the
+ invalid_exclusive_edge protocol error will be raised.
+ </description>
+ <arg name="edge" type="uint" enum="anchor"/>
+ </request>
+ </interface>
+</protocol>
diff --git a/river/Cursor.zig b/river/Cursor.zig
index b9ac020..fb0caaf 100644
--- a/river/Cursor.zig
+++ b/river/Cursor.zig
@@ -370,7 +370,7 @@ pub fn processMotionRelative(cursor: *Cursor, event: *const wlr.Pointer.event.Mo
}
fn updateHovered(cursor: *Cursor) void {
- const old = cursor.seat.wm_scheduled.window;
+ const old = cursor.seat.wm_scheduled.hovered;
if (server.scene.at(cursor.wlr_cursor.x, cursor.wlr_cursor.y)) |result| {
switch (result.data) {
.window => |window| {
@@ -379,28 +379,30 @@ fn updateHovered(cursor: *Cursor) void {
// Exclude input regions of the toplevel that extend beyond the window
if (result.surface != null and result.surface.?.getRootSurface() == toplevel.wlr_toplevel.base.surface) {
if (window.box.containsPoint(cursor.wlr_cursor.x, cursor.wlr_cursor.y)) {
- cursor.seat.wm_scheduled.window = window.ref;
+ cursor.seat.wm_scheduled.hovered = window.ref;
}
} else {
- cursor.seat.wm_scheduled.window = window.ref;
+ cursor.seat.wm_scheduled.hovered = window.ref;
}
},
.xwayland => cursor.seat.wm_scheduled.window = window.ref,
.destroying => {},
}
},
- .shell_surface, .lock_surface => cursor.seat.wm_scheduled.window = null,
+ .shell_surface, .lock_surface, .layer_surface => {
+ cursor.seat.wm_scheduled.hovered = null;
+ },
.override_redirect => {
assert(build_options.xwayland);
assert(server.xwayland != null);
- cursor.seat.wm_scheduled.window = null;
+ cursor.seat.wm_scheduled.hovered = null;
},
}
} else {
- cursor.seat.wm_scheduled.window = null;
+ cursor.seat.wm_scheduled.hovered = null;
}
- if (cursor.seat.wm_scheduled.window != old) {
+ if (cursor.seat.wm_scheduled.hovered != old) {
server.wm.dirtyWindowing();
}
}
@@ -554,6 +556,17 @@ fn interact(cursor: Cursor, result: Scene.AtResult) void {
assert(server.lock_manager.state != .unlocked);
cursor.seat.focus(.{ .lock_surface = lock_surface });
},
+ .layer_surface => |layer_surface| {
+ switch (cursor.seat.layer_shell.scheduled.focus) {
+ .none, .non_exclusive => {
+ cursor.seat.layer_shell.scheduled.focus = .{
+ .non_exclusive = layer_surface.ref,
+ };
+ server.wm.dirtyWindowing();
+ },
+ .exclusive => {},
+ }
+ },
.override_redirect => |override_redirect| {
assert(server.lock_manager.state != .locked);
override_redirect.focusIfDesired();
diff --git a/river/IdleInhibitManager.zig b/river/IdleInhibitManager.zig
index ca7e01f..287fcaa 100644
--- a/river/IdleInhibitManager.zig
+++ b/river/IdleInhibitManager.zig
@@ -58,7 +58,7 @@ pub fn checkActive(inhibit_manager: *IdleInhibitManager) void {
inhibited = true; // XXX be strict
break;
},
- .shell_surface, .lock_surface, .override_redirect => {
+ .shell_surface, .lock_surface, .layer_surface, .override_redirect => {
inhibited = true;
break;
},
diff --git a/river/InputPopup.zig b/river/InputPopup.zig
index 8fc3dd9..d9448a3 100644
--- a/river/InputPopup.zig
+++ b/river/InputPopup.zig
@@ -112,6 +112,7 @@ pub fn update(input_popup: *InputPopup) void {
.shell_surface => |shell_surface| shell_surface.popup_tree,
// TODO fix positioning for lock surfaces not at 0,0 in layout coords
.lock_surface => |_| server.scene.layers.popups,
+ .layer_surface => |layer_surface| layer_surface.popup_tree,
// Xwayland doesn't use the text-input protocol
.override_redirect => unreachable,
};
diff --git a/river/LayerShell.zig b/river/LayerShell.zig
new file mode 100644
index 0000000..4e2408b
--- /dev/null
+++ b/river/LayerShell.zig
@@ -0,0 +1,206 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2025 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, version 3.
+//
+// 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 LayerShell = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const math = std.math;
+const wlr = @import("wlroots");
+const wayland = @import("wayland");
+const wl = wayland.server.wl;
+const river = wayland.server.river;
+const zwlr = wayland.server.zwlr;
+
+const server = &@import("main.zig").server;
+const util = @import("util.zig");
+
+const LayerShellOutput = @import("LayerShellOutput.zig");
+const LayerShellSeat = @import("LayerShellSeat.zig");
+const LayerSurface = @import("LayerSurface.zig");
+const Output = @import("Output.zig");
+const SceneNodeData = @import("SceneNodeData.zig");
+const Seat = @import("Seat.zig");
+const SlotMap = @import("slotmap").SlotMap;
+
+const log = std.log.scoped(.wm);
+
+global: *wl.Global,
+wlr_shell: *wlr.LayerShellV1,
+
+/// The layer shell object of the active window manager, if any
+objects: wl.list.Head(river.LayerShellV1, null),
+
+surfaces: SlotMap(*LayerSurface) = .empty,
+
+new_surface: wl.Listener(*wlr.LayerSurfaceV1) = .init(handleNewSurface),
+
+pub fn init(layer_shell: *LayerShell) !void {
+ layer_shell.* = .{
+ .global = try wl.Global.create(server.wl_server, river.LayerShellV1, 1, *LayerShell, layer_shell, bind),
+ .wlr_shell = try wlr.LayerShellV1.create(server.wl_server, 4),
+ .objects = undefined,
+ };
+ layer_shell.objects.init();
+ layer_shell.wlr_shell.events.new_surface.add(&layer_shell.new_surface);
+}
+
+// Use a deinit function rather than listening for the wl_server to be destroyed
+// in order to avoid a signal ordering issue. The wlr.LayerShellV1 also listens
+// for the wl_server to be destroyed and asserts that the new_surface event has
+// no remaining listeners.
+pub fn deinit(layer_shell: *LayerShell) void {
+ layer_shell.global.destroy();
+ layer_shell.new_surface.link.remove();
+}
+
+fn bind(client: *wl.Client, layer_shell: *LayerShell, version: u32, id: u32) void {
+ const object = river.LayerShellV1.create(client, version, id) catch {
+ client.postNoMemory();
+ log.err("out of memory", .{});
+ return;
+ };
+ object.setHandler(?*anyopaque, handleRequest, handleDestroy, null);
+ layer_shell.objects.append(object);
+}
+
+fn handleDestroy(object: *river.LayerShellV1, _: ?*anyopaque) void {
+ object.getLink().remove();
+}
+
+fn handleRequest(
+ object: *river.LayerShellV1,
+ request: river.LayerShellV1.Request,
+ _: ?*anyopaque,
+) void {
+ switch (request) {
+ .destroy => object.destroy(),
+ .get_output => |args| {
+ const output_data = args.output.getUserData() orelse return;
+ const output: *Output = @ptrCast(@alignCast(output_data));
+ if (output.layer_shell.object != null) {
+ object.postError(
+ .object_already_created,
+ "river_layer_shell_output_v1 already created",
+ );
+ return;
+ }
+ output.layer_shell.createObject(object.getClient(), object.getVersion(), args.id);
+ },
+ .get_seat => |args| {
+ const seat_data = args.seat.getUserData() orelse return;
+ const seat: *Seat = @ptrCast(@alignCast(seat_data));
+ if (seat.layer_shell.object != null) {
+ object.postError(
+ .object_already_created,
+ "river_layer_shell_seat_v1 already created",
+ );
+ return;
+ }
+ seat.layer_shell.createObject(object.getClient(), object.getVersion(), args.id);
+ },
+ }
+}
+
+fn supported(layer_shell: *LayerShell) bool {
+ const wm_v1 = server.wm.object orelse return false;
+ var it = layer_shell.objects.iterator(.forward);
+ while (it.next()) |object| {
+ if (object.getClient() == wm_v1.getClient()) return true;
+ }
+ return false;
+}
+
+fn handleNewSurface(_: *wl.Listener(*wlr.LayerSurfaceV1), wlr_layer_surface: *wlr.LayerSurfaceV1) void {
+ log.debug(
+ "new layer surface: namespace {s}, layer {s}, anchor {b:0>4}, size {},{}, margin {},{},{},{}, exclusive_zone {}",
+ .{
+ wlr_layer_surface.namespace,
+ @tagName(wlr_layer_surface.current.layer),
+ @as(u32, @bitCast(wlr_layer_surface.current.anchor)),
+ wlr_layer_surface.current.desired_width,
+ wlr_layer_surface.current.desired_height,
+ wlr_layer_surface.current.margin.top,
+ wlr_layer_surface.current.margin.right,
+ wlr_layer_surface.current.margin.bottom,
+ wlr_layer_surface.current.margin.left,
+ wlr_layer_surface.current.exclusive_zone,
+ },
+ );
+
+ if (!server.layer_shell.supported()) {
+ log.info("window manager did not bind river_layer_shell_v1, closing layer surface", .{});
+ wlr_layer_surface.destroy();
+ return;
+ }
+
+ if (wlr_layer_surface.output == null) {
+ var it = server.om.outputs.iterator(.forward);
+ while (it.next()) |output| {
+ if (output.layer_shell.requested.default) {
+ wlr_layer_surface.output = output.wlr_output;
+ break;
+ }
+ } else {
+ if (server.om.outputs.first()) |output| {
+ log.info("window manager did not set default layer surface output, choosing arbitrary output", .{});
+ wlr_layer_surface.output = output.wlr_output;
+ } else {
+ log.err("no output available for layer surface '{s}'", .{wlr_layer_surface.namespace});
+ wlr_layer_surface.destroy();
+ return;
+ }
+ }
+ }
+
+ LayerSurface.create(wlr_layer_surface) catch {
+ wlr_layer_surface.resource.postNoMemory();
+ return;
+ };
+}
+
+pub fn updateFocus(_: *LayerShell) void {
+ // Find the topmost layer surface (if any) in the top or overlay layers which
+ // requests exclusive keyboard interactivity.
+ const to_focus = blk: {
+ for ([_]zwlr.LayerShellV1.Layer{ .overlay, .top }) |layer| {
+ const tree = server.scene.layerSurfaceTree(layer);
+ // Iterate in reverse to match rendering order.
+ var it = tree.children.iterator(.reverse);
+ while (it.next()) |node| {
+ assert(node.type == .tree);
+ const node_data: *SceneNodeData = @ptrCast(@alignCast(node.data orelse continue));
+ const layer_surface = node_data.data.layer_surface;
+ const wlr_layer_surface = layer_surface.wlr_layer_surface;
+ if (wlr_layer_surface.surface.mapped and
+ wlr_layer_surface.current.keyboard_interactive == .exclusive)
+ {
+ break :blk layer_surface;
+ }
+ }
+ }
+ break :blk null;
+ };
+
+ var it = server.input_manager.seats.iterator(.forward);
+ while (it.next()) |seat| {
+ if (to_focus) |layer_surface| {
+ seat.layer_shell.scheduled.focus = .{ .exclusive = layer_surface.ref };
+ } else if (seat.layer_shell.scheduled.focus == .exclusive) {
+ seat.layer_shell.scheduled.focus = .none;
+ }
+ }
+}
diff --git a/river/LayerShellOutput.zig b/river/LayerShellOutput.zig
new file mode 100644
index 0000000..a0dbf84
--- /dev/null
+++ b/river/LayerShellOutput.zig
@@ -0,0 +1,190 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2025 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, version 3.
+//
+// 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 LayerShellOutput = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wlr = @import("wlroots");
+const wayland = @import("wayland");
+const wl = wayland.server.wl;
+const river = wayland.server.river;
+const zwlr = wayland.server.zwlr;
+
+const server = &@import("main.zig").server;
+const util = @import("util.zig");
+
+const Output = @import("Output.zig");
+const SceneNodeData = @import("SceneNodeData.zig");
+
+const log = std.log.scoped(.wm);
+
+object: ?*river.LayerShellOutputV1 = null,
+
+scheduled: struct {
+ non_exclusive_area: wlr.Box = .{ .x = 0, .y = 0, .width = 0, .height = 0 },
+} = .{},
+sent: struct {
+ non_exclusive_area: ?wlr.Box = null,
+} = .{},
+requested: struct {
+ default: bool = false,
+} = .{},
+
+pub fn createObject(
+ shell_output: *LayerShellOutput,
+ client: *wl.Client,
+ version: u32,
+ id: u32,
+) void {
+ assert(shell_output.object == null);
+ shell_output.object = river.LayerShellOutputV1.create(client, version, id) catch {
+ client.postNoMemory();
+ log.err("out of memory", .{});
+ return;
+ };
+ shell_output.object.?.setHandler(*LayerShellOutput, handleRequest, handleDestroy, shell_output);
+ server.wm.dirtyWindowing();
+}
+
+pub fn makeInert(shell_output: *LayerShellOutput) void {
+ if (shell_output.object) |object| {
+ object.setHandler(?*anyopaque, handleRequestInert, null, null);
+ shell_output.object = null;
+ shell_output.sent = .{};
+ }
+}
+
+fn handleRequestInert(
+ object: *river.LayerShellOutputV1,
+ request: river.LayerShellOutputV1.Request,
+ _: ?*anyopaque,
+) void {
+ if (request == .destroy) object.destroy();
+}
+
+fn handleDestroy(_: *river.LayerShellOutputV1, shell_output: *LayerShellOutput) void {
+ shell_output.object = null;
+}
+
+fn handleRequest(
+ layer_shell_output_v1: *river.LayerShellOutputV1,
+ request: river.LayerShellOutputV1.Request,
+ shell_output: *LayerShellOutput,
+) void {
+ assert(shell_output.object == layer_shell_output_v1);
+ switch (request) {
+ .destroy => layer_shell_output_v1.destroy(),
+ .set_default => {
+ var it = server.om.outputs.iterator(.forward);
+ while (it.next()) |output| {
+ output.layer_shell.requested.default = false;
+ }
+ shell_output.requested.default = true;
+ },
+ }
+}
+
+pub fn arrange(shell_output: *LayerShellOutput) void {
+ const output: *Output = @fieldParentPtr("layer_shell", shell_output);
+ shell_output.scheduled.non_exclusive_area = output.scheduled.box();
+ sendConfigures(output, .exclusive);
+ sendConfigures(output, .non_exclusive);
+ if (!std.meta.eql(
+ shell_output.sent.non_exclusive_area,
+ shell_output.scheduled.non_exclusive_area,
+ )) {
+ server.wm.dirtyWindowing();
+ }
+}
+
+fn sendConfigures(
+ output: *Output,
+ mode: enum { exclusive, non_exclusive },
+) void {
+ const output_width, const output_height = output.scheduled.dimensions();
+ const output_box = output.scheduled.box();
+ for ([_]zwlr.LayerShellV1.Layer{ .background, .bottom, .top, .overlay }) |layer| {
+ const tree = server.scene.layerSurfaceTree(layer);
+ var it = tree.children.safeIterator(.forward);
+ while (it.next()) |node| {
+ assert(node.type == .tree);
+ const node_data: *SceneNodeData = @ptrCast(@alignCast(node.data orelse continue));
+ const layer_surface = node_data.data.layer_surface;
+ if (!layer_surface.wlr_layer_surface.initialized) {
+ continue;
+ }
+ if (layer_surface.wlr_layer_surface.output != output.wlr_output) {
+ continue;
+ }
+ const current = layer_surface.wlr_layer_surface.current;
+ const exclusive = current.exclusive_zone > 0;
+ if (exclusive != (mode == .exclusive)) {
+ continue;
+ }
+ {
+ var new_area = output.layer_shell.scheduled.non_exclusive_area;
+ layer_surface.scene_layer_surface.configure(&output_box, &new_area);
+ // Clients can request bogus exclusive zones larger than the output
+ // dimensions and river must handle this gracefully. It seems reasonable
+ // to close layer shell clients that would cause the usable area of the
+ // output to become less than half the width/height of its full dimensions.
+ if (new_area.width < output_width / 2 or new_area.height < output_height / 2) {
+ layer_surface.wlr_layer_surface.destroy();
+ continue;
+ }
+ output.layer_shell.scheduled.non_exclusive_area = new_area;
+ }
+ const x = layer_surface.scene_layer_surface.tree.node.x;
+ const y = layer_surface.scene_layer_surface.tree.node.y;
+ layer_surface.popup_tree.node.setPosition(x, y);
+ layer_surface.scene_layer_surface.tree.node.subsurfaceTreeSetClip(&.{
+ .x = -x,
+ .y = -y,
+ .width = output_width,
+ .height = output_height,
+ });
+ }
+ }
+}
+
+pub fn manageStart(shell_output: *LayerShellOutput) void {
+ const output: *Output = @fieldParentPtr("layer_shell", shell_output);
+ assert(output.scheduled.state == .enabled or output.scheduled.state == .disabled_soft);
+
+ const scheduled_width, const scheduled_height = output.scheduled.dimensions();
+ const sent_width, const sent_height = output.sent.dimensions();
+
+ if (scheduled_width != sent_width or scheduled_height != sent_height) {
+ shell_output.scheduled.non_exclusive_area = output.scheduled.box();
+ sendConfigures(output, .exclusive);
+ sendConfigures(output, .non_exclusive);
+ }
+ if (!std.meta.eql(
+ shell_output.sent.non_exclusive_area,
+ shell_output.scheduled.non_exclusive_area,
+ )) {
+ if (shell_output.object) |layer_shell_output_v1| {
+ layer_shell_output_v1.sendNonExclusiveArea(
+ shell_output.scheduled.non_exclusive_area.x,
+ shell_output.scheduled.non_exclusive_area.y,
+ shell_output.scheduled.non_exclusive_area.width,
+ shell_output.scheduled.non_exclusive_area.height,
+ );
+ }
+ shell_output.sent.non_exclusive_area = shell_output.scheduled.non_exclusive_area;
+ }
+}
diff --git a/river/LayerShellSeat.zig b/river/LayerShellSeat.zig
new file mode 100644
index 0000000..a73bd88
--- /dev/null
+++ b/river/LayerShellSeat.zig
@@ -0,0 +1,109 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2025 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, version 3.
+//
+// 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 LayerShellSeat = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wlr = @import("wlroots");
+const wayland = @import("wayland");
+const wl = wayland.server.wl;
+const river = wayland.server.river;
+
+const server = &@import("main.zig").server;
+const util = @import("util.zig");
+
+const LayerSurface = @import("LayerSurface.zig");
+const Seat = @import("Seat.zig");
+
+const log = std.log.scoped(.wm);
+
+const Focus = union(enum) {
+ exclusive: LayerSurface.Ref,
+ non_exclusive: LayerSurface.Ref,
+ none,
+};
+
+object: ?*river.LayerShellSeatV1 = null,
+
+scheduled: struct {
+ focus: Focus = .none,
+} = .{},
+sent: struct {
+ focus: Focus = .none,
+} = .{},
+requested: struct {} = .{},
+
+pub fn createObject(
+ shell_seat: *LayerShellSeat,
+ client: *wl.Client,
+ version: u32,
+ id: u32,
+) void {
+ assert(shell_seat.object == null);
+ shell_seat.object = river.LayerShellSeatV1.create(client, version, id) catch {
+ client.postNoMemory();
+ return;
+ };
+ shell_seat.object.?.setHandler(*LayerShellSeat, handleRequest, handleDestroy, shell_seat);
+ server.wm.dirtyWindowing();
+}
+
+pub fn makeInert(shell_seat: *LayerShellSeat) void {
+ if (shell_seat.object) |object| {
+ object.setHandler(?*anyopaque, handleRequestInert, null, null);
+ shell_seat.object = null;
+ }
+}
+
+fn handleRequestInert(
+ object: *river.LayerShellSeatV1,
+ request: river.LayerShellSeatV1.Request,
+ _: ?*anyopaque,
+) void {
+ if (request == .destroy) object.destroy();
+}
+
+fn handleDestroy(_: *river.LayerShellSeatV1, shell_seat: *LayerShellSeat) void {
+ shell_seat.object = null;
+}
+
+fn handleRequest(
+ object: *river.LayerShellSeatV1,
+ request: river.LayerShellSeatV1.Request,
+ shell_seat: *LayerShellSeat,
+) void {
+ assert(shell_seat.object == object);
+ switch (request) {
+ .destroy => object.destroy(),
+ }
+}
+
+pub fn manageStart(shell_seat: *LayerShellSeat) void {
+ if (@as(std.meta.Tag(Focus), shell_seat.scheduled.focus) != shell_seat.sent.focus) {
+ if (shell_seat.object) |shell_seat_v1| {
+ switch (shell_seat.scheduled.focus) {
+ .exclusive => shell_seat_v1.sendFocusExclusive(),
+ .non_exclusive => shell_seat_v1.sendFocusNonExclusive(),
+ .none => shell_seat_v1.sendFocusNone(),
+ }
+ }
+ }
+ shell_seat.sent.focus = shell_seat.scheduled.focus;
+ if (shell_seat.scheduled.focus == .non_exclusive) {
+ shell_seat.scheduled.focus = .none;
+ }
+}
diff --git a/river/LayerSurface.zig b/river/LayerSurface.zig
new file mode 100644
index 0000000..9502323
--- /dev/null
+++ b/river/LayerSurface.zig
@@ -0,0 +1,188 @@
+// This file is part of river, a dynamic tiling wayland compositor.
+//
+// Copyright 2025 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, version 3.
+//
+// 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 LayerSurface = @This();
+
+const std = @import("std");
+const assert = std.debug.assert;
+const wlr = @import("wlroots");
+const wl = @import("wayland").server.wl;
+const zwlr = @import("wayland").server.zwlr;
+
+const server = &@import("main.zig").server;
+const util = @import("util.zig");
+
+const Output = @import("Output.zig");
+const SceneNodeData = @import("SceneNodeData.zig");
+const SlotMap = @import("slotmap").SlotMap;
+const XdgPopup = @import("XdgPopup.zig");
+
+const log = std.log.scoped(.wm);
+
+/// Only packed in order to make == work.
+pub const Ref = packed struct {
+ key: SlotMap(*LayerSurface).Key,
+
+ pub fn get(ref: Ref) ?*LayerSurface {
+ return server.layer_shell.surfaces.get(ref.key);
+ }
+};
+
+ref: Ref,
+
+wlr_layer_surface: *wlr.LayerSurfaceV1,
+scene_layer_surface: *wlr.SceneLayerSurfaceV1,
+popup_tree: *wlr.SceneTree,
+
+destroy: wl.Listener(*wlr.LayerSurfaceV1) = wl.Listener(*wlr.LayerSurfaceV1).init(handleDestroy),
+map: wl.Listener(void) = wl.Listener(void).init(handleMap),
+unmap: wl.Listener(void) = wl.Listener(void).init(handleUnmap),
+commit: wl.Listener(*wlr.Surface) = wl.Listener(*wlr.Surface).init(handleCommit),
+new_popup: wl.Listener(*wlr.XdgPopup) = wl.Listener(*wlr.XdgPopup).init(handleNewPopup),
+
+pub fn create(wlr_layer_surface: *wlr.LayerSurfaceV1) error{OutOfMemory}!void {
+ const layer_surface = try util.gpa.create(LayerSurface);
+ errdefer util.gpa.destroy(layer_surface);
+
+ const key = try server.layer_shell.surfaces.put(util.gpa, layer_surface);
+ errdefer server.layer_shell.surfaces.remove(key);
+
+ const layer_tree = server.scene.layerSurfaceTree(wlr_layer_surface.current.layer);
+
+ layer_surface.* = .{
+ .ref = .{ .key = key },
+ .wlr_layer_surface = wlr_layer_surface,
+ .scene_layer_surface = try layer_tree.createSceneLayerSurfaceV1(wlr_layer_surface),
+ .popup_tree = try server.scene.layers.popups.createSceneTree(),
+ };
+
+ try SceneNodeData.attach(&layer_surface.scene_layer_surface.tree.node, .{ .layer_surface = layer_surface });
+ try SceneNodeData.attach(&layer_surface.popup_tree.node, .{ .layer_surface = layer_surface });
+
+ wlr_layer_surface.surface.data = &layer_surface.scene_layer_surface.tree.node;
+
+ wlr_layer_surface.events.destroy.add(&layer_surface.destroy);
+ wlr_layer_surface.surface.events.map.add(&layer_surface.map);
+ wlr_layer_surface.surface.events.unmap.add(&layer_surface.unmap);
+ wlr_layer_surface.surface.events.commit.add(&layer_surface.commit);
+ wlr_layer_surface.events.new_popup.add(&layer_surface.new_popup);
+}
+
+pub fn destroyPopups(layer_surface: *LayerSurface) void {
+ var it = layer_surface.wlr_layer_surface.popups.safeIterator(.forward);
+ while (it.next()) |wlr_xdg_popup| wlr_xdg_popup.destroy();
+}
+
+fn handleDestroy(listener: *wl.Listener(*wlr.LayerSurfaceV1), _: *wlr.LayerSurfaceV1) void {
+ const layer_surface: *LayerSurface = @fieldParentPtr("destroy", listener);
+
+ log.debug("layer surface '{s}' destroyed", .{layer_surface.wlr_layer_surface.namespace});
+
+ layer_surface.destroy.link.remove();
+ layer_surface.map.link.remove();
+ layer_surface.unmap.link.remove();
+ layer_surface.commit.link.remove();
+ layer_surface.new_popup.link.remove();
+
+ layer_surface.destroyPopups();
+
+ layer_surface.popup_tree.node.destroy();
+
+ // The wlr_surface may outlive the wlr_layer_surface so we must clean up the user data.
+ layer_surface.wlr_layer_surface.surface.data = null;
+
+ server.layer_shell.surfaces.remove(layer_surface.ref.key);
+ util.gpa.destroy(layer_surface);
+}
+
+fn handleMap(listener: *wl.Listener(void)) void {
+ const layer_surface: *LayerSurface = @fieldParentPtr("map", listener);
+ const wlr_layer_surface = layer_surface.wlr_layer_surface;
+
+ log.debug("layer surface '{s}' mapped", .{wlr_layer_surface.namespace});
+
+ if (wlr_layer_surface.current.keyboard_interactive == .on_demand) {
+ var it = server.input_manager.seats.iterator(.forward);
+ while (it.next()) |seat| {
+ if (seat.layer_shell.scheduled.focus != .exclusive) {
+ seat.layer_shell.scheduled.focus = .{ .non_exclusive = layer_surface.ref };
+ }
+ }
+ }
+
+ // Beware: it is possible for arrange() to destroy this LayerSurface!
+ const output: *Output = @ptrCast(@alignCast(layer_surface.wlr_layer_surface.output.?.data));
+ output.layer_shell.arrange();
+ server.layer_shell.updateFocus();
+ server.wm.dirtyWindowing();
+}
+
+fn handleUnmap(listener: *wl.Listener(void)) void {
+ const layer_surface: *LayerSurface = @fieldParentPtr("unmap", listener);
+
+ log.debug("layer surface '{s}' unmapped", .{layer_surface.wlr_layer_surface.namespace});
+
+ {
+ var it = server.input_manager.seats.iterator(.forward);
+ while (it.next()) |seat| {
+ if (seat.focused == .layer_surface and seat.focused.layer_surface == layer_surface) {
+ seat.focus(.none);
+ }
+ }
+ }
+
+ // Beware: it is possible for arrange() to destroy this LayerSurface!
+ const output: *Output = @ptrCast(@alignCast(layer_surface.wlr_layer_surface.output.?.data));
+ output.layer_shell.arrange();
+ server.layer_shell.updateFocus();
+ server.wm.dirtyWindowing();
+}
+
+fn handleCommit(listener: *wl.Listener(*wlr.Surface), _: *wlr.Surface) void {
+ const layer_surface: *LayerSurface = @fieldParentPtr("commit", listener);
+ const wlr_layer_surface = layer_surface.wlr_layer_surface;
+
+ assert(wlr_layer_surface.output != null);
+
+ // If the layer was changed, move the LayerSurface to the proper tree.
+ if (wlr_layer_surface.current.committed.layer) {
+ const tree = server.scene.layerSurfaceTree(wlr_layer_surface.current.layer);
+ layer_surface.scene_layer_surface.tree.node.reparent(tree);
+ }
+
+ if (wlr_layer_surface.initial_commit or
+ @as(u32, @bitCast(wlr_layer_surface.current.committed)) != 0)
+ {
+ // Beware: it is possible for arrange() to destroy this LayerSurface!
+ const output: *Output = @ptrCast(@alignCast(layer_surface.wlr_layer_surface.output.?.data));
+ output.layer_shell.arrange();
+ server.layer_shell.updateFocus();
+ server.wm.dirtyWindowing();
+ }
+}
+
+fn handleNewPopup(listener: *wl.Listener(*wlr.XdgPopup), wlr_xdg_popup: *wlr.XdgPopup) void {
+ const layer_surface: *LayerSurface = @fieldParentPtr("new_popup", listener);
+
+ XdgPopup.create(
+ wlr_xdg_popup,
+ layer_surface.popup_tree,
+ layer_surface.popup_tree,
+ ) catch {
+ wlr_xdg_popup.resource.postNoMemory();
+ return;
+ };
+}
diff --git a/river/Output.zig b/river/Output.zig
index 3eef3ba..be05c07 100644
--- a/river/Output.zig
+++ b/river/Output.zig
@@ -1,6 +1,6 @@
// This file is part of river, a dynamic tiling wayland compositor.
//
-// Copyright 2020-2024 The River Developers
+// Copyright 2020-2025 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
@@ -31,6 +31,7 @@ const river = wayland.server.river;
const server = &@import("main.zig").server;
const util = @import("util.zig");
+const LayerShellOutput = @import("LayerShellOutput.zig");
const LockSurface = @import("LockSurface.zig");
const SceneNodeData = @import("SceneNodeData.zig");
const Window = @import("Window.zig");
@@ -86,6 +87,11 @@ pub const State = struct {
};
}
+ pub fn box(state: *const State) wlr.Box {
+ const w, const h = state.dimensions();
+ return .{ .x = state.x, .y = state.y, .width = w, .height = h };
+ }
+
pub fn applyNoModeset(state: *const State, wlr_state: *wlr.Output.State) void {
wlr_state.setScale(state.scale);
wlr_state.setTransform(state.transform);
@@ -108,6 +114,7 @@ wlr_output: ?*wlr.Output,
scene_output: ?*wlr.SceneOutput,
object: ?*river.OutputV1 = null,
+layer_shell: LayerShellOutput = .{},
/// Tracks the currently presented frame on the output as it pertains to ext-session-lock.
/// The output is initially considered blanked:
@@ -221,6 +228,8 @@ pub fn manageStart(output: *Output) void {
.enabled, .disabled_soft => {
const wlr_output = output.wlr_output.?;
+ output.layer_shell.manageStart();
+
if (server.wm.object) |wm_v1| {
const new = output.object == null;
const output_v1 = output.object orelse blk: {
@@ -237,9 +246,6 @@ pub fn manageStart(output: *Output) void {
};
errdefer comptime unreachable;
- const pending = &output.scheduled;
- const sent = &output.sent;
-
if (!output.sent_wl_output) {
// wl_output globals are created/destroyed by the wlroots output layout.
if (wlr_output.global) |global| {
@@ -248,14 +254,17 @@ pub fn manageStart(output: *Output) void {
}
}
- const pending_width, const pending_height = pending.dimensions();
+ const scheduled = &output.scheduled;
+ const sent = &output.sent;
+
+ const scheduled_width, const scheduled_height = scheduled.dimensions();
const sent_width, const sent_height = sent.dimensions();
- if (new or pending_width != sent_width or pending_height != sent_height) {
- output_v1.sendDimensions(pending_width, pending_height);
+ if (new or scheduled_width != sent_width or scheduled_height != sent_height) {
+ output_v1.sendDimensions(scheduled_width, scheduled_height);
}
- if (new or pending.x != sent.x or pending.y != sent.y) {
- output_v1.sendPosition(pending.x, pending.y);
+ if (new or scheduled.x != sent.x or scheduled.y != sent.y) {
+ output_v1.sendPosition(scheduled.x, scheduled.y);
}
}
@@ -268,6 +277,7 @@ pub fn manageStart(output: *Output) void {
if (output.object) |output_v1| {
output_v1.sendRemoved();
output_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
+ output.layer_shell.makeInert();
output.object = null;
}
diff --git a/river/Scene.zig b/river/Scene.zig
index b1b5ac9..33eb69e 100644
--- a/river/Scene.zig
+++ b/river/Scene.zig
@@ -20,6 +20,7 @@ const std = @import("std");
const assert = std.debug.assert;
const build_options = @import("build_options");
const wlr = @import("wlroots");
+const zwlr = @import("wayland").server.zwlr;
const server = &@import("main.zig").server;
@@ -130,6 +131,16 @@ pub fn at(scene: *const Scene, lx: f64, ly: f64) ?AtResult {
}
}
+pub fn layerSurfaceTree(scene: *Scene, layer: zwlr.LayerShellV1.Layer) *wlr.SceneTree {
+ return switch (layer) {
+ .background => scene.layers.background,
+ .bottom => scene.layers.bottom,
+ .top => scene.layers.top,
+ .overlay => scene.layers.overlay,
+ _ => unreachable,
+ };
+}
+
pub const SaveableSurfaces = struct {
tree: *wlr.SceneTree,
saved: *wlr.SceneTree,
diff --git a/river/SceneNodeData.zig b/river/SceneNodeData.zig
index 324a410..0e5a0ac 100644
--- a/river/SceneNodeData.zig
+++ b/river/SceneNodeData.zig
@@ -23,6 +23,7 @@ const wl = @import("wayland").server.wl;
const util = @import("util.zig");
const LockSurface = @import("LockSurface.zig");
+const LayerSurface = @import("LayerSurface.zig");
const InputPopup = @import("InputPopup.zig");
const Window = @import("Window.zig");
const ShellSurface = @import("ShellSurface.zig");
@@ -32,6 +33,7 @@ pub const Data = union(enum) {
window: *Window,
shell_surface: *ShellSurface,
lock_surface: *LockSurface,
+ layer_surface: *LayerSurface,
override_redirect: if (build_options.xwayland) *XwaylandOverrideRedirect else noreturn,
};
diff --git a/river/Seat.zig b/river/Seat.zig
index fc61808..09333d6 100644
--- a/river/Seat.zig
+++ b/river/Seat.zig
@@ -36,6 +36,8 @@ const InputManager = @import("InputManager.zig");
const InputRelay = @import("InputRelay.zig");
const Keyboard = @import("Keyboard.zig");
const KeyboardGroup = @import("KeyboardGroup.zig");
+const LayerShellSeat = @import("LayerShellSeat.zig");
+const LayerSurface = @import("LayerSurface.zig");
const LockSurface = @import("LockSurface.zig");
const Output = @import("Output.zig");
const PointerBinding = @import("PointerBinding.zig");
@@ -79,18 +81,13 @@ pub const Event = union(enum) {
pointer_pinch_end: wlr.Pointer.event.PinchEnd,
};
-pub const WmFocus = union(enum) {
- none,
- window: Window.Ref,
- shell_surface: *ShellSurface,
-};
-
pub const Focus = union(enum) {
none,
window: *Window,
shell_surface: *ShellSurface,
override_redirect: if (build_options.xwayland) *XwaylandOverrideRedirect else noreturn,
lock_surface: *LockSurface,
+ layer_surface: *LayerSurface,
pub fn surface(target: Focus) ?*wlr.Surface {
return switch (target) {
@@ -98,6 +95,7 @@ pub const Focus = union(enum) {
.shell_surface => |shell_surface| shell_surface.surface,
.override_redirect => |override_redirect| override_redirect.xsurface.surface,
.lock_surface => |lock_surface| lock_surface.wlr_lock_surface.surface,
+ .layer_surface => |layer_surface| layer_surface.wlr_layer_surface.surface,
.none => null,
};
}
@@ -110,28 +108,38 @@ link: wl.list.Link,
destroying: bool = false,
object: ?*river.SeatV1 = null,
+layer_shell: LayerShellSeat = .{},
event_queue: Deque(Event),
/// State to be sent to the wm in the next manage sequence.
wm_scheduled: struct {
/// The window entered/hovered by the pointer, if any
- window: ?Window.Ref = null,
+ hovered: ?Window.Ref = null,
/// The window clicked on, touched, etc.
- interaction: WmFocus = .none,
+ interaction: union(enum) {
+ none,
+ window: Window.Ref,
+ shell_surface: *ShellSurface,
+ } = .none,
op_release: bool = false,
} = .{},
/// State sent to the wm in the latest manage sequence.
wm_sent: struct {
/// The window entered/hovered by the pointer, if any
- window: ?Window.Ref = null,
+ hovered: ?Window.Ref = null,
} = .{},
link_sent: wl.list.Link,
/// Windowing state requested by the wm.
wm_requested: struct {
- focus: WmFocus = .none,
+ focus: union(enum) {
+ none,
+ clear,
+ window: Window.Ref,
+ shell_surface: *ShellSurface,
+ } = .none,
op: union(enum) {
none,
start_pointer,
@@ -298,12 +306,15 @@ pub fn manageStart(seat: *Seat) void {
if (seat.object) |seat_v1| {
seat_v1.sendRemoved();
seat_v1.setHandler(?*anyopaque, handleRequestInert, null, null);
+ seat.layer_shell.makeInert();
seat.object = null;
}
seat.destroy();
return;
}
+ seat.layer_shell.manageStart();
+
if (server.wm.object) |wm_v1| {
const new = seat.object == null;
const seat_v1 = seat.object orelse blk: {
@@ -328,24 +339,24 @@ pub fn manageStart(seat: *Seat) void {
}
if (new) {
- if (seat.wm_scheduled.window) |ref| {
+ if (seat.wm_scheduled.hovered) |ref| {
if (ref.get()) |window| {
if (window.object) |window_v1| {
seat_v1.sendPointerEnter(window_v1);
- seat.wm_sent.window = seat.wm_scheduled.window;
+ seat.wm_sent.hovered = seat.wm_scheduled.hovered;
}
}
}
- } else if (seat.wm_scheduled.window != seat.wm_sent.window) {
- if (seat.wm_sent.window != null) {
+ } else if (seat.wm_scheduled.hovered != seat.wm_sent.hovered) {
+ if (seat.wm_sent.hovered != null) {
seat_v1.sendPointerLeave();
- seat.wm_sent.window = null;
+ seat.wm_sent.hovered = null;
}
- if (seat.wm_scheduled.window) |ref| {
+ if (seat.wm_scheduled.hovered) |ref| {
if (ref.get()) |window| {
if (window.object) |window_v1| {
seat_v1.sendPointerEnter(window_v1);
- seat.wm_sent.window = seat.wm_scheduled.window;
+ seat.wm_sent.hovered = seat.wm_scheduled.hovered;
}
}
}
@@ -453,7 +464,7 @@ fn handleRequest(
const shell_surface: *ShellSurface = @ptrCast(@alignCast(data));
seat.wm_requested.focus = .{ .shell_surface = shell_surface };
},
- .clear_focus => seat.wm_requested.focus = .none,
+ .clear_focus => seat.wm_requested.focus = .clear,
.op_start_pointer => {
if (!server.wm.ensureWindowing()) return;
@@ -487,15 +498,31 @@ fn handleRequest(
pub fn manageFinish(seat: *Seat) void {
if (server.lock_manager.state != .unlocked) return;
- switch (seat.wm_requested.focus) {
- .none => seat.focus(.none),
- .window => |ref| {
- if (ref.get()) |window| {
+ switch (seat.layer_shell.sent.focus) {
+ .exclusive => |ref| if (ref.get()) |layer_surface| {
+ seat.focus(.{ .layer_surface = layer_surface });
+ },
+ .non_exclusive, .none => switch (seat.wm_requested.focus) {
+ .none => switch (seat.layer_shell.sent.focus) {
+ .exclusive => unreachable,
+ .non_exclusive => |ref| if (ref.get()) |layer_surface| {
+ seat.focus(.{ .layer_surface = layer_surface });
+ seat.layer_shell.sent.focus = .none;
+ },
+ .none => {},
+ },
+ .clear => {
+ seat.focus(.none);
+ },
+ .window => |ref| if (ref.get()) |window| {
seat.focus(.{ .window = window });
- }
+ },
+ .shell_surface => |shell_surface| {
+ seat.focus(.{ .shell_surface = shell_surface });
+ },
},
- .shell_surface => |shell_surface| seat.focus(.{ .shell_surface = shell_surface }),
}
+ seat.wm_requested.focus = .none;
switch (seat.wm_requested.op) {
.none => {},
@@ -525,12 +552,13 @@ pub fn focus(seat: *Seat, new_focus: Focus) void {
// First clear the current focus
switch (seat.focused) {
.window => |window| window.destroyPopups(),
+ .layer_surface => |layer_surface| layer_surface.destroyPopups(),
.shell_surface, .override_redirect, .lock_surface, .none => {},
}
// Set the new focus
switch (new_focus) {
- .window, .shell_surface => assert(server.lock_manager.state != .locked),
+ .window, .shell_surface, .layer_surface => assert(server.lock_manager.state != .locked),
.lock_surface => assert(server.lock_manager.state != .unlocked),
.override_redirect, .none => {},
}
diff --git a/river/Server.zig b/river/Server.zig
index e93f6b5..ea90628 100644
--- a/river/Server.zig
+++ b/river/Server.zig
@@ -39,6 +39,7 @@ const Seat = @import("Seat.zig");
const TabletTool = @import("TabletTool.zig");
const WindowManager = @import("WindowManager.zig");
const XkbBindings = @import("XkbBindings.zig");
+const LayerShell = @import("LayerShell.zig");
const XdgDecoration = @import("XdgDecoration.zig");
const XdgToplevel = @import("XdgToplevel.zig");
const XwaylandOverrideRedirect = @import("XwaylandOverrideRedirect.zig");
@@ -91,6 +92,7 @@ idle_inhibit_manager: IdleInhibitManager,
lock_manager: LockManager,
wm: WindowManager,
xkb_bindings: XkbBindings,
+layer_shell: LayerShell,
xwayland: if (build_options.xwayland) ?*wlr.Xwayland else void = if (build_options.xwayland) null,
new_xsurface: if (build_options.xwayland) wl.Listener(*wlr.XwaylandSurface) else void =
@@ -158,6 +160,7 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void {
.lock_manager = undefined,
.wm = undefined,
.xkb_bindings = undefined,
+ .layer_shell = undefined,
};
if (renderer.getTextureFormats(@intFromEnum(wlr.BufferCap.dmabuf)) != null) {
@@ -177,6 +180,7 @@ pub fn init(server: *Server, runtime_xwayland: bool) !void {
try server.wm.init();
try server.xkb_bindings.init();
+ try server.layer_shell.init();
try server.scene.init();
try server.om.init();
try server.input_manager.init();
@@ -227,6 +231,7 @@ pub fn deinit(server: *Server) void {
server.input_manager.deinit();
server.idle_inhibit_manager.deinit();
server.lock_manager.deinit();
+ server.layer_shell.deinit();
server.wl_server.destroy();
@@ -320,6 +325,8 @@ fn allowlist(server: *Server, global: *const wl.Global) bool {
fn blocklist(server: *Server, global: *const wl.Global) bool {
return global == server.security_context_manager.global or
global == server.wm.global or
+ global == server.layer_shell.global or
+ global == server.layer_shell.wlr_shell.global or
global == server.xkb_bindings.global or
global == server.foreign_toplevel_manager.global or
global == server.screencopy_manager.global or