a modern tui library written in zig
1const std = @import("std");
2const vaxis = @import("../main.zig");
3
4const Allocator = std.mem.Allocator;
5
6const vxfw = @import("vxfw.zig");
7
8pub const BorderLabel = struct {
9 text: []const u8,
10 alignment: enum {
11 top_left,
12 top_center,
13 top_right,
14 bottom_left,
15 bottom_center,
16 bottom_right,
17 },
18};
19
20const Border = @This();
21
22child: vxfw.Widget,
23style: vaxis.Style = .{},
24labels: []const BorderLabel = &[_]BorderLabel{},
25
26pub fn widget(self: *const Border) vxfw.Widget {
27 return .{
28 .userdata = @constCast(self),
29 .drawFn = typeErasedDrawFn,
30 };
31}
32
33fn typeErasedDrawFn(ptr: *anyopaque, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
34 const self: *const Border = @ptrCast(@alignCast(ptr));
35 return self.draw(ctx);
36}
37
38/// If Border has a bounded maximum size, it will shrink the maximum size to account for the border
39/// before drawing the child. If the size is unbounded, border will draw the child and then itself
40/// around the childs size
41pub fn draw(self: *const Border, ctx: vxfw.DrawContext) Allocator.Error!vxfw.Surface {
42 const max_width: ?u16 = if (ctx.max.width) |width| width -| 2 else null;
43 const max_height: ?u16 = if (ctx.max.height) |height| height -| 2 else null;
44
45 const child_ctx = ctx.withConstraints(ctx.min, .{
46 .width = max_width,
47 .height = max_height,
48 });
49 const child = try self.child.draw(child_ctx);
50
51 const children = try ctx.arena.alloc(vxfw.SubSurface, 1);
52 children[0] = .{
53 .origin = .{ .col = 1, .row = 1 },
54 .z_index = 0,
55 .surface = child,
56 };
57
58 const size: vxfw.Size = .{ .width = child.size.width + 2, .height = child.size.height + 2 };
59
60 var surf = try vxfw.Surface.initWithChildren(ctx.arena, self.widget(), size, children);
61
62 // Draw the border
63 const right_edge = size.width -| 1;
64 const bottom_edge = size.height -| 1;
65 surf.writeCell(0, 0, .{ .char = .{ .grapheme = "╭", .width = 1 }, .style = self.style });
66 surf.writeCell(right_edge, 0, .{ .char = .{ .grapheme = "╮", .width = 1 }, .style = self.style });
67 surf.writeCell(right_edge, bottom_edge, .{ .char = .{ .grapheme = "╯", .width = 1 }, .style = self.style });
68 surf.writeCell(0, bottom_edge, .{ .char = .{ .grapheme = "╰", .width = 1 }, .style = self.style });
69
70 var col: u16 = 1;
71 while (col < right_edge) : (col += 1) {
72 surf.writeCell(col, 0, .{ .char = .{ .grapheme = "─", .width = 1 }, .style = self.style });
73 surf.writeCell(col, bottom_edge, .{ .char = .{ .grapheme = "─", .width = 1 }, .style = self.style });
74 }
75
76 var row: u16 = 1;
77 while (row < bottom_edge) : (row += 1) {
78 surf.writeCell(0, row, .{ .char = .{ .grapheme = "│", .width = 1 }, .style = self.style });
79 surf.writeCell(right_edge, row, .{ .char = .{ .grapheme = "│", .width = 1 }, .style = self.style });
80 }
81
82 // Add border labels
83 for (self.labels) |label| {
84 const text_len: u16 = @intCast(ctx.stringWidth(label.text));
85 if (text_len == 0) continue;
86
87 const text_row: u16 = switch (label.alignment) {
88 .top_left, .top_center, .top_right => 0,
89 .bottom_left, .bottom_center, .bottom_right => bottom_edge,
90 };
91
92 var text_col: u16 = switch (label.alignment) {
93 .top_left, .bottom_left => 1,
94 .top_center, .bottom_center => @max((size.width - text_len) / 2, 1),
95 .top_right, .bottom_right => @max(size.width - 1 - text_len, 1),
96 };
97
98 var iter = ctx.graphemeIterator(label.text);
99 while (iter.next()) |grapheme| {
100 const text = grapheme.bytes(label.text);
101 const width: u16 = @intCast(ctx.stringWidth(text));
102 surf.writeCell(text_col, text_row, .{
103 .char = .{ .grapheme = text, .width = @intCast(width) },
104 .style = self.style,
105 });
106 text_col += width;
107 }
108 }
109
110 return surf;
111}
112
113test Border {
114 const Text = @import("Text.zig");
115 // Will be height=1, width=3
116 const text: Text = .{ .text = "abc" };
117
118 const border: Border = .{ .child = text.widget() };
119
120 var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
121 defer arena.deinit();
122 vxfw.DrawContext.init(.unicode);
123
124 // Border will draw itself tightly around the child
125 const ctx: vxfw.DrawContext = .{
126 .arena = arena.allocator(),
127 .min = .{},
128 .max = .{ .width = 10, .height = 10 },
129 .cell_size = .{ .width = 10, .height = 20 },
130 };
131
132 const surface = try border.draw(ctx);
133 // Border should be the size of Text + 2
134 try std.testing.expectEqual(5, surface.size.width);
135 try std.testing.expectEqual(3, surface.size.height);
136 // Border has 1 child
137 try std.testing.expectEqual(1, surface.children.len);
138 const child = surface.children[0];
139 // The child is 1x3
140 try std.testing.expectEqual(3, child.surface.size.width);
141 try std.testing.expectEqual(1, child.surface.size.height);
142}
143
144test "refAllDecls" {
145 std.testing.refAllDecls(@This());
146}