const std = @import("std"); const span = std.mem.span; const posix = std.posix; const utils = @import("utils.zig"); const wayland = @import("wayland"); const wl = wayland.client.wl; const xdg = wayland.client.xdg; const zxdg = wayland.client.zxdg; const z2d = @import("z2d"); const Client = @This(); alloc: std.mem.Allocator, display: *wl.Display, shm: ?*wl.Shm = null, compositor: ?*wl.Compositor = null, wm_base: ?*xdg.WmBase = null, decoration_manager: ?*zxdg.DecorationManagerV1 = null, supported_formats: std.ArrayList(wl.Shm.Format) = .empty, // TODO: Allow multiple windows? window: Window, pub fn init(client: *Client, alloc: std.mem.Allocator) !void { client.alloc = alloc; client.display = try wl.Display.connect(null); client.supported_formats = .empty; const registry = try client.display.getRegistry(); registry.setListener(*Client, registryListener, client); if (client.display.roundtrip() != .SUCCESS) return error.RoundtripFailed; _ = client.shm orelse return error.NoWlShm; _ = client.compositor orelse return error.NoWlCompositor; _ = client.wm_base orelse return error.NoXdgWmBase; client.shm.?.setListener(*Client, shmListener, client); client.wm_base.?.setListener(*Client, wmBaseListener, client); if (client.display.roundtrip() != .SUCCESS) return error.RoundtripFailed; } pub fn createWindow(client: *Client, title: [:0]const u8) *Window { const window: *Window = &client.window; window.* = .{ .client = client, .alloc = client.alloc, .title = title, }; return window; } pub const Window = struct { client: *Client = undefined, alloc: std.mem.Allocator = undefined, title: [:0]const u8 = "", width: u32 = 0, height: u32 = 0, wl_surface: ?*wl.Surface = null, xdg_toplevel: ?*xdg.Toplevel = null, xgd_surface: ?*xdg.Surface = null, pool: ?*wl.ShmPool = null, decoration: Decoration = .{}, wl_buffer: ?*wl.Buffer = null, z2d_surface: ?z2d.Surface = null, buffer_data: ?[]align(std.heap.page_size_min) u8 = null, wl_buffers: [2]?*wl.Buffer = .{ null, null }, z2d_surfaces: [2]?z2d.Surface = .{ null, null }, buffer_swapped: bool = false, /// In miliseconds. last_frame: u32 = 0, is_open: bool = false, need_redraw: bool = false, /// Returns a z2d context pub fn context(window: *Window) z2d.Context { return .init(window.alloc, &window.surfaces[window.sel]); } // Returns a z2d surface to the window. pub fn surface(window: *Window) ?z2d.Surface { if (window.buffer_data) |data| { return z2d.Surface.initBuffer( .image_surface_rgb, .fromClamped(1, 1, 1), @ptrCast(@alignCast(data[window.dataOffset(true)..])), @intCast(window.width), @intCast(window.height), ); } return null; } pub fn deinit(window: *Window) void { if (window.decoration.xdg_decoration) |d| d.destroy(); if (window.xdg_toplevel) |t| t.destroy(); if (window.xgd_surface) |s| s.destroy(); if (window.wl_buffer) |b| b.destroy(); if (window.pool) |p| p.destroy(); if (window.wl_surface) |s| s.destroy(); if (window.z2d_surface) |z| posix.munmap(@ptrCast(@alignCast(z.image_surface_rgb.buf))); } fn commit(window: *Window) void { window.wl_surface.?.attach(window.wl_buffer, 0, 0); window.wl_surface.?.damage(0, 0, std.math.maxInt(i32), std.math.maxInt(i32)); window.wl_surface.?.commit(); } pub fn submit(window: *Window) !void { window.buffer_swapped = !window.buffer_swapped; const buffer = try window.pool.?.createBuffer( @intCast(window.dataOffset(false)), @intCast(window.width), @intCast(window.height), @intCast(window.width * @sizeOf(z2d.pixel.RGB)), .xrgb8888, ); if (window.wl_buffer) |b| b.destroy(); window.wl_buffer = buffer; window.commit(); window.need_redraw = false; } pub fn show(window: *Window, width: u32, height: u32) !void { const client = window.client; const wl_surface = try client.compositor.?.createSurface(); errdefer wl_surface.destroy(); const xdg_surface = try client.wm_base.?.getXdgSurface(wl_surface); errdefer xdg_surface.destroy(); const xdg_toplevel = try xdg_surface.getToplevel(); errdefer xdg_toplevel.destroy(); if (window.client.decoration_manager) |manager| { window.decoration.xdg_decoration = try manager.getToplevelDecoration(xdg_toplevel); window.decoration.xdg_decoration.?.setListener(*Window, xdgDecorationListener, window); window.decoration.xdg_decoration.?.setMode(.server_side); } const cb = try wl_surface.frame(); cb.setListener(*Window, surfaceCbListener, window); window.wl_surface = wl_surface; window.xgd_surface = xdg_surface; window.xdg_toplevel = xdg_toplevel; window.xgd_surface.?.setListener(*Window, xdgSurfaceListener, window); window.xdg_toplevel.?.setListener(*Window, xdgToplevelListener, window); window.xdg_toplevel.?.setTitle(window.title.ptr); window.xdg_toplevel.?.setAppId("dev.jeremia.GuiApp"); window.wl_surface.?.commit(); if (client.display.roundtrip() != .SUCCESS) return error.RoundtripFailed; window.commit(); try window.resize(width, height); window.is_open = true; } pub fn resize(window: *Window, width: u32, height: u32) !void { try window.onSize(width, height); window.commit(); window.width = width; window.height = height; } pub fn setTitle(window: *Window, title: [:0]const u8) void { window.title = title; if (window.xdg_toplevel) |toplevel| toplevel.setTitle(title.ptr); } fn newPool(window: *Window, size: u32) !void { if (size == 0) return; if (window.buffer_data) |b| if (b.len >= size) return error.OldShmIsLarger; window.pool, const data = try window.client.createShmPool(size * 2); if (window.buffer_data) |b| posix.munmap(b); window.buffer_data = data; } /// If `next_frame` is false, it returns the frame that should currently be displayed. /// If true, it returns the next frame's offset. fn dataOffset(window: *Window, next_frame: bool) usize { const size = window.buffer_data.?.len; var b: bool = window.buffer_swapped; if (next_frame) b = !b; return if (b) @intCast(size / 2) else 0; } // Called when the window needs to be resized. // Creates a wl_shm_pool if the one the Window currently has is not big enough. // The pool is allocated enough memory for two frames, given the size. // Two wl_buffer are created with half the memory each. fn onSize(window: *Window, width: u32, height: u32) !void { const size = width * height * @sizeOf(z2d.pixel.RGB) * 2; std.log.debug("(wayland) onSize: {d}x{d} ({d})", .{ width, height, size }); if (size == 0) return; window.newPool(size) catch |err| switch (err) { error.OldShmIsLarger => { std.log.debug("(wayland): Old pool is larger!", .{}); }, else => |e| return e, }; const pool: *wl.ShmPool = window.pool orelse return; std.log.debug("(wayland): createBuffer(s) {d}x{d} ({d})", .{ width, height, width * @sizeOf(z2d.pixel.RGB) }); const buffer = try pool.createBuffer( @intCast(window.dataOffset(false)), @intCast(width), @intCast(height), @intCast(width * @sizeOf(z2d.pixel.RGB)), .xrgb8888, ); if (window.wl_buffer) |b| b.destroy(); window.wl_buffer = buffer; } }; /// Creates a shared memory pool, and returns both the pool, and its underlying data. fn createShmPool(client: *Client, size: u32) !struct { *wl.ShmPool, []align(std.heap.page_size_min) u8 } { var buf: [100]u8 = undefined; var retries: u8 = 0; var fd: i32 = 0; while (retries < 100) { retries += 1; const name = try std.fmt.bufPrint(&buf, "wayland-window-{d}", .{std.crypto.random.int(u32)}); fd = posix.memfd_create(name, 0) catch continue; } errdefer posix.close(fd); try posix.ftruncate(fd, size); const data = try posix.mmap(null, size, posix.PROT.READ | posix.PROT.WRITE, .{ .TYPE = .SHARED }, fd, 0); errdefer posix.munmap(data); const pool = try client.shm.?.createPool(fd, @intCast(size)); return .{ pool, data }; } const Decoration = struct { xdg_decoration: ?*zxdg.ToplevelDecorationV1 = null, // TODO(wayland): Handle client-side decorations? fn onMode(decoration: *Decoration, _: zxdg.ToplevelDecorationV1.Mode) void { const window: *Window = @fieldParentPtr("decoration", decoration); _ = window; } }; //----------------------------------------------------------------------------- // Listeners. //----------------------------------------------------------------------------- // This might be a tad excessive. const GlobalsEnum = utils.Enum(&.{ span(wl.Compositor.interface.name), span(wl.Shm.interface.name), span(xdg.WmBase.interface.name), span(zxdg.DecorationManagerV1.interface.name), }); fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, client: *Client) void { switch (event) { .global => |global| { std.log.debug("(wayland): registry: {s} (v{d})", .{ global.interface, global.version }); const val = std.meta.stringToEnum(GlobalsEnum, span(global.interface)) orelse return; switch (val) { .wl_compositor => client.compositor = registry.bind(global.name, wl.Compositor, 1) catch return, .wl_shm => client.shm = registry.bind(global.name, wl.Shm, 1) catch return, .xdg_wm_base => client.wm_base = registry.bind(global.name, xdg.WmBase, 1) catch return, .zxdg_decoration_manager_v1 => client.decoration_manager = registry.bind(global.name, zxdg.DecorationManagerV1, 1) catch return, } }, .global_remove => {}, } } fn shmListener(_: *wl.Shm, event: wl.Shm.Event, client: *Client) void { switch (event) { .format => |format| { std.log.debug("(wayland): shm format: {t}", .{format.format}); client.supported_formats.append(client.alloc, format.format) catch return; }, } } fn wmBaseListener(base: *xdg.WmBase, event: xdg.WmBase.Event, _: *Client) void { switch (event) { .ping => |ping| { base.pong(ping.serial); }, } } fn surfaceCbListener(_: *wl.Callback, event: wl.Callback.Event, window: *Window) void { std.log.debug("(wayland): wl_surface callback", .{}); const cb = window.wl_surface.?.frame() catch return; cb.setListener(*Window, surfaceCbListener, window); // window.commit(); window.onSize(window.width, window.height) catch return; // window.need_redraw = true; window.last_frame = event.done.callback_data; } fn xdgSurfaceListener(xdg_surface: *xdg.Surface, event: xdg.Surface.Event, window: *Window) void { std.log.debug("(wayland): xdgSurfaceListener {}", .{event}); switch (event) { .configure => |configure| { xdg_surface.ackConfigure(configure.serial); window.wl_surface.?.commit(); }, } } fn xdgToplevelListener(_: *xdg.Toplevel, event: xdg.Toplevel.Event, window: *Window) void { std.log.debug("(wayland): xdgToplevelListener {}", .{event}); switch (event) { .configure => |conf| { if (window.height != @as(u32, @intCast(conf.height)) or window.width != @as(u32, @intCast(conf.width))) window.need_redraw = true; window.height = @intCast(conf.height); window.width = @intCast(conf.width); }, .close => window.is_open = false, } } pub fn xdgDecorationListener(_: *zxdg.ToplevelDecorationV1, event: zxdg.ToplevelDecorationV1.Event, window: *Window) void { std.log.debug("(wayland): xdgDecorationListener {}", .{event}); switch (event) { .configure => |conf| { window.decoration.onMode(conf.mode); }, } }