mod(structure): update project structure

Remove examples, add description for design goals in README.md and
apply re-names and naming changes accordingly for the project structure.
Implement a flat hierachry, as the library shall remain pretty simple.
This commit is contained in:
2025-01-30 23:02:34 +01:00
parent 3decc541a9
commit bdbe05c996
41 changed files with 204 additions and 3474 deletions

View File

@@ -6,7 +6,7 @@ const event = @import("event.zig");
const mergeTaggedUnions = event.mergeTaggedUnions;
const isTaggedUnion = event.isTaggedUnion;
const Key = terminal.Key;
const Key = @import("key.zig");
const Queue = @import("queue.zig").Queue;
const log = std.log.scoped(.app);

View File

@@ -1,27 +1,20 @@
const std = @import("std");
pub const Style = @import("Style.zig");
const Style = @import("Style.zig");
pub const Cell = @This();
style: Style = .{},
rune: u8 = ' ',
pub const Character = struct {
grapheme: []const u8,
width: u8,
};
pub fn eql(this: @This(), other: @This()) bool {
pub fn eql(this: Cell, other: Cell) bool {
return this.rune == other.rune and this.style.eql(other.style);
}
pub fn reset(this: *@This()) void {
pub fn reset(this: *Cell) void {
this.style = .{ .fg = .default, .bg = .default, .ul = .default, .ul_style = .off };
this.rune = ' ';
}
pub fn value(this: @This(), writer: anytype) !void {
pub fn value(this: Cell, writer: anytype) !void {
try this.style.value(writer, this.rune);
}
test {
_ = Style;
}

86
src/color.zig Normal file
View File

@@ -0,0 +1,86 @@
const std = @import("std");
pub const Color = union(enum) {
default,
index: u8,
rgb: [3]u8,
pub fn eql(a: Color, b: Color) bool {
switch (a) {
.default => return b == .default,
.index => |a_idx| {
switch (b) {
.index => |b_idx| return a_idx == b_idx,
else => return false,
}
},
.rgb => |a_rgb| {
switch (b) {
.rgb => |b_rgb| return a_rgb[0] == b_rgb[0] and
a_rgb[1] == b_rgb[1] and
a_rgb[2] == b_rgb[2],
else => return false,
}
},
}
}
pub fn rgbFromUint(val: u24) Color {
const r_bits = val & 0b11111111_00000000_00000000;
const g_bits = val & 0b00000000_11111111_00000000;
const b_bits = val & 0b00000000_00000000_11111111;
const rgb = [_]u8{
@truncate(r_bits >> 16),
@truncate(g_bits >> 8),
@truncate(b_bits),
};
return .{ .rgb = rgb };
}
/// parse an XParseColor-style rgb specification into an rgb Color. The spec
/// is of the form: rgb:rrrr/gggg/bbbb. Generally, the high two bits will always
/// be the same as the low two bits.
pub fn rgbFromSpec(spec: []const u8) !Color {
var iter = std.mem.splitScalar(u8, spec, ':');
const prefix = iter.next() orelse return error.InvalidColorSpec;
if (!std.mem.eql(u8, "rgb", prefix)) return error.InvalidColorSpec;
const spec_str = iter.next() orelse return error.InvalidColorSpec;
var spec_iter = std.mem.splitScalar(u8, spec_str, '/');
const r_raw = spec_iter.next() orelse return error.InvalidColorSpec;
if (r_raw.len != 4) return error.InvalidColorSpec;
const g_raw = spec_iter.next() orelse return error.InvalidColorSpec;
if (g_raw.len != 4) return error.InvalidColorSpec;
const b_raw = spec_iter.next() orelse return error.InvalidColorSpec;
if (b_raw.len != 4) return error.InvalidColorSpec;
const r = try std.fmt.parseUnsigned(u8, r_raw[2..], 16);
const g = try std.fmt.parseUnsigned(u8, g_raw[2..], 16);
const b = try std.fmt.parseUnsigned(u8, b_raw[2..], 16);
return .{
.rgb = [_]u8{ r, g, b },
};
}
test "rgbFromSpec" {
const spec = "rgb:aaaa/bbbb/cccc";
const actual = try rgbFromSpec(spec);
switch (actual) {
.rgb => |rgb| {
try std.testing.expectEqual(0xAA, rgb[0]);
try std.testing.expectEqual(0xBB, rgb[1]);
try std.testing.expectEqual(0xCC, rgb[2]);
},
else => try std.testing.expect(false),
}
}
};
test {
_ = Color;
}

0
src/container.zig Normal file
View File

0
src/element.zig Normal file
View File

View File

@@ -3,26 +3,31 @@
const std = @import("std");
const terminal = @import("terminal.zig");
const Size = terminal.Size;
const Key = terminal.Key;
pub const Error = struct {
err: anyerror,
msg: []const u8,
};
const Size = @import("size.zig");
const Key = @import("key.zig");
// System events available to every application.
// TODO: should this also already include the .view enum option?
pub const SystemEvent = union(enum) {
/// Initialize event, which is send once at the beginning of the event loop and before the first render loop
init,
/// Quit event to signify the end of the event loop (rendering should stop afterwards)
quit,
err: Error,
/// Error event to notify other containers about a recoverable error
err: struct {
err: anyerror,
/// associated error message
msg: []const u8,
},
/// Resize event emitted by the terminal to derive the `Size` of the current terminal the application is rendered in
resize: Size,
/// Input key event received from the user
key: Key,
/// Focus event for mouse interaction
/// TODO: this should instead be a union with a `Size` to derive which container / element the focus meant for
focus: bool,
};
pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type {
// TODO: should this expect one of the unions to contain the .view value option with its corresponding associated type?
if (!isTaggedUnion(A) or !isTaggedUnion(B)) {
@compileError("Both types for merging tagged unions need to be of type `union(enum)`.");
}

View File

@@ -1,6 +1,8 @@
//! Keybindings and Modifiers for user input detection and selection.
const std = @import("std");
pub const Key = @This();
pub const Modifier = struct {
shift: bool = false,
alt: bool = false,

View File

@@ -1,106 +0,0 @@
//! Dynamic dispatch for layout implementations. Each `Layout` has to implement
//! the `Layout.Interface`.
//!
//! Create a `Layout` using `createFrom(object: anytype)` and use them through
//! the defined `Layout.Interface`. The layout will take care of calling the
//! correct implementation of the corresponding underlying type.
//!
//! Each `Layout` is responsible for clearing the allocated memory of the used
//! `Element`s (union of `Layout` or `Widget`) when deallocated. This means
//! that `deinit()` will also deallocate every used `Element` too.
//!
//! When `Layout.render` is called the provided `Renderer` type is expected
//! which handles how contents are rendered for a given layout.
const std = @import("std");
const isTaggedUnion = @import("event.zig").isTaggedUnion;
pub fn Layout(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 Events = std.ArrayList(Event);
const Type = struct {
const LayoutType = @This();
const Element = union(enum) {
layout: LayoutType,
widget: @import("widget.zig").Widget(Event, Renderer),
};
pub const Interface = @import("interface").Interface(.{
.handle = fn (anytype, Event) anyerror!*Events,
.render = fn (anytype, *Renderer) anyerror!void,
.deinit = fn (anytype) void,
}, .{});
const VTable = struct {
handle: *const fn (this: *LayoutType, event: Event) anyerror!*Events,
render: *const fn (this: *LayoutType, renderer: *Renderer) anyerror!void,
deinit: *const fn (this: *LayoutType) void,
};
object: *anyopaque = undefined,
vtable: *const VTable = undefined,
// Handle the provided `Event` for this `Layout`.
pub fn handle(this: *LayoutType, event: Event) !*Events {
return try this.vtable.handle(this, event);
}
// Render this `Layout` completely. This will render contained sub-elements too.
pub fn render(this: *LayoutType, renderer: *Renderer) !void {
return try this.vtable.render(this, renderer);
}
pub fn deinit(this: *LayoutType) void {
this.vtable.deinit(this);
}
pub fn createFrom(object: anytype) LayoutType {
return LayoutType{
.object = @ptrCast(@alignCast(object)),
.vtable = &.{
.handle = struct {
// Handle the provided `Event` for this `Layout`.
fn handle(this: *LayoutType, event: Event) !*Events {
const layout: @TypeOf(object) = @ptrCast(@alignCast(this.object));
return try layout.handle(event);
}
}.handle,
.render = struct {
// Render the contents of this `Layout`.
fn render(this: *LayoutType, renderer: *Renderer) !void {
const layout: @TypeOf(object) = @ptrCast(@alignCast(this.object));
try layout.render(renderer);
}
}.render,
.deinit = struct {
fn deinit(this: *LayoutType) void {
const layout: @TypeOf(object) = @ptrCast(@alignCast(this.object));
layout.deinit();
}
}.deinit,
},
};
}
// import and export of `Layout` implementations
pub const HContainer = @import("layout/HContainer.zig").Layout(Event, Element, Renderer);
pub const HStack = @import("layout/HStack.zig").Layout(Event, Element, Renderer);
pub const VContainer = @import("layout/VContainer.zig").Layout(Event, Element, Renderer);
pub const VStack = @import("layout/VStack.zig").Layout(Event, Element, Renderer);
pub const Padding = @import("layout/Padding.zig").Layout(Event, Element, Renderer);
pub const Margin = @import("layout/Margin.zig").Layout(Event, Element, Renderer);
pub const Framing = @import("layout/Framing.zig").Layout(Event, Element, Renderer);
pub const Tab = @import("layout/Tab.zig").Layout(Event, Element, Renderer);
};
// test layout implementation satisfies the interface
comptime Type.Interface.satisfiedBy(Type);
comptime Type.Interface.satisfiedBy(Type.HContainer);
comptime Type.Interface.satisfiedBy(Type.HStack);
comptime Type.Interface.satisfiedBy(Type.VContainer);
comptime Type.Interface.satisfiedBy(Type.VStack);
comptime Type.Interface.satisfiedBy(Type.Padding);
comptime Type.Interface.satisfiedBy(Type.Margin);
comptime Type.Interface.satisfiedBy(Type.Framing);
comptime Type.Interface.satisfiedBy(Type.Tab);
return Type;
}

View File

@@ -1,198 +0,0 @@
//! Framing layout for a nested `Layout`s or `Widget`s.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const Style = terminal.Cell.Style;
const log = std.log.scoped(.layout_framing);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Events = std.ArrayList(Event);
return struct {
allocator: std.mem.Allocator,
size: terminal.Size,
require_render: bool,
element: Element,
events: Events,
config: Config,
const Config = struct {
style: Style = .{ .fg = .default },
frame: Frame = .round,
title: Title = .{},
const Title = struct {
str: []const u8 = &.{},
style: Style = .{ .fg = .default },
};
const Frame = enum {
round,
square,
};
};
pub fn init(allocator: std.mem.Allocator, config: Config, element: Element) *@This() {
var this = allocator.create(@This()) catch @panic("Framing.zig: Failed to create.");
this.allocator = allocator;
this.require_render = true;
this.config = config;
this.element = element;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
switch ((&this.element).*) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
// order is important
switch (event) {
.resize => |size| {
this.size = size;
this.require_render = true;
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
// adjust size according to the containing elements
const sub_event: Event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col + 1,
.row = size.anchor.row + 1,
},
.cols = size.cols -| 2,
.rows = size.rows -| 2,
},
};
switch ((&this.element).*) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
},
else => {
switch ((&this.element).*) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
},
}
return &this.events;
}
const round_frame: [6][]const u8 = .{ "", "", "", "", "", "" };
const square_frame: [6][]const u8 = .{ "", "", "", "", "", "" };
fn renderFrame(this: *@This(), renderer: *Renderer) !void {
// FIXME: use renderer instead!
_ = renderer;
const frame = switch (this.config.frame) {
.round => round_frame,
.square => square_frame,
};
std.debug.assert(frame.len == 6);
// render top: +---+
try terminal.setCursorPosition(this.size.anchor);
const writer = terminal.writer();
// try this.config.style.value(writer, frame[0]);
for (0..this.config.title.str.len) |i| {
try this.config.title.style.value(writer, this.config.title.str[i]);
}
for (0..this.size.cols -| 2 -| this.config.title.str.len) |_| {
// try this.config.style.value(writer, frame[1]);
}
// try this.config.style.value(writer, frame[2]);
// render left: |
for (1..this.size.rows -| 1) |r| {
const row: u16 = @truncate(r);
try terminal.setCursorPosition(.{
.col = this.size.anchor.col,
.row = this.size.anchor.row + row,
});
// try this.config.style.value(writer, frame[3]);
}
// render right: |
for (1..this.size.rows -| 1) |r| {
const row: u16 = @truncate(r);
try terminal.setCursorPosition(.{
.col = this.size.anchor.col + this.size.cols -| 1,
.row = this.size.anchor.row + row,
});
// try this.config.style.value(writer, frame[3]);
}
// render bottom: +---+
try terminal.setCursorPosition(.{
.col = this.size.anchor.col,
.row = this.size.anchor.row + this.size.rows - 1,
});
// try this.config.style.value(writer, frame[4]);
for (0..this.size.cols -| 2) |_| {
// try this.config.style.value(writer, frame[1]);
}
// try this.config.style.value(writer, frame[5]);
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (this.require_render) {
try renderer.clear(this.size);
try this.renderFrame(renderer);
this.require_render = false;
}
switch ((&this.element).*) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
};
}

View File

@@ -1,187 +0,0 @@
//! Horizontal Container layout for nested `Layout`s and/or `Widget`s.
//! The contained elements are sized according to the provided configuration.
//! For an evenly spaced horizontal stacking see the `HStack` layout.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const log = std.log.scoped(.layout_hcontainer);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Container = struct {
element: Element,
container_size: u8, // 0 - 100 %
};
const Containers = std.ArrayList(Container);
const LayoutType = @typeInfo(Element).@"union".fields[0].type;
const WidgetType = @typeInfo(Element).@"union".fields[1].type;
const Events = std.ArrayList(Event);
return struct {
// TODO: current focused `Element`?
allocator: std.mem.Allocator,
size: terminal.Size,
containers: Containers,
events: Events,
pub fn init(allocator: std.mem.Allocator, 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));
}
comptime var total_size = 0;
const fields_info = args_type_info.@"struct".fields;
var containers = Containers.initCapacity(allocator, fields_info.len) catch @panic("HContainer.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 != .@"struct") {
@compileError("expected tuple or struct as child type, found " ++ @typeName(ChildType));
}
const child_fields = child_type_info.@"struct".fields;
if (child_fields.len != 2) {
@compileError("expected nested tuple or struct to have exactly 2 fields, but found " ++ child_fields.len);
}
const element = @field(child, child_fields[0].name);
const ElementType = @TypeOf(element);
const element_size = @field(child, child_fields[1].name);
const ElementSizeType = @TypeOf(element_size);
if (ElementSizeType != u8 and ElementSizeType != comptime_int) {
@compileError("expected an u8 or comptime_int as second argument of nested tuple or struct child, but found " ++ @typeName(ElementSizeType));
}
total_size += element_size;
if (total_size > 100) {
@compileError("cannot place element: " ++ child_fields[0].name ++ " as total size of used container elements would overflow");
}
if (ElementType == WidgetType) {
containers.append(.{
.element = .{ .widget = element },
.container_size = element_size,
}) catch {};
continue;
}
if (ElementType == LayoutType) {
containers.append(.{
.element = .{ .layout = element },
.container_size = element_size,
}) catch {};
continue;
}
@compileError("nested child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
}
var this = allocator.create(@This()) catch @panic("HContainer.zig: Failed to create.");
this.allocator = allocator;
this.containers = containers;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
for (this.containers.items) |*container| {
switch (container.element) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
}
this.containers.deinit();
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
// order is important
switch (event) {
.resize => |size| {
this.size = size;
// adjust size according to the containing elements
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
// adjust size according to the container size
var offset: u16 = 0;
for (this.containers.items) |*container| {
const cols = @divTrunc(size.cols * container.container_size, 100);
const sub_event: Event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col + offset,
.row = size.anchor.row,
},
.cols = cols,
.rows = size.rows,
},
};
offset += cols;
switch (container.element) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
}
},
else => {
for (this.containers.items) |*container| {
switch (container.element) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
}
},
}
return &this.events;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
for (this.containers.items) |*container| {
switch (container.element) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
}
};
}

