diff --git a/examples/container.zig b/examples/container.zig index 825c1d3..1ed9bba 100644 --- a/examples/container.zig +++ b/examples/container.zig @@ -23,6 +23,7 @@ pub fn main() !void { defer renderer.deinit(); var container = try App.Container.init(allocator, .{}); + try container.append(try App.Container.init(allocator, .{})); defer container.deinit(); // NOTE: should the min-size here be required? diff --git a/src/cell.zig b/src/cell.zig index 0bd6e30..4aecd83 100644 --- a/src/cell.zig +++ b/src/cell.zig @@ -3,18 +3,18 @@ const Style = @import("style.zig"); pub const Cell = @This(); -style: Style = .{}, -rune: u8 = ' ', +style: Style = .{ .attributes = &.{} }, +cp: u21 = ' ', pub fn eql(this: Cell, other: Cell) bool { - return this.rune == other.rune and this.style.eql(other.style); + return this.cp == other.cp and this.style.eql(other.style); } pub fn reset(this: *Cell) void { - this.style = .{ .fg = .default, .bg = .default, .ul = .default, .ul_style = .off }; - this.rune = ' '; + this.style = .{ .attributes = &.{} }; + this.cp = ' '; } pub fn value(this: Cell, writer: anytype) !void { - try this.style.value(writer, this.rune); + try this.style.value(writer, this.cp); } diff --git a/src/color.zig b/src/color.zig index 07f2f37..38d0a04 100644 --- a/src/color.zig +++ b/src/color.zig @@ -1,16 +1,32 @@ const std = @import("std"); pub const Color = union(enum) { - default, - index: u8, + 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 fn eql(a: Color, b: Color) bool { switch (a) { - .default => return b == .default, - .index => |a_idx| { + .ansi => |a_idx| { switch (b) { - .index => |b_idx| return a_idx == b_idx, + .ansi => |b_idx| return a_idx == b_idx, else => return false, } }, @@ -25,6 +41,30 @@ pub const Color = union(enum) { } } + 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; diff --git a/src/container.zig b/src/container.zig index 8413bbd..8e8ff56 100644 --- a/src/container.zig +++ b/src/container.zig @@ -10,8 +10,10 @@ const log = std.log.scoped(.container); /// Border configuration struct pub const Border = struct { + pub const rounded_border: [6]u21 = .{ '╭', '─', '╮', '│', '╰', '╯' }; + pub const squared_border: [6]u21 = .{ '┌', '─', '┐', '│', '└', '┘' }; /// Color to use for the border - color: Color = .default, + color: Color = .{ .ansi = .reset }, /// Configure the corner type to be used for the border corners: enum(u1) { squared, @@ -27,13 +29,55 @@ pub const Border = struct { /// Configure separator borders between child element to added to the layout separator: struct { enabled: bool = false, - color: Color = .default, + color: Color = .{ .ansi = .reset }, line: enum { line, dotted, // TODO: add more variations which could be used for the separator } = .line, } = .{}, + + // NOTE: caller owns `cells` slice and ensures that `cells.len == size.cols * size.rows` + pub fn contents(this: @This(), cells: []Cell, size: Size) void { + std.debug.assert(cells.len == size.cols * size.rows); + + const frame = switch (this.corners) { + .rounded => Border.rounded_border, + .squared => Border.squared_border, + }; + std.debug.assert(frame.len == 6); + // TODO: respect color configuration + // TODO: respect sides configuration + // render top and bottom border + for (0..size.cols) |col| { + const last_row = (size.rows - 1) * size.cols; + if (col == 0) { + // top left corner + cells[col].cp = frame[0]; + // bottom left corner + cells[last_row + col].cp = frame[4]; + } else if (col == size.cols - 1) { + // top right corner + cells[col].cp = frame[2]; + // bottom left corner + cells[last_row + col].cp = frame[5]; + } else { + // top side + cells[col].cp = frame[1]; + // bottom side + 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 = &.{} }; + } + // render left and right border + for (1..size.rows - 1) |row| { + const idx = (row * size.cols); + cells[idx].cp = frame[3]; // left + cells[idx + size.cols - 1].cp = frame[3]; // right + } + } }; /// Rectangle configuration struct @@ -41,7 +85,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 = .default, + fill: Color = .{ .ansi = .reset }, /// Configure the corners of the `Rectangle` corners: enum(u1) { squared, @@ -105,6 +149,7 @@ pub fn Container(comptime Event: type) type { @compileError("Provided user event `Event` for `Container(comptime Event: type)`"); } return struct { + allocator: std.mem.Allocator, size: Size, properties: Properties, elements: std.ArrayList(@This()), @@ -121,6 +166,7 @@ pub fn Container(comptime Event: type) type { pub fn init(allocator: std.mem.Allocator, properties: Properties) !@This() { return .{ + .allocator = allocator, .size = .{ .cols = 0, .rows = 0 }, .properties = properties, .elements = std.ArrayList(@This()).init(allocator), @@ -197,7 +243,25 @@ pub fn Container(comptime Event: type) type { offset += rows; }, } - // TODO; adjust size according to the layout of the `Container` + + // border resizing + const sides = this.properties.border.sides; + if (sides.top) { + element_size.anchor.row += 1; + element_size.rows -|= 1; + } + if (sides.bottom) { + element_size.rows -|= 1; + } + if (sides.left) { + element_size.anchor.col += 1; + element_size.cols -|= 1; + } + if (sides.right) { + element_size.cols -|= 1; + } + + // TODO: adjust size according to the layout of the `Container` if (element.handle(.{ .resize = element_size })) |e| { _ = e; } @@ -219,8 +283,10 @@ pub fn Container(comptime Event: type) type { pub fn contents(this: *const @This()) []const Cell { // TODO: use the size and the corresponding contents to determine what should be show in form of a `Cell` array - _ = this; - return &[0]Cell{}; + const cells = this.allocator.alloc(Cell, this.size.cols * this.size.rows) catch @panic("Container::contents: Out of memory."); + @memset(cells, .{}); // reset all cells + this.properties.border.contents(cells, this.size); + return cells; } }; } diff --git a/src/render.zig b/src/render.zig index eda554d..1f3b886 100644 --- a/src/render.zig +++ b/src/render.zig @@ -63,6 +63,7 @@ pub const Buffered = struct { log.debug("renderer::clear", .{}); var vs = this.virtual_screen; const anchor = (size.anchor.row * this.size.rows) + size.anchor.col; + // TODO: use memset to effectivly reset the coresponding cells? for (0..size.rows) |row| { for (0..size.cols) |col| { vs[anchor + (row * this.size.cols) + col].reset(); @@ -72,31 +73,38 @@ pub const Buffered = struct { /// Render provided cells at size (anchor and dimension) into the *virtual screen*. pub fn render(this: *@This(), comptime T: type, container: *T) void { - const cells: []const Cell = container.contents(); const size: Size = container.size; - log.debug("renderer:render: cells: {any} @ {any}", .{ cells, size }); + const cells: []const Cell = container.contents(); + + // log.debug("renderer:render: cells: {any} @ {any}", .{ cells, size }); if (cells.len == 0) return; var idx: usize = 0; var vs = this.virtual_screen; - const anchor = (size.anchor.row * this.size.rows) + size.anchor.col; + const anchor = (size.anchor.row * this.size.cols) + size.anchor.col; - for (0..size.rows) |row| { + blk: for (0..size.rows) |row| { for (0..size.cols) |col| { const cell = cells[idx]; idx += 1; vs[anchor + (row * this.size.cols) + col].style = cell.style; - vs[anchor + (row * this.size.cols) + col].rune = cell.rune; + vs[anchor + (row * this.size.cols) + col].cp = cell.cp; - if (cells.len == idx) return; + if (cells.len == idx) break :blk; } } + container.allocator.free(cells); + + for (container.elements.items) |*element| { + this.render(T, element); + } } /// Write *virtual screen* to alternate screen (should be called once and last during each render loop iteration in the main loop). pub fn flush(this: *@This()) !void { + // TODO: measure timings of rendered frames? log.debug("renderer::flush", .{}); const writer = terminal.writer(); const s = this.screen; @@ -110,7 +118,7 @@ pub const Buffered = struct { continue; // render differences found in virtual screen // TODO: improve the writing speed (many unnecessary writes (i.e. the style for every character..)) - try terminal.setCursorPosition(.{ .row = @truncate(row), .col = @truncate(col) }); + try terminal.setCursorPosition(.{ .row = @truncate(row + 1), .col = @truncate(col + 1) }); try cvs.value(writer); // update screen to be the virtual screen for the next frame s[idx] = vs[idx]; diff --git a/src/style.zig b/src/style.zig index bc195ba..6a77544 100644 --- a/src/style.zig +++ b/src/style.zig @@ -8,7 +8,6 @@ // taken from https://github.com/rockorager/libvaxis/blob/main/src/Cell.zig (MIT-License) // with slight modifications const std = @import("std"); -const ctlseqs = @import("ctlseqs.zig"); const Color = @import("color.zig").Color; @@ -23,208 +22,64 @@ pub const Underline = enum { dashed, }; -fg: Color = .default, -bg: Color = .default, -ul: Color = .default, -ul_style: Underline = .off, +pub const Attribute = enum(u32) { + reset = 0, + bold = 1, + dim, + italic, + underline, + blink, + invert = 7, + hidden, + strikethrough, +}; -bold: bool = false, -dim: bool = false, -italic: bool = false, -blink: bool = false, -reverse: bool = false, -invisible: bool = false, -strikethrough: bool = false, +fg: Color = .{ .ansi = .reset }, +bg: Color = .{ .ansi = .reset }, +ul: Color = .{ .ansi = .reset }, +ul_style: Underline = .off, +attributes: []const Attribute, pub fn eql(this: Style, other: Style) bool { - return this.fg.eql(other.fg) and + 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.bold == this.bold and - other.dim == this.dim and - other.italic == this.italic and - other.blink == this.blink and - other.reverse == this.reverse and - other.invisible == this.invisible and - other.strikethrough == this.strikethrough; + 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; } -/// Merge _other_ `Style` to _this_ style and overwrite _this_ `Style`'s value -/// if the _other_ value differs from the default value. -pub fn merge(this: *Style, other: Style) void { - if (other.fg != .default) this.fg = other.fg; - if (other.bg != .default) this.bg = other.bg; - if (other.ul != .default) this.ul = other.ul; - if (other.ul_style != .off) this.ul_style = other.ul_style; - if (other.bold != false) this.bold = other.bold; - if (other.dim != false) this.dim = other.dim; - if (other.italic != false) this.italic = other.italic; - if (other.blink != false) this.blink = other.blink; - if (other.reverse != false) this.reverse = other.reverse; - if (other.invisible != false) this.invisible = other.invisible; - if (other.strikethrough != false) this.strikethrough = other.strikethrough; -} - -fn start(this: Style, writer: anytype) !void { - // foreground - switch (this.fg) { - .default => try std.fmt.format(writer, ctlseqs.fg_reset, .{}), - .index => |idx| { - switch (idx) { - 0...7 => { - try std.fmt.format(writer, ctlseqs.fg_base, .{idx}); - }, - 8...15 => { - try std.fmt.format(writer, ctlseqs.fg_bright, .{idx - 8}); - }, - else => { - try std.fmt.format(writer, ctlseqs.fg_indexed, .{idx}); - }, - } - }, - .rgb => |rgb| { - try std.fmt.format(writer, ctlseqs.fg_rgb, .{ rgb[0], rgb[1], rgb[2] }); - }, +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 + try std.fmt.format(writer, "\x1b[", .{}); + // colors + try this.fg.write(writer, .fg); + 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 + for (this.attributes) |attribute| { + try std.fmt.format(writer, ":{d}", .{@intFromEnum(attribute)}); } - // background - switch (this.bg) { - .default => try std.fmt.format(writer, ctlseqs.bg_reset, .{}), - .index => |idx| { - switch (idx) { - 0...7 => { - try std.fmt.format(writer, ctlseqs.bg_base, .{idx}); - }, - 8...15 => { - try std.fmt.format(writer, ctlseqs.bg_bright, .{idx}); - }, - else => { - try std.fmt.format(writer, ctlseqs.bg_indexed, .{idx}); - }, - } - }, - .rgb => |rgb| { - try std.fmt.format(writer, ctlseqs.bg_rgb, .{ rgb[0], rgb[1], rgb[2] }); - }, - } - // underline color - switch (this.ul) { - .default => try std.fmt.format(writer, ctlseqs.ul_reset, .{}), - .index => |idx| { - try std.fmt.format(writer, ctlseqs.ul_indexed, .{idx}); - }, - .rgb => |rgb| { - try std.fmt.format(writer, ctlseqs.ul_rgb, .{ rgb[0], rgb[1], rgb[2] }); - }, - } - // underline style - switch (this.ul_style) { - .off => try std.fmt.format(writer, ctlseqs.ul_off, .{}), - .single => try std.fmt.format(writer, ctlseqs.ul_single, .{}), - .double => try std.fmt.format(writer, ctlseqs.ul_double, .{}), - .curly => try std.fmt.format(writer, ctlseqs.ul_curly, .{}), - .dotted => try std.fmt.format(writer, ctlseqs.ul_dotted, .{}), - .dashed => try std.fmt.format(writer, ctlseqs.ul_dashed, .{}), - } - // bold - switch (this.bold) { - true => try std.fmt.format(writer, ctlseqs.bold_set, .{}), - false => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), - } - // dim - switch (this.dim) { - true => try std.fmt.format(writer, ctlseqs.dim_set, .{}), - false => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), - } - // italic - switch (this.italic) { - true => try std.fmt.format(writer, ctlseqs.italic_set, .{}), - false => try std.fmt.format(writer, ctlseqs.italic_reset, .{}), - } - // blink - switch (this.blink) { - true => try std.fmt.format(writer, ctlseqs.blink_set, .{}), - false => try std.fmt.format(writer, ctlseqs.blink_reset, .{}), - } - // reverse - switch (this.reverse) { - true => try std.fmt.format(writer, ctlseqs.reverse_set, .{}), - false => try std.fmt.format(writer, ctlseqs.reverse_reset, .{}), - } - // invisible - switch (this.invisible) { - true => try std.fmt.format(writer, ctlseqs.invisible_set, .{}), - false => try std.fmt.format(writer, ctlseqs.invisible_reset, .{}), - } - // strikethrough - switch (this.strikethrough) { - true => try std.fmt.format(writer, ctlseqs.strikethrough_set, .{}), - false => try std.fmt.format(writer, ctlseqs.strikethrough_reset, .{}), - } -} - -fn end(this: Style, writer: anytype) !void { - // foreground - switch (this.fg) { - .default => {}, - else => try std.fmt.format(writer, ctlseqs.fg_reset, .{}), - } - // background - switch (this.bg) { - .default => {}, - else => try std.fmt.format(writer, ctlseqs.bg_reset, .{}), - } - // underline color - switch (this.ul) { - .default => {}, - else => try std.fmt.format(writer, ctlseqs.ul_reset, .{}), - } - // underline style - switch (this.ul_style) { - .off => {}, - else => try std.fmt.format(writer, ctlseqs.ul_off, .{}), - } - // bold - switch (this.bold) { - true => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), - false => {}, - } - // dim - switch (this.dim) { - true => try std.fmt.format(writer, ctlseqs.bold_dim_reset, .{}), - false => {}, - } - // italic - switch (this.italic) { - true => try std.fmt.format(writer, ctlseqs.italic_reset, .{}), - false => {}, - } - // blink - switch (this.blink) { - true => try std.fmt.format(writer, ctlseqs.blink_reset, .{}), - false => {}, - } - // reverse - switch (this.reverse) { - true => try std.fmt.format(writer, ctlseqs.reverse_reset, .{}), - false => {}, - } - // invisible - switch (this.invisible) { - true => try std.fmt.format(writer, ctlseqs.invisible_reset, .{}), - false => {}, - } - // strikethrough - switch (this.strikethrough) { - true => try std.fmt.format(writer, ctlseqs.strikethrough_reset, .{}), - false => {}, - } -} - -pub fn value(this: Style, writer: anytype, content: u8) !void { - try this.start(writer); - _ = try writer.write(&[_]u8{content}); - try this.end(writer); + // 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: implement helper functions for terminal capabilities: diff --git a/src/terminal.zig b/src/terminal.zig index 1c74ff1..4bebad3 100644 --- a/src/terminal.zig +++ b/src/terminal.zig @@ -20,7 +20,7 @@ pub const ReportMode = enum { pub fn getTerminalSize() Size { var ws: std.posix.winsize = undefined; _ = std.posix.system.ioctl(std.posix.STDIN_FILENO, std.posix.T.IOCGWINSZ, @intFromPtr(&ws)); - return .{ .cols = ws.col - 1, .rows = ws.row - 1 }; + return .{ .cols = ws.col, .rows = ws.row }; } pub fn saveScreen() !void {