a modern tui library written in zig
1const std = @import("std");
2const fmt = std.fmt;
3const heap = std.heap;
4const mem = std.mem;
5const meta = std.meta;
6
7const vaxis = @import("../main.zig");
8
9/// Table Context for maintaining state and drawing Tables with `drawTable()`.
10pub const TableContext = struct {
11 /// Current selected Row of the Table.
12 row: usize = 0,
13 /// Current selected Column of the Table.
14 col: usize = 0,
15 /// Starting point within the Data List.
16 start: usize = 0,
17
18 /// Active status of the Table.
19 active: bool = false,
20
21 /// The Background Color for Selected Rows and Column Headers.
22 selected_bg: vaxis.Cell.Color,
23 /// First Column Header Background Color
24 hdr_bg_1: vaxis.Cell.Color = .{ .rgb = [_]u8{ 64, 64, 64 } },
25 /// Second Column Header Background Color
26 hdr_bg_2: vaxis.Cell.Color = .{ .rgb = [_]u8{ 8, 8, 24 } },
27 /// First Row Background Color
28 row_bg_1: vaxis.Cell.Color = .{ .rgb = [_]u8{ 32, 32, 32 } },
29 /// Second Row Background Color
30 row_bg_2: vaxis.Cell.Color = .{ .rgb = [_]u8{ 8, 8, 8 } },
31
32 /// Y Offset for drawing to the parent Window.
33 y_off: usize = 0,
34
35 /// Column Width
36 /// Note, this should be treated as Read Only. The Column Width will be calculated during `drawTable()`.
37 col_width: usize = 0,
38};
39
40/// Draw a Table for the TUI.
41pub fn drawTable(
42 /// This should be an ArenaAllocator that can be deinitialized after each event call.
43 /// The Allocator is only used in two cases:
44 /// 1. If a cell is a non-String. If the Allocator is not provided, those cells will show "[unsupported (TypeName)]".
45 /// 2. To show that a value is too large to fit into a cell. If the Allocator is not provided, they'll just be cutoff.
46 alloc: ?mem.Allocator,
47 /// The parent Window to draw to.
48 win: vaxis.Window,
49 /// Headers for the Table
50 headers: []const []const u8,
51 /// This must be an ArrayList.
52 data_list: anytype,
53 // The Table Context for this Table.
54 table_ctx: *TableContext,
55) !void {
56 const table_win = win.initChild(
57 0,
58 table_ctx.y_off,
59 .{ .limit = win.width },
60 .{ .limit = win.height },
61 );
62
63 table_ctx.col_width = table_win.width / headers.len;
64 if (table_ctx.col_width % 2 != 0) table_ctx.col_width +|= 1;
65 while (table_ctx.col_width * headers.len < table_win.width - 1) table_ctx.col_width +|= 1;
66
67 if (table_ctx.col > headers.len - 1) table_ctx.*.col = headers.len - 1;
68 for (headers[0..], 0..) |hdr_txt, idx| {
69 const hdr_bg =
70 if (table_ctx.active and idx == table_ctx.col) table_ctx.selected_bg else if (idx % 2 == 0) table_ctx.hdr_bg_1 else table_ctx.hdr_bg_2;
71 const hdr_win = table_win.initChild(
72 idx * table_ctx.col_width,
73 0,
74 .{ .limit = table_ctx.col_width },
75 .{ .limit = 1 },
76 );
77 var hdr = vaxis.widgets.alignment.center(hdr_win, @min(table_ctx.col_width -| 1, hdr_txt.len +| 1), 1);
78 hdr_win.fill(.{ .style = .{ .bg = hdr_bg } });
79 var seg = [_]vaxis.Cell.Segment{.{
80 .text = if (hdr_txt.len > table_ctx.col_width and alloc != null) try fmt.allocPrint(alloc.?, "{s}...", .{hdr_txt[0..(table_ctx.col_width -| 4)]}) else hdr_txt,
81 .style = .{
82 .bg = hdr_bg,
83 .bold = true,
84 .ul_style = if (idx == table_ctx.col) .single else .dotted,
85 },
86 }};
87 _ = try hdr.print(seg[0..], .{ .wrap = .word });
88 }
89
90 const max_items = if (data_list.items.len > table_win.height -| 1) table_win.height -| 1 else data_list.items.len;
91 var end = table_ctx.*.start + max_items;
92 if (end > data_list.items.len) end = data_list.items.len;
93 table_ctx.*.start = tableStart: {
94 if (table_ctx.row == 0)
95 break :tableStart 0;
96 if (table_ctx.row < table_ctx.start)
97 break :tableStart table_ctx.start - (table_ctx.start - table_ctx.row);
98 if (table_ctx.row >= data_list.items.len - 1)
99 table_ctx.*.row = data_list.items.len - 1;
100 if (table_ctx.row >= end)
101 break :tableStart table_ctx.start + (table_ctx.row - end + 1);
102 break :tableStart table_ctx.start;
103 };
104 end = table_ctx.*.start + max_items;
105 if (end > data_list.items.len) end = data_list.items.len;
106 for (data_list.items[table_ctx.start..end], 0..) |data, idx| {
107 const row_bg =
108 if (table_ctx.active and table_ctx.start + idx == table_ctx.row) table_ctx.selected_bg else if (idx % 2 == 0) table_ctx.row_bg_1 else table_ctx.row_bg_2;
109
110 const row_win = table_win.initChild(
111 0,
112 1 + idx,
113 .{ .limit = table_win.width },
114 .{ .limit = 1 },
115 );
116 const DataT = @TypeOf(data);
117 if (DataT == []const u8) {
118 row_win.fill(.{ .style = .{ .bg = row_bg } });
119 var seg = [_]vaxis.Cell.Segment{.{
120 .text = if (data.len > table_ctx.col_width and alloc != null) try fmt.allocPrint(alloc.?, "{s}...", .{data[0..(table_ctx.col_width -| 4)]}) else data,
121 .style = .{ .bg = row_bg },
122 }};
123 _ = try row_win.print(seg[0..], .{ .wrap = .word });
124 return;
125 }
126 const item_fields = meta.fields(DataT);
127 inline for (item_fields[0..], 0..) |item_field, item_idx| {
128 const item = @field(data, item_field.name);
129 const ItemT = @TypeOf(item);
130 const item_win = row_win.initChild(
131 item_idx * table_ctx.col_width,
132 0,
133 .{ .limit = table_ctx.col_width },
134 .{ .limit = 1 },
135 );
136 const item_txt = switch (ItemT) {
137 []const u8 => item,
138 else => nonStr: {
139 switch (@typeInfo(ItemT)) {
140 .Optional => {
141 const opt_item = item orelse break :nonStr "-";
142 switch (@typeInfo(ItemT).Optional.child) {
143 []const u8 => break :nonStr opt_item,
144 else => {
145 break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{any}", .{opt_item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)});
146 },
147 }
148 },
149 else => {
150 break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{any}", .{item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)});
151 },
152 }
153 },
154 };
155 item_win.fill(.{ .style = .{ .bg = row_bg } });
156 var seg = [_]vaxis.Cell.Segment{.{
157 .text = if (item_txt.len > table_ctx.col_width and alloc != null) try fmt.allocPrint(alloc.?, "{s}...", .{item_txt[0..(table_ctx.col_width -| 4)]}) else item_txt,
158 .style = .{ .bg = row_bg },
159 }};
160 _ = try item_win.print(seg[0..], .{ .wrap = .word });
161 }
162 }
163}