Files
Terminal.Gui/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs
Tig 0a9f4b8ef1 Fixes #4492, #4480 - Transparent shadows cause underlying wide glyph rendering issues (#4490)
* WIP - experiments in fixing shadow rendering issues based on #4465

Previously, shadow size was fixed at 1x1. This change introduces ShadowWidth and ShadowHeight properties to both Margin and View, allowing variable shadow dimensions. The Margin class now manages its own shadow sizing, enforcing valid values based on ShadowStyle (e.g., Opaque and Transparent require a minimum of 1, and Opaque only allows 1). Margin.Thickness is dynamically adjusted to account for shadow size, with original values preserved and restored as needed.

ShadowView rendering is updated to correctly handle wide graphemes (such as emojis) in the shadow area, preventing rendering errors. The View class exposes ShadowWidth and ShadowHeight, synchronizing with Margin. Extensive new unit tests verify correct behavior for shadow sizing, style changes, thickness adjustments, and rendering, including edge cases and visual output.

Additional minor bug fixes and refactoring are included, such as proper management of Margin's cached clip region and correcting a loop order bug in ShadowView. The codebase is also modernized with recent C# features.

* more merge

* added border tests

* Experiment...

* Incorporated latest wideglyphs

* Comment tweaks

* Add Adornments and ViewportSettings editors to WideGlyphs

Introduce AdornmentsEditor and ViewportSettingsEditor with custom border styles and positioning, enhancing UI editing capabilities. Also update arrangeableViewAtEven to use Color.Black and Color.Green, and adjust a commented border style from Dashed to Dotted.

* Fix scenario editors and tweak scenarios.

Enhance ShadowStyles with a second shadow window (transparent style) and a button event handler that shows a message box. In WideGlyphs, add AdornmentsEditor and ViewportSettingsEditor for view property editing, apply custom color schemes to arrangeable views, and update superView with a transparent shadow and increased shadow width. These changes improve interactivity and visualization in the demo scenarios.

* Fix scenario editors and tweak scenarios.

Enhance ShadowStyles with a second shadow window (transparent style) and a button event handler that shows a message box. In WideGlyphs, add AdornmentsEditor and ViewportSettingsEditor for view property editing, apply custom color schemes to arrangeable views, and update superView with a transparent shadow and increased shadow width. These changes improve interactivity and visualization in the demo scenarios.

* Make replacement char themeable via Glyphs.ReplacementChar

Adds Glyphs.ReplacementChar as a configurable replacement character, replacing all uses of Rune.ReplacementChar. The default is now a space (' ') and can be set via config.json. Updates all rendering, string decoding, and buffer invalidation logic to use the new property, ensuring consistency and themeability. Updates tests and comments accordingly. Also includes minor UI tweaks in WideGlyphs.cs and .DotSettings updates.

* merging

* merge errors

* merged

* merged

* Refactor shadow properties to Margin; update tests

ShadowWidth and ShadowHeight are now managed solely in the Margin class, with related properties and validation logic removed from View. All code and tests now use view.Margin.ShadowWidth/ShadowHeight. Tests and documentation were updated accordingly, and wide glyph handling in test output was improved for consistency.

* Simplify ShadowSize; remove it from View as it's infreqnetly used. Make it a Size to reduce API surface area.

Replace ShadowWidth/ShadowHeight with a single ShadowSize property (of type Size) in the Margin class and related code. Update all usages, validation logic, and tests to use ShadowSize.Width and ShadowSize.Height. Introduce TryValidateShadowSize for unified validation. Modernize code with C# features and improve clarity and maintainability by treating shadow dimensions as a single unit.

* reveted

* Fix wide glyph attribute handling for second column

Ensure the attribute for the second column of wide glyphs is set correctly when within the clip region, addressing issues #4258 and #4492. Add comprehensive unit tests to verify correct attribute assignment and output rendering, including scenarios with transparent shadows. Remove obsolete test code for clarity. This improves color/style consistency for wide glyphs, especially in overlapping UI situations.

* added url
2025-12-16 14:23:32 -07:00

288 lines
10 KiB
C#

