const mfb = @import("minifb"); const std = @import("std"); const min = std.math.min; const max = std.math.max; const buffer_width = 800; const buffer_height = 600; const aspect_ratio = @intToFloat(comptime_float, buffer_width) / @intToFloat(comptime_float, buffer_height); const cursor_width = 25; fn grey(value: u8) mfb.Rgb { return mfb.Rgb{ .r = value, .g = value, .b = value }; } const Coords = struct { x: i32, y: i32, }; const FCoords = struct { x: f32, y: f32, pub fn toCoords(self: FCoords) Coords { return Coords{ .x = @floatToInt(i32, self.x), .y = @floatToInt(i32, self.y), }; } }; const State = struct { period: f64, timer: mfb.Timer, active: bool = true, alloc: std.mem.Allocator, cursor_pos: Coords = undefined, rectangle_pos: FCoords = FCoords{ .x = @intToFloat(f32, buffer_width) / 2.0, .y = @intToFloat(f32, buffer_height) / 2.0, }, rectangle_width: f32 = @intToFloat(f32, buffer_width) / 3.0, rectangle_height: f32 = @intToFloat(f32, buffer_height) / 3.0, paused: bool = false, cheat_code: []const u8 = "rave", cheat_code_progress: usize = 0, pub fn init(alloc: std.mem.Allocator, period: f64) std.mem.Allocator.Error!State { return State{ .period = period, .timer = try mfb.Timer.init(), .alloc = alloc, }; } pub fn deinit(self: State) void { self.timer.deinit(); } pub fn cheatInput(self: *State, char: u32) void { if (self.isCheating()) { return; } if (char == self.cheat_code[self.cheat_code_progress]) { self.cheat_code_progress += 1; } else { self.cheat_code_progress = 0; } } fn isCheating(self: State) bool { return self.cheat_code_progress == self.cheat_code.len; } pub fn render(self: State, buf: Buffer) void { const period = if (!self.isCheating()) self.period else self.period / 10; var bgValue = @rem(self.timer.now(), period) / period * 512; if (bgValue >= 256) { bgValue = 512 - bgValue; } const rectColor = if (self.active) mfb.Rgb{ .r = 0, .g = 255, .b = 0 } else mfb.Rgb{ .r = 255, .g = 0, .b = 0 }; buf.drawRectangle(.{ .x = 0, .y = 0 }, .{ .x = buffer_width, .y = buffer_height }, grey(@floatToInt(u8, bgValue))); buf.drawRectangle( (FCoords{ .x = self.rectangle_pos.x - self.rectangle_width / 2, .y = self.rectangle_pos.y - self.rectangle_height / 2, }).toCoords(), (FCoords{ .x = self.rectangle_pos.x + self.rectangle_width / 2, .y = self.rectangle_pos.y + self.rectangle_height / 2, }).toCoords(), rectColor, ); // Draw cursor buf.drawRectangle( .{ .x = self.cursor_pos.x - cursor_width / 2, .y = self.cursor_pos.y - cursor_width / 2 }, .{ .x = self.cursor_pos.x + cursor_width / 2, .y = self.cursor_pos.y + cursor_width / 2 }, mfb.Rgb{ .r = 61, .g = 97, .b = 170 }, ); } }; const Buffer = struct { alloc: std.mem.Allocator, slice: []mfb.Rgb, width: u32, height: u32, pub fn init(alloc: std.mem.Allocator, width: u32, height: u32) std.mem.Allocator.Error!Buffer { const slice = try alloc.alloc(mfb.Rgb, width * height); return Buffer{ .alloc = alloc, .slice = slice, .width = width, .height = height, }; } pub fn deinit(self: Buffer) void { self.alloc.free(self.slice); } pub fn setPixel(self: Buffer, coords: Coords, color: mfb.Rgb) void { const x = @intCast(u32, coords.x); const y = @intCast(u32, coords.y); self.slice[y * self.width + x] = color; } pub fn drawRectangle(self: Buffer, a: Coords, b: Coords, color: mfb.Rgb) void { var start = Coords{ .x = max(0, min(a.x, b.x)), .y = max(0, min(a.y, b.y)), }; var end = Coords{ .x = min(@intCast(i32, self.width), max(a.x, b.x)), .y = min(@intCast(i32, self.height), max(a.y, b.y)), }; var y = start.y; while (y < end.y) { var x = start.x; while (x < end.x) { self.setPixel(.{ .x = x, .y = y }, color); x += 1; } y += 1; } } }; fn handleActive(win: mfb.Window(State), is_active: bool) callconv(.C) void { win.getUserData().?.*.active = is_active; } fn handleResize(win: mfb.Window(State), width: i32, height: i32) callconv(.C) void { const win_aspect_ratio = @intToFloat(f32, width) / @intToFloat(f32, height); var vp_height: i32 = height; var vp_width: i32 = width; var vp_offset_x: i32 = 0; var vp_offset_y: i32 = 0; if (win_aspect_ratio > aspect_ratio) { vp_width = @floatToInt(i32, @intToFloat(f32, height) * aspect_ratio); vp_offset_x = @divTrunc(width - vp_width, 2); } else { vp_height = @floatToInt(i32, @intToFloat(f32, width) / aspect_ratio); vp_offset_y = @divTrunc(height - vp_height, 2); } const did_set = win.setViewport( @intCast(u32, vp_offset_x), @intCast(u32, vp_offset_y), @intCast(u32, vp_width), @intCast(u32, vp_height), ); if (!did_set) { std.log.warn("did not set viewport!", .{}); } } fn handleMouseButton(win: mfb.Window(State), mouse_button: mfb.MouseButton, key_mod: mfb.KeyMod, is_pressed: bool) callconv(.C) void { const up_down: []const u8 = if (is_pressed) "down" else "up"; std.log.info("{any} mouse{d} {s}", .{ key_mod, @enumToInt(mouse_button), up_down }); var buf_string = [_]u8{'.', ' ', '.', ' ', '.', ' ', '.', ' ', '.', ' ', '.', ' ', '.', ' ', '.'}; for (win.getMouseButtonBuffer().*) |button, i| { if (button) { buf_string[i * 2] = '*'; } } std.log.info("Mouse button buffer: {s}", .{&buf_string}); } fn handleMouseMove(win: mfb.Window(State), x: i32, y: i32) callconv(.C) void { win.getUserData().?.*.cursor_pos = .{ .x = x, .y = y }; } fn handleMouseScroll(win: mfb.Window(State), key_mod: mfb.KeyMod, delta_x: f32, delta_y: f32) callconv(.C) void { _ = delta_x; const scroll_speed = 3.5; var state = win.getUserData().?; std.log.info("scroll dx={d} dy={d}", .{ delta_x, delta_y }); if (key_mod.control) { if (key_mod.shift) { state.rectangle_width += delta_y * scroll_speed; } else { state.rectangle_height += delta_y * scroll_speed; } } else { if (key_mod.shift) { state.rectangle_pos.x += delta_y * scroll_speed; } else { state.rectangle_pos.y -= delta_y * scroll_speed; } } } fn handleKeyboard(win: mfb.Window(State), key: mfb.Key, _: mfb.KeyMod, is_pressed: bool) callconv(.C) void { if (key == mfb.Key.escape and is_pressed) { win.close(); } else if (key == mfb.Key.space and is_pressed) { var state = win.getUserData().?; state.*.paused = !state.*.paused; } const out = std.io.getStdOut().writer(); var has_written = false; for (win.getKeyBuffer()) |buf_key, i| { if (buf_key) { if (!has_written) { _ = out.write("Keyboard buffer:") catch unreachable; } std.fmt.format(out, " {}", .{@intToEnum(mfb.Key, i)}) catch unreachable; has_written = true; } } if (has_written) { out.writeByte('\n') catch unreachable; } } fn handleCharInput(win: mfb.Window(State), char: u32) callconv(.C) void { win.getUserData().?.cheatInput(char); } pub fn main() !void { var gp_allocator = std.heap.GeneralPurposeAllocator(.{}){}; const alloc = gp_allocator.allocator(); var state = try State.init(alloc, 3); defer state.deinit(); var win = mfb.Window(State).open("Hello minifb-zig", buffer_width, buffer_height, .{ .resizable = true, .always_on_top = true }) catch unreachable; mfb.setTargetFPS(30); win.setUserData(&state); const scale = win.getMonitorScale(); try std.io.getStdOut().writer().print( \\Monitor scale: {d} * {d} \\ \\Press spacebar to pause, escape to quit. \\Try [Ctrl+][Shift+]scroll wheel to change the colored rectangle. \\Type "{s}" to get crazy. \\ , .{ scale.x, scale.y, state.cheat_code }); win.setActiveCallback(handleActive); win.setResizeCallback(handleResize); win.setMouseButtonCallback(handleMouseButton); win.setMouseMoveCallback(handleMouseMove); win.setMouseScrollCallback(handleMouseScroll); win.setKeyboardCallback(handleKeyboard); win.setCharInputCallback(handleCharInput); var buf = try Buffer.init(alloc, buffer_width, buffer_height); defer buf.deinit(); state.render(buf); while (win.waitSync()) { state.render(buf); // TODO Handle mfb state if (state.paused) { _ = win.updateEvents() catch unreachable; } else { _ = win.update(buf.slice) catch unreachable; } } }