a modern tui library written in zig
at v0.4.1 7.1 kB view raw
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}