From ef5275b17feb5dcdbf9b0090e171a211a79440d1 Mon Sep 17 00:00:00 2001 From: Rokas Puzonas Date: Mon, 10 Aug 2026 00:52:26 +0300 Subject: [PATCH] cherry pick changes from gamejam --- src/color.zig | 12 + src/graphics.zig | 679 +++++++++++++++++++++++++++++++++++++---------- src/imgui.zig | 266 +++++++++++++++---- src/input.zig | 4 +- src/math.zig | 44 ++- src/platform.zig | 97 ++++--- src/slot_map.zig | 57 ++-- 7 files changed, 890 insertions(+), 269 deletions(-) diff --git a/src/color.zig b/src/color.zig index 007f842..524f3c0 100644 --- a/src/color.zig +++ b/src/color.zig @@ -11,6 +11,9 @@ a: f32, pub const black = rgb(0, 0, 0); pub const white = rgb(255, 255, 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 { 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; 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), + }; +} diff --git a/src/graphics.zig b/src/graphics.zig index 4e9639e..487e3f6 100644 --- a/src/graphics.zig +++ b/src/graphics.zig @@ -39,11 +39,14 @@ const State = struct { nearest_sampler: sg.Sampler, default_sampler: Sampler, - quads_buffer: [2048]Quad, - quads: std.ArrayList(Quad), + quads_buffer: [8192]VertexQuad, + quads: std.ArrayList(VertexQuad), clear_color: Color, + transforms_buffer: [32]TransformFrame, + transforms: std.ArrayList(TransformFrame) = .empty, + textures_buffer: [Texture.max_textures]Texture.SlotMap.Slot, textures: Texture.SlotMap, 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; bindings.vertex_buffers[0] = sg.makeBuffer(.{ - .size = @sizeOf(Quad) * max_quads, + .size = @sizeOf(VertexQuad) * max_quads, .usage = .{ .vertex_buffer = true, .stream_update = true }, .label = "quad-vertices" }); @@ -163,22 +166,25 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { .linear_sampler = linear_sampler, .nearest_sampler = nearest_sampler, - .default_sampler = .linear, + .default_sampler = .nearest, + + .transforms_buffer = undefined, + .transforms = .initBuffer(&self.transforms_buffer), .textures_buffer = undefined, - .textures = .init(.initBuffer(&self.textures_buffer)), + .textures = .init(&self.textures_buffer), .nil_texture = undefined, .sprites_buffer = undefined, - .sprites = .init(.initBuffer(&self.sprites_buffer)), + .sprites = .init(&self.sprites_buffer), .nil_sprite = undefined, .spritesheets_buffer = undefined, - .spritesheets = .init(.initBuffer(&self.spritesheets_buffer)), + .spritesheets = .init(&self.spritesheets_buffer), .nil_spritesheet = undefined, .fonts_buffer = undefined, - .fonts = .init(.initBuffer(&self.fonts_buffer)), + .fonts = .init(&self.fonts_buffer), .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; @memset(&pixels, 0xFF); - self.default_sprite = initSprite(.{}); + self.default_sprite = initSprite(.{ .padding = 0 }); setSprite(self.default_sprite, .{ .image = ImageData{ .width = width, @@ -272,7 +278,7 @@ pub fn init(gpa: std.mem.Allocator, logger: sg.Logger) !void { } } - self.error_sprite = initSprite(.{}); + self.error_sprite = initSprite(.{ }); setSprite(self.error_sprite, .{ .image = .{ .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 { const self = &g_state; + self.transforms.clearRetainingCapacity(); + self.transforms.appendAssumeCapacity(TransformFrame{ + .offset = .init(0, 0), + .scale = .init(1, 1), + }); + var pass_action: sg.PassAction = .{}; pass_action.colors[0] = .{ .load_action = .CLEAR, @@ -462,36 +521,87 @@ pub fn endFrame() void { while (spritesheet_iter.next()) |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 { - const self = &g_state; - - drawSprite(self.default_sprite, pos, size, color); + drawQuad(Quad.initRect(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 step = to.sub(from).normalized().multiplyScalar(width/2); - const top_left = from.add(step.rotateLeft90()); - const bottom_left = from.add(step.rotateRight90()); + 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()); - var quad: Quad = undefined; - quad.setColor(color); - quad.setUVRect(.init(0, 0, 1, 1)); - quad.vertices[0] = top_right; - quad.vertices[1] = top_left; - quad.vertices[2] = bottom_right; - quad.vertices[3] = bottom_left; - - draw(.{ - .texture = self.default_sprite, - .quad = quad - }); + drawSpriteQuad( + self.default_sprite, + .init(.{ top_right, top_left, bottom_right, bottom_left }), + color + ); } const TextureOptions = struct { @@ -500,7 +610,7 @@ const TextureOptions = struct { pub fn initTexture(opts: TextureOptions) Texture.Id { 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}); 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 { - var quad: Quad = undefined; + var quad: VertexQuad = undefined; quad.setColor(color); quad.setRect(.{ .pos = pos, .size = size }); quad.setUVRect(.init(0, 0, 1, 1)); @@ -689,7 +799,7 @@ pub const Sampler = enum { const DrawOptions = struct { sampler: ?Sampler = null, texture: Texture.Id, - quad: Quad + quad: VertexQuad }; pub fn draw(opts: DrawOptions) void { @@ -738,6 +848,12 @@ pub fn draw(opts: DrawOptions) void { flush(.{}); } + if (getTransformPtr()) |transform| { + for (&quad.vertices) |*vertex| { + vertex.position = transform.apply(vertex.position); + } + } + self.quads.appendAssumeCapacity(quad); } @@ -754,7 +870,7 @@ pub fn getNilSprite() Sprite.Id { pub fn initSprite(opts: SpriteOptions) Sprite.Id { 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}); return self.nil_sprite; }; @@ -1055,29 +1171,37 @@ pub fn getSpriteUVRect(id: Sprite.Id) ?Rect { assert(sprite.position != null); + const sprite_size = Vec2.initFromInt(u32, sprite_data.width, sprite_data.height); - return Rect{ + var uv_rect = Rect{ .pos = sprite.position.?.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; - var quad: Quad = undefined; + var quad: VertexQuad = undefined; 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; if (id != self.nil_sprite) { if (getSpriteUVRect(id)) |sprite_uv| { quad.setUVRect(sprite_uv); - if (self.sprites.get(id)) |sprite| { - if (self.spritesheets.get(sprite.spritesheet)) |spritesheet| { - texture = spritesheet.texture; - } - } + + const sprite = self.sprites.getAssumeExists(id); + const spritesheet = self.spritesheets.getAssumeExists(sprite.spritesheet); + texture = spritesheet.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 { format: ImageData.Format = .rgba8 }; pub fn initSpritesheet(opts: SpriteSheetOptions) Spritesheet.Id { 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}); return self.nil_spritesheet; }; @@ -1127,7 +1256,7 @@ pub fn deinitSpritesheet(id: Spritesheet.Id) void { pub fn initFont() Font.Id { 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}); return self.nil_font; }; @@ -1183,116 +1312,327 @@ pub fn setFont(id: Font.Id, ttf_data: [:0]const u8) void { font.cache = .init(); } -const DrawTextOptions = struct { +fn getFont(id: Font.Id) ?*Font { + const self = &g_state; + if (id == self.nil_font) { + return null; + } + + return self.fonts.get(id); +} + +pub fn getGlyphIndex(id: Font.Id, codepoint: u21) ?GlyphIndex { + const font = getFont(id) orelse return null; + const stb_font = font.stb orelse return null; + + if (stb_font.findGlyphIndex(codepoint)) |index| { + return index; + } 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 scale = stb_font.scaleForPixelHeight(height); + return @as(f32, @floatFromInt(vmetrics.ascent)) * scale; +} + +pub fn getGlyph(id: Font.Id, index: GlyphIndex, scale_x: f32, scale_y: f32) ?*Font.Glyph { + const self = &g_state; + const font = getFont(id) orelse return null; + const stb_font = font.stb orelse return null; + + // TODO: Allow specifying a different scale for x and y + const glyph_key = Font.Glyph.Key{ + .index = index, + .scale_x = scale_x, + .scale_y = scale_y + }; + + var glyph: *Font.Glyph = undefined; + if (font.cache.lookup(glyph_key)) |glyph_id| { + return font.cache.get(glyph_id); + } else { + const glyph_id = font.cache.insert(glyph_key) orelse { + @panic("TODO: render glyph lru_tail"); + }; + glyph = font.cache.get(glyph_id); + glyph.box = stb_font.getGlyphBitmapBox(glyph_key.index, glyph_key.scale_x, glyph_key.scale_y); + + const box_width: u32 = @intCast(glyph.box.x1 - glyph.box.x0); + const box_height: u32 = @intCast(glyph.box.y1 - glyph.box.y0); + if (box_width > 0 and box_height > 0) { + assert(glyph.sprite == self.nil_sprite); + glyph.sprite = initSprite(.{ .spritesheet = font.spritesheet, .padding = 1 }); + + assert(box_width < Font.Glyph.max_width); + assert(box_height < Font.Glyph.max_height); + + var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined; + const bitmap = ImageData{ + .pixels = .{ .r8 = (&bitmap_buffer).ptr }, + .width = box_width, + .height = box_height + }; + stb_font.makeGlyphBitmap( + bitmap.pixels.?.r8, + box_width, box_height, + box_width, + glyph_key.scale_x, glyph_key.scale_y, + glyph_key.index + ); + setSprite(glyph.sprite, .{ .image = bitmap }); + } else { + glyph.sprite = self.nil_sprite; + } + } + glyph.used_this_frame = true; + + return glyph; +} + +pub const TextRunLayout = struct { + font: Font.Id, + scale_x: f32, + 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 glyph_size = Vec2.initFromInt(i32, glyph.box.x1 - glyph.box.x0, glyph.box.y1 - glyph.box.y0); + + 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 + color: Color = .white, + alignment: Vec2 = .init(0, 0) }; pub fn drawText(id: Font.Id, opts: DrawTextOptions) void { const self = &g_state; - if (id == self.nil_font) { - return; + + 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)); } - const font = self.fonts.get(id) orelse { - log.warn("Attempt to draw text with font that doesn't exist: {}", .{id}); - return; + var iter = std.unicode.Utf8Iterator{ + .bytes = opts.text, + .i = 0 }; - 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 { - log.warn("Failed to find fallback glyph '?'", .{}); - return; - }; - - const scale = stb_font.scaleForPixelHeight(opts.height); - const scale_x = scale; - const scale_y = scale; - - const vmetrics = stb_font.getFontVMetrics(); - const baseline = @as(f32, @floatFromInt(vmetrics.ascent)) * scale; - - var current_point = opts.pos; - current_point.y += baseline; - - var prev_glyph: ?u32 = null; - var cursor: usize = 0; - while (cursor < opts.text.len) { - const codepoint_len = std.unicode.utf8ByteSequenceLength(opts.text[cursor]) catch |e| { - log.err("Failed to draw text, invalid codepoint at {} in '{s}': {}", .{cursor, opts.text, e}); - 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; - if (font.cache.lookup(glyph_key)) |glyph_id| { - glyph = font.cache.get(glyph_id); - } else { - const glyph_id = font.cache.insert(glyph_key) orelse { - @panic("TODO: render glyph lru_tail"); - }; - glyph = font.cache.get(glyph_id); - 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 height: u32 = @intCast(glyph.box.y1 - glyph.box.y0); - if (width > 0 and height > 0) { - assert(glyph.sprite == self.nil_sprite); - glyph.sprite = initSprite(.{ .spritesheet = font.spritesheet, .padding = 1 }); - - assert(width < Font.Glyph.max_width); - assert(height < Font.Glyph.max_height); - - var bitmap_buffer: [Font.Glyph.max_width * Font.Glyph.max_height]u8 = undefined; - const bitmap = ImageData{ - .pixels = .{ .r8 = (&bitmap_buffer).ptr }, - .width = width, - .height = height - }; - stb_font.makeGlyphBitmap( - bitmap.pixels.?.r8, - width, height, - width, - glyph_key.scale_x, glyph_key.scale_y, - glyph_key.index - ); - setSprite(glyph.sprite, .{ .image = bitmap }); - } else { - glyph.sprite = self.nil_sprite; - } - } - glyph.used_this_frame = true; - - if (prev_glyph != null) { - const kerning = stb_font.getGlyphKernAdvance(prev_glyph.?, glyph_index); - current_point.x += @as(f32, @floatFromInt(kerning)) * scale; + 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; } - if (glyph.sprite != self.nil_sprite) { - const glyph_size = Vec2.initFromInt(i32, glyph.box.x1 - glyph.box.x0, glyph.box.y1 - glyph.box.y0); - const glyph_pos = current_point.sub(.initFromInt(i32, glyph.box.x0, -glyph.box.y0)); - drawSprite(glyph.sprite, glyph_pos, glyph_size, opts.color); - } - - const hmetrics = stb_font.getGlyphHMetrics(glyph_index); - current_point.x += @as(f32, @floatFromInt(hmetrics.advance_width)) * scale; - prev_glyph = glyph_index; + 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 { const Format = enum { 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; if (self.pixels) |pixels| { return switch (pixels) { @@ -1482,7 +1822,7 @@ pub const Sprite = struct { position: ?Vec2, padding: u32, - const max_sprites = 128; + const max_sprites = 256; const SlotMap = SlotMapType(std.math.IntFittingRange(0, max_sprites-1), u8, Sprite); pub const Id = SlotMap.Id; }; @@ -1912,7 +2252,10 @@ pub const Font = struct { const max_width: u32 = 512; const max_height: u32 = 512; + const Index = u32; + const Key = packed struct { + // TODO: Quantize `scale_x` and `scale_y`. index: u32, scale_x: f32, scale_y: f32, @@ -1944,22 +2287,24 @@ pub const Font = struct { }; }; +const GlyphIndex = Font.Glyph.Index; + pub const Vertex = extern struct { position: Vec2, texcoord: Vec2, color: Vec4, }; -pub const Quad = extern struct { +pub const VertexQuad = extern struct { vertices: [4]Vertex, - pub fn init(vertices: [4]Vertex) Quad { - return Quad{ + pub fn init(vertices: [4]Vertex) VertexQuad { + return VertexQuad{ .vertices = vertices }; } - pub fn setColor(self: *Quad, color: Color) void { + pub fn setColor(self: *VertexQuad, color: Color) void { for (&self.vertices) |*vertex| { vertex.color = Vec4{ .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 size = rect.size; self.vertices[0].texcoord = pos; @@ -1979,7 +2324,7 @@ pub const Quad = extern struct { 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 size = rect.size; self.vertices[0].position = pos; @@ -1988,3 +2333,45 @@ pub const Quad = extern struct { 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); + } +}; diff --git a/src/imgui.zig b/src/imgui.zig index cbe58f8..54ed204 100644 --- a/src/imgui.zig +++ b/src/imgui.zig @@ -15,6 +15,8 @@ const simgui = sokol.imgui; const ig = @import("cimgui"); +const Nanoseconds = @import("./platform.zig").Nanoseconds; + const ImGUI = @This(); const State = struct { @@ -130,16 +132,16 @@ pub fn endFrame() void { ig.igEnd(); - if (ig.igBeginMainMenuBar()) { - defer ig.igEndMainMenuBar(); - - if (ig.igBeginMenu("foo")) { - defer ig.igEndMenu(); - - if (ig.igMenuItem("bar")) { - } - } - } + // if (ig.igBeginMainMenuBar()) { + // defer ig.igEndMainMenuBar(); + // + // if (ig.igBeginMenu("foo")) { + // defer ig.igEndMenu(); + // + // if (ig.igMenuItem("bar")) { + // } + // } + // } simgui.render(); } @@ -150,14 +152,16 @@ pub const WindowOptions = struct { size: ?Vec2 = null, collapsed: ?bool = null, open: ?*bool = null, + alpha: f32 = 1 }; -pub fn beginWindow(opts: WindowOptions) bool { - if (!build_options.has_imgui) { - return false; - } +inline fn isDisabled() bool { 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; } @@ -175,7 +179,7 @@ pub fn beginWindow(opts: WindowOptions) bool { ig.igSetNextWindowCollapsed(collapsed, ig.ImGuiCond_Once); } - ig.igSetNextWindowBgAlpha(1); + ig.igSetNextWindowBgAlpha(opts.alpha); const frame = g_state.frame.?; const namez = frame.dupeSentinel(u8, opts.name, 0) catch blk: { @@ -197,11 +201,7 @@ pub fn beginWindow(opts: WindowOptions) bool { } pub fn endWindow() void { - if (!build_options.has_imgui) { - return; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { 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 { - if (!build_options.has_imgui) { - return; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { return; } @@ -229,11 +225,7 @@ pub fn text(comptime fmt: []const u8, args: anytype) void { } pub fn button(label: []const u8) bool { - if (!build_options.has_imgui) { - return false; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { return false; } @@ -248,11 +240,7 @@ pub const SliderOptions = struct { }; pub fn slider(opts: SliderOptions) bool { - if (!build_options.has_imgui) { - return false; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { return false; } @@ -264,12 +252,115 @@ pub fn slider(opts: SliderOptions) bool { ); } -pub fn checkbox(label: []const u8, value: *bool) bool { - if (!build_options.has_imgui) { +const InputF32Options = struct { + 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; } - 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; } @@ -280,11 +371,7 @@ pub fn checkbox(label: []const u8, value: *bool) bool { } pub fn separator() void { - if (!build_options.has_imgui) { - return; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { return; } @@ -298,11 +385,7 @@ const ColorEditOptions = struct { }; pub fn colorEdit(opts: ColorEditOptions) bool { - if (!build_options.has_imgui) { - return false; - } - const self = &g_state; - if (!self.enabled) { + if (isDisabled()) { return false; } @@ -337,3 +420,86 @@ fn toImVec2(vec2: Vec2) ig.ImVec2 { .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 + ); +} diff --git a/src/input.zig b/src/input.zig index dcd58c4..4b746da 100644 --- a/src/input.zig +++ b/src/input.zig @@ -15,6 +15,7 @@ focused: bool, keyboard: KeyStateType(KeyCode), mouse_buttons: KeyStateType(MouseButton), mouse_position: ?Vec2, +mouse_delta: Vec2, mouse_scroll: Vec2, pub fn init(window_size: Vec2) Input { @@ -24,6 +25,7 @@ pub fn init(window_size: Vec2) Input { .keyboard = .empty, .mouse_buttons = .empty, .mouse_position = null, + .mouse_delta = .init(0, 0), .mouse_scroll = .init(0, 0), .window_size = window_size }; @@ -45,7 +47,7 @@ pub fn isMousePressed(self: Input, button: MouseButton) bool { 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); } diff --git a/src/math.zig b/src/math.zig index abe04e5..037b6fa 100644 --- a/src/math.zig +++ b/src/math.zig @@ -336,11 +336,11 @@ pub const Mat4 = extern struct { return self; } - pub fn initScale(scale: Vec3) Mat4 { + pub fn initScale(vec: Vec3) Mat4 { var self = Mat4.initIdentity(); - self.columns[0][0] = scale.x; - self.columns[1][1] = scale.y; - self.columns[2][2] = scale.z; + self.columns[0][0] = vec.x; + self.columns[1][1] = vec.y; + self.columns[2][2] = vec.z; return self; } @@ -352,6 +352,18 @@ pub const Mat4 = extern struct { 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 { const ptr: [*]f32 = @alignCast(@ptrCast(@as(*anyopaque, @ptrCast(&self.columns)))); 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 { 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; 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 { diff --git a/src/platform.zig b/src/platform.zig index 5d6b26f..85e309f 100644 --- a/src/platform.zig +++ b/src/platform.zig @@ -44,6 +44,7 @@ fn PlatformType(App: type) type { last_frame_at: Io.Timestamp, assets: Assets, + last_mouse_position: ?Vec2, input: Input, events_buffer: [256]Input.Event, events_overflow: bool, @@ -108,6 +109,13 @@ fn PlatformType(App: type) type { 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(); 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.mouse_buttons.pressed = .empty; self.input.mouse_buttons.released = .empty; + self.input.mouse_scroll = .init(0, 0); } 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. // 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; switch (e) { .key_pressed => |key| { - assert(!input.isKeyDown(key)); + if (input.isKeyDown(key)) { + return false; + } + input.keyboard.press(key); self.last_key_pressed = key; }, .key_released => |key| { - assert(input.isKeyDown(key)); + if (!input.isKeyDown(key)) { + return false; + } + input.keyboard.release(key); }, .mouse_pressed => |button| { - assert(input.mouse_position != null); - assert(!input.isMouseDown(button)); + if (input.mouse_position == null or input.isMouseDown(button)) { + return false; + } + input.mouse_buttons.press(button); }, .mouse_released => |button| { - assert(input.mouse_position != null); - assert(input.isMouseDown(button)); + if (input.mouse_position == null or !input.isMouseDown(button)) { + return false; + } + input.mouse_buttons.release(button); }, .mouse_enter => |pos| { - assert(input.mouse_position == null); + if (input.mouse_position != null) { + return false; + } + input.mouse_position = pos; }, .mouse_move => |pos| { - assert(input.mouse_position != null); + if (input.mouse_position == null) { + return false; + } + input.mouse_position = pos; }, .mouse_leave => { - assert(input.mouse_position != null); + if (input.mouse_position == null) { + return false; + } + input.mouse_position = null; }, .char => |char| { // A 'key_pressed' event always occurs before a 'char' event. // This allows us to differentiate between keycodes and scan 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); self.last_key_pressed = null; }, .mouse_scroll => |offset| { - assert(input.mouse_position != null); + if (input.mouse_position == null) { + return false; + } + input.mouse_scroll = input.mouse_scroll.add(offset); }, .window_resize => |window_size| { self.input.window_size = window_size; }, .focused => { - assert(!input.focused); + if (input.focused) { + return false; + } + input.focused = true; }, .unfocused => { - assert(input.focused); + if (!input.focused) { + return false; + } + assert(input.mouse_position == null); assert(input.keyboard.down.eql(.empty)); assert(input.mouse_buttons.down.eql(.empty)); input.focused = false; } } + + return true; } fn appendEvent(self: *Self, e: Input.Event) void { @@ -228,8 +270,9 @@ fn PlatformType(App: type) type { return; } - self.events.appendAssumeCapacity(e); - self.applyEventToState(e); + if (self.applyEventToState(e)) { + self.events.appendAssumeCapacity(e); + } } fn pushEvent(self: *Self, e: Input.Event) void { @@ -240,16 +283,6 @@ fn PlatformType(App: type) type { 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) { var key_iter = input.keyboard.down.iterator(); while (key_iter.next()) |key| { @@ -259,9 +292,7 @@ fn PlatformType(App: type) type { while (mouse_buttons_iter.next()) |button| { self.appendEvent(.{ .mouse_released = button }); } - if (input.mouse_position != null) { - self.appendEvent(.{ .mouse_leave = {} }); - } + self.appendEvent(.{ .mouse_leave = {} }); } } else { @@ -372,6 +403,11 @@ fn PlatformType(App: type) type { .unfocused = {} }); }, + .RESIZED => { + self.pushEvent(.{ + .window_resize = .initFromInt(i32, e.window_width, e.window_height) + }); + }, else => {} } @@ -566,7 +602,8 @@ pub fn run(App: type, opts: RunOptions) void { .last_key_pressed = null, .frame_arena = .init(gpa), .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| { diff --git a/src/slot_map.zig b/src/slot_map.zig index 5eb1cd2..0ee00cb 100644 --- a/src/slot_map.zig +++ b/src/slot_map.zig @@ -78,10 +78,7 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type { }; pub fn clearRetainingCapacity(self: *Self) void { - var slots = self.slots; - slots.clearRetainingCapacity(); - - self.* = .init(slots); + self.* = .init(self.slots.items); } fn insertHole(self: *Self, index: Index) void { @@ -121,19 +118,6 @@ pub fn SlotMapType(Index: type, Generation: type, Value: type) type { 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 { const capacity = @min(self.slots.capacity, std.math.maxInt(Index)); 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 { - try self.ensureUnusedCapacity(gpa, 1); - return self.insertAssumeCapacity(); - } - - pub fn insertBounded(self: *Self) Allocator.Error!Id { + pub fn insertUndefined(self: *Self) Allocator.Error!Id { if (self.unusedCapacity() == 0) { return error.OutOfMemory; } 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 { if (id.index >= self.slots.items.len) { 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; - self.slots = slots; + self.slots = .initBuffer(slots); 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; defer map.deinit(gpa); - const id1 = try map.insert(gpa); + const id1 = try map.insertUndefined(gpa); try expect(map.exists(id1)); try expect(map.remove(id1)); try expect(!map.exists(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(id1)); } @@ -253,14 +234,14 @@ test "generation wrap around" { defer map.deinit(gpa); // 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); // Artificially increase generation count map.slots.items[id1.index].generation = std.math.maxInt(@FieldType(TestMap.Id, "generation")); // Check if generation wraps around - const id2 = try map.insert(gpa); + const id2 = try map.insertUndefined(gpa); map.removeAssumeExists(id2); try expectEqual(id1.index, id2.index); try expectEqual(0, map.slots.items[id1.index].generation); @@ -274,9 +255,9 @@ test "iterator" { defer map.deinit(gpa); // Create array which has a hole - const id1 = try map.insert(gpa); - const id2 = try map.insert(gpa); - const id3 = try map.insert(gpa); + const id1 = try map.insertUndefined(gpa); + const id2 = try map.insertUndefined(gpa); + const id3 = try map.insertUndefined(gpa); map.removeAssumeExists(id2); @@ -294,11 +275,11 @@ test "clear retaining capacity" { var map: TestMap = .empty; defer map.deinit(gpa); - const id1 = try map.insert(gpa); + const id1 = try map.insertUndefined(gpa); try expect(map.exists(id1)); map.clearRetainingCapacity(); - const id2 = try map.insert(gpa); + const id2 = try map.insertUndefined(gpa); try expect(map.exists(id2)); try expectEqual(id1, id2);