From de3182744be1a44fd5991fb198f1472d2bdc280c Mon Sep 17 00:00:00 2001 From: copygirl Date: Sat, 20 Apr 2024 14:51:19 +0200 Subject: [PATCH] Initial commit --- .gitignore | 2 + .vscode/launch.json | 12 +++++ .vscode/tasks.json | 16 +++++++ build.zig | 38 +++++++++++++++ build.zig.zon | 18 +++++++ gfx/sprite_map.png | Bin 0 -> 3190 bytes gfx/sprite_map.xcf | Bin 0 -> 53639 bytes src/canvas.zig | 102 ++++++++++++++++++++++++++++++++++++++++ src/grid.zig | 51 ++++++++++++++++++++ src/keys.zig | 112 ++++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 92 ++++++++++++++++++++++++++++++++++++ src/window.zig | 105 +++++++++++++++++++++++++++++++++++++++++ 12 files changed, 548 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/tasks.json create mode 100644 build.zig create mode 100644 build.zig.zon create mode 100644 gfx/sprite_map.png create mode 100644 gfx/sprite_map.xcf create mode 100644 src/canvas.zig create mode 100644 src/grid.zig create mode 100644 src/keys.zig create mode 100644 src/main.zig create mode 100644 src/window.zig diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d864d9e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/zig-cache/ +/zig-out/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..4dbc419 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,12 @@ +{ + "version": "0.2.0", + "configurations": [{ + "name": "Debug", + "type": "lldb", + "preLaunchTask": "build", + "request": "launch", + "cwd": "${workspaceFolder}", + "program": "${workspaceFolder}/zig-out/bin/gridstep", + "args": [], + }] +} diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..7baa495 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,16 @@ +{ + "version": "2.0.0", + "tasks": [{ + "label": "build", + "group": { "kind": "build", "isDefault": true }, + "type": "shell", + "command": "zig build", + "problemMatcher": [] + },{ + "label": "test", + "group": { "kind": "test", "isDefault": true }, + "type": "shell", + "command": "zig build test", + "problemMatcher": [] + }] +} diff --git a/build.zig b/build.zig new file mode 100644 index 0000000..b58fd3a --- /dev/null +++ b/build.zig @@ -0,0 +1,38 @@ +const std = @import("std"); +const sdl = @import("sdl"); + +pub fn build(b: *std.Build) void { + const target = b.standardTargetOptions(.{}); + const optimize = b.standardOptimizeOption(.{}); + + const sdl_sdk = sdl.init(b, null); + + const exe = b.addExecutable(.{ + .name = "gridstep", + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + + sdl_sdk.link(exe, .dynamic); + exe.linkSystemLibrary("sdl2_image"); + exe.linkSystemLibrary("libpng"); + + exe.root_module.addImport("sdl", sdl_sdk.getNativeModule()); + b.installArtifact(exe); + + const run_cmd = b.addRunArtifact(exe); + run_cmd.step.dependOn(b.getInstallStep()); + if (b.args) |args| run_cmd.addArgs(args); + const run_step = b.step("run", "Run the app"); + run_step.dependOn(&run_cmd.step); + + const unit_tests = b.addTest(.{ + .root_source_file = .{ .path = "src/main.zig" }, + .target = target, + .optimize = optimize, + }); + const run_unit_tests = b.addRunArtifact(unit_tests); + const test_step = b.step("test", "Run unit tests"); + test_step.dependOn(&run_unit_tests.step); +} diff --git a/build.zig.zon b/build.zig.zon new file mode 100644 index 0000000..eed2f0c --- /dev/null +++ b/build.zig.zon @@ -0,0 +1,18 @@ +.{ + .name = "gridstep", + .version = "0.0.0", + .minimum_zig_version = "0.11.0", + + .dependencies = .{ + .sdl = .{ + .url = "https://github.com/MasterQ32/SDL.zig/archive/39fb8355cccb45a241a891c4848ab925af20fee4.tar.gz", + .hash = "12203537fc1357c4efce1c7a5ae3b407b9070490d83acd69aa3363b6b8e8a2b27870", + }, + }, + + .paths = .{ + "src", + "build.zig", + "build.zig.zon", + }, +} diff --git a/gfx/sprite_map.png b/gfx/sprite_map.png new file mode 100644 index 0000000000000000000000000000000000000000..85aba4f0a486f97ea7ac5e8075cf62efaf35619c GIT binary patch literal 3190 zcmcIm`#;l*8~?Z*rEgB*REG%7rPL&sO3URG+sq*xeS}_+xfn}|HAkEdOffA^UL$R->>I+J+If3r0pdDkol3y06P_akk{>!fFGoI3+WyP07j61LMF11!)&q%@&S zI&5-Q4Y-38qdRwE0P$rGA$tHX<-!_Ygv%WFcr+`&a4^QfU;OMGRB;q+;#q0^5dSY; z{Yoa*Jz%nuJqL3cgn;LVM$P0tZArPzr+snSqqP0EYLYB$E-CrWN0`V(1LsOsrGH50 z<^ES3c?!~5S05peU*%ogE=s0b&SCo=ho*hbWeOOc0*k3!Dpv>Bt_PFTly?jE>rs!U znZNdjpYJN!z6v1(>&v;qH|Putj(cs*wAdWx<*}$U)ktr#_#v=!r%;wmjxsx;ktd)TSq1B0m(SFRm*OgGaxnkwxnD<>7vz5A$?e=GPV@O z`Ovi{QAvN(9_4y#ihHW9dX0V}7xz=tr-|4i@`xl)%?6|0Q-D<&S2j3%iOB?9qj5lv`~2kd7zQM8A1(NfE6lRfirhKl#{zXm85KdZ&ZqGE(b zN7t|~n*;PDAIwI{vyB(Z+!V`~s`J5lf#*|8^%!cav@3T>Xo;b8yb>N4P*rUYU(^K{ zjQBLMZLS451T(3ne9E-NZyni?G6Q~l+X%j-?T-Sp5AMoed*jJr(jC%~TDAe!w)Snw zTq$u!N@~8rI?m8L?&RXt0!G90F~oqDE8j zT}U}Z2i=XZiB4-OPtf|N7(jdxf(zL1vB#JF%g~2-1S*wWhP&fB@-j=3%d{r%(e_Z~ zvy#SEG#i`J1az}b>SU$iSA@DBnDLz0)nKy)2Zeu?( z8>9<93oIc66-5dizv$H?e+lQi_1lk|k+_&+D})AbsooJDmEK`#{51zoe(^{$qE%;V zQ)neuoACbOnns^hLBabD3f{;<`Z=&OvlnODB5+kz^yCeuOOD^AJ%|v@Gqab)EY~1J z1vYO};`WZmU;qC8!TBH~_sb*%b}xS9MvYhfR}I9+5X(leBB*VpmWyT=6=e&pr?&mG zzJN8a25vZ1Ne!bn23-CeKR`04+{VTC4TA#}TVizhn$B0XY0*mK_TvgIG{ZImo%TB=Sq9` z8hOb@qDFq^f7$+9*3j^f}_$Z9UEKg#7oUqSHtasw-()n(tUD~4oXHh zB1Qx8G3=~4wz-~Z3xlSx<#BQu9MX0hb@+pwFX7+c6~S+!b4+bp8LJz@5`f@=80xu% z$Jv|Zuy+`f-A>kP`36iTAD6w2zSzCn0T(M^$;1q$S=7%Cq?)Fonlg)<)6=YRiFQ?l zCx!xHeg510s~Gz_q;)%`Z=AkSd)NAj`<|lkqvg5GS1lxYG<5s^ao_vIwvm;45mV3I z`oqfJp&t&pmmhV>ao-sSVr0=Mn_~<108pg3#4oEhq>lv? zA4K*iuGvfav5U_uWP>9iqmL7$FJ21p_lEW3kg|`mC-rN4O6|O% zqv+X$@&c!r{X85^!a*%~IzArMYd*Y3dCYsAcfmn4f4TcE^(llCbKIL5GGE7Dxv6>W z>;dvuNiBEtvA(%xabC92dCpZu`F-hwGnKwO*2lFpQ2L&j=ZuCvK-UR|TNe%LO-#s0NnS2DXKAZL`Jb1s_2{ZC;hGh|@FiptVhD~bp8lOYSdv*|YvSsRawH zX}p?w9BKCzg(hoihZSUuRnkS?ltpyECK_Q_q*$RzE@{9F2Yr-cp;Q5bJ#hk`$n3=y zg0+7_6gO^mMRsO=@6or_T}~$r9GCNrP+xWla(njx?bw-@5sMpXqHdX?RHTOU>P@Ti zVGrb;Ih|;ch@C>2{_|CTA`^zRq8632)uf0W?OA%Lff@auRHhN zm==;15fs1Y3bAdBPnN;GvI}d!n*c|SPk_Oppe2ArwN-Qo&wy3DjYZ_SxvN<++Nx(P zV&IeMSz=2Q@Kga+8 literal 0 HcmV?d00001 diff --git a/gfx/sprite_map.xcf b/gfx/sprite_map.xcf new file mode 100644 index 0000000000000000000000000000000000000000..e265c91203d4080edf908e568e1c4f748c29efcd GIT binary patch literal 53639 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 NaJ_O2$)lAk{tqR?beI4D literal 0 HcmV?d00001 diff --git a/src/canvas.zig b/src/canvas.zig new file mode 100644 index 0000000..c25715d --- /dev/null +++ b/src/canvas.zig @@ -0,0 +1,102 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const sdl = @import("sdl"); +const Window = @import("./window.zig"); +const makeSdlError = Window.makeSdlError; + +const Canvas = @This(); + +allocator: Allocator, +surface: *sdl.SDL_Surface, +pixels: []Pixel, +width: usize, +height: usize, + +pub fn init(allocator: Allocator, width: usize, height: usize) !*Canvas { + const surface = sdl.SDL_CreateRGBSurfaceWithFormat( + 0, + @intCast(width), + @intCast(height), + 0, + sdl.SDL_PIXELFORMAT_ABGR8888, + ) orelse + return makeSdlError(); + + const pixels_ptr: [*]Pixel = @alignCast(@ptrCast(surface.pixels)); + const pixels_len = width * height; + const pixels = pixels_ptr[0..pixels_len]; + + const result = try allocator.create(Canvas); + result.* = .{ + .allocator = allocator, + .surface = surface, + .pixels = pixels, + .width = width, + .height = height, + }; + return result; +} + +pub fn deinit(self: *Canvas) void { + sdl.SDL_FreeSurface(self.surface); + self.allocator.destroy(self); +} + +pub fn getUnsafe(self: *Canvas, x: usize, y: usize) *Pixel { + const index = x + y * self.width; + return &self.pixels[index]; +} + +pub fn get(self: *Canvas, x: usize, y: usize) *Pixel { + std.debug.assert(x < self.width); + std.debug.assert(y < self.height); + return self.getUnsafe(x, y); +} + +pub fn put(self: *Canvas, x: usize, y: usize, value: Pixel) void { + self.get(x, y).* = value; +} + +pub fn putRect(self: *Canvas, x: usize, y: usize, w: usize, h: usize, value: Pixel) void { + std.debug.assert(x + w <= self.width); + std.debug.assert(y + h <= self.height); + const start_index = x + y * self.width; + for (0..h) |yo| { + const row_index = start_index + yo * self.width; + @memset(self.pixels[row_index..][0..w], value); + } +} + +pub fn putSprite(self: *Canvas, x: usize, y: usize, sprite: *const Sprite) void { + std.debug.assert(x + 16 <= self.width); + std.debug.assert(y + 16 <= self.height); + for (0..16) |yo| + for (0..16) |xo| { + const value = sprite[yo][xo]; + if (value.a == 0x00) continue; + + const dest = self.getUnsafe(x + xo, y + yo); + if (value.a == 0xFF) + dest.* = value + else + @panic("Not supported"); + }; +} + +pub fn clear(self: *Canvas, value: Pixel) void { + @memset(self.pixels, value); +} + +pub const Pixel = packed struct { + r: u8, + g: u8, + b: u8, + a: u8, + + pub const transparent = Pixel{ .r = 0x00, .g = 0x00, .b = 0x00, .a = 0x00 }; + pub const black = Pixel{ .r = 0x00, .g = 0x00, .b = 0x00, .a = 0xFF }; + pub const white = Pixel{ .r = 0xFF, .g = 0xFF, .b = 0xFF, .a = 0xFF }; +}; + +pub const Sprite = [16][16]Pixel; diff --git a/src/grid.zig b/src/grid.zig new file mode 100644 index 0000000..d19d53b --- /dev/null +++ b/src/grid.zig @@ -0,0 +1,51 @@ +const std = @import("std"); + +const Grid = @This(); + +pub const size = 16; + +array: [size][size][size]Tile = .{.{.{0} ** size} ** size} ** size, + +pub fn get(self: *Grid, pos: Pos) Ref { + return .{ .grid = self, .pos = pos }; +} + +pub fn layerAs(self: *Grid, z: i4, comptime T: type) []T { + const bytes = std.mem.sliceAsBytes(&self.array[@as(u4, @bitCast(z))]); + return std.mem.bytesAsSlice(T, bytes); +} + +pub const Tile = u8; + +pub const Pos = packed struct { + x: i4, + y: i4, + z: i4, + + pub fn init(x: i4, y: i4, z: i4) Pos { + return .{ .x = x, .y = y, .z = z }; + } + + pub fn initTruncate(x: anytype, y: anytype, z: anytype) Pos { + const sign = @typeInfo(@TypeOf(x, y, z)).Int.signedness; + const T = @Type(.{ .Int = .{ .signedness = sign, .bits = 4 } }); + return .{ + .x = @bitCast(@as(T, @truncate(x))), + .y = @bitCast(@as(T, @truncate(y))), + .z = @bitCast(@as(T, @truncate(z))), + }; + } +}; + +pub const Ref = struct { + grid: *Grid, + pos: Pos, + + pub fn as(self: Ref, comptime T: type) *T { + std.debug.assert(@sizeOf(T) == @sizeOf(Tile)); + const x: usize = @as(u4, @bitCast(self.pos.x)); + const y: usize = @as(u4, @bitCast(self.pos.y)); + const z: usize = @as(u4, @bitCast(self.pos.z)); + return @ptrCast(&self.grid.array[z][y][x]); + } +}; diff --git a/src/keys.zig b/src/keys.zig new file mode 100644 index 0000000..5c3fa09 --- /dev/null +++ b/src/keys.zig @@ -0,0 +1,112 @@ +const std = @import("std"); +const sdl = @import("sdl"); + +pub const PhysicalKey = enum(u8) { + // zig fmt: off + backspace = 0x08, + tab = '\t', + enter = '\r', + escape = 0x1B, + + // Numbers + _0 = '0', _1 = '1', _2 = '2', _3 = '3', _4 = '4', + _5 = '5', _6 = '6', _7 = '7', _8 = '8', _9 = '9', + + // Letters + a = 'A', b = 'B', c = 'C', d = 'D', e = 'E', f = 'F', g = 'G', + h = 'H', i = 'I', j = 'J', k = 'K', l = 'L', m = 'M', n = 'N', + o = 'O', p = 'P', q = 'Q', r = 'R', s = 'S', t = 'T', u = 'U', + v = 'V', w = 'W', x = 'X', y = 'Y', z = 'Z', + + // Visual + space = ' ', + grave = '`', + minus = '-', + equals = '=', + left_bracket = '[', + right_bracket = ']', + semicolon = ';', + apostrophe = '\'', + backslash = '\\', + comma = ',', + period = '.', + slash = '/', + + // Nagivation + insert = 0x8D, + delete = 0x7F, + home = 0x8E, + end = 0x9E, + page_up = 0x8F, + page_down = 0x9F, + + up = 0x8C, + down = 0x9C, + left = 0x9B, + right = 0x9D, + + // Modifiers + ctrl = 0x88, + alt = 0x89, + shift = 0x8A, + caps = 0x8B, + + _, + // zig fmt: on +}; + +pub const lookup = blk: { + const size = sdl.SDL_NUM_SCANCODES; + var result: [size]?PhysicalKey = .{null} ** size; + + result[sdl.SDL_SCANCODE_BACKSPACE] = PhysicalKey.backspace; + result[sdl.SDL_SCANCODE_TAB] = PhysicalKey.tab; + result[sdl.SDL_SCANCODE_RETURN] = PhysicalKey.enter; + result[sdl.SDL_SCANCODE_ESCAPE] = PhysicalKey.escape; + + for ('0'..'9' + 1) |i| { + const scancode = @field(sdl, "SDL_SCANCODE_" ++ .{i}); + const physical_key = @field(PhysicalKey, "_" ++ .{i}); + result[scancode] = physical_key; + } + + for ('A'..'Z' + 1) |i| { + const scancode = @field(sdl, "SDL_SCANCODE_" ++ .{i}); + const physical_key = @field(PhysicalKey, &.{std.ascii.toLower(i)}); + result[scancode] = physical_key; + } + + result[sdl.SDL_SCANCODE_GRAVE] = PhysicalKey.grave; + result[sdl.SDL_SCANCODE_MINUS] = PhysicalKey.minus; + result[sdl.SDL_SCANCODE_EQUALS] = PhysicalKey.equals; + result[sdl.SDL_SCANCODE_LEFTBRACKET] = PhysicalKey.left_bracket; + result[sdl.SDL_SCANCODE_RIGHTBRACKET] = PhysicalKey.right_bracket; + result[sdl.SDL_SCANCODE_SEMICOLON] = PhysicalKey.semicolon; + result[sdl.SDL_SCANCODE_APOSTROPHE] = PhysicalKey.apostrophe; + result[sdl.SDL_SCANCODE_BACKSLASH] = PhysicalKey.backslash; + result[sdl.SDL_SCANCODE_COMMA] = PhysicalKey.comma; + result[sdl.SDL_SCANCODE_PERIOD] = PhysicalKey.period; + result[sdl.SDL_SCANCODE_SLASH] = PhysicalKey.slash; + result[sdl.SDL_SCANCODE_SPACE] = PhysicalKey.space; + + result[sdl.SDL_SCANCODE_INSERT] = PhysicalKey.insert; + result[sdl.SDL_SCANCODE_DELETE] = PhysicalKey.delete; + result[sdl.SDL_SCANCODE_HOME] = PhysicalKey.home; + result[sdl.SDL_SCANCODE_END] = PhysicalKey.end; + result[sdl.SDL_SCANCODE_PAGEUP] = PhysicalKey.page_up; + result[sdl.SDL_SCANCODE_PAGEDOWN] = PhysicalKey.page_down; + result[sdl.SDL_SCANCODE_UP] = PhysicalKey.up; + result[sdl.SDL_SCANCODE_DOWN] = PhysicalKey.down; + result[sdl.SDL_SCANCODE_LEFT] = PhysicalKey.left; + result[sdl.SDL_SCANCODE_RIGHT] = PhysicalKey.right; + + result[sdl.SDL_SCANCODE_LCTRL] = PhysicalKey.ctrl; + result[sdl.SDL_SCANCODE_RCTRL] = PhysicalKey.ctrl; + result[sdl.SDL_SCANCODE_LALT] = PhysicalKey.alt; + result[sdl.SDL_SCANCODE_RALT] = PhysicalKey.alt; + result[sdl.SDL_SCANCODE_LSHIFT] = PhysicalKey.shift; + result[sdl.SDL_SCANCODE_RSHIFT] = PhysicalKey.shift; + result[sdl.SDL_SCANCODE_CAPSLOCK] = PhysicalKey.caps; + + break :blk result; +}; diff --git a/src/main.zig b/src/main.zig new file mode 100644 index 0000000..bee6a19 --- /dev/null +++ b/src/main.zig @@ -0,0 +1,92 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const sdl = @import("sdl"); +const Window = @import("./window.zig"); +const makeSdlError = Window.makeSdlError; + +const Canvas = @import("./canvas.zig"); +const Pixel = Canvas.Pixel; +const Sprite = Canvas.Sprite; + +const Grid = @import("./grid.zig"); + +pub fn main() !void { + var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + const allocator = gpa.allocator(); + var prng = std.Random.DefaultPrng.init(0); + const random = prng.random(); + + var grid = Grid{}; + const key_layer = grid.layerAs(0, u8); + + var sprite_lookup: [256]Sprite = undefined; + try loadSprites(&sprite_lookup); + + const size = Grid.size * Grid.size; + + const scale = 3; + const window = try Window.init(allocator, size * scale, size * scale, key_layer); + defer window.deinit(); + + const canvas = try Canvas.init(allocator, size, size); + defer canvas.deinit(); + + while (window.running) { + window.pollEvents(); + + grid.get(Grid.Pos.init(-5, -4, 0)).as(u8).* +%= 1; + grid.get(Grid.Pos.init(-4, -4, 0)).as(u8).* = random.int(u8); + + render(&grid, canvas, &sprite_lookup); + try window.updateSurface(canvas); + } +} + +fn render(grid: *Grid, canvas: *Canvas, sprite_lookup: *[256]Sprite) void { + canvas.clear(Pixel.black); + + for (0..Grid.size) |vx| + for (0..Grid.size) |vy| + for (0..Grid.size) |vz| { + const x = @as(isize, @intCast(vx)) - Grid.size / 2; + const y = @as(isize, @intCast(vy)) - Grid.size / 2; + const z = @as(isize, @intCast(vz)) - Grid.size / 2; + + const pos = Grid.Pos.initTruncate(x, y, z); + const value = grid.get(pos).as(u8).*; + + const sprite = &sprite_lookup[value]; + canvas.putSprite(vx * Grid.size, vy * Grid.size, sprite); + }; +} + +fn loadSprites(sprite_lookup: *[256]Sprite) !void { + const png_surface = sdl.IMG_Load("./gfx/sprite_map.png") orelse + return makeSdlError(); + defer sdl.SDL_FreeSurface(png_surface); + + std.debug.assert(png_surface.w == 256); + std.debug.assert(png_surface.h == 256); + + const pixels_surface = sdl.SDL_ConvertSurfaceFormat( + png_surface, + sdl.SDL_PIXELFORMAT_ABGR8888, + 0, + ) orelse + return makeSdlError(); + defer sdl.SDL_FreeSurface(pixels_surface); + + const pixels_ptr: [*]Pixel = @alignCast(@ptrCast(pixels_surface.pixels)); + const pixels_len: usize = @intCast(pixels_surface.w * pixels_surface.h); + const pixels = pixels_ptr[0..pixels_len]; + + for (sprite_lookup, 0..) |*sprite, i| { + const x = i % 16; + const y = i / 16; + for (0..16) |yo| { + const dst_index = x * 16 + (y * 16 + yo) * 256; + @memcpy(&sprite[yo], pixels[dst_index..][0..16]); + } + } +} diff --git a/src/window.zig b/src/window.zig new file mode 100644 index 0000000..6c10a77 --- /dev/null +++ b/src/window.zig @@ -0,0 +1,105 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const sdl = @import("sdl"); +const keys = @import("./keys.zig"); +const PhysicalKey = keys.PhysicalKey; +const Canvas = @import("./canvas.zig"); + +const Window = @This(); + +allocator: Allocator, + +handle: *sdl.SDL_Window, +surface: ?*sdl.SDL_Surface = null, +key_status: []u8, +running: bool = true, + +pub fn init(allocator: Allocator, width: usize, height: usize, key_status: []u8) !*Window { + if (sdl.SDL_Init(sdl.SDL_INIT_AUDIO | sdl.SDL_INIT_VIDEO | sdl.SDL_INIT_EVENTS) < 0) + return makeSdlError(); + + const window = sdl.SDL_CreateWindow( + "Gridstep", + sdl.SDL_WINDOWPOS_UNDEFINED, + sdl.SDL_WINDOWPOS_UNDEFINED, + @intCast(width), + @intCast(height), + sdl.SDL_WINDOW_SHOWN | sdl.SDL_WINDOW_RESIZABLE, + ) orelse + return makeSdlError(); + + const result = try allocator.create(Window); + result.* = .{ + .allocator = allocator, + .handle = window, + .key_status = key_status, + }; + return result; +} + +pub fn deinit(self: *Window) void { + sdl.SDL_DestroyWindow(self.handle); + sdl.SDL_Quit(); + self.allocator.destroy(self); +} + +pub fn pollEvents(self: *Window) void { + { + // Special handling for the `.caps` key. + const i = @intFromEnum(PhysicalKey.caps); + const on = (sdl.SDL_GetModState() & sdl.KMOD_CAPS) != 0; + self.key_status[i] = if (on) i else 0; + } + + var ev = std.mem.zeroes(sdl.SDL_Event); + while (sdl.SDL_PollEvent(&ev) != 0) { + switch (ev.type) { + sdl.SDL_QUIT => { + self.running = false; + }, + sdl.SDL_WINDOWEVENT => { + switch (ev.window.event) { + sdl.SDL_WINDOWEVENT_RESIZED => { + // Invalidate surface when window is resized. + self.surface = null; + }, + else => {}, + } + }, + sdl.SDL_KEYDOWN, sdl.SDL_KEYUP => { + const key = ev.key.keysym; + const down = (ev.type == sdl.SDL_KEYDOWN); + if (keys.lookup[key.scancode]) |k| { + if (k == .caps) break; + const i = @intFromEnum(k); + self.key_status[i] = if (down) i else 0; + } + }, + else => {}, + } + } +} + +pub fn updateSurface(self: *Window, canvas: *Canvas) !void { + // If surface has not yet been created or invalidated due + // to window being resized, reaquire the window's surface. + if (self.surface == null) + self.surface = sdl.SDL_GetWindowSurface(self.handle) orelse + return makeSdlError(); + + const w: c_int = @intCast(canvas.width); + const h: c_int = @intCast(canvas.height); + const src_rect = sdl.SDL_Rect{ .x = 0, .y = 0, .w = w, .h = h }; + var dest_rect = sdl.SDL_Rect{ .x = 0, .y = 0, .w = self.surface.?.w, .h = self.surface.?.h }; + if (sdl.SDL_BlitScaled(canvas.surface, &src_rect, self.surface, &dest_rect) < 0) + return makeSdlError(); + + if (sdl.SDL_UpdateWindowSurface(self.handle) < 0) + return makeSdlError(); +} + +pub fn makeSdlError() error{SdlError} { + std.debug.print("{s}\n", .{sdl.SDL_GetError()}); + return error.SdlError; +}