From 1a647eced2de85b396576a83b31bd6966e356d12 Mon Sep 17 00:00:00 2001 From: Simen Kirkvik Date: Sat, 7 Mar 2026 14:29:17 +0100 Subject: [PATCH] Keyboard and rotation! --- build.zig | 3 + shell.nix | 3 + src/main.zig | 134 +++++++++++++++++++++++++++++++++++++++++-- src/platform.zig | 12 ++++ src/root.zig | 146 ++++++++++++++++++++++++++++++++--------------- 5 files changed, 245 insertions(+), 53 deletions(-) diff --git a/build.zig b/build.zig index 3f0882c..16d0b48 100644 --- a/build.zig +++ b/build.zig @@ -8,6 +8,7 @@ pub fn build(b: *std.Build) void { // of this build script using `b.option()`. All defined flags (including // target and optimize options) will be listed when running `zig build --help` // in this directory. + const llvm = b.option(bool, "llvm", "Build with llvm"); const mod = b.addModule("puzzle", .{ .root_source_file = b.path("src/root.zig"), @@ -16,6 +17,7 @@ pub fn build(b: *std.Build) void { const exe = b.addExecutable(.{ .name = "wl-main", + .use_llvm = llvm, .root_module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, @@ -27,6 +29,7 @@ pub fn build(b: *std.Build) void { }); exe.root_module.link_libc = true; exe.root_module.linkSystemLibrary("wayland-client", .{}); + exe.root_module.linkSystemLibrary("xkbcommon", .{}); exe.root_module.addIncludePath(b.path("src")); exe.root_module.addCSourceFiles(.{ diff --git a/shell.nix b/shell.nix index 812b008..fcccd91 100644 --- a/shell.nix +++ b/shell.nix @@ -4,9 +4,12 @@ mkShell { zig zls wayland-scanner + gdb + lldb ]; buildInputs = [ wayland + libxkbcommon ]; } diff --git a/src/main.zig b/src/main.zig index 68b9b89..ff44ac4 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const linux = std.os.linux; const puzzle = @import("puzzle"); @@ -7,6 +8,7 @@ const c = @cImport({ @cInclude("wayland-client-core.h"); @cInclude("xdg-shell.h"); @cInclude("linux/input-event-codes.h"); + @cInclude("xkbcommon/xkbcommon.h"); }); const WlPointerEventMask = packed struct(u16) { @@ -45,6 +47,10 @@ const WaylandState = struct { shared_memory: ?*c.wl_shm, xdg_wm_base: ?*c.xdg_wm_base, + xkb_context: ?*c.xkb_context, + xkb_keymap: ?*c.xkb_keymap, + xkb_state: ?*c.xkb_state, + allocator: std.mem.Allocator, resize: bool, @@ -88,8 +94,8 @@ fn createSharedMemoryPool(wl_state: WaylandState) !MemoryPool { const fd: c_int = try createShmFile(pool_size); - const prot = std.os.linux.PROT.READ | std.os.linux.PROT.WRITE; - const flags: std.os.linux.MAP = .{ .TYPE = .SHARED }; + const prot = linux.PROT.READ | std.os.linux.PROT.WRITE; + const flags: linux.MAP = .{ .TYPE = .SHARED }; const ret = try std.posix.mmap(null, pool_size, prot, flags, fd, 0); const pool = c.wl_shm_create_pool(wl_state.shared_memory, fd, @intCast(pool_size)); @@ -585,14 +591,124 @@ const wl_pointer_listener: c.struct_wl_pointer_listener = .{ .frame = wlPointerHandleFrame, }; +fn wlKeyboardHandleKeymap( + data: ?*anyopaque, + wl_keyboard: ?*c.struct_wl_keyboard, + format: u32, + fd: i32, + size: u32, +) callconv(.c) void { + _ = wl_keyboard; + std.debug.assert(format == c.WL_KEYBOARD_KEYMAP_FORMAT_XKB_V1); + const wl_state: *WaylandState = @ptrCast(@alignCast(data)); + + const map_shm: []align(4096) const u8 = std.posix.mmap(null, size, linux.PROT.READ, .{ .TYPE = .SHARED }, fd, 0) catch { + std.debug.panic("[Wayland] Keyboard keymap handler failed mmap", .{}); + }; + + const keymap: ?*c.struct_xkb_keymap = c.xkb_keymap_new_from_string(wl_state.xkb_context, @ptrCast(map_shm), c.XKB_KEYMAP_FORMAT_TEXT_V1, c.XKB_KEYMAP_COMPILE_NO_FLAGS); + + std.posix.munmap(map_shm); + std.posix.close(fd); + + const xkb_state: ?*c.struct_xkb_state = c.xkb_state_new(keymap); + + // NOTE: Unmap existing ones + if (wl_state.xkb_keymap != null) c.xkb_keymap_unref(wl_state.xkb_keymap); + if (wl_state.xkb_state != null) c.xkb_state_unref(wl_state.xkb_state); + + wl_state.xkb_keymap = keymap; + wl_state.xkb_state = xkb_state; +} + +fn wlKeyboardHandleEnter(data: ?*anyopaque, keyboard: ?*c.wl_keyboard, serial: u32, surface: ?*c.wl_surface, keys: [*c]c.wl_array) callconv(.c) void { + _ = data; + _ = keyboard; + _ = serial; + _ = surface; + _ = keys; +} + +fn wlKeyboardHandleLeave(data: ?*anyopaque, keyboard: ?*c.wl_keyboard, serial: u32, surface: ?*c.wl_surface) callconv(.c) void { + _ = data; + _ = keyboard; + _ = serial; + _ = surface; +} + +fn wlKeyboardHandleKey(data: ?*anyopaque, keyboard: ?*c.wl_keyboard, serial: u32, time: u32, key: u32, state: u32) callconv(.c) void { + _ = keyboard; + _ = serial; + _ = time; + + const wl_state: *WaylandState = @ptrCast(@alignCast(data)); + var buf: [32:0]u8 = undefined; + const keycode: u32 = key + 8; + + const sym: c.xkb_keysym_t = c.xkb_state_key_get_one_sym(wl_state.xkb_state, keycode); + _ = c.xkb_keysym_get_name(sym, &buf, buf.len); + + const pressed = state == c.WL_KEYBOARD_KEY_STATE_PRESSED or state == c.WL_KEYBOARD_KEY_STATE_REPEATED; + + std.debug.print("{s} {any}\n", .{buf, pressed}); + + switch (sym) { + c.XKB_KEY_Escape => { + wl_state.running = false; + }, + c.XKB_KEY_space => { + wl_state.app_input.key.space.down = pressed; + }, + c.XKB_KEY_r => { + wl_state.app_input.key.r.down = pressed; + }, + c.XKB_KEY_q => { + wl_state.app_input.key.q.down = pressed; + }, + else => {}, + } +} + +fn wlKeyboardHandleModifiers(data: ?*anyopaque, keyboard: ?*c.wl_keyboard, serial: u32, mods_depressed: u32, mods_latched: u32, mods_locked: u32, group: u32) callconv(.c) void { + _ = data; + _ = keyboard; + _ = serial; + _ = mods_depressed; + _ = mods_latched; + _ = mods_locked; + _ = group; +} + +fn wlKeyboardHandleRepeatInfo(data: ?*anyopaque, keyboard: ?*c.wl_keyboard, rate: i32, delay: i32) callconv(.c) void { + _ = data; + _ = keyboard; + _ = rate; + _ = delay; +} + +const wl_keyboard_listener: c.wl_keyboard_listener = .{ + .keymap = wlKeyboardHandleKeymap, + .enter = wlKeyboardHandleEnter, + .leave = wlKeyboardHandleLeave, + .key = wlKeyboardHandleKey, + .modifiers = wlKeyboardHandleModifiers, + .repeat_info = wlKeyboardHandleRepeatInfo, +}; + fn wlSeatHandleCapabilities(data: ?*anyopaque, wl_seat: ?*c.struct_wl_seat, capabilities: u32) callconv(.c) void { const wl_state: *WaylandState = @ptrCast(@alignCast(data)); - if (capabilities & c.WL_SEAT_CAPABILITY_POINTER == 1) { + if (capabilities & c.WL_SEAT_CAPABILITY_POINTER == c.WL_SEAT_CAPABILITY_POINTER) { std.debug.print("[Wayland] wl_seat: got pointer capability\n", .{}); const pointer: ?*c.struct_wl_pointer = c.wl_seat_get_pointer(wl_seat); _ = c.wl_pointer_add_listener(pointer, &wl_pointer_listener, wl_state); } + + if (capabilities & c.WL_SEAT_CAPABILITY_KEYBOARD == c.WL_SEAT_CAPABILITY_KEYBOARD) { + std.debug.print("[Wayland] wl_seat: got keyboard capability\n", .{}); + const keyboard = c.wl_seat_get_keyboard(wl_seat); + _ = c.wl_keyboard_add_listener(keyboard, &wl_keyboard_listener, wl_state); + } } fn wlSeatHandleName(data: ?*anyopaque, wl_seat: ?*c.struct_wl_seat, name: [*c]const u8) callconv(.c) void { @@ -647,6 +763,9 @@ pub fn main() u8 { std.debug.print("[Wayland] Connection established.\n", .{}); var wl_state: WaylandState = undefined; + wl_state.xkb_context = null; + wl_state.xkb_state = null; + wl_state.xkb_keymap = null; const registry = c.wl_display_get_registry(display); if (registry == null) { @@ -656,6 +775,8 @@ pub fn main() u8 { _ = c.wl_registry_add_listener(registry, ®istry_listener, &wl_state); + wl_state.xkb_context = c.xkb_context_new(c.XKB_CONTEXT_NO_FLAGS); + _ = c.wl_display_roundtrip(display); if (wl_state.compositor == null or wl_state.shared_memory == null or wl_state.seat == null) { std.debug.print("ERROR: Wayland compositor, seat, or shm not bound.\n", .{}); @@ -835,13 +956,14 @@ pub fn main() u8 { _ = c.wl_display_roundtrip(display); - std.posix.nanosleep(0, std.time.ns_per_ms * 32); + // std.posix.nanosleep(0, std.time.ns_per_ms * 32); const current = timer.lap(); + const current_ms = current / std.time.ns_per_ms; const sleep_time_ms: f64 = @as(f64, @floatFromInt(sleep_time)) / @as(f64, @floatFromInt(std.time.ns_per_ms)); std.debug.print( - "Render: {d}ms, Sleeping: {d}ms, Frame: {d}ms.\n", - .{ render_time_ms, sleep_time_ms, current / std.time.ns_per_ms }, + "Render: {d:.3}ms, Sleeping: {d:.3}ms, Frame: {d}ms.\n", + .{ render_time_ms, sleep_time_ms, current_ms }, ); } diff --git a/src/platform.zig b/src/platform.zig index 808a258..3225dec 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -8,6 +8,16 @@ pub fn megaBytes(bytes: usize) usize { return kiloBytes(bytes) * 1024; } +const Key = struct { + down: bool, +}; + +const Keyboard = struct { + r: Key, + q: Key, + space: Key, +}; + pub const AppInput = struct { mouse_present: bool = false, mouse_left_down: bool = false, @@ -15,6 +25,8 @@ pub const AppInput = struct { mouse_y: f64, scroll_y: f64, + + key: Keyboard, }; /// Buffer the platform creates and hands off to the app diff --git a/src/root.zig b/src/root.zig index 0622c57..7596168 100644 --- a/src/root.zig +++ b/src/root.zig @@ -30,8 +30,28 @@ const Camera = struct { }; const Brick = struct { + rotating: bool, pos: V2, + color: u32, tiles: [4][4]u32, + + fn rotate(self: Brick) [4][4]u32 { + var result = [4][4]u32{ + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, + }; + + for (0..self.tiles.len) |i| { + const row = self.tiles[i]; + for (0..row.len) |j| { + result[j][row.len - i - 1] = self.tiles[i][j]; + } + } + + return result; + } }; const State = struct { @@ -74,8 +94,8 @@ fn fillRect( if (buffer.width < end_x) end_x = buffer.width; if (buffer.height < end_y) end_y = buffer.height; - for (@intCast(start_y) .. @intCast(end_y)) |y| { - for (@intCast(start_x) .. @intCast(end_x)) |x| { + for (@intCast(start_y)..@intCast(end_y)) |y| { + for (@intCast(start_x)..@intCast(end_x)) |x| { const idx: usize = y * @as(usize, @intCast(buffer.width)) + x; pixels[idx] = color; @@ -96,91 +116,109 @@ fn fillSquare( pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBuffer, input: platform.AppInput) void { var state: *State = @ptrCast(@alignCast(memory.permanent_storage)); - const mid = V2{ .x = @floatFromInt(@divTrunc(buffer.width, 2)), .y = @floatFromInt(@divTrunc(buffer.height, 2)), }; + const mid = V2{ + .x = @floatFromInt(@divTrunc(buffer.width, 2)), + .y = @floatFromInt(@divTrunc(buffer.height, 2)), + }; if (!memory.initialized) { state.camera.offset = mid; state.bricks[0] = Brick{ - .pos = V2{ .x = 0, .y = 50}, + .rotating = false, + .pos = V2{ .x = 0, .y = 50 }, + .color = 0xFFFF0000, .tiles = [4][4]u32{ - .{1, 1, 1, 1}, - .{1, 0, 0, 0}, - .{0, 0, 0, 0}, - .{0, 0, 0, 0}, + .{ 1, 1, 1, 1 }, + .{ 1, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, }, }; state.bricks[1] = Brick{ - .pos = V2{ .x = 32, .y = 50}, + .rotating = false, + .pos = V2{ .x = 32, .y = 50 }, + .color = 0xFFEF0000, .tiles = [4][4]u32{ - .{2, 2, 2, 0}, - .{2, 2, 0, 0}, - .{0, 0, 0, 0}, - .{0, 0, 0, 0}, + .{ 2, 2, 2, 0 }, + .{ 2, 2, 0, 0 }, + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, }, }; state.bricks[2] = Brick{ - .pos = V2{ .x = 64, .y = 50}, + .rotating = false, + .pos = V2{ .x = 64, .y = 50 }, + .color = 0xFFDF0000, .tiles = [4][4]u32{ - .{3, 3, 3, 0}, - .{0, 0, 3, 3}, - .{0, 0, 0, 0}, - .{0, 0, 0, 0}, + .{ 3, 3, 3, 0 }, + .{ 0, 0, 3, 3 }, + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, }, }; state.bricks[3] = Brick{ - .pos = V2{ .x = 96, .y = 50}, + .rotating = false, + .pos = V2{ .x = 96, .y = 50 }, + .color = 0xFFCF0000, .tiles = [4][4]u32{ - .{4, 0, 0, 0}, - .{4, 4, 4, 0}, - .{0, 0, 4, 0}, - .{0, 0, 0, 0}, + .{ 4, 0, 0, 0 }, + .{ 4, 4, 4, 0 }, + .{ 0, 0, 4, 0 }, + .{ 0, 0, 0, 0 }, }, }; state.bricks[4] = Brick{ - .pos = V2{ .x = 128, .y = 50}, + .rotating = false, + .pos = V2{ .x = 128, .y = 150 }, + .color = 0xFFBF0000, .tiles = [4][4]u32{ - .{5, 5, 5, 5}, - .{0, 5, 0, 0}, - .{0, 0, 0, 0}, - .{0, 0, 0, 0}, + .{ 5, 5, 5, 5 }, + .{ 0, 5, 0, 0 }, + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, }, }; state.bricks[5] = Brick{ - .pos = V2{ .x = 160, .y = 50}, + .rotating = false, + .pos = V2{ .x = 160, .y = 250 }, + .color = 0xFFAF0000, .tiles = [4][4]u32{ - .{6, 6, 6, 0}, - .{6, 0, 0, 0}, - .{6, 0, 0, 0}, - .{0, 0, 0, 0}, + .{ 6, 6, 6, 0 }, + .{ 6, 0, 0, 0 }, + .{ 6, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, }, }; state.bricks[6] = Brick{ - .pos = V2{ .x = 192, .y = 50}, + .rotating = false, + .pos = V2{ .x = 192, .y = 350 }, + .color = 0xFF9F0000, .tiles = [4][4]u32{ - .{7, 7, 7, 0}, - .{7, 7, 7, 0}, - .{0, 0, 0, 0}, - .{0, 0, 0, 0}, + .{ 7, 7, 7, 0 }, + .{ 7, 7, 7, 0 }, + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, }, }; state.bricks[7] = Brick{ - .pos = V2{ .x = 224, .y = 50}, + .rotating = false, + .pos = V2{ .x = 224, .y = 450 }, + .color = 0xFF8F0000, .tiles = [4][4]u32{ - .{8, 8, 8, 0}, - .{8, 0, 8, 0}, - .{0, 0, 0, 0}, - .{0, 0, 0, 0}, + .{ 8, 8, 8, 0 }, + .{ 8, 0, 8, 0 }, + .{ 0, 0, 0, 0 }, + .{ 0, 0, 0, 0 }, }, }; - memory.initialized = true; } @@ -194,6 +232,8 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu var mouse_on_brick = false; for (&state.bricks) |*brick| { + brick.rotating = false; + for (brick.tiles, 0..) |row, i| { for (row, 0..) |tile, j| { if (tile > 0) { @@ -208,6 +248,14 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu if (input.mouse_left_down) { const diff = mouse_pos.sub(state.mouse_pos); brick.pos = brick.pos.add(diff); + + if (input.key.space.down and !brick.rotating) { + // TODO: Rotate + brick.rotating = true; + const rotated = brick.rotate(); + brick.tiles = rotated; + } + mouse_on_brick = true; } } @@ -221,7 +269,6 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu state.camera.pos = state.camera.pos.add(diff); } - state.mouse_pos = mouse_pos; // Render @@ -255,7 +302,13 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu .y = @floatFromInt(i * TILE_SIZE), }; const camera_pos = brick.pos.add(pos).add(state.camera.pos); - fillSquare(buffer, @intFromFloat(camera_pos.x), @intFromFloat(camera_pos.y), 30, 0xFFFF0000); + fillSquare( + buffer, + @intFromFloat(camera_pos.x), + @intFromFloat(camera_pos.y), + 30, + if (brick.rotating) 0xFF00FF00 else brick.color , + ); } } } @@ -263,4 +316,3 @@ pub fn updateAndRender(memory: *platform.AppMemory, buffer: platform.OffscreenBu state.frame_count += 1; } -