intermediate #1

Merged
yves-biener merged 31 commits from intermediate into main 2025-02-16 16:02:59 +01:00
4 changed files with 64 additions and 154 deletions
Showing only changes of commit 9c06ced658 - Show all commits

View File

@@ -22,8 +22,12 @@ pub fn main() !void {
var renderer = zterm.Renderer.Buffered.init(allocator); var renderer = zterm.Renderer.Buffered.init(allocator);
defer renderer.deinit(); defer renderer.deinit();
var container = try App.Container.init(allocator, .{}); var container = try App.Container.init(allocator, .{
try container.append(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(); defer container.deinit();
// NOTE: should the min-size here be required? // NOTE: should the min-size here be required?

View File

@@ -1,126 +1,38 @@
const std = @import("std"); const std = @import("std");
pub const Color = union(enum) { pub const Color = enum(u8) {
ansi: enum(u32) { default = 0,
reset = 0, black = 16,
black = 30, light_red = 1,
red, light_green,
green, light_yellow,
yellow, light_blue,
blue, light_magenta,
magenta, light_cyan,
cyan, light_grey,
white, grey,
bright_black = 90, red,
bright_red, green,
bright_green, yellow,
bright_yellow, blue,
bright_blue, magenta,
bright_magenta, cyan,
bright_cyan, white,
bright_white, // TODO: add further colors as described in https://gist.github.com/ConnerWill/d4b6c776b509add763e17f9f113fd25b # Color / Graphics Mode - 256 Colors
},
rgb: [3]u8,
pub fn eql(a: Color, b: Color) bool { pub inline fn write(this: Color, writer: anytype, comptime coloring: enum { fg, bg, ul }) !void {
switch (a) { if (this == .default) {
.ansi => |a_idx| { switch (coloring) {
switch (b) { .fg => try std.fmt.format(writer, "39", .{}),
.ansi => |b_idx| return a_idx == b_idx, .bg => try std.fmt.format(writer, "49", .{}),
else => return false, .ul => try std.fmt.format(writer, "59", .{}),
} }
}, } else {
.rgb => |a_rgb| { switch (coloring) {
switch (b) { .fg => try std.fmt.format(writer, "38;5;{d}", .{@intFromEnum(this)}),
.rgb => |b_rgb| return a_rgb[0] == b_rgb[0] and .bg => try std.fmt.format(writer, "48;5;{d}", .{@intFromEnum(this)}),
a_rgb[1] == b_rgb[1] and .ul => try std.fmt.format(writer, "58;5;{d}", .{@intFromEnum(this)}),
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),
} }
} }
}; };
test {
_ = Color;
}

View File

@@ -5,6 +5,7 @@ const isTaggedUnion = @import("event.zig").isTaggedUnion;
const Cell = @import("cell.zig"); const Cell = @import("cell.zig");
const Color = @import("color.zig").Color; const Color = @import("color.zig").Color;
const Size = @import("size.zig"); const Size = @import("size.zig");
const Style = @import("style.zig");
const log = std.log.scoped(.container); const log = std.log.scoped(.container);
@@ -13,7 +14,7 @@ pub const Border = struct {
pub const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' }; pub const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' };
pub const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' }; pub const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' };
/// Color to use for the border /// Color to use for the border
color: Color = .{ .ansi = .reset }, color: Color = .white,
/// Configure the corner type to be used for the border /// Configure the corner type to be used for the border
corners: enum(u1) { corners: enum(u1) {
squared, squared,
@@ -29,7 +30,7 @@ pub const Border = struct {
/// Configure separator borders between child element to added to the layout /// Configure separator borders between child element to added to the layout
separator: struct { separator: struct {
enabled: bool = false, enabled: bool = false,
color: Color = .{ .ansi = .reset }, color: Color = .white,
line: enum { line: enum {
line, line,
dotted, dotted,
@@ -39,6 +40,7 @@ pub const Border = struct {
// NOTE: caller owns `cells` slice and ensures that `cells.len == size.cols * size.rows` // NOTE: caller owns `cells` slice and ensures that `cells.len == size.cols * size.rows`
pub fn contents(this: @This(), cells: []Cell, size: Size) void { 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); std.debug.assert(cells.len == size.cols * size.rows);
const frame = switch (this.corners) { const frame = switch (this.corners) {
@@ -68,14 +70,16 @@ pub const Border = struct {
cells[last_row + col].cp = frame[1]; cells[last_row + col].cp = frame[1];
} }
// TODO: fix rendering of styling? // TODO: fix rendering of styling?
cells[col].style = .{ .fg = .{ .ansi = .red }, .attributes = &.{} }; cells[col].style.fg = this.color;
cells[last_row + col].style = .{ .fg = .{ .ansi = .red }, .attributes = &.{} }; cells[last_row + col].style.fg = this.color;
} }
// render left and right border // render left and right border
for (1..size.rows - 1) |row| { for (1..size.rows - 1) |row| {
const idx = (row * size.cols); const idx = (row * size.cols);
cells[idx].cp = frame[3]; // left 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].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 /// `Color` to use to fill the `Rectangle` with
/// NOTE: used as background color when rendering! such that it renders the /// NOTE: used as background color when rendering! such that it renders the
/// children accordingly without removing the coloring of the `Rectangle` /// children accordingly without removing the coloring of the `Rectangle`
fill: Color = .{ .ansi = .reset }, fill: Color = .black,
/// Configure the corners of the `Rectangle` /// Configure the corners of the `Rectangle`
corners: enum(u1) { corners: enum(u1) {
squared, squared,

View File

@@ -22,7 +22,7 @@ pub const Underline = enum {
dashed, dashed,
}; };
pub const Attribute = enum(u32) { pub const Attribute = enum(u8) {
reset = 0, reset = 0,
bold = 1, bold = 1,
dim, dim,
@@ -34,52 +34,42 @@ pub const Attribute = enum(u32) {
strikethrough, strikethrough,
}; };
fg: Color = .{ .ansi = .reset }, fg: Color = .white,
bg: Color = .{ .ansi = .reset }, bg: Color = .default,
ul: Color = .{ .ansi = .reset }, ul: Color = .default,
ul_style: Underline = .off, ul_style: Underline = .off,
attributes: []const Attribute, attributes: []const Attribute,
pub fn eql(this: Style, other: Style) bool { pub fn eql(this: Style, other: Style) bool {
const ret = this.fg.eql(other.fg) and return std.meta.eql(this, other);
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;
} }
pub fn value(this: Style, writer: anytype, cp: u21) !void { pub fn value(this: Style, writer: anytype, cp: u21) !void {
var buffer: [4]u8 = undefined; var buffer: [4]u8 = undefined;
const bytes = try std.unicode.utf8Encode(cp, &buffer); const bytes = try std.unicode.utf8Encode(cp, &buffer);
std.debug.assert(bytes > 0); std.debug.assert(bytes > 0);
// start escape sequence // build ansi sequence for 256 colors ...
// foreground
try std.fmt.format(writer, "\x1b[", .{}); try std.fmt.format(writer, "\x1b[", .{});
// colors
try this.fg.write(writer, .fg); try this.fg.write(writer, .fg);
// background
try std.fmt.format(writer, ";", .{}); try std.fmt.format(writer, ";", .{});
try this.bg.write(writer, .bg); try this.bg.write(writer, .bg);
// try std.fmt.format(writer, ":", .{});
// underline // underline
// try this.ul.write(writer, .ul); // 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 std.fmt.format(writer, ";", .{});
// attributes try this.ul.write(writer, .ul);
// append styles (aka attributes like bold, italic, strikethrough, etc.)
for (this.attributes) |attribute| { 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", .{}); try std.fmt.format(writer, "m", .{});
// content // content
try std.fmt.format(writer, "{s}", .{buffer}); try std.fmt.format(writer, "{s}", .{buffer});
// end escape sequence // TODO: is it necessary to reset the graphics mode afterwards to ensure
try writer.print("\x1b[0m", .{}); // that everything rendered behind is still as expected (without any
// side-effects)?
// try writer.print("\x1b[0m", .{});
} }
// TODO: implement helper functions for terminal capabilities: // TODO: implement helper functions for terminal capabilities: