widgets: add ScrollView widget

The ScrollView widget can be used to introduce scrollable elements into
existing widgets.

To use the ScrollView, you must use the ScrollView's writeCell and
readCell functions rather than the ones from Window.
This commit is contained in:
Jari Vetoniemi 2024-06-01 00:10:49 +09:00 committed by Tim Culverhouse
parent fbaa6ca8df
commit f25b8ab421
2 changed files with 130 additions and 0 deletions

View file

@ -6,3 +6,4 @@ pub const Scrollbar = @import("widgets/Scrollbar.zig");
pub const Table = @import("widgets/Table.zig");
pub const TextInput = @import("widgets/TextInput.zig");
pub const nvim = @import("widgets/nvim.zig");
pub const ScrollView = @import("widgets/ScrollView.zig");

129
src/widgets/ScrollView.zig Normal file
View file

@ -0,0 +1,129 @@
const std = @import("std");
const vaxis = @import("../main.zig");
pub const Scroll = struct {
x: usize = 0,
y: usize = 0,
pub fn restrictTo(self: *@This(), w: usize, h: usize) void {
self.x = @min(self.x, w);
self.y = @min(self.y, h);
}
};
pub const VerticalScrollbar = struct {
character: vaxis.Cell.Character = .{ .grapheme = "", .width = 1 },
fg: vaxis.Style = .{},
bg: vaxis.Style = .{ .fg = .{ .index = 8 } },
};
scroll: Scroll = .{},
vertical_scrollbar: ?VerticalScrollbar = .{},
/// Standard input mappings.
/// It is not neccessary to use this, you can set `scroll` manually.
pub fn input(self: *@This(), key: vaxis.Key) void {
if (key.matches(vaxis.Key.right, .{})) {
self.scroll.x +|= 1;
} else if (key.matches(vaxis.Key.right, .{ .shift = true })) {
self.scroll.x +|= 32;
} else if (key.matches(vaxis.Key.left, .{})) {
self.scroll.x -|= 1;
} else if (key.matches(vaxis.Key.left, .{ .shift = true })) {
self.scroll.x -|= 32;
} else if (key.matches(vaxis.Key.up, .{})) {
self.scroll.y -|= 1;
} else if (key.matches(vaxis.Key.page_up, .{})) {
self.scroll.y -|= 32;
} else if (key.matches(vaxis.Key.down, .{})) {
self.scroll.y +|= 1;
} else if (key.matches(vaxis.Key.page_down, .{})) {
self.scroll.y +|= 32;
} else if (key.matches(vaxis.Key.end, .{})) {
self.scroll.y = std.math.maxInt(usize);
} else if (key.matches(vaxis.Key.home, .{})) {
self.scroll.y = 0;
}
}
/// Must be called before doing any `writeCell` calls.
pub fn draw(self: *@This(), parent: vaxis.Window, content_size: struct {
cols: usize,
rows: usize,
}) void {
var content_cols = content_size.cols;
if (self.vertical_scrollbar) |opts| {
const vbar: vaxis.widgets.Scrollbar = .{
.character = opts.character,
.style = opts.fg,
.total = content_size.rows,
.view_size = parent.height,
.top = self.scroll.y,
};
const bg = parent.child(.{
.x_off = parent.width -| opts.character.width,
.width = .{ .limit = opts.character.width },
.height = .{ .limit = parent.height },
});
bg.fill(.{ .char = opts.character, .style = opts.bg });
vbar.draw(bg);
content_cols +|= 1;
}
const max_scroll_x = content_cols -| parent.width;
const max_scroll_y = content_size.rows -| parent.height;
self.scroll.restrictTo(max_scroll_x, max_scroll_y);
}
pub const BoundingBox = struct {
x1: usize,
y1: usize,
x2: usize,
y2: usize,
pub inline fn below(self: @This(), row: usize) bool {
return row < self.y1;
}
pub inline fn above(self: @This(), row: usize) bool {
return row >= self.y2;
}
pub inline fn rowInside(self: @This(), row: usize) bool {
return row >= self.y1 and row < self.y2;
}
pub inline fn colInside(self: @This(), col: usize) bool {
return col >= self.x1 and col < self.x2;
}
pub inline fn inside(self: @This(), col: usize, row: usize) bool {
return self.rowInside(row) and self.colInside(col);
}
};
/// Boundary of the content, useful for culling to improve draw performance.
pub fn bounds(self: *@This(), parent: vaxis.Window) BoundingBox {
const right_pad: usize = if (self.vertical_scrollbar != null) 1 else 0;
return .{
.x1 = self.scroll.x,
.y1 = self.scroll.y,
.x2 = self.scroll.x +| parent.width -| right_pad,
.y2 = self.scroll.y +| parent.height,
};
}
/// Use this function instead of `Window.writeCell` to draw your cells and they will magically scroll.
pub fn writeCell(self: *@This(), parent: vaxis.Window, col: usize, row: usize, cell: vaxis.Cell) void {
const b = self.bounds(parent);
if (!b.inside(col, row)) return;
const win = parent.child(.{ .width = .{ .limit = b.x2 - b.x1 }, .height = .{ .limit = b.y2 - b.y1 } });
win.writeCell(col -| self.scroll.x, row -| self.scroll.y, cell);
}
/// Use this function instead of `Window.readCell` to read the correct cell in scrolling context.
pub fn readCell(self: *@This(), parent: vaxis.Window, col: usize, row: usize) ?vaxis.Cell {
const b = self.bounds(parent);
if (!b.inside(col, row)) return;
const win = parent.child(.{ .width = .{ .limit = b.width }, .height = .{ .limit = b.height } });
return win.readCell(col -| self.scroll.x, row -| self.scroll.y);
}