Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 38 additions & 7 deletions src/XTerm.NET.Tests/Graphics/KittyPlaceholderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -331,24 +331,55 @@ public void Placeholders_do_nothing_when_kitty_is_switched_off()
Assert.False(ImageAssertions.IsImageAt(terminal, 0, 0));
}

/// <summary>Placeholder cells are cells, so everything that happens to text happens to them.</summary>
/// <summary>
/// A placeholder tile is CONTENT: the picture is there because the cell holds the placeholder
/// character, so printing over the cell takes that tile with it and leaves the rest.
/// </summary>
/// <remarks>
/// This test used to assert the opposite — that the picture survived the write — because the
/// tile was stored as a classic Kitty overlay. That reading leaves no way to ever remove a
/// tile: the protocol has no "delete the tile at this cell" command precisely because
/// overwriting the cell IS the deletion. It is how kitty itself behaves and what image.nvim
/// relies on when it redraws. Stored as an overlay, an application that drew a dialog across a
/// picture kept the picture on top of the dialog, permanently.
/// </remarks>
[Fact]
public void A_placeholder_picture_is_an_overlay_rather_than_content()
public void Printing_over_a_placeholder_cell_removes_that_tile()
{
var terminal = WithStoredImage(5);
terminal.Write(SelectImageId(5) + Placeholder + Placeholder);

terminal.Write($"{Esc}[1;1H{Esc}[39mX");

// The character lands and the picture stays: a placeholder places a Kitty run, and a Kitty
// run is an overlay rather than content.
// The character lands and only its own tile goes; the neighbour keeps showing.
Assert.Equal("X", Cell(terminal, 0, 0).Content);
Assert.True(ImageAssertions.IsImageAt(terminal, 0, 0));
Assert.False(ImageAssertions.IsImageAt(terminal, 0, 0));
Assert.True(ImageAssertions.IsImageAt(terminal, 1, 0));

// Erasing takes both.
// Erasing takes the rest.
terminal.Write($"{Esc}[2J");
Assert.False(ImageAssertions.IsImageAt(terminal, 0, 0));
Assert.False(ImageAssertions.IsImageAt(terminal, 1, 0));
}

/// <summary>
/// The other direction of the split: writing one placeholder cell of a NEW picture over one
/// cell of an old one replaces exactly that tile.
/// </summary>
[Fact]
public void A_new_placeholder_over_an_old_tile_replaces_it()
{
var terminal = WithStoredImage(5);
terminal.Write($"{Esc}_Ga=t,i=7,f=32,s=4,v=6,q=2;{SolidRgba(4, 6, 30)}{St}");
terminal.Write(SelectImageId(5) + Placeholder + Placeholder);

var oldImage = ImageAssertions.ImageAt(terminal, 0, 0);

terminal.Write($"{Esc}[1;1H" + SelectImageId(7) + Placeholder);

var newImage = ImageAssertions.ImageAt(terminal, 0, 0);
Assert.NotNull(newImage);
Assert.False(ReferenceEquals(oldImage, newImage), "the tile should now show the new picture");
Assert.True(ReferenceEquals(oldImage, ImageAssertions.ImageAt(terminal, 1, 0)),
"the neighbouring tile still belongs to the old picture");
}
}
17 changes: 17 additions & 0 deletions src/XTerm.NET.Tests/Graphics/KittyZIndexTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ private static string Content(Terminal terminal, int col, int row)

// ---- ordering between pictures ----------------------------------------------------------------

/// <summary>
/// Printing over a classic placement leaves it whole — the z-index decides what shows.
/// Pinned here because placeholder tiles took the OPPOSITE behaviour (they are content and a
/// write removes the tile), and the split that implements that must not reach these.
/// </summary>
[Fact]
public void Text_printed_over_a_classic_placement_leaves_it_whole()
{
var terminal = WithTwoImages();
PlaceAt(terminal, 1, 0, 0, z: 0);

terminal.Write($"{Esc}[1;1HX");

Assert.Equal("X", Content(terminal, 0, 0));
Assert.Single(ImageAssertions.StackAt(terminal, 0, 0));
}

