diff --git a/.gitignore b/.gitignore index 2fc31d6..1a40778 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .zig-cache/ zig-out/ log +TODO.md diff --git a/src/app.zig b/src/app.zig index 79db1e1..39e3c20 100644 --- a/src/app.zig +++ b/src/app.zig @@ -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, diff --git a/src/layout.zig b/src/layout.zig index 398971b..80ee1b4 100644 --- a/src/layout.zig +++ b/src/layout.zig @@ -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); }; } diff --git a/src/layout/Framing.zig b/src/layout/Framing.zig index f8931d6..70a9718 100644 --- a/src/layout/Framing.zig +++ b/src/layout/Framing.zig @@ -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; } }; } diff --git a/src/layout/HStack.zig b/src/layout/HStack.zig index 331dd12..e3a0856 100644 --- a/src/layout/HStack.zig +++ b/src/layout/HStack.zig @@ -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; } }; } diff --git a/src/layout/Padding.zig b/src/layout/Padding.zig index 9d5d477..388af0a 100644 --- a/src/layout/Padding.zig +++ b/src/layout/Padding.zig @@ -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; } }; } diff --git a/src/layout/VStack.zig b/src/layout/VStack.zig index e04262c..0d04ab9 100644 --- a/src/layout/VStack.zig +++ b/src/layout/VStack.zig @@ -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; } }; } diff --git a/src/main.zig b/src/main.zig index 100633d..9c066e6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -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); } } diff --git a/src/render.zig b/src/render.zig index 3b968d2..12726f5 100644 --- a/src/render.zig +++ b/src/render.zig @@ -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); } }; } diff --git a/src/terminal.zig b/src/terminal.zig index d9c9b16..fdff6c1 100644 --- a/src/terminal.zig +++ b/src/terminal.zig @@ -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. diff --git a/src/widget.zig b/src/widget.zig index ca26adf..f8aa50d 100644 --- a/src/widget.zig +++ b/src/widget.zig @@ -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); } diff --git a/src/widget/RawText.zig b/src/widget/RawText.zig index a208bae..dcc714f 100644 --- a/src/widget/RawText.zig +++ b/src/widget/RawText.zig @@ -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]; }