From f25b8ab42190e140c6bd4a2679b7497cc42a0c21 Mon Sep 17 00:00:00 2001 From: Jari Vetoniemi Date: Sat, 1 Jun 2024 00:10:49 +0900 Subject: [PATCH] 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. --- src/widgets.zig | 1 + src/widgets/ScrollView.zig | 129 +++++++++++++++++++++++++++++++++++++ 2 files changed, 130 insertions(+) create mode 100644 src/widgets/ScrollView.zig diff --git a/src/widgets.zig b/src/widgets.zig index d3c2e51..8773fbf 100644 --- a/src/widgets.zig +++ b/src/widgets.zig @@ -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"); diff --git a/src/widgets/ScrollView.zig b/src/widgets/ScrollView.zig new file mode 100644 index 0000000..a5a02d3 --- /dev/null +++ b/src/widgets/ScrollView.zig @@ -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); +}