aboutsummaryrefslogtreecommitdiff
path: root/vendor/libghostty-vt/src/renderer/cell.zig
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-04-04 19:22:04 +0300
committerCan Celik <ogulcancelik@gmail.com>2026-04-05 01:23:06 +0300
commit41dd733ca7123c9ac4bfc6c781697a660e95b052 (patch)
tree3fc91815ca139ef2e0b281bba0abda4ef15175f4 /vendor/libghostty-vt/src/renderer/cell.zig
parent55c138263e190a1e79d4b997bfb9397306c0ec1f (diff)
feat: finish ghostty pane backend integration
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/cell.zig')
-rw-r--r--vendor/libghostty-vt/src/renderer/cell.zig680
1 files changed, 680 insertions, 0 deletions
diff --git a/vendor/libghostty-vt/src/renderer/cell.zig b/vendor/libghostty-vt/src/renderer/cell.zig
new file mode 100644
index 00000000..196ebb17
--- /dev/null
+++ b/vendor/libghostty-vt/src/renderer/cell.zig
@@ -0,0 +1,680 @@
+const std = @import("std");
+const Allocator = std.mem.Allocator;
+const assert = @import("../quirks.zig").inlineAssert;
+const font = @import("../font/main.zig");
+const terminal = @import("../terminal/main.zig");
+const renderer = @import("../renderer.zig");
+const shaderpkg = renderer.Renderer.API.shaders;
+const ArrayListCollection = @import("../datastruct/array_list_collection.zig").ArrayListCollection;
+const symbols = @import("../unicode/symbols_table.zig").table;
+
+/// The possible cell content keys that exist.
+pub const Key = enum {
+ bg,
+ text,
+ underline,
+ strikethrough,
+ overline,
+
+ /// Returns the GPU vertex type for this key.
+ pub fn CellType(self: Key) type {
+ return switch (self) {
+ .bg => shaderpkg.CellBg,
+
+ .text,
+ .underline,
+ .strikethrough,
+ .overline,
+ => shaderpkg.CellText,
+ };
+ }
+};
+
+/// The contents of all the cells in the terminal.
+///
+/// The goal of this data structure is to allow for efficient row-wise
+/// clearing of data from the GPU buffers, to allow for row-wise dirty
+/// tracking to eliminate the overhead of rebuilding the GPU buffers
+/// each frame.
+///
+/// Must be initialized by resizing before calling any operations.
+pub const Contents = struct {
+ size: renderer.GridSize = .{ .rows = 0, .columns = 0 },
+
+ /// Flat array containing cell background colors for the terminal grid.
+ ///
+ /// Indexed as `bg_cells[row * size.columns + col]`.
+ ///
+ /// Prefer accessing with `Contents.bgCell(row, col).*` instead
+ /// of directly indexing in order to avoid integer size bugs.
+ bg_cells: []shaderpkg.CellBg = undefined,
+
+ /// The ArrayListCollection which holds all of the foreground cells. When
+ /// sized with Contents.resize the individual ArrayLists are given enough
+ /// room that they can hold a single row with #cols glyphs, underlines, and
+ /// strikethroughs; however, appendAssumeCapacity MUST NOT be used since
+ /// it is possible to exceed this with combining glyphs that add a glyph
+ /// but take up no column since they combine with the previous one, as
+ /// well as with fonts that perform multi-substitutions for glyphs, which
+ /// can result in a similar situation where multiple glyphs reside in the
+ /// same column.
+ ///
+ /// Allocations should nevertheless be exceedingly rare since hitting the
+ /// initial capacity of a list would require a row filled with underlined
+ /// struck through characters, at least one of which is a multi-glyph
+ /// composite.
+ ///
+ /// Rows are indexed as Contents.fg_rows[y + 1], because the first list in
+ /// the collection is reserved for the cursor, which must be the first item
+ /// in the buffer.
+ ///
+ /// Must be initialized by calling resize on the Contents struct before
+ /// calling any operations.
+ fg_rows: ArrayListCollection(shaderpkg.CellText) = .{ .lists = &.{} },
+
+ pub fn deinit(self: *Contents, alloc: Allocator) void {
+ alloc.free(self.bg_cells);
+ self.fg_rows.deinit(alloc);
+ }
+
+ /// Resize the cell contents for the given grid size. This will
+ /// always invalidate the entire cell contents.
+ pub fn resize(
+ self: *Contents,
+ alloc: Allocator,
+ size: renderer.GridSize,
+ ) Allocator.Error!void {
+ self.size = size;
+
+ const cell_count = @as(usize, size.columns) * @as(usize, size.rows);
+
+ const bg_cells = try alloc.alloc(shaderpkg.CellBg, cell_count);
+ errdefer alloc.free(bg_cells);
+ @memset(bg_cells, .{ 0, 0, 0, 0 });
+
+ // The foreground lists can hold 3 types of items:
+ // - Glyphs
+ // - Underlines
+ // - Strikethroughs
+ // So we give them an initial capacity of size.columns * 3, which will
+ // avoid any further allocations in the vast majority of cases. Sadly
+ // we can not assume capacity though, since with combining glyphs that
+ // form a single grapheme, and multi-substitutions in fonts, the number
+ // of glyphs in a row is theoretically unlimited.
+ //
+ // We have size.rows + 2 lists because indexes 0 and size.rows - 1 are
+ // used for special lists containing the cursor cell which need to
+ // be first and last in the buffer, respectively.
+ var fg_rows: ArrayListCollection(shaderpkg.CellText) = try .init(
+ alloc,
+ size.rows + 2,
+ size.columns * 3,
+ );
+ errdefer fg_rows.deinit(alloc);
+
+ // We don't need 3*cols worth of cells for the cursor lists, so we can
+ // replace them with smaller lists. This is technically a tiny bit of
+ // extra work but resize is not a hot function so it's worth it to not
+ // waste the memory.
+ fg_rows.lists[0].deinit(alloc);
+ fg_rows.lists[0] = try .initCapacity(alloc, 1);
+ fg_rows.lists[size.rows + 1].deinit(alloc);
+ fg_rows.lists[size.rows + 1] = try .initCapacity(alloc, 1);
+
+ // Perform the swap, no going back from here.
+ errdefer comptime unreachable;
+ alloc.free(self.bg_cells);
+ self.fg_rows.deinit(alloc);
+ self.bg_cells = bg_cells;
+ self.fg_rows = fg_rows;
+ }
+
+ /// Reset the cell contents to an empty state without resizing.
+ pub fn reset(self: *Contents) void {
+ @memset(self.bg_cells, .{ 0, 0, 0, 0 });
+ self.fg_rows.reset();
+ }
+
+ /// Set the cursor value. If the value is null then the cursor is hidden.
+ pub fn setCursor(
+ self: *Contents,
+ v: ?shaderpkg.CellText,
+ cursor_style: ?renderer.CursorStyle,
+ ) void {
+ if (self.size.rows == 0) return;
+ self.fg_rows.lists[0].clearRetainingCapacity();
+ self.fg_rows.lists[self.size.rows + 1].clearRetainingCapacity();
+
+ const cell = v orelse return;
+ const style = cursor_style orelse return;
+
+ switch (style) {
+ // Block cursors should be drawn first
+ .block => self.fg_rows.lists[0].appendAssumeCapacity(cell),
+ // Other cursor styles should be drawn last
+ .block_hollow, .bar, .underline, .lock => self.fg_rows.lists[self.size.rows + 1].appendAssumeCapacity(cell),
+ }
+ }
+
+ /// Returns the current cursor glyph if present, checking both cursor lists.
+ pub fn getCursorGlyph(self: *Contents) ?shaderpkg.CellText {
+ if (self.size.rows == 0) return null;
+ if (self.fg_rows.lists[0].items.len > 0) {
+ return self.fg_rows.lists[0].items[0];
+ }
+ if (self.fg_rows.lists[self.size.rows + 1].items.len > 0) {
+ return self.fg_rows.lists[self.size.rows + 1].items[0];
+ }
+ return null;
+ }
+
+ /// Access a background cell. Prefer this function over direct indexing
+ /// of `bg_cells` in order to avoid integer size bugs causing overflows.
+ pub inline fn bgCell(
+ self: *Contents,
+ row: usize,
+ col: usize,
+ ) *shaderpkg.CellBg {
+ return &self.bg_cells[row * self.size.columns + col];
+ }
+
+ /// Add a cell to the appropriate list. Adding the same cell twice will
+ /// result in duplication in the vertex buffer. The caller should clear
+ /// the corresponding row with Contents.clear to remove old cells first.
+ pub fn add(
+ self: *Contents,
+ alloc: Allocator,
+ comptime key: Key,
+ cell: key.CellType(),
+ ) Allocator.Error!void {
+ const y = cell.grid_pos[1];
+
+ assert(y < self.size.rows);
+
+ switch (key) {
+ .bg => comptime unreachable,
+
+ .text,
+ .underline,
+ .strikethrough,
+ .overline,
+ // We have a special list containing the cursor cell at the start
+ // of our fg row collection, so we need to add 1 to the y to get
+ // the correct index.
+ => try self.fg_rows.lists[y + 1].append(alloc, cell),
+ }
+ }
+
+ /// Clear all of the cell contents for a given row.
+ pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
+ assert(y < self.size.rows);
+
+ @memset(self.bg_cells[@as(usize, y) * self.size.columns ..][0..self.size.columns], .{ 0, 0, 0, 0 });
+
+ // We have a special list containing the cursor cell at the start
+ // of our fg row collection, so we need to add 1 to the y to get
+ // the correct index.
+ self.fg_rows.lists[y + 1].clearRetainingCapacity();
+ }
+};
+
+/// Returns true if a codepoint for a cell is a covering character. A covering
+/// character is a character that covers the entire cell. This is used to
+/// make window-padding-color=extend work better. See #2099.
+pub fn isCovering(cp: u21) bool {
+ return switch (cp) {
+ // U+2588 FULL BLOCK
+ 0x2588 => true,
+
+ else => false,
+ };
+}
+
+/// Returns true of the codepoint is a "symbol-like" character, which
+/// for now we define as anything in a private use area, and anything
+/// in several unicode blocks:
+/// - Arrows
+/// - Dingbats
+/// - Emoticons
+/// - Miscellaneous Symbols
+/// - Enclosed Alphanumerics
+/// - Enclosed Alphanumeric Supplement
+/// - Miscellaneous Symbols and Pictographs
+/// - Transport and Map Symbols
+///
+/// In the future it may be prudent to expand this to encompass more
+/// symbol-like characters, and/or exclude some PUA sections.
+pub fn isSymbol(cp: u21) bool {
+ return symbols.get(cp);
+}
+
+/// Returns the appropriate `constraint_width` for
+/// the provided cell when rendering its glyph(s).
+pub fn constraintWidth(
+ raw_slice: []const terminal.page.Cell,
+ x: usize,
+ cols: usize,
+) u2 {
+ const cell = raw_slice[x];
+ const cp = cell.codepoint();
+
+ const grid_width = cell.gridWidth();
+
+ // If the grid width of the cell is 2, the constraint
+ // width will always be 2, so we can just return early.
+ if (grid_width > 1) return grid_width;
+
+ // We allow "symbol-like" glyphs to extend to 2 cells wide if there's
+ // space, and if the previous glyph wasn't also a symbol. So if this
+ // codepoint isn't a symbol then we can return the grid width.
+ if (!isSymbol(cp)) return grid_width;
+
+ // If we are at the end of the screen it must be constrained to one cell.
+ if (x == cols - 1) return 1;
+
+ // If we have a previous cell and it was a symbol then we need
+ // to also constrain. This is so that multiple PUA glyphs align.
+ // This does not apply if the previous symbol is a graphics
+ // element such as a block element or Powerline glyph.
+ if (x > 0) {
+ const prev_cp = raw_slice[x - 1].codepoint();
+ if (isSymbol(prev_cp) and !isGraphicsElement(prev_cp)) {
+ return 1;
+ }
+ }
+
+ // If the next cell is whitespace, then we
+ // allow the glyph to be up to two cells wide.
+ const next_cp = raw_slice[x + 1].codepoint();
+ if (next_cp == 0 or isSpace(next_cp)) return 2;
+
+ // Otherwise, this has to be 1 cell wide.
+ return 1;
+}
+
+/// Whether min contrast should be disabled for a given glyph. True
+/// for graphics elements such as blocks and Powerline glyphs.
+pub fn noMinContrast(cp: u21) bool {
+ return isGraphicsElement(cp);
+}
+
+// Some general spaces, others intentionally kept
+// to force the font to render as a fixed width.
+fn isSpace(char: u21) bool {
+ return switch (char) {
+ 0x0020, // SPACE
+ 0x2002, // EN SPACE
+ => true,
+ else => false,
+ };
+}
+
+/// Returns true if the codepoint is used for terminal graphics, such
+/// as box drawing characters, block elements, and Powerline glyphs.
+fn isGraphicsElement(char: u21) bool {
+ return isBoxDrawing(char) or isBlockElement(char) or isLegacyComputing(char) or isPowerline(char);
+}
+
+// Returns true if the codepoint is a box drawing character.
+fn isBoxDrawing(char: u21) bool {
+ return switch (char) {
+ 0x2500...0x257F => true,
+ else => false,
+ };
+}
+
+// Returns true if the codepoint is a block element.
+fn isBlockElement(char: u21) bool {
+ return switch (char) {
+ 0x2580...0x259F => true,
+ else => false,
+ };
+}
+
+// Returns true if the codepoint is in a Symbols for Legacy
+// Computing block, including supplements.
+fn isLegacyComputing(char: u21) bool {
+ return switch (char) {
+ 0x1FB00...0x1FBFF => true,
+ 0x1CC00...0x1CEBF => true, // Supplement introduced in Unicode 16.0
+ else => false,
+ };
+}
+
+// Returns true if the codepoint is a part of the Powerline range.
+fn isPowerline(char: u21) bool {
+ return switch (char) {
+ 0xE0B0...0xE0D7 => true,
+ else => false,
+ };
+}
+
+test Contents {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const rows = 10;
+ const cols = 10;
+
+ var c: Contents = .{};
+ try c.resize(alloc, .{ .rows = rows, .columns = cols });
+ defer c.deinit(alloc);
+
+ // We should start off empty after resizing.
+ for (0..rows) |y| {
+ try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
+ for (0..cols) |x| {
+ try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*);
+ }
+ }
+ // And the cursor row should have a capacity of 1 and also be empty.
+ try testing.expect(c.fg_rows.lists[0].capacity == 1);
+ try testing.expect(c.fg_rows.lists[0].items.len == 0);
+
+ // Add some contents.
+ const bg_cell: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
+ const fg_cell: shaderpkg.CellText = .{
+ .atlas = .grayscale,
+ .grid_pos = .{ 4, 1 },
+ .color = .{ 0, 0, 0, 1 },
+ };
+ c.bgCell(1, 4).* = bg_cell;
+ try c.add(alloc, .text, fg_cell);
+ try testing.expectEqual(bg_cell, c.bgCell(1, 4).*);
+ // The fg row index is offset by 1 because of the cursor list.
+ try testing.expectEqual(fg_cell, c.fg_rows.lists[2].items[0]);
+
+ // And we should be able to clear it.
+ c.clear(1);
+ for (0..rows) |y| {
+ try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
+ for (0..cols) |x| {
+ try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*);
+ }
+ }
+
+ // Add a block cursor.
+ const cursor_cell: shaderpkg.CellText = .{
+ .atlas = .grayscale,
+ .bools = .{ .is_cursor_glyph = true },
+ .grid_pos = .{ 2, 3 },
+ .color = .{ 0, 0, 0, 1 },
+ };
+ c.setCursor(cursor_cell, .block);
+ try testing.expectEqual(cursor_cell, c.fg_rows.lists[0].items[0]);
+ try testing.expectEqual(cursor_cell, c.getCursorGlyph().?);
+
+ // And remove it.
+ c.setCursor(null, null);
+ try testing.expectEqual(0, c.fg_rows.lists[0].items.len);
+ try testing.expect(c.getCursorGlyph() == null);
+
+ // Add a hollow cursor.
+ c.setCursor(cursor_cell, .block_hollow);
+ try testing.expectEqual(cursor_cell, c.fg_rows.lists[rows + 1].items[0]);
+ try testing.expectEqual(cursor_cell, c.getCursorGlyph().?);
+}
+
+test "Contents clear retains other content" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const rows = 10;
+ const cols = 10;
+
+ var c: Contents = .{};
+ try c.resize(alloc, .{ .rows = rows, .columns = cols });
+ defer c.deinit(alloc);
+
+ // Set some contents
+ // bg and fg cells in row 1
+ const bg_cell_1: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
+ const fg_cell_1: shaderpkg.CellText = .{
+ .atlas = .grayscale,
+ .grid_pos = .{ 4, 1 },
+ .color = .{ 0, 0, 0, 1 },
+ };
+ c.bgCell(1, 4).* = bg_cell_1;
+ try c.add(alloc, .text, fg_cell_1);
+ // bg and fg cells in row 2
+ const bg_cell_2: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
+ const fg_cell_2: shaderpkg.CellText = .{
+ .atlas = .grayscale,
+ .grid_pos = .{ 4, 2 },
+ .color = .{ 0, 0, 0, 1 },
+ };
+ c.bgCell(2, 4).* = bg_cell_2;
+ try c.add(alloc, .text, fg_cell_2);
+
+ // Clear row 1, this should leave row 2 untouched
+ c.clear(1);
+
+ // Row 2 should still contain its cells.
+ try testing.expectEqual(bg_cell_2, c.bgCell(2, 4).*);
+ // Fg row index is +1 because of cursor list at start
+ try testing.expectEqual(fg_cell_2, c.fg_rows.lists[3].items[0]);
+}
+
+test "Contents clear last added content" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ const rows = 10;
+ const cols = 10;
+
+ var c: Contents = .{};
+ try c.resize(alloc, .{ .rows = rows, .columns = cols });
+ defer c.deinit(alloc);
+
+ // Set some contents
+ // bg and fg cells in row 1
+ const bg_cell_1: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
+ const fg_cell_1: shaderpkg.CellText = .{
+ .atlas = .grayscale,
+ .grid_pos = .{ 4, 1 },
+ .color = .{ 0, 0, 0, 1 },
+ };
+ c.bgCell(1, 4).* = bg_cell_1;
+ try c.add(alloc, .text, fg_cell_1);
+ // bg and fg cells in row 2
+ const bg_cell_2: shaderpkg.CellBg = .{ 0, 0, 0, 1 };
+ const fg_cell_2: shaderpkg.CellText = .{
+ .atlas = .grayscale,
+ .grid_pos = .{ 4, 2 },
+ .color = .{ 0, 0, 0, 1 },
+ };
+ c.bgCell(2, 4).* = bg_cell_2;
+ try c.add(alloc, .text, fg_cell_2);
+
+ // Clear row 2, this should leave row 1 untouched
+ c.clear(2);
+
+ // Row 1 should still contain its cells.
+ try testing.expectEqual(bg_cell_1, c.bgCell(1, 4).*);
+ // Fg row index is +1 because of cursor list at start
+ try testing.expectEqual(fg_cell_1, c.fg_rows.lists[2].items[0]);
+}
+
+test "Contents with zero-sized screen" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var c: Contents = .{};
+ defer c.deinit(alloc);
+
+ c.setCursor(null, null);
+ try testing.expect(c.getCursorGlyph() == null);
+}
+
+test "Cell constraint widths" {
+ const testing = std.testing;
+ const alloc = testing.allocator;
+
+ var t: terminal.Terminal = try .init(alloc, .{
+ .cols = 4,
+ .rows = 1,
+ });
+ defer t.deinit(alloc);
+
+ var s = t.vtStream();
+ defer s.deinit();
+
+ var state: terminal.RenderState = .empty;
+ defer state.deinit(alloc);
+
+ // for each case, the numbers in the comment denote expected
+ // constraint widths for the symbol-containing cells
+
+ // symbol->nothing: 2
+ {
+ t.fullReset();
+ s.nextSlice("");
+ try state.update(alloc, &t);
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ }
+
+ // symbol->character: 1
+ {
+ t.fullReset();
+ s.nextSlice("z");
+ try state.update(alloc, &t);
+ try testing.expectEqual(1, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ }
+
+ // symbol->space: 2
+ {
+ t.fullReset();
+ s.nextSlice(" z");
+ try state.update(alloc, &t);
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ }
+ // symbol->no-break space: 1
+ {
+ t.fullReset();
+ s.nextSlice("\u{00a0}z");
+ try state.update(alloc, &t);
+ try testing.expectEqual(1, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ }
+
+ // symbol->end of row: 1
+ {
+ t.fullReset();
+ s.nextSlice(" ");
+ try state.update(alloc, &t);
+ try testing.expectEqual(1, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 3,
+ state.cols,
+ ));
+ }
+
+ // character->symbol: 2
+ {
+ t.fullReset();
+ s.nextSlice("z");
+ try state.update(alloc, &t);
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 1,
+ state.cols,
+ ));
+ }
+
+ // symbol->symbol: 1,1
+ {
+ t.fullReset();
+ s.nextSlice("");
+ try state.update(alloc, &t);
+ try testing.expectEqual(1, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ try testing.expectEqual(1, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 1,
+ state.cols,
+ ));
+ }
+
+ // symbol->space->symbol: 2,2
+ {
+ t.fullReset();
+ s.nextSlice(" ");
+ try state.update(alloc, &t);
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 2,
+ state.cols,
+ ));
+ }
+
+ // symbol->powerline: 1 (dedicated test because powerline is special-cased in cellpkg)
+ {
+ t.fullReset();
+ s.nextSlice("");
+ try state.update(alloc, &t);
+ try testing.expectEqual(1, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ }
+
+ // powerline->symbol: 2 (dedicated test because powerline is special-cased in cellpkg)
+ {
+ t.fullReset();
+ s.nextSlice("");
+ try state.update(alloc, &t);
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 1,
+ state.cols,
+ ));
+ }
+
+ // powerline->nothing: 2 (dedicated test because powerline is special-cased in cellpkg)
+ {
+ t.fullReset();
+ s.nextSlice("");
+ try state.update(alloc, &t);
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ }
+
+ // powerline->space: 2 (dedicated test because powerline is special-cased in cellpkg)
+ {
+ t.fullReset();
+ s.nextSlice(" z");
+ try state.update(alloc, &t);
+ try testing.expectEqual(2, constraintWidth(
+ state.row_data.get(0).cells.items(.raw),
+ 0,
+ state.cols,
+ ));
+ }
+}