const std = @import("std"); const measure = @import("measure.zig"); const util = @import("util.zig"); const position = @import("position.zig"); const ArrayList = std.ArrayList; const Allocator = std.mem.Allocator; /// Joins `blocks` side by side along a vertical axis. `pos` places shorter /// blocks vertically — 0 = top, 0.5 = center, 1 = bottom, with any fraction /// in between allowed. Mirrors lipgloss `JoinHorizontal`. Caller owns the /// returned slice. pub fn horizontal( allocator: Allocator, pos: f64, blocks: []const []const u8, ) Allocator.Error![]u8 { if (blocks.len == 0) return allocator.alloc(u8, 0); const widths = try allocator.alloc(u16, blocks.len); defer allocator.free(widths); const heights = try allocator.alloc(u16, blocks.len); defer allocator.free(heights); var max_h: u16 = 0; for (blocks, 0..) |b, i| { widths[i] = measure.maxLineWidth(b); heights[i] = measure.lineCount(b); if (heights[i] > max_h) max_h = heights[i]; } var out: ArrayList(u8) = .empty; errdefer out.deinit(allocator); var row: u16 = 0; while (row < max_h) : (row += 1) { for (blocks, 0..) |b, i| { const top_pad = startPad(max_h - heights[i], pos); if (row < top_pad or row - top_pad >= heights[i]) { try padCells(allocator, &out, widths[i]); } else { const line = nthLine(b, row - top_pad); try out.appendSlice(allocator, line); const w = measure.cellWidth(line); if (w < widths[i]) try padCells(allocator, &out, widths[i] - w); } } if (row + 1 < max_h) try out.append(allocator, '\n'); } return out.toOwnedSlice(allocator); } /// Joins `blocks` top to bottom along a horizontal axis. `pos` places /// narrower blocks horizontally — 0 = left, 0.5 = center, 1 = right, with any /// fraction in between allowed. Mirrors lipgloss `JoinVertical`. Caller owns /// the returned slice. pub fn vertical( allocator: Allocator, pos: f64, blocks: []const []const u8, ) Allocator.Error![]u8 { if (blocks.len == 0) return allocator.alloc(u8, 0); var max_w: u16 = 0; for (blocks) |b| { const w = measure.maxLineWidth(b); if (w > max_w) max_w = w; } var out: ArrayList(u8) = .empty; errdefer out.deinit(allocator); for (blocks) |b| { const lines = measure.lineCount(b); var li: u16 = 0; while (li < lines) : (li += 1) { const line = nthLine(b, li); const line_w = measure.cellWidth(line); const gap = max_w - line_w; const left_pad = startPad(gap, pos); try padCells(allocator, &out, left_pad); try out.appendSlice(allocator, line); try padCells(allocator, &out, gap - left_pad); try out.append(allocator, '\n'); } } if (out.items.len > 0 and out.items[out.items.len - 1] == '\n') { _ = out.pop(); } return out.toOwnedSlice(allocator); } /// Returns the start-side padding for `n` filler cells at `pos`: `round(n * /// pos)`, matching lipgloss's `int(math.Round(...))`. The remainder is the /// end-side padding. fn startPad(n: u16, pos: f64) u16 { const v = position.value(pos); return @intFromFloat(@round(@as(f64, @floatFromInt(n)) * v)); } fn padCells(allocator: Allocator, out: *ArrayList(u8), count: u16) Allocator.Error!void { var i: u16 = 0; while (i < count) : (i += 1) try out.append(allocator, ' '); } fn nthLine(string: []const u8, index: u16) []const u8 { var idx: u16 = 0; var start: usize = 0; var i: usize = 0; while (i < string.len) : (i += 1) { if (string[i] == '\n') { if (idx == index) return string[start..i]; idx = util.satAdd(idx, 1); start = i + 1; } } if (idx == index) return string[start..]; return ""; } test "horizontal top align" { const a = "aa\nbb\ncc"; const b = "X\nY"; const out = try horizontal(std.testing.allocator, 0, &.{ a, b }); defer std.testing.allocator.free(out); try std.testing.expectEqualStrings("aaX\nbbY\ncc ", out); } test "horizontal bottom align" { const a = "aa\nbb\ncc"; const b = "X\nY"; const out = try horizontal(std.testing.allocator, 1, &.{ a, b }); defer std.testing.allocator.free(out); try std.testing.expectEqualStrings("aa \nbbX\nccY", out); } test "vertical center align" { // Gap of 3: round(3 * 0.5) = 2 cells lead, 1 trails. const out = try vertical(std.testing.allocator, 0.5, &.{ "hi", "world" }); defer std.testing.allocator.free(out); try std.testing.expectEqualStrings(" hi \nworld", out); } test "vertical fractional position" { // Gap of 3: round(3 * 0.25) = 1 cell lead, 2 trail. const out = try vertical(std.testing.allocator, 0.25, &.{ "hi", "world" }); defer std.testing.allocator.free(out); try std.testing.expectEqualStrings(" hi \nworld", out); }