diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-10 21:51:57 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-10 21:51:57 +0300 |
| commit | 425c86179791cd32e2d4ce0ae7f269940b4b0663 (patch) | |
| tree | e52c4383a6d69216cbe2a32638bf90841d7e2189 /vendor/libghostty-vt/src/renderer/cell.zig | |
| parent | 90e947a6e26e02f10ec7f1694c4eb5b78a86406b (diff) | |
chore: upgrade libghostty and preserve terminal behavior (#3906)
Diffstat (limited to 'vendor/libghostty-vt/src/renderer/cell.zig')
| -rw-r--r-- | vendor/libghostty-vt/src/renderer/cell.zig | 156 |
1 files changed, 91 insertions, 65 deletions
diff --git a/vendor/libghostty-vt/src/renderer/cell.zig b/vendor/libghostty-vt/src/renderer/cell.zig index 196ebb17..8753d1ad 100644 --- a/vendor/libghostty-vt/src/renderer/cell.zig +++ b/vendor/libghostty-vt/src/renderer/cell.zig @@ -5,9 +5,10 @@ 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; +const CellTextRow = std.ArrayListUnmanaged(shaderpkg.CellText); + /// The possible cell content keys that exist. pub const Key = enum { bg, @@ -47,11 +48,11 @@ pub const Contents = struct { /// /// Prefer accessing with `Contents.bgCell(row, col).*` instead /// of directly indexing in order to avoid integer size bugs. - bg_cells: []shaderpkg.CellBg = undefined, + bg_cells: []shaderpkg.CellBg = &.{}, - /// 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 + /// The lists which hold 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 @@ -70,11 +71,12 @@ pub const Contents = struct { /// /// Must be initialized by calling resize on the Contents struct before /// calling any operations. - fg_rows: ArrayListCollection(shaderpkg.CellText) = .{ .lists = &.{} }, + fg_rows: []CellTextRow = &.{}, pub fn deinit(self: *Contents, alloc: Allocator) void { alloc.free(self.bg_cells); - self.fg_rows.deinit(alloc); + for (self.fg_rows) |*row| row.deinit(alloc); + alloc.free(self.fg_rows); } /// Resize the cell contents for the given grid size. This will @@ -84,55 +86,50 @@ pub const Contents = struct { alloc: Allocator, size: renderer.GridSize, ) Allocator.Error!void { - self.size = size; + const row_count: usize = size.rows; + + // The two extra lists hold cursor cells: index 0 is drawn before the + // row contents, and index row_count + 1 is drawn after them. + const fg_rows = try alloc.alloc(CellTextRow, row_count + 2); + @memset(fg_rows, .empty); + errdefer { + for (fg_rows) |*row| row.deinit(alloc); + alloc.free(fg_rows); + } - 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); + // Foreground rows hold glyphs plus underlines, overlines, and + // strikethroughs. Three entries per column cover the common cases + // without reserving space for every decoration combination. We can't + // assume this capacity because combining glyphs and font substitutions + // can produce arbitrarily many glyphs in one column. + const fg_row_capacity = @as(usize, size.columns) * 3; + + // The cursor lists need just one cell. The rest get the full capacity. + fg_rows[0] = try .initCapacity(alloc, 1); + fg_rows[row_count + 1] = try .initCapacity(alloc, 1); + for (fg_rows[1 .. row_count + 1]) |*row| { + row.* = try .initCapacity(alloc, fg_row_capacity); + } - // 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); + const bg_cells = try alloc.realloc( + self.bg_cells, + row_count * @as(usize, size.columns), + ); // Perform the swap, no going back from here. errdefer comptime unreachable; - alloc.free(self.bg_cells); - self.fg_rows.deinit(alloc); + for (self.fg_rows) |*row| row.deinit(alloc); + alloc.free(self.fg_rows); + self.size = size; self.bg_cells = bg_cells; self.fg_rows = fg_rows; + self.reset(); } /// 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(); + for (self.fg_rows) |*row| row.clearRetainingCapacity(); } /// Set the cursor value. If the value is null then the cursor is hidden. @@ -142,28 +139,28 @@ pub const Contents = struct { 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(); + self.fg_rows[0].clearRetainingCapacity(); + self.fg_rows[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), + .block => self.fg_rows[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), + .block_hollow, .bar, .underline, .lock => self.fg_rows[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[0].items.len > 0) { + return self.fg_rows[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]; + if (self.fg_rows[self.size.rows + 1].items.len > 0) { + return self.fg_rows[self.size.rows + 1].items[0]; } return null; } @@ -201,7 +198,7 @@ pub const Contents = struct { // 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), + => try self.fg_rows[y + 1].append(alloc, cell), } } @@ -214,7 +211,7 @@ pub const Contents = struct { // 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(); + self.fg_rows[y + 1].clearRetainingCapacity(); } }; @@ -362,14 +359,14 @@ test Contents { // We should start off empty after resizing. for (0..rows) |y| { - try testing.expect(c.fg_rows.lists[y + 1].items.len == 0); + try testing.expect(c.fg_rows[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); + try testing.expect(c.fg_rows[0].capacity == 1); + try testing.expect(c.fg_rows[0].items.len == 0); // Add some contents. const bg_cell: shaderpkg.CellBg = .{ 0, 0, 0, 1 }; @@ -382,12 +379,12 @@ test Contents { 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]); + try testing.expectEqual(fg_cell, c.fg_rows[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); + try testing.expect(c.fg_rows[y + 1].items.len == 0); for (0..cols) |x| { try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(y, x).*); } @@ -401,20 +398,49 @@ test Contents { .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.fg_rows[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.expectEqual(0, c.fg_rows[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.fg_rows[rows + 1].items[0]); try testing.expectEqual(cursor_cell, c.getCursorGlyph().?); } +test "Contents resize grows and shrinks" { + const testing = std.testing; + const alloc = testing.allocator; + + var c: Contents = .{}; + defer c.deinit(alloc); + + try c.resize(alloc, .{ .rows = 2, .columns = 2 }); + var cell: shaderpkg.CellText = .{ + .atlas = .grayscale, + .grid_pos = .{ 1, 1 }, + .color = .{ 0, 0, 0, 1 }, + }; + try c.add(alloc, .text, cell); + c.bgCell(1, 1).* = .{ 0, 0, 0, 1 }; + c.setCursor(cell, .bar); + + try c.resize(alloc, .{ .rows = 3, .columns = 4 }); + try testing.expectEqual(.{ 0, 0, 0, 0 }, c.bgCell(1, 1).*); + try testing.expectEqual(@as(usize, 0), c.fg_rows[2].items.len); + try testing.expectEqual(@as(?shaderpkg.CellText, null), c.getCursorGlyph()); + cell.grid_pos = .{ 3, 2 }; + try c.add(alloc, .text, cell); + + try c.resize(alloc, .{ .rows = 1, .columns = 1 }); + cell.grid_pos = .{ 0, 0 }; + try c.add(alloc, .text, cell); +} + test "Contents clear retains other content" { const testing = std.testing; const alloc = testing.allocator; @@ -452,7 +478,7 @@ test "Contents clear retains other content" { // 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]); + try testing.expectEqual(fg_cell_2, c.fg_rows[3].items[0]); } test "Contents clear last added content" { @@ -492,7 +518,7 @@ test "Contents clear last added content" { // 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]); + try testing.expectEqual(fg_cell_1, c.fg_rows[2].items[0]); } test "Contents with zero-sized screen" { @@ -510,7 +536,7 @@ test "Cell constraint widths" { const testing = std.testing; const alloc = testing.allocator; - var t: terminal.Terminal = try .init(alloc, .{ + var t: terminal.Terminal = try .init(testing.io, alloc, .{ .cols = 4, .rows = 1, }); |
