stevee

My wayland statusbar
git clone git://gtms.dev/stevee
Log | Files | Refs | Submodules | README | LICENSE

commit e2a1a91f361fcce9616d73210f877fa65845d821
parent 84b6cd7bc5158a00badaa0c79694d1b6b3d7a2a5
Author: Andrea Feletto <andrea@andreafeletto.com>
Date:   Wed, 19 Jan 2022 16:29:21 +0100

black rectangle correctly rendered

Diffstat:
A.gitmodules | 6++++++
Mbuild.zig | 28++++++++++++++++++++++++++++
Adeps/zig-pixman | 1+
Adeps/zig-wayland | 1+
Aprotocol/river-status-unstable-v1.xml | 117+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aprotocol/wlr-layer-shell-unstable-v1.xml | 390+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/event.zig | 84+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/main.zig | 23+++++++++++++++++++++++
Asrc/render.zig | 31+++++++++++++++++++++++++++++++
Asrc/shm.zig | 69+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/wayland.zig | 201+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
11 files changed, 951 insertions(+), 0 deletions(-)

diff --git a/.gitmodules b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "deps/zig-wayland"] + path = deps/zig-wayland + url = https://github.com/ifreund/zig-wayland +[submodule "deps/zig-pixman"] + path = deps/zig-pixman + url = https://github.com/ifreund/zig-pixman diff --git a/build.zig b/build.zig @@ -1,12 +1,40 @@ const std = @import("std"); +const Pkg = std.build.Pkg; + +const ScanProtocolsStep = @import("deps/zig-wayland/build.zig").ScanProtocolsStep; pub fn build(b: *std.build.Builder) void { const target = b.standardTargetOptions(.{}); const mode = b.standardReleaseOptions(); + const scanner = ScanProtocolsStep.create(b); + scanner.addSystemProtocol("stable/xdg-shell/xdg-shell.xml"); + scanner.addProtocolPath("protocol/wlr-layer-shell-unstable-v1.xml"); + scanner.addProtocolPath("protocol/river-status-unstable-v1.xml"); + + const wayland = Pkg{ + .name = "wayland", + .path = .{ .generated = &scanner.result }, + }; + const pixman = Pkg{ + .name = "pixman", + .path = .{ .path = "deps/zig-pixman/pixman.zig" }, + }; + const exe = b.addExecutable("levee", "src/main.zig"); exe.setTarget(target); exe.setBuildMode(mode); + + exe.linkLibC(); + + exe.addPackage(wayland); + exe.linkSystemLibrary("wayland-client"); + exe.step.dependOn(&scanner.step); + scanner.addCSource(exe); + + exe.addPackage(pixman); + exe.linkSystemLibrary("pixman-1"); + exe.install(); const run_cmd = exe.run(); diff --git a/deps/zig-pixman b/deps/zig-pixman @@ -0,0 +1 @@ +Subproject commit d381567de9b6e40dd7f4c6e0b5740f94ebd8c9d7 diff --git a/deps/zig-wayland b/deps/zig-wayland @@ -0,0 +1 @@ +Subproject commit d4bf1dd7aef84731e9d7274a8146322956a8b4ab diff --git a/protocol/river-status-unstable-v1.xml b/protocol/river-status-unstable-v1.xml @@ -0,0 +1,117 @@ +<?xml version="1.0" encoding="UTF-8"?> +<protocol name="river_status_unstable_v1"> + <copyright> + Copyright 2020 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> + + <interface name="zriver_status_manager_v1" version="1"> + <description summary="manage river status objects"> + A global factory for objects that receive status information specific + to river. It could be used to implement, for example, a status bar. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the river_status_manager object"> + This request indicates that the client will not use the + river_status_manager object any more. Objects that have been created + through this instance are not affected. + </description> + </request> + + <request name="get_river_output_status"> + <description summary="create an output status object"> + This creates a new river_output_status object for the given wl_output. + </description> + <arg name="id" type="new_id" interface="zriver_output_status_v1"/> + <arg name="output" type="object" interface="wl_output"/> + </request> + + <request name="get_river_seat_status"> + <description summary="create a seat status object"> + This creates a new river_seat_status object for the given wl_seat. + </description> + <arg name="id" type="new_id" interface="zriver_seat_status_v1"/> + <arg name="seat" type="object" interface="wl_seat"/> + </request> + </interface> + + <interface name="zriver_output_status_v1" version="1"> + <description summary="track output tags and focus"> + This interface allows clients to receive information about the current + windowing state of an output. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the river_output_status object"> + This request indicates that the client will not use the + river_output_status object any more. + </description> + </request> + + <event name="focused_tags"> + <description summary="focused tags of the output"> + Sent once binding the interface and again whenever the tag focus of + the output changes. + </description> + <arg name="tags" type="uint" summary="32-bit bitfield"/> + </event> + + <event name="view_tags"> + <description summary="tag state of an output's views"> + Sent once on binding the interface and again whenever the tag state + of the output changes. + </description> + <arg name="tags" type="array" summary="array of 32-bit bitfields"/> + </event> + </interface> + + <interface name="zriver_seat_status_v1" version="1"> + <description summary="track seat focus"> + This interface allows clients to receive information about the current + focus of a seat. Note that (un)focused_output events will only be sent + if the client has bound the relevant wl_output globals. + </description> + + <request name="destroy" type="destructor"> + <description summary="destroy the river_seat_status object"> + This request indicates that the client will not use the + river_seat_status object any more. + </description> + </request> + + <event name="focused_output"> + <description summary="the seat focused an output"> + Sent on binding the interface and again whenever an output gains focus. + </description> + <arg name="output" type="object" interface="wl_output"/> + </event> + + <event name="unfocused_output"> + <description summary="the seat unfocused an output"> + Sent whenever an output loses focus. + </description> + <arg name="output" type="object" interface="wl_output"/> + </event> + + <event name="focused_view"> + <description summary="information on the focused view"> + Sent once on binding the interface and again whenever the focused + view or a property thereof changes. The title may be an empty string + if no view is focused or the focused view did not set a title. + </description> + <arg name="title" type="string" summary="title of the focused view"/> + </event> + </interface> +</protocol> diff --git a/protocol/wlr-layer-shell-unstable-v1.xml b/protocol/wlr-layer-shell-unstable-v1.xml @@ -0,0 +1,390 @@ +<?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="4"> + <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="4"> + <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"/> + </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> + </interface> +</protocol> diff --git a/src/event.zig b/src/event.zig @@ -0,0 +1,84 @@ +const std = @import("std"); +const mem = std.mem; +const os = std.os; +const ArrayList = std.ArrayList; + +const wl = @import("wayland").client.wl; + +const State = @import("main.zig").State; + +pub const Loop = struct { + state: *State, + + fds: [2]os.pollfd, + timers: ArrayList(*Timer), + + pub fn init(state: *State) !Loop { + const tfd = os.linux.timerfd_create( + os.CLOCK.MONOTONIC, + os.linux.TFD.CLOEXEC, + ); + + return Loop{ + .state = state, + .fds = .{ + .{ + .fd = state.wayland.display.getFd(), + .events = os.POLL.IN, + .revents = 0, + }, + .{ + .fd = @intCast(os.fd_t, tfd), + .events = os.POLL.IN, + .revents = 0, + }, + }, + .timers = ArrayList(*Timer).init(state.allocator), + }; + } + + pub fn run(self: *Loop) !void { + const display = self.state.wayland.display; + + while (true) loop: { + while (true) { + const ret = try display.dispatchPending(); + _ = try display.flush(); + if (ret <= 0) break; + } + _ = try os.poll(&self.fds, -1); + + for (self.fds) |fd| { + if (fd.revents & os.POLL.HUP != 0) { + break :loop; + } + if (fd.revents & os.POLL.ERR != 0) { + break :loop; + } + } + + // wayland + if (self.fds[0].revents & os.POLL.IN != 0) { + _ = try display.dispatch(); + } + if (self.fds[0].revents & os.POLL.OUT != 0) { + _ = try display.flush(); + } + + // timer + if (self.fds[1].revents & os.POLL.IN != 0) { + for (self.timers.items) |timer, i| { + const callback = timer.callback; + const payload = timer.payload; + _ = self.timers.swapRemove(i); + callback(payload); + } + } + } + } +}; + +pub const Timer = struct { + callback: fn (*anyopaque) void, + payload: *anyopaque, +}; diff --git a/src/main.zig b/src/main.zig @@ -1,4 +1,27 @@ const std = @import("std"); +const Loop = @import("event.zig").Loop; +const Wayland = @import("wayland.zig").Wayland; + +pub const State = struct { + allocator: std.mem.Allocator, + wayland: Wayland, + loop: Loop, +}; + pub fn main() anyerror!void { + var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); + defer arena.deinit(); + + std.log.info("initialization", .{}); + var state: State = undefined; + state.allocator = arena.allocator(); + state.wayland = try Wayland.init(&state); + state.loop = try Loop.init(&state); + + std.log.info("wayland globals registration", .{}); + try state.wayland.registerGlobals(); + + std.log.info("event loop start", .{}); + try state.loop.run(); } diff --git a/src/render.zig b/src/render.zig @@ -0,0 +1,31 @@ +const pixman = @import("pixman"); + +const Buffer = @import("shm.zig").Buffer; +const Surface = @import("wayland.zig").Surface; + +pub fn renderBackground(surface: *Surface) !void { + const wlSurface = surface.backgroundSurface; + + const buffer = try Buffer.nextBuffer( + &surface.backgroundBuffers, + surface.output.state.wayland.shm, + surface.width, + surface.height, + ); + buffer.busy = true; + + const area = [_]pixman.Rectangle16{ + .{ .x = 0, .y = 0, .width = surface.width, .height = surface.height }, + }; + const color = pixman.Color{ + .red = 0, + .green = 0, + .blue = 0, + .alpha = 0xffff, + }; + _ = pixman.Image.fillRectangles(.src, buffer.pix.?, &color, 1, &area); + + wlSurface.setBufferScale(surface.output.scale); + wlSurface.damageBuffer(0, 0, surface.width, surface.height); + wlSurface.attach(buffer.buffer, 0, 0); +} diff --git a/src/shm.zig b/src/shm.zig @@ -0,0 +1,69 @@ +const std = @import("std"); +const mem = std.mem; +const os = std.os; + +const wayland = @import("wayland").client; +const wl = wayland.wl; + +const pixman = @import("pixman"); + +pub const Buffer = struct { + data: ?[]align(4096) u8, + buffer: ?*wl.Buffer, + pix: ?*pixman.Image, + + busy: bool, + width: u31, + height: u31, + size: u31, + + pub fn init(self: *Buffer, shm: *wl.Shm, width: u31, height: u31) !void { + self.busy = true; + self.width = width; + self.height = height; + + const fd = try os.memfd_create("levee-wayland-shm-buffer-pool", 0); + defer os.close(fd); + + const stride = width * 4; + self.size = stride * height; + try os.ftruncate(fd, self.size); + + self.data = try os.mmap(null, self.size, os.PROT.READ | os.PROT.WRITE, os.MAP.SHARED, fd, 0); + errdefer os.munmap(self.data.?); + + const pool = try shm.createPool(fd, self.size); + defer pool.destroy(); + + self.buffer = try pool.createBuffer(0, width, height, stride, .argb8888); + errdefer self.buffer.?.destroy(); + self.buffer.?.setListener(*Buffer, listener, self); + + self.pix = pixman.Image.createBitsNoClear(.a8r8g8b8, width, height, @ptrCast([*c]u32, self.data.?), stride); + } + + pub fn deinit(self: *Buffer) void { + if (self.pix) |pix| _ = pix.unref(); + if (self.buffer) |buf| buf.destroy(); + if (self.data) |data| os.munmap(data); + } + + fn listener(_: *wl.Buffer, event: wl.Buffer.Event, buffer: *Buffer) void { + switch (event) { + .release => buffer.busy = false, + } + } + + pub fn nextBuffer(pool: *[2]Buffer, shm: *wl.Shm, width: u16, height: u16) !*Buffer { + if (pool[0].busy and pool[1].busy) { + return error.NoAvailableBuffers; + } + const buffer = if (!pool[0].busy) &pool[0] else &pool[1]; + + if (buffer.width != width or buffer.height != height) { + buffer.deinit(); + try buffer.init(shm, width, height); + } + return buffer; + } +}; diff --git a/src/wayland.zig b/src/wayland.zig @@ -0,0 +1,201 @@ +const std = @import("std"); +const mem = std.mem; +const strcmp = std.cstr.cmp; +const ArrayList = std.ArrayList; + +const wl = @import("wayland").client.wl; +const zwlr = @import("wayland").client.zwlr; +const zriver = @import("wayland").client.zriver; + +const Buffer = @import("shm.zig").Buffer; +const render = @import("render.zig"); +const State = @import("main.zig").State; + +pub const Wayland = struct { + state: *State, + display: *wl.Display, + registry: *wl.Registry, + + outputs: ArrayList(*Output), + + compositor: *wl.Compositor, + shm: *wl.Shm, + layerShell: *zwlr.LayerShellV1, + statusManager: *zriver.StatusManagerV1, + + globalsRegistered: [4]bool, + + pub fn init(state: *State) !Wayland { + const display = try wl.Display.connect(null); + + return Wayland{ + .state = state, + .display = display, + .registry = try display.getRegistry(), + .outputs = ArrayList(*Output).init(state.allocator), + .compositor = undefined, + .shm = undefined, + .layerShell = undefined, + .statusManager = undefined, + .globalsRegistered = mem.zeroes([4]bool), + }; + } + + pub fn registerGlobals(self: *Wayland) !void { + self.registry.setListener(*State, registryListener, self.state); + _ = try self.display.roundtrip(); + + for (self.globalsRegistered) |globalRegistered| { + if (!globalRegistered) return error.UnsupportedGlobal; + } + } + + fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, state: *State) void { + const wayland = &state.wayland; + + switch (event) { + .global => |global| { + const interface = global.interface; + const name = global.name; + + if (strcmp(interface, wl.Compositor.getInterface().name) == 0) { + wayland.compositor = registry.bind(name, wl.Compositor, 4) catch return; + wayland.globalsRegistered[0] = true; + } else if (strcmp(interface, wl.Shm.getInterface().name) == 0) { + wayland.shm = registry.bind(name, wl.Shm, 1) catch return; + wayland.globalsRegistered[1] = true; + } else if (strcmp(interface, zwlr.LayerShellV1.getInterface().name) == 0) { + wayland.layerShell = registry.bind(name, zwlr.LayerShellV1, 1) catch return; + wayland.globalsRegistered[2] = true; + } else if (strcmp(interface, zriver.StatusManagerV1.getInterface().name) == 0) { + wayland.statusManager = registry.bind(name, zriver.StatusManagerV1, 1) catch return; + wayland.globalsRegistered[3] = true; + } else if (strcmp(interface, wl.Output.getInterface().name) == 0) { + const output = Output.create(state, registry, name) catch return; + wayland.outputs.append(output) catch return; + } + }, + .global_remove => |data| { + for (wayland.outputs.items) |output, i| { + if (output.globalName == data.name) { + state.allocator.destroy(output); + _ = wayland.outputs.swapRemove(i); + break; + } + } + }, + } + } +}; + +pub const Output = struct { + state: *State, + wlOutput: *wl.Output, + globalName: u32, + scale: i32, + surface: ?*Surface, + + pub fn create(state: *State, registry: *wl.Registry, name: u32) !*Output { + const self = try state.allocator.create(Output); + self.state = state; + self.wlOutput = try registry.bind(name, wl.Output, 3); + self.globalName = name; + self.scale = 1; + self.surface = null; + + self.wlOutput.setListener(*Output, listener, self); + return self; + } + + fn listener(_: *wl.Output, event: wl.Output.Event, output: *Output) void { + switch (event) { + .scale => |scale| { + output.scale = scale.factor; + }, + .geometry => {}, + .mode => {}, + .name => {}, + .description => {}, + .done => { + if (output.surface) |_| {} else { + output.surface = Surface.create(output) catch return; + } + }, + } + } +}; + +pub const Surface = struct { + output: *Output, + + backgroundSurface: *wl.Surface, + backgroundBuffers: [2]Buffer, + + layerSurface: *zwlr.LayerSurfaceV1, + + configured: bool, + width: u16, + height: u16, + + pub fn create(output: *Output) !*Surface { + const state = output.state; + + const self = try state.allocator.create(Surface); + self.output = output; + self.configured = false; + + self.backgroundSurface = try state.wayland.compositor.createSurface(); + self.backgroundBuffers = mem.zeroes([2]Buffer); + + self.layerSurface = try state.wayland.layerShell.getLayerSurface( + self.backgroundSurface, + output.wlOutput, + .overlay, + "levee", + ); + + const height = 32; + self.layerSurface.setSize(0, height); + self.layerSurface.setAnchor( + .{ .top = true, .left = true, .right = true, .bottom = false }, + ); + self.layerSurface.setExclusiveZone(@intCast(i32, height)); + self.layerSurface.setMargin(0, 0, 0, 0); + self.layerSurface.setListener(*Surface, layerSurfaceListener, self); + + self.backgroundSurface.commit(); + + return self; + } + + pub fn destroy(self: *Surface) void { + self.output.surface = null; + self.backgroundSurface.destroy(); + self.layerSurface.destroy(); + self.backgroundBuffers[0].deinit(); + self.backgroundBuffers[1].deinit(); + self.output.state.allocator.destroy(self); + } + + fn layerSurfaceListener( + layerSurface: *zwlr.LayerSurfaceV1, + event: zwlr.LayerSurfaceV1.Event, + surface: *Surface, + ) void { + switch (event) { + .configure => |data| { + surface.configured = true; + surface.width = @intCast(u16, data.width); + surface.height = @intCast(u16, data.height); + + layerSurface.ackConfigure(data.serial); + + render.renderBackground(surface) catch return; + surface.backgroundSurface.commit(); + }, + .closed => { + surface.destroy(); + }, + } + } +};