View File

@@ -1,162 +0,0 @@
//! Horizontal Stacking layout for nested `Layout`s and/or `Widget`s.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const log = std.log.scoped(.layout_hstack);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Elements = std.ArrayList(Element);
const LayoutType = @typeInfo(Element).@"union".fields[0].type;
const WidgetType = @typeInfo(Element).@"union".fields[1].type;
const Events = std.ArrayList(Event);
return struct {
// TODO: current focused `Element`?
allocator: std.mem.Allocator,
size: terminal.Size,
elements: Elements,
events: Events,
pub fn init(allocator: std.mem.Allocator, 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 elements = Elements.initCapacity(allocator, fields_info.len) catch @panic("HStack.zig: out of memory");
inline for (comptime fields_info) |field| {
const child = @field(children, field.name);
const ChildType = @TypeOf(child);
if (ChildType == WidgetType) {
elements.append(.{ .widget = child }) catch {};
continue;
}
if (ChildType == LayoutType) {
elements.append(.{ .layout = child }) catch {};
continue;
}
@compileError("child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
}
var this = allocator.create(@This()) catch @panic("HStack.zig: Failed to create.");
this.allocator = allocator;
this.elements = elements;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
for (this.elements.items) |*element| {
switch (element.*) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
}
this.elements.deinit();
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
// order is important
switch (event) {
.resize => |size| {
this.size = size;
// adjust size according to the containing elements
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
const len: u16 = @truncate(this.elements.items.len);
const element_cols = @divTrunc(size.cols, len);
var overflow = size.cols % len;
var offset: u16 = 0;
// adjust size according to the containing elements
for (this.elements.items) |*element| {
var cols = element_cols;
if (overflow > 0) {
overflow -|= 1;
cols += 1;
}
const sub_event: Event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col + offset,
.row = size.anchor.row,
},
.cols = cols,
.rows = size.rows,
},
};
offset += cols;
switch (element.*) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
}
},
else => {
for (this.elements.items) |*element| {
switch (element.*) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
}
},
}
return &this.events;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
for (this.elements.items) |*element| {
switch (element.*) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
}
};
}

