Something went wrong. Try again.
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681const 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, )); }}