Something went wrong. Try again.
A charm-like tui library
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151const 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);}