mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-16 16:56:09 +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;
|
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
|
// Add all the codepoints for our grapheme
|
||||||
try self.addCodepoint(
|
try self.addCodepoint(
|
||||||
&hasher,
|
&hasher,
|
||||||
@ -284,8 +290,20 @@ pub const RunIterator = struct {
|
|||||||
style: font.Style,
|
style: font.Style,
|
||||||
presentation: ?font.Presentation,
|
presentation: ?font.Presentation,
|
||||||
) !?font.Collection.Index {
|
) !?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.
|
// 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(
|
const primary = try self.grid.getIndex(
|
||||||
alloc,
|
alloc,
|
||||||
primary_cp,
|
primary_cp,
|
||||||
|
@ -124,6 +124,7 @@ images: ImageMap = .{},
|
|||||||
image_placements: ImagePlacementList = .{},
|
image_placements: ImagePlacementList = .{},
|
||||||
image_bg_end: u32 = 0,
|
image_bg_end: u32 = 0,
|
||||||
image_text_end: u32 = 0,
|
image_text_end: u32 = 0,
|
||||||
|
image_virtual: bool = false,
|
||||||
|
|
||||||
/// Metal state
|
/// Metal state
|
||||||
shaders: Shaders, // Compiled shaders
|
shaders: Shaders, // Compiled shaders
|
||||||
@ -939,7 +940,13 @@ pub fn updateFrame(
|
|||||||
// If we have Kitty graphics data, we enter a SLOW SLOW SLOW path.
|
// 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
|
// We only do this if the Kitty image state is dirty meaning only if
|
||||||
// it changes.
|
// 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);
|
try self.prepKittyGraphics(state.terminal);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1596,6 +1603,7 @@ fn prepKittyGraphics(
|
|||||||
// We always clear our previous placements no matter what because
|
// We always clear our previous placements no matter what because
|
||||||
// we rebuild them from scratch.
|
// we rebuild them from scratch.
|
||||||
self.image_placements.clearRetainingCapacity();
|
self.image_placements.clearRetainingCapacity();
|
||||||
|
self.image_virtual = false;
|
||||||
|
|
||||||
// Go through our known images and if there are any that are no longer
|
// Go through our known images and if there are any that are no longer
|
||||||
// in use then mark them to be freed.
|
// 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.
|
// Go through the placements and ensure the image is loaded on the GPU.
|
||||||
var it = storage.placements.iterator();
|
var it = storage.placements.iterator();
|
||||||
while (it.next()) |kv| {
|
while (it.next()) |kv| {
|
||||||
// Find the image in storage
|
|
||||||
const p = kv.value_ptr;
|
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 {
|
const image = storage.imageById(kv.key_ptr.image_id) orelse {
|
||||||
log.warn(
|
log.warn(
|
||||||
"missing image for placement, ignoring image_id={}",
|
"missing image for placement, ignoring image_id={}",
|
||||||
@ -1629,102 +1654,18 @@ fn prepKittyGraphics(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the selection isn't within our viewport then skip it.
|
try self.prepKittyPlacement(t, &top, &bot, &image, p);
|
||||||
const rect = p.rect(image, t);
|
}
|
||||||
if (bot.before(rect.top_left)) continue;
|
|
||||||
if (rect.bottom_right.before(top)) continue;
|
|
||||||
|
|
||||||
// If the top left is outside the viewport we need to calc an offset
|
// If we have virtual placements then we need to scan for placeholders.
|
||||||
// so that we render (0, 0) with some offset for the texture.
|
if (self.image_virtual) {
|
||||||
const offset_y: u32 = if (rect.top_left.before(top)) offset_y: {
|
var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot);
|
||||||
const vp_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y;
|
while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement(
|
||||||
const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
|
t,
|
||||||
const offset_cells = vp_y - img_y;
|
&virtual_p,
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the placements by their Z value.
|
// Sort the placements by their Z value.
|
||||||
std.mem.sortUnstable(
|
std.mem.sortUnstable(
|
||||||
mtl_image.Placement,
|
mtl_image.Placement,
|
||||||
@ -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.
|
/// Update the configuration.
|
||||||
pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
|
pub fn changeConfig(self: *Metal, config: *DerivedConfig) !void {
|
||||||
// We always redo the font shaper in case font features changed. We
|
// We always redo the font shaper in case font features changed. We
|
||||||
|
@ -120,6 +120,7 @@ images: ImageMap = .{},
|
|||||||
image_placements: ImagePlacementList = .{},
|
image_placements: ImagePlacementList = .{},
|
||||||
image_bg_end: u32 = 0,
|
image_bg_end: u32 = 0,
|
||||||
image_text_end: u32 = 0,
|
image_text_end: u32 = 0,
|
||||||
|
image_virtual: bool = false,
|
||||||
|
|
||||||
/// Defererred OpenGL operation to update the screen size.
|
/// Defererred OpenGL operation to update the screen size.
|
||||||
const SetScreenSize = struct {
|
const SetScreenSize = struct {
|
||||||
@ -693,7 +694,13 @@ pub fn updateFrame(
|
|||||||
// If we have Kitty graphics data, we enter a SLOW SLOW SLOW path.
|
// 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
|
// We only do this if the Kitty image state is dirty meaning only if
|
||||||
// it changes.
|
// 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
|
// prepKittyGraphics touches self.images which is also used
|
||||||
// in drawFrame so if we're drawing on a separate thread we need
|
// in drawFrame so if we're drawing on a separate thread we need
|
||||||
// to lock this.
|
// to lock this.
|
||||||
@ -752,6 +759,7 @@ fn prepKittyGraphics(
|
|||||||
// We always clear our previous placements no matter what because
|
// We always clear our previous placements no matter what because
|
||||||
// we rebuild them from scratch.
|
// we rebuild them from scratch.
|
||||||
self.image_placements.clearRetainingCapacity();
|
self.image_placements.clearRetainingCapacity();
|
||||||
|
self.image_virtual = false;
|
||||||
|
|
||||||
// Go through our known images and if there are any that are no longer
|
// Go through our known images and if there are any that are no longer
|
||||||
// in use then mark them to be freed.
|
// in use then mark them to be freed.
|
||||||
@ -777,6 +785,23 @@ fn prepKittyGraphics(
|
|||||||
while (it.next()) |kv| {
|
while (it.next()) |kv| {
|
||||||
// Find the image in storage
|
// Find the image in storage
|
||||||
const p = kv.value_ptr;
|
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 {
|
const image = storage.imageById(kv.key_ptr.image_id) orelse {
|
||||||
log.warn(
|
log.warn(
|
||||||
"missing image for placement, ignoring image_id={}",
|
"missing image for placement, ignoring image_id={}",
|
||||||
@ -785,102 +810,18 @@ fn prepKittyGraphics(
|
|||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
// If the selection isn't within our viewport then skip it.
|
try self.prepKittyPlacement(t, &top, &bot, &image, p);
|
||||||
const rect = p.rect(image, t);
|
}
|
||||||
if (bot.before(rect.top_left)) continue;
|
|
||||||
if (rect.bottom_right.before(top)) continue;
|
|
||||||
|
|
||||||
// If the top left is outside the viewport we need to calc an offset
|
// If we have virtual placements then we need to scan for placeholders.
|
||||||
// so that we render (0, 0) with some offset for the texture.
|
if (self.image_virtual) {
|
||||||
const offset_y: u32 = if (rect.top_left.before(top)) offset_y: {
|
var v_it = terminal.kitty.graphics.unicode.placementIterator(top, bot);
|
||||||
const vp_y = t.screen.pages.pointFromPin(.screen, top).?.screen.y;
|
while (v_it.next()) |virtual_p| try self.prepKittyVirtualPlacement(
|
||||||
const img_y = t.screen.pages.pointFromPin(.screen, rect.top_left).?.screen.y;
|
t,
|
||||||
const offset_cells = vp_y - img_y;
|
&virtual_p,
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort the placements by their Z value.
|
// Sort the placements by their Z value.
|
||||||
std.mem.sortUnstable(
|
std.mem.sortUnstable(
|
||||||
gl_image.Placement,
|
gl_image.Placement,
|
||||||
@ -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
|
/// 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.
|
/// 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
|
/// 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 Allocator = std.mem.Allocator;
|
||||||
const assert = std.debug.assert;
|
const assert = std.debug.assert;
|
||||||
const fastmem = @import("../fastmem.zig");
|
const fastmem = @import("../fastmem.zig");
|
||||||
|
const kitty = @import("kitty.zig");
|
||||||
const point = @import("point.zig");
|
const point = @import("point.zig");
|
||||||
const pagepkg = @import("page.zig");
|
const pagepkg = @import("page.zig");
|
||||||
const stylepkg = @import("style.zig");
|
const stylepkg = @import("style.zig");
|
||||||
@ -1056,6 +1057,11 @@ const ReflowCursor = struct {
|
|||||||
self.page_cell.style_id = id;
|
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();
|
self.cursorForward();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3224,12 +3230,12 @@ pub const Pin = struct {
|
|||||||
|
|
||||||
/// Returns the grapheme codepoints for the given cell. These are only
|
/// Returns the grapheme codepoints for the given cell. These are only
|
||||||
/// the EXTRA codepoints and not the first codepoint.
|
/// 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);
|
return self.page.data.lookupGrapheme(cell);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the style for the given cell in this pin.
|
/// 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 .{};
|
if (cell.style_id == stylepkg.default_id) return .{};
|
||||||
return self.page.data.styles.get(
|
return self.page.data.styles.get(
|
||||||
self.page.data.memory,
|
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 (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());
|
@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
|
// column. Otherwise, we need to check if there is text to
|
||||||
// figure out if we're attaching to the prev or current.
|
// figure out if we're attaching to the prev or current.
|
||||||
if (self.screen.cursor.x != right_limit - 1) break :left 1;
|
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
|
// 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
|
// If our cell has no content, then this is a new cell and
|
||||||
// necessarily a grapheme break.
|
// necessarily a grapheme break.
|
||||||
if (!prev.cell.hasText()) break :grapheme;
|
if (prev.cell.codepoint() == 0) break :grapheme;
|
||||||
|
|
||||||
const grapheme_break = brk: {
|
const grapheme_break = brk: {
|
||||||
var state: unicode.GraphemeBreakState = .{};
|
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();
|
self.screen.cursorMarkDirty();
|
||||||
try self.screen.appendGrapheme(prev.cell, c);
|
try self.screen.appendGrapheme(prev.cell, c);
|
||||||
return;
|
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
|
// We check for an active hyperlink first because setHyperlink
|
||||||
// handles clearing the old hyperlink and an optimization if we're
|
// handles clearing the old hyperlink and an optimization if we're
|
||||||
// overwriting the same hyperlink.
|
// 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" {
|
test "Terminal: soft wrap" {
|
||||||
var t = try init(testing.allocator, .{ .cols = 3, .rows = 80 });
|
var t = try init(testing.allocator, .{ .cols = 3, .rows = 80 });
|
||||||
defer t.deinit(testing.allocator);
|
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
|
//! 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.
|
//! 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_command.zig");
|
||||||
pub usingnamespace @import("graphics_exec.zig");
|
pub usingnamespace @import("graphics_exec.zig");
|
||||||
pub usingnamespace @import("graphics_image.zig");
|
pub usingnamespace @import("graphics_image.zig");
|
||||||
pub usingnamespace @import("graphics_storage.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
|
rows: u32 = 0, // r
|
||||||
cursor_movement: CursorMovement = .after, // C
|
cursor_movement: CursorMovement = .after, // C
|
||||||
virtual_placement: bool = false, // U
|
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
|
z: i32 = 0, // z
|
||||||
|
|
||||||
pub const CursorMovement = enum {
|
pub const CursorMovement = enum {
|
||||||
@ -537,6 +541,22 @@ pub const Display = struct {
|
|||||||
result.z = @bitCast(v);
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -184,19 +184,33 @@ fn display(
|
|||||||
// Make sure our response has the image id in case we looked up by number
|
// Make sure our response has the image id in case we looked up by number
|
||||||
result.id = img.id;
|
result.id = img.id;
|
||||||
|
|
||||||
|
// 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
|
// Track a new pin for our cursor. The cursor is always tracked but we
|
||||||
// don't want this one to move with the cursor.
|
// don't want this one to move with the cursor.
|
||||||
const placement_pin = terminal.screen.pages.trackPin(
|
const pin = terminal.screen.pages.trackPin(
|
||||||
terminal.screen.cursor.page_pin.*,
|
terminal.screen.cursor.page_pin.*,
|
||||||
) catch |err| {
|
) catch |err| {
|
||||||
log.warn("failed to create pin for Kitty graphics err={}", .{err});
|
log.warn("failed to create pin for Kitty graphics err={}", .{err});
|
||||||
result.message = "EINVAL: failed to prepare terminal state";
|
result.message = "EINVAL: failed to prepare terminal state";
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
break :location .{ .pin = pin };
|
||||||
|
};
|
||||||
|
|
||||||
// Add the placement
|
// Add the placement
|
||||||
const p: ImageStorage.Placement = .{
|
const p: ImageStorage.Placement = .{
|
||||||
.pin = placement_pin,
|
.location = location,
|
||||||
.x_offset = d.x_offset,
|
.x_offset = d.x_offset,
|
||||||
.y_offset = d.y_offset,
|
.y_offset = d.y_offset,
|
||||||
.source_x = d.x,
|
.source_x = d.x,
|
||||||
@ -218,8 +232,10 @@ fn display(
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cursor needs to move after placement
|
// Apply cursor movement setting. This only applies to pin placements.
|
||||||
switch (d.cursor_movement) {
|
switch (p.location) {
|
||||||
|
.virtual => {},
|
||||||
|
.pin => |pin| switch (d.cursor_movement) {
|
||||||
.none => {},
|
.none => {},
|
||||||
.after => {
|
.after => {
|
||||||
// We use terminal.index to properly handle scroll regions.
|
// We use terminal.index to properly handle scroll regions.
|
||||||
@ -231,9 +247,10 @@ fn display(
|
|||||||
|
|
||||||
terminal.setCursorPos(
|
terminal.setCursorPos(
|
||||||
terminal.screen.cursor.y,
|
terminal.screen.cursor.y,
|
||||||
p.pin.x + size.cols + 1,
|
pin.x + size.cols + 1,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Display does not result in a response on success
|
// Display does not result in a response on success
|
||||||
|
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,
|
cmd: command.Delete,
|
||||||
) void {
|
) void {
|
||||||
switch (cmd) {
|
switch (cmd) {
|
||||||
.all => |delete_images| if (delete_images) {
|
.all => |delete_images| {
|
||||||
// We just reset our entire state.
|
var it = self.placements.iterator();
|
||||||
self.deinit(alloc, &t.screen);
|
while (it.next()) |entry| {
|
||||||
self.* = .{
|
// Skip virtual placements
|
||||||
.dirty = true,
|
switch (entry.value_ptr.location) {
|
||||||
.total_limit = self.total_limit,
|
.pin => {},
|
||||||
};
|
.virtual => continue,
|
||||||
} else {
|
}
|
||||||
// Delete all our placements
|
|
||||||
self.clearPlacements(&t.screen);
|
// Deinit the placement and remove it
|
||||||
self.placements.deinit(alloc);
|
const image_id = entry.key_ptr.image_id;
|
||||||
self.placements = .{};
|
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;
|
self.dirty = true;
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -318,7 +327,7 @@ pub const ImageStorage = struct {
|
|||||||
var it = self.placements.iterator();
|
var it = self.placements.iterator();
|
||||||
while (it.next()) |entry| {
|
while (it.next()) |entry| {
|
||||||
const img = self.imageById(entry.key_ptr.image_id) orelse continue;
|
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) {
|
if (rect.top_left.x <= x and rect.bottom_right.x >= x) {
|
||||||
entry.value_ptr.deinit(&t.screen);
|
entry.value_ptr.deinit(&t.screen);
|
||||||
self.placements.removeByPtr(entry.key_ptr);
|
self.placements.removeByPtr(entry.key_ptr);
|
||||||
@ -345,7 +354,7 @@ pub const ImageStorage = struct {
|
|||||||
var it = self.placements.iterator();
|
var it = self.placements.iterator();
|
||||||
while (it.next()) |entry| {
|
while (it.next()) |entry| {
|
||||||
const img = self.imageById(entry.key_ptr.image_id) orelse continue;
|
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
|
// We need to copy our pin to ensure we are at least at
|
||||||
// the top-left x.
|
// the top-left x.
|
||||||
@ -365,6 +374,14 @@ pub const ImageStorage = struct {
|
|||||||
.z => |v| {
|
.z => |v| {
|
||||||
var it = self.placements.iterator();
|
var it = self.placements.iterator();
|
||||||
while (it.next()) |entry| {
|
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) {
|
if (entry.value_ptr.z == v.z) {
|
||||||
const image_id = entry.key_ptr.image_id;
|
const image_id = entry.key_ptr.image_id;
|
||||||
entry.value_ptr.deinit(&t.screen);
|
entry.value_ptr.deinit(&t.screen);
|
||||||
@ -451,7 +468,7 @@ pub const ImageStorage = struct {
|
|||||||
var it = self.placements.iterator();
|
var it = self.placements.iterator();
|
||||||
while (it.next()) |entry| {
|
while (it.next()) |entry| {
|
||||||
const img = self.imageById(entry.key_ptr.image_id) orelse continue;
|
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 (target_pin.isBetween(rect.top_left, rect.bottom_right)) {
|
||||||
if (filter) |f| if (!f(filter_ctx, entry.value_ptr.*)) continue;
|
if (filter) |f| if (!f(filter_ctx, entry.value_ptr.*)) continue;
|
||||||
entry.value_ptr.deinit(&t.screen);
|
entry.value_ptr.deinit(&t.screen);
|
||||||
@ -576,8 +593,8 @@ pub const ImageStorage = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const Placement = struct {
|
pub const Placement = struct {
|
||||||
/// The tracked pin for this placement.
|
/// The location where this placement should be drawn.
|
||||||
pin: *PageList.Pin,
|
location: Location,
|
||||||
|
|
||||||
/// Offset of the x/y from the top-left of the cell.
|
/// Offset of the x/y from the top-left of the cell.
|
||||||
x_offset: u32 = 0,
|
x_offset: u32 = 0,
|
||||||
@ -596,11 +613,22 @@ pub const ImageStorage = struct {
|
|||||||
/// The z-index for this placement.
|
/// The z-index for this placement.
|
||||||
z: i32 = 0,
|
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(
|
pub fn deinit(
|
||||||
self: *const Placement,
|
self: *const Placement,
|
||||||
s: *terminal.Screen,
|
s: *terminal.Screen,
|
||||||
) void {
|
) 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.
|
/// 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
|
/// 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(
|
pub fn rect(
|
||||||
self: Placement,
|
self: Placement,
|
||||||
image: Image,
|
image: Image,
|
||||||
t: *const terminal.Terminal,
|
t: *const terminal.Terminal,
|
||||||
) Rect {
|
) ?Rect {
|
||||||
const grid_size = self.gridSize(image, t);
|
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,
|
.offset => |v| v,
|
||||||
.overflow => |v| v.end,
|
.overflow => |v| v.end,
|
||||||
};
|
};
|
||||||
@ -658,12 +691,12 @@ pub const ImageStorage = struct {
|
|||||||
// We need to sub one here because the x value is
|
// We need to sub one here because the x value is
|
||||||
// one width already. So if the image is width "1"
|
// one width already. So if the image is width "1"
|
||||||
// then we add zero to X because X itelf 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,
|
t.cols - 1,
|
||||||
);
|
);
|
||||||
|
|
||||||
return .{
|
return .{
|
||||||
.top_left = self.pin.*,
|
.top_left = pin.*,
|
||||||
.bottom_right = br,
|
.bottom_right = br,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -692,8 +725,8 @@ test "storage: add placement with zero placement id" {
|
|||||||
defer s.deinit(alloc, &t.screen);
|
defer s.deinit(alloc, &t.screen);
|
||||||
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
||||||
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
|
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, .{ .location = .{ .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 testing.expectEqual(@as(usize, 2), s.placements.count());
|
try testing.expectEqual(@as(usize, 2), s.placements.count());
|
||||||
try testing.expectEqual(@as(usize, 2), s.images.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 = 1 });
|
||||||
try s.addImage(alloc, .{ .id = 2 });
|
try s.addImage(alloc, .{ .id = 2 });
|
||||||
try s.addImage(alloc, .{ .id = 3 });
|
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, 1, .{ .location = .{ .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, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||||
|
|
||||||
s.dirty = false;
|
s.dirty = false;
|
||||||
s.delete(alloc, &t, .{ .all = true });
|
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 = 1 });
|
||||||
try s.addImage(alloc, .{ .id = 2 });
|
try s.addImage(alloc, .{ .id = 2 });
|
||||||
try s.addImage(alloc, .{ .id = 3 });
|
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, 1, .{ .location = .{ .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, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||||
|
|
||||||
s.dirty = false;
|
s.dirty = false;
|
||||||
s.delete(alloc, &t, .{ .all = true });
|
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 = 1 });
|
||||||
try s.addImage(alloc, .{ .id = 2 });
|
try s.addImage(alloc, .{ .id = 2 });
|
||||||
try s.addImage(alloc, .{ .id = 3 });
|
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, 1, .{ .location = .{ .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, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||||
|
|
||||||
s.dirty = false;
|
s.dirty = false;
|
||||||
s.delete(alloc, &t, .{ .all = 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 = 1 });
|
||||||
try s.addImage(alloc, .{ .id = 2 });
|
try s.addImage(alloc, .{ .id = 2 });
|
||||||
try s.addImage(alloc, .{ .id = 3 });
|
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, 1, .{ .location = .{ .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, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||||
|
|
||||||
s.dirty = false;
|
s.dirty = false;
|
||||||
s.delete(alloc, &t, .{ .id = .{ .image_id = 2 } });
|
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 = 1 });
|
||||||
try s.addImage(alloc, .{ .id = 2 });
|
try s.addImage(alloc, .{ .id = 2 });
|
||||||
try s.addImage(alloc, .{ .id = 3 });
|
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, 1, .{ .location = .{ .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, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||||
|
|
||||||
s.dirty = false;
|
s.dirty = false;
|
||||||
s.delete(alloc, &t, .{ .id = .{ .delete = true, .image_id = 2 } });
|
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 = 1 });
|
||||||
try s.addImage(alloc, .{ .id = 2 });
|
try s.addImage(alloc, .{ .id = 2 });
|
||||||
try s.addImage(alloc, .{ .id = 3 });
|
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, 1, .{ .location = .{ .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, 1, 2, .{ .location = .{ .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, 2, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) } });
|
||||||
|
|
||||||
s.dirty = false;
|
s.dirty = false;
|
||||||
s.delete(alloc, &t, .{ .id = .{
|
s.delete(alloc, &t, .{ .id = .{
|
||||||
@ -867,8 +900,8 @@ test "storage: delete intersecting cursor" {
|
|||||||
defer s.deinit(alloc, &t.screen);
|
defer s.deinit(alloc, &t.screen);
|
||||||
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
||||||
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
|
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, 1, .{ .location = .{ .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, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
|
||||||
|
|
||||||
t.screen.cursorAbsolute(12, 12);
|
t.screen.cursorAbsolute(12, 12);
|
||||||
|
|
||||||
@ -899,8 +932,8 @@ test "storage: delete intersecting cursor plus unused" {
|
|||||||
defer s.deinit(alloc, &t.screen);
|
defer s.deinit(alloc, &t.screen);
|
||||||
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
||||||
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
|
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, 1, .{ .location = .{ .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, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
|
||||||
|
|
||||||
t.screen.cursorAbsolute(12, 12);
|
t.screen.cursorAbsolute(12, 12);
|
||||||
|
|
||||||
@ -931,8 +964,8 @@ test "storage: delete intersecting cursor hits multiple" {
|
|||||||
defer s.deinit(alloc, &t.screen);
|
defer s.deinit(alloc, &t.screen);
|
||||||
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
||||||
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
|
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, 1, .{ .location = .{ .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, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
|
||||||
|
|
||||||
t.screen.cursorAbsolute(26, 26);
|
t.screen.cursorAbsolute(26, 26);
|
||||||
|
|
||||||
@ -957,8 +990,8 @@ test "storage: delete by column" {
|
|||||||
defer s.deinit(alloc, &t.screen);
|
defer s.deinit(alloc, &t.screen);
|
||||||
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
||||||
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
|
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, 1, .{ .location = .{ .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, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
|
||||||
|
|
||||||
s.dirty = false;
|
s.dirty = false;
|
||||||
s.delete(alloc, &t, .{ .column = .{
|
s.delete(alloc, &t, .{ .column = .{
|
||||||
@ -988,9 +1021,9 @@ test "storage: delete by column 1x1" {
|
|||||||
var s: ImageStorage = .{};
|
var s: ImageStorage = .{};
|
||||||
defer s.deinit(alloc, &t.screen);
|
defer s.deinit(alloc, &t.screen);
|
||||||
try s.addImage(alloc, .{ .id = 1, .width = 1, .height = 1 });
|
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, 1, .{ .location = .{ .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, 2, .{ .location = .{ .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, 3, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 2, .y = 0 }) } });
|
||||||
|
|
||||||
s.delete(alloc, &t, .{ .column = .{
|
s.delete(alloc, &t, .{ .column = .{
|
||||||
.delete = false,
|
.delete = false,
|
||||||
@ -1023,8 +1056,8 @@ test "storage: delete by row" {
|
|||||||
defer s.deinit(alloc, &t.screen);
|
defer s.deinit(alloc, &t.screen);
|
||||||
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
try s.addImage(alloc, .{ .id = 1, .width = 50, .height = 50 });
|
||||||
try s.addImage(alloc, .{ .id = 2, .width = 25, .height = 25 });
|
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, 1, .{ .location = .{ .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, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .x = 25, .y = 25 }) } });
|
||||||
|
|
||||||
s.dirty = false;
|
s.dirty = false;
|
||||||
s.delete(alloc, &t, .{ .row = .{
|
s.delete(alloc, &t, .{ .row = .{
|
||||||
@ -1054,9 +1087,9 @@ test "storage: delete by row 1x1" {
|
|||||||
var s: ImageStorage = .{};
|
var s: ImageStorage = .{};
|
||||||
defer s.deinit(alloc, &t.screen);
|
defer s.deinit(alloc, &t.screen);
|
||||||
try s.addImage(alloc, .{ .id = 1, .width = 1, .height = 1 });
|
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, 1, .{ .location = .{ .pin = try trackPin(&t, .{ .y = 0 }) } });
|
||||||
try s.addPlacement(alloc, 1, 2, .{ .pin = try trackPin(&t, .{ .y = 1 }) });
|
try s.addPlacement(alloc, 1, 2, .{ .location = .{ .pin = try trackPin(&t, .{ .y = 1 }) } });
|
||||||
try s.addPlacement(alloc, 1, 3, .{ .pin = try trackPin(&t, .{ .y = 2 }) });
|
try s.addPlacement(alloc, 1, 3, .{ .location = .{ .pin = try trackPin(&t, .{ .y = 2 }) } });
|
||||||
|
|
||||||
s.delete(alloc, &t, .{ .row = .{
|
s.delete(alloc, &t, .{ .row = .{
|
||||||
.delete = false,
|
.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 fastmem = @import("../fastmem.zig");
|
||||||
const color = @import("color.zig");
|
const color = @import("color.zig");
|
||||||
const hyperlink = @import("hyperlink.zig");
|
const hyperlink = @import("hyperlink.zig");
|
||||||
|
const kitty = @import("kitty.zig");
|
||||||
const sgr = @import("sgr.zig");
|
const sgr = @import("sgr.zig");
|
||||||
const style = @import("style.zig");
|
const style = @import("style.zig");
|
||||||
const size = @import("size.zig");
|
const size = @import("size.zig");
|
||||||
@ -825,6 +826,9 @@ pub const Page = struct {
|
|||||||
src_cell.style_id,
|
src_cell.style_id,
|
||||||
) orelse 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.hyperlink = true;
|
||||||
dst_row.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.grapheme = false;
|
||||||
src_row.hyperlink = false;
|
src_row.hyperlink = false;
|
||||||
src_row.styled = 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 (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
|
// Zero the cells as u64s since empirically this seems
|
||||||
// to be a bit faster than using @memset(cells, .{})
|
// to be a bit faster than using @memset(cells, .{})
|
||||||
@memset(@as([]u64, @ptrCast(cells)), 0);
|
@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 {
|
pub fn setGraphemes(self: *Page, row: *Row, cell: *Cell, cps: []u21) Allocator.Error!void {
|
||||||
defer self.assertIntegrity();
|
defer self.assertIntegrity();
|
||||||
|
|
||||||
assert(cell.hasText());
|
assert(cell.codepoint() > 0);
|
||||||
assert(cell.content_tag == .codepoint);
|
assert(cell.content_tag == .codepoint);
|
||||||
|
|
||||||
const cell_offset = getOffset(Cell, self.memory, cell);
|
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 {
|
pub fn appendGrapheme(self: *Page, row: *Row, cell: *Cell, cp: u21) Allocator.Error!void {
|
||||||
defer self.assertIntegrity();
|
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);
|
const cell_offset = getOffset(Cell, self.memory, cell);
|
||||||
var map = self.grapheme_map.map(self.memory);
|
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
|
/// Returns the codepoints for the given cell. These are the codepoints
|
||||||
/// in addition to the first codepoint. The first codepoint is NOT
|
/// in addition to the first codepoint. The first codepoint is NOT
|
||||||
/// included since it is on the cell itself.
|
/// 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 cell_offset = getOffset(Cell, self.memory, cell);
|
||||||
const map = self.grapheme_map.map(self.memory);
|
const map = self.grapheme_map.map(self.memory);
|
||||||
const slice = map.get(cell_offset) orelse return null;
|
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.
|
/// running program, or "unknown" if it was never set.
|
||||||
semantic_prompt: SemanticPrompt = .unknown,
|
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.
|
/// Semantic prompt type.
|
||||||
pub const SemanticPrompt = enum(u3) {
|
pub const SemanticPrompt = enum(u3) {
|
||||||
@ -1673,6 +1695,12 @@ pub const Cell = packed struct(u64) {
|
|||||||
return @as(u64, @bitCast(self)) == 0;
|
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 {
|
pub fn hasText(self: Cell) bool {
|
||||||
return switch (self.content_tag) {
|
return switch (self.content_tag) {
|
||||||
.codepoint,
|
.codepoint,
|
||||||
|
Reference in New Issue
Block a user