From 8d689451008c6925854b569ce425067a8b5dd35a Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Sat, 23 Nov 2024 22:41:42 +0100 Subject: [PATCH] add(widget/List): initial list widget; used in tabs.zig example --- examples/tabs.zig | 8 +-- src/widget.zig | 2 + src/widget/List.zig | 129 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 src/widget/List.zig diff --git a/examples/tabs.zig b/examples/tabs.zig index ec53d8e..4c0cee3 100644 --- a/examples/tabs.zig +++ b/examples/tabs.zig @@ -67,9 +67,11 @@ pub fn main() !void { .layout = Layout.createFrom(margin: { var margin = Layout.Margin.init(allocator, .{ .margin = 10 }, .{ .widget = Widget.createFrom(blk: { - const file = try std.fs.cwd().openFile("./examples/padding.zig", .{}); - defer file.close(); - var widget = Widget.RawText.init(allocator, file); + var widget = Widget.List.init(allocator, .ordered, .{ + &[_]Cell{.{ .content = "First entry" }}, + &[_]Cell{.{ .content = "Second entry" }}, + &[_]Cell{.{ .content = "Third entry" }}, + }); break :blk &widget; }), }); diff --git a/src/widget.zig b/src/widget.zig index 8cd3b61..f748c11 100644 --- a/src/widget.zig +++ b/src/widget.zig @@ -93,11 +93,13 @@ pub fn Widget(comptime Event: type, comptime Renderer: type) type { pub const Text = @import("widget/Text.zig").Widget(Event, Renderer); pub const RawText = @import("widget/RawText.zig").Widget(Event, Renderer); pub const Spacer = @import("widget/Spacer.zig").Widget(Event, Renderer); + pub const List = @import("widget/List.zig").Widget(Event, Renderer); }; // test widget implementation satisfies the interface comptime Type.Interface.satisfiedBy(Type); comptime Type.Interface.satisfiedBy(Type.Text); comptime Type.Interface.satisfiedBy(Type.RawText); comptime Type.Interface.satisfiedBy(Type.Spacer); + comptime Type.Interface.satisfiedBy(Type.List); return Type; } diff --git a/src/widget/List.zig b/src/widget/List.zig new file mode 100644 index 0000000..bcbaf7a --- /dev/null +++ b/src/widget/List.zig @@ -0,0 +1,129 @@ +const std = @import("std"); +const terminal = @import("../terminal.zig"); + +const isTaggedUnion = @import("../event.zig").isTaggedUnion; +const Error = @import("../event.zig").Error; +const Cell = terminal.Cell; +const Size = terminal.Size; + +const log = std.log.scoped(.widget_list); + +pub fn Widget(comptime Event: type, comptime Renderer: type) type { + if (!isTaggedUnion(Event)) { + @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); + } + const ListItems = std.ArrayList([]const Cell); + return struct { + idx: usize = 0, + config: ListType = undefined, + contents: ListItems = undefined, + size: terminal.Size = undefined, + require_render: bool = false, + + const ListType = enum { + unordered, + ordered, + }; + + pub fn init(allocator: std.mem.Allocator, config: ListType, children: anytype) @This() { + const ArgsType = @TypeOf(children); + const args_type_info = @typeInfo(ArgsType); + if (args_type_info != .@"struct") { + @compileError("expected tuple or struct argument, found " ++ @typeName(ArgsType)); + } + const fields_info = args_type_info.@"struct".fields; + var contents = ListItems.initCapacity(allocator, fields_info.len) catch @panic("List.zig: out of memory"); + inline for (comptime fields_info) |field| { + const child = @field(children, field.name); + const ChildType = @TypeOf(child); + const child_type_info = @typeInfo(ChildType); + if (child_type_info != .array and child_type_info != .pointer) { + @compileError("child: " ++ field.name ++ " is not an array of const Cell but " ++ @typeName(ChildType)); + } + contents.append(child) catch {}; + } + return .{ + .config = config, + .contents = contents, + }; + } + + pub fn deinit(this: *@This()) void { + this.contents.deinit(); + this.* = undefined; + } + + pub fn handle(this: *@This(), event: Event) ?Event { + switch (event) { + // store the received size + .resize => |size| { + this.size = size; + this.require_render = true; + }, + .key => |key| { + var require_render = true; + if (key.matches(.{ .cp = 'g' })) { + // top + this.idx = 0; + } else if (key.matches(.{ .cp = 'G' })) { + // bottom + this.idx = this.contents.items.len -| 1; + } else if (key.matches(.{ .cp = 'j' })) { + // down + if (this.idx < this.contents.items.len -| 1) { + this.idx +|= 1; + } + } else if (key.matches(.{ .cp = 'k' })) { + // up + this.idx -|= 1; + } else { + require_render = false; + } + this.require_render = require_render; + }, + else => {}, + } + return null; + } + + pub fn render(this: *@This(), renderer: *Renderer) !void { + if (!this.require_render) { + return; + } + try renderer.clear(this.size); + var row: u16 = 0; + for (this.contents.items[this.idx..], this.idx + 1..) |content, num| { + var size: Size = .{ + .anchor = .{ + .col = this.size.anchor.col, + .row = this.size.anchor.row + row, + }, + .rows = this.size.rows -| row, + .cols = this.size.cols, + }; + switch (this.config) { + .unordered => { + try renderer.render(size, &[_]Cell{ + .{ .content = "•" }, + }); + size.anchor.col += 2; + size.cols -|= 2; + }, + .ordered => { + var buf: [32]u8 = undefined; + const val = try std.fmt.bufPrint(&buf, "{d}.", .{num}); + try renderer.render(size, &[_]Cell{ + .{ .content = val }, + }); + const cols: u16 = @truncate(val.len + 1); + size.anchor.col += cols; + size.cols -|= cols; + }, + } + try renderer.render(size, content); + row += 1; // NOTE: as there are no line breaks currently there will always exactly one line be written + } + this.require_render = false; + } + }; +}