Merge pull request #2015 from ghostty-org/kitty-unicode

Kitty Graphics Unicode Placeholders
This commit is contained in:
Mitchell Hashimoto
2024-07-31 09:56:12 -07:00
committed by GitHub
14 changed files with 2169 additions and 276 deletions

View File

@ -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,

View File

@ -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

View File

@ -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

View File

@ -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);
}

View File

@ -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());
}

View File

@ -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);

View File

@ -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());
}

View File

@ -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;
}
};

View File

@ -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,
);
},
},
}

View 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,
};

View File

@ -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,

File diff suppressed because it is too large Load Diff

BIN
src/terminal/kitty/testdata/dog.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

View File

@ -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,