const std = @import("std"); const mem = std.mem; const posix = std.posix; const wayland = @import("wayland"); const wl = wayland.client.wl; const xdg = wayland.client.xdg; const pangocairo = @import("pangocairo.zig"); const cairo = pangocairo.cairo; const pango = pangocairo.pango; /// Global Wayland registry context; contains pointers to the globals we expect /// from the compositor. const RegistryContext = struct { shm: ?*wl.Shm = null, compositor: ?*wl.Compositor = null, wm_base: ?*xdg.WmBase = null, }; var registry_ctx = RegistryContext{}; /// A window's buffer data; we will paint the frames to these buffers. const WindowBuffer = struct { /// Wayland buffer wl_buffer: *wl.Buffer, /// Backing data slice from memory mapping for this buffer map: []u8, /// Cairo surface for painting with cairo c_surface: cairo.Surface, /// Are we being read by the compositor right now? busy: bool = false, }; /// A single Window (surface); data and methods we need to tell the compositor /// to render a surface for us. pub const Window = struct { /// The allocator that we were allocated on, needed to destroy ourselves allocator: mem.Allocator, /// Wayland surface wl_surface: *wl.Surface, /// XDG surface attached to the Wayland surface xdg_surface: *xdg.Surface = undefined, /// XDG toplevel data xdg_toplevel: *xdg.Toplevel = undefined, /// Buffers A and B; Wayland requires double-buffering buffers: [2]WindowBuffer = undefined, /// Memory mapped for both buffers buffers_map: []align(mem.page_size) u8 = undefined, /// Pango font description for rendering our text font: pango.FontDescription, /// This window's title title: [*:0]const u8, /// Width in pixels width: i32 = 480, /// Height in pixels height: i32 = 360, /// Offset of our checkerboard animation offset: f32 = 0.0, /// Last frame's timestamp in ms last_frame: u32 = 0, /// Frames per second statistic current_fps: f32 = 0.0, /// Should we keep this window open? open: bool = true, /// Allocate a new window and all backing resources for it pub fn create(allocator: mem.Allocator, title: [*:0]const u8) !*Window { const compositor = registry_ctx.compositor orelse return error.NoWlCompositor; const wm_base = registry_ctx.wm_base orelse return error.NoXdgWmBase; var win = try allocator.create(Window); win.* = Window{ .allocator = allocator, .wl_surface = try compositor.createSurface(), .title = title, // .font = try pango.FontDescription.fromString("monospace 24"), .font = try pango.FontDescription.create(), }; win.xdg_surface = try wm_base.getXdgSurface(win.wl_surface); win.xdg_toplevel = try win.xdg_surface.getToplevel(); win.font.setFamily("monospace"); win.font.setWeight(600); win.font.setSizePt(18); try win.allocBuffers(); win.xdg_surface.setListener(*Window, xdgSurfaceListener, win); win.xdg_toplevel.setListener(*Window, xdgToplevelListener, win); win.xdg_toplevel.setTitle(title); win.wl_surface.commit(); const wl_cb = try win.wl_surface.frame(); wl_cb.setListener(*Window, frameListener, win); return win; } /// Destroy this window and all its associated resources pub fn destroy(win: *Window) void { win.destroyBuffers(); win.font.destroy(); win.xdg_toplevel.destroy(); win.xdg_surface.destroy(); win.wl_surface.destroy(); win.allocator.destroy(win); } /// Allocate new buffers for this window fn allocBuffers(win: *Window) !void { const c_format = cairo.Format.argb32; const stride = cairo.Surface.formatStrideForWidth(c_format, win.width); const buffer_size = stride * win.height; const total_size = buffer_size * 2; // we need two buffers const fd = try posix.memfd_create("zigway", 0); defer posix.close(fd); try posix.ftruncate(fd, @intCast(total_size)); win.buffers_map = try posix.mmap( null, @intCast(total_size), posix.PROT.READ | posix.PROT.WRITE, .{ .TYPE = .SHARED }, fd, 0, ); const shm = registry_ctx.shm orelse return error.NoShm; const pool = try shm.createPool(fd, total_size); defer pool.destroy(); inline for (&win.buffers, 0..win.buffers.len) |*buffer, i| { buffer.wl_buffer = try pool.createBuffer( buffer_size *% @as(i32, i), win.width, win.height, stride, .argb8888, ); const bsize: usize = @intCast(buffer_size); buffer.busy = false; buffer.map = win.buffers_map[bsize *% i .. bsize *% (i + 1)]; buffer.c_surface = try cairo.Surface.createImageForData( buffer.map, c_format, win.width, win.height, stride, ); buffer.wl_buffer.setListener(*WindowBuffer, bufferListener, buffer); } } /// Destroy this window's buffers fn destroyBuffers(win: *Window) void { win.buffers[0].c_surface.destroy(); win.buffers[1].c_surface.destroy(); win.buffers[0].wl_buffer.destroy(); win.buffers[1].wl_buffer.destroy(); posix.munmap(win.buffers_map); } /// Draw a new frame to the first free buffer and return it pub fn drawFrame(win: *Window) !*wl.Buffer { var buffer: *WindowBuffer = undefined; if (!win.buffers[0].busy) { buffer = &win.buffers[0]; } else if (!win.buffers[1].busy) { buffer = &win.buffers[1]; } else { return error.NoBuffersAvaiable; } const canvas: *[]u32 = @ptrCast(&buffer.map); const box_side = 16; var offset: i32 = @intFromFloat(win.offset); offset = @mod(offset, box_side); var y: i32 = 0; while (y < win.height) : (y += 1) { var x: i32 = 0; while (x < win.width) : (x += 1) { const i: usize = @intCast(y * win.width + x); const side = @mod( x + offset + @divFloor(y + offset, box_side) * box_side, (box_side * 2), ); canvas.*[i] = if (side < box_side) 0xFF0E0E0E else 0xFF2F2F2F; } } const layout = try pango.Layout.createForCairo(&buffer.c_surface); defer layout.unref(); buffer.c_surface.setSourceRgba(1, 1, 1, 1); layout.setFont(win.font); layout.setText("Hello, wayland!", -1); buffer.c_surface.moveTo(5, 5); layout.show(&buffer.c_surface); var fps_buf: [32]u8 = undefined; const fps_output = std.fmt.bufPrintZ( &fps_buf, "{d} FPS", .{win.current_fps}, ) catch unreachable; layout.setText(fps_output, -1); buffer.c_surface.moveTo(5, 40); layout.show(&buffer.c_surface); buffer.busy = true; return buffer.wl_buffer; } /// Resize this window and reallocate the buffers if needed fn resize(win: *Window, width: i32, height: i32) !void { win.height = if (height != 0) height else win.height; win.width = if (width != 0) width else win.width; if (width == 0 and height == 0) return; win.destroyBuffers(); try win.allocBuffers(); } /// XDG Surface callbacks fn xdgSurfaceListener(xdg_surface: *xdg.Surface, event: xdg.Surface.Event, win: *Window) void { switch (event) { .configure => |configure| { xdg_surface.ackConfigure(configure.serial); brk: { const buffer = win.drawFrame() catch |err| { std.debug.print("error drawing frame {}\n", .{err}); break :brk; }; win.wl_surface.attach(buffer, 0, 0); } win.wl_surface.commit(); }, } } /// XDG Toplevel callbacks fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, win: *Window) void { switch (event) { .configure => |configure| { win.resize(configure.width, configure.height) catch |err| { std.debug.print("error resizing window {}\n", .{err}); return; }; }, .close => win.open = false, } } /// Wayland buffer callbacks fn bufferListener(_: *wl.Buffer, event: wl.Buffer.Event, buffer: *WindowBuffer) void { switch (event) { .release => { buffer.busy = false; }, } } /// Wayland frame callback fn frameListener(wl_cb: *wl.Callback, event: wl.Callback.Event, win: *Window) void { switch (event) { .done => |done| { wl_cb.destroy(); const new_cb = win.wl_surface.frame() catch |err| { std.debug.print("unable to get frame {}\n", .{err}); win.open = false; return; }; new_cb.setListener(*Window, frameListener, win); if (win.last_frame != 0) { const elapsed: f32 = @floatFromInt(done.callback_data -% win.last_frame); win.offset -= elapsed / 1000.0 * 32.0; win.current_fps = 1000.0 / elapsed; } win.last_frame = done.callback_data; const wl_buffer = win.drawFrame() catch |err| { std.debug.print("error drawing frame {}\n", .{err}); return; }; win.wl_surface.attach(wl_buffer, 0, 0); win.wl_surface.damageBuffer(0, 0, std.math.maxInt(i32), std.math.maxInt(i32)); win.wl_surface.commit(); }, } } }; pub fn main() !void { var gpa = std.heap.GeneralPurposeAllocator(.{}){}; const allocator = gpa.allocator(); const display = try wl.Display.connect(null); defer display.disconnect(); const registry = try display.getRegistry(); registry.setListener(*RegistryContext, registryListener, ®istry_ctx); if (display.roundtrip() != .SUCCESS) return error.RoundTripFailed; const wm_base = registry_ctx.wm_base orelse return error.NoXdgWmBase; wm_base.setListener(?*void, xdgWmBaseListener, null); const win = try Window.create(allocator, "Hello, wayland!"); defer win.destroy(); while (win.open) { if (display.dispatch() != .SUCCESS) return error.DispatchFailed; } } /// Global registry callback fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, ctx: *RegistryContext) void { switch (event) { .global => |global| { std.debug.print("inteface: {s}, version {}, name {}\n", .{ global.interface, global.version, global.name, }); if (mem.orderZ(u8, global.interface, wl.Compositor.getInterface().name) == .eq) { ctx.compositor = registry.bind(global.name, wl.Compositor, 6) catch return; } else if (mem.orderZ(u8, global.interface, wl.Shm.getInterface().name) == .eq) { ctx.shm = registry.bind(global.name, wl.Shm, 1) catch return; } else if (mem.orderZ(u8, global.interface, xdg.WmBase.getInterface().name) == .eq) { ctx.wm_base = registry.bind(global.name, xdg.WmBase, 2) catch return; } }, .global_remove => {}, } } /// XDG Wm Base callback fn xdgWmBaseListener(wm_base: *xdg.WmBase, event: xdg.WmBase.Event, _: ?*void) void { switch (event) { .ping => |ping| { wm_base.pong(ping.serial); }, } }