add imgui and tracy

This commit is contained in:
Rokas Puzonas 2025-12-13 17:47:07 +02:00
parent 11fc686caa
commit 0174a3f3f0
13 changed files with 3062 additions and 72 deletions

View File

@ -1,9 +1,13 @@
const std = @import("std"); 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 target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{}); 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(.{ const exe = b.addExecutable(.{
.name = "game_2025_12_13", .name = "game_2025_12_13",
.root_module = b.createModule(.{ .root_module = b.createModule(.{
@ -12,6 +16,70 @@ pub fn build(b: *std.Build) void {
.optimize = optimize, .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); b.installArtifact(exe);

View File

@ -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 <url>`, 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, .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", .version = "0.0.0",
// Together with name, this represents a globally unique package .fingerprint = 0x5704f7ae3ffdd7f8,
// 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.
.minimum_zig_version = "0.15.2", .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 = .{ .dependencies = .{
// See `zig fetch --save <url>` for a command-line interface for adding dependencies. .sokol = .{
//.example = .{ .url = "git+https://github.com/floooh/sokol-zig.git#1e233203b41893a8bf9c1c91933eba98204b6ed8",
// // When updating this field to a new URL, be sure to delete the corresponding .hash = "sokol-0.1.0-pb1HK42FNgDb5sqnsadiO2qabkfUX8jXP_DheOZGcD1W",
// // `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 .cimgui = .{
// // which will prevent zig from using it. .url = "git+https://github.com/floooh/dcimgui.git#33c99ef426b68030412b5a4b11487a23da9d4f13",
// .url = "https://example.com/foo.tar.gz", .hash = "cimgui-0.1.0-44ClkQRJlABdFMKRqIG8KDD6jy1eQbgPO335NziPYjmL",
// .lazy = true,
// // 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 .tracy = .{
// // `paths`. .url = "git+https://github.com/sagehane/zig-tracy.git#80933723efe9bf840fe749b0bfc0d610f1db1669",
// // .hash = "zig_tracy-0.0.5-aOIqsX1tAACKaRRB-sraMLuNiMASXi_y-4FtRuw4cTpx",
// // 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 .stb = .{
// // obtain a package matching this `hash`. .url = "git+https://github.com/nothings/stb.git#f1c79c02822848a9bed4315b12c8c8f3761e1296",
// // .hash = "N-V-__8AABQ7TgCnPlp8MP4YA8znrjd6E-ZjpF1rvrS8J_2I",
// // Uses the [multihash](https://multiformats.io/multihash/) format. },
// .hash = "...", .sokol_c = .{
// .url = "git+https://github.com/floooh/sokol.git#c66a1f04e6495d635c5e913335ab2308281e0492",
// // When this is provided, the package is found in a directory relative to the .hash = "N-V-__8AAC3eYABB1DVLb4dkcEzq_xVeEZZugVfQ6DoNQBDN",
// // build root. In this case the package's hash is irrelevant and therefore not },
// // computed. This field and `url` are mutually exclusive. .fontstash = .{
// .path = "foo", .url = "git+https://github.com/memononen/fontstash.git#b5ddc9741061343740d85d636d782ed3e07cf7be",
// .hash = "N-V-__8AAA9xHgAxdLYPmlNTy6qzv9IYqiIePEHQUOPWYQ_6",
// // 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,
//},
}, },
// 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 = .{ .paths = .{
"build.zig", "build.zig",
"build.zig.zon", "build.zig.zon",
"src", "src",
// For example...
//"LICENSE",
//"README.md",
}, },
} }

View File

@ -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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1375
src/graphics.zig Normal file

File diff suppressed because it is too large Load Diff

502
src/imgui.zig Normal file
View File

@ -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();
}

View File

@ -0,0 +1,85 @@
#include "stdlib.h"
#include "stdio.h"
#include "stdbool.h"
#include <stdint.h>
#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
);
}

2
src/libs/stb_image.c Normal file
View File

@ -0,0 +1,2 @@
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"

View File

@ -1,6 +1,328 @@
const std = @import("std"); 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 { pub fn main() !void {
// Prints to stderr, ignoring potential errors. var debug_allocator: std.heap.DebugAllocator(.{}) = .init;
std.debug.print("All your {s} are belong to us.\n", .{"codebase"}); 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,
});
}

399
src/math.zig Normal file
View File

@ -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);
}

187
src/window.zig Normal file
View File

@ -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
}