View File

@@ -1,164 +0,0 @@
//! Margin layout for a nested `Layout`s or `Widget`s.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const log = std.log.scoped(.layout_margin);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Events = std.ArrayList(Event);
return struct {
allocator: std.mem.Allocator,
size: terminal.Size,
require_render: bool,
element: Element,
events: Events,
config: Config,
const Config = struct {
margin: ?u8 = null,
left: u8 = 0,
right: u8 = 0,
top: u8 = 0,
bottom: u8 = 0,
};
pub fn init(allocator: std.mem.Allocator, config: Config, element: Element) *@This() {
if (config.margin) |margin| {
std.debug.assert(margin <= 50);
} else {
std.debug.assert(config.left + config.right < 100);
std.debug.assert(config.top + config.bottom < 100);
}
var this = allocator.create(@This()) catch @panic("Margin.zig: Failed to create.");
this.allocator = allocator;
this.require_render = true;
this.config = config;
this.element = element;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
switch ((&this.element).*) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
// order is important
switch (event) {
.resize => |size| {
this.size = size;
this.require_render = true;
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
var sub_event: Event = undefined;
if (this.config.margin) |margin| {
// used overall margin
const h_margin: u16 = @divTrunc(margin * size.cols, 100);
const v_margin: u16 = @divFloor(margin * size.rows, 100);
sub_event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col + h_margin,
.row = size.anchor.row + v_margin,
},
.cols = size.cols -| (h_margin * 2),
.rows = size.rows -| (v_margin * 2),
},
};
} else {
// use all for directions individually
const left_margin: u16 = @divFloor(this.config.left * size.cols, 100);
const right_margin: u16 = @divFloor(this.config.right * size.cols, 100);
const top_margin: u16 = @divFloor(this.config.top * size.rows, 100);
const bottom_margin: u16 = @divFloor(this.config.bottom * size.rows, 100);
sub_event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col + left_margin,
.row = size.anchor.row + top_margin,
},
.cols = size.cols -| left_margin -| right_margin,
.rows = size.rows -| top_margin -| bottom_margin,
},
};
}
// adjust size according to the containing elements
switch ((&this.element).*) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
},
else => {
switch ((&this.element).*) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
},
}
return &this.events;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (this.require_render) {
try renderer.clear(this.size);
this.require_render = false;
}
switch ((&this.element).*) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
};
}

View File

@@ -1,152 +0,0 @@
//! Padding layout for a nested `Layout`s or `Widget`s.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const log = std.log.scoped(.layout_padding);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Events = std.ArrayList(Event);
return struct {
allocator: std.mem.Allocator,
size: terminal.Size,
require_render: bool,
element: Element,
events: Events,
config: Config,
const Config = struct {
padding: ?u16 = null,
left: u16 = 0,
right: u16 = 0,
top: u16 = 0,
bottom: u16 = 0,
};
pub fn init(allocator: std.mem.Allocator, config: Config, element: Element) *@This() {
var this = allocator.create(@This()) catch @panic("Padding.zig: Failed to create.");
this.allocator = allocator;
this.require_render = true;
this.config = config;
this.element = element;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
switch ((&this.element).*) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
// order is important
switch (event) {
.resize => |size| {
this.size = size;
this.require_render = true;
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
var sub_event: Event = undefined;
if (this.config.padding) |padding| {
// used overall padding
sub_event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col + padding,
.row = size.anchor.row + padding,
},
.cols = size.cols -| (padding * 2),
.rows = size.rows -| (padding * 2),
},
};
} else {
// use all for directions individually
sub_event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col + this.config.left,
.row = size.anchor.row + this.config.top,
},
.cols = size.cols -| this.config.left -| this.config.right,
.rows = size.rows -| this.config.top -| this.config.bottom,
},
};
}
// adjust size according to the containing elements
switch ((&this.element).*) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
},
else => {
switch ((&this.element).*) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
},
}
return &this.events;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (this.require_render) {
try renderer.clear(this.size);
this.require_render = false;
}
switch ((&this.element).*) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
};
}

View File

