Something went wrong. Try again.
This repository has no description
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412const std = @import("std");const fmt = std.fmt;const heap = std.heap;const mem = std.mem;const meta = std.meta;
const vaxis = @import("../main.zig");
/// Table Context for maintaining state and drawing Tables with `drawTable()`.pub const TableContext = struct { /// Current active Row of the Table. row: u16 = 0, /// Current active Column of the Table. col: u16 = 0, /// Starting point within the Data List. start: u16 = 0, /// Selected Rows. sel_rows: ?[]u16 = null,
/// Active status of the Table. active: bool = false, /// Active Content Callback Function. /// If available, this will be called to vertically expand the active row with additional info. active_content_fn: ?*const fn (*vaxis.Window, *const anyopaque) anyerror!u16 = null, /// Active Content Context /// This will be provided to the `active_content` callback when called. active_ctx: *const anyopaque = &{}, /// Y Offset for rows beyond the Active Content. /// (This will be calculated automatically) active_y_off: u16 = 0,
/// The Background Color for Selected Rows. selected_bg: vaxis.Cell.Color, /// The Foreground Color for Selected Rows. selected_fg: vaxis.Cell.Color = .default, /// The Background Color for the Active Row and Column Header. active_bg: vaxis.Cell.Color, /// The Foreground Color for the Active Row and Column Header. active_fg: vaxis.Cell.Color = .default, /// First Column Header Background Color hdr_bg_1: vaxis.Cell.Color = .{ .rgb = [_]u8{ 64, 64, 64 } }, /// Second Column Header Background Color hdr_bg_2: vaxis.Cell.Color = .{ .rgb = [_]u8{ 8, 8, 24 } }, /// First Row Background Color row_bg_1: vaxis.Cell.Color = .{ .rgb = [_]u8{ 32, 32, 32 } }, /// Second Row Background Color row_bg_2: vaxis.Cell.Color = .{ .rgb = [_]u8{ 8, 8, 8 } },
/// Y Offset for drawing to the parent Window. y_off: u16 = 0, /// X Offset for printing each Cell/Item. cell_x_off: u16 = 1,
/// Column Width /// Note, if this is left `null` the Column Width will be dynamically calculated during `drawTable()`. //col_width: ?usize = null, col_width: WidthStyle = .dynamic_fill,
// Header Names header_names: HeaderNames = .field_names, // Column Indexes col_indexes: ColumnIndexes = .all, // Header Alignment header_align: HorizontalAlignment = .center, // Column Alignment col_align: ColumnAlignment = .{ .all = .left },
// Header Borders header_borders: bool = false, // Row Borders //row_borders: bool = false, // Col Borders col_borders: bool = false,};
/// Width Styles for `col_width`.pub const WidthStyle = union(enum) { /// Dynamically calculate Column Widths such that the entire (or most) of the screen is filled horizontally. dynamic_fill, /// Dynamically calculate the Column Width for each Column based on its Header Length and the provided Padding length. dynamic_header_len: u16, /// Statically set all Column Widths to the same value. static_all: u16, /// Statically set individual Column Widths to specific values. static_individual: []const u16,};
/// Column Indexespub const ColumnIndexes = union(enum) { /// Use all of the Columns. all, /// Use Columns from the specified indexes. by_idx: []const usize,};
/// Header Namespub const HeaderNames = union(enum) { /// Use Field Names as Headers field_names, /// Custom custom: []const []const u8,};
/// Horizontal Alignmentpub const HorizontalAlignment = enum { left, center,};/// Column Alignmentpub const ColumnAlignment = union(enum) { all: HorizontalAlignment, by_idx: []const HorizontalAlignment,};
/// Draw a Table for the TUI.pub fn drawTable( /// This should be an ArenaAllocator that can be deinitialized after each event call. /// The Allocator is only used in three cases: /// 1. If a cell is a non-String. (If the Allocator is not provided, those cells will show "[unsupported (TypeName)]".) /// 2. To show that a value is too large to fit into a cell using '...'. (If the Allocator is not provided, they'll just be cutoff.) /// 3. To copy a MultiArrayList into a normal slice. (Note, this is an expensive operation. Prefer to pass a Slice or ArrayList if possible.) alloc: ?mem.Allocator, /// The parent Window to draw to. win: vaxis.Window, /// This must be a Slice, ArrayList, or MultiArrayList. /// Note, MultiArrayList support currently requires allocation. data_list: anytype, // The Table Context for this Table. table_ctx: *TableContext,) !void { var di_is_mal = false; const data_items = getData: { const DataListT = @TypeOf(data_list); const data_ti = @typeInfo(DataListT); switch (data_ti) { .pointer => |ptr| { if (ptr.size != .slice) return error.UnsupportedTableDataType; break :getData data_list; }, .@"struct" => { const di_fields = meta.fields(DataListT); const al_fields = meta.fields(std.ArrayList([]const u8)); const mal_fields = meta.fields(std.MultiArrayList(struct { a: u8 = 0, b: u32 = 0 })); // Probably an ArrayList const is_al = comptime if (mem.indexOf(u8, @typeName(DataListT), "MultiArrayList") == null and mem.indexOf(u8, @typeName(DataListT), "ArrayList") != null and al_fields.len == di_fields.len) isAL: { var is = true; for (al_fields, di_fields) |al_field, di_field| is = is and mem.eql(u8, al_field.name, di_field.name); break :isAL is; } else false; if (is_al) break :getData data_list.items;
// Probably a MultiArrayList const is_mal = if (mem.indexOf(u8, @typeName(DataListT), "MultiArrayList") != null and mal_fields.len == di_fields.len) isMAL: { var is = true; inline for (mal_fields, di_fields) |mal_field, di_field| is = is and mem.eql(u8, mal_field.name, di_field.name); break :isMAL is; } else false; if (!is_mal) return error.UnsupportedTableDataType; if (alloc) |_alloc| { di_is_mal = true; const mal_slice = data_list.slice(); const DataT = dataType: { const fn_info = @typeInfo(@TypeOf(@field(@TypeOf(mal_slice), "get"))); break :dataType fn_info.@"fn".return_type orelse @panic("No Child Type"); }; var data_out_list = std.ArrayList(DataT){}; for (0..mal_slice.len) |idx| try data_out_list.append(_alloc, mal_slice.get(idx)); break :getData try data_out_list.toOwnedSlice(_alloc); } return error.UnsupportedTableDataType; }, else => return error.UnsupportedTableDataType, } }; defer if (di_is_mal) alloc.?.free(data_items); const DataT = @TypeOf(data_items[0]); const fields = meta.fields(DataT); const field_indexes = switch (table_ctx.col_indexes) { .all => comptime allIdx: { var indexes_buf: [fields.len]usize = undefined; for (0..fields.len) |idx| indexes_buf[idx] = idx; const indexes = indexes_buf; break :allIdx indexes[0..]; }, .by_idx => |by_idx| by_idx, };
// Headers for the Table var hdrs_buf: [fields.len][]const u8 = undefined; const headers = hdrs: { switch (table_ctx.header_names) { .field_names => { for (field_indexes) |f_idx| { inline for (fields, 0..) |field, idx| { if (f_idx == idx) hdrs_buf[idx] = field.name; } } break :hdrs hdrs_buf[0..]; }, .custom => |hdrs| break :hdrs hdrs, } };
const table_win = win.child(.{ .y_off = table_ctx.y_off, .width = win.width, .height = win.height, });
// Headers if (table_ctx.col > headers.len - 1) table_ctx.col = @intCast(headers.len - 1); var col_start: u16 = 0; for (headers[0..], 0..) |hdr_txt, idx| { const col_width = try calcColWidth( @intCast(idx), headers, table_ctx.col_width, table_win, ); defer col_start += col_width; const hdr_fg, const hdr_bg = hdrColors: { if (table_ctx.active and idx == table_ctx.col) break :hdrColors .{ table_ctx.active_fg, table_ctx.active_bg } else if (idx % 2 == 0) break :hdrColors .{ .default, table_ctx.hdr_bg_1 } else break :hdrColors .{ .default, table_ctx.hdr_bg_2 }; }; const hdr_win = table_win.child(.{ .x_off = col_start, .y_off = 0, .width = col_width, .height = 1, .border = .{ .where = if (table_ctx.header_borders and idx > 0) .left else .none }, }); var hdr = switch (table_ctx.header_align) { .left => hdr_win, .center => vaxis.widgets.alignment.center(hdr_win, @min(col_width -| 1, hdr_txt.len +| 1), 1), }; hdr_win.fill(.{ .style = .{ .bg = hdr_bg } }); var seg = [_]vaxis.Cell.Segment{.{ .text = if (hdr_txt.len > col_width and alloc != null) try fmt.allocPrint(alloc.?, "{s}...", .{hdr_txt[0..(col_width -| 4)]}) else hdr_txt, .style = .{ .fg = hdr_fg, .bg = hdr_bg, .bold = true, .ul_style = if (idx == table_ctx.col) .single else .dotted, }, }}; _ = hdr.print(seg[0..], .{ .wrap = .word }); }
// Rows if (table_ctx.active_content_fn == null) table_ctx.active_y_off = 0; const max_items: u16 = if (data_items.len > table_win.height -| 1) table_win.height -| 1 else @intCast(data_items.len); var end = table_ctx.start + max_items; if (table_ctx.row + table_ctx.active_y_off >= win.height -| 2) end -|= table_ctx.active_y_off; if (end > data_items.len) end = @intCast(data_items.len); table_ctx.start = tableStart: { if (table_ctx.row == 0) break :tableStart 0; if (table_ctx.row < table_ctx.start) break :tableStart table_ctx.start - (table_ctx.start - table_ctx.row); if (table_ctx.row >= data_items.len - 1) table_ctx.row = @intCast(data_items.len - 1); if (table_ctx.row >= end) break :tableStart table_ctx.start + (table_ctx.row - end + 1); break :tableStart table_ctx.start; }; end = table_ctx.start + max_items; if (table_ctx.row + table_ctx.active_y_off >= win.height -| 2) end -|= table_ctx.active_y_off; if (end > data_items.len) end = @intCast(data_items.len); table_ctx.start = @min(table_ctx.start, end); table_ctx.active_y_off = 0; for (data_items[table_ctx.start..end], 0..) |data, row| { const row_fg, const row_bg = rowColors: { if (table_ctx.active and table_ctx.start + row == table_ctx.row) break :rowColors .{ table_ctx.active_fg, table_ctx.active_bg }; if (table_ctx.sel_rows) |rows| { if (mem.indexOfScalar(u16, rows, @intCast(table_ctx.start + row)) != null) break :rowColors .{ table_ctx.selected_fg, table_ctx.selected_bg }; } if (row % 2 == 0) break :rowColors .{ .default, table_ctx.row_bg_1 }; break :rowColors .{ .default, table_ctx.row_bg_2 }; }; var row_win = table_win.child(.{ .x_off = 0, .y_off = @intCast(1 + row + table_ctx.active_y_off), .width = table_win.width, .height = 1, //.border = .{ .where = if (table_ctx.row_borders) .top else .none }, }); if (table_ctx.start + row == table_ctx.row) { table_ctx.active_y_off = if (table_ctx.active_content_fn) |content| try content(&row_win, table_ctx.active_ctx) else 0; } col_start = 0; const item_fields = meta.fields(DataT); var col_idx: usize = 0; for (field_indexes) |f_idx| { inline for (item_fields[0..], 0..) |item_field, item_idx| contFields: { switch (table_ctx.col_indexes) { .all => {}, .by_idx => { if (item_idx != f_idx) break :contFields; }, } defer col_idx += 1; const col_width = try calcColWidth( item_idx, headers, table_ctx.col_width, table_win, ); defer col_start += col_width; const item = @field(data, item_field.name); const ItemT = @TypeOf(item); const item_win = row_win.child(.{ .x_off = col_start, .y_off = 0, .width = col_width, .height = 1, .border = .{ .where = if (table_ctx.col_borders and col_idx > 0) .left else .none }, }); const item_txt = switch (ItemT) { []const u8 => item, [][]const u8, []const []const u8 => strSlice: { if (alloc) |_alloc| break :strSlice try fmt.allocPrint(_alloc, "{s}", .{item}); break :strSlice item; }, else => nonStr: { switch (@typeInfo(ItemT)) { .@"enum" => break :nonStr @tagName(item), .optional => { const opt_item = item orelse break :nonStr "-"; switch (@typeInfo(ItemT).optional.child) { []const u8 => break :nonStr opt_item, [][]const u8, []const []const u8 => { break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{s}", .{opt_item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)}); }, else => { break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{any}", .{opt_item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)}); }, } }, else => { break :nonStr if (alloc) |_alloc| try fmt.allocPrint(_alloc, "{any}", .{item}) else fmt.comptimePrint("[unsupported ({s})]", .{@typeName(DataT)}); }, } }, }; item_win.fill(.{ .style = .{ .bg = row_bg } }); const item_align_win = itemAlignWin: { const col_align = switch (table_ctx.col_align) { .all => |all| all, .by_idx => |aligns| aligns[col_idx], }; break :itemAlignWin switch (col_align) { .left => item_win, .center => center: { const center = vaxis.widgets.alignment.center(item_win, @min(col_width -| 1, item_txt.len +| 1), 1); center.fill(.{ .style = .{ .bg = row_bg } }); break :center center; }, }; }; var seg = [_]vaxis.Cell.Segment{.{ .text = if (item_txt.len > col_width and alloc != null) try fmt.allocPrint(alloc.?, "{s}...", .{item_txt[0..(col_width -| 4)]}) else item_txt, .style = .{ .fg = row_fg, .bg = row_bg }, }}; _ = item_align_win.print(seg[0..], .{ .wrap = .word, .col_offset = table_ctx.cell_x_off }); } } }}
/// Calculate the Column Width of `col` using the provided Number of Headers (`num_hdrs`), Width Style (`style`), and Table Window (`table_win`).pub fn calcColWidth( col: u16, headers: []const []const u8, style: WidthStyle, table_win: vaxis.Window,) !u16 { return switch (style) { .dynamic_fill => dynFill: { var cw: u16 = table_win.width / @as(u16, @intCast(headers.len)); if (cw % 2 != 0) cw +|= 1; while (cw * headers.len < table_win.width - 1) cw +|= 1; break :dynFill cw; }, .dynamic_header_len => dynHdrs: { if (col >= headers.len) break :dynHdrs error.NotEnoughStaticWidthsProvided; break :dynHdrs @as(u16, @intCast(headers[col].len)) + (style.dynamic_header_len * 2); }, .static_all => style.static_all, .static_individual => statInd: { if (col >= headers.len) break :statInd error.NotEnoughStaticWidthsProvided; break :statInd style.static_individual[col]; }, };}