a modern tui library written in zig
at main 146 lines 5.1 kB view raw
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}