mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-14 15:56:13 +03:00
Merge pull request #2015 from ghostty-org/kitty-unicode
Kitty Graphics Unicode Placeholders
This commit is contained in:
@ -228,6 +228,12 @@ pub const RunIterator = struct {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If we're a Kitty unicode placeholder then we add a blank.
|
||||
if (cell.codepoint() == terminal.kitty.graphics.unicode.placeholder) {
|
||||
try self.addCodepoint(&hasher, ' ', @intCast(cluster));
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add all the codepoints for our grapheme
|
||||
try self.addCodepoint(
|
||||
&hasher,
|
||||
@ -284,8 +290,20 @@ pub const RunIterator = struct {
|
||||
style: font.Style,
|
||||
presentation: ?font.Presentation,
|
||||
) !?font.Collection.Index {
|
||||
if (cell.isEmpty() or
|
||||
cell.codepoint() == 0 or
|
||||
cell.codepoint() == terminal.kitty.graphics.unicode.placeholder)
|
||||
{
|
||||
return try self.grid.getIndex(
|
||||
alloc,
|
||||
' ',
|
||||
style,
|
||||
presentation,
|
||||
);
|
||||
}
|
||||
|
||||
// Get the font index for the primary codepoint.
|
||||
const primary_cp: u32 = if (cell.isEmpty() or cell.codepoint() == 0) ' ' else cell.codepoint();
|
||||
const primary_cp: u32 = cell.codepoint();
|
||||
const primary = try self.grid.getIndex(
|
||||
alloc,
|
||||
primary_cp,
|
||||
|
@ -124,6 +124,7 @@ images: ImageMap = .{},
|
||||
image_placements: ImagePlacementList = .{},
|
||||
image_bg_end: u32 = 0,
|
||||
image_text_end: u32 = 0,
|
||||
image_virtual: bool = false,
|
||||
|
||||
/// Metal state
|
||||
shaders: Shaders, // Compiled shaders
|
||||
@ -939,7 +940,13 @@ pub fn updateFrame(
|
||||
// If we have Kitty graphics data, we enter a SLOW SLOW SLOW path.
|
||||
// We only do this if the Kitty image state is dirty meaning only if
|
||||
// it changes.
|
||||
if (state.terminal.screen.kitty_images.dirty) {
|
||||
//
|
||||
// If we have any virtual references, we must also rebuild our
|
||||
// kitty state on every frame because any cell change can move
|
||||
// an image.
|
||||
if (state.terminal.screen.kitty_images.dirty or
|
||||
self.image_virtual)
|
||||
{
|
||||
try self.prepKittyGraphics(state.terminal);
|
||||
}
|
||||
|
||||
@ -1596,6 +1603,7 @@ fn prepKittyGraphics(
|
||||
// We always clear our previous placements no matter what because
|
||||
// we rebuild them from scratch.
|
||||
self.image_placements.clearRetainingCapacity();
|
||||
self.image_virtual = false;
|
||||
|
||||
// Go through our known images and if there are any that are no longer
|
||||
// in use then mark them to be freed.
|
||||
@ -1619,8 +1627,25 @@ fn prepKittyGraphics(
|
||||
// Go through the placements and ensure the image is loaded on the GPU.
|
||||
var it = storage.placements.iterator();
|
||||
while (it.next()) |kv| {
|
||||
// Find the image in storage
|
||||
const p = kv.value_ptr;
|
||||
|
||||
// Special logic based on location
|
||||
switch (p.location) {
|
||||
.pin => {},
|
||||
.virtual => {
|
||||
// We need to mark virtual placements on our renderer so that
|
||||
// we know to rebuild in more scenarios since cell changes can
|
||||
// now trigger placement changes.
|
||||
self.image_virtual = true;
|
||||
|
||||
// We also continue out because virtual placements are
|
||||
// only triggered by the unicode placeholder, not by the
|
||||
// placement itself.
|
||||
continue;
|
||||
},
|
||||
}
|
||||
|
||||
// Get the image for the placement
|
||||
const image = storage.imageById(kv.key_ptr.image_id) orelse {
|
||||
log.warn(
|
||||
"missing image for placement, ignoring image_id={}",
|
||||
@ -1629,100 +1654,16 @@ fn prepKittyGraphics(
|
||||
continue;
|
||||
};
|
||||
|
||||
// If the selection isn't within our viewport then skip it.
|
||||
const rect = p.rect(image, t);
|
||||
if (bot.before(rect.top_left)) continue;
|
||||
if (rect.bottom_right.before(top)) continue;
|
||||
try self.prepKittyPlacement(t, &top, &bot, &image, p);
|
||||
}
|
||||
|
||||
// If the top left is outside the viewport we need to calc an offset
|
||||
// so that we render (0, 0) with some offset for the texture.
|
||||
const offset_y: u32 = if (rect.top_left.before(top)) offset_y: {
|
||||
const vp_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y;
|
||||
const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
|
||||
const offset_cells = vp_y - img_y;
|
||||
const offset_pixels = offset_cells * self.grid_metrics.cell_height;
|
||||
break :offset_y @intCast(offset_pixels);
|
||||
} else 0;
|
||||
|
||||
// We need to prep this image for upload if it isn't in the cache OR
|
||||
// it is in the cache but the transmit time doesn't match meaning this
|
||||
// image is different.
|
||||
const gop = try self.images.getOrPut(self.alloc, kv.key_ptr.image_id);
|
||||
if (!gop.found_existing or
|
||||
gop.value_ptr.transmit_time.order(image.transmit_time) != .eq)
|
||||
{
|
||||
// Copy the data into the pending state.
|
||||
const data = try self.alloc.dupe(u8, image.data);
|
||||
errdefer self.alloc.free(data);
|
||||
|
||||
// Store it in the map
|
||||
const pending: Image.Pending = .{
|
||||
.width = image.width,
|
||||
.height = image.height,
|
||||
.data = data.ptr,
|
||||
};
|
||||
|
||||
const new_image: Image = switch (image.format) {
|
||||
.grey_alpha => .{ .pending_grey_alpha = pending },
|
||||
.rgb => .{ .pending_rgb = pending },
|
||||
.rgba => .{ .pending_rgba = pending },
|
||||
.png => unreachable, // should be decoded by now
|
||||
};
|
||||
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = .{
|
||||
.image = new_image,
|
||||
.transmit_time = undefined,
|
||||
};
|
||||
} else {
|
||||
try gop.value_ptr.image.markForReplace(
|
||||
self.alloc,
|
||||
new_image,
|
||||
);
|
||||
}
|
||||
|
||||
gop.value_ptr.transmit_time = image.transmit_time;
|
||||
}
|
||||
|
||||
// Convert our screen point to a viewport point
|
||||
const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
|
||||
.viewport,
|
||||
p.pin.*,
|
||||
) orelse .{ .viewport = .{} };
|
||||
|
||||
// Calculate the source rectangle
|
||||
const source_x = @min(image.width, p.source_x);
|
||||
const source_y = @min(image.height, p.source_y + offset_y);
|
||||
const source_width = if (p.source_width > 0)
|
||||
@min(image.width - source_x, p.source_width)
|
||||
else
|
||||
image.width;
|
||||
const source_height = if (p.source_height > 0)
|
||||
@min(image.height, p.source_height)
|
||||
else
|
||||
image.height -| source_y;
|
||||
|
||||
// Calculate the width/height of our image.
|
||||
const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width;
|
||||
const dest_height = if (p.rows > 0) p.rows * self.grid_metrics.cell_height else source_height;
|
||||
|
||||
// Accumulate the placement
|
||||
if (image.width > 0 and image.height > 0) {
|
||||
try self.image_placements.append(self.alloc, .{
|
||||
.image_id = kv.key_ptr.image_id,
|
||||
.x = @intCast(p.pin.x),
|
||||
.y = @intCast(viewport.viewport.y),
|
||||
.z = p.z,
|
||||
.width = dest_width,
|
||||
.height = dest_height,
|
||||
.cell_offset_x = p.x_offset,
|
||||
.cell_offset_y = p.y_offset,
|
||||
.source_x = source_x,
|
||||
.source_y = source_y,
|
||||
.source_width = source_width,
|
||||
.source_height = source_height,
|
||||
});
|
||||
}
|
||||
// If we have virtual placements then we need to scan for placeholders.
|
||||
if (self.image_virtual) {
|
||||
var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot);
|
||||
while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement(
|
||||
t,
|
||||
&virtual_p,
|
||||
);
|
||||
}
|
||||
|
||||
// Sort the placements by their Z value.
|
||||
@ -1759,6 +1700,180 @@ fn prepKittyGraphics(
|
||||
}
|
||||
}
|
||||
|
||||
fn prepKittyVirtualPlacement(
|
||||
self: *Metal,
|
||||
t: *terminal.Terminal,
|
||||
p: *const terminal.kitty.graphics.unicode.Placement,
|
||||
) !void {
|
||||
const storage = &t.screen.kitty_images;
|
||||
const image = storage.imageById(p.image_id) orelse {
|
||||
log.warn(
|
||||
"missing image for virtual placement, ignoring image_id={}",
|
||||
.{p.image_id},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
const rp = p.renderPlacement(
|
||||
storage,
|
||||
&image,
|
||||
self.grid_metrics.cell_width,
|
||||
self.grid_metrics.cell_height,
|
||||
) catch |err| {
|
||||
log.warn("error rendering virtual placement err={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
// If our placement is zero sized then we don't do anything.
|
||||
if (rp.dest_width == 0 or rp.dest_height == 0) return;
|
||||
|
||||
const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
|
||||
.viewport,
|
||||
rp.top_left,
|
||||
) orelse {
|
||||
// This is unreachable with virtual placements because we should
|
||||
// only ever be looking at virtual placements that are in our
|
||||
// viewport in the renderer and virtual placements only ever take
|
||||
// up one row.
|
||||
unreachable;
|
||||
};
|
||||
|
||||
// Send our image to the GPU and store the placement for rendering.
|
||||
try self.prepKittyImage(&image);
|
||||
try self.image_placements.append(self.alloc, .{
|
||||
.image_id = image.id,
|
||||
.x = @intCast(rp.top_left.x),
|
||||
.y = @intCast(viewport.viewport.y),
|
||||
.z = -1,
|
||||
.width = rp.dest_width,
|
||||
.height = rp.dest_height,
|
||||
.cell_offset_x = rp.offset_x,
|
||||
.cell_offset_y = rp.offset_y,
|
||||
.source_x = rp.source_x,
|
||||
.source_y = rp.source_y,
|
||||
.source_width = rp.source_width,
|
||||
.source_height = rp.source_height,
|
||||
});
|
||||
}
|
||||
|
||||
fn prepKittyPlacement(
|
||||
self: *Metal,
|
||||
t: *terminal.Terminal,
|
||||
top: *const terminal.Pin,
|
||||
bot: *const terminal.Pin,
|
||||
image: *const terminal.kitty.graphics.Image,
|
||||
p: *const terminal.kitty.graphics.ImageStorage.Placement,
|
||||
) !void {
|
||||
// Get the rect for the placement. If this placement doesn't have
|
||||
// a rect then its virtual or something so skip it.
|
||||
const rect = p.rect(image.*, t) orelse return;
|
||||
|
||||
// If the selection isn't within our viewport then skip it.
|
||||
if (bot.before(rect.top_left)) return;
|
||||
if (rect.bottom_right.before(top.*)) return;
|
||||
|
||||
// If the top left is outside the viewport we need to calc an offset
|
||||
// so that we render (0, 0) with some offset for the texture.
|
||||
const offset_y: u32 = if (rect.top_left.before(top.*)) offset_y: {
|
||||
const vp_y = t.screen.pages.pointFromPin(.screen, top.*).?.screen.y;
|
||||
const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
|
||||
const offset_cells = vp_y - img_y;
|
||||
const offset_pixels = offset_cells * self.grid_metrics.cell_height;
|
||||
break :offset_y @intCast(offset_pixels);
|
||||
} else 0;
|
||||
|
||||
// We need to prep this image for upload if it isn't in the cache OR
|
||||
// it is in the cache but the transmit time doesn't match meaning this
|
||||
// image is different.
|
||||
try self.prepKittyImage(image);
|
||||
|
||||
// Convert our screen point to a viewport point
|
||||
const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
|
||||
.viewport,
|
||||
rect.top_left,
|
||||
) orelse .{ .viewport = .{} };
|
||||
|
||||
// Calculate the source rectangle
|
||||
const source_x = @min(image.width, p.source_x);
|
||||
const source_y = @min(image.height, p.source_y + offset_y);
|
||||
const source_width = if (p.source_width > 0)
|
||||
@min(image.width - source_x, p.source_width)
|
||||
else
|
||||
image.width;
|
||||
const source_height = if (p.source_height > 0)
|
||||
@min(image.height, p.source_height)
|
||||
else
|
||||
image.height -| source_y;
|
||||
|
||||
// Calculate the width/height of our image.
|
||||
const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width;
|
||||
const dest_height = if (p.rows > 0) p.rows * self.grid_metrics.cell_height else source_height;
|
||||
|
||||
// Accumulate the placement
|
||||
if (image.width > 0 and image.height > 0) {
|
||||
try self.image_placements.append(self.alloc, .{
|
||||
.image_id = image.id,
|
||||
.x = @intCast(rect.top_left.x),
|
||||
.y = @intCast(viewport.viewport.y),
|
||||
.z = p.z,
|
||||
.width = dest_width,
|
||||
.height = dest_height,
|
||||
.cell_offset_x = p.x_offset,
|
||||
.cell_offset_y = p.y_offset,
|
||||
.source_x = source_x,
|
||||
.source_y = source_y,
|
||||
.source_width = source_width,
|
||||
.source_height = source_height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn prepKittyImage(
|
||||
self: *Metal,
|
||||
image: *const terminal.kitty.graphics.Image,
|
||||
) !void {
|
||||
// If this image exists and its transmit time is the same we assume
|
||||
// it is the identical image so we don't need to send it to the GPU.
|
||||
const gop = try self.images.getOrPut(self.alloc, image.id);
|
||||
if (gop.found_existing and
|
||||
gop.value_ptr.transmit_time.order(image.transmit_time) == .eq)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy the data into the pending state.
|
||||
const data = try self.alloc.dupe(u8, image.data);
|
||||
errdefer self.alloc.free(data);
|
||||
|
||||
// Store it in the map
|
||||
const pending: Image.Pending = .{
|
||||
.width = image.width,
|
||||
.height = image.height,
|
||||
.data = data.ptr,
|
||||
};
|
||||
|
||||
const new_image: Image = switch (image.format) {
|
||||
.grey_alpha => .{ .pending_grey_alpha = pending },
|
||||
.rgb => .{ .pending_rgb = pending },
|
||||
.rgba => .{ .pending_rgba = pending },
|
||||
.png => unreachable, // should be decoded by now
|
||||
};
|
||||
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = .{
|
||||
.image = new_image,
|
||||
.transmit_time = undefined,
|
||||
};
|
||||
} else {
|
||||
try gop.value_ptr.image.markForReplace(
|
||||
self.alloc,
|
||||
new_image,
|
||||
);
|
||||
}
|
||||
|
||||
gop.value_ptr.transmit_time = image.transmit_time;
|
||||
}
|
||||
|
||||
/// Update the configuration.
|
||||
pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
|
||||
// We always redo the font shaper in case font features changed. We
|
||||
|
@ -120,6 +120,7 @@ images: ImageMap = .{},
|
||||
image_placements: ImagePlacementList = .{},
|
||||
image_bg_end: u32 = 0,
|
||||
image_text_end: u32 = 0,
|
||||
image_virtual: bool = false,
|
||||
|
||||
/// Defererred OpenGL operation to update the screen size.
|
||||
const SetScreenSize = struct {
|
||||
@ -693,7 +694,13 @@ pub fn updateFrame(
|
||||
// If we have Kitty graphics data, we enter a SLOW SLOW SLOW path.
|
||||
// We only do this if the Kitty image state is dirty meaning only if
|
||||
// it changes.
|
||||
if (state.terminal.screen.kitty_images.dirty) {
|
||||
//
|
||||
// If we have any virtual references, we must also rebuild our
|
||||
// kitty state on every frame because any cell change can move
|
||||
// an image.
|
||||
if (state.terminal.screen.kitty_images.dirty or
|
||||
self.image_virtual)
|
||||
{
|
||||
// prepKittyGraphics touches self.images which is also used
|
||||
// in drawFrame so if we're drawing on a separate thread we need
|
||||
// to lock this.
|
||||
@ -752,6 +759,7 @@ fn prepKittyGraphics(
|
||||
// We always clear our previous placements no matter what because
|
||||
// we rebuild them from scratch.
|
||||
self.image_placements.clearRetainingCapacity();
|
||||
self.image_virtual = false;
|
||||
|
||||
// Go through our known images and if there are any that are no longer
|
||||
// in use then mark them to be freed.
|
||||
@ -777,6 +785,23 @@ fn prepKittyGraphics(
|
||||
while (it.next()) |kv| {
|
||||
// Find the image in storage
|
||||
const p = kv.value_ptr;
|
||||
|
||||
// Special logic based on location
|
||||
switch (p.location) {
|
||||
.pin => {},
|
||||
.virtual => {
|
||||
// We need to mark virtual placements on our renderer so that
|
||||
// we know to rebuild in more scenarios since cell changes can
|
||||
// now trigger placement changes.
|
||||
self.image_virtual = true;
|
||||
|
||||
// We also continue out because virtual placements are
|
||||
// only triggered by the unicode placeholder, not by the
|
||||
// placement itself.
|
||||
continue;
|
||||
},
|
||||
}
|
||||
|
||||
const image = storage.imageById(kv.key_ptr.image_id) orelse {
|
||||
log.warn(
|
||||
"missing image for placement, ignoring image_id={}",
|
||||
@ -785,100 +810,16 @@ fn prepKittyGraphics(
|
||||
continue;
|
||||
};
|
||||
|
||||
// If the selection isn't within our viewport then skip it.
|
||||
const rect = p.rect(image, t);
|
||||
if (bot.before(rect.top_left)) continue;
|
||||
if (rect.bottom_right.before(top)) continue;
|
||||
try self.prepKittyPlacement(t, &top, &bot, &image, p);
|
||||
}
|
||||
|
||||
// If the top left is outside the viewport we need to calc an offset
|
||||
// so that we render (0, 0) with some offset for the texture.
|
||||
const offset_y: u32 = if (rect.top_left.before(top)) offset_y: {
|
||||
const vp_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y;
|
||||
const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
|
||||
const offset_cells = vp_y - img_y;
|
||||
const offset_pixels = offset_cells * self.grid_metrics.cell_height;
|
||||
break :offset_y @intCast(offset_pixels);
|
||||
} else 0;
|
||||
|
||||
// We need to prep this image for upload if it isn't in the cache OR
|
||||
// it is in the cache but the transmit time doesn't match meaning this
|
||||
// image is different.
|
||||
const gop = try self.images.getOrPut(self.alloc, kv.key_ptr.image_id);
|
||||
if (!gop.found_existing or
|
||||
gop.value_ptr.transmit_time.order(image.transmit_time) != .eq)
|
||||
{
|
||||
// Copy the data into the pending state.
|
||||
const data = try self.alloc.dupe(u8, image.data);
|
||||
errdefer self.alloc.free(data);
|
||||
|
||||
// Store it in the map
|
||||
const pending: Image.Pending = .{
|
||||
.width = image.width,
|
||||
.height = image.height,
|
||||
.data = data.ptr,
|
||||
};
|
||||
|
||||
const new_image: Image = switch (image.format) {
|
||||
.grey_alpha => .{ .pending_grey_alpha = pending },
|
||||
.rgb => .{ .pending_rgb = pending },
|
||||
.rgba => .{ .pending_rgba = pending },
|
||||
.png => unreachable, // should be decoded by now
|
||||
};
|
||||
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = .{
|
||||
.image = new_image,
|
||||
.transmit_time = undefined,
|
||||
};
|
||||
} else {
|
||||
try gop.value_ptr.image.markForReplace(
|
||||
self.alloc,
|
||||
new_image,
|
||||
);
|
||||
}
|
||||
|
||||
gop.value_ptr.transmit_time = image.transmit_time;
|
||||
}
|
||||
|
||||
// Convert our screen point to a viewport point
|
||||
const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
|
||||
.viewport,
|
||||
p.pin.*,
|
||||
) orelse .{ .viewport = .{} };
|
||||
|
||||
// Calculate the source rectangle
|
||||
const source_x = @min(image.width, p.source_x);
|
||||
const source_y = @min(image.height, p.source_y + offset_y);
|
||||
const source_width = if (p.source_width > 0)
|
||||
@min(image.width - source_x, p.source_width)
|
||||
else
|
||||
image.width;
|
||||
const source_height = if (p.source_height > 0)
|
||||
@min(image.height, p.source_height)
|
||||
else
|
||||
image.height -| source_y;
|
||||
|
||||
// Calculate the width/height of our image.
|
||||
const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width;
|
||||
const dest_height = if (p.rows > 0) p.rows * self.grid_metrics.cell_height else source_height;
|
||||
|
||||
// Accumulate the placement
|
||||
if (image.width > 0 and image.height > 0) {
|
||||
try self.image_placements.append(self.alloc, .{
|
||||
.image_id = kv.key_ptr.image_id,
|
||||
.x = @intCast(p.pin.x),
|
||||
.y = @intCast(viewport.viewport.y),
|
||||
.z = p.z,
|
||||
.width = dest_width,
|
||||
.height = dest_height,
|
||||
.cell_offset_x = p.x_offset,
|
||||
.cell_offset_y = p.y_offset,
|
||||
.source_x = source_x,
|
||||
.source_y = source_y,
|
||||
.source_width = source_width,
|
||||
.source_height = source_height,
|
||||
});
|
||||
}
|
||||
// If we have virtual placements then we need to scan for placeholders.
|
||||
if (self.image_virtual) {
|
||||
var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot);
|
||||
while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement(
|
||||
t,
|
||||
&virtual_p,
|
||||
);
|
||||
}
|
||||
|
||||
// Sort the placements by their Z value.
|
||||
@ -915,6 +856,181 @@ fn prepKittyGraphics(
|
||||
}
|
||||
}
|
||||
|
||||
fn prepKittyVirtualPlacement(
|
||||
self: *OpenGL,
|
||||
t: *terminal.Terminal,
|
||||
p: *const terminal.kitty.graphics.unicode.Placement,
|
||||
) !void {
|
||||
const storage = &t.screen.kitty_images;
|
||||
const image = storage.imageById(p.image_id) orelse {
|
||||
log.warn(
|
||||
"missing image for virtual placement, ignoring image_id={}",
|
||||
.{p.image_id},
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
const rp = p.renderPlacement(
|
||||
storage,
|
||||
&image,
|
||||
self.grid_metrics.cell_width,
|
||||
self.grid_metrics.cell_height,
|
||||
) catch |err| {
|
||||
log.warn("error rendering virtual placement err={}", .{err});
|
||||
return;
|
||||
};
|
||||
|
||||
// If our placement is zero sized then we don't do anything.
|
||||
if (rp.dest_width == 0 or rp.dest_height == 0) return;
|
||||
|
||||
const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
|
||||
.viewport,
|
||||
rp.top_left,
|
||||
) orelse {
|
||||
// This is unreachable with virtual placements because we should
|
||||
// only ever be looking at virtual placements that are in our
|
||||
// viewport in the renderer and virtual placements only ever take
|
||||
// up one row.
|
||||
unreachable;
|
||||
};
|
||||
|
||||
// Send our image to the GPU and store the placement for rendering.
|
||||
try self.prepKittyImage(&image);
|
||||
try self.image_placements.append(self.alloc, .{
|
||||
.image_id = image.id,
|
||||
.x = @intCast(rp.top_left.x),
|
||||
.y = @intCast(viewport.viewport.y),
|
||||
.z = -1,
|
||||
.width = rp.dest_width,
|
||||
.height = rp.dest_height,
|
||||
.cell_offset_x = rp.offset_x,
|
||||
.cell_offset_y = rp.offset_y,
|
||||
.source_x = rp.source_x,
|
||||
.source_y = rp.source_y,
|
||||
.source_width = rp.source_width,
|
||||
.source_height = rp.source_height,
|
||||
});
|
||||
}
|
||||
|
||||
fn prepKittyPlacement(
|
||||
self: *OpenGL,
|
||||
t: *terminal.Terminal,
|
||||
top: *const terminal.Pin,
|
||||
bot: *const terminal.Pin,
|
||||
image: *const terminal.kitty.graphics.Image,
|
||||
p: *const terminal.kitty.graphics.ImageStorage.Placement,
|
||||
) !void {
|
||||
// Get the rect for the placement. If this placement doesn't have
|
||||
// a rect then its virtual or something so skip it.
|
||||
const rect = p.rect(image.*, t) orelse return;
|
||||
|
||||
// If the selection isn't within our viewport then skip it.
|
||||
if (bot.before(rect.top_left)) return;
|
||||
if (rect.bottom_right.before(top.*)) return;
|
||||
|
||||
// If the top left is outside the viewport we need to calc an offset
|
||||
// so that we render (0, 0) with some offset for the texture.
|
||||
const offset_y: u32 = if (rect.top_left.before(top.*)) offset_y: {
|
||||
const vp_y = t.screen.pages.pointFromPin(.screen, top.*).?.screen.y;
|
||||
const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
|
||||
const offset_cells = vp_y - img_y;
|
||||
const offset_pixels = offset_cells * self.grid_metrics.cell_height;
|
||||
break :offset_y @intCast(offset_pixels);
|
||||
} else 0;
|
||||
|
||||
// We need to prep this image for upload if it isn't in the cache OR
|
||||
// it is in the cache but the transmit time doesn't match meaning this
|
||||
// image is different.
|
||||
try self.prepKittyImage(image);
|
||||
|
||||
// Convert our screen point to a viewport point
|
||||
const viewport: terminal.point.Point = t.screen.pages.pointFromPin(
|
||||
.viewport,
|
||||
rect.top_left,
|
||||
) orelse .{ .viewport = .{} };
|
||||
|
||||
// Calculate the source rectangle
|
||||
const source_x = @min(image.width, p.source_x);
|
||||
const source_y = @min(image.height, p.source_y + offset_y);
|
||||
const source_width = if (p.source_width > 0)
|
||||
@min(image.width - source_x, p.source_width)
|
||||
else
|
||||
image.width;
|
||||
const source_height = if (p.source_height > 0)
|
||||
@min(image.height, p.source_height)
|
||||
else
|
||||
image.height -| source_y;
|
||||
|
||||
// Calculate the width/height of our image.
|
||||
const dest_width = if (p.columns > 0) p.columns * self.grid_metrics.cell_width else source_width;
|
||||
const dest_height = if (p.rows > 0) p.rows * self.grid_metrics.cell_height else source_height;
|
||||
|
||||
// Accumulate the placement
|
||||
if (image.width > 0 and image.height > 0) {
|
||||
try self.image_placements.append(self.alloc, .{
|
||||
.image_id = image.id,
|
||||
.x = @intCast(rect.top_left.x),
|
||||
.y = @intCast(viewport.viewport.y),
|
||||
.z = p.z,
|
||||
.width = dest_width,
|
||||
.height = dest_height,
|
||||
.cell_offset_x = p.x_offset,
|
||||
.cell_offset_y = p.y_offset,
|
||||
.source_x = source_x,
|
||||
.source_y = source_y,
|
||||
.source_width = source_width,
|
||||
.source_height = source_height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn prepKittyImage(
|
||||
self: *OpenGL,
|
||||
image: *const terminal.kitty.graphics.Image,
|
||||
) !void {
|
||||
// We need to prep this image for upload if it isn't in the cache OR
|
||||
// it is in the cache but the transmit time doesn't match meaning this
|
||||
// image is different.
|
||||
const gop = try self.images.getOrPut(self.alloc, image.id);
|
||||
if (gop.found_existing and
|
||||
gop.value_ptr.transmit_time.order(image.transmit_time) == .eq)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy the data into the pending state.
|
||||
const data = try self.alloc.dupe(u8, image.data);
|
||||
errdefer self.alloc.free(data);
|
||||
|
||||
// Store it in the map
|
||||
const pending: Image.Pending = .{
|
||||
.width = image.width,
|
||||
.height = image.height,
|
||||
.data = data.ptr,
|
||||
};
|
||||
|
||||
const new_image: Image = switch (image.format) {
|
||||
.grey_alpha => .{ .pending_grey_alpha = pending },
|
||||
.rgb => .{ .pending_rgb = pending },
|
||||
.rgba => .{ .pending_rgba = pending },
|
||||
.png => unreachable, // should be decoded by now
|
||||
};
|
||||
|
||||
if (!gop.found_existing) {
|
||||
gop.value_ptr.* = .{
|
||||
.image = new_image,
|
||||
.transmit_time = undefined,
|
||||
};
|
||||
} else {
|
||||
try gop.value_ptr.image.markForReplace(
|
||||
self.alloc,
|
||||
new_image,
|
||||
);
|
||||
}
|
||||
|
||||
gop.value_ptr.transmit_time = image.transmit_time;
|
||||
}
|
||||
|
||||
/// rebuildCells rebuilds all the GPU cells from our CPU state. This is a
|
||||
/// slow operation but ensures that the GPU state exactly matches the CPU state.
|
||||
/// In steady-state operation, we use some GPU tricks to send down stale data
|
||||
|
@ -7,6 +7,7 @@ const std = @import("std");
|
||||
const Allocator = std.mem.Allocator;
|
||||
const assert = std.debug.assert;
|
||||
const fastmem = @import("../fastmem.zig");
|
||||
const kitty = @import("kitty.zig");
|
||||
const point = @import("point.zig");
|
||||
const pagepkg = @import("page.zig");
|
||||
const stylepkg = @import("style.zig");
|
||||
@ -1056,6 +1057,11 @@ const ReflowCursor = struct {
|
||||
self.page_cell.style_id = id;
|
||||
}
|
||||
|
||||
// Copy Kitty virtual placeholder status
|
||||
if (cell.codepoint() == kitty.graphics.unicode.placeholder) {
|
||||
self.page_row.kitty_virtual_placeholder = true;
|
||||
}
|
||||
|
||||
self.cursorForward();
|
||||
}
|
||||
|
||||
@ -3224,12 +3230,12 @@ pub const Pin = struct {
|
||||
|
||||
/// Returns the grapheme codepoints for the given cell. These are only
|
||||
/// the EXTRA codepoints and not the first codepoint.
|
||||
pub fn grapheme(self: Pin, cell: *pagepkg.Cell) ?[]u21 {
|
||||
pub fn grapheme(self: Pin, cell: *const pagepkg.Cell) ?[]u21 {
|
||||
return self.page.data.lookupGrapheme(cell);
|
||||
}
|
||||
|
||||
/// Returns the style for the given cell in this pin.
|
||||
pub fn style(self: Pin, cell: *pagepkg.Cell) stylepkg.Style {
|
||||
pub fn style(self: Pin, cell: *const pagepkg.Cell) stylepkg.Style {
|
||||
if (cell.style_id == stylepkg.default_id) return .{};
|
||||
return self.page.data.styles.get(
|
||||
self.page.data.memory,
|
||||
@ -7956,3 +7962,122 @@ test "PageList resize reflow less cols to wrap a wide char" {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "PageList resize reflow less cols copy kitty placeholder" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 4, 2, 0);
|
||||
defer s.deinit();
|
||||
{
|
||||
try testing.expect(s.pages.first == s.pages.last);
|
||||
const page = &s.pages.first.?.data;
|
||||
|
||||
// Write unicode placeholders
|
||||
for (0..s.cols - 1) |x| {
|
||||
const rac = page.getRowAndCell(x, 0);
|
||||
rac.row.kitty_virtual_placeholder = true;
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = kitty.graphics.unicode.placeholder },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Resize
|
||||
try s.resize(.{ .cols = 2, .reflow = true });
|
||||
try testing.expectEqual(@as(usize, 2), s.cols);
|
||||
try testing.expectEqual(@as(usize, 2), s.totalRows());
|
||||
|
||||
var it = s.rowIterator(.right_down, .{ .active = .{} }, null);
|
||||
while (it.next()) |offset| {
|
||||
for (0..s.cols - 1) |x| {
|
||||
var offset_copy = offset;
|
||||
offset_copy.x = @intCast(x);
|
||||
const rac = offset_copy.rowAndCell();
|
||||
|
||||
const row = rac.row;
|
||||
try testing.expect(row.kitty_virtual_placeholder);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
test "PageList resize reflow more cols clears kitty placeholder" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 4, 2, 0);
|
||||
defer s.deinit();
|
||||
{
|
||||
try testing.expect(s.pages.first == s.pages.last);
|
||||
const page = &s.pages.first.?.data;
|
||||
|
||||
// Write unicode placeholders
|
||||
for (0..s.cols - 1) |x| {
|
||||
const rac = page.getRowAndCell(x, 0);
|
||||
rac.row.kitty_virtual_placeholder = true;
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = kitty.graphics.unicode.placeholder },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Resize smaller then larger
|
||||
try s.resize(.{ .cols = 2, .reflow = true });
|
||||
try s.resize(.{ .cols = 4, .reflow = true });
|
||||
try testing.expectEqual(@as(usize, 4), s.cols);
|
||||
try testing.expectEqual(@as(usize, 2), s.totalRows());
|
||||
|
||||
var it = s.rowIterator(.right_down, .{ .active = .{} }, null);
|
||||
{
|
||||
const row = it.next().?;
|
||||
const rac = row.rowAndCell();
|
||||
try testing.expect(rac.row.kitty_virtual_placeholder);
|
||||
}
|
||||
{
|
||||
const row = it.next().?;
|
||||
const rac = row.rowAndCell();
|
||||
try testing.expect(!rac.row.kitty_virtual_placeholder);
|
||||
}
|
||||
try testing.expect(it.next() == null);
|
||||
}
|
||||
|
||||
test "PageList resize reflow wrap moves kitty placeholder" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
var s = try init(alloc, 4, 2, 0);
|
||||
defer s.deinit();
|
||||
{
|
||||
try testing.expect(s.pages.first == s.pages.last);
|
||||
const page = &s.pages.first.?.data;
|
||||
|
||||
// Write unicode placeholders
|
||||
for (2..s.cols - 1) |x| {
|
||||
const rac = page.getRowAndCell(x, 0);
|
||||
rac.row.kitty_virtual_placeholder = true;
|
||||
rac.cell.* = .{
|
||||
.content_tag = .codepoint,
|
||||
.content = .{ .codepoint = kitty.graphics.unicode.placeholder },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
try s.resize(.{ .cols = 2, .reflow = true });
|
||||
try testing.expectEqual(@as(usize, 2), s.cols);
|
||||
try testing.expectEqual(@as(usize, 2), s.totalRows());
|
||||
|
||||
var it = s.rowIterator(.right_down, .{ .active = .{} }, null);
|
||||
{
|
||||
const row = it.next().?;
|
||||
const rac = row.rowAndCell();
|
||||
try testing.expect(!rac.row.kitty_virtual_placeholder);
|
||||
}
|
||||
{
|
||||
const row = it.next().?;
|
||||
const rac = row.rowAndCell();
|
||||
try testing.expect(rac.row.kitty_virtual_placeholder);
|
||||
}
|
||||
try testing.expect(it.next() == null);
|
||||
}
|
||||
|
@ -1012,6 +1012,16 @@ pub fn clearCells(
|
||||
if (cells.len == self.pages.cols) row.styled = false;
|
||||
}
|
||||
|
||||
if (row.kitty_virtual_placeholder and
|
||||
cells.len == self.pages.cols)
|
||||
{
|
||||
for (cells) |c| {
|
||||
if (c.codepoint() == kitty.graphics.unicode.placeholder) {
|
||||
break;
|
||||
}
|
||||
} else row.kitty_virtual_placeholder = false;
|
||||
}
|
||||
|
||||
@memset(cells, self.blankCell());
|
||||
}
|
||||
|
||||
|
@ -281,7 +281,7 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
// column. Otherwise, we need to check if there is text to
|
||||
// figure out if we're attaching to the prev or current.
|
||||
if (self.screen.cursor.x != right_limit - 1) break :left 1;
|
||||
break :left @intFromBool(!self.screen.cursor.page_cell.hasText());
|
||||
break :left @intFromBool(self.screen.cursor.page_cell.codepoint() == 0);
|
||||
};
|
||||
|
||||
// If the previous cell is a wide spacer tail, then we actually
|
||||
@ -299,7 +299,7 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
|
||||
// If our cell has no content, then this is a new cell and
|
||||
// necessarily a grapheme break.
|
||||
if (!prev.cell.hasText()) break :grapheme;
|
||||
if (prev.cell.codepoint() == 0) break :grapheme;
|
||||
|
||||
const grapheme_break = brk: {
|
||||
var state: unicode.GraphemeBreakState = .{};
|
||||
@ -379,7 +379,11 @@ pub fn print(self: *Terminal, c: u21) !void {
|
||||
}
|
||||
}
|
||||
|
||||
log.debug("c={x} grapheme attach to left={}", .{ c, prev.left });
|
||||
log.debug("c={X} grapheme attach to left={} primary_cp={X}", .{
|
||||
c,
|
||||
prev.left,
|
||||
prev.cell.codepoint(),
|
||||
});
|
||||
self.screen.cursorMarkDirty();
|
||||
try self.screen.appendGrapheme(prev.cell, c);
|
||||
return;
|
||||
@ -636,6 +640,12 @@ fn printCell(
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a Kitty unicode placeholder then we need to mark the
|
||||
// row so that the renderer can lookup rows with these much faster.
|
||||
if (c == kitty.graphics.unicode.placeholder) {
|
||||
self.screen.cursor.page_row.kitty_virtual_placeholder = true;
|
||||
}
|
||||
|
||||
// We check for an active hyperlink first because setHyperlink
|
||||
// handles clearing the old hyperlink and an optimization if we're
|
||||
// overwriting the same hyperlink.
|
||||
@ -3542,6 +3552,24 @@ test "Terminal: print invoke charset single" {
|
||||
}
|
||||
}
|
||||
|
||||
test "Terminal: print kitty unicode placeholder" {
|
||||
var t = try init(testing.allocator, .{ .cols = 10, .rows = 10 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
||||
try t.print(kitty.graphics.unicode.placeholder);
|
||||
try testing.expectEqual(@as(usize, 0), t.screen.cursor.y);
|
||||
try testing.expectEqual(@as(usize, 1), t.screen.cursor.x);
|
||||
|
||||
{
|
||||
const list_cell = t.screen.pages.getCell(.{ .screen = .{ .x = 0, .y = 0 } }).?;
|
||||
const cell = list_cell.cell;
|
||||
try testing.expectEqual(@as(u21, kitty.graphics.unicode.placeholder), cell.content.codepoint);
|
||||
try testing.expect(list_cell.row.kitty_virtual_placeholder);
|
||||
}
|
||||
|
||||
try testing.expect(t.isDirty(.{ .screen = .{ .x = 0, .y = 0 } }));
|
||||
}
|
||||
|
||||
test "Terminal: soft wrap" {
|
||||
var t = try init(testing.allocator, .{ .cols = 3, .rows = 80 });
|
||||
defer t.deinit(testing.allocator);
|
||||
|
@ -16,7 +16,14 @@
|
||||
//! aim to ship a v1 of this implementation came at some cost. I learned a lot
|
||||
//! though and I think we can go back through and fix this up.
|
||||
|
||||
const render = @import("graphics_render.zig");
|
||||
pub usingnamespace @import("graphics_command.zig");
|
||||
pub usingnamespace @import("graphics_exec.zig");
|
||||
pub usingnamespace @import("graphics_image.zig");
|
||||
pub usingnamespace @import("graphics_storage.zig");
|
||||
pub const unicode = @import("graphics_unicode.zig");
|
||||
pub const RenderPlacement = render.Placement;
|
||||
|
||||
test {
|
||||
@import("std").testing.refAllDecls(@This());
|
||||
}
|
||||
|
@ -462,6 +462,10 @@ pub const Display = struct {
|
||||
rows: u32 = 0, // r
|
||||
cursor_movement: CursorMovement = .after, // C
|
||||
virtual_placement: bool = false, // U
|
||||
parent_id: u32 = 0, // P
|
||||
parent_placement_id: u32 = 0, // Q
|
||||
horizontal_offset: u32 = 0, // H
|
||||
vertical_offset: u32 = 0, // V
|
||||
z: i32 = 0, // z
|
||||
|
||||
pub const CursorMovement = enum {
|
||||
@ -537,6 +541,22 @@ pub const Display = struct {
|
||||
result.z = @bitCast(v);
|
||||
}
|
||||
|
||||
if (kv.get('P')) |v| {
|
||||
result.parent_id = v;
|
||||
}
|
||||
|
||||
if (kv.get('Q')) |v| {
|
||||
result.parent_placement_id = v;
|
||||
}
|
||||
|
||||
if (kv.get('H')) |v| {
|
||||
result.horizontal_offset = v;
|
||||
}
|
||||
|
||||
if (kv.get('V')) |v| {
|
||||
result.vertical_offset = v;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
@ -184,19 +184,33 @@ fn display(
|
||||
// Make sure our response has the image id in case we looked up by number
|
||||
result.id = img.id;
|
||||
|
||||
// Track a new pin for our cursor. The cursor is always tracked but we
|
||||
// don't want this one to move with the cursor.
|
||||
const placement_pin = terminal.screen.pages.trackPin(
|
||||
terminal.screen.cursor.page_pin.*,
|
||||
) catch |err| {
|
||||
log.warn("failed to create pin for Kitty graphics err={}", .{err});
|
||||
result.message = "EINVAL: failed to prepare terminal state";
|
||||
return result;
|
||||
// Location where the placement will go.
|
||||
const location: ImageStorage.Placement.Location = location: {
|
||||
// Virtual placements are not tracked
|
||||
if (d.virtual_placement) {
|
||||
if (d.parent_id > 0) {
|
||||
result.message = "EINVAL: virtual placement cannot refer to a parent";
|
||||
return result;
|
||||
}
|
||||
|
||||
break :location .{ .virtual = {} };
|
||||
}
|
||||
|
||||
// Track a new pin for our cursor. The cursor is always tracked but we
|
||||
// don't want this one to move with the cursor.
|
||||
const pin = terminal.screen.pages.trackPin(
|
||||
terminal.screen.cursor.page_pin.*,
|
||||
) catch |err| {
|
||||
log.warn("failed to create pin for Kitty graphics err={}", .{err});
|
||||
result.message = "EINVAL: failed to prepare terminal state";
|
||||
return result;
|
||||
};
|
||||
break :location .{ .pin = pin };
|
||||
};
|
||||
|
||||
// Add the placement
|
||||
const p: ImageStorage.Placement = .{
|
||||
.pin = placement_pin,
|
||||
.location = location,
|
||||
.x_offset = d.x_offset,
|
||||
.y_offset = d.y_offset,
|
||||
.source_x = d.x,
|
||||
@ -218,21 +232,24 @@ fn display(
|
||||
return result;
|
||||
};
|
||||
|
||||
// Cursor needs to move after placement
|
||||
switch (d.cursor_movement) {
|
||||
.none => {},
|
||||
.after => {
|
||||
// We use terminal.index to properly handle scroll regions.
|
||||
const size = p.gridSize(img, terminal);
|
||||
for (0..size.rows) |_| terminal.index() catch |err| {
|
||||
log.warn("failed to move cursor: {}", .{err});
|
||||
break;
|
||||
};
|
||||
// Apply cursor movement setting. This only applies to pin placements.
|
||||
switch (p.location) {
|
||||
.virtual => {},
|
||||
.pin => |pin| switch (d.cursor_movement) {
|
||||
.none => {},
|
||||
.after => {
|
||||
// We use terminal.index to properly handle scroll regions.
|
||||
const size = p.gridSize(img, terminal);
|
||||
for (0..size.rows) |_| terminal.index() catch |err| {
|
||||
log.warn("failed to move cursor: {}", .{err});
|
||||
break;
|
||||
};
|
||||
|
||||
terminal.setCursorPos(
|
||||
terminal.screen.cursor.y,
|
||||
p.pin.x + size.cols + 1,
|
||||
);
|
||||
terminal.setCursorPos(
|
||||
terminal.screen.cursor.y,
|
||||
pin.x + size.cols + 1,
|
||||
);
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
28
src/terminal/kitty/graphics_render.zig
Normal file
28
src/terminal/kitty/graphics_render.zig
Normal file
@ -0,0 +1,28 @@
|
||||
const std = @import("std");
|
||||
const assert = std.debug.assert;
|
||||
const testing = std.testing;
|
||||
const terminal = @import("../main.zig");
|
||||
|
||||
/// A render placement is a way to position a Kitty graphics image onto
|
||||
/// the screen. It is broken down into the fields that make it easier to
|
||||
/// position the image using a renderer.
|
||||
pub const Placement = struct {
|
||||
/// The top-left corner of the image in grid coordinates.
|
||||
top_left: terminal.Pin,
|
||||
|
||||
/// The offset in pixels from the top-left corner of the grid cell.
|
||||
offset_x: u32 = 0,
|
||||
offset_y: u32 = 0,
|
||||
|
||||
/// The source rectangle of the image to render. This doesn't have to
|
||||
/// match the size the destination size and the renderer is expected
|
||||
/// to scale the image to fit the destination size.
|
||||
source_x: u32 = 0,
|
||||
source_y: u32 = 0,
|
||||
source_width: u32 = 0,
|
||||
source_height: u32 = 0,
|
||||
|
||||
/// The final width/height of the image in pixels.
|
||||
dest_width: u32 = 0,
|
||||
dest_height: u32 = 0,
|
||||
};
|
@ -218,18 +218,27 @@ pub const ImageStorage = struct {
|
||||
cmd: command.Delete,
|
||||
) void {
|
||||
switch (cmd) {
|
||||
.all => |delete_images| if (delete_images) {
|
||||
// We just reset our entire state.
|
||||
self.deinit(alloc, &t.screen);
|
||||
self.* = .{
|
||||
.dirty = true,
|
||||
.total_limit = self.total_limit,
|
||||
};
|
||||
} else {
|
||||
// Delete all our placements
|
||||
self.clearPlacements(&t.screen);
|
||||
self.placements.deinit(alloc);
|
||||
self.placements = .{};
|
||||
.all => |delete_images| {
|
||||
var it = self.placements.iterator();
|
||||
while (it.next()) |entry| {
|
||||
// Skip virtual placements
|
||||
switch (entry.value_ptr.location) {
|
||||
.pin => {},
|
||||
.virtual => continue,
|
||||
}
|
||||
|
||||
// Deinit the placement and remove it
|
||||
const image_id = entry.key_ptr.image_id;
|
||||
entry.value_ptr.deinit(&t.screen);
|
||||
self.placements.removeByPtr(entry.key_ptr);
|
||||
if (delete_images) self.deleteIfUnused(alloc, image_id);
|
||||
}
|
||||
|
||||
if (delete_images) {
|
||||
var image_it = self.images.iterator();
|
||||
while (image_it.next()) |kv| self.deleteIfUnused(alloc, kv.key_ptr.*);
|
||||
}
|
||||
|
||||
self.dirty = true;
|
||||
},
|
||||
|
||||
@ -318,7 +327,7 @@ pub const ImageStorage = struct {
|
||||
var it = self.placements.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const img = self.imageById(entry.key_ptr.image_id) orelse continue;
|
||||
const rect = entry.value_ptr.rect(img, t);
|
||||
const rect = entry.value_ptr.rect(img, t) orelse continue;
|
||||
if (rect.top_left.x <= x and rect.bottom_right.x >= x) {
|
||||
entry.value_ptr.deinit(&t.screen);
|
||||
self.placements.removeByPtr(entry.key_ptr);
|
||||
@ -345,7 +354,7 @@ pub const ImageStorage = struct {
|
||||
var it = self.placements.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const img = self.imageById(entry.key_ptr.image_id) orelse continue;
|
||||
const rect = entry.value_ptr.rect(img, t);
|
||||
const rect = entry.value_ptr.rect(img, t) orelse continue;
|
||||
|
||||
// We need to copy our pin to ensure we are at least at
|
||||
// the top-left x.
|
||||
@ -365,6 +374,14 @@ pub const ImageStorage = struct {
|
||||
.z => |v| {
|
||||
var it = self.placements.iterator();
|
||||
while (it.next()) |entry| {
|
||||
switch (entry.value_ptr.location) {
|
||||
.pin => {},
|
||||
|
||||
// Virtual placeholders cannot delete by z according
|
||||
// to the spec.
|
||||
.virtual => continue,
|
||||
}
|
||||
|
||||
if (entry.value_ptr.z == v.z) {
|
||||
const image_id = entry.key_ptr.image_id;
|
||||
entry.value_ptr.deinit(&t.screen);
|
||||
@ -451,7 +468,7 @@ pub const ImageStorage = struct {
|
||||
var it = self.placements.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const img = self.imageById(entry.key_ptr.image_id) orelse continue;
|
||||
const rect = entry.value_ptr.rect(img, t);
|
||||
const rect = entry.value_ptr.rect(img, t) orelse continue;
|
||||
if (target_pin.isBetween(rect.top_left, rect.bottom_right)) {
|
||||
if (filter) |f| if (!f(filter_ctx, entry.value_ptr.*)) continue;
|
||||
entry.value_ptr.deinit(&t.screen);
|
||||
@ -576,8 +593,8 @@ pub const ImageStorage = struct {
|
||||
};
|
||||
|
||||
pub const Placement = struct {
|
||||
/// The tracked pin for this placement.
|
||||
pin: *PageList.Pin,
|
||||
/// The location where this placement should be drawn.
|
||||
location: Location,
|
||||
|
||||
/// Offset of the x/y from the top-left of the cell.
|
||||
x_offset: u32 = 0,
|
||||
@ -596,11 +613,22 @@ pub const ImageStorage = struct {
|
||||
/// The z-index for this placement.
|
||||
z: i32 = 0,
|
||||
|
||||
pub const Location = union(enum) {
|
||||
/// Exactly placed on a screen pin.
|
||||
pin: *PageList.Pin,
|
||||
|
||||
/// Virtual placement (U=1) for unicode placeholders.
|
||||
virtual: void,
|
||||
};
|
||||
|
||||
pub fn deinit(
|
||||
self: *const Placement,
|
||||
s: *terminal.Screen,
|
||||
) void {
|
||||
s.pages.untrackPin(self.pin);
|
||||
switch (self.location) {
|
||||
.pin => |p| s.pages.untrackPin(p),
|
||||
.virtual => {},
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the size in grid cells that this placement takes up.
|
||||
@ -642,15 +670,20 @@ pub const ImageStorage = struct {
|
||||
}
|
||||
|
||||
/// Returns a selection of the entire rectangle this placement
|
||||
/// occupies within the screen.
|
||||
/// occupies within the screen. This can return null if the placement
|
||||
/// doesn't have an associated rect (i.e. a virtual placement).
|
||||
pub fn rect(
|
||||
self: Placement,
|
||||
image: Image,
|
||||
t: *const terminal.Terminal,
|
||||
) Rect {
|
||||
) ?Rect {
|
||||
const grid_size = self.gridSize(image, t);
|
||||
const pin = switch (self.location) {
|
||||
.pin => |p| p,
|
||||
.virtual => return null,
|
||||
};
|
||||
|
||||
var br = switch (self.pin.downOverflow(grid_size.rows - 1)) {
|
||||
var br = switch (pin.downOverflow(grid_size.rows - 1)) {
|
||||
.offset => |v| v,
|
||||
.overflow => |v| v.end,
|
||||
};
|
||||
@ -658,12 +691,12 @@ pub const ImageStorage = struct {
|
||||
// We need to sub one here because the x value is
|
||||
// one width already. So if the image is width "1"
|
||||
// then we add zero to X because X itelf is width 1.
|
||||
self.pin.x + (grid_size.cols - 1),
|
||||
pin.x + (grid_size.cols - 1),
|
||||
t.cols - 1,
|
||||
);
|
||||
|
||||
return .{
|
||||
.top_left = self.pin.*,
|
||||
.top_left = pin.*,
|
||||
.bottom_right = br,
|
||||
};
|
||||
}
|
||||
@ -692,8 +725,8 @@ test "storage: add placement with zero placement id" {
|
||||
defer s.deinit(alloc, &t.screen);
|
||||
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
||||
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
|
||||
try s.addPlacement(alloc, 1, 0, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) });
|
||||
try s.addPlacement(alloc, 1, 0, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) });
|
||||
try s.addPlacement(alloc, 1, 0, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
|
||||
try s.addPlacement(alloc, 1, 0, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
|
||||
|
||||
try testing.expectEqual(@as(usize, 2), s.placements.count());
|
||||
try testing.expectEqual(@as(usize, 2), s.images.count());
|
||||
@ -721,8 +754,8 @@ test "storage: delete all placements and images" {
|
||||
try s.addImage(alloc, .{ .id = 1 });
|
||||
try s.addImage(alloc, .{ .id = 2 });
|
||||
try s.addImage(alloc, .{ .id = 3 });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
|
||||
try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||
try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||
|
||||
s.dirty = false;
|
||||
s.delete(alloc, &t, .{ .all = true });
|
||||
@ -745,8 +778,8 @@ test "storage: delete all placements and images preserves limit" {
|
||||
try s.addImage(alloc, .{ .id = 1 });
|
||||
try s.addImage(alloc, .{ .id = 2 });
|
||||
try s.addImage(alloc, .{ .id = 3 });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
|
||||
try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||
try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||
|
||||
s.dirty = false;
|
||||
s.delete(alloc, &t, .{ .all = true });
|
||||
@ -769,8 +802,8 @@ test "storage: delete all placements" {
|
||||
try s.addImage(alloc, .{ .id = 1 });
|
||||
try s.addImage(alloc, .{ .id = 2 });
|
||||
try s.addImage(alloc, .{ .id = 3 });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
|
||||
try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||
try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||
|
||||
s.dirty = false;
|
||||
s.delete(alloc, &t, .{ .all = false });
|
||||
@ -792,8 +825,8 @@ test "storage: delete all placements by image id" {
|
||||
try s.addImage(alloc, .{ .id = 1 });
|
||||
try s.addImage(alloc, .{ .id = 2 });
|
||||
try s.addImage(alloc, .{ .id = 3 });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
|
||||
try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||
try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||
|
||||
s.dirty = false;
|
||||
s.delete(alloc, &t, .{ .id = .{ .image_id = 2 } });
|
||||
@ -815,8 +848,8 @@ test "storage: delete all placements by image id and unused images" {
|
||||
try s.addImage(alloc, .{ .id = 1 });
|
||||
try s.addImage(alloc, .{ .id = 2 });
|
||||
try s.addImage(alloc, .{ .id = 3 });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
|
||||
try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||
try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||
|
||||
s.dirty = false;
|
||||
s.delete(alloc, &t, .{ .id = .{ .delete = true, .image_id = 2 } });
|
||||
@ -838,9 +871,9 @@ test "storage: delete placement by specific id" {
|
||||
try s.addImage(alloc, .{ .id = 1 });
|
||||
try s.addImage(alloc, .{ .id = 2 });
|
||||
try s.addImage(alloc, .{ .id = 3 });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
|
||||
try s.addPlacement(alloc, 2, 1, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||
try s.addPlacement(alloc, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||
|
||||
s.dirty = false;
|
||||
s.delete(alloc, &t, .{ .id = .{
|
||||
@ -867,8 +900,8 @@ test "storage: delete intersecting cursor" {
|
||||
defer s.deinit(alloc, &t.screen);
|
||||
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
||||
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
|
||||
|
||||
t.screen.cursorAbsolute(12, 12);
|
||||
|
||||
@ -899,8 +932,8 @@ test "storage: delete intersecting cursor plus unused" {
|
||||
defer s.deinit(alloc, &t.screen);
|
||||
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
||||
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
|
||||
|
||||
t.screen.cursorAbsolute(12, 12);
|
||||
|
||||
@ -931,8 +964,8 @@ test "storage: delete intersecting cursor hits multiple" {
|
||||
defer s.deinit(alloc, &t.screen);
|
||||
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
||||
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
|
||||
|
||||
t.screen.cursorAbsolute(26, 26);
|
||||
|
||||
@ -957,8 +990,8 @@ test "storage: delete by column" {
|
||||
defer s.deinit(alloc, &t.screen);
|
||||
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
||||
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
|
||||
|
||||
s.dirty = false;
|
||||
s.delete(alloc, &t, .{ .column = .{
|
||||
@ -988,9 +1021,9 @@ test "storage: delete by column 1x1" {
|
||||
var s: ImageStorage = .{};
|
||||
defer s.deinit(alloc, &t.screen);
|
||||
try s.addImage(alloc, .{ .id = 1, .width = 1, .height = 1 });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 1, .y = 0 }) });
|
||||
try s.addPlacement(alloc, 1, 3, .{ .pin = try trackPin(&t, .{ .x = 2, .y = 0 }) });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 0 }) } });
|
||||
try s.addPlacement(alloc, 1, 3, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 2, .y = 0 }) } });
|
||||
|
||||
s.delete(alloc, &t, .{ .column = .{
|
||||
.delete = false,
|
||||
@ -1023,8 +1056,8 @@ test "storage: delete by row" {
|
||||
defer s.deinit(alloc, &t.screen);
|
||||
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
||||
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 0, .y = 0 }) } });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
|
||||
|
||||
s.dirty = false;
|
||||
s.delete(alloc, &t, .{ .row = .{
|
||||
@ -1054,9 +1087,9 @@ test "storage: delete by row 1x1" {
|
||||
var s: ImageStorage = .{};
|
||||
defer s.deinit(alloc, &t.screen);
|
||||
try s.addImage(alloc, .{ .id = 1, .width = 1, .height = 1 });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .pin = try trackPin(&t, .{ .y = 0 }) });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .y = 1 }) });
|
||||
try s.addPlacement(alloc, 1, 3, .{ .pin = try trackPin(&t, .{ .y = 2 }) });
|
||||
try s.addPlacement(alloc, 1, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .y = 0 }) } });
|
||||
try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .y = 1 }) } });
|
||||
try s.addPlacement(alloc, 1, 3, .{ .location = .{ .pin = try trackPin(&t, .{ .y = 2 }) } });
|
||||
|
||||
s.delete(alloc, &t, .{ .row = .{
|
||||
.delete = false,
|
||||
|
1348
src/terminal/kitty/graphics_unicode.zig
Normal file
1348
src/terminal/kitty/graphics_unicode.zig
Normal file
File diff suppressed because it is too large
Load Diff
BIN
src/terminal/kitty/testdata/dog.png
vendored
Normal file
BIN
src/terminal/kitty/testdata/dog.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 232 KiB |
@ -8,6 +8,7 @@ const posix = std.posix;
|
||||
const fastmem = @import("../fastmem.zig");
|
||||
const color = @import("color.zig");
|
||||
const hyperlink = @import("hyperlink.zig");
|
||||
const kitty = @import("kitty.zig");
|
||||
const sgr = @import("sgr.zig");
|
||||
const style = @import("style.zig");
|
||||
const size = @import("size.zig");
|
||||
@ -825,6 +826,9 @@ pub const Page = struct {
|
||||
src_cell.style_id,
|
||||
) orelse src_cell.style_id;
|
||||
}
|
||||
if (src_cell.codepoint() == kitty.graphics.unicode.placeholder) {
|
||||
dst_row.kitty_virtual_placeholder = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -912,6 +916,9 @@ pub const Page = struct {
|
||||
dst.hyperlink = true;
|
||||
dst_row.hyperlink = true;
|
||||
}
|
||||
if (src.codepoint() == kitty.graphics.unicode.placeholder) {
|
||||
dst_row.kitty_virtual_placeholder = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -931,6 +938,7 @@ pub const Page = struct {
|
||||
src_row.grapheme = false;
|
||||
src_row.hyperlink = false;
|
||||
src_row.styled = false;
|
||||
src_row.kitty_virtual_placeholder = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1028,6 +1036,16 @@ pub const Page = struct {
|
||||
if (cells.len == self.size.cols) row.styled = false;
|
||||
}
|
||||
|
||||
if (row.kitty_virtual_placeholder and
|
||||
cells.len == self.size.cols)
|
||||
{
|
||||
for (cells) |c| {
|
||||
if (c.codepoint() == kitty.graphics.unicode.placeholder) {
|
||||
break;
|
||||
}
|
||||
} else row.kitty_virtual_placeholder = false;
|
||||
}
|
||||
|
||||
// Zero the cells as u64s since empirically this seems
|
||||
// to be a bit faster than using @memset(cells, .{})
|
||||
@memset(@as([]u64, @ptrCast(cells)), 0);
|
||||
@ -1134,7 +1152,7 @@ pub const Page = struct {
|
||||
pub fn setGraphemes(self: *Page, row: *Row, cell: *Cell, cps: []u21) Allocator.Error!void {
|
||||
defer self.assertIntegrity();
|
||||
|
||||
assert(cell.hasText());
|
||||
assert(cell.codepoint() > 0);
|
||||
assert(cell.content_tag == .codepoint);
|
||||
|
||||
const cell_offset = getOffset(Cell, self.memory, cell);
|
||||
@ -1160,7 +1178,7 @@ pub const Page = struct {
|
||||
pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) Allocator.Error!void {
|
||||
defer self.assertIntegrity();
|
||||
|
||||
if (comptime std.debug.runtime_safety) assert(cell.hasText());
|
||||
if (comptime std.debug.runtime_safety) assert(cell.codepoint() != 0);
|
||||
|
||||
const cell_offset = getOffset(Cell, self.memory, cell);
|
||||
var map = self.grapheme_map.map(self.memory);
|
||||
@ -1219,7 +1237,7 @@ pub const Page = struct {
|
||||
/// Returns the codepoints for the given cell. These are the codepoints
|
||||
/// in addition to the first codepoint. The first codepoint is NOT
|
||||
/// included since it is on the cell itself.
|
||||
pub fn lookupGrapheme(self: *const Page, cell: *Cell) ?[]u21 {
|
||||
pub fn lookupGrapheme(self: *const Page, cell: *const Cell) ?[]u21 {
|
||||
const cell_offset = getOffset(Cell, self.memory, cell);
|
||||
const map = self.grapheme_map.map(self.memory);
|
||||
const slice = map.get(cell_offset) orelse return null;
|
||||
@ -1551,7 +1569,11 @@ pub const Row = packed struct(u64) {
|
||||
/// running program, or "unknown" if it was never set.
|
||||
semantic_prompt: SemanticPrompt = .unknown,
|
||||
|
||||
_padding: u24 = 0,
|
||||
/// True if this row contains a virtual placeholder for the Kitty
|
||||
/// graphics protocol. (U+10EEEE)
|
||||
kitty_virtual_placeholder: bool = false,
|
||||
|
||||
_padding: u23 = 0,
|
||||
|
||||
/// Semantic prompt type.
|
||||
pub const SemanticPrompt = enum(u3) {
|
||||
@ -1673,6 +1695,12 @@ pub const Cell = packed struct(u64) {
|
||||
return @as(u64, @bitCast(self)) == 0;
|
||||
}
|
||||
|
||||
/// Returns true if this cell represents a cell with text to render.
|
||||
///
|
||||
/// Cases this returns false:
|
||||
/// - Cell text is blank
|
||||
/// - Cell is styled but only with a background color and no text
|
||||
/// - Cell has a unicode placeholder for Kitty graphics protocol
|
||||
pub fn hasText(self: Cell) bool {
|
||||
return switch (self.content_tag) {
|
||||
.codepoint,
|
||||
|
Reference in New Issue
Block a user