add(layout/Marging): relative margins for Elements
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 32s

This commit is contained in:
2024-11-13 18:08:58 +01:00
parent 61f6c72bf8
commit bc1bc757d4
8 changed files with 293 additions and 77 deletions

View File

@@ -88,13 +88,15 @@ pub fn Layout(comptime Event: type, comptime Renderer: type) type {
pub const HStack = @import("layout/HStack.zig").Layout(Event, Element, Renderer); pub const HStack = @import("layout/HStack.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 Marging = @import("layout/Marging.zig").Layout(Event, Element, Renderer);
pub const Framing = @import("layout/Framing.zig").Layout(Event, Element, Renderer); pub const Framing = @import("layout/Framing.zig").Layout(Event, Element, Renderer);
}; };
// test widget 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.HStack); comptime Type.Interface.satisfiedBy(Type.HStack);
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.Marging);
comptime Type.Interface.satisfiedBy(Type.Framing); comptime Type.Interface.satisfiedBy(Type.Framing);
return Type; return Type;
} }

View File

@@ -19,6 +19,12 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t
if (!isTaggedUnion(Element)) { if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); @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 Events = std.ArrayList(Event);
return struct { return struct {
size: terminal.Size = undefined, size: terminal.Size = undefined,
@@ -159,7 +165,7 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t
// render bottom: +---+ // render bottom: +---+
try terminal.setCursorPosition(.{ try terminal.setCursorPosition(.{
.col = this.size.anchor.col, .col = this.size.anchor.col,
.row = this.size.anchor.row + this.size.rows, .row = this.size.anchor.row + this.size.rows - 1,
}); });
try this.config.style.value(writer, frame[4]); try this.config.style.value(writer, frame[4]);
for (0..this.size.cols -| 2) |_| { for (0..this.size.cols -| 2) |_| {

View File

@@ -18,14 +18,14 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t
if (!isTaggedUnion(Element)) { if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); @compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
} }
const Elements = std.ArrayList(Element);
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[0].name, "layout")) { 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); @compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[0].name);
} }
const LayoutType = @typeInfo(Element).Union.fields[0].type;
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[1].name, "widget")) { 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); @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 WidgetType = @typeInfo(Element).Union.fields[1].type;
const Events = std.ArrayList(Event); const Events = std.ArrayList(Event);
return struct { return struct {

161
src/layout/Marging.zig Normal file
View File

@@ -0,0 +1,161 @@
//! Marging 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_marging);
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 {
size: terminal.Size = undefined,
require_render: bool = false,
element: Element = undefined,
events: Events = undefined,
config: Config = undefined,
// TODO: marging (for all 4 directions - relative!)
const Config = struct {
margin: ?u8 = undefined,
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);
}
return .{
.config = config,
.element = element,
.events = Events.init(allocator),
};
}
pub fn deinit(this: *@This()) void {
this.events.deinit();
switch ((&this.element).*) {
.layout => |*layout| {
layout.deinit();
},
.widget => |*widget| {
widget.deinit();
},
}
}
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

@@ -18,6 +18,12 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t
if (!isTaggedUnion(Element)) { if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); @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 Events = std.ArrayList(Event);
return struct { return struct {
size: terminal.Size = undefined, size: terminal.Size = undefined,

View File

@@ -18,14 +18,14 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t
if (!isTaggedUnion(Element)) { if (!isTaggedUnion(Element)) {
@compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`."); @compileError("Provided type `Element` for `Layout(comptime Event: type, comptime Element: type, comptime Renderer: type)` is not of type `union(enum)`.");
} }
const Elements = std.ArrayList(Element);
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[0].name, "layout")) { 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); @compileError("Expected `layout: Layout` to be the first union element, but has name: " ++ @typeInfo(Element).Union.fields[0].name);
} }
const LayoutType = @typeInfo(Element).Union.fields[0].type;
if (!std.mem.eql(u8, @typeInfo(Element).Union.fields[1].name, "widget")) { 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); @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 WidgetType = @typeInfo(Element).Union.fields[1].type;
const Events = std.ArrayList(Event); const Events = std.ArrayList(Event);
return struct { return struct {

View File

@@ -50,40 +50,17 @@ pub fn main() !void {
// }); // });
// break :layout &stack; // break :layout &stack;
// }); // });
var layout = Layout.createFrom(layout: { var layout = Layout.createFrom(
var hstack = Layout.HStack.init(allocator, .{
Widget.createFrom(blk: {
var spacer = Widget.Spacer.init();
break :blk &spacer;
}),
Layout.createFrom(framing: {
var framing = Layout.Framing.init(
allocator,
.{
.style = .{
.fg = .{
.index = 6,
},
},
.frame = .round,
.title = .{
.str = "VStack",
.style = .{
.ul_style = .single,
.ul = .{ .index = 6 },
.bold = true,
},
},
},
.{
.layout = Layout.createFrom(
padding: { padding: {
var padding = Layout.Padding.init( var padding = Layout.Marging.init(
allocator, allocator,
.{ .{
.padding = 3, .left = 15,
.right = 15,
}, },
.{ .{
.layout = Layout.createFrom(framing: {
var framing = Layout.Framing.init(allocator, .{ .style = .{ .fg = .{ .index = 6 } } }, .{
.layout = Layout.createFrom(vstack: { .layout = Layout.createFrom(vstack: {
var vstack = Layout.VStack.init(allocator, .{ var vstack = Layout.VStack.init(allocator, .{
Widget.createFrom(blk: { Widget.createFrom(blk: {
@@ -105,22 +82,85 @@ pub fn main() !void {
}); });
break :vstack &vstack; break :vstack &vstack;
}), }),
});
break :framing &framing;
}),
}, },
); );
break :padding &padding; break :padding &padding;
}, },
),
},
); );
break :framing &framing; // var layout = Layout.createFrom(layout: {
}), // var hstack = Layout.HStack.init(allocator, .{
Widget.createFrom(blk: { // Widget.createFrom(blk: {
var spacer = Widget.Spacer.init(); // var spacer = Widget.Spacer.init();
break :blk &spacer; // break :blk &spacer;
}), // }),
}); // Layout.createFrom(framing: {
break :layout &hstack; // var framing = Layout.Framing.init(
}); // allocator,
// .{
// .style = .{
// .fg = .{
// .index = 6,
// },
// },
// .frame = .round,
// .title = .{
// .str = "VStack",
// .style = .{
// .ul_style = .single,
// .ul = .{ .index = 6 },
// .bold = true,
// },
// },
// },
// .{
// .layout = Layout.createFrom(
// padding: {
// var padding = Layout.Marging.init(
// allocator,
// .{
// .margin = 10,
// },
// .{
// .layout = Layout.createFrom(vstack: {
// var vstack = Layout.VStack.init(allocator, .{
// Widget.createFrom(blk: {
// const file = try std.fs.cwd().openFile("./src/app.zig", .{});
// defer file.close();
// var widget = Widget.RawText.init(allocator, file);
// break :blk &widget;
// }),
// Widget.createFrom(blk: {
// var spacer = Widget.Spacer.init();
// break :blk &spacer;
// }),
// Widget.createFrom(blk: {
// const file = try std.fs.cwd().openFile("./src/main.zig", .{});
// defer file.close();
// var widget = Widget.RawText.init(allocator, file);
// break :blk &widget;
// }),
// });
// break :vstack &vstack;
// }),
// },
// );
// break :padding &padding;
// },
// ),
// },
// );
// break :framing &framing;
// }),
// Widget.createFrom(blk: {
// var spacer = Widget.Spacer.init();
// break :blk &spacer;
// }),
// });
// break :layout &hstack;
// });
defer layout.deinit(); defer layout.deinit();
try app.start(); try app.start();

View File

@@ -131,6 +131,7 @@ pub fn Direct(comptime _: bool) type {
pub fn clear(this: *@This(), size: Size) !void { pub fn clear(this: *@This(), size: Size) !void {
_ = this; _ = this;
// TODO: this should instead by dynamic and correct of size (terminal could be too large currently)
std.debug.assert(1028 > size.cols); std.debug.assert(1028 > size.cols);
var buf: [1028]u8 = undefined; var buf: [1028]u8 = undefined;
@memset(buf[0..], ' '); @memset(buf[0..], ' ');