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