diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs
index 5c0d5ad72..2368dd631 100644
--- a/Terminal.Gui/Drivers/DriverImpl.cs
+++ b/Terminal.Gui/Drivers/DriverImpl.cs
@@ -330,9 +330,6 @@ internal class DriverImpl : IDriver
///
public void FillRect (Rectangle rect, Rune rune = default) { OutputBuffer.FillRect (rect, rune); }
- ///
- public void FillRect (Rectangle rect, char c) { OutputBuffer.FillRect (rect, c); }
-
///
public Attribute SetAttribute (Attribute newAttribute)
{
diff --git a/Terminal.Gui/Drivers/IDriver.cs b/Terminal.Gui/Drivers/IDriver.cs
index 0abd121b7..41ff4d091 100644
--- a/Terminal.Gui/Drivers/IDriver.cs
+++ b/Terminal.Gui/Drivers/IDriver.cs
@@ -257,14 +257,6 @@ public interface IDriver : IDisposable
/// The Rune used to fill the rectangle
void FillRect (Rectangle rect, Rune rune = default);
- ///
- /// Fills the specified rectangle with the specified . This method is a convenience method
- /// that calls .
- ///
- ///
- ///
- void FillRect (Rectangle rect, char c);
-
/// Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.
/// Implementations should call base.SetAttribute(c).
/// C.
diff --git a/Terminal.Gui/Drivers/OutputBase.cs b/Terminal.Gui/Drivers/OutputBase.cs
index 347eba70b..cbeb403a0 100644
--- a/Terminal.Gui/Drivers/OutputBase.cs
+++ b/Terminal.Gui/Drivers/OutputBase.cs
@@ -127,6 +127,13 @@ public abstract class OutputBase
Cell cell = buffer.Contents [row, col];
buffer.Contents [row, col].IsDirty = false;
AppendCellAnsi (cell, outputStringBuilder, ref redrawAttr, ref _redrawTextStyle, cols, ref col, ref outputWidth);
+
+ if (col != lastCol)
+ {
+ // Was a wide grapheme so mark clean next cell
+ // See https://github.com/gui-cs/Terminal.Gui/issues/4466
+ buffer.Contents [row, col].IsDirty = false;
+ }
}
}
diff --git a/Terminal.Gui/Drivers/OutputBufferImpl.cs b/Terminal.Gui/Drivers/OutputBufferImpl.cs
index c12dc29f5..be03a26c6 100644
--- a/Terminal.Gui/Drivers/OutputBufferImpl.cs
+++ b/Terminal.Gui/Drivers/OutputBufferImpl.cs
@@ -86,7 +86,7 @@ public class OutputBufferImpl : IOutputBuffer
get => _clip;
set
{
- if (_clip == value)
+ if (ReferenceEquals (_clip, value))
{
return;
}
@@ -94,10 +94,7 @@ public class OutputBufferImpl : IOutputBuffer
_clip = value;
// Don't ever let Clip be bigger than Screen
- if (_clip is { })
- {
- _clip.Intersect (Screen);
- }
+ _clip?.Intersect (Screen);
}
}
@@ -105,7 +102,7 @@ public class OutputBufferImpl : IOutputBuffer
///
///
/// When the method returns, will be incremented by the number of columns
- /// required, even if the new column value is outside of the or screen
+ /// required, even if the new column value is outside the or screen
/// dimensions defined by .
///
///
@@ -156,25 +153,19 @@ public class OutputBufferImpl : IOutputBuffer
Clip ??= new (Screen);
Rectangle clipRect = Clip!.GetBounds ();
- string text = grapheme;
- int textWidth = -1;
+ int printableGraphemeWidth = -1;
lock (Contents)
{
- bool validLocation = IsValidLocation (text, Col, Row);
-
- if (validLocation)
+ if (IsValidLocation (grapheme, Col, Row))
{
- text = text.MakePrintable ();
- textWidth = text.GetColumns ();
-
// Set attribute and mark dirty for current cell
- Contents [Row, Col].Attribute = CurrentAttribute;
- Contents [Row, Col].IsDirty = true;
+ SetAttributeAndDirty (Col, Row);
+ InvalidateOverlappedWideGlyph (Col, Row);
- InvalidateOverlappedWideGlyph ();
-
- WriteGraphemeByWidth (text, textWidth, clipRect);
+ string printableGrapheme = grapheme.MakePrintable ();
+ printableGraphemeWidth = printableGrapheme.GetColumns ();
+ WriteGraphemeByWidth (Col, Row, printableGrapheme, printableGraphemeWidth, clipRect);
DirtyLines [Row] = true;
}
@@ -183,7 +174,7 @@ public class OutputBufferImpl : IOutputBuffer
// Keep Col/Row updates inside the lock to prevent race conditions
Col++;
- if (textWidth > 1)
+ if (printableGraphemeWidth > 1)
{
// Skip the second column of a wide character
// IMPORTANT: We do NOT modify column N+1's IsDirty or Attribute here.
@@ -194,86 +185,111 @@ public class OutputBufferImpl : IOutputBuffer
}
///
- /// If we're writing at an odd column and there's a wide glyph to our left,
+ /// INTERNAL: Helper to set the attribute and mark the cell as dirty.
+ ///
+ /// The column.
+ /// The row.
+ private void SetAttributeAndDirty (int col, int row)
+ {
+ Contents! [row, col].Attribute = CurrentAttribute;
+ Contents [row, col].IsDirty = true;
+ }
+
+ ///
+ /// INTERNAL: If we're writing at an odd column and there's a wide glyph to our left,
/// invalidate it since we're overwriting the second half.
///
- private void InvalidateOverlappedWideGlyph ()
+ /// The column.
+ /// The row.
+ private void InvalidateOverlappedWideGlyph (int col, int row)
{
- if (Col > 0 && Contents! [Row, Col - 1].Grapheme.GetColumns () > 1)
+ if (col > 0 && Contents! [row, col - 1].Grapheme.GetColumns () > 1)
{
- Contents [Row, Col - 1].Grapheme = Rune.ReplacementChar.ToString ();
- Contents [Row, Col - 1].IsDirty = true;
+ Contents [row, col - 1].Grapheme = Rune.ReplacementChar.ToString ();
+ Contents [row, col - 1].IsDirty = true;
}
}
///
- /// Writes a grapheme to the buffer based on its width (0, 1, or 2 columns).
+ /// INTERNAL: Writes a Grapheme to the buffer based on its width (0, 1, or 2 columns).
///
+ /// The column.
+ /// The row.
/// The printable text to write.
/// The column width of the text.
/// The clipping rectangle.
- private void WriteGraphemeByWidth (string text, int textWidth, Rectangle clipRect)
+ private void WriteGraphemeByWidth (int col, int row, string text, int textWidth, Rectangle clipRect)
{
switch (textWidth)
{
case 0:
case 1:
- WriteSingleWidthGrapheme (text, clipRect);
+ WriteGrapheme (col, row, text, clipRect);
break;
case 2:
- WriteWideGrapheme (text);
+ WriteWideGrapheme (col, row, text);
break;
default:
// Negative width or non-spacing character (shouldn't normally occur)
- Contents! [Row, Col].Grapheme = " ";
- Contents [Row, Col].IsDirty = false;
+ Contents! [row, col].Grapheme = " ";
+ Contents [row, col].IsDirty = false;
break;
}
}
///
- /// Writes a single-width character (0 or 1 column wide).
+ /// INTERNAL: Writes a (0 or 1 column wide) Grapheme.
///
- private void WriteSingleWidthGrapheme (string text, Rectangle clipRect)
+ /// The column.
+ /// The row.
+ /// The single-width Grapheme to write.
+ /// The clipping rectangle.
+ private void WriteGrapheme (int col, int row, string grapheme, Rectangle clipRect)
{
- Contents! [Row, Col].Grapheme = text;
+ Debug.Assert (grapheme.GetColumns () < 2);
+ Contents! [row, col].Grapheme = grapheme;
// Mark the next cell as dirty to ensure proper rendering of adjacent content
- if (Col < clipRect.Right - 1 && Col + 1 < Cols)
+ if (col < clipRect.Right - 1 && col + 1 < Cols)
{
- Contents [Row, Col + 1].IsDirty = true;
+ Contents [row, col + 1].IsDirty = true;
}
}
///
- /// Writes a wide character (2 columns wide) handling clipping and partial overlap cases.
+ /// INTERNAL: Writes a wide Grapheme (2 columns wide) handling clipping and partial overlap cases.
///
- private void WriteWideGrapheme (string text)
+ /// The column.
+ /// The row.
+ /// The wide Grapheme to write.
+ private void WriteWideGrapheme (int col, int row, string grapheme)
{
- if (!Clip!.Contains (Col + 1, Row))
+ Debug.Assert (grapheme.GetColumns () == 2);
+ if (!Clip!.Contains (col + 1, row))
{
// Second column is outside clip - can't fit wide char here
- Contents! [Row, Col].Grapheme = Rune.ReplacementChar.ToString ();
+ Contents! [row, col].Grapheme = Rune.ReplacementChar.ToString ();
}
- else if (!Clip.Contains (Col, Row))
+ else if (!Clip.Contains (col, row))
{
// First column is outside clip but second isn't
// Mark second column as replacement to indicate partial overlap
- if (Col + 1 < Cols)
+ if (col + 1 < Cols)
{
- Contents! [Row, Col + 1].Grapheme = Rune.ReplacementChar.ToString ();
+ Contents! [row, col + 1].Grapheme = Rune.ReplacementChar.ToString ();
+ Contents! [row, col + 1].IsDirty = true;
}
}
else
{
// Both columns are in bounds - write the wide character
// It will naturally render across both columns when output to the terminal
- Contents! [Row, Col].Grapheme = text;
+ Contents! [row, col].Grapheme = grapheme;
// DO NOT modify column N+1 here!
// The wide glyph will naturally render across both columns.
@@ -288,7 +304,7 @@ public class OutputBufferImpl : IOutputBuffer
{
Contents = new Cell [Rows, Cols];
- //CONCURRENCY: Unsynchronized access to Clip isn't safe.
+ // CONCURRENCY: Unsynchronized access to Clip isn't safe.
// TODO: ClearContents should not clear the clip; it should only clear the contents. Move clearing it elsewhere.
Clip = new (Screen);
@@ -311,9 +327,6 @@ public class OutputBufferImpl : IOutputBuffer
DirtyLines [row] = true;
}
}
-
- // TODO: Who uses this and why? I am removing for now - this class is a state class not an events class
- //ClearedContents?.Invoke (this, EventArgs.Empty);
}
/// Tests whether the specified coordinate are valid for drawing the specified Text.
@@ -342,8 +355,9 @@ public class OutputBufferImpl : IOutputBuffer
///
public void FillRect (Rectangle rect, Rune rune)
{
+ Rectangle clipBounds = Clip?.GetBounds () ?? Screen;
// BUGBUG: This should be a method on Region
- rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen);
+ rect = Rectangle.Intersect (rect, clipBounds);
lock (Contents!)
{
@@ -356,11 +370,12 @@ public class OutputBufferImpl : IOutputBuffer
continue;
}
- Contents [r, c] = new ()
- {
- Grapheme = rune != default (Rune) ? rune.ToString () : " ",
- Attribute = CurrentAttribute, IsDirty = true
- };
+ // We could call AddGrapheme here, but that would acquire the lock again.
+ // So we inline the logic instead.
+ SetAttributeAndDirty (c, r);
+ InvalidateOverlappedWideGlyph (c, r);
+ string grapheme = rune != default (Rune) ? rune.ToString () : " ";
+ WriteGraphemeByWidth (c, r, grapheme, grapheme.GetColumns (), clipBounds);
}
}
}
@@ -379,7 +394,6 @@ public class OutputBufferImpl : IOutputBuffer
}
}
- // TODO: Make internal once Menu is upgraded
///
/// Updates and to the specified column and row in .
/// Used by and to determine where to add content.
@@ -393,9 +407,8 @@ public class OutputBufferImpl : IOutputBuffer
///
/// Column to move to.
/// Row to move to.
- public virtual void Move (int col, int row)
+ public void Move (int col, int row)
{
- //Debug.Assert (col >= 0 && row >= 0 && col < Contents.GetLength(1) && row < Contents.GetLength(0));
Col = col;
Row = row;
}
diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings
index a33c71b8d..e3f73a543 100644
--- a/Terminal.sln.DotSettings
+++ b/Terminal.sln.DotSettings
@@ -426,6 +426,7 @@
True
True
True
+ True
True
True
True
diff --git a/Tests/UnitTestsParallelizable/Drivers/ClipRegionTests.cs b/Tests/UnitTestsParallelizable/Drivers/ClipRegionTests.cs
index c07d4070d..d36323999 100644
--- a/Tests/UnitTestsParallelizable/Drivers/ClipRegionTests.cs
+++ b/Tests/UnitTestsParallelizable/Drivers/ClipRegionTests.cs
@@ -23,12 +23,12 @@ public class ClipRegionTests (ITestOutputHelper output) : FakeDriverBase
Assert.Equal ("x", driver.Contents [5, 5].Grapheme);
// Clear the contents
- driver.FillRect (new Rectangle (0, 0, driver.Rows, driver.Cols), ' ');
+ driver.FillRect (new (0, 0, driver.Rows, driver.Cols), new Rune(' '));
Assert.Equal (" ", driver.Contents [0, 0].Grapheme);
// Setup the region with a single rectangle, fill screen with 'x'
- driver.Clip = new (new Rectangle (5, 5, 5, 5));
- driver.FillRect (new Rectangle (0, 0, driver.Rows, driver.Cols), 'x');
+ driver.Clip = new (new (5, 5, 5, 5));
+ driver.FillRect (new (0, 0, driver.Rows, driver.Cols), new Rune ('x'));
Assert.Equal (" ", driver.Contents [0, 0].Grapheme);
Assert.Equal (" ", driver.Contents [4, 9].Grapheme);
Assert.Equal ("x", driver.Contents [5, 5].Grapheme);
diff --git a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs
index 6ac00a739..19c6ddcec 100644
--- a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs
+++ b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs
@@ -189,9 +189,9 @@ public class OutputBaseTests
// Column 0 was written (wide glyph)
Assert.False (buffer.Contents! [0, 0].IsDirty);
- // Column 1 was skipped by OutputBase.Write because column 0 had a wide glyph
- // So its dirty flag remains true (it was initialized as dirty by ClearContents)
- Assert.True (buffer.Contents! [0, 1].IsDirty);
+ // Column 1 was marked as clean by OutputBase.Write when it processed the wide glyph at column 0
+ // See: https://github.com/gui-cs/Terminal.Gui/issues/4466
+ Assert.False (buffer.Contents! [0, 1].IsDirty);
// Column 2 was written ('A')
Assert.False (buffer.Contents! [0, 2].IsDirty);
diff --git a/Tests/UnitTestsParallelizable/Drivers/OutputBufferWideCharTests.cs b/Tests/UnitTestsParallelizable/Drivers/OutputBufferWideCharTests.cs
new file mode 100644
index 000000000..d8da44b65
--- /dev/null
+++ b/Tests/UnitTestsParallelizable/Drivers/OutputBufferWideCharTests.cs
@@ -0,0 +1,359 @@
+using System.Text;
+using Xunit.Abstractions;
+
+namespace DriverTests;
+
+///
+/// Tests for https://github.com/gui-cs/Terminal.Gui/issues/4466.
+/// These tests validate that FillRect properly handles wide characters when overlapping existing content.
+/// Specifically, they ensure that wide characters are properly invalidated and replaced when a MessageBox border or similar UI element is drawn over them, preventing visual corruption.
+///
+public class OutputBufferWideCharTests (ITestOutputHelper output)
+{
+ ///
+ /// Tests that FillRect properly invalidates wide characters when overwriting them.
+ /// This is the core issue in #4466 - when a MessageBox border is drawn over Chinese text,
+ /// the wide characters need to be properly invalidated.
+ ///
+ [Fact]
+ [Trait ("Category", "Output")]
+ public void FillRect_OverwritesWideChar_InvalidatesProperly ()
+ {
+ // Arrange - Create a buffer and draw a wide character
+ OutputBufferImpl buffer = new ()
+ {
+ Rows = 5, Cols = 10,
+ CurrentAttribute = new (Color.White, Color.Black)
+ };
+
+ // Draw a Chinese character (2 columns wide) at position 2,1
+ buffer.Move (2, 1);
+ buffer.AddStr ("你"); // Chinese character "you", 2 columns wide
+
+ // Verify the wide character was drawn
+ Assert.Equal ("你", buffer.Contents! [1, 2].Grapheme);
+ Assert.True (buffer.Contents [1, 2].IsDirty);
+
+ // With the fix, the second column should NOT be modified by AddStr
+ // The wide glyph naturally renders across both columns
+ Assert.NotEqual ("你", buffer.Contents [1, 3].Grapheme);
+
+ // Clear dirty flags to test FillRect behavior
+ for (var r = 0; r < buffer.Rows; r++)
+ {
+ for (var c = 0; c < buffer.Cols; c++)
+ {
+ buffer.Contents [r, c].IsDirty = false;
+ }
+ }
+
+ // Act - Fill a rectangle that overlaps the first column of the wide character
+ // This simulates drawing a MessageBox border over Chinese text
+ buffer.FillRect (new (2, 1, 1, 1), new Rune ('│'));
+
+ // Assert
+
+ // With FIXES_4466: FillRect calls AddStr, which properly invalidates the wide character
+ // The wide character at [1,2] should be replaced with replacement char or the new content
+ Assert.Equal ("│", buffer.Contents [1, 2].Grapheme);
+ Assert.True (buffer.Contents [1, 2].IsDirty, "Cell [1,2] should be marked dirty after FillRect");
+
+ // The adjacent cell should also be marked dirty for proper rendering
+ Assert.True (buffer.Contents [1, 3].IsDirty, "Adjacent cell [1,3] should be marked dirty to ensure proper rendering");
+ }
+
+ ///
+ /// Tests that FillRect handles overwriting the second column of a wide character.
+ /// When drawing at an odd column that's the second half of a wide glyph, the
+ /// wide glyph should be invalidated.
+ ///
+ [Fact]
+ [Trait ("Category", "Output")]
+ public void FillRect_OverwritesSecondColumnOfWideChar_InvalidatesWideChar ()
+ {
+ // Arrange
+ OutputBufferImpl buffer = new ()
+ {
+ Rows = 5, Cols = 10,
+ CurrentAttribute = new (Color.White, Color.Black)
+ };
+
+ // Draw a wide character at position 2,1
+ buffer.Move (2, 1);
+ buffer.AddStr ("好"); // Chinese character, 2 columns wide
+
+ Assert.Equal ("好", buffer.Contents! [1, 2].Grapheme);
+
+ // Clear dirty flags
+ for (var r = 0; r < buffer.Rows; r++)
+ {
+ for (var c = 0; c < buffer.Cols; c++)
+ {
+ buffer.Contents [r, c].IsDirty = false;
+ }
+ }
+
+ // Act - Fill at the second column of the wide character (position 3)
+ buffer.FillRect (new (3, 1, 1, 1), new Rune ('│'));
+
+ // Assert
+ // With the fix: The original wide character at col 2 should be invalidated
+ // because we're overwriting its second column
+ Assert.True (buffer.Contents [1, 2].IsDirty, "Wide char at col 2 should be invalidated when its second column is overwritten");
+ Assert.Equal (buffer.Contents [1, 2].Grapheme, Rune.ReplacementChar.ToString ());
+
+ Assert.Equal ("│", buffer.Contents [1, 3].Grapheme);
+ Assert.True (buffer.Contents [1, 3].IsDirty);
+ }
+
+ ///
+ /// Tests the ChineseUI scenario: Drawing a MessageBox with borders over Chinese button text.
+ /// This simulates the specific repro case from the issue. See: https://github.com/gui-cs/Terminal.Gui/issues/4466
+ ///
+ [Fact]
+ [Trait ("Category", "Output")]
+ public void ChineseUI_MessageBox_Over_WideChars ()
+ {
+ // Arrange - Simulate the ChineseUI scenario
+ OutputBufferImpl buffer = new ()
+ {
+ Rows = 10, Cols = 30,
+ CurrentAttribute = new (Color.White, Color.Black)
+ };
+
+ // Draw Chinese button text (like "你好呀")
+ buffer.Move (5, 3);
+ buffer.AddStr ("你好呀"); // 3 Chinese characters, 6 columns total
+
+ // Verify initial state
+ Assert.Equal ("你", buffer.Contents! [3, 5].Grapheme);
+ Assert.Equal ("好", buffer.Contents [3, 7].Grapheme);
+ Assert.Equal ("呀", buffer.Contents [3, 9].Grapheme);
+
+ // Clear dirty flags to simulate the state before MessageBox draws
+ for (var r = 0; r < buffer.Rows; r++)
+ {
+ for (var c = 0; c < buffer.Cols; c++)
+ {
+ buffer.Contents [r, c].IsDirty = false;
+ }
+ }
+
+ // Act - Draw a MessageBox border that partially overlaps the Chinese text
+ // This simulates the mouse moving over the border, causing HighlightState changes
+ // Draw vertical line at column 8 (overlaps second char "好")
+ for (var row = 2; row < 6; row++)
+ {
+ buffer.FillRect (new (8, row, 1, 1), new Rune ('│'));
+ }
+
+ // Assert - The wide characters should be properly handled
+ // With the fix: Wide characters are properly invalidated
+ // The first character "你" at col 5 should be unaffected
+ Assert.Equal ("你", buffer.Contents [3, 5].Grapheme);
+
+ // The second character "好" at col 7 had its second column overwritten
+ // so it should be replaced with replacement char
+ Assert.Equal (buffer.Contents [3, 7].Grapheme, Rune.ReplacementChar.ToString ());
+ Assert.True (buffer.Contents [3, 7].IsDirty, "Invalidated wide char should be marked dirty");
+
+ // The border should be drawn at col 8
+ Assert.Equal ("│", buffer.Contents [3, 8].Grapheme);
+ Assert.True (buffer.Contents [3, 8].IsDirty);
+
+ // The third character "呀" at col 9 should be unaffected
+ Assert.Equal ("呀", buffer.Contents [3, 9].Grapheme);
+ }
+
+ ///
+ /// Tests that FillRect works correctly with single-width characters (baseline behavior).
+ /// This should work the same with or without FIXES_4466.
+ ///
+ [Fact]
+ [Trait ("Category", "Output")]
+ public void FillRect_SingleWidthChars_WorksCorrectly ()
+ {
+ // Arrange
+ OutputBufferImpl buffer = new ()
+ {
+ Rows = 5, Cols = 10,
+ CurrentAttribute = new (Color.White, Color.Black)
+ };
+
+ // Draw some ASCII text
+ buffer.Move (2, 1);
+ buffer.AddStr ("ABC");
+
+ Assert.Equal ("A", buffer.Contents! [1, 2].Grapheme);
+ Assert.Equal ("B", buffer.Contents [1, 3].Grapheme);
+ Assert.Equal ("C", buffer.Contents [1, 4].Grapheme);
+
+ // Clear dirty flags
+ for (var r = 0; r < buffer.Rows; r++)
+ {
+ for (var c = 0; c < buffer.Cols; c++)
+ {
+ buffer.Contents [r, c].IsDirty = false;
+ }
+ }
+
+ // Act - Overwrite with FillRect
+ buffer.FillRect (new (3, 1, 1, 1), new Rune ('X'));
+
+ // Assert - This should work the same regardless of FIXES_4466
+ Assert.Equal ("A", buffer.Contents [1, 2].Grapheme);
+ Assert.Equal ("X", buffer.Contents [1, 3].Grapheme);
+ Assert.True (buffer.Contents [1, 3].IsDirty);
+ Assert.Equal ("C", buffer.Contents [1, 4].Grapheme);
+ }
+
+ ///
+ /// Tests FillRect with wide characters at buffer boundaries.
+ ///
+ [Fact]
+ [Trait ("Category", "Output")]
+ public void FillRect_WideChar_AtBufferBoundary ()
+ {
+ // Arrange
+ OutputBufferImpl buffer = new ()
+ {
+ Rows = 5, Cols = 10,
+ CurrentAttribute = new (Color.White, Color.Black)
+ };
+
+ // Draw a wide character at the right edge (col 8, which would extend to col 9)
+ buffer.Move (8, 1);
+ buffer.AddStr ("山"); // Chinese character "mountain", 2 columns wide
+
+ Assert.Equal ("山", buffer.Contents! [1, 8].Grapheme);
+
+ // Clear dirty flags
+ for (var r = 0; r < buffer.Rows; r++)
+ {
+ for (var c = 0; c < buffer.Cols; c++)
+ {
+ buffer.Contents [r, c].IsDirty = false;
+ }
+ }
+
+ // Act - FillRect at the wide character position
+ buffer.FillRect (new (8, 1, 1, 1), new Rune ('│'));
+
+ // Assert
+ Assert.Equal ("│", buffer.Contents [1, 8].Grapheme);
+ Assert.True (buffer.Contents [1, 8].IsDirty);
+
+ // Adjacent cell should be marked dirty
+ Assert.True (
+ buffer.Contents [1, 9].IsDirty,
+ "Cell after wide char replacement should be marked dirty");
+ }
+
+ ///
+ /// Tests OutputBase.Write method marks cells dirty correctly for wide characters.
+ /// This tests the other half of the fix in OutputBase.cs.
+ ///
+ [Fact]
+ [Trait ("Category", "Output")]
+ public void OutputBase_Write_WideChar_MarksCellsDirty ()
+ {
+ // Arrange
+ OutputBufferImpl buffer = new ()
+ {
+ Rows = 5, Cols = 20,
+ CurrentAttribute = new (Color.White, Color.Black)
+ };
+
+ // Draw a line with wide characters
+ buffer.Move (0, 1);
+ buffer.AddStr ("你好"); // Two wide characters
+
+ // Mark all as not dirty to simulate post-Write state
+ for (var r = 0; r < buffer.Rows; r++)
+ {
+ for (var c = 0; c < buffer.Cols; c++)
+ {
+ buffer.Contents! [r, c].IsDirty = false;
+ }
+ }
+
+ // Verify initial state
+ Assert.Equal ("你", buffer.Contents! [1, 0].Grapheme);
+ Assert.Equal ("好", buffer.Contents [1, 2].Grapheme);
+
+ // Act - Now overwrite the first wide char by writing at its position
+ buffer.Move (0, 1);
+ buffer.AddStr ("A"); // Single width char
+
+ // Assert
+ // With the fix: The first cell is replaced with 'A' and marked dirty
+ Assert.Equal ("A", buffer.Contents [1, 0].Grapheme);
+ Assert.True (buffer.Contents [1, 0].IsDirty);
+
+ // The adjacent cell (col 1) should be marked dirty for proper rendering
+ Assert.True (
+ buffer.Contents [1, 1].IsDirty,
+ "Adjacent cell should be marked dirty after writing single-width char over wide char");
+
+ // The second wide char should remain
+ Assert.Equal ("好", buffer.Contents [1, 2].Grapheme);
+ }
+
+ ///
+ /// Tests that filling a rectangle with spaces properly handles wide character cleanup.
+ /// This simulates clearing a region that contains wide characters.
+ ///
+ [Fact]
+ [Trait ("Category", "Output")]
+ public void FillRect_WithSpaces_OverWideChars ()
+ {
+ // Arrange
+ OutputBufferImpl buffer = new ()
+ {
+ Rows = 5, Cols = 15,
+ CurrentAttribute = new (Color.White, Color.Black)
+ };
+
+ // Draw a line of mixed content
+ buffer.Move (2, 2);
+ buffer.AddStr ("A你B好C");
+
+ // Verify setup
+ Assert.Equal ("A", buffer.Contents! [2, 2].Grapheme);
+ Assert.Equal ("你", buffer.Contents [2, 3].Grapheme);
+ Assert.Equal ("B", buffer.Contents [2, 5].Grapheme);
+ Assert.Equal ("好", buffer.Contents [2, 6].Grapheme);
+ Assert.Equal ("C", buffer.Contents [2, 8].Grapheme);
+
+ // Clear dirty flags
+ for (var r = 0; r < buffer.Rows; r++)
+ {
+ for (var c = 0; c < buffer.Cols; c++)
+ {
+ buffer.Contents [r, c].IsDirty = false;
+ }
+ }
+
+ // Act - Fill the region with spaces (simulating clearing)
+ buffer.FillRect (new (3, 2, 4, 1), new Rune (' '));
+
+ // Assert
+ // With the fix: Wide characters are properly handled
+ Assert.Equal (" ", buffer.Contents [2, 3].Grapheme);
+ Assert.True (buffer.Contents [2, 3].IsDirty);
+
+ // Wide character '你' at col 3 was replaced, so col 4 should be marked dirty
+ Assert.True (
+ buffer.Contents [2, 4].IsDirty,
+ "Cell after replaced wide char should be dirty");
+
+ Assert.Equal (" ", buffer.Contents [2, 4].Grapheme);
+ Assert.Equal (" ", buffer.Contents [2, 5].Grapheme);
+ Assert.Equal (" ", buffer.Contents [2, 6].Grapheme);
+
+ // Cell 7 should be dirty because '好' was partially overwritten
+ Assert.True (
+ buffer.Contents [2, 7].IsDirty,
+ "Adjacent cell should be dirty after wide char replacement");
+ }
+}