/// <summary>The stack at a cell is ordered by z, front first.</summary>
[Fact]
public void The_higher_z_picture_is_in_front()
Expand Down
18 changes: 10 additions & 8 deletions src/XTerm.NET/Buffer/BufferLine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -953,12 +953,14 @@ internal bool RemovePlacements(Func<Graphics.LinePlacement, bool> predicate)
/// Splits any Sixel run covering <paramref name="column"/> around the text just written there.
/// </summary>
/// <remarks>
/// <para>Sixel semantics: printing replaces that part of the picture. With tiles in cells this
/// <para>Content semantics: printing replaces that part of the picture. With tiles in cells this
/// happened for free, because the write overwrote the cell; with runs it has to be done on
/// purpose. The run becomes the fragments either side, each with its source rectangle narrowed
/// to match, so the rest of the picture survives a character landing in the middle of it.</para>
/// <para>Kitty runs are left alone — there the z-index decides what is on top, and text never
/// modifies a placement.</para>
/// to match, so the rest of the picture survives a character landing in the middle of it. Sixel
/// is content, and so is a placeholder tile — it exists because its cell holds the placeholder
/// character, and overwriting the cell is the only way the protocol offers to remove it.</para>
/// <para>Classic Kitty runs are left alone — there the z-index decides what is on top, and text
/// never modifies a placement.</para>
/// <para>Guarded on a null field at every call site, so a line without pictures — which is
/// nearly every line — pays a single test.</para>
/// </remarks>
Expand All @@ -973,10 +975,10 @@ internal void SplitPlacementsAt(int column, bool includeOverlays = false)
if (!placement.Covers(column))
continue;

// Printing only splits a Sixel, because only a Sixel is content. ERASING splits both:
// a cleared cell is blank, and a picture still showing through one would be a leak
// whichever protocol put it there.
if (!includeOverlays && placement.Kind != Graphics.PlacementKind.Sixel)
// Printing splits content — Sixel, and placeholder tiles — and leaves classic Kitty
// overlays alone. ERASING splits all of them: a cleared cell is blank, and a picture
// still showing through one would be a leak whichever protocol put it there.
if (!includeOverlays && placement.Kind == Graphics.PlacementKind.Kitty)
continue;

_placements.RemoveAt(i);
Expand Down
14 changes: 14 additions & 0 deletions src/XTerm.NET/Graphics/LinePlacement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,20 @@ public enum PlacementKind
/// the placement at all.
/// </summary>
Kitty = 1,

/// <summary>
/// Kitty graphics shown by Unicode placeholder cells: the cell IS the picture, so a write into
/// it takes the tile with it, exactly as Sixel content behaves.
/// </summary>
/// <remarks>
/// Distinct from <see cref="Kitty"/> because the two protocols disagree about what a write
/// means. A classic placement is an overlay and survives text; a placeholder tile exists
/// BECAUSE the cell holds the placeholder character, so a client erases a picture by
/// overwriting its cells and nothing else — there is no escape sequence for "remove the tile
/// at this cell". Left as an overlay, a dialog drawn across a picture kept the picture on top
/// of it, permanently.
/// </remarks>
Placeholder = 2,
}

/// <summary>
Expand Down
5 changes: 3 additions & 2 deletions src/XTerm.NET/InputHandler.KittyGraphics.cs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,8 @@ private bool WritePlaceholderCell(int row, int col, Graphics.TerminalImage image
return false;

// The cell keeps the placeholder character it was printed with; the picture is beside it
// rather than in it. Written before the run so SetCell's Sixel split cannot see it.
// rather than in it. Written before the run so SetCell's content split cannot see it --
// and so that the split clears any stale tile another picture left at this cell.
var cell = new BufferCell(" ", 1, _curAttr);
line.SetCell(col, ref cell);

Expand All @@ -169,7 +170,7 @@ private bool WritePlaceholderCell(int row, int col, Graphics.TerminalImage image
new Graphics.LinePlacement(
image.Id, col, 1,
srcX: srcX, srcY: srcY, srcWidth: srcWidth, srcHeight: srcHeight,
kind: Graphics.PlacementKind.Kitty,
kind: Graphics.PlacementKind.Placeholder,
serial: serial),
image);
return true;
Expand Down
Loading