From 5acca12abf7a90b736fe14e8482f7b767aaa3270 Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Thu, 30 Oct 2025 16:28:49 +0100 Subject: [PATCH] feat: rework header; add footer; padding for content --- src/content.zig | 83 +++++++++++++++++++++++++++++++++++++++++++ src/main.zig | 88 ++++++++++++++++++++++++++++++---------------- src/navigation.zig | 63 +++++++++++++++++++++++++++++++++ 3 files changed, 204 insertions(+), 30 deletions(-) create mode 100644 src/navigation.zig diff --git a/src/content.zig b/src/content.zig index a18c92c..c524487 100644 --- a/src/content.zig +++ b/src/content.zig @@ -53,6 +53,89 @@ pub fn Content(App: type) type { }; } +pub fn Title(App: type) type { + return struct { + text: []const u8, + + pub fn init() @This() { + return .{ + .text = "", + }; + } + + pub fn element(this: *@This()) App.Element { + return .{ + .ptr = this, + .vtable = &.{ + .content = content, + }, + }; + } + + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { + const this: *const @This() = @ptrCast(@alignCast(ctx)); + assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); + for (0.., this.text) |idx, cp| { + cells[idx].style.fg = .green; + cells[idx].style.emphasis = &.{.bold}; + cells[idx].cp = cp; + + // NOTE do not write over the contents of this `Container`'s `Size` + if (idx == cells.len - 1) break; + } + } + }; +} + +pub fn InfoBanner(App: type) type { + return struct { + left_text: []const u8, + right_text: []const u8, + + pub fn init() @This() { + return .{ + .left_text = "Build with zig", + .right_text = "Yves Biener (@yves-biener)", + }; + } + + pub fn element(this: *@This()) App.Element { + return .{ + .ptr = this, + .vtable = &.{ + .content = content, + }, + }; + } + + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { + const this: *const @This() = @ptrCast(@alignCast(ctx)); + assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); + + for (0.., this.left_text) |idx, cp| { + // NOTE do not write over the contents of this `Container`'s `Size` + if (idx == cells.len) break; + + cells[idx].style.fg = .default; + cells[idx].style.emphasis = &.{.dim}; + cells[idx].cp = cp; + } + + var start_idx = size.x -| this.right_text.len; + if (start_idx <= this.left_text.len) start_idx = this.left_text.len + 1; + + for (start_idx.., this.right_text) |idx, cp| { + // NOTE do not write over the contents of this `Container`'s `Size` + if (idx == cells.len) break; + + cells[idx].style.fg = .default; + cells[idx].style.emphasis = &.{.dim}; + cells[idx].cp = cp; + } + } + }; +} + const std = @import("std"); const Allocator = std.mem.Allocator; const assert = std.debug.assert; diff --git a/src/main.zig b/src/main.zig index 0b623b4..754b0b1 100644 --- a/src/main.zig +++ b/src/main.zig @@ -19,41 +19,65 @@ pub fn main() !void { }, .{}); defer container.deinit(); - var header: App.Container = try .init(allocator, .{ - .size = .{ - .dim = .{ .y = 1 }, - .grow = .horizontal, - }, - }, .{}); - var navigation: App.Container = try .init(allocator, .{ - .layout = .{ - .separator = .{ .enabled = true }, - }, - }, .{}); - // about navigation button + // header with navigation buttons and content's title { - var button: App.Button(.about) = .init(&app.queue, .init(.default, "about")); - try navigation.append(try .init(allocator, .{ + var header: App.Container = try .init(allocator, .{ .size = .{ - .dim = .{ .x = 5 + 2 }, - .grow = .vertical, + .dim = .{ .y = 1 }, + .grow = .horizontal, }, - }, button.element())); + .layout = .{ + .separator = .{ .enabled = true }, + }, + }, .{}); + // title + { + var title: Title = .init(); + try header.append(try .init(allocator, .{}, title.element())); + } + // about navigation button + { + var button: NavigationButton(.about) = .init(&app.model, &app.queue); + try header.append(try .init(allocator, .{ + .size = .{ + .dim = .{ .x = 5 + 2 }, + .grow = .vertical, + }, + }, button.element())); + } + // blog navigation button + { + var button: NavigationButton(.blog) = .init(&app.model, &app.queue); + try header.append(try .init(allocator, .{ + .size = .{ + .dim = .{ .x = 4 + 2 }, + .grow = .vertical, + }, + }, button.element())); + } + try container.append(header); } - // blog navigation button + // main actual tui_website page content { - var button: App.Button(.blog) = .init(&app.queue, .init(.default, "blog")); - try navigation.append(try .init(allocator, .{ - .size = .{ - .dim = .{ .x = 4 + 2 }, - .grow = .vertical, - }, - }, button.element())); + // intermediate container for *padding* + var content: Content = .init(allocator); + var content_container: App.Container = try .init(allocator, .{ + .layout = .{ .padding = .horizontal(2) }, + }, .{}); + try content_container.append(try .init(allocator, .{}, content.element())); + try container.append(content_container); + } + // footer + { + var info_banner: InfoBanner = .init(); + const footer: App.Container = try .init(allocator, .{ + .size = .{ + .dim = .{ .y = 1 }, + .grow = .horizontal, + }, + }, info_banner.element()); + try container.append(footer); } - try header.append(navigation); - try container.append(header); - var content: Content = .init(allocator); - try container.append(try .init(allocator, .{}, content.element())); try app.start(); defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); @@ -107,8 +131,12 @@ const App = zterm.App( }, ); +const contents = @import("content.zig"); const Model = @import("model.zig"); -const Content = @import("content.zig").Content(App); +const Content = contents.Content(App); +const Title = contents.Title(App); +const InfoBanner = contents.InfoBanner(App); +const NavigationButton = @import("navigation.zig").NavigationButton(App); test { std.testing.refAllDeclsRecursive(@This()); diff --git a/src/navigation.zig b/src/navigation.zig new file mode 100644 index 0000000..16acc83 --- /dev/null +++ b/src/navigation.zig @@ -0,0 +1,63 @@ +pub fn NavigationButton(App: type) fn (std.meta.FieldEnum(App.Event)) type { + const navigation_struct = struct { + fn navigation_fn(page: std.meta.FieldEnum(App.Event)) type { + return struct { + text: []const u8, + highlight: bool, + queue: *App.Queue, + + pub fn init(model: *const App.Model, queue: *App.Queue) @This() { + return .{ + .text = @tagName(page), + .highlight = switch (page) { + .about => model.page == .about, + .blog => model.page == .blog, + else => @compileError("NavigationButton initiated with non page `App.Event` variant"), + }, + .queue = queue, + }; + } + + pub fn element(this: *@This()) App.Element { + return .{ + .ptr = this, + .vtable = &.{ + .handle = handle, + .content = content, + }, + }; + } + + fn handle(ctx: *anyopaque, _: *App.Model, event: App.Event) !void { + const this: *@This() = @ptrCast(@alignCast(ctx)); + switch (event) { + .about, .blog => this.highlight = event == page, + // TODO accept key input too? + .mouse => |mouse| if (mouse.button == .left and mouse.kind == .release) this.queue.push(page), + else => {}, + } + } + + fn content(ctx: *anyopaque, _: *const App.Model, cells: []zterm.Cell, size: zterm.Point) !void { + const this: *const @This() = @ptrCast(@alignCast(ctx)); + assert(cells.len == @as(usize, size.x) * @as(usize, size.y)); + assert(size.x == this.text.len + 2); + assert(size.y == 1); + + for (1.., this.text) |idx, cp| { + cells[idx].style.fg = if (this.highlight) .black else .default; + cells[idx].style.bg = if (this.highlight) .green else .default; + cells[idx].style.emphasis = &.{.underline}; + cells[idx].cp = cp; + } + } + }; + } + }; + return navigation_struct.navigation_fn; +} + +const std = @import("std"); +const Allocator = std.mem.Allocator; +const assert = std.debug.assert; +const zterm = @import("zterm");