add(layout/tab): Tab layout implementation
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 9m46s
All checks were successful
Zig Project Action / Lint, Spell-check and test zig project (push) Successful in 9m46s
This commit is contained in:
@@ -171,6 +171,7 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
|
||||
if (buf[0] == 0x1b and read_bytes > 1) {
|
||||
switch (buf[1]) {
|
||||
// TODO: parse corresponding codes
|
||||
// 0x5B => parseCsi(input, &self.buf), // CSI see https://github.com/rockorager/libvaxis/blob/main/src/Parser.zig
|
||||
else => {},
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -91,6 +91,7 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type {
|
||||
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);
|
||||
@@ -101,5 +102,6 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type {
|
||||
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;
|
||||
}
|
||||
|
||||
309
src/layout/Tab.zig
Normal file
309
src/layout/Tab.zig
Normal file
@@ -0,0 +1,309 @@
|
||||
//! 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 {
|
||||
size: terminal.Size = undefined,
|
||||
require_render: bool = true,
|
||||
tabs: Tabs = undefined,
|
||||
active_tab: usize = 0,
|
||||
events: Events = undefined,
|
||||
config: Config = undefined,
|
||||
|
||||
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));
|
||||
}
|
||||
return .{
|
||||
.config = config,
|
||||
.tabs = tabs,
|
||||
.events = Events.init(allocator),
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user