@@ -1,314 +0,0 @@
//! Tab layout for a nested `Layout`s or `Widget`s.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const Cell = terminal.Cell;
const Style = Cell.Style;
const Color = Style.Color;
const log = std.log.scoped(.layout_tab);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Events = std.ArrayList(Event);
const LayoutType = @typeInfo(Element).@"union".fields[0].type;
const WidgetType = @typeInfo(Element).@"union".fields[1].type;
const Tab = struct {
element: Element,
title: []const u8,
color: Color,
};
const Tabs = std.ArrayList(Tab);
return struct {
allocator: std.mem.Allocator,
size: terminal.Size,
require_render: bool,
tabs: Tabs,
active_tab: usize,
events: Events,
config: Config,
const Config = struct {
frame: Frame = .round,
const Frame = enum {
round,
square,
};
};
pub fn init(allocator: std.mem.Allocator, config: Config, 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 tabs = Tabs.initCapacity(allocator, fields_info.len) catch @panic("Tab.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 != .@"struct") {
@compileError("expected tuple or struct as child type, found " ++ @typeName(ChildType));
}
const child_fields = child_type_info.@"struct".fields;
if (child_fields.len != 3) {
@compileError("expected nested tuple or struct to have exactly 3 fields, but found " ++ child_fields.len);
}
const element = @field(child, child_fields[0].name);
const ElementType = @TypeOf(element);
const tab_title = @field(child, child_fields[1].name);
const TabTitleType = @TypeOf(tab_title);
const tab_title_type_info = @typeInfo(TabTitleType);
const tab_color = @field(child, child_fields[2].name);
const TabColorType = @TypeOf(tab_color);
if (tab_title_type_info != .array and tab_title_type_info != .pointer) {
// TODO: check for inner type of the title to be u8
@compileError("expected an u8 array second argument of nested tuple or struct child, but found " ++ @tagName(tab_title_type_info));
}
if (TabColorType != Color) {
@compileError("expected an Color typed third argument of nested tuple or struct child, but found " ++ @typeName(TabColorType));
}
if (ElementType == WidgetType) {
tabs.append(.{
.element = .{ .widget = element },
.title = tab_title,
.color = tab_color,
}) catch {};
continue;
}
if (ElementType == LayoutType) {
tabs.append(.{
.element = .{ .layout = element },
.title = tab_title,
.color = tab_color,
}) catch {};
continue;
}
@compileError("nested child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
}
var this = allocator.create(@This()) catch @panic("Tab.zig: Failed to create.");
this.allocator = allocator;
this.active_tab = 0;
this.require_render = true;
this.config = config;
this.tabs = tabs;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
for (this.tabs.items) |*tab| {
switch (tab.element) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
}
this.tabs.deinit();
this.allocator.destroy(this);
}
fn resize_active_tab(this: *@This()) !void {
const sub_event: Event = .{
.resize = .{
.anchor = .{
.col = this.size.anchor.col + 1,
.row = this.size.anchor.row + 1,
},
.cols = this.size.cols -| 2,
.rows = this.size.rows -| 2,
},
};
// resize active tab to re-render the widget in the following render loop
var tab = this.tabs.items[this.active_tab];
switch ((&tab.element).*) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
if (this.tabs.items.len == 0) {
return &this.events;
}
// order is important
switch (event) {
.resize => |size| {
this.size = size;
this.require_render = true;
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
try this.resize_active_tab();
},
.key => |key| {
// tab -> cycle forward
// back-tab -> cycle backward
if (key.matches(.{ .cp = Key.tab })) {
this.active_tab += 1;
this.active_tab %= this.tabs.items.len;
this.require_render = true;
try this.resize_active_tab();
} else if (key.matches(.{ .cp = Key.tab, .mod = .{ .shift = true } })) { // backtab / shift + tab
if (this.active_tab > 0) {
this.active_tab -|= 1;
} else {
this.active_tab = this.tabs.items.len - 1;
}
this.require_render = true;
try this.resize_active_tab();
} else {
// TODO: absorb tab key or send key down too?
var tab = this.tabs.items[this.active_tab];
switch ((&tab.element).*) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
}
},
else => {
// NOTE: should this only send the event to the 'active_tab'
var tab = this.tabs.items[this.active_tab];
switch ((&tab.element).*) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
},
}
return &this.events;
}
const round_frame = .{ "", "", "", "", "", "" };
const square_frame = .{ "", "", "", "", "", "" };
fn renderFrame(this: *@This(), renderer: *Renderer) !void {
// FIXME: use renderer instead!
_ = renderer;
const frame = switch (this.config.frame) {
.round => round_frame,
.square => square_frame,
};
std.debug.assert(frame.len == 6);
// render top: +---+
try terminal.setCursorPosition(this.size.anchor);
const writer = terminal.writer();
var style: Style = .{ .fg = this.tabs.items[this.active_tab].color };
try style.value(writer, frame[0]);
var tab_title_len: usize = 0;
for (this.tabs.items, 0..) |tab, idx| {
var tab_style: Cell.Style = .{
.fg = tab.color,
.bg = .default,
};
if (idx == this.active_tab) {
tab_style.fg = .default;
tab_style.bg = tab.color;
}
const cell: Cell = .{
.content = tab.title,
.style = tab_style,
};
try cell.value(writer, 0, tab.title.len);
tab_title_len += tab.title.len;
}
for (0..this.size.cols -| 2 -| tab_title_len) |_| {
try style.value(writer, frame[1]);
}
try style.value(writer, frame[2]);
// render left: |
for (1..this.size.rows -| 1) |r| {
const row: u16 = @truncate(r);
try terminal.setCursorPosition(.{
.col = this.size.anchor.col,
.row = this.size.anchor.row + row,
});
try style.value(writer, frame[3]);
}
// render right: |
for (1..this.size.rows -| 1) |r| {
const row: u16 = @truncate(r);
try terminal.setCursorPosition(.{
.col = this.size.anchor.col + this.size.cols -| 1,
.row = this.size.anchor.row + row,
});
try style.value(writer, frame[3]);
}
// render bottom: +---+
try terminal.setCursorPosition(.{
.col = this.size.anchor.col,
.row = this.size.anchor.row + this.size.rows - 1,
});
try style.value(writer, frame[4]);
for (0..this.size.cols -| 2) |_| {
try style.value(writer, frame[1]);
}
try style.value(writer, frame[5]);
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (this.require_render) {
try renderer.clear(this.size);
try this.renderFrame(renderer);
this.require_render = false;
}
var tab = this.tabs.items[this.active_tab];
switch ((&tab.element).*) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
};
}

View File

@@ -1,187 +0,0 @@
//! Vertical Container layout for nested `Layout`s and/or `Widget`s.
//! The contained elements are sized according to the provided configuration.
//! For an evenly spaced vertical stacking see the `VStack` layout.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const log = std.log.scoped(.layout_vcontainer);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Container = struct {
element: Element,
container_size: u8, // 0 - 100 %
};
const Containers = std.ArrayList(Container);
const LayoutType = @typeInfo(Element).@"union".fields[0].type;
const WidgetType = @typeInfo(Element).@"union".fields[1].type;
const Events = std.ArrayList(Event);
return struct {
// TODO: current focused `Element`?
allocator: std.mem.Allocator,
size: terminal.Size,
containers: Containers,
events: Events,
pub fn init(allocator: std.mem.Allocator, 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));
}
comptime var total_size = 0;
const fields_info = args_type_info.@"struct".fields;
var containers = Containers.initCapacity(allocator, fields_info.len) catch @panic("VContainer.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 != .@"struct") {
@compileError("expected tuple or struct as child type, found " ++ @typeName(ChildType));
}
const child_fields = child_type_info.@"struct".fields;
if (child_fields.len != 2) {
@compileError("expected nested tuple or struct to have exactly 2 fields, but found " ++ child_fields.len);
}
const element = @field(child, child_fields[0].name);
const ElementType = @TypeOf(element);
const element_size = @field(child, child_fields[1].name);
const ElementSizeType = @TypeOf(element_size);
if (ElementSizeType != u8 and ElementSizeType != comptime_int) {
@compileError("expected an u8 or comptime_int as second argument of nested tuple or struct child, but found " ++ @typeName(ElementSizeType));
}
total_size += element_size;
if (total_size > 100) {
@compileError("cannot place element: " ++ child_fields[0].name ++ " as total size of used container elements would overflow");
}
if (ElementType == WidgetType) {
containers.append(.{
.element = .{ .widget = element },
.container_size = element_size,
}) catch {};
continue;
}
if (ElementType == LayoutType) {
containers.append(.{
.element = .{ .layout = element },
.container_size = element_size,
}) catch {};
continue;
}
@compileError("nested child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
}
var this = allocator.create(@This()) catch @panic("VContainer.zig: Failed to create.");
this.allocator = allocator;
this.containers = containers;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
for (this.containers.items) |*container| {
switch (container.element) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
}
this.containers.deinit();
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
// order is important
switch (event) {
.resize => |size| {
this.size = size;
// adjust size according to the containing elements
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
// adjust size according to the container size
var offset: u16 = 0;
for (this.containers.items) |*container| {
const rows = @divTrunc(size.rows * container.container_size, 100);
const sub_event: Event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col,
.row = size.anchor.row + offset,
},
.cols = size.cols,
.rows = rows,
},
};
offset += rows;
switch (container.element) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
}
},
else => {
for (this.containers.items) |*container| {
switch (container.element) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
}
},
}
return &this.events;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
for (this.containers.items) |*container| {
switch (container.element) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
}
};
}

View File

@@ -1,161 +0,0 @@
//! Vertical Stacking layout for nested `Layout`s and/or `Widget`s.
//!
//! # Example
//! ...
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const log = std.log.scoped(.layout_vstack);
pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[0].name, "layout")) {
@compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[0].name);
}
if (!std.mem.eql(u8, @typeInfo(Element).@"union".fields[1].name, "widget")) {
@compileError("Expected `widget: Widget` to be the first union element, but has name: " ++ @typeInfo(Element).@"union".fields[1].name);
}
const Elements = std.ArrayList(Element);
const LayoutType = @typeInfo(Element).@"union".fields[0].type;
const WidgetType = @typeInfo(Element).@"union".fields[1].type;
const Events = std.ArrayList(Event);
return struct {
// TODO: current focused `Element`?
allocator: std.mem.Allocator,
size: terminal.Size,
elements: Elements,
events: Events,
pub fn init(allocator: std.mem.Allocator, 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 elements = Elements.initCapacity(allocator, fields_info.len) catch @panic("VStack.zig out of memory");
inline for (comptime fields_info) |field| {
const child = @field(children, field.name);
const ChildType = @TypeOf(child);
if (ChildType == WidgetType) {
elements.append(.{ .widget = child }) catch {};
continue;
}
if (ChildType == LayoutType) {
elements.append(.{ .layout = child }) catch {};
continue;
}
@compileError("child: " ++ field.name ++ " is not of type " ++ @typeName(WidgetType) ++ " or " ++ @typeName(LayoutType) ++ " but " ++ @typeName(ChildType));
}
var this = allocator.create(@This()) catch @panic("VStack.zig: Failed to create.");
this.allocator = allocator;
this.elements = elements;
this.events = Events.init(allocator);
return this;
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
for (this.elements.items) |*element| {
switch (element.*) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
}
this.elements.deinit();
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) !*Events {
this.events.clearRetainingCapacity();
// order is important
switch (event) {
.resize => |size| {
this.size = size;
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
const len: u16 = @truncate(this.elements.items.len);
const element_rows = @divTrunc(size.rows, len);
var overflow = size.rows % len;
var offset: u16 = 0;
// adjust size according to the containing elements
for (this.elements.items) |*element| {
var rows = element_rows;
if (overflow > 0) {
overflow -|= 1;
rows += 1;
}
const sub_event: Event = .{
.resize = .{
.anchor = .{
.col = size.anchor.col,
.row = size.anchor.row + offset,
},
.cols = size.cols,
.rows = rows,
},
};
offset += rows;
switch (element.*) {
.layout => |*layout| {
const events = try layout.handle(sub_event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(sub_event)) |e| {
try this.events.append(e);
}
},
}
}
},
else => {
for (this.elements.items) |*element| {
switch (element.*) {
.layout => |*layout| {
const events = try layout.handle(event);
try this.events.appendSlice(events.items);
},
.widget => |*widget| {
if (widget.handle(event)) |e| {
try this.events.append(e);
}
},
}
}
},
}
return &this.events;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
for (this.elements.items) |*element| {
switch (element.*) {
.layout => |*layout| {
try layout.render(renderer);
},
.widget => |*widget| {
try widget.render(renderer);
},
}
}
}
};
}

0
src/properties.zig Normal file
View File

View File

