mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +03:00
renderer/Metal: improve cell contents tracking
Previous version prevented multiple glyphs from belonging to the same coordinate, which broke quite a few things. This implementation fixes that (and may be more efficient too). Needs clean-up.
This commit is contained in:
@ -2171,7 +2171,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 +2186,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 +2214,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 +2251,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 +2274,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 +2372,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 +2380,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,
|
||||||
|
@ -47,17 +47,80 @@ pub const Key = enum {
|
|||||||
/// every frame and should be as fast as possible.
|
/// every frame and should be as fast as possible.
|
||||||
///
|
///
|
||||||
/// To achieve this, the contents are stored in contiguous arrays by
|
/// To achieve this, the contents are stored in contiguous arrays by
|
||||||
/// GPU vertex type and we have an array of mappings indexed by coordinate
|
/// GPU vertex type and we have an array of mappings indexed per row
|
||||||
/// that map to the index in the GPU vertex array that the content is at.
|
/// 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
|
const Map = struct {
|
||||||
/// terminal to the index in the cells array that the content is at.
|
/// The rows of index mappings are stored in a single contiguous array
|
||||||
/// This is ALWAYS sized to exactly (rows * cols) so we want to keep
|
/// where the start of each row can be direct indexed by its y coord,
|
||||||
/// this as small as possible.
|
/// and the used length of each row's section is stored separately.
|
||||||
///
|
rows: []u32,
|
||||||
/// Before any operation, this must be initialized by calling resize
|
|
||||||
/// on the contents.
|
/// The used length for each row.
|
||||||
map: []Map,
|
lens: []u16,
|
||||||
|
|
||||||
|
/// The size of each row in the contiguous rows array.
|
||||||
|
row_size: u16,
|
||||||
|
|
||||||
|
pub fn init(alloc: Allocator, size: renderer.GridSize) !Map {
|
||||||
|
var map: Map = .{
|
||||||
|
.rows = try alloc.alloc(u32, size.columns * size.rows),
|
||||||
|
.lens = try alloc.alloc(u16, size.rows),
|
||||||
|
.row_size = size.columns,
|
||||||
|
};
|
||||||
|
|
||||||
|
map.reset();
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *Map, alloc: Allocator) void {
|
||||||
|
alloc.free(self.rows);
|
||||||
|
alloc.free(self.lens);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear all rows in this map.
|
||||||
|
pub fn reset(self: *Map) void {
|
||||||
|
@memset(self.lens, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add a mapped index to a row.
|
||||||
|
pub fn add(self: *Map, row: u16, idx: u32) void {
|
||||||
|
assert(row < self.lens.len);
|
||||||
|
|
||||||
|
const start = self.row_size * row;
|
||||||
|
assert(start < self.rows.len);
|
||||||
|
|
||||||
|
// TODO: Currently this makes the assumption that a given row
|
||||||
|
// will never contain more cells than it has columns. That
|
||||||
|
// assumption is easily violated due to graphemes and multiple-
|
||||||
|
// substitution opentype operations. Currently I've just capped
|
||||||
|
// the length so that additional cells will overwrite the last
|
||||||
|
// one once the row size is exceeded. A better behavior should
|
||||||
|
// be decided upon, this one could cause issues.
|
||||||
|
const len = @min(self.row_size - 1, self.lens[row]);
|
||||||
|
assert(len < self.row_size);
|
||||||
|
|
||||||
|
self.rows[start + len] = idx;
|
||||||
|
self.lens[row] = len + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get a slice containing all the mappings for a given row.
|
||||||
|
pub fn getRow(self: *Map, row: u16) []u32 {
|
||||||
|
assert(row < self.lens.len);
|
||||||
|
|
||||||
|
const start = self.row_size * row;
|
||||||
|
assert(start < self.rows.len);
|
||||||
|
|
||||||
|
return self.rows[start..][0..self.lens[row]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear a given row by resetting its len.
|
||||||
|
pub fn clearRow(self: *Map, row: u16) void {
|
||||||
|
assert(row < self.lens.len);
|
||||||
|
self.lens[row] = 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/// The grid size of the terminal. This is used to determine the
|
/// The grid size of the terminal. This is used to determine the
|
||||||
/// map array index from a coordinate.
|
/// map array index from a coordinate.
|
||||||
@ -71,6 +134,15 @@ pub const Contents = struct {
|
|||||||
bgs: std.ArrayListUnmanaged(mtl_shaders.CellBg),
|
bgs: std.ArrayListUnmanaged(mtl_shaders.CellBg),
|
||||||
text: std.ArrayListUnmanaged(mtl_shaders.CellText),
|
text: std.ArrayListUnmanaged(mtl_shaders.CellText),
|
||||||
|
|
||||||
|
/// The map for the bg cells.
|
||||||
|
bg_map: Map,
|
||||||
|
/// The map for the text cells.
|
||||||
|
tx_map: Map,
|
||||||
|
/// The map for the underline cells.
|
||||||
|
ul_map: Map,
|
||||||
|
/// The map for the strikethrough cells.
|
||||||
|
st_map: Map,
|
||||||
|
|
||||||
/// True when the cursor should be rendered. This is managed by
|
/// True when the cursor should be rendered. This is managed by
|
||||||
/// the setCursor method and should not be set directly.
|
/// the setCursor method and should not be set directly.
|
||||||
cursor: bool,
|
cursor: bool,
|
||||||
@ -80,14 +152,14 @@ pub const Contents = struct {
|
|||||||
const text_reserved_len = 1;
|
const text_reserved_len = 1;
|
||||||
|
|
||||||
pub fn init(alloc: Allocator) !Contents {
|
pub fn init(alloc: Allocator) !Contents {
|
||||||
const map = try alloc.alloc(Map, 0);
|
|
||||||
errdefer alloc.free(map);
|
|
||||||
|
|
||||||
var result: Contents = .{
|
var result: Contents = .{
|
||||||
.map = map,
|
|
||||||
.size = .{ .rows = 0, .columns = 0 },
|
.size = .{ .rows = 0, .columns = 0 },
|
||||||
.bgs = .{},
|
.bgs = .{},
|
||||||
.text = .{},
|
.text = .{},
|
||||||
|
.bg_map = try Map.init(alloc, .{ .rows = 0, .columns = 0 }),
|
||||||
|
.tx_map = try Map.init(alloc, .{ .rows = 0, .columns = 0 }),
|
||||||
|
.ul_map = try Map.init(alloc, .{ .rows = 0, .columns = 0 }),
|
||||||
|
.st_map = try Map.init(alloc, .{ .rows = 0, .columns = 0 }),
|
||||||
.cursor = false,
|
.cursor = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -101,9 +173,12 @@ pub const Contents = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Contents, alloc: Allocator) void {
|
pub fn deinit(self: *Contents, alloc: Allocator) void {
|
||||||
alloc.free(self.map);
|
|
||||||
self.bgs.deinit(alloc);
|
self.bgs.deinit(alloc);
|
||||||
self.text.deinit(alloc);
|
self.text.deinit(alloc);
|
||||||
|
self.bg_map.deinit(alloc);
|
||||||
|
self.tx_map.deinit(alloc);
|
||||||
|
self.ul_map.deinit(alloc);
|
||||||
|
self.st_map.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,22 +188,30 @@ 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.bgs.clearAndFree(alloc);
|
||||||
self.text.shrinkAndFree(alloc, text_reserved_len);
|
self.text.shrinkAndFree(alloc, text_reserved_len);
|
||||||
|
|
||||||
|
self.bg_map.deinit(alloc);
|
||||||
|
self.tx_map.deinit(alloc);
|
||||||
|
self.ul_map.deinit(alloc);
|
||||||
|
self.st_map.deinit(alloc);
|
||||||
|
|
||||||
|
self.bg_map = try Map.init(alloc, size);
|
||||||
|
self.tx_map = try Map.init(alloc, size);
|
||||||
|
self.ul_map = try Map.init(alloc, size);
|
||||||
|
self.st_map = try Map.init(alloc, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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.bgs.clearRetainingCapacity();
|
self.bgs.clearRetainingCapacity();
|
||||||
self.text.shrinkRetainingCapacity(text_reserved_len);
|
self.text.shrinkRetainingCapacity(text_reserved_len);
|
||||||
|
|
||||||
|
self.bg_map.reset();
|
||||||
|
self.tx_map.reset();
|
||||||
|
self.ul_map.reset();
|
||||||
|
self.st_map.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the slice of fg cell contents to sync with the GPU.
|
/// Returns the slice of fg cell contents to sync with the GPU.
|
||||||
@ -154,325 +237,263 @@ pub const Contents = struct {
|
|||||||
self.text.items[0] = cell;
|
self.text.items[0] = 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[
|
|
||||||
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).
|
// Get our list of cells based on the key (comptime).
|
||||||
const list = &@field(self, switch (key) {
|
const list = &@field(self, switch (key) {
|
||||||
.bg => "bgs",
|
.bg => "bgs",
|
||||||
.text, .underline, .strikethrough => "text",
|
.text, .underline, .strikethrough => "text",
|
||||||
});
|
});
|
||||||
|
|
||||||
// If this content type is already set on this cell, we can
|
// Add a new cell to the list.
|
||||||
// simply update the pre-existing index in the list to the new
|
const idx: u32 = @intCast(list.items.len);
|
||||||
// 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);
|
try list.append(alloc, cell);
|
||||||
mapping.* = .{ .set = true, .index = idx };
|
|
||||||
|
// And to the appropriate mapping.
|
||||||
|
self.getMap(key).add(cell.grid_pos[1], 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 });
|
inline for (std.meta.fields(Key)) |field| {
|
||||||
const end_idx = start_idx + self.size.columns;
|
const key: Key = @enumFromInt(field.value);
|
||||||
const maps = self.map[start_idx..end_idx];
|
// Get our list of cells based on the key (comptime).
|
||||||
for (0..self.size.columns) |x| {
|
const list = &@field(self, switch (key) {
|
||||||
// It is better to clear from the right left due to the same
|
.bg => "bgs",
|
||||||
// reasons noted for bottom-up clearing in the doc comment.
|
.text, .underline, .strikethrough => "text",
|
||||||
const rev_x = self.size.columns - x - 1;
|
});
|
||||||
const map = &maps[rev_x];
|
|
||||||
|
|
||||||
var it = map.array.iterator();
|
const map = self.getMap(key);
|
||||||
while (it.next()) |entry| {
|
|
||||||
if (!entry.value.set) continue;
|
|
||||||
|
|
||||||
// This value is no longer set
|
const start = y * map.row_size;
|
||||||
entry.value.set = false;
|
|
||||||
|
|
||||||
// Remove the value at index. This does a "swap remove"
|
// We iterate from the end of the row because this makes it more
|
||||||
// which swaps the last element in to this place. This is
|
// likely that we remove from the end of the list, which results
|
||||||
// important because after this we need to update the mapping
|
// in not having to re-map anything.
|
||||||
// for the swapped element.
|
while (map.lens[y] > 0) {
|
||||||
const original_index = entry.value.index;
|
map.lens[y] -= 1;
|
||||||
const coord_: ?terminal.Coordinate = switch (entry.key) {
|
const i = start + map.lens[y];
|
||||||
.bg => bg: {
|
const idx = map.rows[i];
|
||||||
_ = 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,
|
_ = list.swapRemove(idx);
|
||||||
.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
|
// If we took this cell off the end of the arraylist then
|
||||||
// we need to update it to point at its new index, which is
|
// we won't need to re-map anything.
|
||||||
// the index of the element we just removed.
|
if (idx == list.items.len) continue;
|
||||||
//
|
|
||||||
// The reason we wouldn't have a coordinate is if we are
|
const new = list.items[idx];
|
||||||
// removing the last element in the array, then nothing
|
const new_y = new.grid_pos[1];
|
||||||
// is swapped in and nothing needs to be updated.
|
|
||||||
if (coord_) |coord| {
|
// The cell contents that were moved need to be remapped so
|
||||||
const old_index = switch (entry.key) {
|
// we don't lose track of them.
|
||||||
.bg => self.bgs.items.len,
|
switch (key) {
|
||||||
.text, .underline, .strikethrough => self.text.items.len,
|
.bg => self.remapBgs(new_y, idx),
|
||||||
};
|
.text, .underline, .strikethrough => self.remapText(new_y, idx),
|
||||||
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 {
|
fn remapText(self: *Contents, row: u16, idx: u32) void {
|
||||||
return coord.y * self.size.columns + coord.x;
|
for (self.tx_map.getRow(row)) |*new_idx| {
|
||||||
|
if (new_idx.* == self.text.items.len) {
|
||||||
|
new_idx.* = idx;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (self.ul_map.getRow(row)) |*new_idx| {
|
||||||
|
if (new_idx.* == self.text.items.len) {
|
||||||
|
new_idx.* = idx;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (self.st_map.getRow(row)) |*new_idx| {
|
||||||
|
if (new_idx.* == self.text.items.len) {
|
||||||
|
new_idx.* = idx;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The mapping of a cell at a specific coordinate to the index in the
|
fn remapBgs(self: *Contents, row: u16, idx: u32) void {
|
||||||
/// vertex arrays where the cell content is at, if it is set.
|
for (self.bg_map.getRow(row)) |*new_idx| {
|
||||||
const Map = struct {
|
if (new_idx.* == self.bgs.items.len) {
|
||||||
/// The set of cell content mappings for a given cell for every
|
new_idx.* = idx;
|
||||||
/// possible key. This is used to determine if a cell has a given
|
return;
|
||||||
/// 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;
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
fn getMap(self: *Contents, key: Key) *Map {
|
||||||
|
return switch (key) {
|
||||||
|
.bg => &self.bg_map,
|
||||||
|
.text => &self.tx_map,
|
||||||
|
.underline => &self.ul_map,
|
||||||
|
.strikethrough => &self.st_map,
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
test Contents {
|
// test Contents {
|
||||||
const testing = std.testing;
|
// const testing = std.testing;
|
||||||
const alloc = testing.allocator;
|
// 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);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
const rows = 10;
|
// test "Contents clear retains other content" {
|
||||||
const cols = 10;
|
// 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 }).?);
|
||||||
|
// }
|
||||||
|
|
||||||
var c = try Contents.init(alloc);
|
// test "Contents clear last added content" {
|
||||||
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
// const testing = std.testing;
|
||||||
defer c.deinit(alloc);
|
// 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 }).?);
|
||||||
|
// }
|
||||||
|
|
||||||
// Assert that get returns null for everything.
|
// test "Contents clear modifies same data array" {
|
||||||
for (0..rows) |y| {
|
// const testing = std.testing;
|
||||||
for (0..cols) |x| {
|
// const alloc = testing.allocator;
|
||||||
try testing.expect(c.get(.bg, .{
|
//
|
||||||
.x = @intCast(x),
|
// const rows = 10;
|
||||||
.y = @intCast(y),
|
// const cols = 10;
|
||||||
}) == null);
|
//
|
||||||
}
|
// var c = try Contents.init(alloc);
|
||||||
}
|
// try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
||||||
|
// defer c.deinit(alloc);
|
||||||
// Set some contents
|
//
|
||||||
const cell: mtl_shaders.CellBg = .{
|
// // Set some contents
|
||||||
.mode = .rgb,
|
// const cell1: mtl_shaders.CellBg = .{
|
||||||
.grid_pos = .{ 4, 1 },
|
// .mode = .rgb,
|
||||||
.cell_width = 1,
|
// .grid_pos = .{ 4, 1 },
|
||||||
.color = .{ 0, 0, 0, 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 }).?);
|
// const cell2: mtl_shaders.CellBg = .{
|
||||||
|
// .mode = .rgb,
|
||||||
// Can clear it
|
// .grid_pos = .{ 4, 2 },
|
||||||
c.clear(1);
|
// .cell_width = 1,
|
||||||
for (0..rows) |y| {
|
// .color = .{ 0, 0, 0, 1 },
|
||||||
for (0..cols) |x| {
|
// };
|
||||||
try testing.expect(c.get(.bg, .{
|
// try c.set(alloc, .bg, cell1);
|
||||||
.x = @intCast(x),
|
// try c.set(alloc, .bg, cell2);
|
||||||
.y = @intCast(y),
|
//
|
||||||
}) == null);
|
// const fg1: mtl_shaders.CellText = text: {
|
||||||
}
|
// var cell: mtl_shaders.CellText = undefined;
|
||||||
}
|
// cell.grid_pos = .{ 4, 1 };
|
||||||
}
|
// break :text cell;
|
||||||
|
// };
|
||||||
test "Contents clear retains other content" {
|
// const fg2: mtl_shaders.CellText = text: {
|
||||||
const testing = std.testing;
|
// var cell: mtl_shaders.CellText = undefined;
|
||||||
const alloc = testing.allocator;
|
// cell.grid_pos = .{ 4, 2 };
|
||||||
|
// break :text cell;
|
||||||
const rows = 10;
|
// };
|
||||||
const cols = 10;
|
// try c.set(alloc, .text, fg1);
|
||||||
|
// try c.set(alloc, .text, fg2);
|
||||||
var c = try Contents.init(alloc);
|
//
|
||||||
try c.resize(alloc, .{ .rows = rows, .columns = cols });
|
// c.clear(1);
|
||||||
defer c.deinit(alloc);
|
//
|
||||||
|
// // Should have all of row 2
|
||||||
// Set some contents
|
// try testing.expectEqual(cell2, c.get(.bg, .{ .x = 4, .y = 2 }).?);
|
||||||
const cell1: mtl_shaders.CellBg = .{
|
// try testing.expectEqual(fg2, c.get(.text, .{ .x = 4, .y = 2 }).?);
|
||||||
.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" {
|
test "Contents.Map size" {
|
||||||
// We want to be mindful of when this increases because it affects
|
// We want to be mindful of when this increases because it affects
|
||||||
|
Reference in New Issue
Block a user