a modern tui library written in zig
1const std = @import("std");
2const vaxis = @import("../main.zig");
3
4pub const Scroll = struct {
5 x: usize = 0,
6 y: usize = 0,
7
8 pub fn restrictTo(self: *@This(), w: usize, h: usize) void {
9 self.x = @min(self.x, w);
10 self.y = @min(self.y, h);
11 }
12};
13
14pub const VerticalScrollbar = struct {
15 character: vaxis.Cell.Character = .{ .grapheme = "▐", .width = 1 },
16 fg: vaxis.Style = .{},
17 bg: vaxis.Style = .{ .fg = .{ .index = 8 } },
18};
19
20scroll: Scroll = .{},
21vertical_scrollbar: ?VerticalScrollbar = .{},
22
23/// Standard input mappings.
24/// It is not neccessary to use this, you can set `scroll` manually.
25pub fn input(self: *@This(), key: vaxis.Key) void {
26 if (key.matches(vaxis.Key.right, .{})) {
27 self.scroll.x +|= 1;
28 } else if (key.matches(vaxis.Key.right, .{ .shift = true })) {
29 self.scroll.x +|= 32;
30 } else if (key.matches(vaxis.Key.left, .{})) {
31 self.scroll.x -|= 1;
32 } else if (key.matches(vaxis.Key.left, .{ .shift = true })) {
33 self.scroll.x -|= 32;
34 } else if (key.matches(vaxis.Key.up, .{})) {
35 self.scroll.y -|= 1;
36 } else if (key.matches(vaxis.Key.page_up, .{})) {
37 self.scroll.y -|= 32;
38 } else if (key.matches(vaxis.Key.down, .{})) {
39 self.scroll.y +|= 1;
40 } else if (key.matches(vaxis.Key.page_down, .{})) {
41 self.scroll.y +|= 32;
42 } else if (key.matches(vaxis.Key.end, .{})) {
43 self.scroll.y = std.math.maxInt(usize);
44 } else if (key.matches(vaxis.Key.home, .{})) {
45 self.scroll.y = 0;
46 }
47}
48
49/// Must be called before doing any `writeCell` calls.
50pub fn draw(self: *@This(), parent: vaxis.Window, content_size: struct {
51 cols: usize,
52 rows: usize,
53}) void {
54 var content_cols = content_size.cols;
55 if (self.vertical_scrollbar) |opts| {
56 const vbar: vaxis.widgets.Scrollbar = .{
57 .character = opts.character,
58 .style = opts.fg,
59 .total = content_size.rows,
60 .view_size = parent.height,
61 .top = self.scroll.y,
62 };
63 const bg = parent.child(.{
64 .x_off = parent.width -| opts.character.width,
65 .width = .{ .limit = opts.character.width },
66 .height = .{ .limit = parent.height },
67 });
68 bg.fill(.{ .char = opts.character, .style = opts.bg });
69 vbar.draw(bg);
70 content_cols +|= 1;
71 }
72 const max_scroll_x = content_cols -| parent.width;
73 const max_scroll_y = content_size.rows -| parent.height;
74 self.scroll.restrictTo(max_scroll_x, max_scroll_y);
75}
76
77pub const BoundingBox = struct {
78 x1: usize,
79 y1: usize,
80 x2: usize,
81 y2: usize,
82
83 pub inline fn below(self: @This(), row: usize) bool {
84 return row < self.y1;
85 }
86
87 pub inline fn above(self: @This(), row: usize) bool {
88 return row >= self.y2;
89 }
90
91 pub inline fn rowInside(self: @This(), row: usize) bool {
92 return row >= self.y1 and row < self.y2;
93 }
94
95 pub inline fn colInside(self: @This(), col: usize) bool {
96 return col >= self.x1 and col < self.x2;
97 }
98
99 pub inline fn inside(self: @This(), col: usize, row: usize) bool {
100 return self.rowInside(row) and self.colInside(col);
101 }
102};
103
104/// Boundary of the content, useful for culling to improve draw performance.
105pub fn bounds(self: *@This(), parent: vaxis.Window) BoundingBox {
106 const right_pad: usize = if (self.vertical_scrollbar != null) 1 else 0;
107 return .{
108 .x1 = self.scroll.x,
109 .y1 = self.scroll.y,
110 .x2 = self.scroll.x +| parent.width -| right_pad,
111 .y2 = self.scroll.y +| parent.height,
112 };
113}
114
115/// Use this function instead of `Window.writeCell` to draw your cells and they will magically scroll.
116pub fn writeCell(self: *@This(), parent: vaxis.Window, col: usize, row: usize, cell: vaxis.Cell) void {
117 const b = self.bounds(parent);
118 if (!b.inside(col, row)) return;
119 const win = parent.child(.{ .width = .{ .limit = b.x2 - b.x1 }, .height = .{ .limit = b.y2 - b.y1 } });
120 win.writeCell(col -| self.scroll.x, row -| self.scroll.y, cell);
121}
122
123/// Use this function instead of `Window.readCell` to read the correct cell in scrolling context.
124pub fn readCell(self: *@This(), parent: vaxis.Window, col: usize, row: usize) ?vaxis.Cell {
125 const b = self.bounds(parent);
126 if (!b.inside(col, row)) return;
127 const win = parent.child(.{ .width = .{ .limit = b.width }, .height = .{ .limit = b.height } });
128 return win.readCell(col -| self.scroll.x, row -| self.scroll.y);
129}