cherry pick changes from gamejam

This commit is contained in:
Rokas Puzonas 2026-08-10 00:52:26 +03:00
parent 62cbb9e380
commit ef5275b17f
7 changed files with 890 additions and 269 deletions

View File

@ -11,6 +11,9 @@ a: f32,
pub const black = rgb(0, 0, 0); pub const black = rgb(0, 0, 0);
pub const white = rgb(255, 255, 255); pub const white = rgb(255, 255, 255);
pub const purple = rgb(255, 0, 255); pub const purple = rgb(255, 0, 255);
pub const red = rgb(255, 0, 0);
pub const blue = rgb(0, 0, 255);
pub const green = rgb(0, 255, 0);
pub fn rgba(r: u8, g: u8, b: u8, a: f32) Color { pub fn rgba(r: u8, g: u8, b: u8, a: f32) Color {
assert(0 <= a and a <= 1); assert(0 <= a and a <= 1);
@ -38,3 +41,12 @@ pub fn rgb_hex(text: []const u8) ?Color {
const b = std.fmt.parseInt(u8, text[5..7], 16) catch return null; const b = std.fmt.parseInt(u8, text[5..7], 16) catch return null;
return rgb(r, g, b); return rgb(r, g, b);
} }
pub fn lerp(self: Color, other: Color, t: f32) Color {
return Color{
.r = std.math.lerp(self.r, other.r, t),
.g = std.math.lerp(self.g, other.g, t),
.b = std.math.lerp(self.b, other.b, t),
.a = std.math.lerp(self.a, other.a, t),
};
}

View File

@ -39,11 +39,14 @@ const State = struct {
nearest_sampler: sg.Sampler, nearest_sampler: sg.Sampler,
default_sampler: Sampler, default_sampler: Sampler,
quads_buffer: [2048]Quad, quads_buffer: [8192]VertexQuad,
quads: std.ArrayList(Quad), quads: std.ArrayList(VertexQuad),
clear_color: Color, clear_color: Color,
transforms_buffer: [32]TransformFrame,
transforms: std.ArrayList(TransformFrame) = .empty,
textures_buffer: [Texture.max_textures]Texture.SlotMap.Slot, textures_buffer: [Texture.max_textures]Texture.SlotMap.Slot,
textures: Texture.SlotMap, textures: Texture.SlotMap,
nil_texture: Texture.Id, nil_texture: Texture.Id,
@ -123,7 +126,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
const max_quads = self.quads_buffer.len; const max_quads = self.quads_buffer.len;
bindings.vertex_buffers[0] = sg.makeBuffer(.{ bindings.vertex_buffers[0] = sg.makeBuffer(.{
.size = @sizeOf(Quad) * max_quads, .size = @sizeOf(VertexQuad) * max_quads,
.usage = .{ .vertex_buffer = true, .stream_update = true }, .usage = .{ .vertex_buffer = true, .stream_update = true },
.label = "quad-vertices" .label = "quad-vertices"
}); });
@ -163,22 +166,25 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
.linear_sampler = linear_sampler, .linear_sampler = linear_sampler,
.nearest_sampler = nearest_sampler, .nearest_sampler = nearest_sampler,
.default_sampler = .linear, .default_sampler = .nearest,
.transforms_buffer = undefined,
.transforms = .initBuffer(&self.transforms_buffer),
.textures_buffer = undefined, .textures_buffer = undefined,
.textures = .init(.initBuffer(&self.textures_buffer)), .textures = .init(&self.textures_buffer),
.nil_texture = undefined, .nil_texture = undefined,
.sprites_buffer = undefined, .sprites_buffer = undefined,
.sprites = .init(.initBuffer(&self.sprites_buffer)), .sprites = .init(&self.sprites_buffer),
.nil_sprite = undefined, .nil_sprite = undefined,
.spritesheets_buffer = undefined, .spritesheets_buffer = undefined,
.spritesheets = .init(.initBuffer(&self.spritesheets_buffer)), .spritesheets = .init(&self.spritesheets_buffer),
.nil_spritesheet = undefined, .nil_spritesheet = undefined,
.fonts_buffer = undefined, .fonts_buffer = undefined,
.fonts = .init(.initBuffer(&self.fonts_buffer)), .fonts = .init(&self.fonts_buffer),
.nil_font = undefined, .nil_font = undefined,
}; };
@ -238,7 +244,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void {
var pixels: [width * height * 4]u8 = undefined; var pixels: [width * height * 4]u8 = undefined;
@memset(&pixels, 0xFF); @memset(&pixels, 0xFF);
self.default_sprite = initSprite(.{}); self.default_sprite = initSprite(.{ .padding = 0 });
setSprite(self.default_sprite, .{ setSprite(self.default_sprite, .{
.image = ImageData{ .image = ImageData{
.width = width, .width = width,
@ -328,9 +334,62 @@ fn toSokolColor(color: Color) sokol.gfx.Color {
}; };
} }
fn getTransformPtr() ?*TransformFrame {
const self = &g_state;
if (self.transforms.items.len == 0) {
return null;
}
return &self.transforms.items[self.transforms.items.len-1];
}
pub fn transformTranslate(offset: Vec2) void {
const top_frame = getTransformPtr() orelse return;
top_frame.offset = top_frame.offset.add(offset.multiply(top_frame.scale));
}
pub fn transformScale(scale: Vec2) void {
const top_frame = getTransformPtr() orelse return;
top_frame.scale = top_frame.scale.multiply(scale);
}
pub fn transformPush() void {
const self = &g_state;
const top_frame = getTransformPtr() orelse return;
self.transforms.appendBounded(top_frame.*) catch {
log.warn("Transform stack limit reached!", .{});
};
}
pub fn transformPop() void {
const self = &g_state;
_ = self.transforms.pop();
}
pub fn getTransform() TransformFrame {
if (getTransformPtr()) |top_frame| {
return top_frame.*;
} else {
return TransformFrame{
.offset = .init(0, 0),
.scale = .init(1, 1)
};
}
}
pub fn beginFrame() void { pub fn beginFrame() void {
const self = &g_state; const self = &g_state;
self.transforms.clearRetainingCapacity();
self.transforms.appendAssumeCapacity(TransformFrame{
.offset = .init(0, 0),
.scale = .init(1, 1),
});
var pass_action: sg.PassAction = .{}; var pass_action: sg.PassAction = .{};
pass_action.colors[0] = .{ pass_action.colors[0] = .{
.load_action = .CLEAR, .load_action = .CLEAR,
@ -462,15 +521,73 @@ pub fn endFrame() void {
while (spritesheet_iter.next()) |spritesheet_id| { while (spritesheet_iter.next()) |spritesheet_id| {
repackSpritesheetIfNeeded(spritesheet_id); repackSpritesheetIfNeeded(spritesheet_id);
} }
if (self.transforms.items.len > 1) {
log.warn("Too many calls to transformPush()", .{});
} else if (self.transforms.items.len == 0) {
log.warn("Too many calls to transformPop()", .{});
}
} }
pub fn drawRectangle(pos: Vec2, size: Vec2, color: Color) void { pub fn drawRectangle(pos: Vec2, size: Vec2, color: Color) void {
const self = &g_state; drawQuad(Quad.initRect(pos, size), color);
drawSprite(self.default_sprite, pos, size, color);
} }
pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void { pub fn drawQuad(quad: Quad, color: Color) void {
const self = &g_state;
drawSpriteQuad(self.default_sprite, quad, color);
}
pub fn drawRectangleOutline(pos: Vec2, size: Vec2, color: Color, width: f32, alignment: f32) void {
const self = &g_state;
const outer_rect = (Rect{ .pos = pos, .size = size }).grow(Vec2.init(width, width).multiplyScalar(1-alignment));
const inner_rect = (Rect{ .pos = pos, .size = size }).shrink(Vec2.init(width, width).multiplyScalar(alignment));
drawSpriteQuad(
self.default_sprite,
.init(.{
.init(outer_rect.right(), outer_rect.top()),
.init(outer_rect.left(), outer_rect.top()),
.init(inner_rect.right(), inner_rect.top()),
.init(inner_rect.left(), inner_rect.top()),
}),
color
);
drawSpriteQuad(
self.default_sprite,
.init(.{
.init(outer_rect.right(), outer_rect.top()),
.init(inner_rect.right(), inner_rect.top()),
.init(outer_rect.right(), outer_rect.bottom()),
.init(inner_rect.right(), inner_rect.bottom()),
}),
color
);
drawSpriteQuad(
self.default_sprite,
.init(.{
.init(outer_rect.left(), outer_rect.top()),
.init(inner_rect.left(), inner_rect.top()),
.init(outer_rect.left(), outer_rect.bottom()),
.init(inner_rect.left(), inner_rect.bottom()),
}),
color
);
drawSpriteQuad(
self.default_sprite,
.init(.{
.init(outer_rect.right(), outer_rect.bottom()),
.init(outer_rect.left(), outer_rect.bottom()),
.init(inner_rect.right(), inner_rect.bottom()),
.init(inner_rect.left(), inner_rect.bottom()),
}),
color
);
}
pub fn drawLine(from: Vec2, to: Vec2, color: Color, width: f32) void {
const self = &g_state; const self = &g_state;
const step = to.sub(from).normalized().multiplyScalar(width/2); const step = to.sub(from).normalized().multiplyScalar(width/2);
@ -480,18 +597,11 @@ pub fn drawLine(from: Vec2, to: Vec2, color: Vec4, width: f32) void {
const top_right = to.add(step.rotateLeft90()); const top_right = to.add(step.rotateLeft90());
const bottom_right = to.add(step.rotateRight90()); const bottom_right = to.add(step.rotateRight90());
var quad: Quad = undefined; drawSpriteQuad(
quad.setColor(color); self.default_sprite,
quad.setUVRect(.init(0, 0, 1, 1)); .init(.{ top_right, top_left, bottom_right, bottom_left }),
quad.vertices[0] = top_right; color
quad.vertices[1] = top_left; );
quad.vertices[2] = bottom_right;
quad.vertices[3] = bottom_left;
draw(.{
.texture = self.default_sprite,
.quad = quad
});
} }
const TextureOptions = struct { const TextureOptions = struct {
@ -500,7 +610,7 @@ const TextureOptions = struct {
pub fn initTexture(opts: TextureOptions) Texture.Id { pub fn initTexture(opts: TextureOptions) Texture.Id {
const self = &g_state; const self = &g_state;
const id = self.textures.insertBounded() catch { const id = self.textures.insertUndefined() catch {
log.warn("Failed to create texture, limit reached! limit: {}", .{self.textures.slots.capacity}); log.warn("Failed to create texture, limit reached! limit: {}", .{self.textures.slots.capacity});
return self.nil_texture; return self.nil_texture;
}; };
@ -649,7 +759,7 @@ pub fn setTexture(id: Texture.Id, texture_data: ImageData) void {
} }
pub fn drawTexture(id: Texture.Id, pos: Vec2, size: Vec2, color: Color) void { pub fn drawTexture(id: Texture.Id, pos: Vec2, size: Vec2, color: Color) void {
var quad: Quad = undefined; var quad: VertexQuad = undefined;
quad.setColor(color); quad.setColor(color);
quad.setRect(.{ .pos = pos, .size = size }); quad.setRect(.{ .pos = pos, .size = size });
quad.setUVRect(.init(0, 0, 1, 1)); quad.setUVRect(.init(0, 0, 1, 1));
@ -689,7 +799,7 @@ pub const Sampler = enum {
const DrawOptions = struct { const DrawOptions = struct {
sampler: ?Sampler = null, sampler: ?Sampler = null,
texture: Texture.Id, texture: Texture.Id,
quad: Quad quad: VertexQuad
}; };
pub fn draw(opts: DrawOptions) void { pub fn draw(opts: DrawOptions) void {
@ -738,6 +848,12 @@ pub fn draw(opts: DrawOptions) void {
flush(.{}); flush(.{});
} }
if (getTransformPtr()) |transform| {
for (&quad.vertices) |*vertex| {
vertex.position = transform.apply(vertex.position);
}
}
self.quads.appendAssumeCapacity(quad); self.quads.appendAssumeCapacity(quad);
} }
@ -754,7 +870,7 @@ pub fn getNilSprite() Sprite.Id {
pub fn initSprite(opts: SpriteOptions) Sprite.Id { pub fn initSprite(opts: SpriteOptions) Sprite.Id {
const self = &g_state; const self = &g_state;
const id = self.sprites.insertBounded() catch { const id = self.sprites.insertUndefined() catch {
log.warn("Failed to create sprite, limit reached! limit: {}", .{self.sprites.slots.capacity}); log.warn("Failed to create sprite, limit reached! limit: {}", .{self.sprites.slots.capacity});
return self.nil_sprite; return self.nil_sprite;
}; };
@ -1055,31 +1171,39 @@ pub fn getSpriteUVRect(id: Sprite.Id) ?Rect {
assert(sprite.position != null); assert(sprite.position != null);
const sprite_size = Vec2.initFromInt(u32, sprite_data.width, sprite_data.height); const sprite_size = Vec2.initFromInt(u32, sprite_data.width, sprite_data.height);
return Rect{ var uv_rect = Rect{
.pos = sprite.position.?.divide(spritesheet.size), .pos = sprite.position.?.divide(spritesheet.size),
.size = sprite_size.divide(spritesheet.size), .size = sprite_size.divide(spritesheet.size),
}; };
const texel_size = Vec2.init(1, 1).divide(spritesheet.size);
uv_rect = uv_rect.shrink(texel_size.divideScalar(8));
return uv_rect;
} }
pub fn drawSprite(id: Sprite.Id, pos: Vec2, size: Vec2, color: Color) void { pub fn drawSpriteQuad(id: Sprite.Id, points: Quad, color: Color) void {
const self = &g_state; const self = &g_state;
var quad: Quad = undefined; var quad: VertexQuad = undefined;
quad.setColor(color); quad.setColor(color);
quad.setRect(.{ .pos = pos, .size = size }); quad.vertices[0].position = points.positions[0];
quad.vertices[1].position = points.positions[1];
quad.vertices[2].position = points.positions[2];
quad.vertices[3].position = points.positions[3];
var texture: ?Texture.Id = null; var texture: ?Texture.Id = null;
if (id != self.nil_sprite) { if (id != self.nil_sprite) {
if (getSpriteUVRect(id)) |sprite_uv| { if (getSpriteUVRect(id)) |sprite_uv| {
quad.setUVRect(sprite_uv); quad.setUVRect(sprite_uv);
if (self.sprites.get(id)) |sprite| {
if (self.spritesheets.get(sprite.spritesheet)) |spritesheet| { const sprite = self.sprites.getAssumeExists(id);
const spritesheet = self.spritesheets.getAssumeExists(sprite.spritesheet);
texture = spritesheet.texture; texture = spritesheet.texture;
} }
} }
}
}
draw(.{ draw(.{
.texture = texture orelse self.nil_texture, .texture = texture orelse self.nil_texture,
@ -1087,13 +1211,18 @@ pub fn drawSprite(id: Sprite.Id, pos: Vec2, size: Vec2, color: Color) void {
}); });
} }
pub fn drawSprite(id: Sprite.Id, pos: Vec2, size: Vec2, color: Color) void {
const rect = Quad.initRect(pos, size);
drawSpriteQuad(id, rect, color);
}
pub const SpriteSheetOptions = struct { pub const SpriteSheetOptions = struct {
format: ImageData.Format = .rgba8 format: ImageData.Format = .rgba8
}; };
pub fn initSpritesheet(opts: SpriteSheetOptions) Spritesheet.Id { pub fn initSpritesheet(opts: SpriteSheetOptions) Spritesheet.Id {
const self = &g_state; const self = &g_state;
const id = self.spritesheets.insertBounded() catch { const id = self.spritesheets.insertUndefined() catch {
log.warn("Failed to create spritesheet, limit reached! limit: {}", .{self.spritesheets.slots.capacity}); log.warn("Failed to create spritesheet, limit reached! limit: {}", .{self.spritesheets.slots.capacity});
return self.nil_spritesheet; return self.nil_spritesheet;
}; };
@ -1127,7 +1256,7 @@ pub fn deinitSpritesheet(id: Spritesheet.Id) void {
pub fn initFont() Font.Id { pub fn initFont() Font.Id {
const self = &g_state; const self = &g_state;
const id = self.fonts.insertBounded() catch { const id = self.fonts.insertUndefined() catch {
log.warn("Failed to create font, limit reached! limit: {}", .{self.fonts.slots.capacity}); log.warn("Failed to create font, limit reached! limit: {}", .{self.fonts.slots.capacity});
return self.nil_font; return self.nil_font;
}; };
@ -1183,63 +1312,58 @@ pub fn setFont(id: Font.Id, ttf_data: [:0]const u8) void {
font.cache = .init(); font.cache = .init();
} }
const DrawTextOptions = struct { fn getFont(id: Font.Id) ?*Font {
text: []const u8,
pos: Vec2,
height: f32,
color: Color = .white
};
pub fn drawText(id: Font.Id, opts: DrawTextOptions) void {
const self = &g_state; const self = &g_state;
if (id == self.nil_font) { if (id == self.nil_font) {
return; return null;
} }
const font = self.fonts.get(id) orelse { return self.fonts.get(id);
log.warn("Attempt to draw text with font that doesn't exist: {}", .{id}); }
return;
};
const stb_font = font.stb orelse {
log.warn("Attempt to draw text with font that isn't set: {}", .{id});
return;
};
const fallback_glyph = stb_font.findGlyphIndex('?') orelse { pub fn getGlyphIndex(id: Font.Id, codepoint: u21) ?GlyphIndex {
log.warn("Failed to find fallback glyph '?'", .{}); const font = getFont(id) orelse return null;
return; const stb_font = font.stb orelse return null;
};
const scale = stb_font.scaleForPixelHeight(opts.height); if (stb_font.findGlyphIndex(codepoint)) |index| {
const scale_x = scale; return index;
const scale_y = scale; } else {
// Fallback
return getGlyphIndex(id, '?');
}
}
pub fn getFontPixelScale(id: Font.Id, height: f32) f32 {
const font = getFont(id) orelse return 0;
const stb_font = font.stb orelse return 0;
return stb_font.scaleForPixelHeight(height);
}
pub fn getFontAscent(id: Font.Id, height: f32) f32 {
const font = getFont(id) orelse return 0;
const stb_font = font.stb orelse return 0;
const vmetrics = stb_font.getFontVMetrics(); const vmetrics = stb_font.getFontVMetrics();
const baseline = @as(f32, @floatFromInt(vmetrics.ascent)) * scale; const scale = stb_font.scaleForPixelHeight(height);
return @as(f32, @floatFromInt(vmetrics.ascent)) * scale;
}
var current_point = opts.pos; pub fn getGlyph(id: Font.Id, index: GlyphIndex, scale_x: f32, scale_y: f32) ?*Font.Glyph {
current_point.y += baseline; const self = &g_state;
const font = getFont(id) orelse return null;
const stb_font = font.stb orelse return null;
var prev_glyph: ?u32 = null; // TODO: Allow specifying a different scale for x and y
var cursor: usize = 0; const glyph_key = Font.Glyph.Key{
while (cursor < opts.text.len) { .index = index,
const codepoint_len = std.unicode.utf8ByteSequenceLength(opts.text[cursor]) catch |e| { .scale_x = scale_x,
log.err("Failed to draw text, invalid codepoint at {} in '{s}': {}", .{cursor, opts.text, e}); .scale_y = scale_y
return;
}; };
const codepoint_bytes = opts.text[cursor..][0..codepoint_len];
const codepoint = std.unicode.utf8Decode(codepoint_bytes) catch |e| {
log.err("Failed to draw text, invalid codepoint at {} in '{s}': {}", .{cursor, opts.text, e});
return;
};
cursor += codepoint_len;
const glyph_index = stb_font.findGlyphIndex(codepoint) orelse fallback_glyph;
const glyph_key = Font.Glyph.Key{ .index = glyph_index, .scale_x = scale_x, .scale_y = scale_y };
var glyph: *Font.Glyph = undefined; var glyph: *Font.Glyph = undefined;
if (font.cache.lookup(glyph_key)) |glyph_id| { if (font.cache.lookup(glyph_key)) |glyph_id| {
glyph = font.cache.get(glyph_id); return font.cache.get(glyph_id);
} else { } else {
const glyph_id = font.cache.insert(glyph_key) orelse { const glyph_id = font.cache.insert(glyph_key) orelse {
@panic("TODO: render glyph lru_tail"); @panic("TODO: render glyph lru_tail");
@ -1247,25 +1371,25 @@ pub fn drawText(id: Font.Id, opts: DrawTextOptions) void {
glyph = font.cache.get(glyph_id); glyph = font.cache.get(glyph_id);
glyph.box = stb_font.getGlyphBitmapBox(glyph_key.index, glyph_key.scale_x, glyph_key.scale_y); glyph.box = stb_font.getGlyphBitmapBox(glyph_key.index, glyph_key.scale_x, glyph_key.scale_y);
const width: u32 = @intCast(glyph.box.x1 - glyph.box.x0); const box_width: u32 = @intCast(glyph.box.x1 - glyph.box.x0);
const height: u32 = @intCast(glyph.box.y1 - glyph.box.y0); const box_height: u32 = @intCast(glyph.box.y1 - glyph.box.y0);
if (width > 0 and height > 0) { if (box_width > 0 and box_height > 0) {
assert(glyph.sprite == self.nil_sprite); assert(glyph.sprite == self.nil_sprite);
glyph.sprite = initSprite(.{ .spritesheet = font.spritesheet, .padding = 1 }); glyph.sprite = initSprite(.{ .spritesheet = font.spritesheet, .padding = 1 });
assert(width < Font.Glyph.max_width); assert(box_width < Font.Glyph.max_width);
assert(height < Font.Glyph.max_height); assert(box_height < Font.Glyph.max_height);
var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined; var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined;
const bitmap = ImageData{ const bitmap = ImageData{
.pixels = .{ .r8 = (&bitmap_buffer).ptr }, .pixels = .{ .r8 = (&bitmap_buffer).ptr },
.width = width, .width = box_width,
.height = height .height = box_height
}; };
stb_font.makeGlyphBitmap( stb_font.makeGlyphBitmap(
bitmap.pixels.?.r8, bitmap.pixels.?.r8,
width, height, box_width, box_height,
width, box_width,
glyph_key.scale_x, glyph_key.scale_y, glyph_key.scale_x, glyph_key.scale_y,
glyph_key.index glyph_key.index
); );
@ -1276,23 +1400,239 @@ pub fn drawText(id: Font.Id, opts: DrawTextOptions) void {
} }
glyph.used_this_frame = true; glyph.used_this_frame = true;
if (prev_glyph != null) { return glyph;
const kerning = stb_font.getGlyphKernAdvance(prev_glyph.?, glyph_index);
current_point.x += @as(f32, @floatFromInt(kerning)) * scale;
} }
if (glyph.sprite != self.nil_sprite) { pub const TextRunLayout = struct {
const glyph_size = Vec2.initFromInt(i32, glyph.box.x1 - glyph.box.x0, glyph.box.y1 - glyph.box.y0); font: Font.Id,
const glyph_pos = current_point.sub(.initFromInt(i32, glyph.box.x0, -glyph.box.y0)); scale_x: f32,
drawSprite(glyph.sprite, glyph_pos, glyph_size, opts.color); scale_y: f32,
prev_glyph: ?GlyphIndex,
pen_pos: Vec2,
const GlyphRect = struct {
rect: Rect,
sprite: Sprite.Id,
};
pub fn init(font: Font.Id, height: f32) TextRunLayout {
const scale = getFontPixelScale(font, height);
return TextRunLayout{
.font = font,
.scale_x = scale,
.scale_y = scale,
.prev_glyph = null,
.pen_pos = .init(0, 0)
};
}
pub fn next(self: *TextRunLayout, glyph_index: GlyphIndex) ?GlyphRect {
const glyph = getGlyph(self.font, glyph_index, self.scale_x, self.scale_y) orelse return null;
const font = getFont(self.font) orelse return null;
const stb_font = font.stb orelse return null;
if (self.prev_glyph) |prev_glyph| {
const kerning = stb_font.getGlyphKernAdvance(prev_glyph, glyph_index);
self.pen_pos.x += @as(f32, @floatFromInt(kerning)) * self.scale_x;
} }
const hmetrics = stb_font.getGlyphHMetrics(glyph_index); const hmetrics = stb_font.getGlyphHMetrics(glyph_index);
current_point.x += @as(f32, @floatFromInt(hmetrics.advance_width)) * scale; const glyph_size = Vec2.initFromInt(i32, glyph.box.x1 - glyph.box.x0, glyph.box.y1 - glyph.box.y0);
prev_glyph = glyph_index;
var draw_pos = self.pen_pos;
draw_pos.x -= @floatFromInt(glyph.box.x0);
draw_pos.x += @as(f32, @floatFromInt(hmetrics.left_side_bearing)) * self.scale_x;
draw_pos.y += @floatFromInt(glyph.box.y0);
const draw_rect = Rect{
.pos = draw_pos,
.size = glyph_size
};
self.pen_pos.x += @as(f32, @floatFromInt(hmetrics.advance_width)) * self.scale_x;
self.prev_glyph = glyph_index;
return GlyphRect{
.rect = draw_rect,
.sprite = glyph.sprite
};
}
};
pub fn measureText(id: Font.Id, height: f32, text: []const u8) Rect {
var bounds: Rect = .init(0, 0, 0, 0);
var first_glyph = false;
var layout = TextRunLayout.init(id, height);
var iter = std.unicode.Utf8Iterator{
.bytes = text,
.i = 0
};
while (iter.nextCodepoint()) |codepoint| {
const glyph_index = getGlyphIndex(id, codepoint) orelse continue;
const glyph_layout = layout.next(glyph_index) orelse continue;
if (first_glyph) {
bounds = glyph_layout.rect;
first_glyph = false;
} else {
bounds = bounds.expand(glyph_layout.rect);
} }
} }
return bounds;
}
pub const DrawTextOptions = struct {
text: []const u8,
pos: Vec2,
height: f32,
color: Color = .white,
alignment: Vec2 = .init(0, 0)
};
pub fn drawText(id: Font.Id, opts: DrawTextOptions) void {
const self = &g_state;
var layout = TextRunLayout.init(id, opts.height);
var pos = opts.pos;
pos.y += getFontAscent(id, opts.height);
if (!opts.alignment.eql(.zero)) {
const bounds = measureText(id, opts.height, opts.text);
pos = pos.sub(bounds.size.multiply(opts.alignment));
}
var iter = std.unicode.Utf8Iterator{
.bytes = opts.text,
.i = 0
};
while (iter.nextCodepoint()) |codepoint| {
const glyph_index = getGlyphIndex(id, codepoint) orelse continue;
const glyph_layout = layout.next(glyph_index) orelse continue;
if (glyph_layout.sprite == self.nil_sprite) {
continue;
}
drawSprite(
glyph_layout.sprite,
glyph_layout.rect.pos.add(pos),
glyph_layout.rect.size,
opts.color
);
}
}
pub const TextBuilder = struct {
gpa: std.mem.Allocator,
glyphs: std.ArrayList(Glyph),
color: Color,
baseline: f32,
layout: ?TextRunLayout,
bounds: Rect,
const Glyph = struct {
sprite: Sprite.Id,
rect: Rect,
color: Color
};
pub fn init(gpa: std.mem.Allocator) TextBuilder {
return TextBuilder{
.gpa = gpa,
.glyphs = .empty,
.color = .white,
.baseline = 0,
.layout = null,
.bounds = .init(0, 0, 0, 0)
};
}
pub fn font(self: *TextBuilder, id: Font.Id, height: f32) void {
var new_layout = TextRunLayout.init(id, height);
if (self.layout) |layout| {
if (new_layout.font == layout.font and
new_layout.scale_x == layout.scale_x and
new_layout.scale_y == layout.scale_y
) {
return;
}
}
const font_baseline = getFontAscent(id, height);
if (font_baseline > self.baseline) {
// Fixup previously drawn glyph to match the new larger baseline
const offset_y = font_baseline - self.baseline;
for (self.glyphs.items) |*glyph| {
glyph.rect.pos.y += offset_y;
}
self.bounds.pos.y += offset_y;
self.baseline = font_baseline;
}
if (self.layout) |layout| {
new_layout.pen_pos = layout.pen_pos;
if (layout.font == new_layout.font) {
new_layout.prev_glyph = layout.prev_glyph;
}
}
self.layout = new_layout;
}
pub fn putCodepoint(self: *TextBuilder, codepoint: u21) !void {
if (self.layout == null) {
return;
}
const layout = &self.layout.?;
const glyph_index = getGlyphIndex(layout.font, codepoint) orelse return;
const glyph_layout = layout.next(glyph_index) orelse return;
var rect = glyph_layout.rect;
rect.pos.y += self.baseline;
try self.glyphs.append(self.gpa, Glyph{
.sprite = glyph_layout.sprite,
.rect = rect,
.color = self.color
});
if (self.bounds.size.x > 0 and self.bounds.size.y > 0) {
self.bounds = self.bounds.expand(rect);
} else {
self.bounds = rect;
}
}
pub fn put(self: *TextBuilder, text: []const u8) !void {
var iter = std.unicode.Utf8Iterator{ .bytes = text, .i = 0 };
while (iter.nextCodepoint()) |codepoint| {
try self.putCodepoint(codepoint);
}
}
pub fn draw(self: *TextBuilder, pos: Vec2) void {
for (self.glyphs.items) |glyph| {
if (glyph.sprite == g_state.nil_sprite) {
continue;
}
drawSprite(glyph.sprite, glyph.rect.pos.add(pos), glyph.rect.size, glyph.color);
}
}
pub fn offset(self: *TextBuilder, vec2: Vec2) void {
if (self.layout) |*layout| {
layout.pen_pos = layout.pen_pos.add(vec2);
}
}
};
pub const ImageData = struct { pub const ImageData = struct {
const Format = enum { const Format = enum {
rgba8, rgba8,
@ -1375,7 +1715,7 @@ pub const ImageData = struct {
} }
} }
fn getPixel(self: ImageData, x: u32, y: u32) []u8 { pub fn getPixel(self: ImageData, x: u32, y: u32) []u8 {
const pixel_index = y * self.width + x; const pixel_index = y * self.width + x;
if (self.pixels) |pixels| { if (self.pixels) |pixels| {
return switch (pixels) { return switch (pixels) {
@ -1482,7 +1822,7 @@ pub const Sprite = struct {
position: ?Vec2, position: ?Vec2,
padding: u32, padding: u32,
const max_sprites = 128; const max_sprites = 256;
const SlotMap = SlotMapType(std.math.IntFittingRange(0, max_sprites-1), u8, Sprite); const SlotMap = SlotMapType(std.math.IntFittingRange(0, max_sprites-1), u8, Sprite);
pub const Id = SlotMap.Id; pub const Id = SlotMap.Id;
}; };
@ -1912,7 +2252,10 @@ pub const Font = struct {
const max_width: u32 = 512; const max_width: u32 = 512;
const max_height: u32 = 512; const max_height: u32 = 512;
const Index = u32;
const Key = packed struct { const Key = packed struct {
// TODO: Quantize `scale_x` and `scale_y`.
index: u32, index: u32,
scale_x: f32, scale_x: f32,
scale_y: f32, scale_y: f32,
@ -1944,22 +2287,24 @@ pub const Font = struct {
}; };
}; };
const GlyphIndex = Font.Glyph.Index;
pub const Vertex = extern struct { pub const Vertex = extern struct {
position: Vec2, position: Vec2,
texcoord: Vec2, texcoord: Vec2,
color: Vec4, color: Vec4,
}; };
pub const Quad = extern struct { pub const VertexQuad = extern struct {
vertices: [4]Vertex, vertices: [4]Vertex,
pub fn init(vertices: [4]Vertex) Quad { pub fn init(vertices: [4]Vertex) VertexQuad {
return Quad{ return VertexQuad{
.vertices = vertices .vertices = vertices
}; };
} }
pub fn setColor(self: *Quad, color: Color) void { pub fn setColor(self: *VertexQuad, color: Color) void {
for (&self.vertices) |*vertex| { for (&self.vertices) |*vertex| {
vertex.color = Vec4{ vertex.color = Vec4{
.x = color.r, .x = color.r,
@ -1970,7 +2315,7 @@ pub const Quad = extern struct {
} }
} }
pub fn setUVRect(self: *Quad, rect: Rect) void { pub fn setUVRect(self: *VertexQuad, rect: Rect) void {
const pos = rect.pos; const pos = rect.pos;
const size = rect.size; const size = rect.size;
self.vertices[0].texcoord = pos; self.vertices[0].texcoord = pos;
@ -1979,7 +2324,7 @@ pub const Quad = extern struct {
self.vertices[3].texcoord = pos.add(size); self.vertices[3].texcoord = pos.add(size);
} }
pub fn setRect(self: *Quad, rect: Rect) void { pub fn setRect(self: *VertexQuad, rect: Rect) void {
const pos = rect.pos; const pos = rect.pos;
const size = rect.size; const size = rect.size;
self.vertices[0].position = pos; self.vertices[0].position = pos;
@ -1988,3 +2333,45 @@ pub const Quad = extern struct {
self.vertices[3].position = pos.add(size); self.vertices[3].position = pos.add(size);
} }
}; };
pub const Quad = struct {
positions: [4]Vec2,
pub fn init(positions: [4]Vec2) Quad {
return Quad{
.positions = positions
};
}
pub fn initRect(pos: Vec2, size: Vec2) Quad {
return Quad{
.positions = .{
pos,
pos.add(.init(size.x, 0)),
pos.add(.init(0, size.y)),
pos.add(size),
}
};
}
pub fn applyRotate(self: *Quad, rad: f32, origin: Vec2) void {
for (&self.positions) |*pos| {
pos.* = pos.sub(origin).rotate(rad).add(origin);
}
}
pub fn applyOffset(self: *Quad, vec2: Vec2) void {
for (&self.positions) |*pos| {
pos.* = pos.add(vec2);
}
}
};
pub const TransformFrame = struct {
offset: Vec2,
scale: Vec2,
pub fn apply(self: TransformFrame, position: Vec2) Vec2 {
return position.multiply(self.scale).add(self.offset);
}
};

View File

@ -15,6 +15,8 @@ const simgui = sokol.imgui;
const ig = @import("cimgui"); const ig = @import("cimgui");
const Nanoseconds = @import("./platform.zig").Nanoseconds;
const ImGUI = @This(); const ImGUI = @This();
const State = struct { const State = struct {
@ -130,16 +132,16 @@ pub fn endFrame() void {
ig.igEnd(); ig.igEnd();
if (ig.igBeginMainMenuBar()) { // if (ig.igBeginMainMenuBar()) {
defer ig.igEndMainMenuBar(); // defer ig.igEndMainMenuBar();
//
if (ig.igBeginMenu("foo")) { // if (ig.igBeginMenu("foo")) {
defer ig.igEndMenu(); // defer ig.igEndMenu();
//
if (ig.igMenuItem("bar")) { // if (ig.igMenuItem("bar")) {
} // }
} // }
} // }
simgui.render(); simgui.render();
} }
@ -150,14 +152,16 @@ pub const WindowOptions = struct {
size: ?Vec2 = null, size: ?Vec2 = null,
collapsed: ?bool = null, collapsed: ?bool = null,
open: ?*bool = null, open: ?*bool = null,
alpha: f32 = 1
}; };
pub fn beginWindow(opts: WindowOptions) bool { inline fn isDisabled() bool {
if (!build_options.has_imgui) {
return false;
}
const self = &g_state; const self = &g_state;
if (!self.enabled) { return !(build_options.has_imgui and self.enabled);
}
pub fn beginWindow(opts: WindowOptions) bool {
if (isDisabled()) {
return false; return false;
} }
@ -175,7 +179,7 @@ pub fn beginWindow(opts: WindowOptions) bool {
ig.igSetNextWindowCollapsed(collapsed, ig.ImGuiCond_Once); ig.igSetNextWindowCollapsed(collapsed, ig.ImGuiCond_Once);
} }
ig.igSetNextWindowBgAlpha(1); ig.igSetNextWindowBgAlpha(opts.alpha);
const frame = g_state.frame.?; const frame = g_state.frame.?;
const namez = frame.dupeSentinel(u8, opts.name, 0) catch blk: { const namez = frame.dupeSentinel(u8, opts.name, 0) catch blk: {
@ -197,11 +201,7 @@ pub fn beginWindow(opts: WindowOptions) bool {
} }
pub fn endWindow() void { pub fn endWindow() void {
if (!build_options.has_imgui) { if (isDisabled()) {
return;
}
const self = &g_state;
if (!self.enabled) {
return; return;
} }
@ -217,11 +217,7 @@ fn formatString(comptime fmt: []const u8, args: anytype) [:0]const u8 {
} }
pub fn text(comptime fmt: []const u8, args: anytype) void { pub fn text(comptime fmt: []const u8, args: anytype) void {
if (!build_options.has_imgui) { if (isDisabled()) {
return;
}
const self = &g_state;
if (!self.enabled) {
return; return;
} }
@ -229,11 +225,7 @@ pub fn text(comptime fmt: []const u8, args: anytype) void {
} }
pub fn button(label: []const u8) bool { pub fn button(label: []const u8) bool {
if (!build_options.has_imgui) { if (isDisabled()) {
return false;
}
const self = &g_state;
if (!self.enabled) {
return false; return false;
} }
@ -248,11 +240,7 @@ pub const SliderOptions = struct {
}; };
pub fn slider(opts: SliderOptions) bool { pub fn slider(opts: SliderOptions) bool {
if (!build_options.has_imgui) { if (isDisabled()) {
return false;
}
const self = &g_state;
if (!self.enabled) {
return false; return false;
} }
@ -264,12 +252,115 @@ pub fn slider(opts: SliderOptions) bool {
); );
} }
pub fn checkbox(label: []const u8, value: *bool) bool { const InputF32Options = struct {
if (!build_options.has_imgui) { label: []const u8,
value: *f32,
ex: ?struct {
step: f32,
step_fast: f32,
format: [*c]const u8 = null,
} = null
};
pub fn inputF32(opts: InputF32Options) bool {
if (isDisabled()) {
return false; return false;
} }
const self = &g_state;
if (!self.enabled) { if (opts.ex) |ex| {
return ig.igInputFloatEx(
formatString("{s}", .{opts.label}),
opts.value,
ex.step,
ex.step_fast,
ex.format,
ig.ImGuiInputTextFlags_None
);
} else {
return ig.igInputFloat(
formatString("{s}", .{opts.label}),
opts.value,
);
}
}
const InputF32DragOptions = struct {
label: []const u8,
value: *f32,
ex: ?struct {
speed: f32 = 1,
min: f32 = 0,
max: f32 = 10
} = null,
};
pub fn inputF32Drag(opts: InputF32DragOptions) bool {
if (isDisabled()) {
return false;
}
if (opts.ex) |ex| {
return ig.igDragFloatEx(
formatString("{s}", .{opts.label}),
opts.value,
ex.speed,
ex.min,
ex.max,
null,
ig.ImGuiSliderFlags_None
);
} else {
return ig.igDragFloat(
formatString("{s}", .{opts.label}),
opts.value,
);
}
}
pub fn inputDuration(label: []const u8, duration_ns: *Nanoseconds) bool {
const duration_ms: f32 = @floatFromInt(@divTrunc(duration_ns.*, std.time.ns_per_ms));
var duration_s = duration_ms / std.time.ms_per_s;
if (ImGUI.inputF32(.{
.label = label,
.value = &duration_s,
.ex = .{
.step = 0.01,
.step_fast = 0.1,
.format = "%0.3f s"
}
})) {
duration_ns.* = @trunc(duration_s * std.time.ns_per_s);
return true;
}
return false;
}
pub fn inputI32(label: []const u8, value: *i32) bool {
if (isDisabled()) {
return false;
}
return ig.igInputInt(
formatString("{s}", .{label}),
value,
);
}
pub fn inputU32(label: []const u8, value: *u32) bool {
var value_i32: i32 = @intCast(value.*);
if (inputI32(label, &value_i32)) {
if (value_i32 >= 0) {
value.* = @intCast(value_i32);
return true;
}
}
return false;
}
pub fn checkbox(label: []const u8, value: *bool) bool {
if (isDisabled()) {
return false; return false;
} }
@ -280,11 +371,7 @@ pub fn checkbox(label: []const u8, value: *bool) bool {
} }
pub fn separator() void { pub fn separator() void {
if (!build_options.has_imgui) { if (isDisabled()) {
return;
}
const self = &g_state;
if (!self.enabled) {
return; return;
} }
@ -298,11 +385,7 @@ const ColorEditOptions = struct {
}; };
pub fn colorEdit(opts: ColorEditOptions) bool { pub fn colorEdit(opts: ColorEditOptions) bool {
if (!build_options.has_imgui) { if (isDisabled()) {
return false;
}
const self = &g_state;
if (!self.enabled) {
return false; return false;
} }
@ -337,3 +420,86 @@ fn toImVec2(vec2: Vec2) ig.ImVec2 {
.y = vec2.y, .y = vec2.y,
}; };
} }
pub fn sameLine() void {
if (isDisabled()) {
return;
}
ig.igSameLine();
}
pub const ID = union(enum) {
string: []const u8,
int: i32
};
pub fn pushID(id: ID) void {
if (isDisabled()) {
return;
}
switch (id) {
.string => |str| ig.igPushIDStr(str.ptr, str.ptr + str.len),
.int => |int| ig.igPushIDInt(int)
}
}
pub fn popID() void {
if (isDisabled()) {
return;
}
ig.igPopID();
}
pub fn setNextItemWidth(width: f32) void {
if (isDisabled()) {
return;
}
ig.igSetNextItemWidth(width);
}
pub fn beginTabBar(id: [*c]const u8) bool {
if (isDisabled()) {
return false;
}
return ig.igBeginTabBar(id, ig.ImGuiTabBarFlags_None);
}
pub fn endTabBar() void {
if (isDisabled()) {
return;
}
ig.igEndTabBar();
}
pub fn beginTabItem(label: [*c]const u8) bool {
if (isDisabled()) {
return false;
}
return ig.igBeginTabItem(label, null, ig.ImGuiTabItemFlags_None);
}
pub fn endTabItem() void {
if (isDisabled()) {
return;
}
return ig.igEndTabItem();
}
pub fn collapsingHeader(label: []const u8) bool {
if (isDisabled()) {
return false;
}
return ig.igCollapsingHeader(
formatString("{s}", .{label}),
ig.ImGuiTreeNodeFlags_None
);
}

View File

@ -15,6 +15,7 @@ focused: bool,
keyboard: KeyStateType(KeyCode), keyboard: KeyStateType(KeyCode),
mouse_buttons: KeyStateType(MouseButton), mouse_buttons: KeyStateType(MouseButton),
mouse_position: ?Vec2, mouse_position: ?Vec2,
mouse_delta: Vec2,
mouse_scroll: Vec2, mouse_scroll: Vec2,
pub fn init(window_size: Vec2) Input { pub fn init(window_size: Vec2) Input {
@ -24,6 +25,7 @@ pub fn init(window_size: Vec2) Input {
.keyboard = .empty, .keyboard = .empty,
.mouse_buttons = .empty, .mouse_buttons = .empty,
.mouse_position = null, .mouse_position = null,
.mouse_delta = .init(0, 0),
.mouse_scroll = .init(0, 0), .mouse_scroll = .init(0, 0),
.window_size = window_size .window_size = window_size
}; };
@ -45,7 +47,7 @@ pub fn isMousePressed(self: Input, button: MouseButton) bool {
return self.mouse_buttons.pressed.contains(button); return self.mouse_buttons.pressed.contains(button);
} }
pub fn iMouseReleased(self: Input, button: MouseButton) bool { pub fn isMouseReleased(self: Input, button: MouseButton) bool {
return self.mouse_buttons.released.contains(button); return self.mouse_buttons.released.contains(button);
} }

View File

@ -336,11 +336,11 @@ pub const Mat4 = extern struct {
return self; return self;
} }
pub fn initScale(scale: Vec3) Mat4 { pub fn initScale(vec: Vec3) Mat4 {
var self = Mat4.initIdentity(); var self = Mat4.initIdentity();
self.columns[0][0] = scale.x; self.columns[0][0] = vec.x;
self.columns[1][1] = scale.y; self.columns[1][1] = vec.y;
self.columns[2][2] = scale.z; self.columns[2][2] = vec.z;
return self; return self;
} }
@ -352,6 +352,18 @@ pub const Mat4 = extern struct {
return self; return self;
} }
pub fn translate(self: *Mat4, x: f32, y: f32, z: f32) void {
self.columns[3][0] += x;
self.columns[3][1] += y;
self.columns[3][2] += z;
}
pub fn scale(self: *Mat4, x: f32, y: f32, z: f32) void {
self.columns[0][0] *= x;
self.columns[1][1] *= y;
self.columns[2][2] *= z;
}
pub fn asArray(self: *Mat4) []f32 { pub fn asArray(self: *Mat4) []f32 {
const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(&self.columns)))); const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(&self.columns))));
return ptr[0..16]; return ptr[0..16];
@ -392,6 +404,19 @@ pub const Rect = struct {
); );
} }
pub fn expand(self: Rect, other: Rect) Rect {
const left_edge = @min(self.left(), other.left());
const right_edge = @max(self.right(), other.right());
const top_edge = @min(self.top(), other.top());
const bottom_edge = @max(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 { pub fn left(self: Rect) f32 {
return self.pos.x; return self.pos.x;
} }
@ -431,6 +456,17 @@ pub const Rect = struct {
const y_overlap = self.pos.y <= pos.y and pos.y < self.pos.y + self.size.y; const y_overlap = self.pos.y <= pos.y and pos.y < self.pos.y + self.size.y;
return x_overlap and y_overlap; return x_overlap and y_overlap;
} }
pub fn shrink(self: Rect, margin: Vec2) Rect {
return Rect{
.pos = self.pos.add(margin),
.size = self.size.sub(margin.multiplyScalar(2))
};
}
pub fn grow(self: Rect, margin: Vec2) Rect {
return self.shrink(margin.multiplyScalar(-1));
}
}; };
pub const Line = struct { pub const Line = struct {

View File

@ -44,6 +44,7 @@ fn PlatformType(App: type) type {
last_frame_at: Io.Timestamp, last_frame_at: Io.Timestamp,
assets: Assets, assets: Assets,
last_mouse_position: ?Vec2,
input: Input, input: Input,
events_buffer: [256]Input.Event, events_buffer: [256]Input.Event,
events_overflow: bool, events_overflow: bool,
@ -108,6 +109,13 @@ fn PlatformType(App: type) type {
self.show_imgui = !self.show_imgui; self.show_imgui = !self.show_imgui;
} }
if (self.last_mouse_position != null and self.input.mouse_position != null) {
self.input.mouse_delta = self.input.mouse_position.?.sub(self.last_mouse_position.?);
} else {
self.input.mouse_delta = .init(0, 0);
}
self.last_mouse_position = self.input.mouse_position;
{ {
Gfx.beginFrame(); Gfx.beginFrame();
ImGUI.beginFrame(self.show_imgui, self.frame_arena.allocator()); ImGUI.beginFrame(self.show_imgui, self.frame_arena.allocator());
@ -140,6 +148,7 @@ fn PlatformType(App: type) type {
self.input.keyboard.released = .empty; self.input.keyboard.released = .empty;
self.input.mouse_buttons.pressed = .empty; self.input.mouse_buttons.pressed = .empty;
self.input.mouse_buttons.released = .empty; self.input.mouse_buttons.released = .empty;
self.input.mouse_scroll = .init(0, 0);
} }
fn sokolFrame(user_data: ?*anyopaque) callconv(.c) void { fn sokolFrame(user_data: ?*anyopaque) callconv(.c) void {
@ -155,68 +164,101 @@ fn PlatformType(App: type) type {
// TODO: I don't like the names of these 'event' related functions. // TODO: I don't like the names of these 'event' related functions.
// All of them "append an event", but they do different things. // All of them "append an event", but they do different things.
fn applyEventToState(self: *Self, e: Input.Event) void { fn applyEventToState(self: *Self, e: Input.Event) bool {
const input = &self.input; const input = &self.input;
switch (e) { switch (e) {
.key_pressed => |key| { .key_pressed => |key| {
assert(!input.isKeyDown(key)); if (input.isKeyDown(key)) {
return false;
}
input.keyboard.press(key); input.keyboard.press(key);
self.last_key_pressed = key; self.last_key_pressed = key;
}, },
.key_released => |key| { .key_released => |key| {
assert(input.isKeyDown(key)); if (!input.isKeyDown(key)) {
return false;
}
input.keyboard.release(key); input.keyboard.release(key);
}, },
.mouse_pressed => |button| { .mouse_pressed => |button| {
assert(input.mouse_position != null); if (input.mouse_position == null or input.isMouseDown(button)) {
assert(!input.isMouseDown(button)); return false;
}
input.mouse_buttons.press(button); input.mouse_buttons.press(button);
}, },
.mouse_released => |button| { .mouse_released => |button| {
assert(input.mouse_position != null); if (input.mouse_position == null or !input.isMouseDown(button)) {
assert(input.isMouseDown(button)); return false;
}
input.mouse_buttons.release(button); input.mouse_buttons.release(button);
}, },
.mouse_enter => |pos| { .mouse_enter => |pos| {
assert(input.mouse_position == null); if (input.mouse_position != null) {
return false;
}
input.mouse_position = pos; input.mouse_position = pos;
}, },
.mouse_move => |pos| { .mouse_move => |pos| {
assert(input.mouse_position != null); if (input.mouse_position == null) {
return false;
}
input.mouse_position = pos; input.mouse_position = pos;
}, },
.mouse_leave => { .mouse_leave => {
assert(input.mouse_position != null); if (input.mouse_position == null) {
return false;
}
input.mouse_position = null; input.mouse_position = null;
}, },
.char => |char| { .char => |char| {
// A 'key_pressed' event always occurs before a 'char' event. // A 'key_pressed' event always occurs before a 'char' event.
// This allows us to differentiate between keycodes and scan codes. // This allows us to differentiate between keycodes and scan codes.
// And how to map key codes to character codes. // And how to map key codes to character codes.
assert(self.last_key_pressed != null); if (self.last_key_pressed == null) {
return false;
}
input.key_code_mapping.put(self.last_key_pressed.?, char); input.key_code_mapping.put(self.last_key_pressed.?, char);
self.last_key_pressed = null; self.last_key_pressed = null;
}, },
.mouse_scroll => |offset| { .mouse_scroll => |offset| {
assert(input.mouse_position != null); if (input.mouse_position == null) {
return false;
}
input.mouse_scroll = input.mouse_scroll.add(offset); input.mouse_scroll = input.mouse_scroll.add(offset);
}, },
.window_resize => |window_size| { .window_resize => |window_size| {
self.input.window_size = window_size; self.input.window_size = window_size;
}, },
.focused => { .focused => {
assert(!input.focused); if (input.focused) {
return false;
}
input.focused = true; input.focused = true;
}, },
.unfocused => { .unfocused => {
assert(input.focused); if (!input.focused) {
return false;
}
assert(input.mouse_position == null); assert(input.mouse_position == null);
assert(input.keyboard.down.eql(.empty)); assert(input.keyboard.down.eql(.empty));
assert(input.mouse_buttons.down.eql(.empty)); assert(input.mouse_buttons.down.eql(.empty));
input.focused = false; input.focused = false;
} }
} }
return true;
} }
fn appendEvent(self: *Self, e: Input.Event) void { fn appendEvent(self: *Self, e: Input.Event) void {
@ -228,8 +270,9 @@ fn PlatformType(App: type) type {
return; return;
} }
if (self.applyEventToState(e)) {
self.events.appendAssumeCapacity(e); self.events.appendAssumeCapacity(e);
self.applyEventToState(e); }
} }
fn pushEvent(self: *Self, e: Input.Event) void { fn pushEvent(self: *Self, e: Input.Event) void {
@ -240,16 +283,6 @@ fn PlatformType(App: type) type {
return; return;
} }
if (e == .key_pressed and input.isKeyDown(e.key_pressed)) {
return;
}
if (e == .key_released and !input.isKeyDown(e.key_released)) {
return;
}
if (e == .mouse_move and input.mouse_position.?.eql(e.mouse_move)) {
return;
}
if (e == .unfocused) { if (e == .unfocused) {
var key_iter = input.keyboard.down.iterator(); var key_iter = input.keyboard.down.iterator();
while (key_iter.next()) |key| { while (key_iter.next()) |key| {
@ -259,10 +292,8 @@ fn PlatformType(App: type) type {
while (mouse_buttons_iter.next()) |button| { while (mouse_buttons_iter.next()) |button| {
self.appendEvent(.{ .mouse_released = button }); self.appendEvent(.{ .mouse_released = button });
} }
if (input.mouse_position != null) {
self.appendEvent(.{ .mouse_leave = {} }); self.appendEvent(.{ .mouse_leave = {} });
} }
}
} else { } else {
if (e == .unfocused) { if (e == .unfocused) {
@ -372,6 +403,11 @@ fn PlatformType(App: type) type {
.unfocused = {} .unfocused = {}
}); });
}, },
.RESIZED => {
self.pushEvent(.{
.window_resize = .initFromInt(i32, e.window_width, e.window_height)
});
},
else => {} else => {}
} }
@ -566,7 +602,8 @@ pub fn run(App: type, opts: RunOptions) void {
.last_key_pressed = null, .last_key_pressed = null,
.frame_arena = .init(gpa), .frame_arena = .init(gpa),
.show_imgui = builtin.mode == .Debug, .show_imgui = builtin.mode == .Debug,
.assets = .init(gpa, io, assets_dir) .assets = .init(gpa, io, assets_dir),
.last_mouse_position = null
}; };
inline for (EmbeddedAssets.files) |file| { inline for (EmbeddedAssets.files) |file| {

View File

@ -78,10 +78,7 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
}; };
pub fn clearRetainingCapacity(self: *Self) void { pub fn clearRetainingCapacity(self: *Self) void {
var slots = self.slots; self.* = .init(self.slots.items);
slots.clearRetainingCapacity();
self.* = .init(slots);
} }
fn insertHole(self: *Self, index: Index) void { fn insertHole(self: *Self, index: Index) void {
@ -121,19 +118,6 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
return null; return null;
} }
pub fn ensureUnusedCapacity(self: *Self, gpa: Allocator, additional_count: usize) Allocator.Error!void {
if (additional_count > self.hole_count) {
const new_capacity, const overflow = @addWithOverflow(
self.slots.capacity,
additional_count - self.hole_count
);
if (overflow != 0) return error.OutOfMemory;
if (new_capacity >= std.math.maxInt(Index)) return error.OutOfMemory;
try self.slots.ensureTotalCapacity(gpa, new_capacity);
}
}
pub fn unusedCapacity(self: *Self) usize { pub fn unusedCapacity(self: *Self) usize {
const capacity = @min(self.slots.capacity, std.math.maxInt(Index)); const capacity = @min(self.slots.capacity, std.math.maxInt(Index));
return capacity - self.slots.items.len + self.hole_count; return capacity - self.slots.items.len + self.hole_count;
@ -151,18 +135,19 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
}; };
} }
pub fn insert(self: *Self, gpa: Allocator) Allocator.Error!Id { pub fn insertUndefined(self: *Self) Allocator.Error!Id {
try self.ensureUnusedCapacity(gpa, 1);
return self.insertAssumeCapacity();
}
pub fn insertBounded(self: *Self) Allocator.Error!Id {
if (self.unusedCapacity() == 0) { if (self.unusedCapacity() == 0) {
return error.OutOfMemory; return error.OutOfMemory;
} }
return self.insertAssumeCapacity(); return self.insertAssumeCapacity();
} }
pub fn insert(self: *Self, value: Value) Allocator.Error!Id {
const id = try self.insertUndefined();
self.getAssumeExists(id).* = value;
return id;
}
pub fn exists(self: *Self, id: Id) bool { pub fn exists(self: *Self, id: Id) bool {
if (id.index >= self.slots.items.len) { if (id.index >= self.slots.items.len) {
return false; return false;
@ -211,15 +196,11 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type {
}; };
} }
pub fn init(slots: std.ArrayList(Slot)) Self { pub fn init(slots: []Slot) Self {
var self: Self = .empty; var self: Self = .empty;
self.slots = slots; self.slots = .initBuffer(slots);
return self; return self;
} }
pub fn deinit(self: *Self, gpa: Allocator) void {
self.slots.deinit(gpa);
}
}; };
} }
@ -234,13 +215,13 @@ test "insert & remove" {
var map: TestMap = .empty; var map: TestMap = .empty;
defer map.deinit(gpa); defer map.deinit(gpa);
const id1 = try map.insert(gpa); const id1 = try map.insertUndefined(gpa);
try expect(map.exists(id1)); try expect(map.exists(id1));
try expect(map.remove(id1)); try expect(map.remove(id1));
try expect(!map.exists(id1)); try expect(!map.exists(id1));
try expect(!map.remove(id1)); try expect(!map.remove(id1));
const id2 = try map.insert(gpa); const id2 = try map.insertUndefined(gpa);
try expect(map.exists(id2)); try expect(map.exists(id2));
try expect(!map.exists(id1)); try expect(!map.exists(id1));
} }
@ -253,14 +234,14 @@ test "generation wrap around" {
defer map.deinit(gpa); defer map.deinit(gpa);
// Grow array list so that at least 1 slot exists // Grow array list so that at least 1 slot exists
const id1 = try map.insert(gpa); const id1 = try map.insertUndefined(gpa);
map.removeAssumeExists(id1); map.removeAssumeExists(id1);
// Artificially increase generation count // Artificially increase generation count
map.slots.items[id1.index].generation = std.math.maxInt(@FieldType(TestMap.Id, "generation")); map.slots.items[id1.index].generation = std.math.maxInt(@FieldType(TestMap.Id, "generation"));
// Check if generation wraps around // Check if generation wraps around
const id2 = try map.insert(gpa); const id2 = try map.insertUndefined(gpa);
map.removeAssumeExists(id2); map.removeAssumeExists(id2);
try expectEqual(id1.index, id2.index); try expectEqual(id1.index, id2.index);
try expectEqual(0, map.slots.items[id1.index].generation); try expectEqual(0, map.slots.items[id1.index].generation);
@ -274,9 +255,9 @@ test "iterator" {
defer map.deinit(gpa); defer map.deinit(gpa);
// Create array which has a hole // Create array which has a hole
const id1 = try map.insert(gpa); const id1 = try map.insertUndefined(gpa);
const id2 = try map.insert(gpa); const id2 = try map.insertUndefined(gpa);
const id3 = try map.insert(gpa); const id3 = try map.insertUndefined(gpa);
map.removeAssumeExists(id2); map.removeAssumeExists(id2);
@ -294,11 +275,11 @@ test "clear retaining capacity" {
var map: TestMap = .empty; var map: TestMap = .empty;
defer map.deinit(gpa); defer map.deinit(gpa);
const id1 = try map.insert(gpa); const id1 = try map.insertUndefined(gpa);
try expect(map.exists(id1)); try expect(map.exists(id1));
map.clearRetainingCapacity(); map.clearRetainingCapacity();
const id2 = try map.insert(gpa); const id2 = try map.insertUndefined(gpa);
try expect(map.exists(id2)); try expect(map.exists(id2));
try expectEqual(id1, id2); try expectEqual(id1, id2);