Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented View component for easy scrolling #82

Merged
merged 18 commits into from
Oct 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,17 @@ pub fn build(b: *std.Build) void {

// Examples
const Example = enum {
aio,
cli,
image,
main,
nvim,
table,
text_input,
vaxis,
view,
vt,
xev,
aio,
};
const example_option = b.option(Example, "example", "Example to run (default: text_input)") orelse .text_input;
const example_step = b.step("example", "Run example");
Expand Down
7 changes: 6 additions & 1 deletion examples/table.zig
Original file line number Diff line number Diff line change
Expand Up @@ -79,10 +79,15 @@ pub fn main() !void {
var demo_tbl: vaxis.widgets.Table.TableContext = .{
.active_bg = active_bg,
.active_fg = .{ .rgb = .{ 0, 0, 0 } },
.row_bg_1 = .{ .rgb = .{ 8, 8, 8 } },
.selected_bg = selected_bg,
.header_names = .{ .custom = &.{ "First", "Last", "Username", "Phone#", "Email" } },
//.header_names = .{ .custom = &.{ "First", "Last", "Email", "Phone#" } },
//.header_align = .left,
.col_indexes = .{ .by_idx = &.{ 0, 1, 2, 4, 3 } },
//.col_align = .{ .by_idx = &.{ .left, .left, .center, .center, .left } },
//.col_align = .{ .all = .center },
//.header_borders = true,
//.col_borders = true,
//.col_width = .{ .static_all = 15 },
//.col_width = .{ .dynamic_header_len = 3 },
//.col_width = .{ .static_individual = &.{ 10, 20, 15, 25, 15 } },
Expand Down
361 changes: 361 additions & 0 deletions examples/view.zig

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/Screen.zig
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ width_method: Method = .wcwidth,
mouse_shape: Shape = .default,
cursor_shape: Cell.CursorShape = .default,

pub fn init(alloc: std.mem.Allocator, winsize: Winsize, unicode: *const Unicode) !Screen {
pub fn init(alloc: std.mem.Allocator, winsize: Winsize, unicode: *const Unicode) std.mem.Allocator.Error!Screen {
const w = winsize.cols;
const h = winsize.rows;
const self = Screen{
Expand Down Expand Up @@ -63,7 +63,7 @@ pub fn writeCell(self: *Screen, col: usize, row: usize, cell: Cell) void {
self.buf[i] = cell;
}

pub fn readCell(self: *Screen, col: usize, row: usize) ?Cell {
pub fn readCell(self: *const Screen, col: usize, row: usize) ?Cell {
if (self.width <= col) {
// column out of bounds
return null;
Expand Down
1 change: 1 addition & 0 deletions src/widgets.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ pub const TextView = @import("widgets/TextView.zig");
pub const CodeView = @import("widgets/CodeView.zig");
pub const Terminal = @import("widgets/terminal/Terminal.zig");
pub const TextInput = @import("widgets/TextInput.zig");
pub const View = @import("widgets/View.zig");
48 changes: 46 additions & 2 deletions src/widgets/Table.zig
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,17 @@ pub const TableContext = struct {
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`.
Expand Down Expand Up @@ -90,6 +101,17 @@ pub const HeaderNames = union(enum) {
custom: []const []const u8,
};

/// Horizontal Alignment
pub const HorizontalAlignment = enum {
left,
center,
};
/// Column Alignment
pub 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.
Expand Down Expand Up @@ -218,8 +240,12 @@ pub fn drawTable(
.y_off = 0,
.width = .{ .limit = col_width },
.height = .{ .limit = 1 },
.border = .{ .where = if (table_ctx.header_borders and idx > 0) .left else .none },
});
var hdr = vaxis.widgets.alignment.center(hdr_win, @min(col_width -| 1, hdr_txt.len +| 1), 1);
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,
Expand Down Expand Up @@ -274,12 +300,14 @@ pub fn drawTable(
.y_off = 1 + row + table_ctx.active_y_off,
.width = .{ .limit = table_win.width },
.height = .{ .limit = 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) {
Expand All @@ -288,6 +316,7 @@ pub fn drawTable(
if (item_idx != f_idx) break :contFields;
},
}
defer col_idx += 1;
const col_width = try calcColWidth(
item_idx,
headers,
Expand All @@ -302,6 +331,7 @@ pub fn drawTable(
.y_off = 0,
.width = .{ .limit = col_width },
.height = .{ .limit = 1 },
.border = .{ .where = if (table_ctx.col_borders and col_idx > 0) .left else .none },
});
const item_txt = switch (ItemT) {
[]const u8 => item,
Expand Down Expand Up @@ -331,11 +361,25 @@ pub fn drawTable(
},
};
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 },
}};
_ = try item_win.print(seg[0..], .{ .wrap = .word, .col_offset = table_ctx.cell_x_off });
_ = try item_align_win.print(seg[0..], .{ .wrap = .word, .col_offset = table_ctx.cell_x_off });
}
}
}
Expand Down
159 changes: 159 additions & 0 deletions src/widgets/View.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
//! A View is effectively an "oversized" Window that can be written to and rendered in pieces.

const std = @import("std");
const mem = std.mem;

const View = @This();

const gw = @import("../gwidth.zig");

const Screen = @import("../Screen.zig");
const Window = @import("../Window.zig");
const Unicode = @import("../Unicode.zig");
const Cell = @import("../Cell.zig");

/// View Allocator
alloc: mem.Allocator,

/// Underlying Screen
screen: Screen,

/// View Initialization Config
pub const Config = struct {
width: usize,
height: usize,
};

/// Initialize a new View
pub fn init(alloc: mem.Allocator, unicode: *const Unicode, config: Config) mem.Allocator.Error!View {
const screen = try Screen.init(
alloc,
.{
.cols = config.width,
.rows = config.height,
.x_pixel = 0,
.y_pixel = 0,
},
unicode,
);
return .{
.alloc = alloc,
.screen = screen,
};
}

pub fn window(self: *View) Window {
return .{
.x_off = 0,
.y_off = 0,
.width = self.screen.width,
.height = self.screen.height,
.screen = &self.screen,
};
}

/// Deinitialize this View
pub fn deinit(self: *View) void {
self.screen.deinit(self.alloc);
}

pub const DrawOptions = struct {
x_off: usize = 0,
y_off: usize = 0,
};

pub fn draw(self: *View, win: Window, opts: DrawOptions) void {
if (opts.x_off >= self.screen.width) return;
if (opts.y_off >= self.screen.height) return;

const width = @min(win.width, self.screen.width - opts.x_off);
const height = @min(win.height, self.screen.height - opts.y_off);

for (0..height) |row| {
const src_start = opts.x_off + ((row + opts.y_off) * self.screen.width);
const src_end = src_start + width;
const dst_start = win.x_off + ((row + win.y_off) * win.screen.width);
const dst_end = dst_start + width;
@memcpy(win.screen.buf[dst_start..dst_end], self.screen.buf[src_start..src_end]);
}
}

/// Render Config for `toWin()`
pub const RenderConfig = struct {
x: usize = 0,
y: usize = 0,
width: Extent = .fit,
height: Extent = .fit,

pub const Extent = union(enum) {
fit,
max: usize,
};
};

/// Render a portion of this View to the provided Window (`win`).
/// This will return the bounded X (col), Y (row) coordinates based on the rendering.
pub fn toWin(self: *View, win: Window, config: RenderConfig) !struct { usize, usize } {
var x = @min(self.screen.width - 1, config.x);
var y = @min(self.screen.height - 1, config.y);
const width = width: {
var width = switch (config.width) {
.fit => win.width,
.max => |w| @min(win.width, w),
};
width = @min(width, self.screen.width);
break :width @min(width, self.screen.width -| 1 -| x +| win.width);
};
const height = height: {
var height = switch (config.height) {
.fit => win.height,
.max => |h| @min(win.height, h),
};
height = @min(height, self.screen.height);
break :height @min(height, self.screen.height -| 1 -| y +| win.height);
};
x = @min(x, self.screen.width -| width);
y = @min(y, self.screen.height -| height);
const child = win.child(.{
.width = .{ .limit = width },
.height = .{ .limit = height },
});
self.draw(child, .{ .x_off = x, .y_off = y });
return .{ x, y };
}

/// Writes a cell to the location in the View
pub fn writeCell(self: *View, col: usize, row: usize, cell: Cell) void {
self.screen.writeCell(col, row, cell);
}

/// Reads a cell at the location in the View
pub fn readCell(self: *const View, col: usize, row: usize) ?Cell {
return self.screen.readCell(col, row);
}

/// Fills the View with the default cell
pub fn clear(self: View) void {
self.fill(.{ .default = true });
}

/// Returns the width of the grapheme. This depends on the terminal capabilities
pub fn gwidth(self: View, str: []const u8) usize {
return gw.gwidth(str, self.screen.width_method, &self.screen.unicode.width_data) catch 1;
}

/// Fills the View with the provided cell
pub fn fill(self: View, cell: Cell) void {
@memset(self.screen.buf, cell);
}

/// Prints segments to the View. Returns true if the text overflowed with the
/// given wrap strategy and size.
pub fn print(self: *View, segments: []const Cell.Segment, opts: Window.PrintOptions) !Window.PrintResult {
return self.window().print(segments, opts);
}

/// Print a single segment. This is just a shortcut for print(&.{segment}, opts)
pub fn printSegment(self: *View, segment: Cell.Segment, opts: Window.PrintOptions) !Window.PrintResult {
return self.print(&.{segment}, opts);
}
Loading