widgets: add TextView widget

The TextView widget provides a simple way to show text content in
a scrollable window.

This implementation does not offer automatic line wrapping for now.

Text content is backed by the Buffer struct. The buffer can be styled
for visually appealing and colorful content, for example to provide
a syntax highlighting.

The buffer provides a writer() interface which allows providing content
from a stream, such as logging.
This commit is contained in:
Jari Vetoniemi 2024-06-01 00:14:05 +09:00 committed by Tim Culverhouse
parent b215aec75b
commit 84b4821d43
2 changed files with 187 additions and 0 deletions

View file

@ -8,3 +8,4 @@ pub const TextInput = @import("widgets/TextInput.zig");
pub const nvim = @import("widgets/nvim.zig");
pub const ScrollView = @import("widgets/ScrollView.zig");
pub const LineNumbers = @import("widgets/LineNumbers.zig");
pub const TextView = @import("widgets/TextView.zig");

186
src/widgets/TextView.zig Normal file
View file

@ -0,0 +1,186 @@
const std = @import("std");
const vaxis = @import("../main.zig");
const grapheme = @import("grapheme");
const DisplayWidth = @import("DisplayWidth");
const ScrollView = vaxis.widgets.ScrollView;
pub const BufferWriter = struct {
pub const Error = error{OutOfMemory};
pub const Writer = std.io.GenericWriter(@This(), Error, write);
allocator: std.mem.Allocator,
buffer: *Buffer,
gd: *const grapheme.GraphemeData,
wd: *const DisplayWidth.DisplayWidthData,
pub fn write(self: @This(), bytes: []const u8) Error!usize {
try self.buffer.append(self.allocator, .{
.bytes = bytes,
.gd = self.gd,
.wd = self.wd,
});
return bytes.len;
}
pub fn writer(self: @This()) Writer {
return .{ .context = self };
}
};
pub const Buffer = struct {
const StyleList = std.ArrayListUnmanaged(vaxis.Style);
const StyleMap = std.HashMapUnmanaged(usize, usize, std.hash_map.AutoContext(usize), std.hash_map.default_max_load_percentage);
pub const Content = struct {
bytes: []const u8,
gd: *const grapheme.GraphemeData,
wd: *const DisplayWidth.DisplayWidthData,
};
pub const Style = struct {
begin: usize,
end: usize,
style: vaxis.Style,
};
grapheme: std.MultiArrayList(grapheme.Grapheme) = .{},
content: std.ArrayListUnmanaged(u8) = .{},
style_list: StyleList = .{},
style_map: StyleMap = .{},
rows: usize = 0,
cols: usize = 0,
pub fn deinit(self: *@This(), allocator: std.mem.Allocator) void {
self.style_map.deinit(allocator);
self.style_list.deinit(allocator);
self.grapheme.deinit(allocator);
self.content.deinit(allocator);
self.* = undefined;
}
/// Clears all buffer data.
pub fn clear(self: *@This(), allocator: std.mem.Allocator) void {
self.deinit(allocator);
self.* = .{};
}
/// Replaces contents of the buffer, all previous buffer data is lost.
pub fn update(self: *@This(), allocator: std.mem.Allocator, content: Content) !void {
self.clear(allocator);
errdefer self.clear(allocator);
try self.append(allocator, content);
}
/// Appends content to the buffer.
pub fn append(self: *@This(), allocator: std.mem.Allocator, content: Content) !void {
var cols: usize = 0;
var iter = grapheme.Iterator.init(content.bytes, content.gd);
const dw: DisplayWidth = .{ .data = content.wd };
while (iter.next()) |g| {
try self.grapheme.append(allocator, .{
.len = g.len,
.offset = @as(u32, @intCast(self.content.items.len)) + g.offset,
});
const cluster = g.bytes(content.bytes);
if (std.mem.eql(u8, cluster, "\n")) {
self.cols = @max(self.cols, cols);
cols = 0;
continue;
}
cols +|= dw.strWidth(cluster);
}
try self.content.appendSlice(allocator, content.bytes);
self.cols = @max(self.cols, cols);
self.rows +|= std.mem.count(u8, content.bytes, "\n");
}
/// Clears all styling data.
pub fn clearStyle(self: *@This(), allocator: std.mem.Allocator) void {
self.style_list.deinit(allocator);
self.style_map.deinit(allocator);
}
/// Update style for range of the buffer contents.
pub fn updateStyle(self: *@This(), allocator: std.mem.Allocator, style: Style) !void {
const style_index = blk: {
for (self.style_list.items, 0..) |s, i| {
if (std.meta.eql(s, style.style)) {
break :blk i;
}
}
try self.style_list.append(allocator, style.style);
break :blk self.style_list.items.len - 1;
};
for (style.begin..style.end) |i| {
try self.style_map.put(allocator, i, style_index);
}
}
pub fn writer(
self: *@This(),
allocator: std.mem.Allocator,
gd: *const grapheme.GraphemeData,
wd: *const DisplayWidth.DisplayWidthData,
) BufferWriter.Writer {
return .{
.context = .{
.allocator = allocator,
.buffer = self,
.gd = gd,
.wd = wd,
},
};
}
};
scroll_view: ScrollView = .{},
pub fn input(self: *@This(), key: vaxis.Key) void {
self.scroll_view.input(key);
}
pub fn draw(self: *@This(), win: vaxis.Window, buffer: Buffer) void {
self.scroll_view.draw(win, .{ .cols = buffer.cols, .rows = buffer.rows });
const Pos = struct { x: usize = 0, y: usize = 0 };
var pos: Pos = .{};
var byte_index: usize = 0;
const bounds = self.scroll_view.bounds(win);
for (buffer.grapheme.items(.len), buffer.grapheme.items(.offset), 0..) |g_len, g_offset, index| {
if (bounds.above(pos.y)) {
break;
}
const cluster = buffer.content.items[g_offset..][0..g_len];
defer byte_index += cluster.len;
if (std.mem.eql(u8, cluster, "\n")) {
if (index == buffer.grapheme.len - 1) {
break;
}
pos.y +|= 1;
pos.x = 0;
continue;
} else if (bounds.below(pos.y)) {
continue;
}
const width = win.gwidth(cluster);
defer pos.x +|= width;
if (!bounds.colInside(pos.x)) {
continue;
}
const style: vaxis.Style = blk: {
if (buffer.style_map.get(byte_index)) |style_index| {
break :blk buffer.style_list.items[style_index];
}
break :blk .{};
};
self.scroll_view.writeCell(win, pos.x, pos.y, .{
.char = .{ .grapheme = cluster, .width = width },
.style = style,
});
}
}