From 7b005ea4b15787084b0791beb56beaa41736840f Mon Sep 17 00:00:00 2001 From: Yves Biener Date: Fri, 21 Feb 2025 19:13:11 +0100 Subject: [PATCH] add(examples/styles): text and color styling possiblities This also contains some minor refactoring to improve the readability and understandability of the library (i.e. renaming of Style.Attributes to Style.Emphasis). --- README.md | 26 ++++--- build.zig | 10 +++ examples/styles/palette.zig | 3 +- examples/styles/text.zig | 148 ++++++++++++++++++++++++++++++++++++ src/cell.zig | 4 +- src/style.zig | 8 +- 6 files changed, 181 insertions(+), 18 deletions(-) create mode 100644 examples/styles/text.zig diff --git a/README.md b/README.md index ae4d959..95ae57b 100644 --- a/README.md +++ b/README.md @@ -90,19 +90,12 @@ the primary use-case for myself to create this library in the first place. - [x] horizontal - [x] padding - [x] gap - - [x] sizing (removed - for now at least) - - width - - height - - options - - fit - - grow - - fixed - - percent - [x] Border - [x] sides - [x] corners - [x] separators - [x] Rectangle + - [x] min size - [ ] User control - [x] event loop handling - [x] mouse support @@ -124,7 +117,7 @@ the primary use-case for myself to create this library in the first place. - [ ] image support through kitty protocol (**later**) - [ ] Inline rendering (**later**) - [ ] Examples - - [ ] Layouts + - [x] Layouts - [x] vertical - [x] horizontal - [x] grid @@ -138,6 +131,21 @@ the primary use-case for myself to create this library in the first place. - [x] mouse scrolling aware of mouse position (i.e. through multiple different scrollable `Container`) - [ ] Styles - [ ] Text styles + - [ ] Colors + - [x] forground + - [x] background + - [ ] underline + - [ ] Emphasis + - [x] none + - [x] bold + - [x] dim + - [x] italic + - [x] underline + - [x] blink + - [x] invert + - [x] hidden + - [x] strikethrough + - [ ] combinations - [x] Color palette - [ ] Error Handling - [ ] log and show error's without crashing the application diff --git a/build.zig b/build.zig index 3bbd30e..000fd97 100644 --- a/build.zig +++ b/build.zig @@ -15,6 +15,7 @@ pub fn build(b: *std.Build) void { grid, mixed, // styles: + text, palette, }; @@ -106,6 +107,14 @@ pub fn build(b: *std.Build) void { }); palette.root_module.addImport("zterm", lib); + const text = b.addExecutable(.{ + .name = "text", + .root_source_file = b.path("examples/styles/text.zig"), + .target = target, + .optimize = optimize, + }); + text.root_module.addImport("zterm", lib); + // mapping of user selected example to compile step const exe = switch (example) { // elements: @@ -118,6 +127,7 @@ pub fn build(b: *std.Build) void { .grid => grid, .mixed => mixed, // styles: + .text => text, .palette => palette, }; b.installArtifact(exe); diff --git a/examples/styles/palette.zig b/examples/styles/palette.zig index d76a088..182bc79 100644 --- a/examples/styles/palette.zig +++ b/examples/styles/palette.zig @@ -63,8 +63,7 @@ pub fn main() !void { inline for (std.meta.fields(zterm.Color)) |field| { if (comptime field.value == 0) continue; // zterm.Color.default == 0 -> skip - const color = std.meta.stringToEnum(zterm.Color, field.name).?; - try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = color } }, .{})); + try box.append(try App.Container.init(allocator, .{ .rectangle = .{ .fill = @enumFromInt(field.value) } }, .{})); } var scrollable: App.Scrollable = .init(box); try container.append(try App.Container.init(allocator, .{}, scrollable.element())); diff --git a/examples/styles/text.zig b/examples/styles/text.zig new file mode 100644 index 0000000..c53f48d --- /dev/null +++ b/examples/styles/text.zig @@ -0,0 +1,148 @@ +const std = @import("std"); +const zterm = @import("zterm"); + +const App = zterm.App(union(enum) {}); + +const log = std.log.scoped(.default); + +const QuitText = struct { + const text = "Press ctrl+c to quit."; + + pub fn element(this: *@This()) App.Element { + return .{ .ptr = this, .vtable = &.{ .content = content } }; + } + + pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void { + _ = ctx; + std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows)); + + const row = 2; + const col = size.cols / 2 -| (text.len / 2); + const anchor = (row * size.cols) + col; + + for (text, 0..) |cp, idx| { + cells[anchor + idx].style.fg = .white; + cells[anchor + idx].style.bg = .black; + cells[anchor + idx].cp = cp; + + // NOTE: do not write over the contents of this `Container`'s `Size` + if (anchor + idx == cells.len - 1) break; + } + } +}; + +const TextStyles = struct { + const text = "Example"; + + pub fn element(this: *@This()) App.Element { + return .{ .ptr = this, .vtable = &.{ .content = content } }; + } + + pub fn content(ctx: *anyopaque, cells: []zterm.Cell, size: zterm.Size) !void { + @setEvalBranchQuota(50000); + _ = ctx; + std.debug.assert(cells.len == @as(usize, size.cols) * @as(usize, size.rows)); + + var row: usize = 0; + var col: usize = 0; + + // Color + inline for (std.meta.fields(zterm.Color)) |bg_field| { + if (comptime bg_field.value == 0) continue; // zterm.Color.default == 0 -> skip + + inline for (std.meta.fields(zterm.Color)) |fg_field| { + if (comptime fg_field.value == 0) continue; // zterm.Color.default == 0 -> skip + if (comptime fg_field.value == bg_field.value) continue; + + // witouth any emphasis + for (text) |cp| { + cells[(row * size.cols) + col].style.bg = @enumFromInt(bg_field.value); + cells[(row * size.cols) + col].style.fg = @enumFromInt(fg_field.value); + cells[(row * size.cols) + col].cp = cp; + col += 1; + } + + // emphasis (no combinations) + inline for (std.meta.fields(zterm.Style.Emphasis)) |emp_field| { + if (comptime emp_field.value == 0) continue; // zterm.Style.Emphasis.reset == 0 -> skip + const emphasis: zterm.Style.Emphasis = @enumFromInt(emp_field.value); + + for (text) |cp| { + cells[(row * size.cols) + col].style.bg = @enumFromInt(bg_field.value); + cells[(row * size.cols) + col].style.fg = @enumFromInt(fg_field.value); + cells[(row * size.cols) + col].style.emphasis = &.{emphasis}; + cells[(row * size.cols) + col].cp = cp; + col += 1; + } + } + row += 1; + col = 0; + } + } + } +}; + +pub fn main() !void { + errdefer |err| log.err("Application Error: {any}", .{err}); + + var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init; + defer if (gpa.deinit() == .leak) { + log.err("memory leak", .{}); + }; + const allocator = gpa.allocator(); + + var app: App = .init; + var renderer = zterm.Renderer.Buffered.init(allocator); + defer renderer.deinit(); + + var quit_text: QuitText = .{}; + const element = quit_text.element(); + + var text_styles: TextStyles = .{}; + + var container = try App.Container.init(allocator, .{ + .layout = .{ + .gap = 2, + .padding = .{ .top = 5, .bottom = 3, .left = 3, .right = 3 }, + }, + }, element); + defer container.deinit(); + + var box = try App.Container.init(allocator, .{ + .layout = .{ .direction = .vertical }, + .min_size = .{ + .rows = (std.meta.fields(zterm.Color).len - 1) * (std.meta.fields(zterm.Color).len - 2), + .cols = std.meta.fields(zterm.Style.Emphasis).len * TextStyles.text.len, + }, // ensure enough rows and/or columns to render all text styles -> scrollable otherwise + }, text_styles.element()); + defer box.deinit(); + + var scrollable: App.Scrollable = .init(box); + try container.append(try App.Container.init(allocator, .{}, scrollable.element())); + + try app.start(); + defer app.stop() catch |err| log.err("Failed to stop application: {any}", .{err}); + + while (true) { + const event = app.nextEvent(); + log.debug("received event: {s}", .{@tagName(event)}); + + switch (event) { + .init => continue, + .quit => break, + .resize => |size| try renderer.resize(size), + .key => |key| if (key.eql(.{ .cp = 'c', .mod = .{ .ctrl = true } })) app.quit(), + .err => |err| log.err("Received {s} with message: {s}", .{ @errorName(err.err), err.msg }), + else => {}, + } + + container.handle(event) catch |err| app.postEvent(.{ + .err = .{ + .err = err, + .msg = "Container Event handling failed", + }, + }); + try renderer.render(@TypeOf(container), &container); + try renderer.flush(); + } +} diff --git a/src/cell.zig b/src/cell.zig index f0a4541..470a2e3 100644 --- a/src/cell.zig +++ b/src/cell.zig @@ -3,7 +3,7 @@ const Style = @import("style.zig"); pub const Cell = @This(); -style: Style = .{ .attributes = &.{} }, +style: Style = .{ .emphasis = &.{} }, // TODO: embrace `zg` dependency more due to utf-8 encoding cp: u21 = ' ', @@ -12,7 +12,7 @@ pub fn eql(this: Cell, other: Cell) bool { } pub fn reset(this: *Cell) void { - this.style = .{ .attributes = &.{} }; + this.style = .{ .emphasis = &.{} }; this.cp = ' '; } diff --git a/src/style.zig b/src/style.zig index 7225b57..d14ea5a 100644 --- a/src/style.zig +++ b/src/style.zig @@ -22,7 +22,7 @@ pub const Underline = enum { dashed, }; -pub const Attribute = enum(u8) { +pub const Emphasis = enum(u8) { reset = 0, bold = 1, dim, @@ -38,7 +38,7 @@ fg: Color = .white, bg: Color = .default, ul: Color = .default, ul_style: Underline = .off, -attributes: []const Attribute, +emphasis: []const Emphasis, pub fn eql(this: Style, other: Style) bool { return std.meta.eql(this, other); @@ -60,9 +60,7 @@ pub fn value(this: Style, writer: anytype, cp: u21) !void { try std.fmt.format(writer, ";", .{}); try this.ul.write(writer, .ul); // append styles (aka attributes like bold, italic, strikethrough, etc.) - for (this.attributes) |attribute| { - try std.fmt.format(writer, ";{d}", .{@intFromEnum(attribute)}); - } + for (this.emphasis) |attribute| try std.fmt.format(writer, ";{d}", .{@intFromEnum(attribute)}); try std.fmt.format(writer, "m", .{}); // content try std.fmt.format(writer, "{s}", .{buffer});