diff --git a/examples/tui.zig b/examples/tui.zig index 7948873..7a73d49 100644 --- a/examples/tui.zig +++ b/examples/tui.zig @@ -2,7 +2,11 @@ const std = @import("std"); const zterm = @import("zterm"); const App = zterm.App( - union(enum) {}, + union(enum) { + view: union(enum) { + tui, // view instance to the corresponding view for 'tui' + }, + }, zterm.Renderer.Direct, true, ); @@ -10,6 +14,79 @@ const Cell = zterm.Cell; const Key = zterm.Key; const Layout = App.Layout; const Widget = App.Widget; +const View = App.View; + +const Tui = struct { + const Events = std.ArrayList(App.Event); + allocator: std.mem.Allocator, + layout: Layout, + + pub fn init(allocator: std.mem.Allocator) *Tui { + var tui = allocator.create(Tui) catch @panic("Out of memory: tui.zig"); + tui.allocator = allocator; + // FIXME: the layout creates an 'incorrect alignment'? + tui.layout = Layout.createFrom(Layout.VContainer.init(allocator, .{ + .{ + Layout.createFrom(Layout.Framing.init(allocator, .{ + .title = .{ + .str = "Welcome to my terminal website", + .style = .{ + .ul = .{ .index = 6 }, + .ul_style = .single, + }, + }, + }, .{ + .layout = Layout.createFrom(Layout.HContainer.init(allocator, .{ + .{ + Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{ + .{ .content = "Yves Biener", .style = .{ .bold = true } }, + })), + 25, + }, + .{ + Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{ + .{ .content = "File name", .style = .{ .bold = true } }, + })), + 50, + }, + .{ + Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{ + .{ .content = "Contacts", .style = .{ .bold = true } }, + })), + 25, + }, + })), + })), + 10, + }, + .{ + Layout.createFrom(Layout.Margin.init(allocator, .{ .left = 15, .right = 15 }, .{ + .widget = Widget.createFrom(Widget.Text.init(allocator, .default, &[1]Cell{ + .{ .content = "Does this change anything", .style = .{ .ul = .default, .ul_style = .single } }, + })), + })), + 90, + }, + })); + return tui; + } + + pub fn deinit(this: *Tui) void { + this.layout.deinit(); + this.allocator.destroy(this); + } + + pub fn handle(this: *Tui, event: App.Event) !*Events { + return try this.layout.handle(event); + } + + pub fn render(this: *Tui, renderer: *App.Renderer) !void { + try this.layout.render(renderer); + } +}; + +// TODO: create additional example with a bit more complex functionality for +// dynamic layouts, switching views, etc. const log = std.log.scoped(.tui); @@ -23,53 +100,12 @@ pub fn main() !void { var app: App = .{}; var renderer: App.Renderer = .{}; + var view: View = undefined; - // FIXME: the layout creates an 'incorrect alignment'? - var layout = Layout.createFrom(Layout.VContainer.init(allocator, .{ - .{ - Layout.createFrom(Layout.Framing.init(allocator, .{ - .title = .{ - .str = "Welcome to my terminal website", - .style = .{ - .ul = .{ .index = 6 }, - .ul_style = .single, - }, - }, - }, .{ - .layout = Layout.createFrom(Layout.HContainer.init(allocator, .{ - .{ - Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{ - .{ .content = "Yves Biener", .style = .{ .bold = true } }, - })), - 25, - }, - .{ - Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{ - .{ .content = "File name", .style = .{ .bold = true } }, - })), - 50, - }, - .{ - Widget.createFrom(Widget.Text.init(allocator, .left, &[1]Cell{ - .{ .content = "Contacts", .style = .{ .bold = true } }, - })), - 25, - }, - })), - })), - 10, - }, - .{ - Layout.createFrom(Layout.Margin.init(allocator, .{ .left = 15, .right = 15 }, .{ - .widget = Widget.createFrom(Widget.Text.init(allocator, .default, &[1]Cell{ - .{ .content = "Does this change anything", .style = .{ .ul = .default, .ul_style = .single } }, - })), - })), - 90, - }, - })); - defer layout.deinit(); + var tui_view = View.createFrom(Tui.init(allocator)); + defer tui_view.deinit(); + view = tui_view; try app.start(null); defer app.stop() catch unreachable; @@ -91,13 +127,20 @@ pub fn main() !void { .err => |err| { log.err("Received {any} with message: {s}", .{ err.err, err.msg }); }, + .view => |e| { + switch (e) { + .tui => { + view = tui_view; + }, + } + }, else => {}, } - const events = try layout.handle(event); + const events = try view.handle(event); for (events.items) |e| { app.postEvent(e); } - try layout.render(&renderer); + try view.render(&renderer); } } diff --git a/src/app.zig b/src/app.zig index da9110a..c656f37 100644 --- a/src/app.zig +++ b/src/app.zig @@ -48,6 +48,7 @@ pub fn App(comptime E: type, comptime R: fn (comptime bool) type, comptime fulls pub const Renderer = R(fullscreen); pub const Layout = @import("layout.zig").Layout(Event, Renderer); pub const Widget = @import("widget.zig").Widget(Event, Renderer); + pub const View = @import("view.zig").View(Event, Renderer); queue: Queue(Event, 256) = .{}, thread: ?std.Thread = null, diff --git a/src/event.zig b/src/event.zig index b8814da..8d9f85c 100644 --- a/src/event.zig +++ b/src/event.zig @@ -12,6 +12,7 @@ pub const Error = struct { }; // System events available to every application. +// TODO: should this also already include the .view enum option? pub const SystemEvent = union(enum) { quit, err: Error, @@ -21,6 +22,7 @@ pub const SystemEvent = union(enum) { }; pub fn mergeTaggedUnions(comptime A: type, comptime B: type) type { + // TODO: should this expect one of the unions to contain the .view value option with its corresponding associated type? if (!isTaggedUnion(A) or !isTaggedUnion(B)) { @compileError("Both types for merging tagged unions need to be of type `union(enum)`."); } diff --git a/src/view.zig b/src/view.zig new file mode 100644 index 0000000..7770ba7 --- /dev/null +++ b/src/view.zig @@ -0,0 +1,89 @@ +//! Dynamic dispatch for view implementations. Each `View` has to implement the `View.Interface` +//! +//! Create a `View` using `createFrom(object: anytype)` and use them through +//! the defined `View.Interface`. The view will take care of calling the +//! correct implementation of the corresponding underlying type. +//! +//! A `View` holds the necessary `Layout`'s for different screen sizes as well +//! as the corresponding used `Widget`'s alongside holding the corresponding memory +//! for the data shown through the `View`. +const std = @import("std"); + +const isTaggedUnion = @import("event.zig").isTaggedUnion; + +const log = std.log.scoped(.view); + +pub fn View(comptime Event: type, comptime Renderer: type) type { + if (!isTaggedUnion(Event)) { + @compileError("Provided user event `Event` for `View(comptime Event: type)` is not of type `union(enum)`."); + } + const Events = std.ArrayList(Event); + return struct { + const ViewType = @This(); + pub const Interface = @import("interface").Interface(.{ + .handle = fn (anytype, Event) anyerror!*Events, + .render = fn (anytype, *Renderer) anyerror!void, + .deinit = fn (anytype) void, + }, .{}); + + const VTable = struct { + handle: *const fn (this: *ViewType, event: Event) anyerror!*Events, + render: *const fn (this: *ViewType, renderer: *Renderer) anyerror!void, + deinit: *const fn (this: *ViewType) void, + }; + + object: *anyopaque = undefined, + vtable: *const VTable = undefined, + + /// Handle the provided `Event` for this `View`. + pub fn handle(this: *ViewType, event: Event) anyerror!*Events { + switch (event) { + .resize => |size| { + log.debug("Event .resize: {{ .anchor = {{ .col = {d}, .row = {d} }}, .cols = {d}, .rows = {d} }}", .{ + size.anchor.col, + size.anchor.row, + size.cols, + size.rows, + }); + }, + else => {}, + } + return this.vtable.handle(this, event); + } + + /// Render the content of this `View` given the `Size` of the available terminal (.resize System`Event`). + pub fn render(this: *ViewType, renderer: *Renderer) !void { + try this.vtable.render(this, renderer); + } + + pub fn deinit(this: *ViewType) void { + this.vtable.deinit(this); + } + + pub fn createFrom(object: anytype) ViewType { + return ViewType{ + .object = @ptrCast(@alignCast(object)), + .vtable = &.{ + .handle = struct { + fn handle(this: *ViewType, event: Event) !*Events { + const view: @TypeOf(object) = @ptrCast(@alignCast(this.object)); + return view.handle(event); + } + }.handle, + .render = struct { + fn render(this: *ViewType, renderer: *Renderer) !void { + const view: @TypeOf(object) = @ptrCast(@alignCast(this.object)); + try view.render(renderer); + } + }.render, + .deinit = struct { + fn deinit(this: *ViewType) void { + const view: @TypeOf(object) = @ptrCast(@alignCast(this.object)); + view.deinit(); + } + }.deinit, + }, + }; + } + }; +}