mod: replace Layout.content with Layout.render
Some checks failed
Zig Project Action / Lint, Spell-check and test zig project (push) Failing after 30s

The App.Renderer is used for the new `Layout.render` method. Each layout
renders itself now with corresponding renderers which might only update
parts of the screen, etc.
This commit is contained in:
2024-11-10 14:34:28 +01:00
parent b32556720e
commit b314ff7813
12 changed files with 112 additions and 92 deletions

1
.gitignore vendored
View File

@@ -1,3 +1,4 @@
.zig-cache/
zig-out/
log
TODO.md

View File

@@ -45,9 +45,9 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls
}
return struct {
pub const Event = mergeTaggedUnions(event.SystemEvent, E);
pub const Layout = @import("layout.zig").Layout(Event);
pub const Widget = @import("widget.zig").Widget(Event);
pub const Renderer = R(fullscreen);
pub const Layout = @import("layout.zig").Layout(Event, Renderer);
pub const Widget = @import("widget.zig").Widget(Event);
queue: Queue(Event, 256) = .{},
thread: ?std.Thread = null,

View File

@@ -1,7 +1,7 @@
//! Dynamic dispatch for layout implementations.
//! Each layout should at last implement these functions:
//! - handle(this: *@This(), event: Event) anyerror!*std.ArrayList(Event) {}
//! - content(this: *@This()) anyerror!*std.ArrayList(u8) {}
//! - render(this: *@This(), renderer: Renderer) anyerror!void {}
//! - deinit(this: *@This()) void {}
//!
//! Create a `Layout` using `createFrom(object: anytype)` and use them through
@@ -11,10 +11,13 @@
//! Each `Layout` is responsible for clearing the allocated memory of the used
//! widgets when deallocated. This means that `deinit()` will also deallocate
//! every used widget 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) type {
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)`.");
}
@@ -25,7 +28,7 @@ pub fn Layout(comptime Event: type) type {
const VTable = struct {
handle: *const fn (this: *LayoutType, event: Event) anyerror!*Events,
content: *const fn (this: *LayoutType) anyerror!*std.ArrayList(u8),
render: *const fn (this: *LayoutType, renderer: Renderer) anyerror!void,
deinit: *const fn (this: *LayoutType) void,
};
@@ -37,9 +40,9 @@ pub fn Layout(comptime Event: type) type {
return try this.vtable.handle(this, event);
}
// Return the entire content of this `Layout`.
pub fn content(this: *LayoutType) !*std.ArrayList(u8) {
return try this.vtable.content(this);
// 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 {
@@ -58,13 +61,13 @@ pub fn Layout(comptime Event: type) type {
return try layout.handle(event);
}
}.handle,
.content = struct {
// Return the entire content of this `Layout`.
fn content(this: *LayoutType) !*std.ArrayList(u8) {
.render = struct {
// Render the contents of this `Layout`.
fn render(this: *LayoutType, renderer: Renderer) !void {
const layout: @TypeOf(object) = @ptrFromInt(this.object);
return try layout.content();
try layout.render(renderer);
}
}.content,
}.render,
.deinit = struct {
fn deinit(this: *LayoutType) void {
const layout: @TypeOf(object) = @ptrFromInt(this.object);
@@ -76,9 +79,9 @@ pub fn Layout(comptime Event: type) type {
}
// import and export of `Layout` implementations
pub const HStack = @import("layout/HStack.zig").Layout(Event);
pub const VStack = @import("layout/VStack.zig").Layout(Event);
pub const Padding = @import("layout/Padding.zig").Layout(Event);
pub const Framing = @import("layout/Framing.zig").Layout(Event);
pub const HStack = @import("layout/HStack.zig").Layout(Event, Renderer);
pub const VStack = @import("layout/VStack.zig").Layout(Event, Renderer);
pub const Padding = @import("layout/Padding.zig").Layout(Event, Renderer);
pub const Framing = @import("layout/Framing.zig").Layout(Event, Renderer);
};
}

View File

@@ -11,32 +11,28 @@ const Key = terminal.Key;
const log = std.log.scoped(.layout_framing);
pub fn Layout(comptime Event: type) type {
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 Element = union(enum) {
layout: @import("../layout.zig").Layout(Event),
layout: @import("../layout.zig").Layout(Event, Renderer),
widget: @import("../widget.zig").Widget(Event),
};
const Events = std.ArrayList(Event);
const Contents = std.ArrayList(u8);
return struct {
size: terminal.Size = undefined,
contents: Contents = undefined,
element: Element = undefined,
events: Events = undefined,
pub fn init(allocator: std.mem.Allocator, element: Element) @This() {
return .{
.contents = Contents.init(allocator),
.element = element,
.events = Events.init(allocator),
};
}
pub fn deinit(this: *@This()) void {
this.contents.deinit();
this.events.deinit();
switch ((&this.element).*) {
.layout => |*layout| {
@@ -85,19 +81,18 @@ pub fn Layout(comptime Event: type) type {
return &this.events;
}
pub fn content(this: *@This()) !*Contents {
this.contents.clearRetainingCapacity();
pub fn render(this: *@This(), renderer: Renderer) !void {
// TODO: padding contents accordingly
switch ((&this.element).*) {
.layout => |*layout| {
const layout_content = try layout.content();
try this.contents.appendSlice(layout_content.items);
try layout.render(renderer);
},
.widget => |*widget| {
try this.contents.appendSlice(try widget.content());
const content = try widget.content();
// TODO: use renderer
_ = try terminal.write(content);
},
}
return &this.contents;
}
};
}

View File

@@ -11,23 +11,21 @@ const Key = terminal.Key;
const log = std.log.scoped(.layout_hstack);
pub fn Layout(comptime Event: type) type {
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 Widget = @import("../widget.zig").Widget(Event);
const Lay = @import("../layout.zig").Layout(Event);
const Lay = @import("../layout.zig").Layout(Event, Renderer);
const Element = union(enum) {
layout: Lay,
widget: Widget,
};
const Elements = std.ArrayList(Element);
const Events = std.ArrayList(Event);
const Contents = std.ArrayList(u8);
return struct {
// TODO: current focused `Element`?
size: terminal.Size = undefined,
contents: Contents = undefined,
elements: Elements = undefined,
events: Events = undefined,
@@ -53,7 +51,6 @@ pub fn Layout(comptime Event: type) type {
@compileError("child: " ++ field.name ++ " is not of type " ++ @typeName(Lay) ++ " or " ++ @typeName(Widget) ++ " but " ++ @typeName(ChildType));
}
return .{
.contents = Contents.init(allocator),
.elements = elements,
.events = Events.init(allocator),
};
@@ -61,7 +58,6 @@ pub fn Layout(comptime Event: type) type {
pub fn deinit(this: *@This()) void {
this.events.deinit();
this.contents.deinit();
for (this.elements.items) |*element| {
switch (element.*) {
.layout => |*layout| {
@@ -116,21 +112,21 @@ pub fn Layout(comptime Event: type) type {
return &this.events;
}
pub fn content(this: *@This()) !*Contents {
this.contents.clearRetainingCapacity();
// TODO: concat contents accordingly to create a vertical stack
pub fn render(this: *@This(), renderer: Renderer) !void {
// TODO: concat contents accordingly to create a horizontal stack
for (this.elements.items) |*element| {
switch (element.*) {
.layout => |*layout| {
const layout_content = try layout.content();
try this.contents.appendSlice(layout_content.items);
try layout.render(renderer);
},
.widget => |*widget| {
try this.contents.appendSlice(try widget.content());
// TODO: clear per widget if necessary (i.e. can I query that?)
// TODO: render using `renderer`
const content = try widget.content();
_ = try terminal.write(content);
},
}
}
return &this.contents;
}
};
}

View File

@@ -11,32 +11,28 @@ const Key = terminal.Key;
const log = std.log.scoped(.layout_padding);
pub fn Layout(comptime Event: type) type {
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 Element = union(enum) {
layout: @import("../layout.zig").Layout(Event),
layout: @import("../layout.zig").Layout(Event, Renderer),
widget: @import("../widget.zig").Widget(Event),
};
const Events = std.ArrayList(Event);
const Contents = std.ArrayList(u8);
return struct {
size: terminal.Size = undefined,
contents: Contents = undefined,
element: Element = undefined,
events: Events = undefined,
pub fn init(allocator: std.mem.Allocator, element: Element) @This() {
return .{
.contents = Contents.init(allocator),
.element = element,
.events = Events.init(allocator),
};
}
pub fn deinit(this: *@This()) void {
this.contents.deinit();
this.events.deinit();
switch ((&this.element).*) {
.layout => |*layout| {
@@ -85,19 +81,18 @@ pub fn Layout(comptime Event: type) type {
return &this.events;
}
pub fn content(this: *@This()) !*Contents {
this.contents.clearRetainingCapacity();
pub fn render(this: *@This(), renderer: Renderer) !void {
// TODO: padding contents accordingly
switch ((&this.element).*) {
.layout => |*layout| {
const layout_content = try layout.content();
try this.contents.appendSlice(layout_content.items);
try layout.render(renderer);
},
.widget => |*widget| {
try this.contents.appendSlice(try widget.content());
const content = try widget.content();
// TODO: use renderer
_ = try terminal.write(content);
},
}
return &this.contents;
}
};
}

View File

@@ -11,23 +11,24 @@ const Key = terminal.Key;
const log = std.log.scoped(.layout_vstack);
pub fn Layout(comptime Event: type) type {
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 Widget = @import("../widget.zig").Widget(Event);
const Lay = @import("../layout.zig").Layout(Event);
const Lay = @import("../layout.zig").Layout(Event, Renderer);
const Element = union(enum) {
layout: Lay,
widget: Widget,
};
const Elements = std.ArrayList(Element);
const Events = std.ArrayList(Event);
const Contents = std.ArrayList(u8);
return struct {
// TODO: current focused `Element`?
// FIX: this should not be 'hardcoded' but dynamically be calculated and updated (i.e. through the event system)
anchor: terminal.Position = .{ .col = 1, .row = 1 },
size: terminal.Size = undefined,
contents: Contents = undefined,
element_rows: u16 = undefined,
elements: Elements = undefined,
events: Events = undefined,
@@ -53,7 +54,6 @@ pub fn Layout(comptime Event: type) type {
@compileError("child: " ++ field.name ++ " is not of type " ++ @typeName(Lay) ++ " or " ++ @typeName(Widget) ++ " but " ++ @typeName(ChildType));
}
return .{
.contents = Contents.init(allocator),
.elements = elements,
.events = Events.init(allocator),
};
@@ -61,7 +61,6 @@ pub fn Layout(comptime Event: type) type {
pub fn deinit(this: *@This()) void {
this.events.deinit();
this.contents.deinit();
for (this.elements.items) |*element| {
switch (element.*) {
.layout => |*layout| {
@@ -83,9 +82,15 @@ pub fn Layout(comptime Event: type) type {
this.size = size;
log.debug("Using size: {{ .cols = {d}, .rows = {d} }}", .{ size.cols, size.rows });
const len: u16 = @truncate(this.elements.items.len);
const rows = size.rows / len;
this.element_rows = @divTrunc(size.rows, len);
var overflow = this.size.rows % len;
// adjust size according to the containing elements
for (this.elements.items) |*element| {
var rows = this.element_rows;
if (overflow > 0) {
overflow -|= 1;
rows += 1;
}
const sub_event: Event = .{
.resize = .{
.cols = size.cols,
@@ -124,26 +129,32 @@ pub fn Layout(comptime Event: type) type {
return &this.events;
}
pub fn content(this: *@This()) !*Contents {
this.contents.clearRetainingCapacity();
// TODO: concat contents accordingly to create a horizontal stack
for (this.elements.items, 1..) |*element, i| {
pub fn render(this: *@This(), renderer: Renderer) !void {
// FIX: renderer should clear only what is going to change! (i.e. the 'active' widget / layout)
try renderer.clear(this.anchor, this.size);
var overflow = this.size.rows % this.elements.items.len;
for (this.elements.items, 0..) |*element, i| {
const row_mul: u16 = @truncate(i);
var row = row_mul * this.element_rows + 1;
if (i > 0 and overflow > 0) {
overflow -|= 1;
row += 1;
}
const pos: terminal.Position = .{ .col = 1, .row = row };
log.debug("using position: .{{ .cols = {d}, .rows = {d} }}", .{ pos.col, pos.row });
// TODO: do this using the renderer
try terminal.setCursorPosition(pos);
switch (element.*) {
.layout => |*layout| {
const layout_content = try layout.content();
try this.contents.appendSlice(layout_content.items);
try layout.render(renderer);
},
.widget => |*widget| {
const widget_content = try widget.content();
try this.contents.appendSlice(widget_content);
// TODO: clear per widget if necesary (i.e. can I query that?)
const content = try widget.content();
_ = try terminal.write(content);
},
}
// TODO: support clear positioning of content on the tui screen
if (i != this.elements.items.len) {
try this.contents.appendSlice("\n"); // NOTE: this assumes that the previous content fills all the provided size.rows accordingly with content, such that a newline introduces the start of the next content
}
}
return &this.contents;
}
};
}

View File

@@ -24,7 +24,9 @@ pub fn main() !void {
const allocator = gpa.allocator();
var app: App = .{};
var renderer: App.Renderer = .{};
const renderer: App.Renderer = .{};
// FIX: when not running fullscreen, the application needs to screen down accordingly to display the contents
// -> size hint how much should it use?
const mainFile = try std.fs.cwd().openFile("./src/main.zig", .{});
var mainFileText = App.Widget.RawText.init(allocator, mainFile);
@@ -35,17 +37,11 @@ pub fn main() !void {
appFile.close();
var spacer = App.Widget.Spacer.init();
var framing = App.Layout.Framing.init(allocator, .{
.widget = App.Widget.createFrom(&mainFileText),
});
var hstack = App.Layout.HStack.init(allocator, .{
App.Layout.createFrom(&framing),
});
// TODO: corresponding contents need to be filled out by the layout accordingly!
var vstack = App.Layout.VStack.init(allocator, .{
App.Widget.createFrom(&appFileText),
App.Layout.createFrom(&hstack),
App.Widget.createFrom(&spacer),
App.Widget.createFrom(&mainFileText),
});
var layout = App.Layout.createFrom(&vstack);
defer layout.deinit();
@@ -88,6 +84,6 @@ pub fn main() !void {
for (events.items) |e| {
app.postEvent(e);
}
try renderer.render(try layout.content());
try layout.render(renderer);
}
}

View File

@@ -3,6 +3,7 @@ const std = @import("std");
const terminal = @import("terminal.zig");
const Contents = std.ArrayList(u8);
const Position = terminal.Position;
const Size = terminal.Size;
pub fn Buffered(comptime fullscreen: bool) type {
@@ -40,16 +41,23 @@ pub fn Buffered(comptime fullscreen: bool) type {
};
}
pub fn Plain(comptime fullscreen: bool) type {
pub fn Plain(comptime _: bool) type {
return struct {
pub fn render(this: *@This(), content: *Contents) !void {
pub fn clear(this: @This(), anchor: Position, size: Size) !void {
_ = this;
if (fullscreen) {
_ = size;
// NOTE: clear entire screen for the first content (derived from the anchor being at the very left-top)
if (anchor.col == 1 and anchor.row == 1) {
try terminal.clearScreen();
try terminal.setCursorPositionHome();
}
// TODO: how would I clear the screen in case of a non fullscreen application (i.e. to clear to the start of the command)
_ = try terminal.write(content.items);
}
pub fn render(this: @This(), anchor: Position, size: Size, contents: *Contents) !void {
_ = this;
_ = size;
// FIXME: this should respect the given `size`
try terminal.setCursorPosition(anchor);
_ = try terminal.write(contents.items);
}
};
}

View File

@@ -62,6 +62,12 @@ pub fn write(buf: []const u8) !usize {
return try std.posix.write(std.posix.STDIN_FILENO, buf);
}
pub fn setCursorPosition(pos: 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 {
// Needs Raw mode (no wait for \n) to work properly cause
// control sequence will not be written without it.

View File

@@ -10,8 +10,11 @@
//!
//! Each `Widget` may cache its content and should if the contents will not
//! change for a long time.
const std = @import("std");
const isTaggedUnion = @import("event.zig").isTaggedUnion;
const log = std.log.scoped(.widget);
pub fn Widget(comptime Event: type) type {
if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
@@ -31,6 +34,12 @@ pub fn Widget(comptime Event: type) type {
// Handle the provided `Event` for this `Widget`.
pub fn handle(this: *WidgetType, event: Event) ?Event {
switch (event) {
.resize => |size| {
log.debug("received size: .{{ .cols = {d}, .rows = {d} }}", .{ size.cols, size.rows });
},
else => {},
}
return this.vtable.handle(this, event);
}

View File

@@ -46,7 +46,6 @@ pub fn Widget(comptime Event: type) type {
// store the received size
.resize => |size| {
this.size = size;
log.debug("Using size: {{ .cols = {d}, .rows = {d} }}", .{ size.cols, size.rows });
if (this.line > this.line_index.items.len -| 1 -| size.rows) {
this.line = this.line_index.items.len -| 1 -| size.rows;
}
@@ -82,11 +81,12 @@ pub fn Widget(comptime Event: type) type {
} else {
// more rows than we can display
const i = this.line_index.items[this.line];
log.debug("i := {d} this.line := {d}", .{ i, this.line });
const e = this.size.rows + this.line;
const e = this.size.rows + this.line + 1;
if (e >= this.line_index.items.len) {
return this.contents.items[i..];
}
// last line should not end with the last character (likely a newline character)
// FIX: what about files which do not end with a newline?
const x = this.line_index.items[e] - 1;
return this.contents.items[i..x];
}