From 9c4e2125c18f963168e8a7a715bc6d6a988e508f Mon Sep 17 00:00:00 2001 From: Reuben Dunnington Date: Thu, 28 Sep 2023 22:57:06 -0400 Subject: [PATCH] fleshing out oc_str* zig bindings --- samples/zig-sample/README.md | 10 +- samples/zig-sample/src/main.c | 190 ----------------- samples/zig-sample/src/main.zig | 55 ++++- src/orca.zig | 367 +++++++++++++++++++++++++------- 4 files changed, 346 insertions(+), 276 deletions(-) delete mode 100644 samples/zig-sample/src/main.c diff --git a/samples/zig-sample/README.md b/samples/zig-sample/README.md index 51ac4f2..99b973c 100644 --- a/samples/zig-sample/README.md +++ b/samples/zig-sample/README.md @@ -8,9 +8,7 @@ zig build run These two commands build the runtime - the native host executable - and the sample as a loadable wasm library, then runs it. To only build the sample without running it, use `zig build bundle`. ### Warning -Zig integration is still in-progress and experimental. You may encounter bugs since not all the bound APIs have been tested extensively - this sample is currently the only code doing so! Additionally, not all APIs have zig coverage yet, notably: -* `oc_str8`, `oc_str16`, `oc_str32`: partial coverage -* `oc_ui`: none -* `gles`: none - -Please report any bugs you find on the Handmade discord in the #orca channel. +Zig bindings for Orca are in-progress and experimental. You may encounter bugs since not all the bound APIs have been tested extensively - this sample is currently the only code doing so! Additionally, not all APIs have zig coverage yet, notably: +* `oc_ui` +* `gles` +As more APIs get tested, there is a possibility of breaking changes. Please report any bugs you find on the Handmade discord in the #orca channel. diff --git a/samples/zig-sample/src/main.c b/samples/zig-sample/src/main.c deleted file mode 100644 index fefd5ee..0000000 --- a/samples/zig-sample/src/main.c +++ /dev/null @@ -1,190 +0,0 @@ -#include - -#include - -#define ARRAYSIZE(array) (sizeof(array) / sizeof(array[0])) - -oc_vec2 frameSize = { 100, 100 }; - -oc_surface surface; -oc_canvas canvas; -oc_font font; - -f64 lastSeconds = 0; - -oc_mat2x3 mat_rotation(f32 radians); -oc_mat2x3 mat_translation(f32 x, f32 y); -oc_mat2x3 mat_transform(f32 x, f32 y, f32 radians); -f32 minf(f32 a, f32 b); - -ORCA_EXPORT void oc_on_init(void) -{ - oc_window_set_title(OC_STR8("clock")); - oc_runtime_window_set_size((oc_vec2){ .x = 400, .y = 400 }); - - surface = oc_surface_canvas(); - canvas = oc_canvas_create(); - - { - oc_str8 filename = OC_STR8("/segoeui.ttf"); - oc_file file = oc_file_open(filename, OC_FILE_ACCESS_READ, 0); - if(oc_file_last_error(file) != OC_IO_OK) - { - oc_log_error("Couldn't open file %s\n", oc_str8_to_cstring(oc_scratch(), filename)); - } - u64 size = oc_file_size(file); - char* buffer = oc_arena_push(oc_scratch(), size); - oc_file_read(file, size, buffer); - oc_file_close(file); - - oc_unicode_range ranges[5] = { OC_UNICODE_BASIC_LATIN, - OC_UNICODE_C1_CONTROLS_AND_LATIN_1_SUPPLEMENT, - OC_UNICODE_LATIN_EXTENDED_A, - OC_UNICODE_LATIN_EXTENDED_B, - OC_UNICODE_SPECIALS }; - font = oc_font_create_from_memory(oc_str8_from_buffer(size, buffer), 5, ranges); - } - - oc_arena_clear(oc_scratch()); -} - -ORCA_EXPORT void oc_on_resize(u32 width, u32 height) -{ - frameSize.x = width; - frameSize.y = height; -} - -ORCA_EXPORT void oc_on_frame_refresh(void) -{ - const oc_str8 clock_number_strings[] = { - OC_STR8("12"), - OC_STR8("1"), - OC_STR8("2"), - OC_STR8("3"), - OC_STR8("4"), - OC_STR8("5"), - OC_STR8("6"), - OC_STR8("7"), - OC_STR8("8"), - OC_STR8("9"), - OC_STR8("10"), - OC_STR8("11"), - }; - - oc_canvas_set_current(canvas); - oc_surface_select(surface); - oc_set_color_rgba(.05, .05, .05, 1); - oc_clear(); - - const f64 timestampSecs = oc_clock_time(OC_CLOCK_DATE); - const f64 secs = fmod(timestampSecs, 60); - const f64 minutes = fmod(timestampSecs, 60 * 60) / 60; - const f64 hours = fmod(timestampSecs, 60 * 60 * 24) / (60 * 60); - const f64 hoursAs12Format = fmod(hours, 12.0); - - if(lastSeconds != floor(secs)) - { - lastSeconds = floor(secs); - oc_log_info("current time: %.0f:%.0f:%.0f", floor(hours), floor(minutes), floor(secs)); - } - - const f32 secondsRotation = (M_PI * 2) * (secs / 60.0) - (M_PI / 2); - const f32 minutesRotation = (M_PI * 2) * (minutes / 60.0) - (M_PI / 2); - const f32 hoursRotation = (M_PI * 2) * (hoursAs12Format / 12.0) - (M_PI / 2); - - const f32 centerX = frameSize.x / 2; - const f32 centerY = frameSize.y / 2; - const f32 clockRadius = minf(frameSize.x, frameSize.y) * 0.5f * 0.85f; - - const f32 DEFAULT_CLOCK_RADIUS = 260; - const f32 uiScale = clockRadius / DEFAULT_CLOCK_RADIUS; - - const f32 fontSize = 26 * uiScale; - oc_set_font(font); - oc_set_font_size(fontSize); - - // clock backing - oc_set_color_rgba(1, 1, 1, 1); - oc_circle_fill(centerX, centerY, clockRadius); - - // clock face - for(int i = 0; i < ARRAYSIZE(clock_number_strings); ++i) - { - const f32 rot = -i * ((M_PI * 2) / 12.0f) + (M_PI / 2); - const f32 sinRot = sinf(rot); - const f32 cosRot = cosf(rot); - - oc_rect textRect = oc_text_bounding_box(font, fontSize, clock_number_strings[i]); - textRect.h -= 10 * uiScale; // oc_text_bounding_box height doesn't seem to be a tight fit around the glyph - - const f32 x = cosRot * clockRadius * 0.8f - (textRect.w / 2) + centerX; - const f32 y = -sinRot * clockRadius * 0.8f + (textRect.h / 2) + centerY; - - oc_set_color_rgba(0.2, 0.2, 0.2, 1); - oc_move_to(x, y); - oc_text_outlines(clock_number_strings[i]); - oc_fill(); - } - - oc_matrix_push(mat_transform(centerX, centerY, hoursRotation)); - { - oc_set_color_rgba(.2, 0.2, 0.2, 1); - oc_rounded_rectangle_fill(0, -7.5 * uiScale, clockRadius * 0.5f, 15 * uiScale, 5 * uiScale); - - oc_matrix_pop(); - } - - oc_matrix_push(mat_transform(centerX, centerY, minutesRotation)); - { - oc_set_color_rgba(.2, 0.2, 0.2, 1); - oc_rounded_rectangle_fill(0, -5 * uiScale, clockRadius * 0.7f, 10 * uiScale, 5 * uiScale); - - oc_matrix_pop(); - } - - oc_matrix_push(mat_transform(centerX, centerY, secondsRotation)); - { - oc_set_color_rgba(1, 0.2, 0.2, 1); - oc_rounded_rectangle_fill(0, -2.5 * uiScale, clockRadius * 0.8f, 5 * uiScale, 5 * uiScale); - - oc_matrix_pop(); - } - - oc_set_color_rgba(.2, 0.2, 0.2, 1); - oc_circle_fill(centerX, centerY, 10 * uiScale); - - oc_render(surface, canvas); - oc_surface_present(surface); -} - -oc_mat2x3 mat_rotation(f32 radians) -{ - const f32 sinRot = sinf(radians); - const f32 cosRot = cosf(radians); - oc_mat2x3 rot = { - cosRot, -sinRot, 0, - sinRot, cosRot, 0 - }; - return rot; -} - -oc_mat2x3 mat_translation(f32 x, f32 y) -{ - oc_mat2x3 translation = { - 1, 0, x, - 0, 1, y - }; - return translation; -} - -oc_mat2x3 mat_transform(f32 x, f32 y, f32 radians) -{ - oc_mat2x3 rotation = mat_rotation(radians); - oc_mat2x3 translation = mat_translation(x, y); - return oc_mat2x3_mul_m(translation, rotation); -} - -f32 minf(f32 a, f32 b) -{ - return (a < b) ? a : b; -} diff --git a/samples/zig-sample/src/main.zig b/samples/zig-sample/src/main.zig index eb47cc6..8297124 100644 --- a/samples/zig-sample/src/main.zig +++ b/samples/zig-sample/src/main.zig @@ -3,6 +3,7 @@ const oc = @import("orca"); const Vec2 = oc.Vec2; const Mat2x3 = oc.Mat2x3; +const Str8 = oc.Str8; var surface: oc.Surface = undefined; var canvas: oc.Canvas = undefined; @@ -27,7 +28,7 @@ export fn oc_on_init() void { oc.assert(oc.Canvas.nil().isNil() == true, "nil canvas should be nil", .{}, @src()); oc.assert(canvas.isNil() == false, "created canvas should not be nil", .{}, @src()); - var ranges = oc.UnicodeRange.range(&[_]oc.UnicodeRange.Enum{ + const ranges = oc.UnicodeRange.range(&[_]oc.UnicodeRange.Enum{ .BasicLatin, .C1ControlsAndLatin1Supplement, .LatinExtendedA, @@ -37,7 +38,7 @@ export fn oc_on_init() void { font = oc.Font.createFromPath("/zig.ttf", &ranges); oc.assert(font.isNil() == false, "created font should not be nil", .{}, @src()); - orca_image = oc.Image.createFromPath(surface, oc.Str8.fromSlice("/orca_jumping.jpg"), false); + orca_image = oc.Image.createFromPath(surface, Str8.fromSlice("/orca_jumping.jpg"), false); oc.assert(orca_image.isNil() == false, "created image should not be nil", .{}, @src()); } @@ -129,14 +130,28 @@ export fn oc_on_frame_refresh() void { } { - const str = oc.Str8.fromSlice("Hello from Zig!"); + var scratch_scope = oc.Arena.scratchBegin(); + defer scratch_scope.end(); + + var scratch: *oc.Arena = scratch_scope.arena; + + var str1: Str8 = Str8.collate(scratch, &[_][]const u8{ "Hello", "from", "Zig!" }, ">> ", " ", " <<") catch |e| fatal(e, @src()); + + var str2_list = oc.Str8List.init(); + var tmp = Str8.fromSlice("All"); + str2_list.push(tmp, scratch) catch |e| fatal(e, @src()); + str2_list.pushSlice("your", scratch) catch |e| fatal(e, @src()); + str2_list.pushSlice("base!!", scratch) catch |e| fatal(e, @src()); + var str2: Str8 = str2_list.collate(scratch, Str8.fromSlice("<< "), Str8.fromSlice("-"), Str8.fromSlice(" >>")) catch |e| fatal(e, @src()); + const font_size = 18; - const text_rect = font.textMetrics(font_size, str).ink; + const text_metrics = font.textMetrics(font_size, str1); + const text_rect = text_metrics.ink; const center_x = frame_size.x / 2; const text_begin_x = center_x - text_rect.Flat.w / 2; - Mat2x3.push(Mat2x3.translate(text_begin_x, 150)); + Mat2x3.push(Mat2x3.translate(text_begin_x, 100)); defer Mat2x3.pop(); oc.setColorRgba(1.0, 0.05, 0.05, 1.0); @@ -144,7 +159,30 @@ export fn oc_on_frame_refresh() void { oc.setFont(font); oc.setFontSize(font_size); oc.moveTo(0, 0); - oc.textOutlines(str); + oc.textOutlines(str1); + oc.moveTo(0, 35); + oc.textOutlines(str2); + oc.fill(); + } + + { + var scratch_scope = oc.Arena.scratchBegin(); + defer scratch_scope.end(); + + var scratch: *oc.Arena = scratch_scope.arena; + + var separators = oc.Str8List.init(); + separators.pushSlice(" ", scratch) catch |e| fatal(e, @src()); + separators.pushSlice("|", scratch) catch |e| fatal(e, @src()); + separators.pushSlice("-", scratch) catch |e| fatal(e, @src()); + + const big_string = Str8.fromSlice("This is |a one-word string that | has no spaces in it"); + var strings: oc.Str8List = big_string.split(scratch, separators) catch |e| fatal(e, @src()); + var collated = strings.join(scratch) catch |e| fatal(e, @src()); + + oc.setFontSize(12); + oc.moveTo(0, 170); + oc.textOutlines(collated); oc.fill(); } @@ -171,3 +209,8 @@ export fn oc_on_frame_refresh() void { export fn oc_on_terminate() void { oc.log.info("byebye {}", .{counter}, @src()); } + +fn fatal(err: anyerror, source: std.builtin.SourceLocation) noreturn { + oc.abort("Caught fatal {}", .{err}, source); + unreachable; +} diff --git a/src/orca.zig b/src/orca.zig index dff51f7..566aea4 100644 --- a/src/orca.zig +++ b/src/orca.zig @@ -1,4 +1,5 @@ const std = @import("std"); +const AllocError = std.mem.Allocator.Error; //------------------------------------------------------------------------------------------ // [PLATFORM] @@ -23,14 +24,21 @@ pub const log = struct { Info, }; - pub const Output = opaque {}; + pub const Output = opaque { + extern var OC_LOG_DEFAULT_OUTPUT: ?*Output; + extern fn oc_log_set_output(output: *Output) void; + + pub inline fn default() ?*Output { + return OC_LOG_DEFAULT_OUTPUT; + } + + const set = oc_log_set_output; + }; - extern var OC_LOG_DEFAULT_OUTPUT: ?*Output; extern fn oc_log_set_level(level: Level) void; - extern fn oc_log_set_output(output: *Output) void; extern fn oc_log_ext(level: Level, function: [*]const u8, file: [*]const u8, line: c_int, fmt: [*]const u8, ...) void; - const DEFAULT_OUTPUT = OC_LOG_DEFAULT_OUTPUT; + const setLevel = oc_log_set_level; pub fn info(comptime fmt: []const u8, args: anytype, source: std.builtin.SourceLocation) void { ext(Level.Info, fmt, args, source); @@ -66,7 +74,7 @@ pub fn assert(condition: bool, comptime fmt: []const u8, args: anytype, source: _ = std.fmt.bufPrintZ(&format_buf, fmt, args) catch 0; var line: c_int = @intCast(source.line); - oc_assert_fail(source.file.ptr, source.fn_name.ptr, line, "", format_buf[0..].ptr); + oc_assert_fail(source.file.ptr, source.fn_name.ptr, line, "assertion failed", format_buf[0..].ptr); } } @@ -85,6 +93,28 @@ pub fn abort(comptime fmt: []const u8, args: anytype, source: std.builtin.Source pub const ListElt = extern struct { prev: ?*ListElt, next: ?*ListElt, + + pub fn entry(self: *ListElt, comptime ParentType: type, comptime field_name_in_parent: []const u8) *ParentType { + return @fieldParentPtr(ParentType, field_name_in_parent, self); + } + + pub fn nextEntry(comptime ParentType: type, comptime field_name_in_parent: []const u8, elt_parent: *ParentType) ?*ParentType { + const elt: ?*ListElt = @field(elt_parent, field_name_in_parent); + if (elt.next) |next| { + return next.entry(ParentType, field_name_in_parent); + } + + return null; + } + + pub fn prevEntry(comptime ParentType: type, comptime field_name_in_parent: []const u8, elt_parent: *ParentType) ?*ParentType { + const elt: ?*ListElt = @field(elt_parent, field_name_in_parent); + if (elt.prev) |prev| { + return prev.entry(ParentType, field_name_in_parent); + } + + return null; + } }; pub const List = extern struct { @@ -110,22 +140,72 @@ pub const List = extern struct { return self.last; } - pub fn insert(self: *List, after_elt: *ListElt, elt: *ListElt) void { + pub fn firstEntry(self: *List, comptime EltParentType: type, comptime field_name_in_parent: []const u8) ?*EltParentType { + if (self.first) |elt| { + return elt.entry(EltParentType, field_name_in_parent); + } + return null; + } + + pub fn lastEntry(self: *List, comptime EltParentType: type, comptime field_name_in_parent: []const u8) ?*EltParentType { + if (self.last) |elt| { + return elt.entry(EltParentType, field_name_in_parent); + } + return null; + } + + const IterDirection = enum { + Forward, + Backward, + }; + + pub fn makeIter(comptime direction: IterDirection, comptime EltParentType: type, comptime field_name_in_parent: []const u8) type { + return struct { + const Self = @This(); + + item: ?*ListElt, + + pub fn next(self: *Self) ?*EltParentType { + if (self.item) |elt| { + var entry: *EltParentType = elt.entry(EltParentType, field_name_in_parent); + self.item = if (direction == .Forward) elt.next else elt.prev; + return entry; + } + return null; + } + }; + } + + pub fn iter(self: *const List, comptime EltParentType: type, comptime field_name_in_parent: []const u8) makeIter(.Forward, EltParentType, field_name_in_parent) { + const Iter = makeIter(.Forward, EltParentType, field_name_in_parent); + return Iter{ + .item = self.first, + }; + } + + pub fn iterReverse(self: *const List, comptime EltParentType: type, comptime field_name_in_parent: []const u8) makeIter(.Backward, EltParentType, field_name_in_parent) { + const Iter = makeIter(.Backward, EltParentType, field_name_in_parent); + return Iter{ + .item = self.last, + }; + } + + pub fn insert(self: *List, after_elt: ?*ListElt, elt: *ListElt) void { elt.prev = after_elt; elt.next = after_elt.next; - if (after_elt.next != null) { - after_elt.next.prev = elt; + if (after_elt.next) |elt_next| { + after_elt.next.prev = elt_next; } else { self.last = elt; } after_elt.next = elt; } - pub fn insertBefore(self: *List, before_elt: *ListElt, elt: *ListElt) void { + pub fn insertBefore(self: *List, before_elt: ?*ListElt, elt: *ListElt) void { elt.next = before_elt; elt.prev = before_elt.prev; - if (before_elt.prev != null) { - before_elt.prev.next = elt; + if (before_elt.prev) |elt_prev| { + before_elt.prev.next = elt_prev; } else { self.first = elt; } @@ -153,19 +233,25 @@ pub const List = extern struct { pub fn push(self: *List, elt: *ListElt) void { elt.next = self.first; elt.prev = null; - if (self.first != null) { - self.first.prev = elt; + if (self.first) |elt_first| { + elt_first.prev = elt; } else { self.last = elt; } self.first = elt; } - pub fn pop(self: *List) ListElt { - var elt: *ListElt = begin(self); - if (elt != end(self)) { - remove(self, elt); - return elt; + pub fn pop(self: *List) ?*ListElt { + if (self.begin()) |elt_begin| { + remove(self, elt_begin); + return elt_begin; + } + return null; + } + + pub fn popEntry(self: *List, comptime EltParentType: type, comptime field_name_in_parent: []const u8) ?*EltParentType { + if (self.pop()) |elt| { + return elt.entry(EltParentType, field_name_in_parent); } return null; } @@ -173,19 +259,18 @@ pub const List = extern struct { pub fn pushBack(self: *List, elt: *ListElt) void { elt.prev = self.last; elt.next = null; - if (self.last != null) { - self.last.next = elt; + if (self.last) |last_elt| { + last_elt.next = elt; } else { self.first = elt; } self.last = elt; } - pub fn popBack(self: *List) ListElt { - var elt: *ListElt = last(self); - if (elt != end(self)) { - remove(self, elt); - return elt; + pub fn popBack(self: *List) ?*ListElt { + if (self.last()) |last_elt| { + remove(self, last_elt); + return last_elt; } return null; } @@ -220,6 +305,20 @@ pub const ArenaChunk = extern struct { cap: u64, }; +pub const ArenaScope = extern struct { + arena: *Arena, + chunk: *ArenaChunk, + offset: u64, + + extern fn oc_arena_scope_end(scope: ArenaScope) void; + pub const end = oc_arena_scope_end; +}; + +pub const ArenaOptions = extern struct { + base: ?*BaseAllocator, + reserve: u64, +}; + pub const Arena = extern struct { base: ?*BaseAllocator, chunks: List, @@ -229,11 +328,10 @@ pub const Arena = extern struct { extern fn oc_arena_init_with_options(arena: *Arena, options: *ArenaOptions) void; extern fn oc_arena_cleanup(arena: *Arena) void; extern fn oc_arena_push(arena: *Arena, size: u64) ?[*]u8; + extern fn oc_arena_push_aligned(arena: *Arena, size: u64, alignment: u32) ?[*]u8; extern fn oc_arena_clear(arena: *Arena) void; extern fn oc_arena_scope_begin(arena: *Arena) ArenaScope; - extern fn oc_arena_scope_end(scope: ArenaScope) void; - extern fn oc_scratch() *Arena; extern fn oc_scratch_next(used: *Arena) *Arena; extern fn oc_scratch_begin() ArenaScope; extern fn oc_scratch_begin_next(used: *Arena) ArenaScope; @@ -249,50 +347,38 @@ pub const Arena = extern struct { return arena; } pub const deinit = oc_arena_cleanup; - pub fn push(arena: *Arena, size: u64) ?[]u8 { - if (oc_arena_push(arena, size)) |mem| { - return mem[0..size]; - } - return null; - } + pub const clear = oc_arena_clear; pub const scopeBegin = oc_arena_scope_begin; - pub const scopeEnd = oc_arena_scope_end; - pub fn pushType(arena: *Arena, comptime T: type) ?*T { - if (arena.push(@sizeOf(T))) |mem| { - std.debug.assert(mem.len >= @sizeOf(T)); - return @alignCast(@ptrCast(mem.ptr)); - } - - return null; + pub fn push(arena: *Arena, size: usize) AllocError![]u8 { + return arena.pushAligned(size, 1); } - pub fn pushArray(arena: *Arena, comptime T: type, count: u64) ?[]T { - if (arena.push(@sizeOf(T) * count)) |mem| { - std.debug.assert(mem.len >= @sizeOf(T) * count); - var items: [*]T = @alignCast(@ptrCast(mem.ptr)); - return items[0..count]; + pub fn pushAligned(arena: *Arena, size: usize, alignment: u32) AllocError![]u8 { + if (oc_arena_push_aligned(arena, size, alignment)) |mem| { + return mem[0..size]; } - return null; + return AllocError.OutOfMemory; + } + + pub fn pushType(arena: *Arena, comptime T: type) AllocError!*T { + var mem: []u8 = try arena.pushAligned(@sizeOf(T), @alignOf(T)); + assert(mem.len >= @sizeOf(T), "need at least {} bytes, but got {}", .{ mem.len, @sizeOf(T) }, @src()); + var p: *T = @alignCast(@ptrCast(mem.ptr)); + return p; + } + + pub fn pushArray(arena: *Arena, comptime T: type, count: usize) AllocError![]T { + var mem: []u8 = try arena.pushAligned(@sizeOf(T) * count, @alignOf(T)); + std.debug.assert(mem.len >= @sizeOf(T) * count); + var items: [*]T = @alignCast(@ptrCast(mem.ptr)); + return items[0..count]; } - pub const scratch = oc_scratch; pub const scratchNext = oc_scratch_next; pub const scratchBegin = oc_scratch_begin; pub const scratchBeginNext = oc_scratch_begin_next; - pub const scratchEnd = scopeEnd; -}; - -pub const ArenaScope = extern struct { - arena: ?*Arena, - chunk: ?*ArenaChunk, - offset: u64, -}; - -pub const ArenaOptions = extern struct { - base: ?*BaseAllocator, - reserve: u64, }; //------------------------------------------------------------------------------------------ @@ -319,25 +405,82 @@ fn stringType(comptime CharType: type) type { }; } - pub fn push(list: *StrList, str: *Str, arena: *Arena) void { - var elt: *StrListElt = arena.pushType(ListElt); + pub fn push(list: *StrList, str: Str, arena: *Arena) AllocError!void { + var elt: *StrListElt = try arena.pushType(StrListElt); elt.string = str; - list.append(elt.list_elt); + list.list.pushBack(&elt.list_elt); list.elt_count += 1; list.len += str.len; } - pub fn pushf(list: *StrList, arena: *Arena, comptime format: []const u8, args: anytype) Str { - var str = Str.pushf(arena, format, args); - list.push(str, arena); + pub fn pushSlice(list: *StrList, str: []const CharType, arena: *Arena) AllocError!void { + try list.push(Str.fromSlice(str), arena); } - // TODO - // pub fn join(list: *const StrList, arena: *Arena) Str; - // const empty = Str{ .ptr = null, .len = 0 }; - // return list.collate(arena, empty, empty, empty)); - // } - // pub fn collate(list: *StrList, arena: *Arena, prefix: Str, separator: Str, postfix: Str) Str; + pub fn pushf(list: *StrList, arena: *Arena, comptime format: []const u8, args: anytype) AllocError!Str { + var str = try Str.pushf(arena, format, args); + try list.list.push(str, arena); + } + + pub fn iter(list: *const StrList) List.makeIter(.Forward, StrListElt, "list_elt") { + return list.list.iter(StrListElt, "list_elt"); + } + + pub fn iterReverse(list: *const StrList) List.makeIter(.Backward, StrListElt, "list_elt") { + return list.list.iterReverse(StrListElt, "list_elt"); + } + + pub fn find(list: *const StrList, needle: *const Str) ?*StrListElt { + return list.findSlice(needle.slice()); + } + + pub fn findSlice(list: *const StrList, needle: []const CharType) ?*StrListElt { + var iterator = list.iter(); + while (iterator.next()) |elt_string| { + if (std.mem.eql(CharType, elt_string.string.slice(), needle)) { + return elt_string; + } + } + return null; + } + + pub fn contains(list: *const StrList, needle: *const Str) bool { + return list.findSlice(needle.slice()) != null; + } + + pub fn containsSlice(list: *const StrList, needle: []const CharType) bool { + return list.findSlice(needle) != null; + } + + pub fn join(list: *const StrList, arena: *Arena) AllocError!Str { + const empty = Str{ .ptr = null, .len = 0 }; + return try list.collate(arena, empty, empty, empty); + } + + pub fn collate(list: *const StrList, arena: *Arena, prefix: Str, separator: Str, postfix: Str) AllocError!Str { + var str: Str = undefined; + str.len = @intCast(prefix.len + list.len + (list.elt_count - 1) * separator.len + postfix.len); + str.ptr = (try arena.pushArray(CharType, str.len + 1)).ptr; + @memcpy(str.ptr.?[0..prefix.len], prefix.slice()); + + var offset = prefix.len; + + var iterator = list.iter(); + var index: usize = 0; + while (iterator.next()) |list_str| { + if (index > 0) { + @memcpy(str.ptr.?[offset .. offset + separator.len], separator.slice()); + offset += separator.len; + } + @memcpy(str.ptr.?[offset .. offset + list_str.string.len], list_str.string.slice()); + offset += list_str.string.len; + index += 1; + } + + @memcpy(str.ptr.?[offset .. offset + postfix.len], postfix.slice()); + str.ptr.?[str.len] = 0; + return str; + } }; const Str = @This(); @@ -408,8 +551,84 @@ fn stringType(comptime CharType: type) type { return str; } - // TODO - // pub fn split() void; + pub fn join(arena: *Arena, strings: []const []const CharType) AllocError!Str { + const empty = &[_]CharType{}; + return collate(arena, strings, empty, empty, empty); + } + + pub fn collate( + arena: *Arena, + strings: []const []const CharType, + prefix: []const CharType, + separator: []const CharType, + postfix: []const CharType, + ) AllocError!Str { + var strings_total_len: usize = 0; + for (strings) |s| { + strings_total_len += s.len; + } + + var str: Str = undefined; + str.len = prefix.len + strings_total_len + (strings.len - 1) * separator.len + postfix.len; + str.ptr = (try arena.pushArray(CharType, str.len + 1)).ptr; + @memcpy(str.ptr.?[0..prefix.len], prefix); + + var offset = prefix.len; + + for (strings, 0..) |list_str, index| { + if (index > 0) { + @memcpy(str.ptr.?[offset .. offset + separator.len], separator); + offset += separator.len; + } + @memcpy(str.ptr.?[offset .. offset + list_str.len], list_str); + offset += list_str.len; + } + + @memcpy(str.ptr.?[offset .. offset + postfix.len], postfix); + offset += postfix.len; + str.ptr.?[str.len] = 0; + return str; + } + + pub fn split(str: *const Str, arena: *Arena, separators: StrList) AllocError!StrList { + var list = StrList.init(); + if (str.ptr == null) { + return list; + } + + const ptr = str.ptr.?; + + var offset: usize = 0; + var offset_substring: usize = 0; + + while (offset < str.len) { + const haystack = ptr[offset..str.len]; + var separator_iter = separators.iter(); + while (separator_iter.next()) |list_sep| { + if (std.mem.startsWith(CharType, haystack, list_sep.string.slice()) and list_sep.string.len > 0) { + var substr = ptr[offset_substring..offset]; + if (separators.containsSlice(substr)) { + substr = ptr[offset..offset]; + } + try list.pushSlice(substr, arena); + + // -1 / +1 to account for offset += 1 at the end of the loop + offset += list_sep.string.len - 1; + offset_substring = offset + 1; + break; + } + } + + offset += 1; + } + + if (offset_substring != offset) { + var substr = ptr[offset_substring..offset]; + try list.pushSlice(substr, arena); + } + + return list; + } pub fn cmp(a: *const Str, b: *const Str) std.math.Order { if (CharType != u8) {