add(layout): HContainer and VContainer for arbritrary sized H/V Element placement
This commit is contained in:
@@ -85,7 +85,9 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// import and export of `Layout` implementations
|
// 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 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 VStack = @import("layout/VStack.zig").Layout(Event, Element, Renderer);
|
||||||
pub const Padding = @import("layout/Padding.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 Margin = @import("layout/Margin.zig").Layout(Event, Element, Renderer);
|
||||||
@@ -93,7 +95,9 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type {
|
|||||||
};
|
};
|
||||||
// test layout implementation satisfies the interface
|
// test layout implementation satisfies the interface
|
||||||
comptime Type.Interface.satisfiedBy(Type);
|
comptime Type.Interface.satisfiedBy(Type);
|
||||||
|
comptime Type.Interface.satisfiedBy(Type.HContainer);
|
||||||
comptime Type.Interface.satisfiedBy(Type.HStack);
|
comptime Type.Interface.satisfiedBy(Type.HStack);
|
||||||
|
comptime Type.Interface.satisfiedBy(Type.VContainer);
|
||||||
comptime Type.Interface.satisfiedBy(Type.VStack);
|
comptime Type.Interface.satisfiedBy(Type.VStack);
|
||||||
comptime Type.Interface.satisfiedBy(Type.Padding);
|
comptime Type.Interface.satisfiedBy(Type.Padding);
|
||||||
comptime Type.Interface.satisfiedBy(Type.Margin);
|
comptime Type.Interface.satisfiedBy(Type.Margin);
|
||||||
|
|||||||
184
src/layout/HContainer.zig
Normal file
184
src/layout/HContainer.zig
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
//! 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`?
|
||||||
|
size: terminal.Size = undefined,
|
||||||
|
containers: Containers = undefined,
|
||||||
|
events: Events = undefined,
|
||||||
|
|
||||||
|
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("OOM");
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
return .{
|
||||||
|
.containers = containers,
|
||||||
|
.events = Events.init(allocator),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
184
src/layout/VContainer.zig
Normal file
184
src/layout/VContainer.zig
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
//! 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`?
|
||||||
|
size: terminal.Size = undefined,
|
||||||
|
containers: Containers = undefined,
|
||||||
|
events: Events = undefined,
|
||||||
|
|
||||||
|
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("OOM");
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
return .{
|
||||||
|
.containers = containers,
|
||||||
|
.events = Events.init(allocator),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user