using System.Buffers;
using System.Text;
using UnitTests;
using Xunit.Abstractions;
namespace DriverTests;
public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
{
[Fact]
public void AddRune ()
{
IDriver driver = CreateFakeDriver ();
driver.Rows = 25;
driver.Cols = 80;
driver.AddRune (new Rune ('a'));
Assert.Equal ("a", driver.Contents? [0, 0].Grapheme);
driver.Dispose ();
}
[Fact]
public void AddRune_Accented_Letter_With_Three_Combining_Unicode_Chars ()
{
IDriver driver = CreateFakeDriver ();
var expected = "ắ";
var text = "\u1eaf";
driver.AddStr (text);
Assert.Equal (expected, driver.Contents! [0, 0].Grapheme);
Assert.Equal (" ", driver.Contents [0, 1].Grapheme);
driver.ClearContents ();
driver.Move (0, 0);
expected = "ắ";
text = "\u0103\u0301";
driver.AddStr (text);
Assert.Equal (expected, driver.Contents [0, 0].Grapheme);
Assert.Equal (" ", driver.Contents [0, 1].Grapheme);
driver.ClearContents ();
driver.Move (0, 0);
expected = "ắ";
text = "\u0061\u0306\u0301";
driver.AddStr (text);
Assert.Equal (expected, driver.Contents [0, 0].Grapheme);
Assert.Equal (" ", driver.Contents [0, 1].Grapheme);
driver.Dispose ();
}
[Fact]
public void AddRune_InvalidLocation_DoesNothing ()
{
IDriver driver = CreateFakeDriver ();
driver.Move (driver.Cols, driver.Rows);
driver.AddRune ('a');
for (var col = 0; col < driver.Cols; col++)
{
for (var row = 0; row < driver.Rows; row++)
{
Assert.Equal (" ", driver.Contents? [row, col].Grapheme);
}
}
driver.Dispose ();
}
[Fact]
public void AddRune_MovesToNextColumn ()
{
IDriver driver = CreateFakeDriver ();
driver.AddRune ('a');
Assert.Equal ("a", driver.Contents? [0, 0].Grapheme);
Assert.Equal (0, driver.Row);
Assert.Equal (1, driver.Col);
driver.AddRune ('b');
Assert.Equal ("b", driver.Contents? [0, 1].Grapheme);
Assert.Equal (0, driver.Row);
Assert.Equal (2, driver.Col);
// Move to the last column of the first row
int lastCol = driver.Cols - 1;
driver.Move (lastCol, 0);
Assert.Equal (0, driver.Row);
Assert.Equal (lastCol, driver.Col);
// Add a rune to the last column of the first row; should increment the row or col even though it's now invalid
driver.AddRune ('c');
Assert.Equal ("c", driver.Contents? [0, lastCol].Grapheme);
Assert.Equal (lastCol + 1, driver.Col);
// Add a rune; should succeed but do nothing as it's outside of Contents
driver.AddRune ('d');
Assert.Equal (lastCol + 2, driver.Col);
for (var col = 0; col < driver.Cols; col++)
{
for (var row = 0; row < driver.Rows; row++)
{
Assert.NotEqual ("d", driver.Contents? [row, col].Grapheme);
}
}
driver.Dispose ();
}
[Fact]
public void AddRune_MovesToNextColumn_Wide ()
{
IDriver driver = CreateFakeDriver ();
// 🍕 Slice of Pizza "\U0001F355"
OperationStatus operationStatus = Rune.DecodeFromUtf16 ("\U0001F355", out Rune rune, out int charsConsumed);
Assert.Equal (OperationStatus.Done, operationStatus);
Assert.Equal (charsConsumed, rune.Utf16SequenceLength);
Assert.Equal (2, rune.GetColumns ());
driver.AddRune (rune);
Assert.Equal (rune.ToString (), driver.Contents? [0, 0].Grapheme);
Assert.Equal (0, driver.Row);
Assert.Equal (2, driver.Col);
driver.Dispose ();
}
[Fact]
public void AddStr_Glyph_On_Second_Cell_Of_Wide_Glyph_Outputs_Correctly ()
{
IDriver? driver = CreateFakeDriver ();
driver.SetScreenSize (6, 3);
driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①');
driver.Clip = new (driver.Screen);
driver.Move (1, 0);
driver.AddStr ("┌");
driver.Move (2, 0);
driver.AddStr ("─");
driver.Move (3, 0);
driver.AddStr ("┐");
driver.Clip.Exclude (new Region (new (1, 0, 3, 1)));
driver.Move (0, 0);
driver.AddStr ("🍎🍎🍎🍎");
DriverAssert.AssertDriverContentsAre (
"""
🍎
""",
output,
driver);
driver.Refresh ();
DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;0;0;0m\x1b[48;2;0;0;0m①┌─┐🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m",
output, driver);
}
[Fact]
public void AddStr_WideGlyph_Second_Column_Attribute_Set_When_In_Clip ()
{
// This test verifies the fix for issue #4258
// When a wide glyph is added and the second column is within the clip region,
// the attribute for column N+1 should be set to match the current attribute.
// See: OutputBufferImpl.cs line 194
using IDriver driver = CreateFakeDriver ();
driver.SetScreenSize (4, 2);
// Set a specific attribute for the wide glyph
Attribute wideGlyphAttr = new (Color.BrightRed, Color.BrightYellow);
driver.CurrentAttribute = wideGlyphAttr;
// Add a wide glyph at position (0, 0)
driver.Move (0, 0);
driver.AddStr ("🍎");
// Verify the wide glyph is in column 0
Assert.Equal ("🍎", driver.Contents! [0, 0].Grapheme);
Assert.Equal (wideGlyphAttr, driver.Contents [0, 0].Attribute);
// Verify column 1 (the second column of the wide glyph) has the correct attribute set
// This is the fix: column N+1 should have CurrentAttribute set (line 194 in OutputBufferImpl.cs)
Assert.Equal (wideGlyphAttr, driver.Contents [0, 1].Attribute);
// Verify cursor moved to column 2
Assert.Equal (2, driver.Col);
}
[Fact]
public void AddStr_WideGlyph_Second_Column_Attribute_Not_Set_When_Outside_Clip ()
{
// This test verifies that when a wide glyph's second column is outside the clip,
// the attribute for column N+1 is NOT modified
using IDriver driver = CreateFakeDriver ();
driver.SetScreenSize (4, 2);
// Set initial attribute for the entire contents
Attribute initialAttr = new (Color.White, Color.Black);
driver.CurrentAttribute = initialAttr;
driver.Move (0, 0);
driver.AddStr (" ");
driver.Move (0, 1);
driver.AddStr (" ");
// Create a clip that excludes column 1
driver.Clip = new (new Rectangle (0, 0, 1, 2));
// Set a different attribute for the wide glyph
Attribute wideGlyphAttr = new (Color.BrightRed, Color.BrightYellow);
driver.CurrentAttribute = wideGlyphAttr;
// Try to add a wide glyph at position (0, 0)
// Column 0 is in clip, but column 1 is NOT
driver.Move (0, 0);
driver.AddStr ("🍎");
// Verify column 0 has the replacement character (can't fit wide glyph)
Assert.NotEqual ("🍎", driver.Contents! [0, 0].Grapheme);
// Verify column 1 still has the original attribute (NOT modified)
Assert.Equal (initialAttr, driver.Contents [0, 1].Attribute);
}
[Fact]
public void AddStr_WideGlyph_Second_Column_Attribute_Outputs_Correctly ()
{
// This test verifies the fix for issue #4258 by checking the actual driver output
// This mimics what happens when TransparentShadow redraws a wide glyph from ScreenContents
// WITHOUT line 194, column N+1's attribute doesn't get set, causing wrong colors in output
// See: OutputBufferImpl.cs line ~196 (Contents [Row, Col].Attribute = CurrentAttribute;)
using IDriver driver = CreateFakeDriver ();
driver.SetScreenSize (3, 1);
driver.Force16Colors = true;
// Step 1: Draw initial content - a wide glyph at column 1 with white-on-black
driver.CurrentAttribute = new Attribute (Color.White, Color.Black);
driver.Move (1, 0);
driver.AddStr ("🍎X"); // Wide glyph at columns 1-2, 'X' at column 3 doesn't exist (off-screen)
// At this point:
// - Column 0: space (default) with white-on-black
// - Column 1: 🍎 with white-on-black
// - Column 2: (part of 🍎) with white-on-black (from initial ClearContents)
// Step 2: Now redraw the SAME wide glyph at column 1 but with a DIFFERENT attribute (red-on-yellow)
// This simulates what transparent shadow does - it redraws what's underneath with a dimmed attribute
driver.CurrentAttribute = new Attribute (Color.BrightRed, Color.BrightYellow);
driver.Move (1, 0);
driver.AddStr ("🍎");
// Verify internal state
Assert.Equal ("🍎", driver.Contents! [0, 1].Grapheme);
Assert.Equal (new Attribute (Color.BrightRed, Color.BrightYellow), driver.Contents [0, 1].Attribute);
// THIS is the critical assertion - column 2's attribute MUST be red-on-yellow
// WITHOUT line 194: column 2 retains white-on-black
// WITH line 194: column 2 gets red-on-yellow
Assert.Equal (new Attribute (Color.BrightRed, Color.BrightYellow), driver.Contents [0, 2].Attribute);
driver.Refresh ();
// Expected output:
// Column 0: space with white-on-black
// Columns 1-2: 🍎 with red-on-yellow (both columns must have same attribute!)
//
// WITHOUT line 196, the output would be:
// \x1b[97m\x1b[40m (white-on-black for column 0)
// \x1b[91m\x1b[103m🍎 (red-on-yellow starts at column 1)
// \x1b[97m\x1b[40m (WRONG! Attribute changes mid-glyph because column 2 still has white-on-black)
//
// WITH line 196, the output is:
// \x1b[97m\x1b[40m (white-on-black for column 0)
// \x1b[91m\x1b[103m🍎 (red-on-yellow for both columns 1 and 2)
DriverAssert.AssertDriverOutputIs (
"\x1b[97m\x1b[40m \x1b[91m\x1b[103m🍎",
output,
driver);
}
}