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, + }); + } +}