add(layout/vstack): intial implementation of vstack layout

This commit is contained in:
2024-11-09 16:57:05 +01:00
parent b5c5f4e3e2
commit b418e4d3a7
4 changed files with 67 additions and 32 deletions

View File

@@ -21,20 +21,25 @@ delay for each frame in each line of the output.
- [ ] Have clickable/navigatable links inside of the tui application - [ ] Have clickable/navigatable links inside of the tui application
- [ ] Launch simple http server alongside tui application - [ ] Launch simple http server alongside tui application
- [ ] Create other layouts ---
- [ ] Split into own repository
- [ ] Create other layouts
- [ ] horizontal stack - [ ] horizontal stack
- [ ] vertical stack - [ ] vertical stack
- [ ] `Layout` in `Layout`? -> interfaces are very similar anyway - [ ] `Layout` in `Layout`? -> interfaces are very similar anyway
- [ ] Building Block `Layout`s - [ ] Building Block `Layout`s
- [ ] Framing `Layout` - [ ] Framing `Layout`
- [ ] Padding `Layout` - [ ] Padding `Layout`
- [ ] Create demo gifs using [vhs](https://github.com/charmbracelet/vhs)
- [ ] Move documentation into new repository
- [ ] add dependency to new repository into this project
--- ---
## Branch: `own-tty-visuals` ## Branch: `own-tty-visuals`
- [ ] How can I support to run a sub-process inside of a given pane / layout? - [ ] How can I support to run a sub-process inside of a given pane / layout?
- [ ] Create demo gifs using [vhs](https://github.com/charmbracelet/vhs)
- [x] Could I simulate a corresponding event loop? - [x] Could I simulate a corresponding event loop?
- emmit as many as possible through another thread (until the event queue is full?) [1023] - emmit as many as possible through another thread (until the event queue is full?) [1023]
@@ -43,3 +48,5 @@ delay for each frame in each line of the output.
-> Or buffered writer to the `std.posix.STDOUT_FILENO`? -> Or buffered writer to the `std.posix.STDOUT_FILENO`?
-> I could use this to see if it makes sense to implement a buffered version using a screen buffer (to only render the differences?) -> I could use this to see if it makes sense to implement a buffered version using a screen buffer (to only render the differences?)
- seems pretty good (with some exceptions) - seems pretty good (with some exceptions)
- [ ] styling could be tricky with a given layout (which introduces corresponding line breaks ...)

View File

@@ -8,6 +8,8 @@ const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error; const Error = @import("../event.zig").Error;
const Key = @import("../key.zig"); const Key = @import("../key.zig");
const log = std.log.scoped(.layout_vstack);
pub fn Layout(comptime Event: type) type { pub fn Layout(comptime Event: type) type {
if (!isTaggedUnion(Event)) { if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
@@ -78,9 +80,17 @@ pub fn Layout(comptime Event: type) type {
switch (event) { switch (event) {
.resize => |size| { .resize => |size| {
this.size = size; 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;
// adjust size according to the containing elements // adjust size according to the containing elements
for (this.elements.items) |*element| { for (this.elements.items) |*element| {
const sub_event = event; const sub_event: Event = .{
.resize = .{
.cols = size.cols,
.rows = rows,
},
};
switch (element.*) { switch (element.*) {
.layout => |*layout| { .layout => |*layout| {
const events = try layout.handle(sub_event); const events = try layout.handle(sub_event);
@@ -115,17 +125,22 @@ pub fn Layout(comptime Event: type) type {
pub fn content(this: *@This()) !*Contents { pub fn content(this: *@This()) !*Contents {
this.contents.clearRetainingCapacity(); this.contents.clearRetainingCapacity();
// TODO: concat contents accordingly to create a vertical stack // TODO: concat contents accordingly to create a horizontal stack
for (this.elements.items) |*element| { for (this.elements.items, 1..) |*element, i| {
switch (element.*) { switch (element.*) {
.layout => |*layout| { .layout => |*layout| {
const layout_content = try layout.content(); const layout_content = try layout.content();
try this.contents.appendSlice(layout_content.items); try this.contents.appendSlice(layout_content.items);
}, },
.widget => |*widget| { .widget => |*widget| {
try this.contents.appendSlice(try widget.content()); const widget_content = try widget.content();
try this.contents.appendSlice(widget_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; return &this.contents;
} }

View File

@@ -33,17 +33,21 @@ pub fn main() !void {
var rawText = App.Widget.RawText.init(allocator, file); var rawText = App.Widget.RawText.init(allocator, file);
file.close(); file.close();
const doc = try std.fs.cwd().openFile("./doc/test.md", .{});
var docText = App.Widget.RawText.init(allocator, doc);
doc.close();
var framing = App.Layout.Framing.init(allocator, .{ var framing = App.Layout.Framing.init(allocator, .{
.widget = App.Widget.createFrom(&rawText), .widget = App.Widget.createFrom(&rawText),
}); });
var vstack = App.Layout.VStack.init(allocator, .{ var hstack = App.Layout.HStack.init(allocator, .{
App.Layout.createFrom(&framing), App.Layout.createFrom(&framing),
}); });
var hstack = App.Layout.HStack.init(allocator, .{ var vstack = App.Layout.VStack.init(allocator, .{
App.Layout.createFrom(&vstack), App.Widget.createFrom(&docText),
App.Layout.createFrom(&hstack),
}); });
var layout = App.Layout.createFrom(&vstack);
var layout = App.Layout.createFrom(&hstack);
defer layout.deinit(); defer layout.deinit();
try app.start(); try app.start();

View File

@@ -6,30 +6,37 @@ const isTaggedUnion = @import("../event.zig").isTaggedUnion;
const Error = @import("../event.zig").Error; const Error = @import("../event.zig").Error;
const Key = @import("../key.zig"); const Key = @import("../key.zig");
const log = std.log.scoped(.widget_rawtext);
pub fn Widget(comptime Event: type) type { pub fn Widget(comptime Event: type) type {
if (!isTaggedUnion(Event)) { if (!isTaggedUnion(Event)) {
@compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`."); @compileError("Provided user event `Event` for `Layout(comptime Event: type)` is not of type `union(enum)`.");
} }
const Contents = std.ArrayList(u8);
return struct { return struct {
c: std.ArrayList(u8) = undefined, contents: Contents = undefined,
line_index: std.ArrayList(usize) = undefined, line_index: std.ArrayList(usize) = undefined,
line: usize = 0, line: usize = 0,
size: terminal.Size = undefined, size: terminal.Size = undefined,
pub fn init(allocator: std.mem.Allocator, file: std.fs.File) @This() { pub fn init(allocator: std.mem.Allocator, file: std.fs.File) @This() {
var c = std.ArrayList(u8).init(allocator); var contents = Contents.init(allocator);
var line_index = std.ArrayList(usize).init(allocator); var line_index = std.ArrayList(usize).init(allocator);
file.reader().readAllArrayList(&c, 4192) catch {}; file.reader().readAllArrayList(&contents, std.math.maxInt(usize)) catch {};
for (c.items, 0..) |item, i| { line_index.append(0) catch {};
for (contents.items, 0..) |item, i| {
if (item == '\n') { if (item == '\n') {
line_index.append(i) catch {}; line_index.append(i + 1) catch {};
} }
} }
return .{ .c = c, .line_index = line_index }; return .{
.contents = contents,
.line_index = line_index,
};
} }
pub fn deinit(this: *@This()) void { pub fn deinit(this: *@This()) void {
this.c.deinit(); this.contents.deinit();
this.line_index.deinit(); this.line_index.deinit();
this.* = undefined; this.* = undefined;
} }
@@ -39,8 +46,9 @@ pub fn Widget(comptime Event: type) type {
// store the received size // store the received size
.resize => |size| { .resize => |size| {
this.size = size; this.size = size;
if (this.line > this.line_index.items.len - 1 - size.rows) { log.debug("Using size: {{ .cols = {d}, .rows = {d} }}", .{ size.cols, size.rows });
this.line = this.line_index.items.len - 1 - size.rows; if (this.line > this.line_index.items.len -| 1 -| size.rows) {
this.line = this.line_index.items.len -| 1 -| size.rows;
} }
}, },
.key => |key| { .key => |key| {
@@ -50,11 +58,11 @@ pub fn Widget(comptime Event: type) type {
} }
if (key.matches(.{ .cp = 'G' })) { if (key.matches(.{ .cp = 'G' })) {
// bottom // bottom
this.line = this.line_index.items.len - 1 - this.size.rows; this.line = this.line_index.items.len -| 1 -| this.size.rows;
} }
if (key.matches(.{ .cp = 'j' })) { if (key.matches(.{ .cp = 'j' })) {
// down // down
if (this.line < this.line_index.items.len - 1 - this.size.rows) { if (this.line < this.line_index.items.len -| 1 -| this.size.rows) {
this.line +|= 1; this.line +|= 1;
} }
} }
@@ -70,16 +78,17 @@ pub fn Widget(comptime Event: type) type {
pub fn content(this: *@This()) ![]u8 { pub fn content(this: *@This()) ![]u8 {
if (this.size.rows >= this.line_index.items.len) { if (this.size.rows >= this.line_index.items.len) {
return this.c.items; return this.contents.items;
} else { } else {
// more rows than we can display // more rows than we can display
const i = this.line_index.items[this.line]; 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;
if (e >= this.line_index.items.len) { if (e >= this.line_index.items.len) {
return this.c.items[i..]; return this.contents.items[i..];
} }
const x = this.line_index.items[e]; const x = this.line_index.items[e] - 1;
return this.c.items[i..x]; return this.contents.items[i..x];
} }
} }
}; };