mirror of
https://github.com/gui-cs/Terminal.Gui.git
synced 2025-12-26 07:47:54 +01:00
* Improve wide character handling in output buffer Enhances rendering and state management for wide (double-width) characters. Marks both cells as clean after rendering wide graphemes, ensures replacement cells are marked dirty when partially clipped, and uses Move/AddStr for proper wide character handling and invalidation. * Fix FillRect to handle wide Unicode chars correctly Refactored OutputBufferImpl.FillRect to properly handle wide (double-width) Unicode characters, fixing visual corruption when overwriting CJK text (e.g., with MessageBox borders). Removed the char-based FillRect overload in favor of Rune-based handling. Added helper methods for attribute/dirty management and wide glyph invalidation. Updated OutputBase.Write to always mark adjacent cells dirty for wide chars. Updated tests and added OutputBufferWideCharTests to verify correct behavior in all scenarios. This resolves issue #4466 and ensures robust rendering for wide Unicode text. * Handle wide grapheme clusters in OutputBase rendering Added logic to mark both cells of wide grapheme clusters as clean after rendering, preventing unnecessary redraws. Also included a commented-out preprocessor directive and using statement for potential future use. * Clarify comment for IsDirty logic on wide graphemes Updated the comment explaining why the next cell is marked clean (IsDirty = false) after handling wide graphemes, and added a reference to GitHub issue #4466 for context. * Update test for dirty flag after wide glyph write Adjusted OutputBaseTests to expect column 1's dirty flag to be cleared after writing a wide glyph to column 0, matching current OutputBase.Write behavior. Added clarifying comment and GitHub issue reference. * Update Tests/UnitTestsParallelizable/Drivers/OutputBufferWideCharTests.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Terminal.Gui/Drivers/OutputBufferImpl.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update Terminal.Gui/Drivers/OutputBase.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
286 lines
10 KiB
C#
286 lines
10 KiB
C#
namespace DriverTests;
|
||
|
||
public class OutputBaseTests
|
||
{
|
||
[Fact]
|
||
public void ToAnsi_SingleCell_NoAttribute_ReturnsGraphemeAndNewline ()
|
||
{
|
||
// Arrange
|
||
var output = new FakeOutput ();
|
||
IOutputBuffer buffer = output.GetLastBuffer ()!;
|
||
buffer.SetSize (1, 1);
|
||
|
||
// Act
|
||
buffer.AddStr ("A");
|
||
string ansi = output.ToAnsi (buffer);
|
||
|
||
// Assert: single grapheme plus newline (BuildAnsiForRegion appends a newline per row)
|
||
Assert.Contains ("A" + Environment.NewLine, ansi);
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData (true, false)]
|
||
[InlineData (true, true)]
|
||
[InlineData (false, false)]
|
||
[InlineData (false, true)]
|
||
public void ToAnsi_WithAttribute_AppendsCorrectColorSequence_BasedOnIsLegacyConsole_And_Force16Colors (bool isLegacyConsole, bool force16Colors)
|
||
{
|
||
// Arrange
|
||
var output = new FakeOutput { IsLegacyConsole = isLegacyConsole };
|
||
|
||
// Create DriverImpl and associate it with the FakeOutput to test Sixel output
|
||
IDriver driver = new DriverImpl (
|
||
new FakeInputProcessor (null!),
|
||
new OutputBufferImpl (),
|
||
output,
|
||
new (new AnsiResponseParser ()),
|
||
new SizeMonitorImpl (output));
|
||
|
||
driver.Force16Colors = force16Colors;
|
||
|
||
IOutputBuffer buffer = output.GetLastBuffer ()!;
|
||
buffer.SetSize (1, 1);
|
||
|
||
// Use a known RGB color and attribute
|
||
var fg = new Color (1, 2, 3);
|
||
var bg = new Color (4, 5, 6);
|
||
buffer.CurrentAttribute = new (fg, bg);
|
||
buffer.AddStr ("X");
|
||
|
||
// Act
|
||
string ansi = output.ToAnsi (buffer);
|
||
|
||
// Assert: when true color expected, we should see the RGB CSI; otherwise we should see the 16-color CSI
|
||
if (!isLegacyConsole && !force16Colors)
|
||
{
|
||
Assert.Contains ("\u001b[38;2;1;2;3m", ansi);
|
||
}
|
||
else if (!isLegacyConsole && force16Colors)
|
||
{
|
||
string expected16 = EscSeqUtils.CSI_SetForegroundColor (fg.GetAnsiColorCode ());
|
||
Assert.Contains (expected16, ansi);
|
||
}
|
||
else
|
||
{
|
||
var expected16 = (ConsoleColor)fg.GetClosestNamedColor16 ();
|
||
Assert.Equal (ConsoleColor.Black, expected16);
|
||
Assert.DoesNotContain ('\u001b', ansi);
|
||
}
|
||
|
||
// Grapheme and newline should always be present
|
||
Assert.Contains ("X" + Environment.NewLine, ansi);
|
||
}
|
||
|
||
[Fact]
|
||
public void Write_WritesDirtyCellsAndClearsDirtyFlags ()
|
||
{
|
||
// Arrange
|
||
var output = new FakeOutput ();
|
||
IOutputBuffer buffer = output.GetLastBuffer ()!;
|
||
buffer.SetSize (2, 1);
|
||
|
||
// Mark two characters as dirty by writing them into the buffer
|
||
buffer.AddStr ("AB");
|
||
|
||
// Sanity: ensure cells are dirty before calling Write
|
||
Assert.True (buffer.Contents! [0, 0].IsDirty);
|
||
Assert.True (buffer.Contents! [0, 1].IsDirty);
|
||
|
||
// Act
|
||
output.Write (buffer); // calls OutputBase.Write via FakeOutput
|
||
|
||
// Assert: content was written to the fake output and dirty flags cleared
|
||
Assert.Contains ("AB", output.GetLastOutput ());
|
||
Assert.False (buffer.Contents! [0, 0].IsDirty);
|
||
Assert.False (buffer.Contents! [0, 1].IsDirty);
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData (true)]
|
||
[InlineData (false)]
|
||
public void Write_Virtual_Or_NonVirtual_Uses_WriteToConsole_And_Clears_Dirty_Flags (bool isLegacyConsole)
|
||
{
|
||
// Arrange
|
||
// FakeOutput exposes this because it's in test scope
|
||
var output = new FakeOutput { IsLegacyConsole = isLegacyConsole };
|
||
IOutputBuffer buffer = output.GetLastBuffer ()!;
|
||
buffer.SetSize (3, 1);
|
||
|
||
// Write 'A' at col 0 and 'C' at col 2; leave col 1 untouched (not dirty)
|
||
buffer.Move (0, 0);
|
||
buffer.AddStr ("A");
|
||
buffer.Move (2, 0);
|
||
buffer.AddStr ("C");
|
||
|
||
// Confirm some dirtiness before to write
|
||
Assert.True (buffer.Contents! [0, 0].IsDirty);
|
||
Assert.True (buffer.Contents! [0, 2].IsDirty);
|
||
|
||
// Act
|
||
output.Write (buffer);
|
||
|
||
// Assert: both characters were written (use Contains to avoid CI side effects)
|
||
Assert.Contains ("A", output.GetLastOutput ());
|
||
Assert.Contains ("C", output.GetLastOutput ());
|
||
|
||
// Dirty flags cleared for the written cells
|
||
Assert.False (buffer.Contents! [0, 0].IsDirty);
|
||
Assert.False (buffer.Contents! [0, 2].IsDirty);
|
||
|
||
// Verify SetCursorPositionImpl was invoked by WriteToConsole (cursor set to a written column)
|
||
Assert.Equal (new (0, 0), output.GetCursorPosition ());
|
||
|
||
// Now write 'X' at col 0 to verify subsequent writes also work
|
||
buffer.Move (0, 0);
|
||
buffer.AddStr ("X");
|
||
|
||
// Confirm dirtiness state before to write
|
||
Assert.True (buffer.Contents! [0, 0].IsDirty);
|
||
Assert.False (buffer.Contents! [0, 2].IsDirty);
|
||
|
||
output.Write (buffer);
|
||
|
||
// Assert: both characters were written (use Contains to avoid CI side effects)
|
||
Assert.Contains ("A", output.GetLastOutput ());
|
||
Assert.Contains ("C", output.GetLastOutput ());
|
||
|
||
// Dirty flags cleared for the written cells
|
||
Assert.False (buffer.Contents! [0, 0].IsDirty);
|
||
Assert.False (buffer.Contents! [0, 2].IsDirty);
|
||
|
||
// Verify SetCursorPositionImpl was invoked by WriteToConsole (cursor set to a written column)
|
||
Assert.Equal (new (2, 0), output.GetCursorPosition ());
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData (true)]
|
||
[InlineData (false)]
|
||
public void Write_Virtual_Or_NonVirtual_Uses_WriteToConsole_And_Clears_Dirty_Flags_Mixed_Graphemes (bool isLegacyConsole)
|
||
{
|
||
// Arrange
|
||
// FakeOutput exposes this because it's in test scope
|
||
var output = new FakeOutput { IsLegacyConsole = isLegacyConsole };
|
||
IOutputBuffer buffer = output.GetLastBuffer ()!;
|
||
buffer.SetSize (3, 1);
|
||
|
||
// Write '🦮' at col 0 and 'A' at col 2
|
||
buffer.Move (0, 0);
|
||
buffer.AddStr ("🦮A");
|
||
|
||
// After the fix for https://github.com/gui-cs/Terminal.Gui/issues/4258:
|
||
// Writing a wide glyph at column 0 no longer sets column 1 to IsDirty = false.
|
||
// Column 1 retains whatever state it had (in this case, it was initialized as dirty
|
||
// by ClearContents, but may have been cleared by a previous Write call).
|
||
//
|
||
// What we care about is that wide glyphs work correctly and don't prevent
|
||
// other content from being drawn at odd columns.
|
||
Assert.True (buffer.Contents! [0, 0].IsDirty);
|
||
|
||
// Column 1 state depends on whether it was cleared by a previous Write - don't assert
|
||
Assert.True (buffer.Contents! [0, 2].IsDirty);
|
||
|
||
// Act
|
||
output.Write (buffer);
|
||
|
||
Assert.Contains ("🦮", output.GetLastOutput ());
|
||
Assert.Contains ("A", output.GetLastOutput ());
|
||
|
||
// Dirty flags cleared for the written cells
|
||
// Column 0 was written (wide glyph)
|
||
Assert.False (buffer.Contents! [0, 0].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);
|
||
|
||
Assert.Equal (new (0, 0), output.GetCursorPosition ());
|
||
|
||
// Now write 'X' at col 1 which invalidates the wide glyph at col 0
|
||
buffer.Move (1, 0);
|
||
buffer.AddStr ("X");
|
||
|
||
// Confirm dirtiness state before to write
|
||
Assert.True (buffer.Contents! [0, 0].IsDirty); // Invalidated by writing at col 1
|
||
Assert.True (buffer.Contents! [0, 1].IsDirty); // Just written
|
||
Assert.True (buffer.Contents! [0, 2].IsDirty); // Marked dirty by writing at col 1
|
||
|
||
output.Write (buffer);
|
||
|
||
Assert.Contains ("<22>", output.GetLastOutput ());
|
||
Assert.Contains ("X", output.GetLastOutput ());
|
||
|
||
// Dirty flags cleared for the written cells
|
||
Assert.False (buffer.Contents! [0, 0].IsDirty);
|
||
Assert.False (buffer.Contents! [0, 1].IsDirty);
|
||
Assert.False (buffer.Contents! [0, 2].IsDirty);
|
||
|
||
// Verify SetCursorPositionImpl was invoked by WriteToConsole (cursor set to a written column)
|
||
Assert.Equal (new (0, 0), output.GetCursorPosition ());
|
||
}
|
||
|
||
[Theory]
|
||
[InlineData (true)]
|
||
[InlineData (false)]
|
||
public void Write_EmitsSixelDataAndPositionsCursor (bool isLegacyConsole)
|
||
{
|
||
// Arrange
|
||
var output = new FakeOutput ();
|
||
IOutputBuffer buffer = output.GetLastBuffer ()!;
|
||
buffer.SetSize (1, 1);
|
||
|
||
// Ensure the buffer has some content so Write traverses rows
|
||
buffer.AddStr (".");
|
||
|
||
// Create a Sixel to render
|
||
var s = new SixelToRender
|
||
{
|
||
SixelData = "SIXEL-DATA",
|
||
ScreenPosition = new (4, 2)
|
||
};
|
||
|
||
// Create DriverImpl and associate it with the FakeOutput to test Sixel output
|
||
IDriver driver = new DriverImpl (
|
||
new FakeInputProcessor (null!),
|
||
new OutputBufferImpl (),
|
||
output,
|
||
new (new AnsiResponseParser ()),
|
||
new SizeMonitorImpl (output));
|
||
|
||
// Add the Sixel to the driver
|
||
driver.GetSixels ().Enqueue (s);
|
||
|
||
// FakeOutput exposes this because it's in test scope
|
||
output.IsLegacyConsole = isLegacyConsole;
|
||
|
||
// Act
|
||
output.Write (buffer);
|
||
|
||
if (!isLegacyConsole)
|
||
{
|
||
// Assert: Sixel data was emitted (use Contains to avoid equality/side-effects)
|
||
Assert.Contains ("SIXEL-DATA", output.GetLastOutput ());
|
||
|
||
// Cursor was moved to Sixel position
|
||
Assert.Equal (s.ScreenPosition, output.GetCursorPosition ());
|
||
}
|
||
else
|
||
{
|
||
// Assert: Sixel data was NOT emitted
|
||
Assert.DoesNotContain ("SIXEL-DATA", output.GetLastOutput ());
|
||
|
||
// Cursor was NOT moved to Sixel position
|
||
Assert.NotEqual (s.ScreenPosition, output.GetCursorPosition ());
|
||
}
|
||
|
||
IApplication app = Application.Create ();
|
||
app.Driver = driver;
|
||
|
||
Assert.Equal (driver.GetSixels (), app.Driver.GetSixels ());
|
||
|
||
app.Dispose ();
|
||
}
|
||
}
|