diff --git a/examples/container.zig b/examples/container.zig index 1ed9bba..cd96a58 100644 --- a/examples/container.zig +++ b/examples/container.zig @@ -22,8 +22,12 @@ pub fn main() !void { var renderer = zterm.Renderer.Buffered.init(allocator); defer renderer.deinit(); - var container = try App.Container.init(allocator, .{}); - try container.append(try App.Container.init(allocator, .{})); + var container = try App.Container.init(allocator, .{ + .border = .{ .color = .green, .corners = .rounded }, + }); + try container.append(try App.Container.init(allocator, .{ + .border = .{ .color = .light_green, .corners = .squared }, + })); defer container.deinit(); // NOTE: should the min-size here be required? diff --git a/src/color.zig b/src/color.zig index 38d0a04..fc9ae5e 100644 --- a/src/color.zig +++ b/src/color.zig @@ -1,126 +1,38 @@ const std = @import("std"); -pub const Color = union(enum) { - ansi: enum(u32) { - reset = 0, - black = 30, - red, - green, - yellow, - blue, - magenta, - cyan, - white, - bright_black = 90, - bright_red, - bright_green, - bright_yellow, - bright_blue, - bright_magenta, - bright_cyan, - bright_white, - }, - rgb: [3]u8, +pub const Color = enum(u8) { + default = 0, + black = 16, + light_red = 1, + light_green, + light_yellow, + light_blue, + light_magenta, + light_cyan, + light_grey, + grey, + red, + green, + yellow, + blue, + magenta, + cyan, + white, + // TODO: add further colors as described in https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b # Color / Graphics Mode - 256 Colors - pub fn eql(a: Color, b: Color) bool { - switch (a) { - .ansi => |a_idx| { - switch (b) { - .ansi => |b_idx| return a_idx == b_idx, - else => return false, - } - }, - .rgb => |a_rgb| { - switch (b) { - .rgb => |b_rgb| return a_rgb[0] == b_rgb[0] and - a_rgb[1] == b_rgb[1] and - a_rgb[2] == b_rgb[2], - else => return false, - } - }, - } - } - - pub fn write(this: Color, writer: anytype, comptime coloring: enum { fg, bg, ul }) !void { - switch (this) { - .ansi => |index| blk: { - if (index == .reset) - break :blk switch (coloring) { - .fg => try std.fmt.format(writer, "39", .{}), - .bg => try std.fmt.format(writer, "49", .{}), - .ul => try std.fmt.format(writer, "59", .{}), - }; - - break :blk switch (coloring) { - .fg => try std.fmt.format(writer, "38;5;{d}", .{@intFromEnum(index)}), - .bg => try std.fmt.format(writer, "48;5;{d}", .{@intFromEnum(index)}), - .ul => try std.fmt.format(writer, "58;5;{d}", .{@intFromEnum(index)}), - }; - }, - .rgb => |rgb| switch (coloring) { - .fg => try std.fmt.format(writer, "38;2;{d};{d};{d}", .{ rgb[0], rgb[1], rgb[2] }), - .bg => try std.fmt.format(writer, "48;2;{d};{d};{d}", .{ rgb[0], rgb[1], rgb[2] }), - .ul => try std.fmt.format(writer, "58;2;{d};{d};{d}", .{ rgb[0], rgb[1], rgb[2] }), - }, - } - } - - pub fn rgbFromUint(val: u24) Color { - const r_bits = val & 0b11111111_00000000_00000000; - const g_bits = val & 0b00000000_11111111_00000000; - const b_bits = val & 0b00000000_00000000_11111111; - const rgb = [_]u8{ - @truncate(r_bits >> 16), - @truncate(g_bits >> 8), - @truncate(b_bits), - }; - return .{ .rgb = rgb }; - } - - /// parse an XParseColor-style rgb specification into an rgb Color. The spec - /// is of the form: rgb:rrrr/gggg/bbbb. Generally, the high two bits will always - /// be the same as the low two bits. - pub fn rgbFromSpec(spec: []const u8) !Color { - var iter = std.mem.splitScalar(u8, spec, ':'); - const prefix = iter.next() orelse return error.InvalidColorSpec; - if (!std.mem.eql(u8, "rgb", prefix)) return error.InvalidColorSpec; - - const spec_str = iter.next() orelse return error.InvalidColorSpec; - - var spec_iter = std.mem.splitScalar(u8, spec_str, '/'); - - const r_raw = spec_iter.next() orelse return error.InvalidColorSpec; - if (r_raw.len != 4) return error.InvalidColorSpec; - - const g_raw = spec_iter.next() orelse return error.InvalidColorSpec; - if (g_raw.len != 4) return error.InvalidColorSpec; - - const b_raw = spec_iter.next() orelse return error.InvalidColorSpec; - if (b_raw.len != 4) return error.InvalidColorSpec; - - const r = try std.fmt.parseUnsigned(u8, r_raw[2..], 16); - const g = try std.fmt.parseUnsigned(u8, g_raw[2..], 16); - const b = try std.fmt.parseUnsigned(u8, b_raw[2..], 16); - - return .{ - .rgb = [_]u8{ r, g, b }, - }; - } - - test "rgbFromSpec" { - const spec = "rgb:aaaa/bbbb/cccc"; - const actual = try rgbFromSpec(spec); - switch (actual) { - .rgb => |rgb| { - try std.testing.expectEqual(0xAA, rgb[0]); - try std.testing.expectEqual(0xBB, rgb[1]); - try std.testing.expectEqual(0xCC, rgb[2]); - }, - else => try std.testing.expect(false), + pub inline fn write(this: Color, writer: anytype, comptime coloring: enum { fg, bg, ul }) !void { + if (this == .default) { + switch (coloring) { + .fg => try std.fmt.format(writer, "39", .{}), + .bg => try std.fmt.format(writer, "49", .{}), + .ul => try std.fmt.format(writer, "59", .{}), + } + } else { + switch (coloring) { + .fg => try std.fmt.format(writer, "38;5;{d}", .{@intFromEnum(this)}), + .bg => try std.fmt.format(writer, "48;5;{d}", .{@intFromEnum(this)}), + .ul => try std.fmt.format(writer, "58;5;{d}", .{@intFromEnum(this)}), + } } } }; - -test { - _ = Color; -} diff --git a/src/container.zig b/src/container.zig index 8e8ff56..73b8ed8 100644 --- a/src/container.zig +++ b/src/container.zig @@ -5,6 +5,7 @@ const isTaggedUnion = @import("event.zig").isTaggedUnion; const Cell = @import("cell.zig"); const Color = @import("color.zig").Color; const Size = @import("size.zig"); +const Style = @import("style.zig"); const log = std.log.scoped(.container); @@ -13,7 +14,7 @@ pub const Border = struct { pub const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' }; pub const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' }; /// Color to use for the border - color: Color = .{ .ansi = .reset }, + color: Color = .white, /// Configure the corner type to be used for the border corners: enum(u1) { squared, @@ -29,7 +30,7 @@ pub const Border = struct { /// Configure separator borders between child element to added to the layout separator: struct { enabled: bool = false, - color: Color = .{ .ansi = .reset }, + color: Color = .white, line: enum { line, dotted, @@ -39,6 +40,7 @@ pub const Border = struct { // NOTE: caller owns `cells` slice and ensures that `cells.len == size.cols * size.rows` pub fn contents(this: @This(), cells: []Cell, size: Size) void { + log.debug("Content of Border: {any}", .{this}); std.debug.assert(cells.len == size.cols * size.rows); const frame = switch (this.corners) { @@ -68,14 +70,16 @@ pub const Border = struct { cells[last_row + col].cp = frame[1]; } // TODO: fix rendering of styling? - cells[col].style = .{ .fg = .{ .ansi = .red }, .attributes = &.{} }; - cells[last_row + col].style = .{ .fg = .{ .ansi = .red }, .attributes = &.{} }; + cells[col].style.fg = this.color; + cells[last_row + col].style.fg = this.color; } // render left and right border for (1..size.rows - 1) |row| { const idx = (row * size.cols); cells[idx].cp = frame[3]; // left + cells[idx].style.fg = this.color; cells[idx + size.cols - 1].cp = frame[3]; // right + cells[idx + size.cols - 1].style.fg = this.color; } } }; @@ -85,7 +89,7 @@ pub const Rectangle = struct { /// `Color` to use to fill the `Rectangle` with /// NOTE: used as background color when rendering! such that it renders the /// children accordingly without removing the coloring of the `Rectangle` - fill: Color = .{ .ansi = .reset }, + fill: Color = .black, /// Configure the corners of the `Rectangle` corners: enum(u1) { squared, diff --git a/src/style.zig b/src/style.zig index 6a77544..8bdfcf2 100644 --- a/src/style.zig +++ b/src/style.zig @@ -22,7 +22,7 @@ pub const Underline = enum { dashed, }; -pub const Attribute = enum(u32) { +pub const Attribute = enum(u8) { reset = 0, bold = 1, dim, @@ -34,52 +34,42 @@ pub const Attribute = enum(u32) { strikethrough, }; -fg: Color = .{ .ansi = .reset }, -bg: Color = .{ .ansi = .reset }, -ul: Color = .{ .ansi = .reset }, +fg: Color = .white, +bg: Color = .default, +ul: Color = .default, ul_style: Underline = .off, attributes: []const Attribute, pub fn eql(this: Style, other: Style) bool { - const ret = this.fg.eql(other.fg) and - this.bg.eql(other.bg) and - this.ul.eql(other.ul) and - other.ul_style == this.ul_style and - other.attributes.len == this.attributes.len; - if (ret == false) return false; - - // are the attributes also identical? - for (0..this.attributes.len) |i| { - if (this.attributes[i] != other.attributes[i]) - return false; - } - return true; + return std.meta.eql(this, other); } pub fn value(this: Style, writer: anytype, cp: u21) !void { var buffer: [4]u8 = undefined; const bytes = try std.unicode.utf8Encode(cp, &buffer); std.debug.assert(bytes > 0); - // start escape sequence + // build ansi sequence for 256 colors ... + // foreground try std.fmt.format(writer, "\x1b[", .{}); - // colors try this.fg.write(writer, .fg); + // background try std.fmt.format(writer, ";", .{}); try this.bg.write(writer, .bg); - // try std.fmt.format(writer, ":", .{}); // underline - // try this.ul.write(writer, .ul); - // try std.fmt.format(writer, ":", .{}); - // attributes + // FIX: assert that if the underline property is set that the ul style and the attribute for underlining is available + 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)}); + try std.fmt.format(writer, ";{d}", .{@intFromEnum(attribute)}); } - // end styling try std.fmt.format(writer, "m", .{}); // content try std.fmt.format(writer, "{s}", .{buffer}); - // end escape sequence - try writer.print("\x1b[0m", .{}); + // TODO: is it necessary to reset the graphics mode afterwards to ensure + // that everything rendered behind is still as expected (without any + // side-effects)? + // try writer.print("\x1b[0m", .{}); } // TODO: implement helper functions for terminal capabilities: