diff --git a/examples/showcase.zig b/examples/showcase.zig index 4fc76f6..ac38001 100644 --- a/examples/showcase.zig +++ b/examples/showcase.zig @@ -231,8 +231,7 @@ const Model = struct { \\const std = @import("std"); \\ \\pub fn main() !void { - \\ const stdout = std.Io.getStdOut().writer(); - \\ try stdout.print("Hello, {s}!\n", .{"world"}); + \\ std.debug.print("Hello, {s}!\n", .{"world"}); \\} \\ \\// Edit this code with the text area component. diff --git a/src/input/mouse.zig b/src/input/mouse.zig index 7b914d1..795c66f 100644 --- a/src/input/mouse.zig +++ b/src/input/mouse.zig @@ -37,15 +37,7 @@ pub const MouseEvent = struct { event_type: EventType, modifiers: keys.Modifiers = .{}, - pub fn format( - self: MouseEvent, - comptime fmt: []const u8, - options: std.fmt.FormatOptions, - writer: *Writer, - ) !void { - _ = fmt; - _ = options; - + pub fn format(self: MouseEvent, writer: *Writer) Writer.Error!void { try writer.print("{s} {s} at ({d}, {d})", .{ @tagName(self.event_type), @tagName(self.button), diff --git a/src/layout/layer.zig b/src/layout/layer.zig index 0232c2b..6ef1e6c 100644 --- a/src/layout/layer.zig +++ b/src/layout/layer.zig @@ -63,8 +63,9 @@ pub const LayerStack = struct { const grid = allocator.alloc(Cell, w * h) catch return ""; // Fill with background + const bg = [1]u8{self.background}; for (grid) |*cell| { - cell.* = .{ .char = self.background, .ansi_prefix = "" }; + cell.* = .{ .content = &bg, .ansi_prefix = "" }; } // Sort layers by z-index @@ -89,12 +90,14 @@ pub const LayerStack = struct { if (row > 0) writer.writeByte('\n') catch {}; for (0..w) |col| { const cell = grid[row * w + col]; + // Empty content marks the second column of a wide character + if (cell.content.len == 0) continue; if (cell.ansi_prefix.len > 0) { writer.writeAll(cell.ansi_prefix) catch {}; - writer.writeByte(cell.char) catch {}; + writer.writeAll(cell.content) catch {}; writer.writeAll("\x1b[0m") catch {}; } else { - writer.writeByte(cell.char) catch {}; + writer.writeAll(cell.content) catch {}; } } } @@ -135,22 +138,36 @@ pub const LayerStack = struct { continue; } - if (col < w) { - const is_transparent = layer.transparent and content[i] == ' ' and current_ansi.len == 0; - if (!is_transparent) { - grid[row * w + col] = .{ - .char = content[i], - .ansi_prefix = current_ansi, - }; + // Decode one UTF-8 character; treat invalid bytes as single cells + const char_len = std.unicode.utf8ByteSequenceLength(content[i]) catch 1; + const end = @min(i + char_len, content.len); + const char = content[i..end]; + const codepoint: u21 = std.unicode.utf8Decode(char) catch content[i]; + const char_width = measure.charWidth(codepoint); + i = end; + + // Zero-width characters (e.g. combining marks) get no cell + if (char_width == 0) continue; + + const is_transparent = layer.transparent and codepoint == ' ' and current_ansi.len == 0; + if (!is_transparent and col + char_width <= w) { + grid[row * w + col] = .{ + .content = char, + .ansi_prefix = current_ansi, + }; + // A wide character covers the following cell as well + if (char_width == 2) { + grid[row * w + col + 1] = .{ .content = "" }; } - col += 1; } - i += 1; + col += char_width; } } }; const Cell = struct { - char: u8 = ' ', + /// UTF-8 bytes of the character in this cell. + /// Empty for the second column of a wide character. + content: []const u8 = " ", ansi_prefix: []const u8 = "", }; diff --git a/tests/layout_tests.zig b/tests/layout_tests.zig index fc25f4a..39c91a9 100644 --- a/tests/layout_tests.zig +++ b/tests/layout_tests.zig @@ -133,3 +133,63 @@ test "joinVertical convenience" { defer allocator.free(result); try testing.expectEqualStrings("A\nB", result); } + +test "layer.LayerStack - multibyte UTF-8 chars occupy one cell" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var stack = zz.layout.layer.LayerStack.init(allocator); + defer stack.deinit(); + stack.setSize(5, 1); + + try stack.push(.{ .content = "╭─╮", .transparent = false }); + + try testing.expectEqualStrings("╭─╮ ", stack.render(allocator)); +} + +test "layer.LayerStack - styled multibyte border chars stay intact" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var stack = zz.layout.layer.LayerStack.init(allocator); + defer stack.deinit(); + stack.setSize(4, 1); + + try stack.push(.{ .content = "\x1b[36m─│\x1b[0m", .transparent = false }); + + try testing.expectEqualStrings( + "\x1b[36m─\x1b[0m\x1b[36m│\x1b[0m ", + stack.render(allocator), + ); +} + +test "layer.LayerStack - overlay aligns on UTF-8 background" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var stack = zz.layout.layer.LayerStack.init(allocator); + defer stack.deinit(); + stack.setSize(6, 1); + + try stack.push(.{ .content = "──────", .z = 0, .transparent = false }); + try stack.push(.{ .content = "AB", .x = 2, .z = 1 }); + + try testing.expectEqualStrings("──AB──", stack.render(allocator)); +} + +test "layer.LayerStack - wide characters cover two cells" { + var arena = std.heap.ArenaAllocator.init(testing.allocator); + defer arena.deinit(); + const allocator = arena.allocator(); + + var stack = zz.layout.layer.LayerStack.init(allocator); + defer stack.deinit(); + stack.setSize(4, 1); + + try stack.push(.{ .content = "你a", .transparent = false }); + + try testing.expectEqualStrings("你a ", stack.render(allocator)); +}