From 63bef849ec0930ef232d326584095d410f2116f5 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Tue, 8 Oct 2024 15:34:48 +0200 Subject: [PATCH] add(ViewPort): wrapper for vaxis.widgets.ScrollView --- src/main.zig | 46 +++++++++++------- src/widget.zig | 3 +- src/widget/Header.zig | 35 ++++++-------- src/widget/ViewPort.zig | 102 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 146 insertions(+), 40 deletions(-) create mode 100644 src/widget/ViewPort.zig diff --git a/src/main.zig b/src/main.zig index 068a9d0..d5259c3 100644 --- a/src/main.zig +++ b/src/main.zig @@ -61,6 +61,9 @@ pub fn main() !void { var header = try widget.Header.init(alloc, &vx.unicode); defer header.deinit(); + var view_port = widget.ViewPort.init(alloc, &vx.unicode); + defer view_port.deinit(); + // Sends queries to terminal to detect certain features. This should always // be called after entering the alt screen, if you are using the alt screen try vx.queryTerminal(tty.anyWriter(), 1 * std.time.ns_per_s); @@ -69,6 +72,7 @@ pub fn main() !void { const event = loop.nextEvent(); // update widgets header.update(event); + view_port.update(event); switch (event) { .key_press => |key| { @@ -81,9 +85,15 @@ pub fn main() !void { } else if (key.matches('l', .{ .ctrl = true })) { vx.queueRefresh(); } else if (key.matches(vaxis.Key.enter, .{})) { - const title = alloc.alloc(u8, text_input.buf.buffer.len) catch @panic("OOM"); - @memcpy(title, text_input.buf.buffer); - if (loop.tryPostEvent(.{ .title = title })) { + var len: usize = 0; + for (text_input.buf.buffer) |c| { + if (c == 0xaa or c == 0) + break; + len += 1; + } + const path = alloc.alloc(u8, len) catch @panic("OOM"); + @memcpy(path, text_input.buf.buffer[0..len]); + if (loop.tryPostEvent(.{ .path = path })) { text_input.clearAndFree(); } } else { @@ -91,8 +101,8 @@ pub fn main() !void { } }, .winsize => |ws| try vx.resize(alloc, tty.anyWriter(), ws), - .title => |*title| { - alloc.free(title.*); + .path => |*path| { + alloc.free(path.*); }, } @@ -108,25 +118,25 @@ pub fn main() !void { .border = .{ .where = .all }, })); - // Create a style - const style: vaxis.Style = .{ - .fg = .{ .index = color_idx }, - }; - - // Create a bordered child window - const child = root_window.child(.{ - .x_off = root_window.width / 2 - 20, - .y_off = root_window.height / 2 - 3, + text_input.draw(root_window.child(.{ + .x_off = 20, + .y_off = 3, .width = .{ .limit = 40 }, .height = .{ .limit = 3 }, .border = .{ .where = .all, - .style = style, + .style = .{ .fg = .{ .index = color_idx } }, }, - }); + })); - // Draw the text_input in the child window - text_input.draw(child); + view_port.draw(root_window.child(.{ + .x_off = root_window.width / 4, + .y_off = 3, + .width = .{ .limit = root_window.width / 2 }, + .border = .{ + .where = .{ .other = .{ .right = true, .left = true } }, + }, + })); // Render the screen. Using a buffered writer will offer much better // performance, but is not required diff --git a/src/widget.zig b/src/widget.zig index a566682..ba23d22 100644 --- a/src/widget.zig +++ b/src/widget.zig @@ -12,7 +12,7 @@ const vaxis = @import("vaxis"); pub const Event = union(enum) { key_press: vaxis.Key, winsize: vaxis.Winsize, - title: []const u8, + path: []const u8, }; const Ptr = usize; @@ -59,3 +59,4 @@ pub fn createFrom(object: anytype) @This() { } pub const Header = @import("widget/Header.zig"); +pub const ViewPort = @import("widget/ViewPort.zig"); diff --git a/src/widget/Header.zig b/src/widget/Header.zig index c501ca7..c85fe15 100644 --- a/src/widget/Header.zig +++ b/src/widget/Header.zig @@ -8,14 +8,14 @@ const Event = widget.Event; allocator: std.mem.Allocator = undefined, unicode: *const vaxis.Unicode = undefined, -title: ?[]const u8 = undefined, +path: ?[]const u8 = undefined, view: ?vaxis.widgets.View = undefined, pub fn init(allocator: std.mem.Allocator, unicode: *const vaxis.Unicode) !@This() { return .{ .allocator = allocator, .unicode = unicode, - .title = null, + .path = null, .view = null, }; } @@ -24,8 +24,8 @@ pub fn deinit(this: *@This()) void { if (this.view) |*view| { view.*.deinit(); } - if (this.title) |*title| { - this.allocator.free(title.*); + if (this.path) |*path| { + this.allocator.free(path.*); } this.* = undefined; } @@ -49,18 +49,11 @@ fn fillView(this: *@This()) void { this.view.?.writeCell(i, 0, cell); } - if (this.title) |title| { + if (this.path) |path| { // TODO: this could be a static string on the heap (due to the size of `513`) - var len: usize = 0; - for (title) |c| { - if (c == 0xaa or c == 0) - break; - len += 1; - } - - for (title, 0..len, this.view.?.screen.width / 2 - len / 2..) |_, i, col| { + for (0..path.len, this.view.?.screen.width / 2 - path.len / 2..) |i, col| { const cell: vaxis.Cell = .{ - .char = .{ .grapheme = title[i .. i + 1] }, + .char = .{ .grapheme = path[i .. i + 1] }, .style = .{ .ul_style = .single, }, @@ -69,7 +62,7 @@ fn fillView(this: *@This()) void { } // fill rest with default cells - for (this.view.?.screen.width / 2 + len..this.view.?.screen.width) |i| { + for (this.view.?.screen.width / 2 + path.len..this.view.?.screen.width) |i| { this.view.?.writeCell(i, 0, .{ .default = true }); } } @@ -91,14 +84,14 @@ pub fn update(this: *@This(), event: Event) void { this.fillView(); } }, - .title => |title| { + .path => |path| { // TODO: try to remove the necessary amount of allocations - if (this.title) |*t| { - this.allocator.free(t.*); + if (this.path) |*p| { + this.allocator.free(p.*); } - const t = this.allocator.alloc(u8, title.len) catch @panic("OOM"); - @memcpy(t, title); - this.title = t; + const p = this.allocator.alloc(u8, path.len) catch @panic("OOM"); + @memcpy(p, path); + this.path = p; this.fillView(); }, else => {}, diff --git a/src/widget/ViewPort.zig b/src/widget/ViewPort.zig new file mode 100644 index 0000000..08df345 --- /dev/null +++ b/src/widget/ViewPort.zig @@ -0,0 +1,102 @@ +//! ViewPort widget, which show the content of a file as a pager with corresponding navigation of the view port + +const std = @import("std"); +const vaxis = @import("vaxis"); + +const widget = @import("../widget.zig"); + +const Event = widget.Event; + +allocator: std.mem.Allocator = undefined, +unicode: *const vaxis.Unicode = undefined, +buffer: vaxis.widgets.TextView.Buffer = undefined, +view: vaxis.widgets.ScrollView = undefined, + +pub fn init(allocator: std.mem.Allocator, unicode: *const vaxis.Unicode) @This() { + return .{ + .allocator = allocator, + .unicode = unicode, + .buffer = .{}, + .view = .{ .vertical_scrollbar = .{} }, + }; +} + +pub fn deinit(this: *@This()) void { + this.buffer.deinit(this.allocator); + this.* = undefined; +} + +/// Update loop for a given widget to react to the provided `Event`. It may +/// change its internal state, update variables, react to user input, etc. +pub fn update(this: *@This(), event: Event) void { + switch (event) { + .key_press => |key| { + if (key.matches(vaxis.Key.right, .{}) or key.matches('l', .{})) { + this.view.scroll.x +|= 1; + } else if (key.matches(vaxis.Key.right, .{ .shift = true }) or key.matches('>', .{})) { + this.view.scroll.x +|= 32; + } else if (key.matches(vaxis.Key.left, .{}) or key.matches('h', .{})) { + this.view.scroll.x -|= 1; + } else if (key.matches(vaxis.Key.left, .{ .shift = true }) or key.matches('<', .{})) { + this.view.scroll.x -|= 32; + } else if (key.matches(vaxis.Key.up, .{}) or key.matches('k', .{})) { + this.view.scroll.y -|= 1; + } else if (key.matches(vaxis.Key.page_up, .{}) or key.matches('u', .{ .ctrl = true })) { + this.view.scroll.y -|= 32; + } else if (key.matches(vaxis.Key.down, .{}) or key.matches('j', .{})) { + this.view.scroll.y +|= 1; + } else if (key.matches(vaxis.Key.page_down, .{}) or key.matches('d', .{ .ctrl = true })) { + this.view.scroll.y +|= 32; + } else if (key.matches(vaxis.Key.end, .{}) or key.matches('G', .{})) { + this.view.scroll.y = std.math.maxInt(usize); + } else if (key.matches(vaxis.Key.home, .{}) or key.matches('g', .{})) { + this.view.scroll.y = 0; + } + }, + .path => |path| { + const file = std.fs.cwd().openFile(path, .{ .mode = .read_only }) catch |err| { + // TODO: in case of an error show an error-page or an error notification? + std.log.debug("could not open file: {s} due to {any}", .{ path, err }); + return; + }; + defer file.close(); + + this.buffer.clear(this.allocator); + const writer = this.buffer.writer(this.allocator, &this.unicode.grapheme_data, &this.unicode.width_data); + file.reader().streamUntilDelimiter(writer, 0, null) catch {}; + }, + else => {}, + } +} + +/// Draw a given widget using the provided `vaxis.Window`. The window controls +/// the dimension one widget may take on the screen. The widget itself has no +/// control over this. +pub fn draw(this: *@This(), win: vaxis.Window) void { + this.view.draw(win, .{ .cols = this.buffer.cols, .rows = this.buffer.rows }); + const Pos = struct { x: usize = 0, y: usize = 0 }; + var pos: Pos = .{}; + const bounds = this.view.bounds(win); + + for (this.buffer.grapheme.items(.len), this.buffer.grapheme.items(.offset), 0..) |g_len, g_offset, index| { + if (bounds.above(pos.y)) break; + + const cluster = this.buffer.content.items[g_offset..][0..g_len]; + if (std.mem.eql(u8, cluster, "\n")) { + if (index == this.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; + + this.view.writeCell(win, pos.x, pos.y, .{ .char = .{ .grapheme = cluster, .width = width }, .style = .{} }); + } +}