add(widget/List): initial list widget; used in tabs.zig example
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m34s
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 1m34s
This commit is contained in:
@@ -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;
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
129
src/widget/List.zig
Normal file
129
src/widget/List.zig
Normal file
@@ -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;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user