diff --git a/cat.bgra b/cat.bgra new file mode 100644 index 0000000..1ca94bb Binary files /dev/null and b/cat.bgra differ diff --git a/main.zig b/main.zig index d32cd7d..b5cb6c4 100644 --- a/main.zig +++ b/main.zig @@ -16,14 +16,14 @@ const cairo = @cImport({ }); const Point = struct { - x: i32, - y: i32, + x: i32 = 0, + y: i32 = 0, }; const Size = struct { const Self = @This(); - width: u32, - height: u32, + width: u32 = 0, + height: u32 = 0, fn size(self: *Self) usize { return self.width * self.height; @@ -33,19 +33,27 @@ const Size = struct { const Box = struct { const Self = @This(); - position: Point, - extents: Size, + position: Point = .{.x = 0, .y = 0,}, + extents: Size = .{.width = 0, .height = 0,}, + + fn default() Self { + return Self { + .position = .{.x = 0, .y = 0,}, + .extents = .{.width = 0, .height = 0,}, + }; + } fn intersects(self: *const Self, other: *const Self) bool { const x1 = self.position.x; const y1 = self.position.y; - const w1 = self.extents.width; - const h1 = self.extents.height; + const w1 = @intCast(isize, self.extents.width); + const h1 = @intCast(isize, self.extents.height); const x2 = other.position.x; const y2 = other.position.y; - const w2 = other.extents.width; - const h2 = other.extents.height; + + const w2 = @intCast(isize, other.extents.width); + const h2 = @intCast(isize, other.extents.height); // return self.x < other.x + other.width and // self.x + self.width > other.x and @@ -58,11 +66,29 @@ const Box = struct { y1 + h1 > y2; } - fn contains(self: *Self, point: Point) bool { + fn fromCorners(a: Point, b: Point) Self { + const top = @min(a.y, b.y); + const bot = @max(a.y, b.y); + const left = @min(a.x, b.x); + const right = @max(a.x, b.x); + + const pos = Point{.x = left, .y = top,}; + const extents = Size{ + .width = right - left, + .height = bot - top, + }; + + return Self { + .position = pos, + .extents = extents, + }; + } + + fn contains(self: *const Self, point: Point) bool { const x1 = self.position.x; const y1 = self.position.y; - const w1 = self.extents.width; - const h1 = self.extents.height; + const w1 = @intCast(isize, self.extents.width); + const h1 = @intCast(isize, self.extents.height); const x2 = point.x; const y2 = point.y; @@ -79,12 +105,50 @@ const Box = struct { } }; +test "box" { + const a = Box.default(); + const a2 = Box{.extents = .{.width = 1, .height = 1,}}; + const b = Box.default(); + const b2 = Box{.extents = .{.width = 1, .height = 1,}}; + + std.debug.assert(!a.contains(.{.x = 0, .y = 0})); + std.debug.assert(!a.intersects(&b)); + + std.debug.assert(a2.contains(.{.x = 0, .y = 0})); + std.debug.assert(a2.intersects(&b2)); + + const c1 = Box{ + .position = .{.x = 1, .y = 1,}, + .extents = .{.width = 2, .height = 2,}, + }; + + const c2 = Box{ + .position = .{.x = 1, .y = 1,}, + .extents = .{.width = 1, .height = 1,}, + }; + + std.debug.assert(!c1.intersects(&a2)); + std.debug.assert(c1.intersects(&c2)); +} + fn ListItem(comptime T: type) type { return struct { const Self = @This(); link: list.Link, - value: T, + value: T = undefined, + + fn create(ally: std.mem.Allocator, value: T) !*Self { + const self = try ally.create(Self); + + self.init(value); + + return self; + } + + fn init(self: *Self, value: T) void { + self.value = value; + } fn Head() type { return list.Head(Self, "link"); @@ -94,7 +158,12 @@ fn ListItem(comptime T: type) type { return @fieldParentPtr(Self, "link", link); } + /// automatically calls `deinit()` on `data` if a function with that name exists fn remove(self: *Self) void { + if (@hasDecl(T, "deinit")) { + self.value.deinit(); + } + self.link.remove(); } @@ -166,12 +235,252 @@ const Options = struct { const Output = struct { const Self = @This(); - state: *State, + state: *State.Init = undefined, output: *wl.Output, - geometry: Box, - logical_geometry: Box, - scale: i32, + surface: *wl.Surface = undefined, + layer_surface: *zwlr.LayerSurfaceV1 = undefined, + xdg_output: *zxdg.OutputV1 = undefined, + + geometry: Box = Box.default(), + logical_geometry: Box = Box.default(), + scale: i32 = 1, + + size: Size = Size{}, + + configured: bool = false, + + fn init(self: *Self, state: *State.Init) !void { + std.debug.print("output.init()\n", .{}); + self.state = state; + + self.output.setListener(*Self, outputListener, self); + + self.surface = try state.compositor.createSurface(); + + self.layer_surface = try state.layer_shell.getLayerSurface( + self.surface, + self.output, + .overlay, + "selection", + ); + self.layer_surface.setListener(*Self, layerSurfaceListener, self); + + self.xdg_output = try state.xdg_output_manager.getXdgOutput(self.output); + self.xdg_output.setListener(*Self, xdgOutputListener, self); + + self.layer_surface.setAnchor(.{.top = true, .left = true, .right = true, .bottom = true,}); + self.layer_surface.setKeyboardInteractivity(1); + self.layer_surface.setExclusiveZone(-1); + self.surface.commit(); + } + + fn deinit(self: *Self) void { + self.output.destroy(); + } + + fn xdgOutputListener(output: *zxdg.OutputV1, event: zxdg.OutputV1.Event, self: *Self) void { + _ = output; + + std.debug.print("zxdg_output listener: {}\n", .{event}); + + switch (event) { + .logical_position => |pos| { + self.logical_geometry.position = .{.x = pos.x, .y = pos.y,}; + }, + .logical_size => |size| { + self.logical_geometry.extents = .{ + .width = @intCast(u32, size.width), + .height = @intCast(u32, size.height), + }; + }, + else => {}, + } + } + + fn layerSurfaceListener( + layer: *zwlr.LayerSurfaceV1, + event: zwlr.LayerSurfaceV1.Event, + self: *Self, + ) void { + std.debug.print("zwlr_layer_surface listener: {}\n", .{event}); + + switch (event) { + .configure => |cfg|{ + self.configured = true; + self.size = .{ + .width = cfg.width, + .height = cfg.height, + }; + + layer.ackConfigure(cfg.serial); + // TODO: send frame + }, + else => {}, + } + } + + fn outputListener(output: *wl.Output, event: wl.Output.Event, self: *Self) void { + _ = output; + + std.debug.print("wl_output listener: {}\n", .{event}); + + switch (event) { + .geometry => |geom| { + self.geometry.position = .{.x = geom.x, .y = geom.y}; + }, + .mode => |mode| { + if (!mode.flags.current) { + self.geometry.extents = .{ + .width = @intCast(u32, mode.width), + .height = @intCast(u32, mode.height), + }; + } + }, + .scale => |scale| { + self.scale = scale.factor; + }, + else => { + std.debug.print("wl_output listener: unhandled\n", .{}); + }, + } + } +}; + +const Seat = struct { + const Self = @This(); + + state: *State.Init = undefined, + seat: *wl.Seat, + + keyboard: ?struct { + keyboard: *wl.Keyboard, + xkb: ?struct { + keymap: *xkb.xkb_keymap, + state: *xkb.xkb_state, + } = null, + } = null, + + pointer: ?struct { + pointer: *wl.Pointer, + button_state: ?wl.Pointer.ButtonState = null, + } = null, + + fn init(self: *Self, state: *State.Init) void { + self.state = state; + + std.debug.print("seat.init()\n", .{}); + self.seat.setListener(*Self, seatListener, self); + } + + fn pointerListener(pointer: *wl.Pointer, event: wl.Pointer.Event, self: *Self) void { + _ = pointer; + _ = self; + _ = event; + + } + + fn keyboardListener(keyboard: *wl.Keyboard, event: wl.Keyboard.Event, self: *Self) void { + _ = keyboard; + + switch (event) { + .keymap => |keymap_event| { + const keymap = blk: { + switch (keymap_event.format) { + .no_keymap => { + break :blk xkb.xkb_keymap_new_from_names( + self.state.xkb_context, + null, + xkb.XKB_KEYMAP_COMPILE_NO_FLAGS, + ); + }, + .xkb_v1 => { + const buffer = std.os.mmap( + null, + keymap_event.size, + std.os.PROT.READ, + std.os.MAP.PRIVATE, + keymap_event.fd, + 0, + ) catch break :blk null; + defer std.os.munmap(buffer); + defer std.os.close(keymap_event.fd); + + break :blk xkb.xkb_keymap_new_from_buffer( + self.state.xkb_context, + @ptrCast([*c]const u8, buffer), + keymap_event.size - 1, + xkb.XKB_KEYMAP_FORMAT_TEXT_V1, + xkb.XKB_KEYMAP_COMPILE_NO_FLAGS, + ); + + }, + else => unreachable, + } + }; + + if (keymap) |map| { + const state = xkb.xkb_state_new(map); + if (state) |state_| { + // SAFETY: keyboard cant be null because we are in the keyboard listener + self.keyboard.?.xkb = .{ + .keymap = map, + .state = state_, + }; + } + } + }, + .key => |key| { + std.debug.print("key: {} {s}", .{key.key, if (key.state == .pressed) "pressed" else "released"}); + }, + else => {}, + } + } + + fn seatListener(seat: *wl.Seat, event: wl.Seat.Event, self: *Self) void { + switch (event) { + .capabilities => |value| { + std.debug.print("seat capabilities: {}\n", .{value.capabilities}); + const capabilities = value.capabilities; + if (capabilities.keyboard) { + const keyboard = seat.getKeyboard() catch return; + self.keyboard = .{ + .keyboard = keyboard, + }; + + keyboard.setListener(*Self, keyboardListener, self); + } + + if (capabilities.pointer) { + const pointer = seat.getPointer() catch return; + self.pointer = .{ + .pointer = pointer, + }; + + pointer.setListener(*Self, pointerListener, self); + } + }, + else => {}, + } + } + + fn deinit(self: *Self) void { + if (self.keyboard) |keyboard| { + keyboard.keyboard.destroy(); + + if (keyboard.xkb) |kb| { + xkb.xkb_state_unref(kb.state); + xkb.xkb_keymap_unref(kb.keymap); + } + } + + if (self.pointer) |pointer| { + pointer.pointer.destroy(); + } + + + self.seat.destroy(); + } }; /// helper type to handle dynamic dispatch listeners whatever they're called @@ -202,10 +511,10 @@ fn Listener(comptime T: type, comptime D: type) type { self: *Self, comptime D2: type, new_data: *D2, - new_callback: @TypeOf(Listener(T, D2)).Fn, - ) @TypeOf(Listener(T, D2)) { - const other = @ptrCast(Listener(T, D2), self); - other.new_data = new_data; + new_callback: Listener(T, D2).Fn, + ) *Listener(T, D2) { + const other = @ptrCast(*Listener(T, D2), self); + other.data = new_data; other.callback = new_callback; return other; } @@ -221,13 +530,16 @@ const State = struct { dpy: *wl.Display = undefined, registry: *wl.Registry = undefined, - registry_listener: Listener(wl.Registry, Self) = undefined, + registry_listener: *Listener(wl.Registry, Self) = undefined, shm: ?*wl.Shm = null, compositor: ?*wl.Compositor = null, layer_shell: ?*zwlr.LayerShellV1 = null, xdg_output_manager: ?*zxdg.OutputManagerV1 = null, + seats: *ListItem(Seat).Head() = undefined, + outputs: *ListItem(Output).Head() = undefined, + fn create() !*Self { const ally = std.heap.c_allocator; @@ -236,7 +548,9 @@ const State = struct { const self = try ally.create(Self); - const listener = .{ + const listener = try ally.create(Listener(wl.Registry, Self)); + + listener.* = .{ .data = self, .callback = registryListener, }; @@ -246,16 +560,25 @@ const State = struct { .dpy = dpy, .registry = registry, .registry_listener = listener, + .seats = try ally.create(ListItem(Seat).Head()), + .outputs = try ally.create(ListItem(Output).Head()), }; + self.seats.init(); + self.outputs.init(); + self.registry_listener.set(self.registry); if (dpy.roundtrip() != .SUCCESS) return error.RoundtripFailed; return self; } - fn deinit(self: *Self) !void { + fn deinit(self: *Self) void { + self.ally.destroy(self.seats); + self.ally.destroy(self.outputs); self.ally.destroy(self); + + // TODO: clean up wayland state } fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, self: *Self) void { @@ -270,14 +593,22 @@ const State = struct { } else if (std.cstr.cmp(global.interface, zwlr.LayerShellV1.getInterface().name) == 0) { self.layer_shell = registry.bind(global.name, zwlr.LayerShellV1, 1) catch return; } else if (std.cstr.cmp(global.interface, zxdg.OutputManagerV1.getInterface().name) == 0) { - self.xdg_output_manager = registry.bind(global.name, zxdg.OutputManagerV1, 2) catch + self.xdg_output_manager = + registry.bind(global.name, zxdg.OutputManagerV1, 2) catch return; + } else if (std.cstr.cmp(global.interface, wl.Seat.getInterface().name) == 0) { const seat = registry.bind(global.name, wl.Seat, 1) catch return; - _ = seat; + + if (ListItem(Seat).create(self.ally, Seat{.seat = seat}) catch null) |ele| { + self.seats.append(ele); + } } else if (std.cstr.cmp(global.interface, wl.Output.getInterface().name) == 0) { const output = registry.bind(global.name, wl.Output, 3) catch return; - _ = output; + + if (ListItem(Output).create(self.ally, Output{.output = output}) catch null) |ele| { + self.outputs.append(ele); + } } }, .global_remove => {}, @@ -292,42 +623,7 @@ const State = struct { // free allocated memory if not returning it errdefer self.ally.destroy(init); - const xkb_context = xkb.xkb_context_new(xkb.XKB_CONTEXT_NO_FLAGS) orelse - return error.NoXkbContext; - // free xkb_context if we fail to initialize the state - errdefer xkb.xkb_context_unref(xkb_context); - - const cursor_theme = std.process.getEnvVarOwned(self.ally, "XCURSOR_THEME") catch ""; - // only free cursor_theme if bailing - errdefer self.ally.free(cursor_theme); - - const buf = try std.process.getEnvVarOwned(self.ally, "XCURSOR_SIZE"); - defer self.ally.free(buf); - - const cursor_size = std.fmt.parseInt(u32, buf, 10) catch return error.InvalidXCursorSize; - - init.* = Init{ - .ally = self.ally, - .dpy = self.dpy, - .registry = self.registry, - - // these 4 fields are set by the registry listener so they may be null at this point - // treat that as a hard error tho. - .shm = self.shm orelse return error.NoWlShm, - .compositor = self.compositor orelse return error.NoWlCompositor, - .layer_shell = self.layer_shell orelse return error.NoWlrLayerShell, - .xdg_output_manager = self.xdg_output_manager orelse return error.NoXdgOutputManager, - - // these 3 fields should never be null because init() will bail if these functions fail - .xkb_context = xkb_context, - .cursor_theme = cursor_theme, - .cursor_size = cursor_size, - }; - - init.outputs.init(); - - // i hope this is fine but appears to be requried to reset the listener on the registry - init.registry_listener = self.registry_listener.cast(Init, init, Init.registryListener); + try init.init(self); return init; } @@ -341,7 +637,7 @@ const State = struct { dpy: *wl.Display = undefined, registry: *wl.Registry = undefined, - registry_listener: Listener(wl.Registry, Self) = undefined, + registry_listener: *Listener(wl.Registry, Self) = undefined, shm: *wl.Shm = undefined, compositor: *wl.Compositor = undefined, @@ -350,11 +646,70 @@ const State = struct { xkb_context: *xkb.xkb_context = undefined, - outputs: ListItem(Output).Head() = undefined, - cursor_theme: []const u8 = undefined, cursor_size: u32 = undefined, + seats: *ListItem(Seat).Head() = undefined, + outputs: *ListItem(Output).Head() = undefined, + + fn init(self: *Self, uninit: *Uninit) !void { + const xkb_context = xkb.xkb_context_new(xkb.XKB_CONTEXT_NO_FLAGS) orelse + return error.NoXkbContext; + // free xkb_context if we fail to initialize the state + errdefer xkb.xkb_context_unref(xkb_context); + + const cursor_theme = std.process.getEnvVarOwned(uninit.ally, "XCURSOR_THEME") catch ""; + // only free cursor_theme if bailing + errdefer uninit.ally.free(cursor_theme); + + const buf = try std.process.getEnvVarOwned(uninit.ally, "XCURSOR_SIZE"); + defer uninit.ally.free(buf); + + const cursor_size = std.fmt.parseInt(u32, buf, 10) catch return error.InvalidXCursorSize; + + self.* = Self{ + .ally = uninit.ally, + .dpy = uninit.dpy, + .registry = uninit.registry, + + // these 4 fields are set by the registry listener so they may be null at this point + // treat that as a hard error tho. + .shm = uninit.shm orelse return error.NoWlShm, + .compositor = uninit.compositor orelse return error.NoWlCompositor, + .layer_shell = uninit.layer_shell orelse return error.NoWlrLayerShell, + .xdg_output_manager = uninit.xdg_output_manager orelse return error.NoXdgOutputManager, + + // these 3 fields should never be null because init() will bail if these functions fail + .xkb_context = xkb_context, + .cursor_theme = cursor_theme, + .cursor_size = cursor_size, + + .seats = uninit.seats, + .outputs = uninit.outputs, + }; + + // i hope this is fine but appears to be requried to reset the listener on the registry + self.registry_listener = uninit.registry_listener.cast(Self, self, Self.registryListener); + + // init seats + + var seats_iter = self.seats.safeIterator(.forward); + while (seats_iter.next()) |item| { + const seat = item.get(); + seat.init(self); + } + + // init outputs + + var output_iter = self.outputs.safeIterator(.forward); + while (output_iter.next()) |item| { + const output = item.get(); + try output.init(self); + } + + if (self.dpy.roundtrip() != .SUCCESS) return error.RoundtripFailed; + } + fn registryListener(registry: *wl.Registry, event: wl.Registry.Event, self: *Self) void { _ = registry; _ = self; @@ -365,9 +720,58 @@ const State = struct { } } + fn stuff(self: *Self) !void { + var it = self.outputs.iterator(.forward); + if (it.next()) |link| { + const output = link.get(); + const os = std.os; + + const buffer = blk: { + const width = 128; + const height = 128; + const stride = width * 4; + const size = stride * height; + + const fd = try os.memfd_create("hello-zig-wayland", 0); + try os.ftruncate(fd, size); + const data = try os.mmap(null, size, os.PROT.READ | os.PROT.WRITE, os.MAP.SHARED, fd, 0); + std.mem.copy(u8, data, @embedFile("cat.bgra")); + + const pool = try self.shm.createPool(fd, size); + defer pool.destroy(); + + break :blk try pool.createBuffer(0, width, height, stride, wl.Shm.Format.argb8888); + }; + defer buffer.destroy(); + + output.surface.attach(buffer, 0, 0); + output.surface.commit(); + + while (true) { + if (self.dpy.dispatch() != .SUCCESS) return error.DispatchFailed; + std.debug.print("asdf\n", .{}); + } + } + } + fn deinit(self: *Self) void { defer self.ally.destroy(self); + var seats_iter = self.seats.safeIterator(.forward); + while (seats_iter.next()) |next| { + next.remove(); + self.ally.destroy(next); + } + self.ally.destroy(self.seats); + + + var outputs_iter = self.outputs.safeIterator(.forward); + while (outputs_iter.next()) |next| { + next.remove(); + self.ally.destroy(next); + } + self.ally.destroy(self.outputs); + self.shm.destroy(); self.compositor.destroy(); self.layer_shell.destroy(); @@ -386,8 +790,7 @@ pub fn main() !void { const state = try State.Uninit.create(); const init = try state.intoInit(); - - _ = try std.io.getStdIn().reader().readByte(); - defer init.deinit(); + + try init.stuff(); }