From 84b4821d433067a0a7133637f603b9344b11f531 Mon Sep 17 00:00:00 2001 From: Jari Vetoniemi Date: Sat, 1 Jun 2024 00:14:05 +0900 Subject: [PATCH] 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. --- src/widgets.zig | 1 + src/widgets/TextView.zig | 186 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) create mode 100644 src/widgets/TextView.zig diff --git a/src/widgets.zig b/src/widgets.zig index 5239b6e..5119e11 100644 --- a/src/widgets.zig +++ b/src/widgets.zig @@ -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"); diff --git a/src/widgets/TextView.zig b/src/widgets/TextView.zig new file mode 100644 index 0000000..97f2943 --- /dev/null +++ b/src/widgets/TextView.zig @@ -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, + }); + } +}