@@ -106,7 +106,7 @@ pub fn Buffered(comptime fullscreen: bool) type {
if (cs.eql(cvs))
continue;
// render differences found in virtual screen
// TODO: improve the writing speed (many unecessary writes (i.e. the style for every character..))
// TODO: improve the writing speed (many unnecessary writes (i.e. the style for every character..))
try terminal.setCursorPosition(.{ .row = @truncate(row), .col = @truncate(col) });
try cvs.value(writer);
// update screen to be the virtual screen for the next frame

10
src/size.zig Normal file
View File

@@ -0,0 +1,10 @@
pub const Size = @This();
pub const Position = struct {
col: u16,
row: u16,
};
anchor: Position = .{},
cols: u16,
rows: u16,

View File

@@ -10,6 +10,10 @@
const std = @import("std");
const ctlseqs = @import("ctlseqs.zig");
const Color = @import("color.zig").Color;
pub const Style = @This();
pub const Underline = enum {
off,
single,
@@ -19,87 +23,6 @@ pub const Underline = enum {
dashed,
};
pub const Color = union(enum) {
default,
index: u8,
rgb: [3]u8,
pub fn eql(a: @This(), b: @This()) bool {
switch (a) {
.default => return b == .default,
.index => |a_idx| {
switch (b) {
.index => |b_idx| return a_idx == b_idx,
else => return false,
}
},
.rgb => |a_rgb| {
switch (b) {
.rgb => |b_rgb| return a_rgb[0] == b_rgb[0] and
a_rgb[1] == b_rgb[1] and
a_rgb[2] == b_rgb[2],
else => return false,
}
},
}
}
pub fn rgbFromUint(val: u24) Color {
const r_bits = val & 0b11111111_00000000_00000000;
const g_bits = val & 0b00000000_11111111_00000000;
const b_bits = val & 0b00000000_00000000_11111111;
const rgb = [_]u8{
@truncate(r_bits >> 16),
@truncate(g_bits >> 8),
@truncate(b_bits),
};
return .{ .rgb = rgb };
}
/// parse an XParseColor-style rgb specification into an rgb Color. The spec
/// is of the form: rgb:rrrr/gggg/bbbb. Generally, the high two bits will always
/// be the same as the low two bits.
pub fn rgbFromSpec(spec: []const u8) !Color {
var iter = std.mem.splitScalar(u8, spec, ':');
const prefix = iter.next() orelse return error.InvalidColorSpec;
if (!std.mem.eql(u8, "rgb", prefix)) return error.InvalidColorSpec;
const spec_str = iter.next() orelse return error.InvalidColorSpec;
var spec_iter = std.mem.splitScalar(u8, spec_str, '/');
const r_raw = spec_iter.next() orelse return error.InvalidColorSpec;
if (r_raw.len != 4) return error.InvalidColorSpec;
const g_raw = spec_iter.next() orelse return error.InvalidColorSpec;
if (g_raw.len != 4) return error.InvalidColorSpec;
const b_raw = spec_iter.next() orelse return error.InvalidColorSpec;
if (b_raw.len != 4) return error.InvalidColorSpec;
const r = try std.fmt.parseUnsigned(u8, r_raw[2..], 16);
const g = try std.fmt.parseUnsigned(u8, g_raw[2..], 16);
const b = try std.fmt.parseUnsigned(u8, b_raw[2..], 16);
return .{
.rgb = [_]u8{ r, g, b },
};
}
test "rgbFromSpec" {
const spec = "rgb:aaaa/bbbb/cccc";
const actual = try rgbFromSpec(spec);
switch (actual) {
.rgb => |rgb| {
try std.testing.expectEqual(0xAA, rgb[0]);
try std.testing.expectEqual(0xBB, rgb[1]);
try std.testing.expectEqual(0xCC, rgb[2]);
},
else => try std.testing.expect(false),
}
}
};
fg: Color = .default,
bg: Color = .default,
ul: Color = .default,
@@ -113,7 +36,7 @@ reverse: bool = false,
invisible: bool = false,
strikethrough: bool = false,
pub fn eql(this: @This(), other: @This()) bool {
pub fn eql(this: Style, other: Style) bool {
return this.fg.eql(other.fg) and
this.bg.eql(other.bg) and
this.ul.eql(other.ul) and
@@ -129,7 +52,7 @@ pub fn eql(this: @This(), other: @This()) bool {
/// Merge _other_ `Style` to _this_ style and overwrite _this_ `Style`'s value
/// if the _other_ value differs from the default value.
pub fn merge(this: *@This(), other: @This()) void {
pub fn merge(this: *Style, other: Style) void {
if (other.fg != .default) this.fg = other.fg;
if (other.bg != .default) this.bg = other.bg;
if (other.ul != .default) this.ul = other.ul;
@@ -143,7 +66,7 @@ pub fn merge(this: *@This(), other: @This()) void {
if (other.strikethrough != false) this.strikethrough = other.strikethrough;
}
fn start(this: @This(), writer: anytype) !void {
fn start(this: Style, writer: anytype) !void {
// foreground
switch (this.fg) {
.default => try std.fmt.format(writer, ctlseqs.fg_reset, .{}),
@@ -240,7 +163,7 @@ fn start(this: @This(), writer: anytype) !void {
}
}
fn end(this: @This(), writer: anytype) !void {
fn end(this: Style, writer: anytype) !void {
// foreground
switch (this.fg) {
.default => {},
@@ -298,7 +221,7 @@ fn end(this: @This(), writer: anytype) !void {
}
}
pub fn value(this: @This(), writer: anytype, content: u8) !void {
pub fn value(this: Style, writer: anytype, content: u8) !void {
try this.start(writer);
_ = try writer.write(&[_]u8{content});
try this.end(writer);
@@ -307,7 +230,3 @@ pub fn value(this: @This(), writer: anytype, content: u8) !void {
// TODO: implement helper functions for terminal capabilities:
// - links / url display (osc 8)
// - show / hide cursor?
test {
_ = Color;
}

View File

@@ -1,9 +1,9 @@
const std = @import("std");
pub const Key = @import("terminal/Key.zig");
pub const Size = @import("terminal/Size.zig");
pub const Position = @import("terminal/Position.zig");
pub const Cell = @import("terminal/Cell.zig");
pub const code_point = @import("code_point");
const code_point = @import("code_point");
const Key = @import("key.zig");
const Size = @import("size.zig");
const Cell = @import("cell.zig");
const log = std.log.scoped(.terminal);
@@ -78,13 +78,13 @@ pub fn writer() Writer {
return .{ .context = .{} };
}
pub fn setCursorPosition(pos: Position) !void {
pub fn setCursorPosition(pos: Size.Position) !void {
var buf: [64]u8 = undefined;
const value = try std.fmt.bufPrint(&buf, "\x1b[{d};{d}H", .{ pos.row, pos.col });
_ = try std.posix.write(std.posix.STDIN_FILENO, value);
}
pub fn getCursorPosition() !Position {
pub fn getCursorPosition() !Size.Position {
// Needs Raw mode (no wait for \n) to work properly cause
// control sequence will not be written without it.
_ = try std.posix.write(std.posix.STDIN_FILENO, "\x1b[6n");
@@ -227,7 +227,3 @@ fn getReportMode(ps: u8) ReportMode {
else => ReportMode.not_recognized,
};
}
test {
_ = Cell;
}

View File

@@ -1,2 +0,0 @@
col: u16,
row: u16,

View File

@@ -1,5 +0,0 @@
const Position = @import("Position.zig");
anchor: Position = .{ .col = 0, .row = 0 }, // top left corner by default
cols: u16,
rows: u16,

View File

@@ -1,116 +0,0 @@
//! Dynamic dispatch for view implementations. Each `View` has to implement the `View.Interface`
//!
//! Create a `View` using `createFrom(object: anytype)` and use them through
//! the defined `View.Interface`. The view will take care of calling the
//! correct implementation of the corresponding underlying type.
//!
//! A `View` holds the necessary `Layout`'s for different screen sizes as well
//! as the corresponding used `Widget`'s alongside holding the corresponding memory
//! for the data shown through the `View`.
const std = @import("std");
const isTaggedUnion = @import("event.zig").isTaggedUnion;
const log = std.log.scoped(.view);
pub fn View(comptime Event: type, comptime Renderer: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `View(comptime Event: type)` is not of type `union(enum)`.");
}
const Events = std.ArrayList(Event);
return struct {
const ViewType = @This();
pub const Interface = @import("interface").Interface(.{
.handle = fn (anytype, Event) anyerror!*Events,
.render = fn (anytype, *Renderer) anyerror!void,
.enable = fn (anytype) void,
.disable = fn (anytype) void,
.deinit = fn (anytype) void,
}, .{});
// TODO: this VTable creation and abstraction could maybe even be done through a comptime implementation -> another library?
const VTable = struct {
handle: *const fn (this: *ViewType, event: Event) anyerror!*Events,
render: *const fn (this: *ViewType, renderer: *Renderer) anyerror!void,
enable: *const fn (this: *ViewType) void,
disable: *const fn (this: *ViewType) void,
deinit: *const fn (this: *ViewType) void,
};
object: *anyopaque = undefined,
vtable: *const VTable = undefined,
/// Handle the provided `Event` for this `View`.
pub fn handle(this: *ViewType, event: Event) !*Events {
switch (event) {
.resize => |size| {
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
},
else => {},
}
return this.vtable.handle(this, event);
}
/// Render the content of this `View` given the `Size` of the available terminal (.resize System`Event`).
pub fn render(this: *ViewType, renderer: *Renderer) !void {
try this.vtable.render(this, renderer);
}
/// Function to call when this `View` will be handled and rendered as the 'active' `View`.
pub fn enable(this: *ViewType) void {
this.vtable.enable(this);
}
/// Function to call when this `View` will no longer be 'active'.
pub fn disable(this: *ViewType) void {
this.vtable.disable(this);
}
pub fn deinit(this: *ViewType) void {
this.vtable.deinit(this);
}
pub fn createFrom(object: anytype) ViewType {
return ViewType{
.object = @ptrCast(@alignCast(object)),
.vtable = &.{
.handle = struct {
fn handle(this: *ViewType, event: Event) !*Events {
const view: @TypeOf(object) = @ptrCast(@alignCast(this.object));
return view.handle(event);
}
}.handle,
.render = struct {
fn render(this: *ViewType, renderer: *Renderer) !void {
const view: @TypeOf(object) = @ptrCast(@alignCast(this.object));
try view.render(renderer);
}
}.render,
.enable = struct {
fn enable(this: *ViewType) void {
const view: @TypeOf(object) = @ptrCast(@alignCast(this.object));
view.enable();
}
}.enable,
.disable = struct {
fn disable(this: *ViewType) void {
const view: @TypeOf(object) = @ptrCast(@alignCast(this.object));
view.disable();
}
}.disable,
.deinit = struct {
fn deinit(this: *ViewType) void {
const view: @TypeOf(object) = @ptrCast(@alignCast(this.object));
view.deinit();
}
}.deinit,
},
};
}
};
}

View File

@@ -1,107 +0,0 @@
//! Dynamic dispatch for widget implementations. Each `Widget` has to implement
//! the `Widget.Interface`.
//!
//! Create a `Widget` using `createFrom(object: anytype)` and use them through
//! the defined `Widget.Interface`. The widget will take care of calling the
//! correct implementation of the corresponding underlying type.
//!
//! Each `Widget` may cache its content and should if the contents will not
//! change for a long time.
//!
//! When `Widget.render` is called the provided `Renderer` type is expected
//! which handles how contents are rendered for a given widget.
const isTaggedUnion = @import("event.zig").isTaggedUnion;
const log = @import("std").log.scoped(.widget);
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 Type = struct {
const WidgetType = @This();
pub const Interface = @import("interface").Interface(.{
.handle = fn (anytype, Event) ?Event,
.render = fn (anytype, *Renderer) anyerror!void,
.deinit = fn (anytype) void,
}, .{});
const VTable = struct {
handle: *const fn (this: *WidgetType, event: Event) ?Event,
render: *const fn (this: *WidgetType, renderer: *Renderer) anyerror!void,
deinit: *const fn (this: *WidgetType) void,
};
object: *anyopaque = undefined,
vtable: *const VTable = undefined,
// Handle the provided `Event` for this `Widget`.
pub fn handle(this: *WidgetType, event: Event) ?Event {
switch (event) {
.resize => |size| {
log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{
size.anchor.col,
size.anchor.row,
size.cols,
size.rows,
});
},
else => {},
}
return this.vtable.handle(this, event);
}
// Render the content of this `Widget` given the `Size` of the widget (.resize System`Event`).
pub fn render(this: *WidgetType, renderer: *Renderer) !void {
try this.vtable.render(this, renderer);
}
pub fn deinit(this: *WidgetType) void {
this.vtable.deinit(this);
}
pub fn createFrom(object: anytype) WidgetType {
return WidgetType{
.object = @ptrCast(@alignCast(object)),
.vtable = &.{
.handle = struct {
// Handle the provided `Event` for this `Widget`.
fn handle(this: *WidgetType, event: Event) ?Event {
const widget: @TypeOf(object) = @ptrCast(@alignCast(this.object));
return widget.handle(event);
}
}.handle,
.render = struct {
// Return the entire content of this `Widget`.
fn render(this: *WidgetType, renderer: *Renderer) !void {
const widget: @TypeOf(object) = @ptrCast(@alignCast(this.object));
try widget.render(renderer);
}
}.render,
.deinit = struct {
fn deinit(this: *WidgetType) void {
const widget: @TypeOf(object) = @ptrCast(@alignCast(this.object));
widget.deinit();
}
}.deinit,
},
};
}
// TODO: implement a minimal size requirement for Widgets to render correctly?
// import and export of `Widget` implementations
pub const Input = @import("widget/Input.zig").Widget(Event, Renderer);
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.Input);
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;
}

View File

@@ -1,250 +0,0 @@
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Cell = terminal.Cell;
const Key = terminal.Key;
const Size = terminal.Size;
const log = std.log.scoped(.widget_input);
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)`.");
}
return struct {
active: bool,
allocator: std.mem.Allocator,
label: ?[]const u8,
placeholder: ?[]const u8,
size: Size,
require_render: bool,
value: std.ArrayList(u8),
/// value content length
value_len: usize,
/// current cursor position
cursor_idx: usize,
pub fn init(allocator: std.mem.Allocator, label: ?[]const u8, placeholder: ?[]const u8) *@This() {
var value = std.ArrayList(u8).init(allocator);
value.resize(32) catch @panic("Input.zig: out of memory");
var this = allocator.create(@This()) catch @panic("Input.zig: Failed to create.");
this.allocator = allocator;
this.active = false;
this.require_render = true;
this.label = null;
this.placeholder = null;
this.value_len = 0;
this.cursor_idx = 0;
this.value = value;
this.label = label;
this.placeholder = placeholder;
return this;
}
pub fn deinit(this: *@This()) void {
this.value.deinit();
this.allocator.destroy(this);
}
pub fn getValue(this: *const @This()) []const u8 {
return this.value.items[0..this.value_len];
}
pub fn handle(this: *@This(), event: Event) ?Event {
switch (event) {
.resize => |size| {
this.size = size;
this.require_render = true;
var required_cols: u16 = 4; // '...c'
if (this.label) |label| {
required_cols += @as(u16, @truncate(label.len)); // <label>
required_cols += 2; // ': '
}
if (this.size.cols < required_cols) {
return .{ .err = .{
.err = error.InsufficientSize,
.msg = "Received Size is too small to render App.Widget.Input correctly",
} };
}
},
.key => |key| {
if (!this.active) {
return null;
}
if (key.matches(.{ .cp = Key.tab }) or key.matches(.{ .cp = Key.enter })) {
// ignored keys
} else if (key.mod.alt or key.mod.ctrl or key.matches(.{ .cp = Key.escape })) {
// TODO: what about ctrl-v, ctrl-w, alt-b, alt-f?
// ignored keys
} else if (key.matches(.{ .cp = Key.backspace })) {
// remove one character
_ = this.value.orderedRemove(this.cursor_idx);
this.cursor_idx -|= 1;
this.value_len -|= 1;
this.require_render = true;
} else if (key.matches(.{ .cp = Key.left }) or key.matches(.{ .cp = 'b', .mod = .{ .ctrl = true } })) {
// left
this.cursor_idx -|= 1;
this.require_render = true;
} else if (key.matches(.{ .cp = Key.right }) or key.matches(.{ .cp = 'f', .mod = .{ .ctrl = true } })) {
// right
if (this.cursor_idx < this.value_len) {
this.cursor_idx += 1;
this.require_render = true;
}
} else {
if (this.value.items.len <= this.value_len) {
// double capacity in case we need more space
this.value.resize(this.value.capacity * 2) catch |err| {
return .{
.err = .{
.err = err,
.msg = "Could not resize input value buffer",
},
};
};
}
this.value.insert(this.cursor_idx, @as(u8, @truncate(key.cp))) catch @panic("Input.zig: out of memory");
this.cursor_idx += 1;
this.value_len += 1;
this.require_render = true;
}
// TODO: handle key input that should be used for the input field value
// - move cursor using arrow keys
// - allow word-wise navigation?
// - add / remove characters
// - allow removal of words?
// - do not support pasting, as that can be done by the terminal emulator (not sure if this would even work correctly over ssh)
},
else => {},
}
return null;
}
// Overview of the rendered contents:
//
// With both label and placeholder:
// <label>: <placeholder>
// Without any label:
// <placeholder>
// Without any placeholder, but a label:
// <label>: ____________
// With neither label nor placeholder:
// ____________
// When value is not an empty string, the corresponding placeholder
// (if any) will be replaced with the current value. The current
// cursor position is show when this input field is `active`.
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (!this.require_render) {
return;
}
try renderer.clear(this.size);
var size = this.size;
this.require_render = false;
if (this.label) |label| {
const label_style: Cell.Style = .{
.fg = .default,
.italic = true,
};
try renderer.render(size, &[_]Cell{
.{
.content = label,
.style = label_style,
},
});
size.anchor.col += @as(u16, @truncate(label.len));
size.cols -= @as(u16, @truncate(label.len));
try renderer.render(size, &[_]Cell{
.{
.content = ":",
},
});
size.anchor.col += 2;
size.cols -= 2;
}
if (this.value_len > 0) {
var start: usize = 0;
// TODO: moving the cursor position will change position of the '..' placement (i.e. at the beginning, at the end or both)
// truncate representation according to the available space
if (this.value_len >= size.cols - 1) {
start = this.value_len -| (size.cols - 3);
try renderer.render(size, &[_]Cell{
.{
.content = "..",
.style = .{ .dim = true },
},
});
size.anchor.col += 2;
size.cols -|= 2;
}
// print current value representation (and cursor position if active)
if (this.cursor_idx == 0 and this.value_len > 1) {
try renderer.render(size, &[_]Cell{
.{
.content = this.value.items[0..1],
.style = .{ .reverse = true },
},
.{
.content = this.value.items[1..this.value_len],
},
});
} else if (this.cursor_idx == 0 and this.value_len == 1) {
try renderer.render(size, &[_]Cell{
.{
.content = this.value.items[0..1],
.style = .{ .reverse = true },
},
});
} else if (this.cursor_idx == this.value_len) {
try renderer.render(size, &[_]Cell{
.{
.content = this.value.items[start..this.cursor_idx],
},
.{
.content = " ",
.style = .{ .reverse = true },
},
});
} else {
try renderer.render(size, &[_]Cell{
.{
.content = this.value.items[start..this.cursor_idx],
},
.{
.content = this.value.items[this.cursor_idx .. this.cursor_idx + 1],
.style = .{ .reverse = true, .blink = true },
},
});
size.anchor.col += @as(u16, @truncate(this.cursor_idx)) + 1;
size.cols -= @as(u16, @truncate(this.cursor_idx)) + 1;
if (this.value_len > this.cursor_idx + 1) {
try renderer.render(size, &[_]Cell{
.{
.content = this.value.items[this.cursor_idx + 1 .. this.value_len],
},
});
}
}
} else {
if (this.placeholder) |placeholder| {
var placeholder_style: Cell.Style = .{
.fg = .default,
.dim = true,
};
if (this.active) {
placeholder_style.blink = true;
}
try renderer.render(size, &[_]Cell{
.{
.content = placeholder,
.style = placeholder_style,
},
});
}
}
}
};
}

View File

@@ -1,147 +0,0 @@
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 {
allocator: std.mem.Allocator,
idx: usize,
config: ListType,
contents: ListItems,
size: terminal.Size,
require_render: bool,
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 {};
}
var this = allocator.create(@This()) catch @panic("List.zig: Failed to create.");
this.allocator = allocator;
this.require_render = true;
this.idx = 0;
this.config = config;
this.contents = contents;
return this;
}
pub fn deinit(this: *@This()) void {
this.contents.deinit();
this.allocator.destroy(this);
}
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' }) or key.matches(.{ .cp = terminal.Key.home })) {
// top
if (this.idx != 0) {
this.idx = 0;
} else {
require_render = false;
}
} else if (key.matches(.{ .cp = 'G' }) or key.matches(.{ .cp = terminal.Key.end })) {
// bottom
if (this.idx < this.contents.items.len -| 1) {
this.idx = this.contents.items.len -| 1;
} else {
require_render = false;
}
} else if (key.matches(.{ .cp = 'j' }) or key.matches(.{ .cp = terminal.Key.down })) {
// down
if (this.idx < this.contents.items.len -| 1) {
this.idx += 1;
} else {
require_render = false;
}
} else if (key.matches(.{ .cp = 'k' }) or key.matches(.{ .cp = terminal.Key.up })) {
// up
if (this.idx > 0) {
this.idx -= 1;
} else {
require_render = false;
}
} 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;
}
};
}

View File

@@ -1,127 +0,0 @@
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const Key = terminal.Key;
const Style = terminal.Style;
const log = std.log.scoped(.widget_rawtext);
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 Contents = std.ArrayList(u8);
return struct {
allocator: std.mem.Allocator,
contents: Contents,
line_index: std.ArrayList(usize),
line: usize,
size: terminal.Size,
require_render: bool,
pub fn init(allocator: std.mem.Allocator, file: std.fs.File) *@This() {
var contents = Contents.init(allocator);
var line_index = std.ArrayList(usize).init(allocator);
file.reader().readAllArrayList(&contents, std.math.maxInt(usize)) catch {};
line_index.append(0) catch {};
for (contents.items, 0..) |item, i| {
if (item == '\n') {
line_index.append(i + 1) catch {};
}
}
var this = allocator.create(@This()) catch @panic("RawText.zig: Failed to create.");
this.allocator = allocator;
this.line = 0;
this.require_render = true;
this.contents = contents;
this.line_index = line_index;
return this;
}
pub fn deinit(this: *@This()) void {
this.contents.deinit();
this.line_index.deinit();
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) ?Event {
var require_render = true;
switch (event) {
// store the received size
.resize => |size| {
this.size = size;
if (this.line > this.line_index.items.len -| 1 -| size.rows) {
this.line = this.line_index.items.len -| 1 -| size.rows;
}
},
.key => |key| {
if (key.matches(.{ .cp = 'g' }) or key.matches(.{ .cp = Key.home })) {
// top
if (this.line != 0) {
this.line = 0;
} else {
require_render = false;
}
} else if (key.matches(.{ .cp = 'G' }) or key.matches(.{ .cp = Key.end })) {
// bottom
if (this.line < this.line_index.items.len -| 1 -| this.size.rows) {
this.line = this.line_index.items.len -| 1 -| this.size.rows;
} else {
require_render = false;
}
} else if (key.matches(.{ .cp = 'j' }) or key.matches(.{ .cp = Key.down })) {
// down
if (this.line < this.line_index.items.len -| 1 -| this.size.rows) {
this.line += 1;
} else {
require_render = false;
}
} else if (key.matches(.{ .cp = 'k' }) or key.matches(.{ .cp = Key.up })) {
// up
if (this.line > 0) {
this.line -= 1;
} else {
require_render = false;
}
} else {
require_render = false;
}
},
else => {
require_render = false;
},
}
this.require_render = require_render;
return null;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (!this.require_render) {
return;
}
try renderer.clear(this.size);
if (this.size.rows >= this.line_index.items.len) {
try renderer.render(this.size, &[_]terminal.Cell{
.{ .content = this.contents.items, .style = .{ .dim = true, .fg = .{ .index = 8 } } },
});
} else {
// more rows than we can display
const i = this.line_index.items[this.line];
const e = this.size.rows + this.line;
if (e > this.line_index.items.len) {
try renderer.render(this.size, &[_]terminal.Cell{
.{ .content = this.contents.items[i..], .style = .{ .dim = true, .fg = .{ .index = 7 } } },
});
return;
}
const x = this.line_index.items[e];
try renderer.render(this.size, &[_]terminal.Cell{
.{ .content = this.contents.items[i..x], .style = .{ .dim = true, .fg = .{ .index = 9 } } },
});
}
this.require_render = false;
}
};
}

View File

@@ -1,47 +0,0 @@
const std = @import("std");
const terminal = @import("../terminal.zig");
const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error;
const log = std.log.scoped(.widget_spacer);
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)`.");
}
return struct {
allocator: std.mem.Allocator,
size: terminal.Size,
size_changed: bool,
pub fn init(allocator: std.mem.Allocator) *@This() {
var this = allocator.create(@This()) catch @panic("Space.zig: Failed to create.");
this.allocator = allocator;
this.size_changed = true;
return this;
}
pub fn deinit(this: *@This()) void {
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) ?Event {
switch (event) {
.resize => |size| {
this.size = size;
this.size_changed = true;
},
else => {},
}
return null;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (this.size_changed) {
try renderer.clear(this.size);
this.size_changed = false;
}
}
};
}

View File

@@ -1,141 +0,0 @@
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 log = std.log.scoped(.widget_text);
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)`.");
}
return struct {
allocator: std.mem.Allocator,
alignment: Alignment,
contents: []const Cell,
size: terminal.Size,
require_render: bool,
const Alignment = enum {
default,
center,
top,
bottom,
left,
right,
};
pub fn init(allocator: std.mem.Allocator, alignment: Alignment, contents: []const Cell) *@This() {
var this = allocator.create(@This()) catch @panic("Text.zig: Failed to create");
this.allocator = allocator;
this.require_render = true;
this.alignment = alignment;
this.contents = contents;
return this;
}
pub fn deinit(this: *@This()) void {
this.allocator.destroy(this);
}
pub fn handle(this: *@This(), event: Event) ?Event {
switch (event) {
// store the received size
.resize => |size| {
this.size = size;
this.require_render = true;
},
else => {},
}
return null;
}
pub fn render(this: *@This(), renderer: *Renderer) !void {
if (!this.require_render) {
return;
}
try renderer.clear(this.size);
// update size for aligned contents, default will not change size
const size: terminal.Size = blk: {
switch (this.alignment) {
.default => break :blk this.size,
.center => {
const length_usize = this.contents.len;
const length: u16 = @truncate(length_usize);
const cols = @min(length, this.size.cols);
const rows = cols / length;
break :blk .{
.anchor = .{
.col = this.size.anchor.col + @divTrunc(this.size.cols, 2) - @divTrunc(cols, 2),
.row = this.size.anchor.row + @divTrunc(this.size.rows, 2),
},
.rows = rows,
.cols = cols,
};
},
.top => {
const length_usize = this.contents.len;
const length: u16 = @truncate(length_usize);
const cols = @min(length, this.size.cols);
const rows = cols / length;
break :blk .{
.anchor = .{
.col = this.size.anchor.col + @divTrunc(this.size.cols, 2) - @divTrunc(cols, 2),
.row = this.size.anchor.row,
},
.rows = rows,
.cols = cols,
};
},
.bottom => {
const length_usize = this.contents.len;
const length: u16 = @truncate(length_usize);
const cols = @min(length, this.size.cols);
const rows = cols / length;
break :blk .{
.anchor = .{
.col = this.size.anchor.col + @divTrunc(this.size.cols, 2) - @divTrunc(cols, 2),
.row = this.size.anchor.row + this.size.rows - rows,
},
.rows = rows,
.cols = cols,
};
},
.left => {
const length_usize = this.contents.len;
const length: u16 = @truncate(length_usize);
const cols = @min(length, this.size.cols);
const rows = cols / length;
break :blk .{
.anchor = .{
.col = this.size.anchor.col,
.row = this.size.anchor.row + @divTrunc(this.size.rows, 2) - @divTrunc(rows, 2),
},
.rows = rows,
.cols = cols,
};
},
.right => {
const length_usize = this.contents.len;
const length: u16 = @truncate(length_usize);
const cols = @min(length, this.size.cols);
const rows = cols / length;
break :blk .{
.anchor = .{
.col = this.size.anchor.col + this.size.cols - cols,
.row = this.size.anchor.row + @divTrunc(this.size.rows, 2) - @divTrunc(rows, 2),
},
.rows = rows,
.cols = cols,
};
},
}
};
log.debug("Text.contents: {any}", .{this.contents});
try renderer.render(size, this.contents);
this.require_render = false;
}
};
}

View File

@@ -1,16 +1,19 @@
// private imports
// public import / exports
pub const terminal = @import("terminal.zig");
pub const App = @import("app.zig").App;
pub const Renderer = @import("render.zig");
pub const Key = terminal.Key;
pub const Position = terminal.Position;
pub const Size = terminal.Size;
pub const Cell = terminal.Cell;
pub const Cell = @import("cell.zig");
pub const Color = @import("color.zig").Color;
pub const Key = @import("key.zig");
pub const Size = @import("size.zig");
pub const Style = @import("style.zig");
test {
_ = @import("terminal.zig");
_ = @import("queue.zig");
_ = Cell;
_ = Color;
_ = Key;
_ = Size;
_ = Style;
}