Skip to content

Commit 4f574f1

Browse files
committed
Fix wide character rendering artifacts during window movement
- Add terminal safety clear: before emitting a wide char, explicitly clear the continuation position if the terminal had different content there - Fix clipped visible regions: replace orphaned continuation cells (base under another window) with spaces instead of copying them as-is - Fix clipped wide char bases at right edge of copy regions - Add wide char pair dirty coherence for front buffer continuation mismatch - Prevents ghost border artifacts when moving windows over CJK/emoji content
1 parent ca44fa7 commit 4f574f1

1 file changed

Lines changed: 93 additions & 7 deletions

File tree

SharpConsoleUI/Drivers/ConsoleBuffer.cs

Lines changed: 93 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -294,14 +294,40 @@ public void SetCellsFromBuffer(int destX, int destY, CharacterBuffer source, int
294294
if (sx >= 0 && sx < sourceWidth && srcY >= 0 && srcY < sourceHeight)
295295
{
296296
var srcCell = source.GetCell(sx, srcY);
297-
string ansi = FormatCellAnsi(srcCell.Foreground, srcCell.Background, srcCell.Decorations);
298297

299-
if (destCell.Character != srcCell.Character || destCell.AnsiEscape != ansi || destCell.IsWideContinuation != srcCell.IsWideContinuation || destCell.Combiners != srcCell.Combiners)
298+
// Fix clipped wide char at left edge of copy region: if the first source
299+
// cell is a continuation, its base is outside our copy range (under another
300+
// window). Write a space instead of an orphaned continuation that would be
301+
// skipped during rendering, leaving stale terminal content visible.
302+
// Also fix clipped wide char at right edge: if this is a wide base
303+
// but its continuation would be outside our copy range, write space.
304+
bool isOrphanedContinuation = srcCell.IsWideContinuation && i == 0;
305+
bool isClippedWideBase = !srcCell.IsWideContinuation &&
306+
sx + 1 < sourceWidth && source.GetCell(sx + 1, srcY).IsWideContinuation &&
307+
i == maxWidth - 1;
308+
if (isOrphanedContinuation || isClippedWideBase)
300309
{
301-
destCell.Character = srcCell.Character;
302-
destCell.AnsiEscape = ansi;
303-
destCell.IsWideContinuation = srcCell.IsWideContinuation;
304-
destCell.Combiners = srcCell.Combiners;
310+
string ansi = FormatCellAnsi(srcCell.Foreground, srcCell.Background, srcCell.Decorations);
311+
var spaceRune = new Rune(' ');
312+
if (destCell.Character != spaceRune || destCell.AnsiEscape != ansi || destCell.IsWideContinuation || destCell.Combiners != null)
313+
{
314+
destCell.Character = spaceRune;
315+
destCell.AnsiEscape = ansi;
316+
destCell.IsWideContinuation = false;
317+
destCell.Combiners = null;
318+
}
319+
}
320+
else
321+
{
322+
string ansi = FormatCellAnsi(srcCell.Foreground, srcCell.Background, srcCell.Decorations);
323+
324+
if (destCell.Character != srcCell.Character || destCell.AnsiEscape != ansi || destCell.IsWideContinuation != srcCell.IsWideContinuation || destCell.Combiners != srcCell.Combiners)
325+
{
326+
destCell.Character = srcCell.Character;
327+
destCell.AnsiEscape = ansi;
328+
destCell.IsWideContinuation = srcCell.IsWideContinuation;
329+
destCell.Combiners = srcCell.Combiners;
330+
}
305331
}
306332
}
307333
else
@@ -733,6 +759,34 @@ private void AppendRegionToBuilder(int y, int startX, int endX, StringBuilder bu
733759
continue;
734760
}
735761

762+
// Wide char terminal safety: when emitting a wide character, the terminal
763+
// auto-advances past the continuation cell at x+1. But if the terminal was
764+
// previously showing a different character at x+1 (e.g., a border │), some
765+
// terminals don't reliably clear it — the old content persists as a ghost.
766+
// Fix: emit a space at x+1 first to explicitly clear the old content, then
767+
// reposition cursor back to x before emitting the wide char.
768+
bool isWideChar = x + 1 < _width && _backBuffer[x + 1, y].IsWideContinuation;
769+
if (isWideChar)
770+
{
771+
// Check if the terminal (old front buffer, before sync) had something
772+
// other than this wide char's continuation at x+1
773+
ref var nextFront = ref _frontBuffer[x + 1, y];
774+
bool terminalHadDifferentContent = !nextFront.Equals(_backBuffer[x + 1, y]);
775+
776+
if (terminalHadDifferentContent)
777+
{
778+
// Emit space at x+1 to clear old terminal content
779+
builder.Append(backCell.AnsiEscape);
780+
lastOutputAnsi = backCell.AnsiEscape;
781+
builder.Append(' ');
782+
// Reposition cursor back to x
783+
builder.Append($"\x1b[{y + 1};{x + 1}H");
784+
}
785+
786+
// Sync the continuation cell's front buffer now
787+
nextFront.CopyFrom(_backBuffer[x + 1, y]);
788+
}
789+
736790
// Output ANSI only if it changed
737791
if (backCell.AnsiEscape != lastOutputAnsi)
738792
{
@@ -744,6 +798,16 @@ private void AppendRegionToBuilder(int y, int startX, int endX, StringBuilder bu
744798
builder.AppendRune(backCell.Character);
745799
if (backCell.Combiners != null)
746800
builder.Append(backCell.Combiners);
801+
802+
// Skip past continuation cell — we already synced it above
803+
if (isWideChar)
804+
{
805+
// Emit any combiners on the continuation
806+
ref readonly var contCell = ref _backBuffer[x + 1, y];
807+
if (contCell.Combiners != null)
808+
builder.Append(contCell.Combiners);
809+
x++; // Skip continuation in loop
810+
}
747811
}
748812

749813
// Reset ANSI at end of region
@@ -798,6 +862,28 @@ private void AppendLineToBuilder(int y, StringBuilder builder)
798862
consecutiveUnchanged = 0;
799863
}
800864

865+
// Wide char terminal safety: clear old content at continuation position
866+
// before emitting the wide char (see AppendRegionToBuilder for full explanation)
867+
bool isWideChar = x + 1 < maxWidth && _backBuffer[x + 1, y].IsWideContinuation;
868+
if (isWideChar)
869+
{
870+
ref var nextFront = ref _frontBuffer[x + 1, y];
871+
if (!nextFront.Equals(_backBuffer[x + 1, y]))
872+
{
873+
// Emit ANSI + space at x+1 to clear old terminal content
874+
if (backCell.AnsiEscape != lastOutputAnsi)
875+
{
876+
builder.Append(backCell.AnsiEscape);
877+
lastOutputAnsi = backCell.AnsiEscape;
878+
}
879+
builder.Append(' ');
880+
// Reposition cursor back to x
881+
builder.Append($"\x1b[{y + 1};{x + 1}H");
882+
}
883+
// Sync continuation front buffer
884+
nextFront.CopyFrom(_backBuffer[x + 1, y]);
885+
}
886+
801887
// Only output ANSI if it's different from the last one we output
802888
if (backCell.AnsiEscape != lastOutputAnsi)
803889
{
@@ -823,7 +909,7 @@ private void AppendLineToBuilder(int y, StringBuilder builder)
823909
builder.Append(backCell.Combiners);
824910

825911
// Track if this was a wide char — terminal advances cursor by 2
826-
lastOutputWasWide = x + 1 < maxWidth && _backBuffer[x + 1, y].IsWideContinuation;
912+
lastOutputWasWide = isWideChar;
827913
}
828914
else
829915
{

0 commit comments

Comments
 (0)