mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 07:47:54 +01:00
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
This commit is contained in:
@@ -330,9 +330,6 @@ internal class DriverImpl : IDriver
|
||||
/// <inheritdoc/>
|
||||
public void FillRect (Rectangle rect, Rune rune = default) { OutputBuffer.FillRect (rect, rune); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void FillRect (Rectangle rect, char c) { OutputBuffer.FillRect (rect, c); }
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Attribute SetAttribute (Attribute newAttribute)
|
||||
{
|
||||
|
||||
@@ -257,14 +257,6 @@ public interface IDriver : IDisposable
|
||||
/// <param name="rune">The Rune used to fill the rectangle</param>
|
||||
void FillRect (Rectangle rect, Rune rune = default);
|
||||
|
||||
/// <summary>
|
||||
/// Fills the specified rectangle with the specified <see langword="char"/>. This method is a convenience method
|
||||
/// that calls <see cref="IDriver.FillRect(System.Drawing.Rectangle,System.Text.Rune)"/>.
|
||||
/// </summary>
|
||||
/// <param name="rect"></param>
|
||||
/// <param name="c"></param>
|
||||
void FillRect (Rectangle rect, char c);
|
||||
|
||||
/// <summary>Selects the specified attribute as the attribute to use for future calls to AddRune and AddString.</summary>
|
||||
/// <remarks>Implementations should call <c>base.SetAttribute(c)</c>.</remarks>
|
||||
/// <param name="c">C.</param>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// When the method returns, <see cref="Col"/> will be incremented by the number of columns
|
||||
/// <paramref name="rune"/> required, even if the new column value is outside of the <see cref="Clip"/> or screen
|
||||
/// <paramref name="rune"/> required, even if the new column value is outside the <see cref="Clip"/> or screen
|
||||
/// dimensions defined by <see cref="Cols"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <param name="col">The column.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
private void SetAttributeAndDirty (int col, int row)
|
||||
{
|
||||
Contents! [row, col].Attribute = CurrentAttribute;
|
||||
Contents [row, col].IsDirty = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private void InvalidateOverlappedWideGlyph ()
|
||||
/// <param name="col">The column.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
/// <param name="col">The column.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
/// <param name="text">The printable text to write.</param>
|
||||
/// <param name="textWidth">The column width of the text.</param>
|
||||
/// <param name="clipRect">The clipping rectangle.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Writes a single-width character (0 or 1 column wide).
|
||||
/// INTERNAL: Writes a (0 or 1 column wide) Grapheme.
|
||||
/// </summary>
|
||||
private void WriteSingleWidthGrapheme (string text, Rectangle clipRect)
|
||||
/// <param name="col">The column.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
/// <param name="grapheme">The single-width Grapheme to write.</param>
|
||||
/// <param name="clipRect">The clipping rectangle.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
private void WriteWideGrapheme (string text)
|
||||
/// <param name="col">The column.</param>
|
||||
/// <param name="row">The row.</param>
|
||||
/// <param name="grapheme">The wide Grapheme to write.</param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>Tests whether the specified coordinate are valid for drawing the specified Text.</summary>
|
||||
@@ -342,8 +355,9 @@ public class OutputBufferImpl : IOutputBuffer
|
||||
/// <inheritdoc/>
|
||||
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
|
||||
/// <summary>
|
||||
/// Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>.
|
||||
/// Used by <see cref="AddRune(Rune)"/> and <see cref="AddStr"/> to determine where to add content.
|
||||
@@ -393,9 +407,8 @@ public class OutputBufferImpl : IOutputBuffer
|
||||
/// </remarks>
|
||||
/// <param name="col">Column to move to.</param>
|
||||
/// <param name="row">Row to move to.</param>
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -426,6 +426,7 @@
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Mazing/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ogonek/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Quattro/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=repro/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Roslynator/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=RRGGBB/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=runnables/@EntryIndexedValue">True</s:Boolean>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,359 @@
|
||||
using System.Text;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace DriverTests;
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
public class OutputBufferWideCharTests (ITestOutputHelper output)
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that FillRect works correctly with single-width characters (baseline behavior).
|
||||
/// This should work the same with or without FIXES_4466.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests FillRect with wide characters at buffer boundaries.
|
||||
/// </summary>
|
||||
[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");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests OutputBase.Write method marks cells dirty correctly for wide characters.
|
||||
/// This tests the other half of the fix in OutputBase.cs.
|
||||
/// </summary>
|
||||
[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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tests that filling a rectangle with spaces properly handles wide character cleanup.
|
||||
/// This simulates clearing a region that contains wide characters.
|
||||
/// </summary>
|
||||
[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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user