From 3a6258ab90a807df2b8433152afc0e28b7ef2708 Mon Sep 17 00:00:00 2001 From: dacec354 Date: Thu, 19 Feb 2026 15:31:38 +0800 Subject: [PATCH 1/2] fix: missing placeholder padding and empty row padding of TextArea --- src/components/text_area.zig | 14 +++ tests/input_tests.zig | 199 +++++++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+) diff --git a/src/components/text_area.zig b/src/components/text_area.zig index 18e78da..8f1bd84 100644 --- a/src/components/text_area.zig +++ b/src/components/text_area.zig @@ -640,11 +640,25 @@ pub const TextArea = struct { const styled = try self.placeholder_style.render(allocator, self.placeholder); defer allocator.free(styled); try writer.writeAll(styled); + // Pad to full width + const placeholder_width = measure.width(self.placeholder); + const max_width: usize = text_width; + var rendered_width = @min(placeholder_width, max_width); + while (rendered_width < max_width) { + try writer.writeByte(' '); + rendered_width += 1; + } continue; } // Render line content with cursor try self.renderLine(writer, allocator, line.items, line_idx, text_width); + } else { + // Pad empty rows to full width + var i: usize = 0; + while (i < text_width) : (i += 1) { + try writer.writeByte(' '); + } } } diff --git a/tests/input_tests.zig b/tests/input_tests.zig index 618bbaf..af9aa59 100644 --- a/tests/input_tests.zig +++ b/tests/input_tests.zig @@ -258,3 +258,202 @@ test "TextArea word_wrap keeps cursor visible by wrapped row" { defer testing.allocator.free(plain); try testing.expectEqualStrings("fghij\nk ", plain); } + +// ============================================================================ +// Tests for TextArea setSize/view fix - placeholder and empty row padding +// ============================================================================ + +test "TextArea setSize without setValue pads placeholder to full width" { + var area = zz.TextArea.init(testing.allocator); + defer area.deinit(); + + // Initialize with placeholder + area.placeholder = "Type here"; + + // Set size WITHOUT calling setValue (the bug scenario) + area.setSize(50, 3); + + // Render view + const rendered = try area.view(testing.allocator); + defer testing.allocator.free(rendered); + const plain = try stripAnsi(testing.allocator, rendered); + defer testing.allocator.free(plain); + + // Verify first line contains placeholder padded to 50 chars + var lines = std.mem.splitScalar(u8, plain, '\n'); + const first_line = lines.next().?; + try testing.expectEqual(@as(usize, 50), first_line.len); + try testing.expect(std.mem.startsWith(u8, first_line, "Type here")); +} + +test "TextArea pads empty rows to full width" { + var area = zz.TextArea.init(testing.allocator); + defer area.deinit(); + + // Set content to just one line + try area.setValue("Single line"); + area.setSize(30, 5); + + // Render view + const rendered = try area.view(testing.allocator); + defer testing.allocator.free(rendered); + const plain = try stripAnsi(testing.allocator, rendered); + defer testing.allocator.free(plain); + + // Split into lines + var lines = std.mem.splitScalar(u8, plain, '\n'); + + // First line should have content padded to 30 chars + const line0 = lines.next().?; + try testing.expectEqual(@as(usize, 30), line0.len); + try testing.expect(std.mem.startsWith(u8, line0, "Single line")); + + // Remaining lines should be padded to 30 chars of spaces + var line_count: usize = 1; + while (lines.next()) |line| : (line_count += 1) { + if (line_count >= 5) break; + try testing.expectEqual(@as(usize, 30), line.len); + // Verify it's all spaces + for (line) |c| { + try testing.expectEqual(@as(u8, ' '), c); + } + } +} + +test "TextArea renders at correct width after setSize without setValue" { + var area = zz.TextArea.init(testing.allocator); + defer area.deinit(); + + // Initialize with placeholder (common usage pattern) + area.placeholder = "Enter text..."; + area.placeholder_style = blk: { + var s = zz.style.Style{}; + s = s.fg(zz.Color.gray(12)); + s = s.inline_style(true); + break :blk s; + }; + + // Simulate resize to 80x24 terminal + area.setSize(80, 24); + + // Render without calling setValue + const rendered = try area.view(testing.allocator); + defer testing.allocator.free(rendered); + + // Verify content length is reasonable for 80x24 with ANSI codes + // Expected: ~80 * 24 = 1920 chars + newlines + ANSI codes + try testing.expect(rendered.len > 1000); + + // Strip ANSI and verify dimensions + const plain = try stripAnsi(testing.allocator, rendered); + defer testing.allocator.free(plain); + + var lines = std.mem.splitScalar(u8, plain, '\n'); + var line_count: usize = 0; + while (lines.next()) |line| : (line_count += 1) { + if (line_count >= 24) break; + try testing.expectEqual(@as(usize, 80), line.len); + } + try testing.expectEqual(@as(usize, 24), line_count); +} + +test "TextArea multiple resize operations maintain correct width" { + var area = zz.TextArea.init(testing.allocator); + defer area.deinit(); + + area.placeholder = "Test"; + + // First resize to small + area.setSize(20, 5); + const rendered1 = try area.view(testing.allocator); + defer testing.allocator.free(rendered1); + const plain1 = try stripAnsi(testing.allocator, rendered1); + defer testing.allocator.free(plain1); + + var lines1 = std.mem.splitScalar(u8, plain1, '\n'); + const first_line1 = lines1.next().?; + try testing.expectEqual(@as(usize, 20), first_line1.len); + + // Then resize to large + area.setSize(100, 10); + const rendered2 = try area.view(testing.allocator); + defer testing.allocator.free(rendered2); + const plain2 = try stripAnsi(testing.allocator, rendered2); + defer testing.allocator.free(plain2); + + var lines2 = std.mem.splitScalar(u8, plain2, '\n'); + const first_line2 = lines2.next().?; + try testing.expectEqual(@as(usize, 100), first_line2.len); + + // Then resize back to medium + area.setSize(50, 3); + const rendered3 = try area.view(testing.allocator); + defer testing.allocator.free(rendered3); + const plain3 = try stripAnsi(testing.allocator, rendered3); + defer testing.allocator.free(plain3); + + var lines3 = std.mem.splitScalar(u8, plain3, '\n'); + const first_line3 = lines3.next().?; + try testing.expectEqual(@as(usize, 50), first_line3.len); +} + +test "TextArea empty content renders all rows at correct width" { + var area = zz.TextArea.init(testing.allocator); + defer area.deinit(); + + // Don't set any content or placeholder + area.setSize(40, 10); + + const rendered = try area.view(testing.allocator); + defer testing.allocator.free(rendered); + const plain = try stripAnsi(testing.allocator, rendered); + defer testing.allocator.free(plain); + + // All 10 rows should be 40 spaces + var lines = std.mem.splitScalar(u8, plain, '\n'); + var line_count: usize = 0; + while (lines.next()) |line| : (line_count += 1) { + if (line_count >= 10) break; + try testing.expectEqual(@as(usize, 40), line.len); + // Verify all spaces + for (line) |c| { + try testing.expectEqual(@as(u8, ' '), c); + } + } + try testing.expectEqual(@as(usize, 10), line_count); +} + +test "TextArea partial content fills remaining rows" { + var area = zz.TextArea.init(testing.allocator); + defer area.deinit(); + + // Set 3 lines of content + try area.setValue("Line 1\nLine 2\nLine 3"); + area.setSize(25, 10); + + const rendered = try area.view(testing.allocator); + defer testing.allocator.free(rendered); + const plain = try stripAnsi(testing.allocator, rendered); + defer testing.allocator.free(plain); + + var lines = std.mem.splitScalar(u8, plain, '\n'); + + // First 3 lines have content + try testing.expect(std.mem.startsWith(u8, lines.next().?, "Line 1")); + try testing.expect(std.mem.startsWith(u8, lines.next().?, "Line 2")); + try testing.expect(std.mem.startsWith(u8, lines.next().?, "Line 3")); + + // Next 7 lines should be empty (spaces) + var empty_count: usize = 0; + while (lines.next()) |line| { + if (empty_count >= 7) break; + try testing.expectEqual(@as(usize, 25), line.len); + var all_spaces = true; + for (line) |c| { + if (c != ' ') all_spaces = false; + } + try testing.expect(all_spaces); + empty_count += 1; + } + try testing.expectEqual(@as(usize, 7), empty_count); +} From e8db5ef24b0aa998499e388d5e3a38cc0c6f1a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=A1t=C3=A9=20M=C3=A9sz=C3=A1ros=20=28Laptop=29?= Date: Fri, 20 Feb 2026 15:03:38 +0100 Subject: [PATCH 2/2] Review and fix text_area padding --- src/components/text_area.zig | 56 ++++++++++++++---------- tests/input_tests.zig | 84 +++++++++++++++++++++++++++++------- 2 files changed, 102 insertions(+), 38 deletions(-) diff --git a/src/components/text_area.zig b/src/components/text_area.zig index 8f1bd84..b08019b 100644 --- a/src/components/text_area.zig +++ b/src/components/text_area.zig @@ -5,7 +5,6 @@ const std = @import("std"); const keys = @import("../input/keys.zig"); const style_mod = @import("../style/style.zig"); const Color = @import("../style/color.zig").Color; -const measure = @import("../layout/measure.zig"); const unicode = @import("../unicode.zig"); pub const TextArea = struct { @@ -582,16 +581,7 @@ pub const TextArea = struct { // Show placeholder on first empty line if (is_empty and r.line_idx == 0 and r.is_first_segment and self.placeholder.len > 0) { - const styled = try self.placeholder_style.render(allocator, self.placeholder); - defer allocator.free(styled); - try writer.writeAll(styled); - const placeholder_width = measure.width(self.placeholder); - const max_width: usize = text_width; - var rendered_width = @min(placeholder_width, max_width); - while (rendered_width < max_width) { - try writer.writeByte(' '); - rendered_width += 1; - } + try self.renderPlaceholder(writer, allocator, text_width); continue; } @@ -637,17 +627,7 @@ pub const TextArea = struct { // Show placeholder on first empty line if (is_empty and line_idx == 0 and self.placeholder.len > 0) { - const styled = try self.placeholder_style.render(allocator, self.placeholder); - defer allocator.free(styled); - try writer.writeAll(styled); - // Pad to full width - const placeholder_width = measure.width(self.placeholder); - const max_width: usize = text_width; - var rendered_width = @min(placeholder_width, max_width); - while (rendered_width < max_width) { - try writer.writeByte(' '); - rendered_width += 1; - } + try self.renderPlaceholder(writer, allocator, text_width); continue; } @@ -665,6 +645,38 @@ pub const TextArea = struct { return result.toOwnedSlice(); } + fn renderPlaceholder(self: *const TextArea, writer: anytype, allocator: std.mem.Allocator, max_width: u16) !void { + const width_limit: usize = max_width; + if (width_limit == 0) return; + + var rendered_width: usize = 0; + var byte_idx: usize = 0; + while (byte_idx < self.placeholder.len and rendered_width < width_limit) { + const byte_len = std.unicode.utf8ByteSequenceLength(self.placeholder[byte_idx]) catch 1; + if (byte_idx + byte_len > self.placeholder.len) break; + const char_slice = self.placeholder[byte_idx..][0..byte_len]; + + const cp = std.unicode.utf8Decode(char_slice) catch { + byte_idx += 1; + rendered_width += 1; + continue; + }; + const cw = wrapDisplayWidth(unicode.charWidth(cp), width_limit); + if (rendered_width + cw > width_limit) break; + + const styled = try self.placeholder_style.render(allocator, char_slice); + defer allocator.free(styled); + try writer.writeAll(styled); + byte_idx += byte_len; + rendered_width += cw; + } + + while (rendered_width < width_limit) { + try writer.writeByte(' '); + rendered_width += 1; + } + } + fn renderWrappedLineSegment( self: *const TextArea, writer: anytype, diff --git a/tests/input_tests.zig b/tests/input_tests.zig index af9aa59..f94ce6c 100644 --- a/tests/input_tests.zig +++ b/tests/input_tests.zig @@ -279,11 +279,69 @@ test "TextArea setSize without setValue pads placeholder to full width" { const plain = try stripAnsi(testing.allocator, rendered); defer testing.allocator.free(plain); - // Verify first line contains placeholder padded to 50 chars + // Verify full 50x3 viewport shape var lines = std.mem.splitScalar(u8, plain, '\n'); - const first_line = lines.next().?; - try testing.expectEqual(@as(usize, 50), first_line.len); - try testing.expect(std.mem.startsWith(u8, first_line, "Type here")); + const line0 = lines.next().?; + const line1 = lines.next().?; + const line2 = lines.next().?; + try testing.expectEqual(@as(usize, 50), line0.len); + try testing.expect(std.mem.startsWith(u8, line0, "Type here")); + try testing.expectEqual(@as(usize, 50), line1.len); + try testing.expectEqual(@as(usize, 50), line2.len); + for (line1) |c| { + try testing.expectEqual(@as(u8, ' '), c); + } + for (line2) |c| { + try testing.expectEqual(@as(u8, ' '), c); + } + try testing.expect(lines.next() == null); +} + +test "TextArea placeholder wider than width is clipped and padded" { + var area = zz.TextArea.init(testing.allocator); + defer area.deinit(); + + area.placeholder = "This is a very long placeholder"; + area.setSize(10, 2); + + const rendered = try area.view(testing.allocator); + defer testing.allocator.free(rendered); + const plain = try stripAnsi(testing.allocator, rendered); + defer testing.allocator.free(plain); + + var lines = std.mem.splitScalar(u8, plain, '\n'); + const line0 = lines.next().?; + const line1 = lines.next().?; + try testing.expectEqual(@as(usize, 10), line0.len); + try testing.expectEqual(@as(usize, 10), line1.len); + for (line1) |c| { + try testing.expectEqual(@as(u8, ' '), c); + } + try testing.expect(lines.next() == null); +} + +test "TextArea wrapped placeholder wider than width is clipped and padded" { + var area = zz.TextArea.init(testing.allocator); + defer area.deinit(); + + area.word_wrap = true; + area.placeholder = "This is a very long placeholder"; + area.setSize(10, 2); + + const rendered = try area.view(testing.allocator); + defer testing.allocator.free(rendered); + const plain = try stripAnsi(testing.allocator, rendered); + defer testing.allocator.free(plain); + + var lines = std.mem.splitScalar(u8, plain, '\n'); + const line0 = lines.next().?; + const line1 = lines.next().?; + try testing.expectEqual(@as(usize, 10), line0.len); + try testing.expectEqual(@as(usize, 10), line1.len); + for (line1) |c| { + try testing.expectEqual(@as(u8, ' '), c); + } + try testing.expect(lines.next() == null); } test "TextArea pads empty rows to full width" { @@ -309,15 +367,14 @@ test "TextArea pads empty rows to full width" { try testing.expect(std.mem.startsWith(u8, line0, "Single line")); // Remaining lines should be padded to 30 chars of spaces - var line_count: usize = 1; - while (lines.next()) |line| : (line_count += 1) { - if (line_count >= 5) break; + for (0..4) |_| { + const line = lines.next().?; try testing.expectEqual(@as(usize, 30), line.len); - // Verify it's all spaces for (line) |c| { try testing.expectEqual(@as(u8, ' '), c); } } + try testing.expect(lines.next() == null); } test "TextArea renders at correct width after setSize without setValue" { @@ -340,21 +397,16 @@ test "TextArea renders at correct width after setSize without setValue" { const rendered = try area.view(testing.allocator); defer testing.allocator.free(rendered); - // Verify content length is reasonable for 80x24 with ANSI codes - // Expected: ~80 * 24 = 1920 chars + newlines + ANSI codes - try testing.expect(rendered.len > 1000); - // Strip ANSI and verify dimensions const plain = try stripAnsi(testing.allocator, rendered); defer testing.allocator.free(plain); var lines = std.mem.splitScalar(u8, plain, '\n'); - var line_count: usize = 0; - while (lines.next()) |line| : (line_count += 1) { - if (line_count >= 24) break; + for (0..24) |_| { + const line = lines.next().?; try testing.expectEqual(@as(usize, 80), line.len); } - try testing.expectEqual(@as(usize, 24), line_count); + try testing.expect(lines.next() == null); } test "TextArea multiple resize operations maintain correct width" {