diff --git a/build.zig b/build.zig index 3337402..e65327c 100644 --- a/build.zig +++ b/build.zig @@ -1,9 +1,13 @@ const std = @import("std"); +const sokol = @import("sokol"); -pub fn build(b: *std.Build) void { +pub fn build(b: *std.Build) !void { const target = b.standardTargetOptions(.{}); const optimize = b.standardOptimizeOption(.{}); + const has_imgui = b.option(bool, "imgui", "ImGui integration") orelse (optimize == .Debug); + const has_tracy = b.option(bool, "tracy", "Tracy integration") orelse (optimize == .Debug); + const exe = b.addExecutable(.{ .name = "game_2025_12_13", .root_module = b.createModule(.{ @@ -12,6 +16,70 @@ pub fn build(b: *std.Build) void { .optimize = optimize, }), }); + const exe_mod = exe.root_module; + + const tracy_dependency = b.dependency("tracy", .{ + .target = target, + .optimize = optimize, + .tracy_enable = has_tracy, + .tracy_only_localhost = true + }); + exe_mod.linkLibrary(tracy_dependency.artifact("tracy")); + exe_mod.addImport("tracy", tracy_dependency.module("tracy")); + + const stb_dependency = b.dependency("stb", .{}); + exe_mod.addIncludePath(stb_dependency.path(".")); + + const sokol_c_dependency = b.dependency("sokol_c", .{}); + exe_mod.addIncludePath(sokol_c_dependency.path("util")); + + const fontstash_dependency = b.dependency("fontstash", .{}); + exe_mod.addIncludePath(fontstash_dependency.path("src")); + + const sokol_dependency = b.dependency("sokol", .{ + .target = target, + .optimize = optimize, + .with_sokol_imgui = has_imgui + }); + + exe_mod.addImport("sokol", sokol_dependency.module("sokol")); + exe_mod.linkLibrary(sokol_dependency.artifact("sokol_clib")); + + var cflags_buffer: [64][]const u8 = undefined; + var cflags = std.ArrayListUnmanaged([]const u8).initBuffer(&cflags_buffer); + switch (sokol.resolveSokolBackend(.auto, target.result)) { + .d3d11 => try cflags.appendBounded("-DSOKOL_D3D11"), + .metal => try cflags.appendBounded("-DSOKOL_METAL"), + .gl => try cflags.appendBounded("-DSOKOL_GLCORE"), + .gles3 => try cflags.appendBounded("-DSOKOL_GLES3"), + .wgpu => try cflags.appendBounded("-DSOKOL_WGPU"), + else => @panic("unknown sokol backend"), + } + exe_mod.addIncludePath(b.path("src/libs")); + exe_mod.addCSourceFile(.{ + .file = b.path("src/libs/sokol_fontstash_impl.c"), + .flags = cflags.items + }); + + exe_mod.addCSourceFile(.{ + .file = b.path("src/libs/stb_image.c"), + .flags = &.{} + }); + + if (has_imgui) { + if (b.lazyDependency("cimgui", .{ + .target = target, + .optimize = optimize, + })) |cimgui_dependency| { + sokol_dependency.artifact("sokol_clib").addIncludePath(cimgui_dependency.path("src")); + exe_mod.addImport("cimgui", cimgui_dependency.module("cimgui")); + } + } + + var options = b.addOptions(); + options.addOption(bool, "has_imgui", has_imgui); + options.addOption(bool, "has_tracy", has_tracy); + exe_mod.addOptions("options", options); b.installArtifact(exe); diff --git a/build.zig.zon b/build.zig.zon index 71b16d9..81b3adb 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -1,81 +1,38 @@ .{ - // This is the default name used by packages depending on this one. For - // example, when a user runs `zig fetch --save `, this field is used - // as the key in the `dependencies` table. Although the user can choose a - // different name, most users will stick with this provided value. - // - // It is redundant to include "zig" in this name because it is already - // within the Zig package namespace. .name = .game_2025_12_13, - // This is a [Semantic Version](https://semver.org/). - // In a future version of Zig it will be used for package deduplication. .version = "0.0.0", - // Together with name, this represents a globally unique package - // identifier. This field is generated by the Zig toolchain when the - // package is first created, and then *never changes*. This allows - // unambiguous detection of one package being an updated version of - // another. - // - // When forking a Zig project, this id should be regenerated (delete the - // field and run `zig build`) if the upstream project is still maintained. - // Otherwise, the fork is *hostile*, attempting to take control over the - // original project's identity. Thus it is recommended to leave the comment - // on the following line intact, so that it shows up in code reviews that - // modify the field. - .fingerprint = 0x5704f7ae3ffdd7f8, // Changing this has security and trust implications. - // Tracks the earliest Zig version that the package considers to be a - // supported use case. + .fingerprint = 0x5704f7ae3ffdd7f8, .minimum_zig_version = "0.15.2", - // This field is optional. - // Each dependency must either provide a `url` and `hash`, or a `path`. - // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. - // Once all dependencies are fetched, `zig build` no longer requires - // internet connectivity. .dependencies = .{ - // See `zig fetch --save ` for a command-line interface for adding dependencies. - //.example = .{ - // // When updating this field to a new URL, be sure to delete the corresponding - // // `hash`, otherwise you are communicating that you expect to find the old hash at - // // the new URL. If the contents of a URL change this will result in a hash mismatch - // // which will prevent zig from using it. - // .url = "https://example.com/foo.tar.gz", - // - // // This is computed from the file contents of the directory of files that is - // // obtained after fetching `url` and applying the inclusion rules given by - // // `paths`. - // // - // // This field is the source of truth; packages do not come from a `url`; they - // // come from a `hash`. `url` is just one of many possible mirrors for how to - // // obtain a package matching this `hash`. - // // - // // Uses the [multihash](https://multiformats.io/multihash/) format. - // .hash = "...", - // - // // When this is provided, the package is found in a directory relative to the - // // build root. In this case the package's hash is irrelevant and therefore not - // // computed. This field and `url` are mutually exclusive. - // .path = "foo", - // - // // When this is set to `true`, a package is declared to be lazily - // // fetched. This makes the dependency only get fetched if it is - // // actually used. - // .lazy = false, - //}, + .sokol = .{ + .url = "git+https://github.com/floooh/sokol-zig.git#1e233203b41893a8bf9c1c91933eba98204b6ed8", + .hash = "sokol-0.1.0-pb1HK42FNgDb5sqnsadiO2qabkfUX8jXP_DheOZGcD1W", + }, + .cimgui = .{ + .url = "git+https://github.com/floooh/dcimgui.git#33c99ef426b68030412b5a4b11487a23da9d4f13", + .hash = "cimgui-0.1.0-44ClkQRJlABdFMKRqIG8KDD6jy1eQbgPO335NziPYjmL", + .lazy = true, + }, + .tracy = .{ + .url = "git+https://github.com/sagehane/zig-tracy.git#80933723efe9bf840fe749b0bfc0d610f1db1669", + .hash = "zig_tracy-0.0.5-aOIqsX1tAACKaRRB-sraMLuNiMASXi_y-4FtRuw4cTpx", + }, + .stb = .{ + .url = "git+https://github.com/nothings/stb.git#f1c79c02822848a9bed4315b12c8c8f3761e1296", + .hash = "N-V-__8AABQ7TgCnPlp8MP4YA8znrjd6E-ZjpF1rvrS8J_2I", + }, + .sokol_c = .{ + .url = "git+https://github.com/floooh/sokol.git#c66a1f04e6495d635c5e913335ab2308281e0492", + .hash = "N-V-__8AAC3eYABB1DVLb4dkcEzq_xVeEZZugVfQ6DoNQBDN", + }, + .fontstash = .{ + .url = "git+https://github.com/memononen/fontstash.git#b5ddc9741061343740d85d636d782ed3e07cf7be", + .hash = "N-V-__8AAA9xHgAxdLYPmlNTy6qzv9IYqiIePEHQUOPWYQ_6", + }, }, - // Specifies the set of files and directories that are included in this package. - // Only files and directories listed here are included in the `hash` that - // is computed for this package. Only files listed here will remain on disk - // when using the zig package manager. As a rule of thumb, one should list - // files required for compilation plus any license(s). - // Paths are relative to the build root. Use the empty string (`""`) to refer to - // the build root itself. - // A directory listed here means that all files within, recursively, are included. .paths = .{ "build.zig", "build.zig.zon", "src", - // For example... - //"LICENSE", - //"README.md", }, } diff --git a/src/assets/roboto-font/LICENSE.txt b/src/assets/roboto-font/LICENSE.txt new file mode 100644 index 0000000..9c48e05 --- /dev/null +++ b/src/assets/roboto-font/LICENSE.txt @@ -0,0 +1,93 @@ +Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +https://openfontlicense.org + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/src/assets/roboto-font/Roboto-Bold.ttf b/src/assets/roboto-font/Roboto-Bold.ttf new file mode 100644 index 0000000..9d7cf22 Binary files /dev/null and b/src/assets/roboto-font/Roboto-Bold.ttf differ diff --git a/src/assets/roboto-font/Roboto-Italic.ttf b/src/assets/roboto-font/Roboto-Italic.ttf new file mode 100644 index 0000000..c3abaef Binary files /dev/null and b/src/assets/roboto-font/Roboto-Italic.ttf differ diff --git a/src/assets/roboto-font/Roboto-Regular.ttf b/src/assets/roboto-font/Roboto-Regular.ttf new file mode 100644 index 0000000..7e3bb2f Binary files /dev/null and b/src/assets/roboto-font/Roboto-Regular.ttf differ diff --git a/src/graphics.zig b/src/graphics.zig new file mode 100644 index 0000000..76e32d1 --- /dev/null +++ b/src/graphics.zig @@ -0,0 +1,1375 @@ +const tracy = @import("tracy"); +const sokol = @import("sokol"); +const sg = sokol.gfx; +const sglue = sokol.glue; +const slog = sokol.log; +const sapp = sokol.app; +const simgui = sokol.imgui; +const sgl = sokol.gl; +const imgui = @import("imgui.zig"); +const std = @import("std"); +const assert = std.debug.assert; + +const c = @cImport({ + @cInclude("sokol/sokol_gfx.h"); + @cInclude("sokol/sokol_gl.h"); + @cInclude("fontstash.h"); + @cInclude("sokol_fontstash.h"); + @cInclude("stb_image.h"); +}); + +const Allocator = std.mem.Allocator; + +// TODO: Seems that there is a vertical jitter bug when resizing a window in OpenGL. Seems like a driver bug. +// From other peoples research it seems that disabling vsync when a resize event occurs fixes it. +// Maybe a patch for sokol could be made? +// More info: +// * https://github.com/libsdl-org/SDL/issues/11618 +// * https://github.com/nimgl/nimgl/issues/59 + +const Math = @import("./math.zig"); +const Vec2 = Math.Vec2; +const Vec3 = Math.Vec3; +const Vec4 = Math.Vec4; +const Mat4 = Math.Mat4; +const Rect = Math.Rect; +const rgb = Math.rgb; +const hex = Math.rgb_hex; +const rgba = Math.rgba; +const log = std.log.scoped(.graphics); + +pub const white = rgb(255, 255, 255); +pub const black = rgb(0, 0, 0); + +// Struct definition copies from fonstash.h +// TODO: Create a getter, to avoid copying this struct by hand +const FONSstate = extern struct { + font: c_int, + @"align": c_int, + size: f32, + color: c_uint, + blur: f32, + spacing: f32 +}; + +const FONSfont = opaque{}; +const FONSglyph = opaque{}; + +extern fn zig_isTopLeft(stash: ?*c.FONScontext) bool; +extern fn zig_getGlyphIndex(glyph: ?*FONSglyph) c_int; +extern fn zig_fons__getState(stash: ?*c.struct_FONScontext) ?*FONSstate; +extern fn zig_getFont(stash: ?*c.FONScontext, index: c_int) ?*FONSfont; +extern fn zig_fons__tt_getPixelHeightScale(font: ?*FONSfont, size: f32) f32; +extern fn zig_fons__getVertAlign(stash: ?*c.FONScontext, font: ?*FONSfont, @"align": c_int, isize: i16) f32; +extern fn zig_fons__getGlyph( + stash: ?*c.FONScontext, + font: ?*FONSfont, + codepoint: c_uint, + @"isize": i16, + iblur: i16 +) ?*FONSglyph; + +extern fn zig_fons__getQuad( + stash: ?*c.FONScontext, + font: ?*FONSfont, + prevGlyphIndex: c_int, glyph: ?*FONSglyph, + scale: f32, + spacing: f32, + x: ?*f32, y: ?*f32, + q: ?*c.FONSquad +) void; + +const Vertex = extern struct { + position: Vec2, + color: Vec4, +}; + +const Quad = [4]Vertex; + +const DrawFrame = struct { + screen_size: Vec2 = Vec2.zero, + bg_color: Vec4 = rgb(0, 0, 0), + + scissor_stack_buffer: [32]Rect = undefined, + scissor_stack: std.ArrayListUnmanaged(Rect) = .empty, + + fn init(self: *DrawFrame) void { + self.* = DrawFrame{ + .scissor_stack = .initBuffer(&self.scissor_stack_buffer) + }; + } +}; + +pub const FontVariant = enum { + regular, + italic, + bold +}; + +pub const Font = struct { + pub const AlignX = enum { + left, + right, + center, + + fn toFONSAlignment(self: AlignX) c_int { + return switch (self) { + .left => c.FONS_ALIGN_LEFT, + .center => c.FONS_ALIGN_CENTER, + .right => c.FONS_ALIGN_RIGHT, + }; + } + }; + + pub const AlignY = enum { + top, + middle, + bottom, + baseline, + + fn toFONSAlignment(self: AlignY) c_int { + return switch (self) { + .top => c.FONS_ALIGN_TOP, + .middle => c.FONS_ALIGN_MIDDLE, + .bottom => c.FONS_ALIGN_BOTTOM, + .baseline => c.FONS_ALIGN_BASELINE, + }; + } + }; + + pub const QuadIterator = struct { + x: f32, + y: f32, + next_x: f32, + next_y: f32, + spacing: f32, + scale: f32, + isize: c_short, + iblur: c_short, + prev_glyph_index: c_int, + font: *FONSfont, + + pub fn init(font: Font) QuadIterator { + const stash = g_fons_context; + font.setAsCurrent(); + + const x: f32 = 0; + var y: f32 = 0; + + const state = zig_fons__getState(stash).?; + const fons_font = zig_getFont(stash, state.font) orelse @panic("Invalid font"); + + const font_isize: c_short = @intFromFloat(@trunc(state.size * 10.0)); + const font_iblur: c_short = @intFromFloat(@trunc(state.blur)); + const scale = zig_fons__tt_getPixelHeightScale(fons_font, @as(f32, @floatFromInt(font_isize)) / 10.0); + + assert(state.@"align" & c.FONS_ALIGN_LEFT != 0); + assert(state.@"align" & c.FONS_ALIGN_RIGHT == 0); + assert(state.@"align" & c.FONS_ALIGN_CENTER == 0); + + // Align vertically. + y += zig_fons__getVertAlign(stash, fons_font, @truncate(state.@"align"), font_isize); + + return QuadIterator{ + .x = x, + .y = y, + .next_x = x, + .next_y = y, + .isize = font_isize, + .iblur = font_iblur, + .scale = scale, + .spacing = state.spacing, + .font = fons_font, + .prev_glyph_index = -1 + }; + } + + pub fn next(self: *QuadIterator, codepoint: u21) ?Rect { + const stash = g_fons_context; + + self.x = self.next_x; + self.y = self.next_y; + + const glyph = zig_fons__getGlyph(stash, self.font, codepoint, self.isize, self.iblur); + if (glyph != null) { + var q: c.FONSquad = .{}; + zig_fons__getQuad(stash, self.font, self.prev_glyph_index, glyph, self.scale, self.spacing, &self.x, &self.y, &q); + self.prev_glyph_index = zig_getGlyphIndex(glyph); + return Rect.init(q.x0, q.y0, q.x1 - q.x0, q.y1 - q.y0); + } else { + self.prev_glyph_index = -1; + return null; + } + } + + pub const Ascii = struct { + iter: QuadIterator, + text: []const u8, + index: usize, + + pub fn init(font: Font, text: []const u8) Ascii { + return Ascii{ + .iter = .init(font), + .text = text, + .index = 0 + }; + } + + pub fn next(self: *Ascii) ?Rect { + while (self.index < self.text.len) { + const codepoint_len = std.unicode.utf8ByteSequenceLength(self.text[self.index]) catch { + self.index += 1; + continue; + }; + + if (self.index + codepoint_len > self.text.len) { + self.index = self.text.len; + return null; + } + + const codepoint = std.unicode.utf8Decode(self.text[self.index..][0..codepoint_len]) catch { + self.index += 1; + continue; + }; + self.index += codepoint_len; + + if (self.iter.next(codepoint)) |rect| { + return rect; + } + } + + return null; + } + + pub fn last(self: *Ascii) ?Rect { + var last_rect: ?Rect = null; + while (self.next()) |rect| { + last_rect = rect; + } + return last_rect; + } + }; + + pub const Unicode = struct { + iter: QuadIterator, + text: []const u21, + index: usize, + + pub fn init(font: Font, text: []const u21) Unicode { + return Unicode{ + .iter = .init(font), + .text = text, + .index = 0 + }; + } + + pub fn next(self: *Unicode) ?Rect { + while (self.index < self.text.len) { + const next_rect = self.iter.next(self.text[self.index]); + self.index += 1; + + if (next_rect) |rect| { + return rect; + } + } + + return null; + } + + pub fn last(self: *Unicode) ?Rect { + var last_rect: ?Rect = null; + while (self.next()) |rect| { + last_rect = rect; + } + return last_rect; + } + }; + }; + + // TODO: Maybe this should also have a `color: Vec4` field? + variant: FontVariant, + size: f32, + spacing: f32 = 0, + line_spacing: f32 = 1, + + pub const default = Font{ + .variant = .regular, + .size = 16, + }; + + pub fn setAsCurrent(self: Font) void { + const fs = g_fons_context; + + c.fonsSetFont(fs, font_id_map.get(self.variant)); + c.fonsSetSize(fs, self.size * dpi_scale); + c.fonsSetAlign(fs, Font.AlignX.left.toFONSAlignment() | Font.AlignY.top.toFONSAlignment()); + c.fonsSetSpacing(fs, self.spacing); + } + + pub fn drawText(self: Font, pos: Vec2, color: Vec4, text: []const u8) void { + const fs = g_fons_context; + + // TODO: Test if calling `.setAsCurrent()` isn't expensive + self.setAsCurrent(); + + const fons_color = c.sfons_rgba( + @intFromFloat(color.x * 255), + @intFromFloat(color.y * 255), + @intFromFloat(color.z * 255), + @intFromFloat(color.w * 255) + ); + c.fonsSetColor(fs, fons_color); + + _ = c.fonsDrawText( + fs, + pos.x, + pos.y, + text.ptr, + text.ptr + text.len + ); + } + + pub fn getBounds(self: Font, text: []const u8) Rect { + const fs = g_fons_context; + self.setAsCurrent(); + + var line_bounds: [4]f32 = undefined; + _ = c.fonsTextBounds(fs, 0, 0, text.ptr, text.ptr + text.len, &line_bounds); + + const min_x = line_bounds[0]; + const min_y = line_bounds[1]; + const max_x = line_bounds[2]; + const max_y = line_bounds[3]; + + const width = max_x - min_x; + const height = max_y - min_y; + + return Rect.init(min_x, min_y, width, height); + } + + // A reimplementation of `fonsTextBounds` which uses an array of already decoded codepoints + fn fonsTextBoundsUtf8(stash: ?*c.struct_FONScontext, initial_x: f32, initial_y: f32, str: []const u21, bounds: [*c]f32) f32 { + if (stash == null) { + return 0; + } + + var x = initial_x; + var y = initial_y; + + const state = zig_fons__getState(stash).?; + const font = zig_getFont(stash, state.font) orelse return 0; + + const font_isize: i16 = @intFromFloat(@trunc(state.size * 10.0)); + const font_iblur: i16 = @intFromFloat(@trunc(state.blur)); + + const scale = zig_fons__tt_getPixelHeightScale(font, @as(f32, @floatFromInt(font_isize)) / 10.0); + + // Align vertically. + y += zig_fons__getVertAlign(stash, font, @truncate(state.@"align"), font_isize); + + var minx = x; + var maxx = x; + var miny = y; + var maxy = y; + const startx = x; + + var prevGlyphIndex: c_int = -1; + for (str) |codepoint| { + const glyph = zig_fons__getGlyph(stash, font, codepoint, font_isize, font_iblur); + if (glyph != null) { + var q: c.FONSquad = .{}; + zig_fons__getQuad(stash, font, prevGlyphIndex, glyph, scale, state.spacing, &x, &y, &q); + if (q.x0 < minx) minx = q.x0; + if (q.x1 > maxx) maxx = q.x1; + if (zig_isTopLeft(stash)) { + if (q.y0 < miny) miny = q.y0; + if (q.y1 > maxy) maxy = q.y1; + } else { + if (q.y1 < miny) miny = q.y1; + if (q.y0 > maxy) maxy = q.y0; + } + + prevGlyphIndex = zig_getGlyphIndex(glyph); + } else { + prevGlyphIndex = -1; + } + } + + const advance = x - startx; + + // Align horizontally + if ((state.@"align" & c.FONS_ALIGN_LEFT) != 0) { + // empty + } else if ((state.@"align" & c.FONS_ALIGN_RIGHT) != 0) { + minx -= advance; + maxx -= advance; + } else if ((state.@"align" & c.FONS_ALIGN_CENTER) != 0) { + minx -= advance * 0.5; + maxx -= advance * 0.5; + } + + if (bounds != null) { + bounds[0] = minx; + bounds[1] = miny; + bounds[2] = maxx; + bounds[3] = maxy; + } + + return advance; + } + + pub fn getBoundsUtf8(self: Font, text: []const u21) Rect { + const fs = g_fons_context; + self.setAsCurrent(); + + var line_bounds: [4]f32 = undefined; + _ = fonsTextBoundsUtf8(fs, 0, 0, text, &line_bounds); + + const min_x = line_bounds[0]; + const min_y = line_bounds[1]; + const max_x = line_bounds[2]; + const max_y = line_bounds[3]; + + const width = max_x - min_x; + const height = max_y - min_y; + + return Rect.init(min_x, min_y, width, height); + } + + pub fn measure(self: Font, text: []const u8) Vec2 { + const bounds = self.getBounds(text); + return bounds.size; + } + + pub fn quadIter(self: Font, text: []const u8) QuadIterator.Ascii { + return QuadIterator.Ascii.init(self, text); + } + + pub fn quadUnicodeIter(self: Font, text: []const u21) QuadIterator.Unicode { + return QuadIterator.Unicode.init(self, text); + } +}; + +pub const ImageId = enum { + // package, + // trash +}; + +pub const Borders = struct { + size: f32 = 0, + left: Vec4 = rgba(0, 0, 0, 0), + right: Vec4 = rgba(0, 0, 0, 0), + top: Vec4 = rgba(0, 0, 0, 0), + bottom: Vec4 = rgba(0, 0, 0, 0), + + pub fn initAll(size: f32, color: Vec4) Borders { + return Borders{ + .size = size, + .left = color, + .right = color, + .top = color, + .bottom = color, + }; + } +}; + +pub const Corners = struct { + top_left: f32 = 0, + top_right: f32 = 0, + bottom_left: f32 = 0, + bottom_right: f32 = 0, + + pub fn initAll(size: f32) Corners { + return Corners{ + .top_left = size, + .top_right = size, + .bottom_left = size, + .bottom_right = size, + }; + } +}; + +pub const DrawTextContext = struct { + pub const Line = struct { + // Assumes that the text won't be invalidted between `queue*` and `draw` functions + text: []const u8, + pos: Vec2, + bounds: Rect, + }; + + // TODO: Add support for multiple font in a single context. + // I.e. each command should be able to have a separate font + font: Font, + + line_height: f32 = 0, + pos: Vec2 = Vec2.zero, + lines: std.ArrayListUnmanaged(Line) = .empty, + bounds: Rect = Rect.zero, + + pub fn init(font: Font) DrawTextContext { + var self = DrawTextContext{ + .font = font + }; + + const fs = g_fons_context; + self.font.setAsCurrent(); + c.fonsVertMetrics(fs, null, null, &self.line_height); + + return self; + } + + pub fn deinit(self: *DrawTextContext, allocator: Allocator) void { + self.lines.deinit(allocator); + } + + pub fn queueText(self: *DrawTextContext, allocator: Allocator, text: []const u8) !void { + var line_iter = std.mem.splitScalar(u8, text, '\n'); + while (line_iter.next()) |line| { + try self.queueLine(allocator, line); + } + } + + pub fn measureText(self: *const DrawTextContext, text: []const u8) Rect { + const fs = g_fons_context; + self.font.setAsCurrent(); + + var min_x: f32 = 0; + var max_x: f32 = 0; + var min_y: f32 = 0; + var max_y: f32 = 0; + + var pos = Vec2.zero; + var line_iter = std.mem.splitScalar(u8, text, '\n'); + while (line_iter.next()) |line| { + var line_bounds: [4]f32 = undefined; + _ = c.fonsTextBounds(fs, pos.x, pos.y, line.ptr, line.ptr + line.len, &line_bounds); + min_x = @min(min_x, line_bounds[0]); + min_y = @min(min_y, line_bounds[1]); + max_x = @max(max_x, line_bounds[2]); + max_y = @max(max_y, line_bounds[3]); + + pos.y += self.line_height * self.font.line_spacing; + } + + return Rect.init(min_x, min_y, max_x - min_x, max_y - min_y); + } + + pub fn queueLine(self: *DrawTextContext, allocator: Allocator, line: []const u8) !void { + const fs = g_fons_context; + + self.font.setAsCurrent(); + + var line_bounds: [4]f32 = undefined; + _ = c.fonsTextBounds(fs, self.pos.x, self.pos.y, line.ptr, line.ptr + line.len, &line_bounds); + + try self.lines.append(allocator, .{ + .bounds = Rect.init( + line_bounds[0], + line_bounds[1], + line_bounds[2] - line_bounds[0], + line_bounds[3] - line_bounds[1], + ), + .pos = self.pos, + .text = line + }); + self.pos.y += self.line_height * self.font.line_spacing; + + var min_x = self.bounds.pos.x; + var max_x = self.bounds.pos.x + self.bounds.size.x; + var min_y = self.bounds.pos.y; + var max_y = self.bounds.pos.y + self.bounds.size.y; + + min_x = @min(min_x, line_bounds[0]); + min_y = @min(min_y, line_bounds[1]); + max_x = @max(max_x, line_bounds[2]); + max_y = @max(max_y, line_bounds[3]); + + self.bounds = Rect.init(min_x, min_y, max_x - min_x, max_y - min_y); + } + + pub fn draw(self: *DrawTextContext, pos: Vec2, color: Vec4) void { + for (self.lines.items) |cmd| { + self.font.drawText(pos.add(cmd.pos), color, cmd.text); + } + } + + pub fn isEmpty(self: *DrawTextContext) bool { + return self.lines.items.len == 0; + } + + // pub fn drawLine(self: *DrawTextContext, line: []const u8) void { + // const fs = fons_context; + // + // _ = c.fonsDrawText(fs, self.pos.x, self.pos.y, line.ptr, line.ptr + line.len); + // self.pos.y += self.line_height * self.font.line_spacing; + // } + // + // pub fn drawLines(self: *DrawTextContext, lines: []const []const u8) void { + // for (lines) |line| { + // self.drawLine(line); + // } + // } + // + // pub fn drawText(self: *DrawTextContext, text: []const u8) void { + // var line_iter = std.mem.splitScalar(u8, text, '\n'); + // while (line_iter.next()) |line| { + // self.drawLine(line); + // } + // } + // + // pub fn measureLine(self: *DrawTextContext, line: []const u8) void { + // const fs = fons_context; + // + // var min_x: f32 = 0; + // var min_y: f32 = 0; + // var max_x: f32 = 0; + // var max_y: f32 = 0; + // + // var line_iter = std.mem.splitScalar(u8, text, '\n'); + // var y: f32 = 0; + // while (line_iter.next()) |line| { + // var line_bounds: [4]f32 = undefined; + // + // _ = c.fonsTextBounds(fs, 0, y, line.ptr, line.ptr + line.len, &line_bounds); + // y += line_height * font.line_spacing; + // + // min_x = @min(min_x, line_bounds[0]); + // min_y = @min(min_y, line_bounds[1]); + // max_x = @max(max_x, line_bounds[2]); + // max_y = @max(max_y, line_bounds[3]); + // } + // + // return Rect.init(min_x, min_y, max_x - min_x, max_y - min_y); + // } + + pub fn end(self: *DrawTextContext) void { + _ = self; + } +}; + +// The lower the better. Best quality with 1 +pub var circle_quality: f32 = 6; + +pub var draw_frame: DrawFrame = undefined; +var dpi_scale: f32 = 1; +var g_fons_context: ?*c.struct_FONScontext = null; +var main_pipeline: sgl.Pipeline = .{}; + +var font_id_map: std.EnumArray(FontVariant, c_int) = .initFill(c.FONS_INVALID); + +var image_map: std.EnumArray(ImageId, sg.Image) = .initFill(.{}); +var image_view_map: std.EnumArray(ImageId, sg.View) = .initFill(.{}); +var linear_sampler: sg.Sampler = .{}; + +const Options = struct { + allocator: std.mem.Allocator, + logger: sg.Logger = .{}, +}; + +inline fn structCast(T: type, value: anytype) T { + return @as(*T, @ptrFromInt(@intFromPtr(&value))).*; +} + +fn loadEmbededFont(fs: ?*c.FONScontext, name: [*c]const u8, comptime path: []const u8) c_int { + const data = @embedFile(path); + + const font_id = c.fonsAddFontMem(fs, name, @constCast(data.ptr), data.len, 0); + assert(font_id != c.FONS_INVALID); + + return font_id; +} + +fn makeImageFromMemory(image_datas: []const []const u8) !sg.Image { + var stbi_images_buffer: [16][*c]u8 = undefined; + var stbi_images: std.ArrayListUnmanaged([*c]u8) = .initBuffer(&stbi_images_buffer); + defer { + for (stbi_images.items) |stbi_image| { + c.stbi_image_free(stbi_image); + } + } + + var mip_levels_buffer = [_]sg.Range{.{}} ** 16; + var mip_levels: std.ArrayListUnmanaged(sg.Range) = .initBuffer(&mip_levels_buffer); + + var image_width: c_int = -1; + var image_height: c_int = -1; + + for (image_datas) |image_data| { + var width: c_int = undefined; + var height: c_int = undefined; + const pixels = c.stbi_load_from_memory(image_data.ptr, @intCast(image_data.len), &width, &height, null, 4); + if (pixels == null) { + return error.InvalidPng; + } + + if (image_height == -1) { + image_width = width; + image_height = height; + } + + try stbi_images.appendBounded(pixels); + try mip_levels.appendBounded(.{ + .ptr = pixels, + .size = @intCast(width * height * 4) + }); + } + + assert(image_width > 0); + assert(image_height > 0); + + // TODO: Should error be checked? + const image = sg.makeImage(.{ + .width = image_width, + .height = image_height, + .pixel_format = .RGBA8, + .usage = .{ + .immutable = true + }, + .num_mipmaps = @intCast(mip_levels.items.len), + .data = sg.ImageData{ .mip_levels = mip_levels_buffer }, + }); + if (image.id == sg.invalid_id) { + return error.InvalidImage; + } + + return image; +} + +pub fn init(options: Options) !void { + dpi_scale = sapp.dpiScale(); + + draw_frame.init(); + + sg.setup(.{ + .logger = options.logger, + .environment = sglue.environment(), + }); + + sgl.setup(.{ + .logger = structCast(sgl.Logger, options.logger), + }); + + main_pipeline = sgl.makePipeline(.{ + .colors = init: { + var colors: [8]sg.ColorTargetState = @splat(.{}); + colors[0] = .{ + .blend = .{ + .enabled = true, + .src_factor_rgb = .SRC_ALPHA, + .dst_factor_rgb = .ONE_MINUS_SRC_ALPHA, + .op_rgb = .ADD, + .src_factor_alpha = .ONE, + .dst_factor_alpha = .ONE_MINUS_SRC_ALPHA, + .op_alpha = .ADD, + }, + }; + break :init colors; + }, + }); + + imgui.setup(options.allocator, .{ + .logger = structCast(simgui.Logger, options.logger), + + // TODO: Figure out a way to make imgui play nicely with UI + // Ideally when mouse is inside a Imgui window, then the imgui cursor should be used. + // Otherwise our own cursor should be used. + .disable_set_mouse_cursor = true + }); + + imgui.addFont(@embedFile("./assets/roboto-font/Roboto-Regular.ttf"), 16); + + // make sure the fontstash atlas width/height is pow-2 + const atlas_dim = std.math.ceilPowerOfTwoAssert(u32, @intFromFloat(512 * dpi_scale)); + g_fons_context = c.sfons_create(&c.sfons_desc_t{ + .width = @intCast(atlas_dim), + .height = @intCast(atlas_dim), + }); + assert(g_fons_context != null); + + font_id_map = .init(.{ + .regular = loadEmbededFont(g_fons_context, "regular", "./assets/roboto-font/Roboto-Regular.ttf"), + .italic = loadEmbededFont(g_fons_context, "italic", "./assets/roboto-font/Roboto-Italic.ttf"), + .bold = loadEmbededFont(g_fons_context, "bold", "./assets/roboto-font/Roboto-Bold.ttf"), + }); + + // TODO: Generate mipmap from SVG. + // image_map = .init(.{ + // .package = try makeImageFromMemory(&.{ + // // https://www.iconfinder.com/icons/9026684/package_thin_icon + // @embedFile("../assets/icons/package-512x512.png"), + // @embedFile("../assets/icons/package-256x256.png"), + // @embedFile("../assets/icons/package-128x128.png"), + // @embedFile("../assets/icons/package-64x64.png"), + // @embedFile("../assets/icons/package-32x32.png"), + // }), + // .trash = try makeImageFromMemory(&.{ + // // TODO: Provide attribution for image + // // https://www.iconfinder.com/icons/9027022/trash_thin_icon + // @embedFile("../assets/icons/trash-32x32.png"), + // }) + // }); + + // var image_iter = image_map.iterator(); + // while (image_iter.next()) |entry| { + // const image_view = sg.makeView(.{ + // .texture = .{ .image = entry.value.* } + // }); + // assert(image_view.id != sg.invalid_id); + // + // image_view_map.set(entry.key, image_view); + // } + + linear_sampler = sg.makeSampler(.{ + .min_filter = .LINEAR, + .mag_filter = .LINEAR, + .mipmap_filter = .LINEAR, + .label = "linear-sampler", + }); +} + +pub fn deinit() void { + imgui.shutdown(); + c.sfons_destroy(g_fons_context); + sgl.shutdown(); + sg.shutdown(); +} + +inline fn v2fColor(x: f32, y: f32, color: Vec4) void { + sgl.v2fC4f(x, y, color.x, color.y, color.z, color.w); +} + +inline fn v2fT2Color(x: f32, y: f32, u: f32, v: f32, color: Vec4) void { + sgl.v2fT2fC4f(x, y, u, v, color.x, color.y, color.z, color.w); +} + +inline fn vertexesQuad(top_left: Vec2, top_right: Vec2, bottom_right: Vec2, bottom_left: Vec2, color: Vec4) void { + v2fColor(top_left.x, top_left.y, color); + v2fColor(top_right.x, top_right.y, color); + v2fColor(bottom_right.x, bottom_right.y, color); + v2fColor(bottom_left.x, bottom_left.y, color); +} + +pub fn drawQuad(quad: [4]Vec2, color: Vec4) void { + sgl.beginQuads(); + defer sgl.end(); + + for (quad) |position| { + v2fColor(position.x, position.y, color); + } +} + +pub fn drawTriangle(tri: [3]Vec2, color: Vec4) void { + sgl.beginTriangles(); + defer sgl.end(); + + for (tri) |position| { + v2fColor(position.x, position.y, color); + } +} + +pub fn drawRectangle(pos: Vec2, size: Vec2, color: Vec4) void { + drawQuad( + .{ + // Top left + pos, + // Top right + pos.add(.{ .x = size.x, .y = 0 }), + // Bottom right + pos.add(size), + // Bottom left + pos.add(.{ .x = 0, .y = size.y }), + }, + color + ); +} + +pub fn drawImage(image_id: ImageId, pos: Vec2, size: Vec2, tint: Vec4) void { + sgl.enableTexture(); + defer sgl.disableTexture(); + + + sgl.texture( + image_view_map.get(image_id), + linear_sampler + ); + + const top_left = pos; + const top_right = pos.add(.{ .x = size.x, .y = 0 }); + const bottom_right = pos.add(size); + const bottom_left = pos.add(.{ .x = 0, .y = size.y }); + + sgl.beginQuads(); + defer sgl.end(); + + v2fT2Color(top_left.x, top_left.y, 0, 0, tint); + v2fT2Color(top_right.x, top_right.y, 1, 0, tint); + v2fT2Color(bottom_right.x, bottom_right.y, 1, 1, tint); + v2fT2Color(bottom_left.x, bottom_left.y, 0, 1, tint); +} + +fn getCircleSegmentCount(radius: f32, from_angle: f32, to_angle: f32) usize { + const pi2 = 2 * std.math.pi; + + const C = pi2 * radius; + const C_segment = C * (@abs(to_angle - from_angle) / pi2); + + return @intFromFloat(C_segment / circle_quality); +} + +fn drawCorner(pos: Vec2, radius: f32, color: Vec4, from_angle: f32, to_angle: f32) void { + sgl.beginTriangles(); + defer sgl.end(); + + const detail = getCircleSegmentCount(radius, from_angle, to_angle); + + // TODO: Use precomputed angles + for (0..detail) |i| { + const angle = std.math.lerp(from_angle, to_angle, @as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(detail))); + const angle1 = std.math.lerp(from_angle, to_angle, @as(f32, @floatFromInt(i + 1)) / @as(f32, @floatFromInt(detail))); + const x = @cos(angle); + const y = @sin(angle); + const x1 = @cos(angle1); + const y1 = @sin(angle1); + + v2fColor(pos.x , pos.y , color); + v2fColor(pos.x + x * radius, pos.y + y * radius, color); + v2fColor(pos.x + x1 * radius, pos.y + y1 * radius, color); + } +} + +fn drawOutlineCornerSegment( + pos: Vec2, + color: Vec4, + outer_radius: f32, inner_radius: f32, + from_angle: f32, to_angle: f32 +) void { + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + var detail = getCircleSegmentCount(outer_radius, from_angle, to_angle); + detail = @max(detail, 1); + + // TODO: Use precomputed angles + for (0..detail) |i| { + const angle = std.math.lerp(from_angle, to_angle, @as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(detail))); + const angle1 = std.math.lerp(from_angle, to_angle, @as(f32, @floatFromInt(i + 1)) / @as(f32, @floatFromInt(detail))); + + const circle_point0 = Vec2.init(@cos(angle), @sin(angle)); + const circle_point1 = Vec2.init(@cos(angle1), @sin(angle1)); + + drawQuad( + .{ + pos.add(circle_point1.multiplyScalar(outer_radius)), + pos.add(circle_point0.multiplyScalar(outer_radius)), + pos.add(circle_point0.multiplyScalar(@max(0, inner_radius))), + pos.add(circle_point1.multiplyScalar(@max(0, inner_radius))), + }, + color + ); + } +} + +pub fn drawRoundedRectangle(pos: Vec2, size: Vec2, color: Vec4, corners: Corners) void { + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + const left = pos.x; + const right = pos.x + size.x; + const top = pos.y; + const bottom = pos.y + size.y; + + const tr = corners.top_right; + const tl = corners.top_left; + const bl = corners.bottom_left; + const br = corners.bottom_right; + + { + sgl.beginQuads(); + defer sgl.end(); + + vertexesQuad( + Vec2.init(left + tl, top), + Vec2.init(right - tr, top), + Vec2.init(right - tr, top + tr), + Vec2.init(left + tl, top + tl), + color + ); + + vertexesQuad( + Vec2.init(right - tr, top + tr), + Vec2.init(right, top + tr), + Vec2.init(right, bottom - br), + Vec2.init(right - br, bottom - br), + color + ); + + vertexesQuad( + Vec2.init(left + bl, bottom - bl), + Vec2.init(right - br, bottom - br), + Vec2.init(right - br, bottom), + Vec2.init(left + bl, bottom), + color + ); + + vertexesQuad( + Vec2.init(left, top + tl), + Vec2.init(left + tl, top + tl), + Vec2.init(left + bl, bottom - bl), + Vec2.init(left, bottom - bl), + color + ); + + vertexesQuad( + Vec2.init(left + tl, top + tl), + Vec2.init(right - tr, top + tr), + Vec2.init(right - br, bottom - br), + Vec2.init(left + bl, bottom - bl), + color + ); + } + + drawCorner( + Vec2.init(right - tr, top + tr), + tr, + color, + std.math.pi * (3.0 / 2.0), + std.math.pi * 2.0, + ); + + drawCorner( + Vec2.init(left + tl, top + tl), + tl, + color, + std.math.pi, + std.math.pi * (3.0 / 2.0) + ); + + drawCorner( + Vec2.init(left + bl, bottom - bl), + bl, + color, + std.math.pi, + std.math.pi * (1.0 / 2.0), + ); + + drawCorner( + Vec2.init(right - br, bottom - br), + br, + color, + 0, + std.math.pi * (1.0 / 2.0) + ); +} + +const OutlineCorner = struct { + point: Vec2, + corner_radius: f32, + dir_to_center: Vec2, + + pub fn getCircleCenter(self: OutlineCorner) Vec2 { + return self.point.add(self.dir_to_center.multiplyScalar(self.corner_radius)); + } + + pub fn getEdge(self: OutlineCorner, segment: OutlineSegment, border: f32) [2]Vec2 { + const inward_sign = switch (segment.axis) { + .X => self.dir_to_center.x, + .Y => self.dir_to_center.y, + }; + + var bottom_left = self.point; + if (segment.axis == .X) { + bottom_left.x += self.corner_radius * inward_sign; + } else { + bottom_left.y += self.corner_radius * inward_sign; + } + + var top_left = bottom_left.add(segment.inward_direction.multiplyScalar(border)); + if (segment.axis == .X) { + top_left.x += @max(border - self.corner_radius, 0) * inward_sign; + } else { + top_left.y += @max(border - self.corner_radius, 0) * inward_sign; + } + + return .{ bottom_left, top_left }; + } + + fn draw(self: OutlineCorner, segment: OutlineSegment, border_size: f32, corner_angle_sign: f32, edge: [2]Vec2,) void { + if (self.corner_radius > 0) { + const center = self.getCircleCenter(); + + const inner_radius = self.corner_radius - border_size; + if (inner_radius < 0) { + drawTriangle( + .{ + edge[0], + edge[1], + center + }, + segment.color + ); + } + + drawOutlineCornerSegment( + center, + segment.color, + self.corner_radius, + inner_radius, + segment.outward_angle, + segment.outward_angle + corner_angle_sign * std.math.pi / 4.0, + ); + } + } +}; + +const OutlineSegment = struct { + // TODO: Figure out a way to remove `lower_side_angle_dir` and `upper_side_angle_dir`? + + lower_side: OutlineCorner, + lower_side_angle_dir: f32, + upper_side: OutlineCorner, + upper_side_angle_dir: f32, + + outward_angle: f32, + color: Vec4, + axis: enum { X, Y }, + inward_direction: Vec2, + + pub fn getLowerEdge(self: OutlineSegment, border: f32) [2]Vec2 { + return self.lower_side.getEdge(self, border); + } + + pub fn getUpperEdge(self: OutlineSegment, border: f32) [2]Vec2 { + return self.upper_side.getEdge(self, border); + } + + fn draw(self: OutlineSegment, border: f32) void { + if (self.color.w != 0) { + const lower_edge = self.getLowerEdge(border); + const upper_edge = self.getUpperEdge(border); + + drawQuad( + .{ + lower_edge[0], + lower_edge[1], + + upper_edge[1], + upper_edge[0], + }, + self.color + ); + + self.lower_side.draw(self, border, self.lower_side_angle_dir, lower_edge); + self.upper_side.draw(self, border, self.upper_side_angle_dir, upper_edge); + } + } +}; + +pub fn drawRectangleOutlineRounded(pos: Vec2, size: Vec2, corners: Corners, borders: Borders) void { + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + if (borders.size == 0) { + return; + } + + const left = pos.x; + const right = pos.x+size.x; + const top = pos.y; + const bottom = pos.y+size.y; + + const bottom_left_corner = OutlineCorner{ + .point = Vec2.init(left, bottom), + .corner_radius = corners.bottom_left, + .dir_to_center = Vec2.init(1, -1), + }; + + const bottom_right_corner = OutlineCorner{ + .point = Vec2.init(right, bottom), + .corner_radius = corners.bottom_right, + .dir_to_center = Vec2.init(-1, -1), + }; + + const top_right_corner = OutlineCorner{ + .point = Vec2.init(right, top), + .corner_radius = corners.top_right, + .dir_to_center = Vec2.init(-1, 1), + }; + + const top_left_corner = OutlineCorner{ + .point = Vec2.init(left, top), + .corner_radius = corners.top_left, + .dir_to_center = Vec2.init(1, 1), + }; + + const segments = .{ + OutlineSegment{ + .lower_side = bottom_left_corner, + .lower_side_angle_dir = 1, + .upper_side = bottom_right_corner, + .upper_side_angle_dir = -1, + .outward_angle = std.math.pi * 1.0 / 2.0, + .color = borders.bottom, + .axis = .X, + .inward_direction = Vec2.init(0, -1) + }, + OutlineSegment{ + .lower_side = top_left_corner, + .lower_side_angle_dir = -1, + .upper_side = top_right_corner, + .upper_side_angle_dir = 1, + .outward_angle = std.math.pi * 3.0 / 2.0, + .color = borders.top, + .axis = .X, + .inward_direction = Vec2.init(0, 1) + }, + OutlineSegment{ + .lower_side = top_left_corner, + .lower_side_angle_dir = 1, + .upper_side = bottom_left_corner, + .upper_side_angle_dir = -1, + .outward_angle = std.math.pi, + .color = borders.left, + .axis = .Y, + .inward_direction = Vec2.init(1, 0) + }, + OutlineSegment{ + .lower_side = top_right_corner, + .lower_side_angle_dir = -1, + .upper_side = bottom_right_corner, + .upper_side_angle_dir = 1, + .outward_angle = 0, + .color = borders.right, + .axis = .Y, + .inward_direction = Vec2.init(-1, 0) + } + }; + + inline for (segments) |segment| { + segment.draw(borders.size); + } +} + +pub fn drawRectanglOutline(pos: Vec2, size: Vec2, color: Vec4, width: f32) void { + // TODO: Don't use line segments + drawLine(pos, pos.add(.{ .x = size.x, .y = 0 }), color, width); + drawLine(pos, pos.add(.{ .x = 0, .y = size.y }), color, width); + drawLine(pos.add(.{ .x = 0, .y = size.y }), pos.add(size), color, width); + drawLine(pos.add(.{ .x = size.x, .y = 0 }), pos.add(size), color, width); +} + +pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void { + const step = to.sub(from).normalized().multiplyScalar(width/2); + + const top_left = from.add(step.rotateLeft90()); + const bottom_left = from.add(step.rotateRight90()); + const top_right = to.add(step.rotateLeft90()); + const bottom_right = to.add(step.rotateRight90()); + + drawQuad( + .{ top_right, top_left, bottom_left, bottom_right }, + color + ); +} + +pub fn drawText(allocator: Allocator, pos: Vec2, font: Font, color: Vec4, text: []const u8) !void { + var ctx = DrawTextContext.init(font); + defer ctx.deinit(allocator); + + try ctx.queueText(allocator, text); + ctx.draw(pos, color); +} + +pub fn measureText(font: Font, text: []const u8) Rect { + var ctx = DrawTextContext.init(font); + return ctx.measureText(text); +} + +pub fn pushScissor(rect: Rect) void { + draw_frame.scissor_stack.appendAssumeCapacity(rect); + + sgl.scissorRectf(rect.pos.x, rect.pos.y, rect.size.x, rect.size.y, true); +} + +pub fn popScissor() void { + _ = draw_frame.scissor_stack.pop().?; + const rect = draw_frame.scissor_stack.getLast(); + + sgl.scissorRectf(rect.pos.x, rect.pos.y, rect.size.x, rect.size.y, true); +} + +pub fn event(ev: [*c]const sapp.Event) bool { + return imgui.handleEvent(ev.*); +} + +fn createProjectionMatrix() Mat4 { + const screen_size = Vec2.init(sapp.widthf(), sapp.heightf()); + + return Mat4.initIdentity() + .multiply(Mat4.initTranslate(.{ .x = -1, .y = -1, .z = 0 })) + .multiply(Mat4.initScale(.{ .x = 2, .y = 2, .z = 0 })) + + .multiply(Mat4.initTranslate(.{ .x = 0, .y = 1, .z = 0 })) + .multiply(Mat4.initScale(.{ .x = 1, .y = -1, .z = 0 })) + + .multiply(Mat4.initScale(.{ + .x = 1/screen_size.x, + .y = 1/screen_size.y, + .z = 0 + })); +} + +pub fn beginFrame() void { + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + draw_frame.init(); + draw_frame.screen_size = Vec2.init(sapp.widthf(), sapp.heightf()); + draw_frame.scissor_stack.appendAssumeCapacity(Rect.init(0, 0, sapp.widthf(), sapp.heightf())); + + imgui.newFrame(.{ + .width = @intFromFloat(draw_frame.screen_size.x), + .height = @intFromFloat(draw_frame.screen_size.y), + .delta_time = sapp.frameDuration(), + .dpi_scale = sapp.dpiScale() + }); + + c.fonsClearState(g_fons_context); + sgl.defaults(); + sgl.matrixModeProjection(); + sgl.ortho(0, draw_frame.screen_size.x, draw_frame.screen_size.y, 0, -1, 1); + sgl.loadPipeline(main_pipeline); +} + +pub fn endFrame() void { + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + assert(draw_frame.scissor_stack.items.len == 1); + + var pass_action: sg.PassAction = .{}; + + pass_action.colors[0] = sg.ColorAttachmentAction{ + .load_action = .CLEAR, + .clear_value = .{ + .r = draw_frame.bg_color.x, + .g = draw_frame.bg_color.y, + .b = draw_frame.bg_color.z, + .a = draw_frame.bg_color.w + } + }; + + c.sfons_flush(g_fons_context); + + { + sg.beginPass(.{ + .action = pass_action, + .swapchain = sglue.swapchain() + }); + defer sg.endPass(); + + sgl.draw(); + + imgui.render(); + } + sg.commit(); +} diff --git a/src/imgui.zig b/src/imgui.zig new file mode 100644 index 0000000..02fe58f --- /dev/null +++ b/src/imgui.zig @@ -0,0 +1,502 @@ +const std = @import("std"); +const Math = @import("./math.zig"); +const options = @import("options"); +pub const ig = @import("cimgui"); +const Vec2 = Math.Vec2; +const Vec3 = Math.Vec3; +const Vec4 = Math.Vec4; + +const sokol = @import("sokol"); +const sapp = sokol.app; +const simgui = sokol.imgui; + +const enabled = options.has_imgui; + +var global_allocator: ?std.mem.Allocator = null; + +pub const WindowOptions = struct { + name: [*c]const u8, + pos: ?Vec2 = null, + size: ?Vec2 = null, + collapsed: ?bool = null, + open: ?*bool = null +}; + +pub const SliderOptions = struct { + label: [*c]const u8, + value: *f32, + min: f32, + max: f32, +}; + +fn toImVec2(vec2: Vec2) ig.ImVec2 { + return ig.ImVec2{ + .x = vec2.x, + .y = vec2.y, + }; +} + +inline fn structCast(T: type, value: anytype) T { + return @as(*T, @ptrFromInt(@intFromPtr(&value))).*; +} + +pub fn setup(gpa: std.mem.Allocator, desc: simgui.Desc) void { + if (!enabled) { + return; + } + + global_allocator = gpa; + simgui.setup(desc); +} + +pub fn addFont(ttf_data: []const u8, font_size: f32) void { + if (!enabled) { + return; + } + + var font_config: ig.ImFontConfig = .{}; + font_config.FontDataOwnedByAtlas = false; + font_config.OversampleH = 2; + font_config.OversampleV = 2; + font_config.GlyphMaxAdvanceX = std.math.floatMax(f32); + font_config.RasterizerMultiply = 1.0; + font_config.RasterizerDensity = 1.0; + font_config.EllipsisChar = 0; + + const io = ig.igGetIO(); + _ = ig.ImFontAtlas_AddFontFromMemoryTTF( + io.*.Fonts, + @constCast(@ptrCast(ttf_data.ptr)), + @intCast(ttf_data.len), + font_size, + &font_config, + null + ); +} + +pub fn shutdown() void { + if (!enabled) { + return; + } + + simgui.shutdown(); +} + +pub fn handleEvent(ev: sapp.Event) bool { + if (enabled) { + return simgui.handleEvent(ev); + } else { + return false; + } +} + +pub fn newFrame(desc: simgui.FrameDesc) void { + if (!enabled) { + return; + } + + simgui.newFrame(desc); +} + +pub fn render() void { + if (!enabled) { + return; + } + + simgui.render(); +} + +pub fn beginWindow(opts: WindowOptions) bool { + if (!enabled) { + return false; + } + + if (opts.pos) |pos| { + ig.igSetNextWindowPos(toImVec2(pos), ig.ImGuiCond_Once); + } + if (opts.size) |size| { + ig.igSetNextWindowSize(toImVec2(size), ig.ImGuiCond_Once); + } + if (opts.collapsed) |collapsed| { + ig.igSetNextWindowCollapsed(collapsed, ig.ImGuiCond_Once); + } + + ig.igSetNextWindowBgAlpha(1); + + var open = ig.igBegin(opts.name, opts.open, ig.ImGuiWindowFlags_None); + if (opts.open) |opts_open| { + if (opts_open.* == false) { + open = false; + } + } + if (!open) { + endWindow(); + } + + return open; +} + +pub fn endWindow() void { + if (!enabled) { + return; + } + + ig.igEnd(); +} + +pub fn textFmt(comptime fmt: []const u8, args: anytype) void { + if (!enabled) { + return; + } + + const formatted = std.fmt.allocPrintSentinel(global_allocator, fmt, args, 0) catch return; + defer global_allocator.free(formatted); + + text(formatted); +} + +pub fn text(text_z: [*c]const u8) void { + if (!enabled) { + return; + } + + ig.igText("%s", text_z); +} + +pub fn beginDisabled(disabled: bool) void { + if (!enabled) { + return; + } + + ig.igBeginDisabled(disabled); +} + +pub fn endDisabled() void { + if (!enabled) { + return; + } + + ig.igEndDisabled(); +} + +pub fn button(label: [*c]const u8) bool { + if (!enabled) { + return false; + } + + return ig.igButton(label); +} + +pub fn slider(opts: SliderOptions) bool { + if (!enabled) { + return false; + } + + return ig.igSliderFloat(opts.label, opts.value, opts.min, opts.max); +} + +pub fn checkbox(label: [*c]const u8, value: *bool) bool { + if (!enabled) { + return false; + } + + return ig.igCheckbox(label, value); +} + +pub fn beginTabBar(id: [*c]const u8) bool { + if (!enabled) { + return false; + } + + return ig.igBeginTabBar(id, ig.ImGuiTabBarFlags_None); +} + +pub fn endTabBar() void { + if (!enabled) { + return; + } + + ig.igEndTabBar(); +} + +pub fn beginTabItem(label: [*c]const u8) bool { + if (!enabled) { + return false; + } + + return ig.igBeginTabItem(label, null, ig.ImGuiTabItemFlags_None); +} + +pub fn endTabItem() void { + if (!enabled) { + return; + } + + return ig.igEndTabItem(); +} + +pub fn beginGroup() void { + if (!enabled) { + return; + } + + ig.igBeginGroup(); +} + +pub fn endGroup() void { + if (!enabled) { + return; + } + + ig.igEndGroup(); +} + +pub fn sameLine() void { + if (!enabled) { + return; + } + + ig.igSameLine(); +} + +pub fn beginTable(id: [*c]const u8, columns: u32, flags: ig.ImGuiTableFlags) bool { + if (!enabled) { + return false; + } + + return ig.igBeginTable(id, @intCast(columns), flags); +} + +pub fn endTable() void { + if (!enabled) { + return; + } + + ig.igEndTable(); +} + +pub fn tableNextColumn() void { + if (!enabled) { + return; + } + + _ = ig.igTableNextColumn(); +} + +pub fn tableNextRow() void { + if (!enabled) { + return; + } + + _ = ig.igTableNextRow(); +} + +pub fn tableSetColumnIndex(index: usize) void { + if (!enabled) { + return; + } + + _ = ig.igTableSetColumnIndex(@intCast(index)); +} + +pub fn tableSetupColumn(label: [*c]const u8, flags: ig.ImGuiTableColumnFlags) void { + if (!enabled) { + return; + } + + ig.igTableSetupColumn(label, flags); +} + +pub fn tableHeadersRow() void { + if (!enabled) { + return; + } + + ig.igTableHeadersRow(); +} + +pub const ID = union(enum) { + string: []const u8, + int: i32 +}; + +pub fn pushID(id: ID) void { + if (!enabled) { + return; + } + + switch (id) { + .string => |str| ig.igPushIDStr(str.ptr, str.ptr + str.len), + .int => |int| ig.igPushIDInt(int) + } +} + +pub fn popID() void { + if (!enabled) { + return; + } + + ig.igPopID(); +} + +pub const TreeNodeFlags = packed struct { + selected: bool = false, + framed: bool = false, + allow_overlap: bool = false, + no_tree_pushOnOpen: bool = false, + no_auto_open_on_log: bool = false, + default_open: bool = false, + open_on_double_click: bool = false, + open_on_arrow: bool = false, + leaf: bool = false, + bullet: bool = false, + frame_padding: bool = false, + span_avail_width: bool = false, + span_full_width: bool = false, + span_label_width: bool = false, + span_all_columns: bool = false, + label_span_all_columns: bool = false, + nav_left_jumps_back_here: bool = false, + collapsing_header: bool = false, + + fn toInt(self: TreeNodeFlags) u32 { + // TODO: Try using comptime to reduce this duplication. + // Would be great if `toInt()` could be replaced with just a @bitCast + // + // If the underlying C enum is exhaustive, maybe a bitcast could be performed? + // If the order of enums is correct + const flags = .{ + .{ self.selected, ig.ImGuiTreeNodeFlags_Selected }, + .{ self.framed, ig.ImGuiTreeNodeFlags_Framed }, + .{ self.allow_overlap, ig.ImGuiTreeNodeFlags_AllowOverlap }, + .{ self.no_tree_pushOnOpen, ig.ImGuiTreeNodeFlags_NoTreePushOnOpen }, + .{ self.no_auto_open_on_log, ig.ImGuiTreeNodeFlags_NoAutoOpenOnLog }, + .{ self.default_open, ig.ImGuiTreeNodeFlags_DefaultOpen }, + .{ self.open_on_double_click, ig.ImGuiTreeNodeFlags_OpenOnDoubleClick }, + .{ self.open_on_arrow, ig.ImGuiTreeNodeFlags_OpenOnArrow }, + .{ self.leaf, ig.ImGuiTreeNodeFlags_Leaf }, + .{ self.bullet, ig.ImGuiTreeNodeFlags_Bullet }, + .{ self.frame_padding, ig.ImGuiTreeNodeFlags_FramePadding }, + .{ self.span_avail_width, ig.ImGuiTreeNodeFlags_SpanAvailWidth }, + .{ self.span_full_width, ig.ImGuiTreeNodeFlags_SpanFullWidth }, + .{ self.span_label_width, ig.ImGuiTreeNodeFlags_SpanLabelWidth }, + .{ self.span_all_columns, ig.ImGuiTreeNodeFlags_SpanAllColumns }, + .{ self.label_span_all_columns, ig.ImGuiTreeNodeFlags_LabelSpanAllColumns }, + .{ self.nav_left_jumps_back_here, ig.ImGuiTreeNodeFlags_NavLeftJumpsBackHere }, + .{ self.collapsing_header, ig.ImGuiTreeNodeFlags_CollapsingHeader }, + }; + + var sum: u32 = 0; + inline for (flags) |flag_pair| { + if (flag_pair[0]) { + sum += flag_pair[1]; + } + } + return sum; + } +}; + +pub fn treeNode(label: [*c]const u8, flags: TreeNodeFlags) bool { + if (!enabled) { + return false; + } + + return ig.igTreeNodeEx(label, @intCast(flags.toInt())); +} + +pub fn treePop() void { + if (!enabled) { + return; + } + + ig.igTreePop(); +} + +pub fn isItemClicked() bool { + if (!enabled) { + return false; + } + + return ig.igIsItemClicked(); +} + +pub fn isItemToggledOpen() bool { + if (!enabled) { + return false; + } + + return ig.igIsItemToggledOpen(); +} + +pub fn colorPicker4(label: [*c]const u8, color: *Vec4) bool { + if (!enabled) { + return false; + } + + return ig.igColorPicker4(label, color.asArray().ptr, 0, null); +} + +pub fn colorEdit4(label: [*c]const u8, color: *Vec4) bool { + if (!enabled) { + return false; + } + + return ig.igColorEdit4(label, color.asArray().ptr, 0); +} + +pub fn beginCombo(label: [*c]const u8, preview_value: [*c]const u8) bool { + if (!enabled) { + return false; + } + + return ig.igBeginCombo(label, preview_value, 0); +} + +pub fn endCombo() void { + if (!enabled) { + return; + } + + ig.igEndCombo(); +} + +pub fn selectable(label: [*c]const u8, selected: bool) bool { + if (!enabled) { + return false; + } + + return ig.igSelectableEx(label, selected, 0, .{ }); +} + +pub fn setItemDefaultFocus() void { + if (!enabled) { + return; + } + + ig.igSetItemDefaultFocus(); +} + +pub fn combo(label: [*c]const u8, items: []const [*c]const u8, selected: *usize) void { + if (beginCombo(label, items[selected.*])) { + defer endCombo(); + + for (0.., items) |i, item| { + const is_selected = selected.* == i; + if (selectable(item, is_selected)) { + selected.* = i; + } + + if (is_selected) { + setItemDefaultFocus(); + } + } + } +} + +pub fn separator() void { + if (!enabled) { + return; + } + + ig.igSeparator(); +} diff --git a/src/libs/sokol_fontstash_impl.c b/src/libs/sokol_fontstash_impl.c new file mode 100644 index 0000000..6c3ba3d --- /dev/null +++ b/src/libs/sokol_fontstash_impl.c @@ -0,0 +1,85 @@ +#include "stdlib.h" +#include "stdio.h" +#include "stdbool.h" +#include +#define FONTSTASH_IMPLEMENTATION +#include "fontstash.h" + +#include "sokol/sokol_gfx.h" +#include "sokol/sokol_gl.h" +#define SOKOL_FONTSTASH_IMPL +#include "sokol_fontstash.h" + +typedef struct FONSstate FONSstate; + +// Expose private functions so that `getTextBoundsUtf8` could be implemented in zig +FONSstate* zig_fons__getState(FONScontext* stash) +{ + return fons__getState(stash); +} + +FONSfont* zig_getFont(FONScontext* stash, int index) +{ + if (index < 0 || index >= stash->nfonts) { + return NULL; + } + FONSfont *font = stash->fonts[index]; + if (font->data == NULL) { + return NULL; + } + + return font; +} + +bool zig_isTopLeft(FONScontext* stash) +{ + return stash->params.flags & FONS_ZERO_TOPLEFT; +} + +int zig_getGlyphIndex(FONSglyph* glyph) +{ + return glyph->index; +} + +float zig_fons__tt_getPixelHeightScale(FONSfont *font, float size) +{ + return fons__tt_getPixelHeightScale(&font->font, size); +} + +float zig_fons__getVertAlign(FONScontext* stash, FONSfont* font, int align, short isize) +{ + return fons__getVertAlign(stash, font, align, isize); +} + +FONSglyph* zig_fons__getGlyph( + FONScontext* stash, + FONSfont* font, + unsigned int codepoint, + short isize, + short iblur +) +{ + return fons__getGlyph(stash, font, codepoint, isize, iblur); +} + +void zig_fons__getQuad( + FONScontext* stash, + FONSfont* font, + int prevGlyphIndex, FONSglyph* glyph, + float scale, + float spacing, + float* x, float* y, + FONSquad* q +) +{ + fons__getQuad( + stash, + font, + prevGlyphIndex, + glyph, + scale, + spacing, + x, y, + q + ); +} diff --git a/src/libs/stb_image.c b/src/libs/stb_image.c new file mode 100644 index 0000000..8ddfd1f --- /dev/null +++ b/src/libs/stb_image.c @@ -0,0 +1,2 @@ +#define STB_IMAGE_IMPLEMENTATION +#include "stb_image.h" diff --git a/src/main.zig b/src/main.zig index 6339766..fa13212 100644 --- a/src/main.zig +++ b/src/main.zig @@ -1,6 +1,328 @@ const std = @import("std"); +const tracy = @import("tracy"); +const Gfx = @import("./graphics.zig"); +const builtin = @import("builtin"); + +const Window = @import("./window.zig"); +const Event = Window.Event; +const MouseButton = Window.MouseButton; + +const Math = @import("./math.zig"); +const Vec2 = Math.Vec2; + +const sokol = @import("sokol"); +const slog = sokol.log; +const sg = sokol.gfx; +const sapp = sokol.app; +const sglue = sokol.glue; + +const log = std.log.scoped(.ui); + +var window: Window = undefined; +var event_queue: std.ArrayListUnmanaged(Event) = .empty; +var mouse_inside_window: bool = false; +var event_queue_full_shown = false; +var sokol_logger: sapp.Logger = .{ .func = sokolLogCallback }; + +fn signalHandler(sig: i32) callconv(.c) void { + _ = sig; + sapp.requestQuit(); +} + +fn initErrorable() !void { + // const allocator = window.app.allocator; + + try Gfx.init(.{ + .allocator = window.gpa, + .logger = @as(*sokol.gfx.Logger, @ptrFromInt(@intFromPtr(&sokol_logger))).*, + }); +} + +export fn init() void { + var zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + initErrorable() catch |e| { + log.err("init() failed: {}", .{e}); + sapp.requestQuit(); + }; +} + +fn cStrToZig(c_str: [*c]const u8) [:0]const u8 { + return @import("std").mem.span(c_str); +} + +fn sokolLogFmt(log_level: u32, comptime format: []const u8, args: anytype) void { + const log_sokol = std.log.scoped(.sokol); + + if (log_level == 0) { + log_sokol.err(format, args); + } else if (log_level == 1) { + log_sokol.err(format, args); + } else if (log_level == 2) { + log_sokol.warn(format, args); + } else { + log_sokol.info(format, args); + } +} + +fn sokolLogCallback(tag: [*c]const u8, log_level: u32, log_item: u32, message: [*c]const u8, line_nr: u32, filename: [*c]const u8, user_data: ?*anyopaque) callconv(.c) void { + _ = user_data; + + if (filename != null) { + const format = "[{s}][id:{}] {s}:{}: {s}"; + const args = .{ + cStrToZig(tag orelse "-"), + log_item, + std.fs.path.basename(cStrToZig(filename orelse "-")), + line_nr, + cStrToZig(message orelse "") + }; + + sokolLogFmt(log_level, format, args); + } else { + const format = "[{s}][id:{}] {s}"; + const args = .{ + cStrToZig(tag orelse "-"), + log_item, + cStrToZig(message orelse "") + }; + + sokolLogFmt(log_level, format, args); + } +} + +fn frameErrorable() !void { + Gfx.beginFrame(); + defer Gfx.endFrame(); + + // try window.frame(event_queue.items); + event_queue.clearRetainingCapacity(); +} + +export fn frame() void { + tracy.frameMark(); + + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + frameErrorable() catch |e| { + log.err("frame() failed: {}", .{e}); + if (@errorReturnTrace()) |trace| { + std.debug.dumpStackTrace(trace.*); + } + sapp.requestQuit(); + }; +} + +export fn cleanup() void { + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + // window.beforeExit() catch |e| { + // log.err("window.beforeExit() failed: {}", .{e}); + // if (@errorReturnTrace()) |trace| { + // std.debug.dumpStackTrace(trace.*); + // } + // }; + + // const allocator = window.app.allocator; + + window.deinit(); + Gfx.deinit(); + // event_queue.deinit(allocator); +} + +fn appendToEventQueue(e_ptr: [*c]const sapp.Event) !bool { + const e = e_ptr.*; + blk: switch (e.type) { + .MOUSE_DOWN => { + const mouse_button = Window.MouseButton.fromSokol(e.mouse_button) orelse break :blk; + + try appendEvent(Event{ + .mouse_pressed = .{ + .button = mouse_button, + .position = Vec2.init(e.mouse_x, e.mouse_y) + } + }); + + return true; + }, + .MOUSE_UP => { + const mouse_button = MouseButton.fromSokol(e.mouse_button) orelse break :blk; + + try appendEvent(Event{ + .mouse_released = .{ + .button = mouse_button, + .position = Vec2.init(e.mouse_x, e.mouse_y) + } + }); + + return true; + }, + .MOUSE_MOVE => { + if (!mouse_inside_window) { + try appendEvent(Event{ + .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) + }); + } + + try appendEvent(Event{ + .mouse_move = Vec2.init(e.mouse_x, e.mouse_y) + }); + + mouse_inside_window = true; + return true; + }, + .MOUSE_ENTER => { + if (!mouse_inside_window) { + try appendEvent(Event{ + .mouse_enter = Vec2.init(e.mouse_x, e.mouse_y) + }); + } + + mouse_inside_window = true; + return true; + }, + .RESIZED => { + if (mouse_inside_window) { + try appendEvent(Event{ + .mouse_leave = {} + }); + } + + try appendEvent(Event{ + .window_resize = {} + }); + + mouse_inside_window = false; + return true; + }, + .MOUSE_LEAVE => { + if (mouse_inside_window) { + try appendEvent(Event{ + .mouse_leave = {} + }); + } + + mouse_inside_window = false; + return true; + }, + .MOUSE_SCROLL => { + try appendEvent(Event{ + .mouse_scroll = Vec2.init(e.scroll_x, e.scroll_y) + }); + + return true; + }, + .KEY_DOWN => { + try appendEvent(Event{ + .key_pressed = .{ + .code = @enumFromInt(@intFromEnum(e.key_code)), + .repeat = e.key_repeat + } + }); + + return true; + }, + .KEY_UP => { + try appendEvent(Event{ + .key_released = @enumFromInt(@intFromEnum(e.key_code)) + }); + + return true; + }, + .CHAR => { + try appendEvent(Event{ + .char = @intCast(e.char_code) + }); + + return true; + }, + .QUIT_REQUESTED => { + // TODO: handle quit request. Maybe show confirmation window in certain cases. + }, + else => {} + } + + return false; +} + +fn appendEvent(e: Event) !void { + event_queue.appendBounded(e) catch return error.EventQueueFull; +} + +export fn event(e_ptr: [*c]const sapp.Event) void { + const zone = tracy.initZone(@src(), .{ }); + defer zone.deinit(); + + if (Gfx.event(e_ptr)) { + appendEvent(Event{ + .mouse_leave = {} + }) catch {}; + mouse_inside_window = false; + return; + } + + const consumed_event = appendToEventQueue(e_ptr) catch |e| switch (e) { + error.EventQueueFull => blk: { + if (!event_queue_full_shown) { + log.warn("Event queue is full! Frame is taking too long to process", .{}); + event_queue_full_shown = true; + } + break :blk false; + }, + // else => blk: { + // log.err("Failed to append event to queue: {}", .{e}); + // if (@errorReturnTrace()) |trace| { + // std.debug.dumpStackTrace(trace.*); + // } + // break :blk false; + // } + }; + + if (consumed_event) { + event_queue_full_shown = false; + sapp.consumeEvent(); + } +} pub fn main() !void { - // Prints to stderr, ignoring potential errors. - std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); + var debug_allocator: std.heap.DebugAllocator(.{}) = .init; + defer _ = debug_allocator.deinit(); + + var gpa: std.mem.Allocator = undefined; + if (builtin.mode == .ReleaseFast) { + gpa = std.heap.smp_allocator; + } else { + gpa = debug_allocator.allocator(); + } + + // TODO: Use tracy TracingAllocator + + tracy.setThreadName("Main"); + + try Window.init(&window, gpa); + + var sa: std.posix.Sigaction = .{ + .handler = .{ .handler = signalHandler }, + .mask = std.posix.sigemptyset(), + .flags = std.posix.SA.RESTART, + }; + std.posix.sigaction(std.posix.SIG.INT, &sa, null); + + sapp.run(.{ + .init_cb = init, + .frame_cb = frame, + .cleanup_cb = cleanup, + .event_cb = event, + .width = 640, + .height = 480, + .icon = .{ .sokol_default = true }, + .high_dpi = true, + .sample_count = 4, + .window_title = "Game", + .logger = sokol_logger, + }); } + diff --git a/src/math.zig b/src/math.zig new file mode 100644 index 0000000..0f35670 --- /dev/null +++ b/src/math.zig @@ -0,0 +1,399 @@ +const std = @import("std"); +const assert = std.debug.assert; + +pub const bytes_per_kib = 1024; +pub const bytes_per_mib = bytes_per_kib * 1024; +pub const bytes_per_gib = bytes_per_mib * 1024; + +pub const bytes_per_kb = 1000; +pub const bytes_per_mb = bytes_per_kb * 1000; +pub const bytes_per_gb = bytes_per_mb * 1000; + +pub const Vec2 = extern struct { + x: f32, + y: f32, + + pub const zero = init(0, 0); + + pub fn init(x: f32, y: f32) Vec2 { + return Vec2{ + .x = x, + .y = y, + }; + } + + pub fn initAngle(angle: f32) Vec2 { + return Vec2{ + .x = @cos(angle), + .y = @sin(angle), + }; + } + + pub fn rotateLeft90(self: Vec2) Vec2 { + return Vec2.init(self.y, -self.x); + } + + pub fn rotateRight90(self: Vec2) Vec2 { + return Vec2.init(-self.y, self.x); + } + + pub fn flip(self: Vec2) Vec2 { + return Vec2.init(-self.x, -self.y); + } + + pub fn add(self: Vec2, other: Vec2) Vec2 { + return Vec2.init( + self.x + other.x, + self.y + other.y, + ); + } + + pub fn sub(self: Vec2, other: Vec2) Vec2 { + return Vec2.init( + self.x - other.x, + self.y - other.y, + ); + } + + pub fn multiplyScalar(self: Vec2, value: f32) Vec2 { + return Vec2.init( + self.x * value, + self.y * value, + ); + } + + pub fn multiply(self: Vec2, other: Vec2) Vec2 { + return Vec2.init( + self.x * other.x, + self.y * other.y, + ); + } + + pub fn divide(self: Vec2, other: Vec2) Vec2 { + return Vec2.init( + self.x / other.x, + self.y / other.y, + ); + } + + pub fn divideScalar(self: Vec2, value: f32) Vec2 { + return Vec2.init( + self.x / value, + self.y / value, + ); + } + + pub fn length(self: Vec2) f32 { + return @sqrt(self.x*self.x + self.y*self.y); + } + + pub fn distance(self: Vec2, other: Vec2) f32 { + return self.sub(other).length(); + } + + pub fn limitLength(self: Vec2, max_length: f32) Vec2 { + const self_length = self.length(); + if (self_length > max_length) { + return Vec2.init(self.x / self_length * max_length, self.y / self_length * max_length); + } else { + return self; + } + } + + pub fn normalized(self: Vec2) Vec2 { + const self_length = self.length(); + if (self_length == 0) { + return Vec2.init(0, 0); + } + return Vec2.init(self.x / self_length, self.y / self_length); + } + + pub fn initScalar(value: f32) Vec2 { + return Vec2.init(value, value); + } + + pub fn eql(self: Vec2, other: Vec2) bool { + return self.x == other.x and self.y == other.y; + } + + pub fn format(self: Vec2, writer: *std.io.Writer) std.io.Writer.Error!void { + try writer.print("Vec2{{ {d}, {d} }}", .{ self.x, self.y }); + } +}; + +pub const Vec3 = extern struct { + x: f32, y: f32, z: f32, + + pub const zero = init(0, 0, 0); + + pub fn init(x: f32, y: f32, z: f32) Vec3 { + return Vec3{ + .x = x, + .y = y, + .z = z, + }; + } + + pub fn initScalar(value: f32) Vec3 { + return Vec3.init(value, value, value); + } + + pub fn asArray(self: *Vec3) []f32 { + const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(self)))); + return ptr[0..3]; + } + + pub fn lerp(a: Vec3, b: Vec3, t: f32) Vec3 { + return Vec3.init( + std.math.lerp(a.x, b.x, t), + std.math.lerp(a.y, b.y, t), + std.math.lerp(a.z, b.z, t), + ); + } + + pub fn clamp(self: Vec3, min_value: f32, max_value: f32) Vec3 { + return Vec3.init( + std.math.clamp(self.x, min_value, max_value), + std.math.clamp(self.y, min_value, max_value), + std.math.clamp(self.z, min_value, max_value), + ); + } +}; + +pub const Vec4 = extern struct { + x: f32, y: f32, z: f32, w: f32, + + pub const zero = init(0, 0, 0, 0); + + pub fn init(x: f32, y: f32, z: f32, w: f32) Vec4 { + return Vec4{ + .x = x, + .y = y, + .z = z, + .w = w + }; + } + + pub fn initVec3XYZ(vec3: Vec3, w: f32) Vec4 { + return init(vec3.x, vec3.y, vec3.z, w); + } + + pub fn initScalar(value: f32) Vec4 { + return Vec4.init(value, value, value, value); + } + + pub fn multiplyMat4(left: Vec4, right: Mat4) Vec4 { + var result: Vec4 = undefined; + + // TODO: SIMD + + result.x = left.x * right.columns[0][0]; + result.y = left.x * right.columns[0][1]; + result.z = left.x * right.columns[0][2]; + result.w = left.x * right.columns[0][3]; + + result.x += left.y * right.columns[1][0]; + result.y += left.y * right.columns[1][1]; + result.z += left.y * right.columns[1][2]; + result.w += left.y * right.columns[1][3]; + + result.x += left.z * right.columns[2][0]; + result.y += left.z * right.columns[2][1]; + result.z += left.z * right.columns[2][2]; + result.w += left.z * right.columns[2][3]; + + result.x += left.w * right.columns[3][0]; + result.y += left.w * right.columns[3][1]; + result.z += left.w * right.columns[3][2]; + result.w += left.w * right.columns[3][3]; + + return result; + } + + pub fn multiply(left: Vec4, right: Vec4) Vec4 { + return init( + left.x * right.x, + left.y * right.y, + left.z * right.z, + left.w * right.w + ); + } + + pub fn asArray(self: *Vec4) []f32 { + const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(self)))); + return ptr[0..4]; + } + + pub fn initArray(array: []const f32) Vec4 { + return Vec4.init(array[0], array[1], array[2], array[3]); + } + + pub fn lerp(a: Vec4, b: Vec4, t: f32) Vec4 { + return Vec4.init( + std.math.lerp(a.x, b.x, t), + std.math.lerp(a.y, b.y, t), + std.math.lerp(a.z, b.z, t), + std.math.lerp(a.w, b.w, t), + ); + } + + pub fn clamp(self: Vec4, min_value: f32, max_value: f32) Vec4 { + return Vec4.init( + std.math.clamp(self.x, min_value, max_value), + std.math.clamp(self.y, min_value, max_value), + std.math.clamp(self.z, min_value, max_value), + std.math.clamp(self.w, min_value, max_value), + ); + } + + pub fn toVec3XYZ(self: Vec4) Vec3 { + return Vec3.init(self.x, self.y, self.z); + } +}; + +pub const Mat4 = extern struct { + columns: [4][4]f32, + + pub fn initZero() Mat4 { + var self: Mat4 = undefined; + @memset(self.asArray(), 0); + return self; + } + + pub fn initIdentity() Mat4 { + return Mat4.initDiagonal(1); + } + + pub fn initDiagonal(value: f32) Mat4 { + var self = Mat4.initZero(); + self.columns[0][0] = value; + self.columns[1][1] = value; + self.columns[2][2] = value; + self.columns[3][3] = value; + return self; + } + + pub fn multiply(left: Mat4, right: Mat4) Mat4 { + var self: Mat4 = undefined; + + inline for (.{ 0, 1, 2, 3 }) |i| { + var column = Vec4.initArray(&right.columns[i]).multiplyMat4(left); + @memcpy(&self.columns[i], column.asArray()); + } + + return self; + } + + pub fn initScale(scale: Vec3) Mat4 { + var self = Mat4.initIdentity(); + self.columns[0][0] = scale.x; + self.columns[1][1] = scale.y; + self.columns[2][2] = scale.z; + return self; + } + + pub fn initTranslate(offset: Vec3) Mat4 { + var self = Mat4.initIdentity(); + self.columns[3][0] = offset.x; + self.columns[3][1] = offset.y; + self.columns[3][2] = offset.z; + return self; + } + + pub fn asArray(self: *Mat4) []f32 { + const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(&self.columns)))); + return ptr[0..16]; + } +}; + +pub const Rect = struct { + pos: Vec2, + size: Vec2, + + pub const zero = Rect{ + .pos = Vec2.zero, + .size = Vec2.zero + }; + + pub fn init(x: f32, y: f32, width: f32, height: f32) Rect { + return Rect{ + .pos = Vec2.init(x, y), + .size = Vec2.init(width, height) + }; + } + + pub fn clip(self: Rect, other: Rect) Rect { + const left_edge = @max(self.left(), other.left()); + const right_edge = @min(self.right(), other.right()); + const top_edge = @max(self.top(), other.top()); + const bottom_edge = @min(self.bottom(), other.bottom()); + return Rect.init( + left_edge, + top_edge, + right_edge - left_edge, + bottom_edge - top_edge + ); + } + + pub fn left(self: Rect) f32 { + return self.pos.x; + } + + pub fn right(self: Rect) f32 { + return self.pos.x + self.size.x; + } + + pub fn top(self: Rect) f32 { + return self.pos.y; + } + + pub fn bottom(self: Rect) f32 { + return self.pos.y + self.size.y; + } + + pub fn isInside(self: Rect, pos: Vec2) bool { + const x_overlap = self.pos.x <= pos.x and pos.x < self.pos.x + self.size.x; + const y_overlap = self.pos.y <= pos.y and pos.y < self.pos.y + self.size.y; + return x_overlap and y_overlap; + } +}; + +pub const Line = struct { + p0: Vec2, + p1: Vec2 +}; + +pub fn isInsideRect(rect_pos: Vec2, rect_size: Vec2, pos: Vec2) bool { + const rect = Rect{ + .pos = rect_pos, + .size = rect_size + }; + return rect.isInside(pos); +} + +pub fn rgba(r: u8, g: u8, b: u8, a: f32) Vec4 { + assert(0 <= a and a <= 1); + return Vec4.init( + @as(f32, @floatFromInt(r)) / 255, + @as(f32, @floatFromInt(g)) / 255, + @as(f32, @floatFromInt(b)) / 255, + a, + ); +} + +pub fn rgb(r: u8, g: u8, b: u8) Vec4 { + return rgba(r, g, b, 1); +} + +pub fn rgb_hex(text: []const u8) ?Vec4 { + if (text.len != 7) { + return null; + } + if (text[0] != '#') { + return null; + } + const r = std.fmt.parseInt(u8, text[1..3], 16) catch return null; + const g = std.fmt.parseInt(u8, text[3..5], 16) catch return null; + const b = std.fmt.parseInt(u8, text[5..7], 16) catch return null; + return rgb(r, g, b); +} diff --git a/src/window.zig b/src/window.zig new file mode 100644 index 0000000..79685e3 --- /dev/null +++ b/src/window.zig @@ -0,0 +1,187 @@ +const std = @import("std"); +const Allocator = std.mem.Allocator; + +const sokol = @import("sokol"); + +const Math = @import("./math.zig"); +const Vec2 = Math.Vec2; + +const Window = @This(); + +pub const MouseButton = enum { + left, + right, + middle, + + pub fn fromSokol(mouse_button: sokol.app.Mousebutton) ?MouseButton { + return switch(mouse_button) { + .LEFT => MouseButton.left, + .RIGHT => MouseButton.right, + .MIDDLE => MouseButton.middle, + else => null + }; + } +}; + +pub const KeyCode = enum(std.math.IntFittingRange(0, sokol.app.max_keycodes-1)) { + SPACE = 32, + APOSTROPHE = 39, + COMMA = 44, + MINUS = 45, + PERIOD = 46, + SLASH = 47, + _0 = 48, + _1 = 49, + _2 = 50, + _3 = 51, + _4 = 52, + _5 = 53, + _6 = 54, + _7 = 55, + _8 = 56, + _9 = 57, + SEMICOLON = 59, + EQUAL = 61, + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + LEFT_BRACKET = 91, + BACKSLASH = 92, + RIGHT_BRACKET = 93, + GRAVE_ACCENT = 96, + WORLD_1 = 161, + WORLD_2 = 162, + ESCAPE = 256, + ENTER = 257, + TAB = 258, + BACKSPACE = 259, + INSERT = 260, + DELETE = 261, + RIGHT = 262, + LEFT = 263, + DOWN = 264, + UP = 265, + PAGE_UP = 266, + PAGE_DOWN = 267, + HOME = 268, + END = 269, + CAPS_LOCK = 280, + SCROLL_LOCK = 281, + NUM_LOCK = 282, + PRINT_SCREEN = 283, + PAUSE = 284, + F1 = 290, + F2 = 291, + F3 = 292, + F4 = 293, + F5 = 294, + F6 = 295, + F7 = 296, + F8 = 297, + F9 = 298, + F10 = 299, + F11 = 300, + F12 = 301, + F13 = 302, + F14 = 303, + F15 = 304, + F16 = 305, + F17 = 306, + F18 = 307, + F19 = 308, + F20 = 309, + F21 = 310, + F22 = 311, + F23 = 312, + F24 = 313, + F25 = 314, + KP_0 = 320, + KP_1 = 321, + KP_2 = 322, + KP_3 = 323, + KP_4 = 324, + KP_5 = 325, + KP_6 = 326, + KP_7 = 327, + KP_8 = 328, + KP_9 = 329, + KP_DECIMAL = 330, + KP_DIVIDE = 331, + KP_MULTIPLY = 332, + KP_SUBTRACT = 333, + KP_ADD = 334, + KP_ENTER = 335, + KP_EQUAL = 336, + LEFT_SHIFT = 340, + LEFT_CONTROL = 341, + LEFT_ALT = 342, + LEFT_SUPER = 343, + RIGHT_SHIFT = 344, + RIGHT_CONTROL = 345, + RIGHT_ALT = 346, + RIGHT_SUPER = 347, + MENU = 348, +}; + +pub const Event = union(enum) { + mouse_pressed: struct { + button: MouseButton, + position: Vec2, + }, + mouse_released: struct { + button: MouseButton, + position: Vec2, + }, + mouse_move: Vec2, + mouse_enter: Vec2, + mouse_leave, + mouse_scroll: Vec2, + key_pressed: struct { + code: KeyCode, + repeat: bool + }, + key_released: KeyCode, + window_resize, + char: u21, +}; + +gpa: Allocator, +events: std.ArrayList(Event), + +pub fn init(self: *Window, gpa: Allocator) !void { + self.* = Window{ + .gpa = gpa, + .events = .empty + // .last_frame_at_ns = std.time.nanoTimestamp(), + // .frame_arena = ArenaAllocator.init(gpa), + }; +} + +pub fn deinit(self: *Window) void { + const gpa = self.gpa; + _ = gpa; // autofix + +}