mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-10-08 10:56:34 +00:00
renderer/Metal: ArrayList cell Contents rows
This will allow for unlimited glyphs per row, eliminating the issue run in to with multi-substitution glyphs and combining characters which can result in more glyphs in a row than there are columns.
This commit is contained in:
@@ -1036,11 +1036,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.bgs.pools);
|
||||||
try frame.cells.sync(self.gpu_state.device, cells_fg);
|
const fg_count = try frame.cells.syncFromArrayLists(self.gpu_state.device, self.cells.text.pools);
|
||||||
|
|
||||||
// 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 +1137,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..]);
|
||||||
|
@@ -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,161 +24,84 @@ 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 collection of ArrayLists with methods for bulk operations.
|
||||||
|
fn PooledArrayList(comptime T: type) type {
|
||||||
|
return struct {
|
||||||
|
pools: []std.ArrayListUnmanaged(T),
|
||||||
|
|
||||||
|
pub fn init(alloc: Allocator, pool_count: usize) !PooledArrayList(T) {
|
||||||
|
var self: PooledArrayList(T) = .{
|
||||||
|
.pools = try alloc.alloc(std.ArrayListUnmanaged(T), pool_count),
|
||||||
|
};
|
||||||
|
|
||||||
|
for (self.pools) |*list| {
|
||||||
|
list.* = .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
self.reset();
|
||||||
|
|
||||||
|
return self;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn deinit(self: *PooledArrayList(T), alloc: Allocator) void {
|
||||||
|
for (self.pools) |*list| {
|
||||||
|
list.deinit(alloc);
|
||||||
|
}
|
||||||
|
alloc.free(self.pools);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reset all pools to an empty state without freeing or resizing.
|
||||||
|
pub fn reset(self: *PooledArrayList(T)) void {
|
||||||
|
for (self.pools) |*list| {
|
||||||
|
list.clearRetainingCapacity();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Change the pool count and clear the contents of all pools.
|
||||||
|
pub fn resize(self: *PooledArrayList(T), alloc: Allocator, pool_count: u16) !void {
|
||||||
|
const pools = try alloc.alloc(std.ArrayListUnmanaged(T), pool_count);
|
||||||
|
errdefer alloc.free(pools);
|
||||||
|
|
||||||
|
alloc.free(self.pools);
|
||||||
|
|
||||||
|
self.pools = pools;
|
||||||
|
|
||||||
|
for (self.pools) |*list| {
|
||||||
|
list.* = .{};
|
||||||
|
}
|
||||||
|
|
||||||
|
self.reset();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/// 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
|
||||||
/// 1. Setting the contents of a cell by coordinate. More specifically,
|
/// tracking to eliminate the overhead of rebuilding the GPU buffers
|
||||||
/// we want to be efficient setting cell contents by row since we
|
/// each frame.
|
||||||
/// 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 per row
|
|
||||||
/// that map to the index in the GPU vertex array that the content is at.
|
|
||||||
pub const Contents = struct {
|
pub const Contents = struct {
|
||||||
const Map = struct {
|
|
||||||
/// The rows of index mappings are stored in a single contiguous array
|
|
||||||
/// where the start of each row can be direct indexed by its y coord,
|
|
||||||
/// and the used length of each row's section is stored separately.
|
|
||||||
rows: []u32,
|
|
||||||
|
|
||||||
/// The used length for each row.
|
|
||||||
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
|
|
||||||
/// map array index from a coordinate.
|
|
||||||
size: renderer.GridSize,
|
size: renderer.GridSize,
|
||||||
|
|
||||||
/// The actual GPU data (on the CPU) for all the cells in the terminal.
|
bgs: PooledArrayList(mtl_shaders.CellBg),
|
||||||
/// This only contains the cells that have content set. To determine
|
text: PooledArrayList(mtl_shaders.CellText),
|
||||||
/// 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),
|
|
||||||
|
|
||||||
/// 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
|
|
||||||
/// 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 {
|
pub fn init(alloc: Allocator) !Contents {
|
||||||
var result: Contents = .{
|
const result: Contents = .{
|
||||||
.size = .{ .rows = 0, .columns = 0 },
|
.size = .{ .rows = 0, .columns = 0 },
|
||||||
.bgs = .{},
|
.bgs = try PooledArrayList(mtl_shaders.CellBg).init(alloc, 0),
|
||||||
.text = .{},
|
.text = try PooledArrayList(mtl_shaders.CellText).init(alloc, 0),
|
||||||
.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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn deinit(self: *Contents, alloc: Allocator) void {
|
pub fn deinit(self: *Contents, alloc: Allocator) void {
|
||||||
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
|
||||||
@@ -189,52 +112,26 @@ pub const Contents = struct {
|
|||||||
size: renderer.GridSize,
|
size: renderer.GridSize,
|
||||||
) !void {
|
) !void {
|
||||||
self.size = size;
|
self.size = size;
|
||||||
self.bgs.clearAndFree(alloc);
|
try self.bgs.resize(alloc, size.rows);
|
||||||
self.text.shrinkAndFree(alloc, text_reserved_len);
|
try self.text.resize(alloc, size.rows + 1);
|
||||||
|
|
||||||
self.bg_map.deinit(alloc);
|
// Make sure we don't have to allocate for the cursor cell.
|
||||||
self.tx_map.deinit(alloc);
|
try self.text.pools[0].ensureTotalCapacity(alloc, 1);
|
||||||
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 {
|
||||||
self.bgs.clearRetainingCapacity();
|
self.bgs.reset();
|
||||||
self.text.shrinkRetainingCapacity(text_reserved_len);
|
self.text.reset();
|
||||||
|
|
||||||
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.
|
/// 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.text.pools[0].clearRetainingCapacity();
|
||||||
self.cursor = false;
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
self.cursor = true;
|
if (v) |cell| {
|
||||||
self.text.items[0] = cell;
|
self.text.pools[0].appendAssumeCapacity(cell);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a cell to the appropriate list. Adding the same cell twice will
|
/// Add a cell to the appropriate list. Adding the same cell twice will
|
||||||
@@ -246,98 +143,29 @@ pub const Contents = struct {
|
|||||||
comptime key: Key,
|
comptime key: Key,
|
||||||
cell: key.CellType(),
|
cell: key.CellType(),
|
||||||
) !void {
|
) !void {
|
||||||
// Get our list of cells based on the key (comptime).
|
const y = cell.grid_pos[1];
|
||||||
const list = &@field(self, switch (key) {
|
|
||||||
.bg => "bgs",
|
|
||||||
.text, .underline, .strikethrough => "text",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add a new cell to the list.
|
switch (key) {
|
||||||
const idx: u32 = @intCast(list.items.len);
|
.bg
|
||||||
try list.append(alloc, cell);
|
=> try self.bgs.pools[y].append(alloc, cell),
|
||||||
|
|
||||||
// And to the appropriate mapping.
|
.text,
|
||||||
self.getMap(key).add(cell.grid_pos[1], idx);
|
.underline,
|
||||||
|
.strikethrough
|
||||||
|
// We have a special pool containing the cursor cell at the start
|
||||||
|
// of our text pool list, so we need to add 1 to the y to get the
|
||||||
|
// correct index.
|
||||||
|
=> try self.text.pools[y + 1].append(alloc, cell),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear all of the cell contents for a given row.
|
/// Clear all of the cell contents for a given row.
|
||||||
pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
|
pub fn clear(self: *Contents, y: terminal.size.CellCountInt) void {
|
||||||
inline for (std.meta.fields(Key)) |field| {
|
self.bgs.pools[y].clearRetainingCapacity();
|
||||||
const key: Key = @enumFromInt(field.value);
|
// We have a special pool containing the cursor cell at the start
|
||||||
// Get our list of cells based on the key (comptime).
|
// of our text pool list, so we need to add 1 to the y to get the
|
||||||
const list = &@field(self, switch (key) {
|
// correct index.
|
||||||
.bg => "bgs",
|
self.text.pools[y + 1].clearRetainingCapacity();
|
||||||
.text, .underline, .strikethrough => "text",
|
|
||||||
});
|
|
||||||
|
|
||||||
const map = self.getMap(key);
|
|
||||||
|
|
||||||
const start = y * map.row_size;
|
|
||||||
|
|
||||||
// We iterate from the end of the row because this makes it more
|
|
||||||
// likely that we remove from the end of the list, which results
|
|
||||||
// in not having to re-map anything.
|
|
||||||
while (map.lens[y] > 0) {
|
|
||||||
map.lens[y] -= 1;
|
|
||||||
const i = start + map.lens[y];
|
|
||||||
const idx = map.rows[i];
|
|
||||||
|
|
||||||
_ = list.swapRemove(idx);
|
|
||||||
|
|
||||||
// If we took this cell off the end of the arraylist then
|
|
||||||
// we won't need to re-map anything.
|
|
||||||
if (idx == list.items.len) continue;
|
|
||||||
|
|
||||||
const new = list.items[idx];
|
|
||||||
const new_y = new.grid_pos[1];
|
|
||||||
|
|
||||||
// The cell contents that were moved need to be remapped so
|
|
||||||
// we don't lose track of them.
|
|
||||||
switch (key) {
|
|
||||||
.bg => self.remapBgs(new_y, idx),
|
|
||||||
.text, .underline, .strikethrough => self.remapText(new_y, idx),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remapText(self: *Contents, row: u16, idx: u32) void {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remapBgs(self: *Contents, row: u16, idx: u32) void {
|
|
||||||
for (self.bg_map.getRow(row)) |*new_idx| {
|
|
||||||
if (new_idx.* == self.bgs.items.len) {
|
|
||||||
new_idx.* = idx;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user