//! Position along a horizontal or vertical axis, plus block placement. //! //! `Position` mirrors lipgloss `Position`: 0 = start (top / left), 1 = end //! (bottom / right), 0.5 = center. The `place*` functions mirror lipgloss //! `Place`/`PlaceHorizontal`/`PlaceVertical`. const std = @import("std"); const t = std.testing; const Writer = std.Io.Writer; const Allocator = std.mem.Allocator; const ArrayList = std.ArrayList; const Style = @import("Style.zig"); const measure = @import("measure.zig"); const LineSlicer = @import("LineSlicer.zig"); /// Clamps a position to the valid [0, 1] range (lipgloss `Position.value`). pub fn value(p: f64) f64 { return @max(0.0, @min(1.0, p)); } /// Filler used to pad placed blocks. `chars` is cycled to fill the gap (one /// or more code points, wide characters allowed); an optional `style` colors /// it. Mirrors lipgloss's `WithWhitespaceChars`/`WithWhitespaceStyle`, but as /// a plain struct rather than functional options. pub const Whitespace = struct { chars: []const u8 = " ", style: ?Style = null, }; /// Places `str` in a `width`×`height` box at (`h_pos`, `v_pos`), padding with /// `ws`. Mirrors lipgloss `Place`. The caller owns the returned slice. pub fn place( allocator: Allocator, width: u16, height: u16, h_pos: f64, v_pos: f64, str: []const u8, ws: Whitespace, ) Style.RenderError![]u8 { const horizontal = try placeHorizontal(allocator, width, h_pos, str, ws); defer allocator.free(horizontal); return placeVertical(allocator, height, v_pos, horizontal, ws); } pub fn placeHorizontal( allocator: Allocator, width: u16, pos: f64, str: []const u8, ws: Whitespace, ) Style.RenderError![]u8 { const content_w = measure.maxLineWidth(str); if (width <= content_w) return allocator.dupe(u8, str); var buf: Writer.Allocating = .init(allocator); defer buf.deinit(); const w = &buf.writer; var lines: LineSlicer = .init(str); var first = true; while (lines.next()) |line| { if (!first) try w.writeByte('\n'); first = false; const total_gap = width - measure.cellWidth(line); const start = startPad(total_gap, pos); try renderWhitespace(allocator, w, ws, start); try w.writeAll(line); try renderWhitespace(allocator, w, ws, total_gap - start); } return allocator.dupe(u8, buf.written()); } pub fn placeVertical( allocator: Allocator, height: u16, pos: f64, str: []const u8, ws: Whitespace, ) Style.RenderError![]u8 { const content_h = measure.lineCount(str); if (height <= content_h) return allocator.dupe(u8, str); const content_w = measure.maxLineWidth(str); const gap = height - content_h; const top_lines = startPad(gap, pos); const bottom_lines = gap - top_lines; var buf: Writer.Allocating = .init(allocator); defer buf.deinit(); const w = &buf.writer; var i: u16 = 0; while (i < top_lines) : (i += 1) { try renderWhitespace(allocator, w, ws, content_w); try w.writeByte('\n'); } try w.writeAll(str); i = 0; while (i < bottom_lines) : (i += 1) { try w.writeByte('\n'); try renderWhitespace(allocator, w, ws, content_w); } return allocator.dupe(u8, buf.written()); } fn startPad(n: u16, pos: f64) u16 { return @intFromFloat(@round(@as(f64, @floatFromInt(n)) * value(pos))); } fn renderWhitespace(allocator: Allocator, w: *Writer, ws: Whitespace, cells: u16) Style.RenderError!void { if (cells == 0) return; var filler: ArrayList(u8) = .empty; defer filler.deinit(allocator); const chars = if (ws.chars.len == 0) " " else ws.chars; var counter: u16 = 0; var ci: usize = 0; while (counter < cells) { if (ci >= chars.len) ci = 0; const len = std.unicode.utf8ByteSequenceLength(chars[ci]) catch 1; const end = @min(ci + len, chars.len); const cp = std.unicode.utf8Decode(chars[ci..end]) catch { try filler.append(allocator, ' '); counter += 1; ci = end; continue; }; var width: u16 = @import("width.zig").codepointWidth(cp); if (width == 0) width = 1; if (@as(u32, counter) + width > cells) break; try filler.appendSlice(allocator, chars[ci..end]); counter += width; ci = end; } while (counter < cells) : (counter += 1) try filler.append(allocator, ' '); const style: Style = ws.style orelse .{}; try style.render(w, filler.items); } test "value clamps to 0..1" { try t.expectEqual(@as(f64, 0.0), value(-1.0)); try t.expectEqual(@as(f64, 1.0), value(2.0)); try t.expectEqual(@as(f64, 0.5), value(0.5)); } test "placeHorizontal centers content" { const out = try placeHorizontal(t.allocator, 5, 0.5, "x", .{}); defer t.allocator.free(out); try t.expectEqualStrings(" x ", out); } test "placeHorizontal no-op when too narrow" { const out = try placeHorizontal(t.allocator, 1, 0.5, "wide", .{}); defer t.allocator.free(out); try t.expectEqualStrings("wide", out); } test "placeVertical centers content" { const out = try placeVertical(t.allocator, 3, 0.5, "x", .{}); defer t.allocator.free(out); try t.expectEqualStrings(" \nx\n ", out); } test "place positions in both axes" { const out = try place(t.allocator, 3, 3, 0.5, 0.5, "x", .{}); defer t.allocator.free(out); try t.expectEqualStrings(" \n x \n ", out); } test "placeHorizontal with styled whitespace" { const dotted: Whitespace = .{ .chars = ".", .style = Style.init(.{}).fg(.red) }; const out = try placeHorizontal(t.allocator, 4, 0, "x", dotted); defer t.allocator.free(out); try t.expectEqualStrings("x\x1b[31m...\x1b[0m", out); }