mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
482 lines
16 KiB
Zig
482 lines
16 KiB
Zig
const std = @import("std");
|
|
const assert = std.debug.assert;
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const renderer = @import("../../renderer.zig");
|
|
const terminal = @import("../../terminal/main.zig");
|
|
const mtl_shaders = @import("shaders.zig");
|
|
|
|
/// The possible cell content keys that exist.
|
|
pub const Key = enum {
|
|
bg,
|
|
text,
|
|
underline,
|
|
strikethrough,
|
|
|
|
/// Returns the GPU vertex type for this key.
|
|
fn CellType(self: Key) type {
|
|
return switch (self) {
|
|
.bg => mtl_shaders.CellBg,
|
|
|
|
.text,
|
|
.underline,
|
|
.strikethrough,
|
|
=> mtl_shaders.CellText,
|
|
};
|
|
}
|
|
|
|
/// Returns true if the two keys share the same data array.
|
|
fn sharedData(self: Key, other: Key) bool {
|
|
return switch (self) {
|
|
inline else => |self_tag| switch (other) {
|
|
inline else => |other_tag| self_tag.CellType() == other_tag.CellType(),
|
|
},
|
|
};
|
|
}
|
|
};
|
|
|
|
/// The contents of all the cells in the terminal.
|
|
///
|
|
/// The goal of this data structure is to make it efficient for two operations:
|
|
///
|
|
/// 1. Setting the contents of a cell by coordinate. More specifically,
|
|
/// we want to be efficient setting cell contents by row since we
|
|
/// will be doing row dirty tracking.
|
|
///
|
|
/// 2. Syncing the contents of the CPU buffers to GPU buffers. This happens
|
|
/// every frame and should be as fast as possible.
|
|
///
|
|
/// To achieve this, the contents are stored in contiguous arrays by
|
|
/// GPU vertex type and we have an array of mappings indexed by coordinate
|
|
/// that map to the index in the GPU vertex array that the content is at.
|
|
pub const Contents = struct {
|
|
/// The map contains the mapping of cell content for every cell in the
|
|
/// terminal to the index in the cells array that the content is at.
|
|
/// This is ALWAYS sized to exactly (rows * cols) so we want to keep
|
|
/// this as small as possible.
|
|
///
|
|
/// Before any operation, this must be initialized by calling resize
|
|
/// on the contents.
|
|
map: []Map,
|
|
|
|
/// The grid size of the terminal. This is used to determine the
|
|
/// map array index from a coordinate.
|
|
size: renderer.GridSize,
|
|
|
|
/// The actual GPU data (on the CPU) for all the cells in the terminal.
|
|
/// This only contains the cells that have content set. To determine
|
|
/// if a cell has content set, we check the map.
|
|
///
|
|
/// This data is synced to a buffer on every frame.
|
|
bgs: std.ArrayListUnmanaged(mtl_shaders.CellBg),
|
|
text: std.ArrayListUnmanaged(mtl_shaders.CellText),
|
|
|
|
/// True when the cursor should be rendered. This is managed by
|
|
/// the setCursor method and should not be set directly.
|
|
cursor: bool,
|
|
|
|
/// The amount of text elements we reserve at the beginning for
|
|
/// special elements like the cursor.
|
|
const text_reserved_len = 1;
|
|
|
|
pub fn init(alloc: Allocator) !Contents {
|
|
const map = try alloc.alloc(Map, 0);
|
|
errdefer alloc.free(map);
|
|
|
|
var result: Contents = .{
|
|
.map = map,
|
|
.size = .{ .rows = 0, .columns = 0 },
|
|
.bgs = .{},
|
|
.text = .{},
|
|
.cursor = false,
|
|
};
|
|
|
|
// We preallocate some amount of space for cell contents
|
|
// we always have as a prefix. For now the current prefix
|
|
// is length 1: the cursor.
|
|
try result.text.ensureTotalCapacity(alloc, text_reserved_len);
|
|
result.text.items.len = text_reserved_len;
|
|
|
|
return result;
|
|
}
|
|
|
|
pub fn deinit(self: *Contents, alloc: Allocator) void {
|
|
alloc.free(self.map);
|
|
self.bgs.deinit(alloc);
|
|
self.text.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,
|
|
) !void {
|
|
const map = try alloc.alloc(Map, size.rows * size.columns);
|
|
errdefer alloc.free(map);
|
|
@memset(map, .{});
|
|
|
|
alloc.free(self.map);
|
|
self.map = map;
|
|
self.size = size;
|
|
self.bgs.clearAndFree(alloc);
|
|
self.text.shrinkAndFree(alloc, text_reserved_len);
|
|
}
|
|
|
|
/// Reset the cell contents to an empty state without resizing.
|
|
pub fn reset(self: *Contents) void {
|
|
@memset(self.map, .{});
|
|
self.bgs.clearRetainingCapacity();
|
|
self.text.shrinkRetainingCapacity(text_reserved_len);
|
|
}
|
|
|
|
/// Returns the slice of fg cell contents to sync with the GPU.
|
|
pub fn fgCells(self: *const Contents) []const mtl_shaders.CellText {
|
|
const start: usize = if (self.cursor) 0 else 1;
|
|
return self.text.items[start..];
|
|
}
|
|
|
|
/// Returns the slice of bg cell contents to sync with the GPU.
|
|
pub fn bgCells(self: *const Contents) []const mtl_shaders.CellBg {
|
|
return self.bgs.items;
|
|
}
|
|
|
|
/// Set the cursor value. If the value is null then the cursor
|
|
/// is hidden.
|
|
pub fn setCursor(self: *Contents, v: ?mtl_shaders.CellText) void {
|
|
const cell = v orelse {
|
|
self.cursor = false;
|
|
return;
|
|
};
|
|
|
|
self.cursor = true;
|
|
self.text.items[0] = cell;
|
|
}
|
|
|
|
/// Get the cell contents for the given type and coordinate.
|
|
pub fn get(
|
|
self: *const Contents,
|
|
comptime key: Key,
|
|
coord: terminal.Coordinate,
|
|
) ?key.CellType() {
|
|
const mapping = self.map[self.index(coord)].array.get(key);
|
|
if (!mapping.set) return null;
|
|
return switch (key) {
|
|
.bg => self.bgs.items[mapping.index],
|
|
|
|
.text,
|
|
.underline,
|
|
.strikethrough,
|
|
=> self.text.items[mapping.index],
|
|
};
|
|
}
|
|
|
|
/// Set the cell contents for a given type of content at a given
|
|
/// coordinate (provided by the celll contents).
|
|
pub fn set(
|
|
self: *Contents,
|
|
alloc: Allocator,
|
|
comptime key: Key,
|
|
cell: key.CellType(),
|
|
) !void {
|
|
const mapping = self.map[
|
|
self.index(.{
|
|
.x = cell.grid_pos[0],
|
|
.y = cell.grid_pos[1],
|
|
})
|
|
].array.getPtr(key);
|
|
|
|
// Get our list of cells based on the key (comptime).
|
|
const list = &@field(self, switch (key) {
|
|
.bg => "bgs",
|
|
.text, .underline, .strikethrough => "text",
|
|
});
|
|
|
|
// If this content type is already set on this cell, we can
|
|
// simply update the pre-existing index in the list to the new
|
|
// contents.
|
|
if (mapping.set) {
|
|
list.items[mapping.index] = cell;
|
|
return;
|
|
}
|
|
|
|
// Otherwise we need to append the new cell to the list.
|
|
const idx: u31 = @intCast(list.items.len);
|
|
try list.append(alloc, cell);
|
|
mapping.* = .{ .set = true, .index = idx };
|
|
}
|
|
|
|
/// Clear all of the cell contents for a given row.
|
|
///
|
|
/// Due to the way this works internally, it is best to clear rows
|
|
/// from the bottom up. This is because when we clear a row, we
|
|
/// swap remove the last element in the list and then update the
|
|
/// mapping for the swapped element. If we clear from the top down,
|
|
/// then we would have to update the mapping for every element in
|
|
/// the list. If we clear from the bottom up, then we only have to
|
|
/// update the mapping for the last element in the list.
|
|
pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
|
|
const start_idx = self.index(.{ .x = 0, .y = y });
|
|
const end_idx = start_idx + self.size.columns;
|
|
const maps = self.map[start_idx..end_idx];
|
|
for (0..self.size.columns) |x| {
|
|
// It is better to clear from the right left due to the same
|
|
// reasons noted for bottom-up clearing in the doc comment.
|
|
const rev_x = self.size.columns - x - 1;
|
|
const map = &maps[rev_x];
|
|
|
|
var it = map.array.iterator();
|
|
while (it.next()) |entry| {
|
|
if (!entry.value.set) continue;
|
|
|
|
// This value is no longer set
|
|
entry.value.set = false;
|
|
|
|
// Remove the value at index. This does a "swap remove"
|
|
// which swaps the last element in to this place. This is
|
|
// important because after this we need to update the mapping
|
|
// for the swapped element.
|
|
const original_index = entry.value.index;
|
|
const coord_: ?terminal.Coordinate = switch (entry.key) {
|
|
.bg => bg: {
|
|
_ = self.bgs.swapRemove(original_index);
|
|
if (self.bgs.items.len == original_index) break :bg null;
|
|
const new = self.bgs.items[original_index];
|
|
break :bg .{ .x = new.grid_pos[0], .y = new.grid_pos[1] };
|
|
},
|
|
|
|
.text,
|
|
.underline,
|
|
.strikethrough,
|
|
=> text: {
|
|
_ = self.text.swapRemove(original_index);
|
|
if (self.text.items.len == original_index) break :text null;
|
|
const new = self.text.items[original_index];
|
|
break :text .{ .x = new.grid_pos[0], .y = new.grid_pos[1] };
|
|
},
|
|
};
|
|
|
|
// If we have the coordinate of the swapped element, then
|
|
// we need to update it to point at its new index, which is
|
|
// the index of the element we just removed.
|
|
//
|
|
// The reason we wouldn't have a coordinate is if we are
|
|
// removing the last element in the array, then nothing
|
|
// is swapped in and nothing needs to be updated.
|
|
if (coord_) |coord| {
|
|
const old_index = switch (entry.key) {
|
|
.bg => self.bgs.items.len,
|
|
.text, .underline, .strikethrough => self.text.items.len,
|
|
};
|
|
var old_it = self.map[self.index(coord)].array.iterator();
|
|
while (old_it.next()) |old_entry| {
|
|
if (old_entry.value.set and
|
|
old_entry.value.index == old_index and
|
|
entry.key.sharedData(old_entry.key))
|
|
{
|
|
old_entry.value.index = original_index;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn index(self: *const Contents, coord: terminal.Coordinate) usize {
|
|
return coord.y * self.size.columns + coord.x;
|
|
}
|
|
|
|
/// The mapping of a cell at a specific coordinate to the index in the
|
|
/// vertex arrays where the cell content is at, if it is set.
|
|
const Map = struct {
|
|
/// The set of cell content mappings for a given cell for every
|
|
/// possible key. This is used to determine if a cell has a given
|
|
/// type of content (i.e. an underlyine styling) and if so what index
|
|
/// in the cells array that content is at.
|
|
const Array = std.EnumArray(Key, Mapping);
|
|
|
|
/// The mapping for a given key consists of a bit indicating if the
|
|
/// content is set and the index in the cells array that the content
|
|
/// is at. We pack this into a 32-bit integer so we only use 4 bytes
|
|
/// per possible cell content type.
|
|
const Mapping = packed struct(u32) {
|
|
set: bool = false,
|
|
index: u31 = 0,
|
|
};
|
|
|
|
/// The backing array of mappings.
|
|
array: Array = Array.initFill(.{}),
|
|
|
|
pub fn empty(self: *Map) bool {
|
|
var it = self.array.iterator();
|
|
while (it.next()) |entry| {
|
|
if (entry.value.set) return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
};
|
|
};
|
|
|
|
test Contents {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
const rows = 10;
|
|
const cols = 10;
|
|
|
|
var c = try Contents.init(alloc);
|
|
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
|
defer c.deinit(alloc);
|
|
|
|
// Assert that get returns null for everything.
|
|
for (0..rows) |y| {
|
|
for (0..cols) |x| {
|
|
try testing.expect(c.get(.bg, .{
|
|
.x = @intCast(x),
|
|
.y = @intCast(y),
|
|
}) == null);
|
|
}
|
|
}
|
|
|
|
// Set some contents
|
|
const cell: mtl_shaders.CellBg = .{
|
|
.mode = .rgb,
|
|
.grid_pos = .{ 4, 1 },
|
|
.cell_width = 1,
|
|
.color = .{ 0, 0, 0, 1 },
|
|
};
|
|
try c.set(alloc, .bg, cell);
|
|
try testing.expectEqual(cell, c.get(.bg, .{ .x = 4, .y = 1 }).?);
|
|
|
|
// Can clear it
|
|
c.clear(1);
|
|
for (0..rows) |y| {
|
|
for (0..cols) |x| {
|
|
try testing.expect(c.get(.bg, .{
|
|
.x = @intCast(x),
|
|
.y = @intCast(y),
|
|
}) == null);
|
|
}
|
|
}
|
|
}
|
|
|
|
test "Contents clear retains other content" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
const rows = 10;
|
|
const cols = 10;
|
|
|
|
var c = try Contents.init(alloc);
|
|
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
|
defer c.deinit(alloc);
|
|
|
|
// Set some contents
|
|
const cell1: mtl_shaders.CellBg = .{
|
|
.mode = .rgb,
|
|
.grid_pos = .{ 4, 1 },
|
|
.cell_width = 1,
|
|
.color = .{ 0, 0, 0, 1 },
|
|
};
|
|
const cell2: mtl_shaders.CellBg = .{
|
|
.mode = .rgb,
|
|
.grid_pos = .{ 4, 2 },
|
|
.cell_width = 1,
|
|
.color = .{ 0, 0, 0, 1 },
|
|
};
|
|
try c.set(alloc, .bg, cell1);
|
|
try c.set(alloc, .bg, cell2);
|
|
c.clear(1);
|
|
|
|
// Row 2 should still be valid.
|
|
try testing.expectEqual(cell2, c.get(.bg, .{ .x = 4, .y = 2 }).?);
|
|
}
|
|
|
|
test "Contents clear last added content" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
const rows = 10;
|
|
const cols = 10;
|
|
|
|
var c = try Contents.init(alloc);
|
|
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
|
defer c.deinit(alloc);
|
|
|
|
// Set some contents
|
|
const cell1: mtl_shaders.CellBg = .{
|
|
.mode = .rgb,
|
|
.grid_pos = .{ 4, 1 },
|
|
.cell_width = 1,
|
|
.color = .{ 0, 0, 0, 1 },
|
|
};
|
|
const cell2: mtl_shaders.CellBg = .{
|
|
.mode = .rgb,
|
|
.grid_pos = .{ 4, 2 },
|
|
.cell_width = 1,
|
|
.color = .{ 0, 0, 0, 1 },
|
|
};
|
|
try c.set(alloc, .bg, cell1);
|
|
try c.set(alloc, .bg, cell2);
|
|
c.clear(2);
|
|
|
|
// Row 2 should still be valid.
|
|
try testing.expectEqual(cell1, c.get(.bg, .{ .x = 4, .y = 1 }).?);
|
|
}
|
|
|
|
test "Contents clear modifies same data array" {
|
|
const testing = std.testing;
|
|
const alloc = testing.allocator;
|
|
|
|
const rows = 10;
|
|
const cols = 10;
|
|
|
|
var c = try Contents.init(alloc);
|
|
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
|
defer c.deinit(alloc);
|
|
|
|
// Set some contents
|
|
const cell1: mtl_shaders.CellBg = .{
|
|
.mode = .rgb,
|
|
.grid_pos = .{ 4, 1 },
|
|
.cell_width = 1,
|
|
.color = .{ 0, 0, 0, 1 },
|
|
};
|
|
const cell2: mtl_shaders.CellBg = .{
|
|
.mode = .rgb,
|
|
.grid_pos = .{ 4, 2 },
|
|
.cell_width = 1,
|
|
.color = .{ 0, 0, 0, 1 },
|
|
};
|
|
try c.set(alloc, .bg, cell1);
|
|
try c.set(alloc, .bg, cell2);
|
|
|
|
const fg1: mtl_shaders.CellText = text: {
|
|
var cell: mtl_shaders.CellText = undefined;
|
|
cell.grid_pos = .{ 4, 1 };
|
|
break :text cell;
|
|
};
|
|
const fg2: mtl_shaders.CellText = text: {
|
|
var cell: mtl_shaders.CellText = undefined;
|
|
cell.grid_pos = .{ 4, 2 };
|
|
break :text cell;
|
|
};
|
|
try c.set(alloc, .text, fg1);
|
|
try c.set(alloc, .text, fg2);
|
|
|
|
c.clear(1);
|
|
|
|
// Should have all of row 2
|
|
try testing.expectEqual(cell2, c.get(.bg, .{ .x = 4, .y = 2 }).?);
|
|
try testing.expectEqual(fg2, c.get(.text, .{ .x = 4, .y = 2 }).?);
|
|
}
|
|
|
|
test "Contents.Map size" {
|
|
// We want to be mindful of when this increases because it affects
|
|
// renderer memory significantly.
|
|
try std.testing.expectEqual(@as(usize, 16), @sizeOf(Contents.Map));
|
|
}
|