mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 15:57:56 +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/>
|
/// <inheritdoc/>
|
||||||
public void FillRect (Rectangle rect, Rune rune = default) { OutputBuffer.FillRect (rect, rune); }
|
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/>
|
/// <inheritdoc/>
|
||||||
public Attribute SetAttribute (Attribute newAttribute)
|
public Attribute SetAttribute (Attribute newAttribute)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -257,14 +257,6 @@ public interface IDriver : IDisposable
|
|||||||
/// <param name="rune">The Rune used to fill the rectangle</param>
|
/// <param name="rune">The Rune used to fill the rectangle</param>
|
||||||
void FillRect (Rectangle rect, Rune rune = default);
|
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>
|
/// <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>
|
/// <remarks>Implementations should call <c>base.SetAttribute(c)</c>.</remarks>
|
||||||
/// <param name="c">C.</param>
|
/// <param name="c">C.</param>
|
||||||
|
|||||||
@@ -127,6 +127,13 @@ public abstract class OutputBase
|
|||||||
Cell cell = buffer.Contents [row, col];
|
Cell cell = buffer.Contents [row, col];
|
||||||
buffer.Contents [row, col].IsDirty = false;
|
buffer.Contents [row, col].IsDirty = false;
|
||||||
AppendCellAnsi (cell, outputStringBuilder, ref redrawAttr, ref _redrawTextStyle, cols, ref col, ref outputWidth);
|
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;
|
get => _clip;
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (_clip == value)
|
if (ReferenceEquals (_clip, value))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -94,10 +94,7 @@ public class OutputBufferImpl : IOutputBuffer
|
|||||||
_clip = value;
|
_clip = value;
|
||||||
|
|
||||||
// Don't ever let Clip be bigger than Screen
|
// 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>
|
/// <remarks>
|
||||||
/// <para>
|
/// <para>
|
||||||
/// When the method returns, <see cref="Col"/> will be incremented by the number of columns
|
/// 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"/>.
|
/// dimensions defined by <see cref="Cols"/>.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// <para>
|
/// <para>
|
||||||
@@ -156,25 +153,19 @@ public class OutputBufferImpl : IOutputBuffer
|
|||||||
Clip ??= new (Screen);
|
Clip ??= new (Screen);
|
||||||
Rectangle clipRect = Clip!.GetBounds ();
|
Rectangle clipRect = Clip!.GetBounds ();
|
||||||
|
|
||||||
string text = grapheme;
|
int printableGraphemeWidth = -1;
|
||||||
int textWidth = -1;
|
|
||||||
|
|
||||||
lock (Contents)
|
lock (Contents)
|
||||||
{
|
{
|
||||||
bool validLocation = IsValidLocation (text, Col, Row);
|
if (IsValidLocation (grapheme, Col, Row))
|
||||||
|
|
||||||
if (validLocation)
|
|
||||||
{
|
{
|
||||||
text = text.MakePrintable ();
|
|
||||||
textWidth = text.GetColumns ();
|
|
||||||
|
|
||||||
// Set attribute and mark dirty for current cell
|
// Set attribute and mark dirty for current cell
|
||||||
Contents [Row, Col].Attribute = CurrentAttribute;
|
SetAttributeAndDirty (Col, Row);
|
||||||
Contents [Row, Col].IsDirty = true;
|
InvalidateOverlappedWideGlyph (Col, Row);
|
||||||
|
|
||||||
InvalidateOverlappedWideGlyph ();
|
string printableGrapheme = grapheme.MakePrintable ();
|
||||||
|
printableGraphemeWidth = printableGrapheme.GetColumns ();
|
||||||
WriteGraphemeByWidth (text, textWidth, clipRect);
|
WriteGraphemeByWidth (Col, Row, printableGrapheme, printableGraphemeWidth, clipRect);
|
||||||
|
|
||||||
DirtyLines [Row] = true;
|
DirtyLines [Row] = true;
|
||||||
}
|
}
|
||||||
@@ -183,7 +174,7 @@ public class OutputBufferImpl : IOutputBuffer
|
|||||||
// Keep Col/Row updates inside the lock to prevent race conditions
|
// Keep Col/Row updates inside the lock to prevent race conditions
|
||||||
Col++;
|
Col++;
|
||||||
|
|
||||||
if (textWidth > 1)
|
if (printableGraphemeWidth > 1)
|
||||||
{
|
{
|
||||||
// Skip the second column of a wide character
|
// Skip the second column of a wide character
|
||||||
// IMPORTANT: We do NOT modify column N+1's IsDirty or Attribute here.
|
// IMPORTANT: We do NOT modify column N+1's IsDirty or Attribute here.
|
||||||
@@ -194,86 +185,111 @@ public class OutputBufferImpl : IOutputBuffer
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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.
|
/// invalidate it since we're overwriting the second half.
|
||||||
/// </summary>
|
/// </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].Grapheme = Rune.ReplacementChar.ToString ();
|
||||||
Contents [Row, Col - 1].IsDirty = true;
|
Contents [row, col - 1].IsDirty = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
|
/// <param name="col">The column.</param>
|
||||||
|
/// <param name="row">The row.</param>
|
||||||
/// <param name="text">The printable text to write.</param>
|
/// <param name="text">The printable text to write.</param>
|
||||||
/// <param name="textWidth">The column width of the text.</param>
|
/// <param name="textWidth">The column width of the text.</param>
|
||||||
/// <param name="clipRect">The clipping rectangle.</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)
|
switch (textWidth)
|
||||||
{
|
{
|
||||||
case 0:
|
case 0:
|
||||||
case 1:
|
case 1:
|
||||||
WriteSingleWidthGrapheme (text, clipRect);
|
WriteGrapheme (col, row, text, clipRect);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
WriteWideGrapheme (text);
|
WriteWideGrapheme (col, row, text);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Negative width or non-spacing character (shouldn't normally occur)
|
// Negative width or non-spacing character (shouldn't normally occur)
|
||||||
Contents! [Row, Col].Grapheme = " ";
|
Contents! [row, col].Grapheme = " ";
|
||||||
Contents [Row, Col].IsDirty = false;
|
Contents [row, col].IsDirty = false;
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Writes a single-width character (0 or 1 column wide).
|
/// INTERNAL: Writes a (0 or 1 column wide) Grapheme.
|
||||||
/// </summary>
|
/// </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
|
// 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>
|
/// <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>
|
/// </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
|
// 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
|
// First column is outside clip but second isn't
|
||||||
// Mark second column as replacement to indicate partial overlap
|
// 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
|
else
|
||||||
{
|
{
|
||||||
// Both columns are in bounds - write the wide character
|
// Both columns are in bounds - write the wide character
|
||||||
// It will naturally render across both columns when output to the terminal
|
// 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!
|
// DO NOT modify column N+1 here!
|
||||||
// The wide glyph will naturally render across both columns.
|
// The wide glyph will naturally render across both columns.
|
||||||
@@ -288,7 +304,7 @@ public class OutputBufferImpl : IOutputBuffer
|
|||||||
{
|
{
|
||||||
Contents = new Cell [Rows, Cols];
|
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.
|
// TODO: ClearContents should not clear the clip; it should only clear the contents. Move clearing it elsewhere.
|
||||||
Clip = new (Screen);
|
Clip = new (Screen);
|
||||||
|
|
||||||
@@ -311,9 +327,6 @@ public class OutputBufferImpl : IOutputBuffer
|
|||||||
DirtyLines [row] = true;
|
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>
|
/// <summary>Tests whether the specified coordinate are valid for drawing the specified Text.</summary>
|
||||||
@@ -342,8 +355,9 @@ public class OutputBufferImpl : IOutputBuffer
|
|||||||
/// <inheritdoc/>
|
/// <inheritdoc/>
|
||||||
public void FillRect (Rectangle rect, Rune rune)
|
public void FillRect (Rectangle rect, Rune rune)
|
||||||
{
|
{
|
||||||
|
Rectangle clipBounds = Clip?.GetBounds () ?? Screen;
|
||||||
// BUGBUG: This should be a method on Region
|
// BUGBUG: This should be a method on Region
|
||||||
rect = Rectangle.Intersect (rect, Clip?.GetBounds () ?? Screen);
|
rect = Rectangle.Intersect (rect, clipBounds);
|
||||||
|
|
||||||
lock (Contents!)
|
lock (Contents!)
|
||||||
{
|
{
|
||||||
@@ -356,11 +370,12 @@ public class OutputBufferImpl : IOutputBuffer
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Contents [r, c] = new ()
|
// We could call AddGrapheme here, but that would acquire the lock again.
|
||||||
{
|
// So we inline the logic instead.
|
||||||
Grapheme = rune != default (Rune) ? rune.ToString () : " ",
|
SetAttributeAndDirty (c, r);
|
||||||
Attribute = CurrentAttribute, IsDirty = true
|
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>
|
/// <summary>
|
||||||
/// Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>.
|
/// 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.
|
/// 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>
|
/// </remarks>
|
||||||
/// <param name="col">Column to move to.</param>
|
/// <param name="col">Column to move to.</param>
|
||||||
/// <param name="row">Row 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;
|
Col = col;
|
||||||
Row = row;
|
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/=Mazing/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=ogonek/@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/=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/=Roslynator/@EntryIndexedValue">True</s:Boolean>
|
||||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=RRGGBB/@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>
|
<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);
|
Assert.Equal ("x", driver.Contents [5, 5].Grapheme);
|
||||||
|
|
||||||
// Clear the contents
|
// 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);
|
Assert.Equal (" ", driver.Contents [0, 0].Grapheme);
|
||||||
|
|
||||||
// Setup the region with a single rectangle, fill screen with 'x'
|
// Setup the region with a single rectangle, fill screen with 'x'
|
||||||
driver.Clip = new (new Rectangle (5, 5, 5, 5));
|
driver.Clip = new (new (5, 5, 5, 5));
|
||||||
driver.FillRect (new Rectangle (0, 0, driver.Rows, driver.Cols), 'x');
|
driver.FillRect (new (0, 0, driver.Rows, driver.Cols), new Rune ('x'));
|
||||||
Assert.Equal (" ", driver.Contents [0, 0].Grapheme);
|
Assert.Equal (" ", driver.Contents [0, 0].Grapheme);
|
||||||
Assert.Equal (" ", driver.Contents [4, 9].Grapheme);
|
Assert.Equal (" ", driver.Contents [4, 9].Grapheme);
|
||||||
Assert.Equal ("x", driver.Contents [5, 5].Grapheme);
|
Assert.Equal ("x", driver.Contents [5, 5].Grapheme);
|
||||||
|
|||||||
@@ -189,9 +189,9 @@ public class OutputBaseTests
|
|||||||
// Column 0 was written (wide glyph)
|
// Column 0 was written (wide glyph)
|
||||||
Assert.False (buffer.Contents! [0, 0].IsDirty);
|
Assert.False (buffer.Contents! [0, 0].IsDirty);
|
||||||
|
|
||||||
// Column 1 was skipped by OutputBase.Write because column 0 had a wide glyph
|
// Column 1 was marked as clean by OutputBase.Write when it processed the wide glyph at column 0
|
||||||
// So its dirty flag remains true (it was initialized as dirty by ClearContents)
|
// See: https://github.com/gui-cs/Terminal.Gui/issues/4466
|
||||||
Assert.True (buffer.Contents! [0, 1].IsDirty);
|
Assert.False (buffer.Contents! [0, 1].IsDirty);
|
||||||
|
|
||||||
// Column 2 was written ('A')
|
// Column 2 was written ('A')
|
||||||
Assert.False (buffer.Contents! [0, 2].IsDirty);
|
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