mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 08:46:08 +03:00
Merge pull request #1736 from qwerasd205/metal-cell-fixes
Metal cell Contents structure improvements
This commit is contained in:
@ -566,9 +566,6 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
var cells = try mtl_cell.Contents.init(alloc);
|
|
||||||
errdefer cells.deinit(alloc);
|
|
||||||
|
|
||||||
const display_link: ?DisplayLink = switch (builtin.os.tag) {
|
const display_link: ?DisplayLink = switch (builtin.os.tag) {
|
||||||
.macos => if (options.config.vsync)
|
.macos => if (options.config.vsync)
|
||||||
try macos.video.DisplayLink.createWithActiveCGDisplays()
|
try macos.video.DisplayLink.createWithActiveCGDisplays()
|
||||||
@ -592,7 +589,7 @@ pub fn init(alloc: Allocator, options: renderer.Options) !Metal {
|
|||||||
.current_background_color = options.config.background,
|
.current_background_color = options.config.background,
|
||||||
|
|
||||||
// Render state
|
// Render state
|
||||||
.cells = cells,
|
.cells = .{},
|
||||||
.uniforms = .{
|
.uniforms = .{
|
||||||
.projection_matrix = undefined,
|
.projection_matrix = undefined,
|
||||||
.cell_size = undefined,
|
.cell_size = undefined,
|
||||||
@ -1036,11 +1033,9 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
|
|||||||
// log.debug("drawing frame index={}", .{self.gpu_state.frame_index});
|
// log.debug("drawing frame index={}", .{self.gpu_state.frame_index});
|
||||||
|
|
||||||
// Setup our frame data
|
// Setup our frame data
|
||||||
const cells_bg = self.cells.bgCells();
|
|
||||||
const cells_fg = self.cells.fgCells();
|
|
||||||
try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms});
|
try frame.uniforms.sync(self.gpu_state.device, &.{self.uniforms});
|
||||||
try frame.cells_bg.sync(self.gpu_state.device, cells_bg);
|
const bg_count = try frame.cells_bg.syncFromArrayLists(self.gpu_state.device, self.cells.bg_rows.lists);
|
||||||
try frame.cells.sync(self.gpu_state.device, cells_fg);
|
const fg_count = try frame.cells.syncFromArrayLists(self.gpu_state.device, self.cells.fg_rows.lists);
|
||||||
|
|
||||||
// If we have custom shaders, update the animation time.
|
// If we have custom shaders, update the animation time.
|
||||||
if (self.custom_shader_state) |*state| {
|
if (self.custom_shader_state) |*state| {
|
||||||
@ -1139,13 +1134,13 @@ pub fn drawFrame(self: *Metal, surface: *apprt.Surface) !void {
|
|||||||
try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]);
|
try self.drawImagePlacements(encoder, self.image_placements.items[0..self.image_bg_end]);
|
||||||
|
|
||||||
// Then draw background cells
|
// Then draw background cells
|
||||||
try self.drawCellBgs(encoder, frame, cells_bg.len);
|
try self.drawCellBgs(encoder, frame, bg_count);
|
||||||
|
|
||||||
// Then draw images under text
|
// Then draw images under text
|
||||||
try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]);
|
try self.drawImagePlacements(encoder, self.image_placements.items[self.image_bg_end..self.image_text_end]);
|
||||||
|
|
||||||
// Then draw fg cells
|
// Then draw fg cells
|
||||||
try self.drawCellFgs(encoder, frame, cells_fg.len);
|
try self.drawCellFgs(encoder, frame, fg_count);
|
||||||
|
|
||||||
// Then draw remaining images
|
// Then draw remaining images
|
||||||
try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]);
|
try self.drawImagePlacements(encoder, self.image_placements.items[self.image_text_end..]);
|
||||||
@ -2171,7 +2166,7 @@ fn updateCell(
|
|||||||
break :bg_alpha @intFromFloat(bg_alpha);
|
break :bg_alpha @intFromFloat(bg_alpha);
|
||||||
};
|
};
|
||||||
|
|
||||||
try self.cells.set(self.alloc, .bg, .{
|
try self.cells.add(self.alloc, .bg, .{
|
||||||
.mode = .rgb,
|
.mode = .rgb,
|
||||||
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
||||||
.cell_width = cell.gridWidth(),
|
.cell_width = cell.gridWidth(),
|
||||||
@ -2186,19 +2181,25 @@ fn updateCell(
|
|||||||
@intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))),
|
@intFromFloat(@max(0, @min(255, @round(self.config.background_opacity * 255)))),
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the cell has a character, draw it
|
// If the shaper cell has a glyph, draw it.
|
||||||
if (cell.hasText()) fg: {
|
if (shaper_cell.glyph_index) |glyph_index| glyph: {
|
||||||
// Render
|
// Render
|
||||||
const render = try self.font_grid.renderGlyph(
|
const render = try self.font_grid.renderGlyph(
|
||||||
self.alloc,
|
self.alloc,
|
||||||
shaper_run.font_index,
|
shaper_run.font_index,
|
||||||
shaper_cell.glyph_index orelse break :fg,
|
glyph_index,
|
||||||
.{
|
.{
|
||||||
.grid_metrics = self.grid_metrics,
|
.grid_metrics = self.grid_metrics,
|
||||||
.thicken = self.config.font_thicken,
|
.thicken = self.config.font_thicken,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If the glyph is 0 width or height, it will be invisible
|
||||||
|
// when drawn, so don't bother adding it to the buffer.
|
||||||
|
if (render.glyph.width == 0 or render.glyph.height == 0) {
|
||||||
|
break :glyph;
|
||||||
|
}
|
||||||
|
|
||||||
const mode: mtl_shaders.CellText.Mode = switch (try fgMode(
|
const mode: mtl_shaders.CellText.Mode = switch (try fgMode(
|
||||||
render.presentation,
|
render.presentation,
|
||||||
cell_pin,
|
cell_pin,
|
||||||
@ -2208,7 +2209,7 @@ fn updateCell(
|
|||||||
.constrained => .fg_constrained,
|
.constrained => .fg_constrained,
|
||||||
};
|
};
|
||||||
|
|
||||||
try self.cells.set(self.alloc, .text, .{
|
try self.cells.add(self.alloc, .text, .{
|
||||||
.mode = mode,
|
.mode = mode,
|
||||||
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
||||||
.cell_width = cell.gridWidth(),
|
.cell_width = cell.gridWidth(),
|
||||||
@ -2245,7 +2246,7 @@ fn updateCell(
|
|||||||
|
|
||||||
const color = style.underlineColor(palette) orelse colors.fg;
|
const color = style.underlineColor(palette) orelse colors.fg;
|
||||||
|
|
||||||
try self.cells.set(self.alloc, .underline, .{
|
try self.cells.add(self.alloc, .underline, .{
|
||||||
.mode = .fg,
|
.mode = .fg,
|
||||||
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
||||||
.cell_width = cell.gridWidth(),
|
.cell_width = cell.gridWidth(),
|
||||||
@ -2268,7 +2269,7 @@ fn updateCell(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
try self.cells.set(self.alloc, .strikethrough, .{
|
try self.cells.add(self.alloc, .strikethrough, .{
|
||||||
.mode = .fg,
|
.mode = .fg,
|
||||||
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
||||||
.cell_width = cell.gridWidth(),
|
.cell_width = cell.gridWidth(),
|
||||||
@ -2366,7 +2367,7 @@ fn addPreeditCell(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add our opaque background cell
|
// Add our opaque background cell
|
||||||
try self.cells.set(self.alloc, .bg, .{
|
try self.cells.add(self.alloc, .bg, .{
|
||||||
.mode = .rgb,
|
.mode = .rgb,
|
||||||
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
||||||
.cell_width = if (cp.wide) 2 else 1,
|
.cell_width = if (cp.wide) 2 else 1,
|
||||||
@ -2374,7 +2375,7 @@ fn addPreeditCell(
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Add our text
|
// Add our text
|
||||||
try self.cells.set(self.alloc, .text, .{
|
try self.cells.add(self.alloc, .text, .{
|
||||||
.mode = .fg,
|
.mode = .fg,
|
||||||
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
.grid_pos = .{ @intCast(coord.x), @intCast(coord.y) },
|
||||||
.cell_width = if (cp.wide) 2 else 1,
|
.cell_width = if (cp.wide) 2 else 1,
|
||||||
|
@ -107,5 +107,53 @@ pub fn Buffer(comptime T: type) type {
|
|||||||
|
|
||||||
@memcpy(dst, src);
|
@memcpy(dst, src);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Like Buffer.sync but takes data from an array of ArrayLists,
|
||||||
|
/// rather than a single array. Returns the number of items synced.
|
||||||
|
pub fn syncFromArrayLists(self: *Self, device: objc.Object, lists: []std.ArrayListUnmanaged(T)) !usize {
|
||||||
|
var total_len: usize = 0;
|
||||||
|
for (lists) |list| {
|
||||||
|
total_len += list.items.len;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we need more bytes than our buffer has, we need to reallocate.
|
||||||
|
const req_bytes = total_len * @sizeOf(T);
|
||||||
|
const avail_bytes = self.buffer.getProperty(c_ulong, "length");
|
||||||
|
if (req_bytes > avail_bytes) {
|
||||||
|
// Deallocate previous buffer
|
||||||
|
self.buffer.msgSend(void, objc.sel("release"), .{});
|
||||||
|
|
||||||
|
// Allocate a new buffer with enough to hold double what we require.
|
||||||
|
const size = req_bytes * 2;
|
||||||
|
self.buffer = device.msgSend(
|
||||||
|
objc.Object,
|
||||||
|
objc.sel("newBufferWithLength:options:"),
|
||||||
|
.{
|
||||||
|
@as(c_ulong, @intCast(size * @sizeOf(T))),
|
||||||
|
mtl.MTLResourceStorageModeShared,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can fit within the buffer so we can just replace bytes.
|
||||||
|
const dst = dst: {
|
||||||
|
const ptr = self.buffer.msgSend(?[*]u8, objc.sel("contents"), .{}) orelse {
|
||||||
|
log.warn("buffer contents ptr is null", .{});
|
||||||
|
return error.MetalFailed;
|
||||||
|
};
|
||||||
|
|
||||||
|
break :dst ptr[0..req_bytes];
|
||||||
|
};
|
||||||
|
|
||||||
|
var i: usize = 0;
|
||||||
|
|
||||||
|
for (lists) |list| {
|
||||||
|
const ptr = @as([*]const u8, @ptrCast(list.items.ptr));
|
||||||
|
@memcpy(dst[i..][0..list.items.len*@sizeOf(T)], ptr);
|
||||||
|
i += list.items.len*@sizeOf(T);
|
||||||
|
}
|
||||||
|
|
||||||
|
return total_len;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -24,86 +24,100 @@ pub const Key = enum {
|
|||||||
=> mtl_shaders.CellText,
|
=> 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(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// A pool of ArrayLists with methods for bulk operations.
|
||||||
|
fn ArrayListPool(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
const Self = ArrayListPool(T);
|
||||||
|
const ArrayListT = std.ArrayListUnmanaged(T);
|
||||||
|
|
||||||
|
// An array containing the lists that belong to this pool.
|
||||||
|
lists: []ArrayListT = &[_]ArrayListT{},
|
||||||
|
|
||||||
|
// The pool will be initialized with empty ArrayLists.
|
||||||
|
pub fn init(alloc: Allocator, list_count: usize, initial_capacity: usize) !Self {
|
||||||
|
const self: Self = .{
|
||||||
|
.lists = try alloc.alloc(ArrayListT, list_count),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (self.lists) |*list| {
|
||||||
|
list.* = try ArrayListT.initCapacity(alloc, initial_capacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Self, alloc: Allocator) void {
|
||||||
|
for (self.lists) |*list| {
|
||||||
|
list.deinit(alloc);
|
||||||
|
}
|
||||||
|
alloc.free(self.lists);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all lists in the pool.
|
||||||
|
pub fn reset(self: *Self) void {
|
||||||
|
for (self.lists) |*list| {
|
||||||
|
list.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// The contents of all the cells in the terminal.
|
/// The contents of all the cells in the terminal.
|
||||||
///
|
///
|
||||||
/// The goal of this data structure is to make it efficient for two operations:
|
/// 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.
|
||||||
///
|
///
|
||||||
/// 1. Setting the contents of a cell by coordinate. More specifically,
|
/// Must be initialized by resizing before calling any operations.
|
||||||
/// 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 {
|
pub const Contents = struct {
|
||||||
/// The map contains the mapping of cell content for every cell in the
|
size: renderer.GridSize = .{ .rows = 0, .columns = 0 },
|
||||||
/// 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
|
/// The ArrayListPool which holds all of the background cells. When sized
|
||||||
/// this as small as possible.
|
/// with Contents.resize the individual ArrayLists SHOULD be given enough
|
||||||
|
/// capacity that appendAssumeCapacity may be used, since it should be
|
||||||
|
/// impossible for a row to have more background cells than columns.
|
||||||
///
|
///
|
||||||
/// Before any operation, this must be initialized by calling resize
|
/// HOWEVER, the initial capacity can be exceeded due to multi-glyph
|
||||||
/// on the contents.
|
/// composites each adding a background cell for the same position.
|
||||||
map: []Map,
|
/// This should probably be considered a bug, but for now it means
|
||||||
|
/// that sometimes allocations might happen, so appendAssumeCapacity
|
||||||
/// The grid size of the terminal. This is used to determine the
|
/// MUST NOT be used.
|
||||||
/// 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.
|
/// Rows are indexed as Contents.bg_rows[y].
|
||||||
bgs: std.ArrayListUnmanaged(mtl_shaders.CellBg),
|
///
|
||||||
text: std.ArrayListUnmanaged(mtl_shaders.CellText),
|
/// Must be initialized by calling resize on the Contents struct before
|
||||||
|
/// calling any operations.
|
||||||
|
bg_rows: ArrayListPool(mtl_shaders.CellBg) = .{},
|
||||||
|
|
||||||
/// True when the cursor should be rendered. This is managed by
|
/// The ArrayListPool which holds all of the foreground cells. When sized
|
||||||
/// the setCursor method and should not be set directly.
|
/// with Contents.resize the individual ArrayLists are given enough room
|
||||||
cursor: bool,
|
/// that they can hold a single row with #cols glyphs, underlines, and
|
||||||
|
/// strikethroughs; however, appendAssumeCapacity MUST NOT be used since
|
||||||
/// The amount of text elements we reserve at the beginning for
|
/// it is possible to exceed this with combining glyphs that add a glyph
|
||||||
/// special elements like the cursor.
|
/// but take up no column since they combine with the previous one, as
|
||||||
const text_reserved_len = 1;
|
/// well as with fonts that perform multi-substitutions for glyphs, which
|
||||||
|
/// can result in a similar situation where multiple glyphs reside in the
|
||||||
pub fn init(alloc: Allocator) !Contents {
|
/// same column.
|
||||||
const map = try alloc.alloc(Map, 0);
|
///
|
||||||
errdefer alloc.free(map);
|
/// Allocations should nevertheless be exceedingly rare since hitting the
|
||||||
|
/// initial capacity of a list would require a row filled with underlined
|
||||||
var result: Contents = .{
|
/// struck through characters, at least one of which is a multi-glyph
|
||||||
.map = map,
|
/// composite.
|
||||||
.size = .{ .rows = 0, .columns = 0 },
|
///
|
||||||
.bgs = .{},
|
/// Rows are indexed as Contents.fg_rows[y + 1], because the first list in
|
||||||
.text = .{},
|
/// the pool is reserved for the cursor, which must be the first item in
|
||||||
.cursor = false,
|
/// the buffer.
|
||||||
};
|
///
|
||||||
|
/// Must be initialized by calling resize on the Contents struct before
|
||||||
// We preallocate some amount of space for cell contents
|
/// calling any operations.
|
||||||
// we always have as a prefix. For now the current prefix
|
fg_rows: ArrayListPool(mtl_shaders.CellText) = .{},
|
||||||
// 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 {
|
pub fn deinit(self: *Contents, alloc: Allocator) void {
|
||||||
alloc.free(self.map);
|
self.bg_rows.deinit(alloc);
|
||||||
self.bgs.deinit(alloc);
|
self.fg_rows.deinit(alloc);
|
||||||
self.text.deinit(alloc);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resize the cell contents for the given grid size. This will
|
/// Resize the cell contents for the given grid size. This will
|
||||||
@ -113,211 +127,98 @@ pub const Contents = struct {
|
|||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
size: renderer.GridSize,
|
size: renderer.GridSize,
|
||||||
) !void {
|
) !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.size = size;
|
||||||
self.bgs.clearAndFree(alloc);
|
|
||||||
self.text.shrinkAndFree(alloc, text_reserved_len);
|
// When we create our bg_rows pool, we give the lists an initial
|
||||||
|
// capacity of size.columns. This is to account for the usual case
|
||||||
|
// where you have a row with normal text and background colors.
|
||||||
|
// This can be exceeded due to multi-glyph composites each adding
|
||||||
|
// a background cell for the same position. This should probably be
|
||||||
|
// considered a bug, but for now it means that sometimes allocations
|
||||||
|
// might happen, and appendAssumeCapacity MUST NOT be used.
|
||||||
|
var bg_rows = try ArrayListPool(mtl_shaders.CellBg).init(alloc, size.rows, size.columns);
|
||||||
|
errdefer bg_rows.deinit(alloc);
|
||||||
|
|
||||||
|
// 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 + 1 lists because index 0 is used for a special
|
||||||
|
// list containing the cursor cell which needs to be first in the buffer.
|
||||||
|
var fg_rows = try ArrayListPool(mtl_shaders.CellText).init(alloc, size.rows + 1, size.columns * 3);
|
||||||
|
errdefer fg_rows.deinit(alloc);
|
||||||
|
|
||||||
|
self.bg_rows.deinit(alloc);
|
||||||
|
self.fg_rows.deinit(alloc);
|
||||||
|
|
||||||
|
self.bg_rows = bg_rows;
|
||||||
|
self.fg_rows = fg_rows;
|
||||||
|
|
||||||
|
// We don't need 3*cols worth of cells for the cursor list, so we can
|
||||||
|
// replace it with a smaller list. 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.
|
||||||
|
self.fg_rows.lists[0].deinit(alloc);
|
||||||
|
self.fg_rows.lists[0] = try std.ArrayListUnmanaged(mtl_shaders.CellText).initCapacity(alloc, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reset the cell contents to an empty state without resizing.
|
/// Reset the cell contents to an empty state without resizing.
|
||||||
pub fn reset(self: *Contents) void {
|
pub fn reset(self: *Contents) void {
|
||||||
@memset(self.map, .{});
|
self.bg_rows.reset();
|
||||||
self.bgs.clearRetainingCapacity();
|
self.fg_rows.reset();
|
||||||
self.text.shrinkRetainingCapacity(text_reserved_len);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the slice of fg cell contents to sync with the GPU.
|
/// Set the cursor value. If the value is null then the cursor is hidden.
|
||||||
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 {
|
pub fn setCursor(self: *Contents, v: ?mtl_shaders.CellText) void {
|
||||||
const cell = v orelse {
|
self.fg_rows.lists[0].clearRetainingCapacity();
|
||||||
self.cursor = false;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.cursor = true;
|
if (v) |cell| {
|
||||||
self.text.items[0] = cell;
|
self.fg_rows.lists[0].appendAssumeCapacity(cell);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the cell contents for the given type and coordinate.
|
/// Add a cell to the appropriate list. Adding the same cell twice will
|
||||||
pub fn get(
|
/// result in duplication in the vertex buffer. The caller should clear
|
||||||
self: *const Contents,
|
/// the corresponding row with Contents.clear to remove old cells first.
|
||||||
comptime key: Key,
|
pub fn add(
|
||||||
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,
|
self: *Contents,
|
||||||
alloc: Allocator,
|
alloc: Allocator,
|
||||||
comptime key: Key,
|
comptime key: Key,
|
||||||
cell: key.CellType(),
|
cell: key.CellType(),
|
||||||
) !void {
|
) !void {
|
||||||
const mapping = self.map[
|
const y = cell.grid_pos[1];
|
||||||
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).
|
assert(y < self.size.rows);
|
||||||
const list = &@field(self, switch (key) {
|
|
||||||
.bg => "bgs",
|
|
||||||
.text, .underline, .strikethrough => "text",
|
|
||||||
});
|
|
||||||
|
|
||||||
// If this content type is already set on this cell, we can
|
switch (key) {
|
||||||
// simply update the pre-existing index in the list to the new
|
.bg => try self.bg_rows.lists[y].append(alloc, cell),
|
||||||
// contents.
|
|
||||||
if (mapping.set) {
|
.text,
|
||||||
list.items[mapping.index] = cell;
|
.underline,
|
||||||
return;
|
.strikethrough,
|
||||||
|
// We have a special list containing the cursor cell at the start
|
||||||
|
// of our fg row pool, so we need to add 1 to the y to get the
|
||||||
|
// correct index.
|
||||||
|
=> try self.fg_rows.lists[y + 1].append(alloc, cell),
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
/// 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 {
|
pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
|
||||||
const start_idx = self.index(.{ .x = 0, .y = y });
|
assert(y < self.size.rows);
|
||||||
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();
|
self.bg_rows.lists[y].clearRetainingCapacity();
|
||||||
while (it.next()) |entry| {
|
// We have a special list containing the cursor cell at the start
|
||||||
if (!entry.value.set) continue;
|
// of our fg row pool, so we need to add 1 to the y to get the
|
||||||
|
// correct index.
|
||||||
// This value is no longer set
|
self.fg_rows.lists[y + 1].clearRetainingCapacity();
|
||||||
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 {
|
test Contents {
|
||||||
@ -327,40 +228,60 @@ test Contents {
|
|||||||
const rows = 10;
|
const rows = 10;
|
||||||
const cols = 10;
|
const cols = 10;
|
||||||
|
|
||||||
var c = try Contents.init(alloc);
|
var c: Contents = .{};
|
||||||
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
||||||
defer c.deinit(alloc);
|
defer c.deinit(alloc);
|
||||||
|
|
||||||
// Assert that get returns null for everything.
|
// We should start off empty after resizing.
|
||||||
for (0..rows) |y| {
|
for (0..rows) |y| {
|
||||||
for (0..cols) |x| {
|
try testing.expect(c.bg_rows.lists[y].items.len == 0);
|
||||||
try testing.expect(c.get(.bg, .{
|
try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
|
||||||
.x = @intCast(x),
|
|
||||||
.y = @intCast(y),
|
|
||||||
}) == null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
// 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);
|
||||||
|
|
||||||
// Set some contents
|
// Add some contents.
|
||||||
const cell: mtl_shaders.CellBg = .{
|
const bg_cell: mtl_shaders.CellBg = .{
|
||||||
.mode = .rgb,
|
.mode = .rgb,
|
||||||
.grid_pos = .{ 4, 1 },
|
.grid_pos = .{ 4, 1 },
|
||||||
.cell_width = 1,
|
.cell_width = 1,
|
||||||
.color = .{ 0, 0, 0, 1 },
|
.color = .{ 0, 0, 0, 1 },
|
||||||
};
|
};
|
||||||
try c.set(alloc, .bg, cell);
|
const fg_cell: mtl_shaders.CellText = .{
|
||||||
try testing.expectEqual(cell, c.get(.bg, .{ .x = 4, .y = 1 }).?);
|
.mode = .fg,
|
||||||
|
.grid_pos = .{ 4, 1 },
|
||||||
|
.cell_width = 1,
|
||||||
|
.color = .{ 0, 0, 0, 1 },
|
||||||
|
.bg_color = .{ 0, 0, 0, 1 },
|
||||||
|
};
|
||||||
|
try c.add(alloc, .bg, bg_cell);
|
||||||
|
try c.add(alloc, .text, fg_cell);
|
||||||
|
try testing.expectEqual(bg_cell, c.bg_rows.lists[1].items[0]);
|
||||||
|
// 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]);
|
||||||
|
|
||||||
// Can clear it
|
// And we should be able to clear it.
|
||||||
c.clear(1);
|
c.clear(1);
|
||||||
for (0..rows) |y| {
|
for (0..rows) |y| {
|
||||||
for (0..cols) |x| {
|
try testing.expect(c.bg_rows.lists[y].items.len == 0);
|
||||||
try testing.expect(c.get(.bg, .{
|
try testing.expect(c.fg_rows.lists[y + 1].items.len == 0);
|
||||||
.x = @intCast(x),
|
|
||||||
.y = @intCast(y),
|
|
||||||
}) == null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a cursor.
|
||||||
|
const cursor_cell: mtl_shaders.CellText = .{
|
||||||
|
.mode = .cursor,
|
||||||
|
.grid_pos = .{ 2, 3 },
|
||||||
|
.cell_width = 1,
|
||||||
|
.color = .{ 0, 0, 0, 1 },
|
||||||
|
.bg_color = .{ 0, 0, 0, 1 },
|
||||||
|
};
|
||||||
|
c.setCursor(cursor_cell);
|
||||||
|
try testing.expectEqual(cursor_cell, c.fg_rows.lists[0].items[0]);
|
||||||
|
|
||||||
|
// And remove it.
|
||||||
|
c.setCursor(null);
|
||||||
|
try testing.expectEqual(0, c.fg_rows.lists[0].items.len);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Contents clear retains other content" {
|
test "Contents clear retains other content" {
|
||||||
@ -370,7 +291,7 @@ test "Contents clear retains other content" {
|
|||||||
const rows = 10;
|
const rows = 10;
|
||||||
const cols = 10;
|
const cols = 10;
|
||||||
|
|
||||||
var c = try Contents.init(alloc);
|
var c: Contents = .{};
|
||||||
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
||||||
defer c.deinit(alloc);
|
defer c.deinit(alloc);
|
||||||
|
|
||||||
@ -387,12 +308,12 @@ test "Contents clear retains other content" {
|
|||||||
.cell_width = 1,
|
.cell_width = 1,
|
||||||
.color = .{ 0, 0, 0, 1 },
|
.color = .{ 0, 0, 0, 1 },
|
||||||
};
|
};
|
||||||
try c.set(alloc, .bg, cell1);
|
try c.add(alloc, .bg, cell1);
|
||||||
try c.set(alloc, .bg, cell2);
|
try c.add(alloc, .bg, cell2);
|
||||||
c.clear(1);
|
c.clear(1);
|
||||||
|
|
||||||
// Row 2 should still be valid.
|
// Row 2 should still contain its cell.
|
||||||
try testing.expectEqual(cell2, c.get(.bg, .{ .x = 4, .y = 2 }).?);
|
try testing.expectEqual(cell2, c.bg_rows.lists[2].items[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
test "Contents clear last added content" {
|
test "Contents clear last added content" {
|
||||||
@ -402,7 +323,7 @@ test "Contents clear last added content" {
|
|||||||
const rows = 10;
|
const rows = 10;
|
||||||
const cols = 10;
|
const cols = 10;
|
||||||
|
|
||||||
var c = try Contents.init(alloc);
|
var c: Contents = .{};
|
||||||
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
||||||
defer c.deinit(alloc);
|
defer c.deinit(alloc);
|
||||||
|
|
||||||
@ -419,63 +340,10 @@ test "Contents clear last added content" {
|
|||||||
.cell_width = 1,
|
.cell_width = 1,
|
||||||
.color = .{ 0, 0, 0, 1 },
|
.color = .{ 0, 0, 0, 1 },
|
||||||
};
|
};
|
||||||
try c.set(alloc, .bg, cell1);
|
try c.add(alloc, .bg, cell1);
|
||||||
try c.set(alloc, .bg, cell2);
|
try c.add(alloc, .bg, cell2);
|
||||||
c.clear(2);
|
c.clear(2);
|
||||||
|
|
||||||
// Row 2 should still be valid.
|
// Row 1 should still contain its cell.
|
||||||
try testing.expectEqual(cell1, c.get(.bg, .{ .x = 4, .y = 1 }).?);
|
try testing.expectEqual(cell1, c.bg_rows.lists[1].items[0]);
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user