From bc1bc757d490cfee8e436443d378256871790f83 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Wed, 13 Nov 2024 18:08:58 +0100 Subject: [PATCH] add(layout/Marging): relative margins for `Element`s --- src/layout.zig | 4 +- src/layout/Framing.zig | 8 +- src/layout/HStack.zig | 4 +- src/layout/Marging.zig | 161 ++++++++++++++++++++++++++++++++++++ src/layout/Padding.zig | 6 ++ src/layout/VStack.zig | 4 +- src/main.zig | 182 +++++++++++++++++++++++++---------------- src/render.zig | 1 + 8 files changed, 293 insertions(+), 77 deletions(-) create mode 100644 src/layout/Marging.zig diff --git a/src/layout.zig b/src/layout.zig index 0378e29..9837a3a 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -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 VStack = @import("layout/VStack.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); }; - // test widget implementation satisfies the interface + // test layout implementation satisfies the interface comptime Type.Interface.satisfiedBy(Type); comptime Type.Interface.satisfiedBy(Type.HStack); comptime Type.Interface.satisfiedBy(Type.VStack); comptime Type.Interface.satisfiedBy(Type.Padding); + comptime Type.Interface.satisfiedBy(Type.Marging); comptime Type.Interface.satisfiedBy(Type.Framing); return Type; } diff --git a/src/layout/Framing.zig b/src/layout/Framing.zig index 7fe64db..1399afd 100644 --- a/src/layout/Framing.zig +++ b/src/layout/Framing.zig @@ -19,6 +19,12 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t 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, @@ -159,7 +165,7 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t // render bottom: +---+ try terminal.setCursorPosition(.{ .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]); for (0..this.size.cols -| 2) |_| { diff --git a/src/layout/HStack.zig b/src/layout/HStack.zig index bb5dd7e..7574b2d 100644 --- a/src/layout/HStack.zig +++ b/src/layout/HStack.zig @@ -18,14 +18,14 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t if (!isTaggedUnion(Element)) { @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")) { @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")) { @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 { diff --git a/src/layout/Marging.zig b/src/layout/Marging.zig new file mode 100644 index 0000000..6f329f6 --- /dev/null +++ b/src/layout/Marging.zig @@ -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); + }, + } + } + }; +} diff --git a/src/layout/Padding.zig b/src/layout/Padding.zig index 805ec39..d1fbd92 100644 --- a/src/layout/Padding.zig +++ b/src/layout/Padding.zig @@ -18,6 +18,12 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t 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, diff --git a/src/layout/VStack.zig b/src/layout/VStack.zig index 7b89062..eefe7e2 100644 --- a/src/layout/VStack.zig +++ b/src/layout/VStack.zig @@ -18,14 +18,14 @@ pub fn Layout(comptime Event: type, comptime Element: type, comptime Renderer: t if (!isTaggedUnion(Element)) { @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")) { @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")) { @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 { diff --git a/src/main.zig b/src/main.zig index 86a51fe..e8e68f5 100644 --- a/src/main.zig +++ b/src/main.zig @@ -50,77 +50,117 @@ pub fn main() !void { // }); // break :layout &stack; // }); - var layout = Layout.createFrom(layout: { - 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: { - var padding = Layout.Padding.init( - allocator, - .{ - .padding = 3, - }, - .{ - .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; - }); + var layout = Layout.createFrom( + padding: { + var padding = Layout.Marging.init( + allocator, + .{ + .left = 15, + .right = 15, + }, + .{ + .layout = Layout.createFrom(framing: { + var framing = Layout.Framing.init(allocator, .{ .style = .{ .fg = .{ .index = 6 } } }, .{ + .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 :framing &framing; + }), + }, + ); + break :padding &padding; + }, + ); + // var layout = Layout.createFrom(layout: { + // 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: { + // 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(); try app.start(); diff --git a/src/render.zig b/src/render.zig index 0a7a91a..0772e10 100644 --- a/src/render.zig +++ b/src/render.zig @@ -131,6 +131,7 @@ pub fn Direct(comptime _: bool) type { pub fn clear(this: *@This(), size: Size) !void { _ = this; + // TODO: this should instead by dynamic and correct of size (terminal could be too large currently) std.debug.assert(1028 > size.cols); var buf: [1028]u8 = undefined; @memset(buf[0..], ' ');