mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-08-24 16:11:43 +00:00
terminal/kitty: add relative placement storage
This commit is contained in:
@@ -319,6 +319,10 @@ pub const State = struct {
|
||||
// placement itself.
|
||||
continue;
|
||||
},
|
||||
|
||||
// Nothing creates relative placements yet; rendering
|
||||
// support comes with the protocol wiring.
|
||||
.relative => continue,
|
||||
}
|
||||
|
||||
// Get the image for the placement
|
||||
|
||||
@@ -602,7 +602,7 @@ fn computeViewportPos(
|
||||
// screen position — they are rendered inline by the text layout.
|
||||
const pin = switch (p.location) {
|
||||
.pin => |pin| pin,
|
||||
.virtual => return .{ .col = 0, .row = 0, .visible = false },
|
||||
.virtual, .relative => return .{ .col = 0, .row = 0, .visible = false },
|
||||
};
|
||||
if (pin.garbage) return .{ .col = 0, .row = 0, .visible = false };
|
||||
|
||||
@@ -1632,7 +1632,7 @@ test "placement_render_info returns all fields" {
|
||||
const entry = iter.?.entry.?;
|
||||
const pin = switch (entry.value_ptr.location) {
|
||||
.pin => |pin| pin,
|
||||
.virtual => unreachable,
|
||||
.virtual, .relative => unreachable,
|
||||
};
|
||||
pin.garbage = true;
|
||||
|
||||
|
||||
@@ -304,9 +304,11 @@ fn display(
|
||||
return result;
|
||||
};
|
||||
|
||||
// Apply cursor movement setting. This only applies to pin placements.
|
||||
// Apply cursor movement setting. This only applies to pin placements:
|
||||
// relative placements never move the cursor regardless of C=, just
|
||||
// like kitty.
|
||||
switch (p.location) {
|
||||
.virtual => {},
|
||||
.virtual, .relative => {},
|
||||
.pin => |pin| switch (d.cursor_movement) {
|
||||
.none => {},
|
||||
.after => {
|
||||
@@ -1320,11 +1322,11 @@ test "kittygfx placement moves cursor past a tall image" {
|
||||
}).?;
|
||||
const first_pin = switch (first.location) {
|
||||
.pin => |pin| pin,
|
||||
.virtual => unreachable,
|
||||
.virtual, .relative => unreachable,
|
||||
};
|
||||
const second_pin = switch (second.location) {
|
||||
.pin => |pin| pin,
|
||||
.virtual => unreachable,
|
||||
.virtual, .relative => unreachable,
|
||||
};
|
||||
const first_y = t.screens.active.pages.pointFromPin(
|
||||
.screen,
|
||||
|
||||
@@ -307,10 +307,14 @@ pub const ImageStorage = struct {
|
||||
|
||||
log.debug("addImage image={}", .{img.withoutData()});
|
||||
|
||||
// Retransmitting a specific image ID replaces the old image and all
|
||||
// of its placements, as required by the Kitty graphics protocol.
|
||||
if (gop.found_existing) {
|
||||
// Retransmitting a specific image ID replaces the old image and all
|
||||
// of its placements, as required by the Kitty graphics protocol.
|
||||
self.removePlacementsByImageId(s, img.id);
|
||||
|
||||
// Relative placements parented to the removed placements go too.
|
||||
_ = self.removeOrphans(s, null);
|
||||
|
||||
self.total_bytes -= gop.value_ptr.data.len();
|
||||
gop.value_ptr.deinit(alloc);
|
||||
}
|
||||
@@ -362,13 +366,11 @@ pub const ImageStorage = struct {
|
||||
p,
|
||||
});
|
||||
|
||||
// Tracked pins are marked garbage when their underlying history is
|
||||
// pruned. Kitty removes placements once they scroll out of retained
|
||||
// history, so reclaim those placements before growing the map for a
|
||||
// new one. If allocation below fails, the sweep is still a content
|
||||
// mutation and must be visible to consumers.
|
||||
const removed_garbage = self.removeGarbagePlacements(s);
|
||||
errdefer if (removed_garbage) self.markMutated(io);
|
||||
// When we add a placement, take this opportunity to reap garbage.
|
||||
// Garbage are placements that disappeared off scrollback (were
|
||||
// pruned). We don't clear Kitty state in the hot path so we do
|
||||
// this opportunistically here.
|
||||
self.reapGarbagePlacements(io, s);
|
||||
|
||||
// The important piece here is that the placement ID needs to
|
||||
// be marked internal if it is zero. This allows multiple placements
|
||||
@@ -405,6 +407,7 @@ pub const ImageStorage = struct {
|
||||
}
|
||||
gop.value_ptr.* = p;
|
||||
|
||||
// This always mutates
|
||||
self.markMutated(io);
|
||||
}
|
||||
|
||||
@@ -467,13 +470,14 @@ pub const ImageStorage = struct {
|
||||
const p: *Placement = entry.value_ptr;
|
||||
|
||||
// Virtual placements follow their placeholder cells and
|
||||
// are never adjusted by scrolls, matching kitty.
|
||||
// relative placements follow their parents, so neither is
|
||||
// ever adjusted by scrolls, matching kitty.
|
||||
const pin: *PageList.Pin = switch (p.location) {
|
||||
.pin => |pin| pin,
|
||||
.virtual => continue,
|
||||
.virtual, .relative => continue,
|
||||
};
|
||||
|
||||
// Pruned placements are reaped by removeGarbagePlacements.
|
||||
// Pruned placements are reaped by reapGarbagePlacements.
|
||||
if (pin.garbage) continue;
|
||||
|
||||
// Placements anchored outside the active area (scrollback)
|
||||
@@ -550,8 +554,7 @@ pub const ImageStorage = struct {
|
||||
// placement is deleted, like kitty. The image itself is
|
||||
// retained for future placements.
|
||||
if (!visible) {
|
||||
p.deinit(s);
|
||||
self.removePlacementByPtr(entry.key_ptr);
|
||||
self.removePlacement(s, entry);
|
||||
mutated = true;
|
||||
continue;
|
||||
}
|
||||
@@ -569,31 +572,47 @@ pub const ImageStorage = struct {
|
||||
};
|
||||
}
|
||||
|
||||
if (mutated) self.markMutated(io);
|
||||
if (mutated) {
|
||||
// Placements deleted by clipping may orphan relative placements.
|
||||
// Orphans have no pins so this never touches the restores.
|
||||
_ = self.removeOrphans(s, null);
|
||||
self.markMutated(io);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Remove pin-backed placements whose tracked content has been pruned.
|
||||
/// Virtual placements have no tracked screen location and are retained.
|
||||
fn removeGarbagePlacements(
|
||||
/// Reap pin-backed placements whose tracked content has been pruned
|
||||
/// from history, along with any relative placements orphaned by
|
||||
/// that, marking the content mutation.
|
||||
///
|
||||
/// Virtual and relative placements have no tracked screen location and
|
||||
/// are never pruned themselves. Kitty removes placements once they
|
||||
/// scroll out of retained history.
|
||||
fn reapGarbagePlacements(
|
||||
self: *ImageStorage,
|
||||
io: std.Io,
|
||||
s: *terminal.Screen,
|
||||
) bool {
|
||||
) void {
|
||||
var removed = false;
|
||||
var it = self.placements.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const pin = switch (entry.value_ptr.location) {
|
||||
.pin => |pin| pin,
|
||||
.virtual => continue,
|
||||
.virtual, .relative => continue,
|
||||
};
|
||||
if (!pin.garbage) continue;
|
||||
|
||||
entry.value_ptr.deinit(s);
|
||||
self.removePlacementByPtr(entry.key_ptr);
|
||||
self.removePlacement(s, entry);
|
||||
removed = true;
|
||||
}
|
||||
if (!removed) return;
|
||||
|
||||
return removed;
|
||||
// If we removed a placement, then also remove any orphan
|
||||
// children if this was a parent.
|
||||
_ = self.removeOrphans(s, null);
|
||||
|
||||
self.markMutated(io);
|
||||
}
|
||||
|
||||
fn clearPlacements(self: *ImageStorage, s: *terminal.Screen) void {
|
||||
@@ -610,23 +629,250 @@ pub const ImageStorage = struct {
|
||||
var it = self.placements.iterator();
|
||||
while (it.next()) |entry| {
|
||||
if (entry.key_ptr.image_id != image_id) continue;
|
||||
entry.value_ptr.deinit(s);
|
||||
self.removePlacementByPtr(entry.key_ptr);
|
||||
self.removePlacement(s, entry);
|
||||
}
|
||||
}
|
||||
|
||||
fn removePlacementByPtr(self: *ImageStorage, key: *PlacementKey) void {
|
||||
const img = self.images.getPtr(key.image_id).?;
|
||||
/// Remove a placement by its map entry, releasing any tracked pin
|
||||
/// and keeping the image's placement count in sync. The entry must
|
||||
/// point into the placements map (via iteration or getEntry).
|
||||
fn removePlacement(
|
||||
self: *ImageStorage,
|
||||
s: *terminal.Screen,
|
||||
entry: PlacementMap.Entry,
|
||||
) void {
|
||||
entry.value_ptr.deinit(s);
|
||||
const img = self.images.getPtr(entry.key_ptr.image_id).?;
|
||||
assert(img.metadata.placement_count > 0);
|
||||
img.metadata.placement_count -= 1;
|
||||
self.placements.removeByPtr(key);
|
||||
self.placements.removeByPtr(entry.key_ptr);
|
||||
}
|
||||
|
||||
/// Remove relative placements whose parent placement no longer
|
||||
/// exists, keeping a relative placement's lifetime tied to its
|
||||
/// parent chain. Chains can be several links deep, so this loops
|
||||
/// until a pass removes nothing to take out entire orphaned
|
||||
/// subtrees. Returns true if anything was removed.
|
||||
///
|
||||
/// If `delete_unused` is non-null, an image left without placements
|
||||
/// by the reap is freed using it. This matches uppercase delete
|
||||
/// semantics; every other removal path retains image data and
|
||||
/// passes null.
|
||||
///
|
||||
/// This must be called, outside of any placements iteration, after
|
||||
/// every operation that removes placements. Kitty gets the same
|
||||
/// effect lazily by dropping unresolvable placements while drawing;
|
||||
/// we do it eagerly because our renderer never mutates terminal
|
||||
/// state.
|
||||
fn removeOrphans(
|
||||
self: *ImageStorage,
|
||||
s: *terminal.Screen,
|
||||
delete_unused: ?Allocator,
|
||||
) bool {
|
||||
var removed_any = false;
|
||||
var removed = true;
|
||||
while (removed) {
|
||||
removed = false;
|
||||
var it = self.placements.iterator();
|
||||
while (it.next()) |entry| {
|
||||
const rel = switch (entry.value_ptr.location) {
|
||||
.relative => |rel| rel,
|
||||
.pin, .virtual => continue,
|
||||
};
|
||||
if (self.placements.contains(rel.parent)) continue;
|
||||
|
||||
// Parent is gone, remove this placement.
|
||||
const image_id = entry.key_ptr.image_id;
|
||||
self.removePlacement(s, entry);
|
||||
removed = true;
|
||||
removed_any = true;
|
||||
|
||||
if (delete_unused) |alloc| self.deleteIfUnused(
|
||||
alloc,
|
||||
image_id,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return removed_any;
|
||||
}
|
||||
|
||||
/// Maximum number of parent links in a relative placement chain,
|
||||
/// matching the minimum specified in the spec and the actual
|
||||
/// limit defined by Kitty at the time of authoring.
|
||||
pub const parent_chain_limit = 8;
|
||||
|
||||
pub const ParentError = error{
|
||||
/// The parent image (P=) does not exist.
|
||||
ParentImageNotFound,
|
||||
/// The parent image exists but the requested placement (Q=, or
|
||||
/// any placement when Q is omitted) does not.
|
||||
ParentPlacementNotFound,
|
||||
/// The placement refers to itself as its own parent.
|
||||
SelfParent,
|
||||
/// The parent chain loops back to the placement being created.
|
||||
Cycle,
|
||||
/// The parent chain exceeds parent_chain_limit links.
|
||||
TooDeep,
|
||||
/// An ancestor in the chain no longer exists. This should not
|
||||
/// happen since removeOrphans keeps chains intact, but we
|
||||
/// check anyway rather than trusting the invariant.
|
||||
AncestorNotFound,
|
||||
};
|
||||
|
||||
/// Resolve and validate the parent reference (P=/Q=) of a relative
|
||||
/// placement, returning the concrete key of the parent placement.
|
||||
/// `child` is the key of the placement being created when it is
|
||||
/// addressable (an explicit placement ID); null for placements that
|
||||
/// will receive a fresh internal ID, which nothing can refer to yet.
|
||||
///
|
||||
/// Checks: the parent must exist, the placement must not
|
||||
/// parent itself, and the resulting ancestor chain must be acyclic
|
||||
/// and within parent_chain_limit.
|
||||
pub fn resolveParent(
|
||||
self: *ImageStorage,
|
||||
io: std.Io,
|
||||
s: *terminal.Screen,
|
||||
child: ?PlacementKey,
|
||||
parent_image_id: u32,
|
||||
parent_placement_id: u32,
|
||||
) ParentError!PlacementKey {
|
||||
// Reap placements whose content has been pruned from history
|
||||
// before resolving: a pruned parent must be reported as missing,
|
||||
// not accepted and then immediately reaped by addPlacement's own
|
||||
// sweep, which would store an orphan.
|
||||
self.reapGarbagePlacements(io, s);
|
||||
|
||||
// If the parent doesn't exist we're already failed.
|
||||
if (!self.images.contains(parent_image_id)) {
|
||||
return error.ParentImageNotFound;
|
||||
}
|
||||
|
||||
// Find the parent
|
||||
const parent: PlacementKey = parent: {
|
||||
// An explicit parent placement ID (Q=) selects exactly that
|
||||
// placement.
|
||||
if (parent_placement_id > 0) {
|
||||
const key: PlacementKey = .{
|
||||
.image_id = parent_image_id,
|
||||
.placement_id = .{
|
||||
.tag = .external,
|
||||
.id = parent_placement_id,
|
||||
},
|
||||
};
|
||||
if (!self.placements.contains(key)) {
|
||||
return error.ParentPlacementNotFound;
|
||||
}
|
||||
break :parent key;
|
||||
}
|
||||
|
||||
// No Q: pick a placement of the parent image. Kitty picks
|
||||
// the oldest surviving placement; our placement map doesn't
|
||||
// track creation order so we pick by PlacementId.preferredOver
|
||||
// instead. This is unspecified so any behavior here is fine.
|
||||
// In practice the parent image has a single placement, and
|
||||
// clients use Q when it doesn't.
|
||||
const best: PlacementId = best: {
|
||||
var best: ?PlacementId = null;
|
||||
var it = self.placements.keyIterator();
|
||||
while (it.next()) |key| {
|
||||
if (key.image_id != parent_image_id) continue;
|
||||
const id = key.placement_id;
|
||||
const b = best orelse {
|
||||
best = id;
|
||||
continue;
|
||||
};
|
||||
if (id.preferredOver(b)) best = id;
|
||||
}
|
||||
break :best best orelse return error.ParentPlacementNotFound;
|
||||
};
|
||||
|
||||
break :parent .{
|
||||
.image_id = parent_image_id,
|
||||
.placement_id = best,
|
||||
};
|
||||
};
|
||||
|
||||
// A placement cannot be its own parent. This must be checked
|
||||
// before the chain walk so it reports EINVAL, not ECYCLE.
|
||||
if (child) |c| if (parent.eql(c)) return error.SelfParent;
|
||||
|
||||
// Walk the would-be ancestor chain. `depth` counts parent links,
|
||||
// starting at one for the link we are about to create.
|
||||
var depth: usize = 1;
|
||||
var key = parent;
|
||||
while (true) {
|
||||
if (child) |c| if (key.eql(c)) return error.Cycle;
|
||||
const p = self.placements.get(key) orelse return error.AncestorNotFound;
|
||||
const rel = switch (p.location) {
|
||||
.relative => |rel| rel,
|
||||
// A pin or virtual placement is the chain root.
|
||||
.pin, .virtual => break,
|
||||
};
|
||||
if (depth >= parent_chain_limit) return error.TooDeep;
|
||||
depth += 1;
|
||||
key = rel.parent;
|
||||
}
|
||||
|
||||
return parent;
|
||||
}
|
||||
|
||||
/// The result of resolving a relative placement's parent chain:
|
||||
/// the chain's root placement (always pin or virtual, never
|
||||
/// relative) and the total cell offset from the root's origin.
|
||||
pub const ResolvedChain = struct {
|
||||
root_key: PlacementKey,
|
||||
root: Placement,
|
||||
horizontal_offset: i32,
|
||||
vertical_offset: i32,
|
||||
};
|
||||
|
||||
/// Resolve a relative placement's parent chain to its root,
|
||||
/// accumulating the cell offsets (H=/V=) of every link on the way.
|
||||
/// The placement's position is the root's origin plus the accumulated
|
||||
/// offsets.
|
||||
///
|
||||
/// Returns null when the chain is broken or too deep. Neither can
|
||||
/// normally happen for stored placements (resolveParent validates
|
||||
/// chains and removeOrphans removes broken ones), with one kitty
|
||||
/// quirk: replacing an ancestor placement can deepen the chains
|
||||
/// below it beyond parent_chain_limit after the fact.
|
||||
pub fn resolveChain(
|
||||
self: *const ImageStorage,
|
||||
rel: Placement.Relative,
|
||||
) ?ResolvedChain {
|
||||
var horizontal = rel.horizontal_offset;
|
||||
var vertical = rel.vertical_offset;
|
||||
var key = rel.parent;
|
||||
var depth: usize = 1;
|
||||
while (true) {
|
||||
const p = self.placements.get(key) orelse return null;
|
||||
switch (p.location) {
|
||||
.relative => |parent_rel| {
|
||||
if (depth >= parent_chain_limit) return null;
|
||||
depth += 1;
|
||||
horizontal +|= parent_rel.horizontal_offset;
|
||||
vertical +|= parent_rel.vertical_offset;
|
||||
key = parent_rel.parent;
|
||||
},
|
||||
|
||||
.pin, .virtual => return .{
|
||||
.root_key = key,
|
||||
.root = p,
|
||||
.horizontal_offset = horizontal,
|
||||
.vertical_offset = vertical,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the placement that a unicode placeholder cell referencing
|
||||
/// this image/placement ID pair targets, or null if there is none. A
|
||||
/// zero placement ID targets one of the image's virtual placements,
|
||||
/// chosen by PlacementId.preferredOver so the choice is stable (an
|
||||
/// image rarely has more than one).
|
||||
/// image rarely has more than one). This is the shared lookup used
|
||||
/// both for sizing placeholder runs and for positioning relative
|
||||
/// placements whose chain roots at a virtual placement.
|
||||
pub fn placeholderTarget(
|
||||
self: *const ImageStorage,
|
||||
image_id: u32,
|
||||
@@ -694,8 +940,11 @@ pub const ImageStorage = struct {
|
||||
const placements_before = self.placements.count();
|
||||
const images_before = self.images.count();
|
||||
|
||||
// Delete unused placements and images
|
||||
// Delete unused placements and images. Orphaned relative
|
||||
// placements must be reaped before the image sweep so that
|
||||
// their images are reclaimable too.
|
||||
self.deleteVisiblePlacements(alloc, t, true);
|
||||
_ = self.removeOrphans(t.screens.active, null);
|
||||
var image_it = self.images.iterator();
|
||||
while (image_it.next()) |entry| {
|
||||
self.deleteIfUnused(alloc, entry.key_ptr.*);
|
||||
@@ -817,8 +1066,7 @@ pub const ImageStorage = struct {
|
||||
const img = self.imageById(entry.key_ptr.image_id) orelse continue;
|
||||
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.screens.active);
|
||||
self.removePlacementByPtr(entry.key_ptr);
|
||||
self.removePlacement(t.screens.active, entry);
|
||||
if (v.delete) self.deleteIfUnused(alloc, img.id);
|
||||
}
|
||||
}
|
||||
@@ -846,8 +1094,7 @@ pub const ImageStorage = struct {
|
||||
var target_pin_copy = target_pin;
|
||||
target_pin_copy.x = rect.top_left.x;
|
||||
if (target_pin_copy.isBetween(rect.top_left, rect.bottom_right)) {
|
||||
entry.value_ptr.deinit(t.screens.active);
|
||||
self.removePlacementByPtr(entry.key_ptr);
|
||||
self.removePlacement(t.screens.active, entry);
|
||||
if (v.delete) self.deleteIfUnused(alloc, img.id);
|
||||
}
|
||||
}
|
||||
@@ -857,7 +1104,9 @@ pub const ImageStorage = struct {
|
||||
var it = self.placements.iterator();
|
||||
while (it.next()) |entry| {
|
||||
switch (entry.value_ptr.location) {
|
||||
.pin => {},
|
||||
// Relative placements carry their own z value and
|
||||
// are matched by z deletes just like kitty does.
|
||||
.pin, .relative => {},
|
||||
|
||||
// Virtual placeholders cannot delete by z according
|
||||
// to the spec.
|
||||
@@ -866,8 +1115,7 @@ pub const ImageStorage = struct {
|
||||
|
||||
if (entry.value_ptr.z == v.z) {
|
||||
const image_id = entry.key_ptr.image_id;
|
||||
entry.value_ptr.deinit(t.screens.active);
|
||||
self.removePlacementByPtr(entry.key_ptr);
|
||||
self.removePlacement(t.screens.active, entry);
|
||||
if (v.delete) self.deleteIfUnused(alloc, image_id);
|
||||
}
|
||||
}
|
||||
@@ -885,8 +1133,7 @@ pub const ImageStorage = struct {
|
||||
if (entry.key_ptr.image_id < v.first or
|
||||
entry.key_ptr.image_id > v.last) continue;
|
||||
|
||||
entry.value_ptr.deinit(t.screens.active);
|
||||
self.removePlacementByPtr(entry.key_ptr);
|
||||
self.removePlacement(t.screens.active, entry);
|
||||
}
|
||||
|
||||
// Uppercase deletion also frees matching images that are now
|
||||
@@ -905,6 +1152,21 @@ pub const ImageStorage = struct {
|
||||
// deleted!
|
||||
.animation_frames => {},
|
||||
}
|
||||
|
||||
// Deleting placements orphans any relative placements parented
|
||||
// to them (transitively). Their lifetime is tied to the parent
|
||||
// so they are removed as well, and an uppercase delete also
|
||||
// frees any image the cascade leaves without placements (the
|
||||
// per-branch deleteIfUnused calls above ran while the orphans
|
||||
// still counted as placements).
|
||||
const delete_unused: bool = switch (cmd) {
|
||||
.all, .intersect_cursor, .animation_frames => |v| v,
|
||||
inline else => |v| v.delete,
|
||||
};
|
||||
_ = self.removeOrphans(
|
||||
t.screens.active,
|
||||
if (delete_unused) alloc else null,
|
||||
);
|
||||
}
|
||||
|
||||
/// Delete only non-virtual placements that intersect the active screen.
|
||||
@@ -921,7 +1183,10 @@ pub const ImageStorage = struct {
|
||||
while (it.next()) |entry| {
|
||||
const pin = switch (entry.value_ptr.location) {
|
||||
.pin => |pin| pin,
|
||||
.virtual => continue,
|
||||
// Virtual placements are never selected by visible
|
||||
// deletes per the protocol. Relative placements are
|
||||
// removed with their parents instead (removeOrphans).
|
||||
.virtual, .relative => continue,
|
||||
};
|
||||
if (pin.garbage) continue;
|
||||
|
||||
@@ -938,8 +1203,7 @@ pub const ImageStorage = struct {
|
||||
}
|
||||
|
||||
const image_id = entry.key_ptr.image_id;
|
||||
entry.value_ptr.deinit(t.screens.active);
|
||||
self.removePlacementByPtr(entry.key_ptr);
|
||||
self.removePlacement(t.screens.active, entry);
|
||||
if (delete_unused) self.deleteIfUnused(alloc, image_id);
|
||||
}
|
||||
}
|
||||
@@ -965,8 +1229,7 @@ pub const ImageStorage = struct {
|
||||
.id = placement_id,
|
||||
},
|
||||
})) |entry| {
|
||||
entry.value_ptr.deinit(s);
|
||||
self.removePlacementByPtr(entry.key_ptr);
|
||||
self.removePlacement(s, entry);
|
||||
matched = true;
|
||||
}
|
||||
|
||||
@@ -1005,8 +1268,7 @@ pub const ImageStorage = struct {
|
||||
const rect = entry.value_ptr.rect(img, t) orelse continue;
|
||||
if (rect.contains(target_pin)) {
|
||||
if (filter) |f| if (!f(filter_ctx, entry.value_ptr.*)) continue;
|
||||
entry.value_ptr.deinit(t.screens.active);
|
||||
self.removePlacementByPtr(entry.key_ptr);
|
||||
self.removePlacement(t.screens.active, entry);
|
||||
if (delete_unused) self.deleteIfUnused(alloc, img.id);
|
||||
}
|
||||
}
|
||||
@@ -1071,8 +1333,13 @@ pub const ImageStorage = struct {
|
||||
|
||||
// Evicting anything is a content mutation. This matters for the
|
||||
// setLimit path in particular, which doesn't otherwise mark it.
|
||||
// Evicted placements can also orphan relative placements of
|
||||
// other images, which must be reaped along with them.
|
||||
const images_before = self.images.count();
|
||||
defer if (self.images.count() != images_before) self.markMutated(io);
|
||||
defer if (self.images.count() != images_before) {
|
||||
_ = self.removeOrphans(s, null);
|
||||
self.markMutated(io);
|
||||
};
|
||||
|
||||
var evicted: usize = 0;
|
||||
while (evicted < req) {
|
||||
@@ -1092,8 +1359,7 @@ pub const ImageStorage = struct {
|
||||
var p_it = self.placements.iterator();
|
||||
while (p_it.next()) |entry| {
|
||||
if (entry.key_ptr.image_id == c.id) {
|
||||
entry.value_ptr.deinit(s);
|
||||
self.removePlacementByPtr(entry.key_ptr);
|
||||
self.removePlacement(s, entry);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1146,6 +1412,10 @@ pub const ImageStorage = struct {
|
||||
pub const PlacementKey = struct {
|
||||
image_id: u32,
|
||||
placement_id: PlacementId,
|
||||
|
||||
pub fn eql(self: PlacementKey, other: PlacementKey) bool {
|
||||
return std.meta.eql(self, other);
|
||||
}
|
||||
};
|
||||
|
||||
/// Internal placement IDs are assigned by us for placements created
|
||||
@@ -1191,7 +1461,27 @@ pub const ImageStorage = struct {
|
||||
pin: *PageList.Pin,
|
||||
|
||||
/// Virtual placement (U=1) for unicode placeholders.
|
||||
virtual: void,
|
||||
virtual,
|
||||
|
||||
/// Placed relative to a parent placement (P=/Q=). The
|
||||
/// placement has no screen position of its own; it is
|
||||
/// positioned at render time by resolving the parent chain
|
||||
/// to its root (see resolveChain) and therefore follows the
|
||||
/// parent through scrolls automatically. Its lifetime is
|
||||
/// tied to the parent: removing any ancestor removes it
|
||||
/// too (see removeOrphans).
|
||||
relative: Relative,
|
||||
};
|
||||
|
||||
pub const Relative = struct {
|
||||
/// The parent placement. Resolved to a concrete key when
|
||||
/// the placement is created, so the parent is guaranteed
|
||||
/// to exist at that point.
|
||||
parent: PlacementKey,
|
||||
|
||||
/// Cell offsets from the parent's origin (H=/V=).
|
||||
horizontal_offset: i32 = 0,
|
||||
vertical_offset: i32 = 0,
|
||||
};
|
||||
|
||||
pub fn deinit(
|
||||
@@ -1200,7 +1490,7 @@ pub const ImageStorage = struct {
|
||||
) void {
|
||||
switch (self.location) {
|
||||
.pin => |p| s.pages.untrackPin(p),
|
||||
.virtual => {},
|
||||
.virtual, .relative => {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1459,6 +1749,11 @@ pub const ImageStorage = struct {
|
||||
/// Returns a selection of the entire rectangle this placement
|
||||
/// occupies within the screen. This can return null for a virtual
|
||||
/// placement or when unavailable pixel geometry makes it empty.
|
||||
///
|
||||
/// Relative placements also return null: they have no screen
|
||||
/// position of their own, so geometric queries (delete by
|
||||
/// intersection, row, column, etc.) never match them directly.
|
||||
/// They are instead removed when their parent chain is removed.
|
||||
pub fn rect(
|
||||
self: Placement,
|
||||
image: Image,
|
||||
@@ -1467,7 +1762,7 @@ pub const ImageStorage = struct {
|
||||
const grid_size = self.gridSize(image, t);
|
||||
const pin = switch (self.location) {
|
||||
.pin => |p| p,
|
||||
.virtual => return null,
|
||||
.virtual, .relative => return null,
|
||||
};
|
||||
if (pin.garbage) return null;
|
||||
|
||||
@@ -3623,6 +3918,136 @@ test "storage: scroll margins multi-line scroll up with scrollback" {
|
||||
try testing.expectEqual(@as(usize, 0), storage.placements.count());
|
||||
}
|
||||
|
||||
test "storage: resolveChain accumulates offsets to the pin root" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const io = testing.io;
|
||||
var t = try terminal.Terminal.init(io, alloc, .{ .rows = 5, .cols = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
var s: ImageStorage = .{};
|
||||
defer s.deinit(alloc, t.screens.active);
|
||||
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
|
||||
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{
|
||||
.location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) },
|
||||
});
|
||||
try s.addPlacement(io, alloc, t.screens.active, 1, 2, .{
|
||||
.location = .{ .relative = .{
|
||||
.parent = .{
|
||||
.image_id = 1,
|
||||
.placement_id = .{ .tag = .external, .id = 1 },
|
||||
},
|
||||
.horizontal_offset = 2,
|
||||
.vertical_offset = 1,
|
||||
} },
|
||||
});
|
||||
try s.addPlacement(io, alloc, t.screens.active, 1, 3, .{
|
||||
.location = .{ .relative = .{
|
||||
.parent = .{
|
||||
.image_id = 1,
|
||||
.placement_id = .{ .tag = .external, .id = 2 },
|
||||
},
|
||||
.horizontal_offset = -1,
|
||||
.vertical_offset = 4,
|
||||
} },
|
||||
});
|
||||
|
||||
const grandchild = s.placements.get(.{
|
||||
.image_id = 1,
|
||||
.placement_id = .{ .tag = .external, .id = 3 },
|
||||
}).?;
|
||||
const chain = s.resolveChain(grandchild.location.relative).?;
|
||||
try testing.expect(chain.root_key.eql(.{
|
||||
.image_id = 1,
|
||||
.placement_id = .{ .tag = .external, .id = 1 },
|
||||
}));
|
||||
try testing.expect(chain.root.location == .pin);
|
||||
try testing.expectEqual(@as(i32, 1), chain.horizontal_offset);
|
||||
try testing.expectEqual(@as(i32, 5), chain.vertical_offset);
|
||||
}
|
||||
|
||||
test "storage: resolveChain finds virtual roots" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const io = testing.io;
|
||||
var t = try terminal.Terminal.init(io, alloc, .{ .rows = 5, .cols = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
var s: ImageStorage = .{};
|
||||
defer s.deinit(alloc, t.screens.active);
|
||||
try s.addImage(io, alloc, t.screens.active, .{ .id = 1 });
|
||||
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{
|
||||
.location = .{ .virtual = {} },
|
||||
});
|
||||
try s.addPlacement(io, alloc, t.screens.active, 1, 2, .{
|
||||
.location = .{ .relative = .{
|
||||
.parent = .{
|
||||
.image_id = 1,
|
||||
.placement_id = .{ .tag = .external, .id = 1 },
|
||||
},
|
||||
.horizontal_offset = 1,
|
||||
} },
|
||||
});
|
||||
|
||||
const child = s.placements.get(.{
|
||||
.image_id = 1,
|
||||
.placement_id = .{ .tag = .external, .id = 2 },
|
||||
}).?;
|
||||
const chain = s.resolveChain(child.location.relative).?;
|
||||
try testing.expect(chain.root.location == .virtual);
|
||||
try testing.expect(chain.root_key.eql(.{
|
||||
.image_id = 1,
|
||||
.placement_id = .{ .tag = .external, .id = 1 },
|
||||
}));
|
||||
try testing.expectEqual(@as(i32, 1), chain.horizontal_offset);
|
||||
try testing.expectEqual(@as(i32, 0), chain.vertical_offset);
|
||||
}
|
||||
|
||||
test "storage: eviction removes orphaned relative placements" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
const io = testing.io;
|
||||
var t = try terminal.Terminal.init(io, alloc, .{ .rows = 5, .cols = 5 });
|
||||
defer t.deinit(alloc);
|
||||
|
||||
var s: ImageStorage = .{};
|
||||
defer s.deinit(alloc, t.screens.active);
|
||||
s.total_limit = 8;
|
||||
|
||||
// Image 1 holds most of the byte budget and has a pin placement.
|
||||
try s.addImage(io, alloc, t.screens.active, .{
|
||||
.id = 1,
|
||||
.width = 2,
|
||||
.height = 1,
|
||||
.data = .{ .complete = try alloc.dupe(u8, &.{ 0, 0, 0, 0, 0, 0 }) },
|
||||
});
|
||||
try s.addPlacement(io, alloc, t.screens.active, 1, 1, .{
|
||||
.location = .{ .pin = try trackPin(&t, .{ .x = 1, .y = 1 }) },
|
||||
});
|
||||
|
||||
// Image 2's placement is relative to image 1's placement.
|
||||
try s.addImage(io, alloc, t.screens.active, .{ .id = 2 });
|
||||
try s.addPlacement(io, alloc, t.screens.active, 2, 1, .{
|
||||
.location = .{ .relative = .{ .parent = .{
|
||||
.image_id = 1,
|
||||
.placement_id = .{ .tag = .external, .id = 1 },
|
||||
} } },
|
||||
});
|
||||
|
||||
// Adding image 3 exceeds the limit and evicts image 1 (oldest),
|
||||
// removing its placement, which orphans image 2's placement.
|
||||
try s.addImage(io, alloc, t.screens.active, .{
|
||||
.id = 3,
|
||||
.width = 2,
|
||||
.height = 1,
|
||||
.data = .{ .complete = try alloc.dupe(u8, &.{ 0, 0, 0, 0, 0, 0 }) },
|
||||
});
|
||||
|
||||
try testing.expect(s.imageById(1) == null);
|
||||
try testing.expectEqual(@as(usize, 0), s.placements.count());
|
||||
try testing.expect(s.imageById(2) != null);
|
||||
}
|
||||
|
||||
test "storage: placeholderTarget lookup" {
|
||||
const testing = std.testing;
|
||||
const alloc = testing.allocator;
|
||||
|
||||
Reference in New Issue
Block a user