diff --git a/Examples/UICatalog/Resources/config.json b/Examples/UICatalog/Resources/config.json index 74586e878..cc7b5009f 100644 --- a/Examples/UICatalog/Resources/config.json +++ b/Examples/UICatalog/Resources/config.json @@ -9,6 +9,7 @@ "Themes": [ { "Hot Dog Stand": { + "Glyphs.WideGlyphReplacement": "①", "Schemes": [ { "Runnable": { @@ -134,7 +135,7 @@ } }, { - "UI Catalog Theme": { + "UI Catalog": { "Window.DefaultShadow": "Transparent", "Button.DefaultShadow": "None", "CheckBox.DefaultHighlightStates": "In, Pressed, PressedOutside", diff --git a/Examples/UICatalog/Scenarios/WideGlyphs.cs b/Examples/UICatalog/Scenarios/WideGlyphs.cs index 9b2e80d26..771355f1a 100644 --- a/Examples/UICatalog/Scenarios/WideGlyphs.cs +++ b/Examples/UICatalog/Scenarios/WideGlyphs.cs @@ -34,10 +34,6 @@ public sealed class WideGlyphs : Scenario AutoSelectAdornments = false, ShowViewIdentifier = true }; - adornmentsEditor.ExpanderButton.Accepting += (sender, args) => - { - //adornmentsEditor.ExpanderButton.Collapsed = args.NewValue; - }; appWindow.Add (adornmentsEditor); ViewportSettingsEditor viewportSettingsEditor = new () diff --git a/Terminal.Gui/Drawing/Glyphs.cs b/Terminal.Gui/Drawing/Glyphs.cs index 71336009d..f83666006 100644 --- a/Terminal.Gui/Drawing/Glyphs.cs +++ b/Terminal.Gui/Drawing/Glyphs.cs @@ -26,6 +26,11 @@ public class Glyphs // IMPORTANT: Configuration Manager test SaveDefaults uses this class to generate the default config file // IMPORTANT: in ./UnitTests/bin/Debug/netX.0/config.json + /// Unicode replacement character; used by Drivers when rendering in cases where a wide glyph can't + /// be output because it would be clipped. Defaults to ' ' (Space). + [ConfigurationProperty (Scope = typeof (ThemeScope))] + public static Rune WideGlyphReplacement { get; set; } = (Rune)' '; + /// File icon. Defaults to ☰ (Trigram For Heaven) [ConfigurationProperty (Scope = typeof (ThemeScope))] public static Rune File { get; set; } = (Rune)'☰'; diff --git a/Terminal.Gui/Drivers/DriverImpl.cs b/Terminal.Gui/Drivers/DriverImpl.cs index 2368dd631..ed61da17a 100644 --- a/Terminal.Gui/Drivers/DriverImpl.cs +++ b/Terminal.Gui/Drivers/DriverImpl.cs @@ -146,6 +146,9 @@ internal class DriverImpl : IDriver private readonly IOutput _output; + /// + public IOutputBuffer GetOutputBuffer () => OutputBuffer; + public IOutput GetOutput () => _output; private readonly IInputProcessor _inputProcessor; diff --git a/Terminal.Gui/Drivers/IDriver.cs b/Terminal.Gui/Drivers/IDriver.cs index 41ff4d091..eda8841c8 100644 --- a/Terminal.Gui/Drivers/IDriver.cs +++ b/Terminal.Gui/Drivers/IDriver.cs @@ -64,7 +64,13 @@ public interface IDriver : IDisposable IInputProcessor GetInputProcessor (); /// - /// Gets the output handler responsible for writing to the terminal. + /// Gets the containing the buffered screen contents. + /// + /// + IOutputBuffer GetOutputBuffer (); + + /// + /// Gets the responsible for writing to the terminal. /// IOutput GetOutput (); diff --git a/Terminal.Gui/Drivers/IOutputBuffer.cs b/Terminal.Gui/Drivers/IOutputBuffer.cs index 3344d0ba8..cf096b834 100644 --- a/Terminal.Gui/Drivers/IOutputBuffer.cs +++ b/Terminal.Gui/Drivers/IOutputBuffer.cs @@ -1,5 +1,4 @@ - -namespace Terminal.Gui.Drivers; +namespace Terminal.Gui.Drivers; /// /// Represents the desired screen state for console rendering. This interface provides methods for building up @@ -128,4 +127,15 @@ public interface IOutputBuffer /// Changing this may have unexpected consequences. /// int Top { get; set; } + + /// + /// Sets the replacement character that will be used when a wide glyph (double-width character) cannot fit in the + /// available space. + /// If not set, the default will be . + /// + /// + /// The character used when the first column of a wide character is invalid (for example, when it is overlapped by the + /// trailing half of a previous wide character). + /// + void SetWideGlyphReplacement (Rune column1ReplacementChar); } diff --git a/Terminal.Gui/Drivers/OutputBufferImpl.cs b/Terminal.Gui/Drivers/OutputBufferImpl.cs index be03a26c6..9f0b074fe 100644 --- a/Terminal.Gui/Drivers/OutputBufferImpl.cs +++ b/Terminal.Gui/Drivers/OutputBufferImpl.cs @@ -65,6 +65,14 @@ public class OutputBufferImpl : IOutputBuffer /// The topmost row in the terminal. public virtual int Top { get; set; } = 0; + private Rune _column1ReplacementChar = Glyphs.WideGlyphReplacement; + + /// + public void SetWideGlyphReplacement (Rune column1ReplacementChar) + { + _column1ReplacementChar = column1ReplacementChar; + } + /// /// Indicates which lines have been modified and need to be redrawn. /// @@ -205,7 +213,7 @@ public class OutputBufferImpl : IOutputBuffer { if (col > 0 && Contents! [row, col - 1].Grapheme.GetColumns () > 1) { - Contents [row, col - 1].Grapheme = Rune.ReplacementChar.ToString (); + Contents [row, col - 1].Grapheme = _column1ReplacementChar.ToString (); Contents [row, col - 1].IsDirty = true; } } @@ -273,17 +281,7 @@ public class OutputBufferImpl : IOutputBuffer if (!Clip!.Contains (col + 1, row)) { // Second column is outside clip - can't fit wide char here - Contents! [row, col].Grapheme = Rune.ReplacementChar.ToString (); - } - 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) - { - Contents! [row, col + 1].Grapheme = Rune.ReplacementChar.ToString (); - Contents! [row, col + 1].IsDirty = true; - } + Contents! [row, col].Grapheme = _column1ReplacementChar.ToString (); } else { diff --git a/Terminal.Gui/ViewBase/View.Mouse.cs b/Terminal.Gui/ViewBase/View.Mouse.cs index a1df6dd59..88cc4579d 100644 --- a/Terminal.Gui/ViewBase/View.Mouse.cs +++ b/Terminal.Gui/ViewBase/View.Mouse.cs @@ -256,7 +256,7 @@ public partial class View // Mouse APIs /// /// /// Invokes commands bound to mouse clicks via - /// (default: event) + /// (default: event) /// /// /// @@ -295,7 +295,7 @@ public partial class View // Mouse APIs /// /// /// - /// + /// /// /// public bool? NewMouseEvent (MouseEventArgs mouseEvent) @@ -414,8 +414,8 @@ public partial class View // Mouse APIs /// /// INTERNAL: For cases where the view is grabbed and the mouse is pressed, this method handles the pressed events from /// the driver. - /// When is set, this method will raise the Clicked/Selecting event - /// via each time it is called (after the first time the mouse is pressed). + /// When is set, this method will raise the Clicked/Activating event + /// via each time it is called (after the first time the mouse is pressed). /// /// /// , if processing should stop, otherwise. @@ -531,7 +531,7 @@ public partial class View // Mouse APIs /// /// INTERNAL API: Converts mouse click events into s by invoking the commands bound /// to the mouse button via . By default, all mouse clicks are bound to - /// which raises the event. + /// which raises the event. /// protected bool RaiseCommandsBoundToMouse (MouseEventArgs args) { diff --git a/Tests/UnitTests/View/Draw/ClipTests.cs b/Tests/UnitTests/View/Draw/ClipTests.cs index 565795f85..bbeff3518 100644 --- a/Tests/UnitTests/View/Draw/ClipTests.cs +++ b/Tests/UnitTests/View/Draw/ClipTests.cs @@ -178,6 +178,7 @@ public class ClipTests (ITestOutputHelper _output) public void Clipping_Wide_Runes () { Application.Driver!.SetScreenSize (30, 1); + Application.Driver!.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①'); var top = new View { @@ -231,9 +232,9 @@ public class ClipTests (ITestOutputHelper _output) // 012 34 56 78 90 12 34 56 78 90 12 34 56 78 // │こ れ は 広 い ル ー ン ラ イ ン で す 。 // 01 2345678901234 56 78 90 12 34 56 - // │� |0123456989│� ン ラ イ ン で す 。 + // │① |0123456989│① ン ラ イ ン で す 。 expectedOutput = """ - │�│0123456789│ ンラインです。 + │①│0123456789│ ンラインです。 """; DriverAssert.AssertDriverContentsWithFrameAre (expectedOutput, _output); diff --git a/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs b/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs index 4ec35f770..522f917c6 100644 --- a/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs @@ -181,9 +181,9 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase { 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); @@ -197,14 +197,14 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase 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", + 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); } } diff --git a/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs b/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs index 9d88eb730..371331deb 100644 --- a/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/DriverTests.cs @@ -1,4 +1,5 @@ #nullable enable +using System.Text; using UnitTests; using Xunit.Abstractions; @@ -104,6 +105,7 @@ public class DriverTests (ITestOutputHelper output) : FakeDriverBase IApplication? app = Application.Create (); app.Init (driverName); IDriver driver = app.Driver!; + driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①'); // Need to force "windows" driver to override legacy console mode for this test driver.IsLegacyConsole = false; @@ -127,14 +129,14 @@ public class DriverTests (ITestOutputHelper output) : FakeDriverBase 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", + 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); } } diff --git a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs index 19c6ddcec..9931551b0 100644 --- a/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/OutputBaseTests.cs @@ -1,4 +1,7 @@ -namespace DriverTests; +using System.Text; +using Terminal.Gui.Drivers; + +namespace DriverTests; public class OutputBaseTests { @@ -161,6 +164,8 @@ public class OutputBaseTests // FakeOutput exposes this because it's in test scope var output = new FakeOutput { IsLegacyConsole = isLegacyConsole }; IOutputBuffer buffer = output.GetLastBuffer ()!; + buffer.SetWideGlyphReplacement ((Rune)'①'); + buffer.SetSize (3, 1); // Write '🦮' at col 0 and 'A' at col 2 @@ -209,7 +214,7 @@ public class OutputBaseTests output.Write (buffer); - Assert.Contains ("�", output.GetLastOutput ()); + Assert.Contains ("①", output.GetLastOutput ()); Assert.Contains ("X", output.GetLastOutput ()); // Dirty flags cleared for the written cells diff --git a/Tests/UnitTestsParallelizable/Drivers/OutputBufferWideCharTests.cs b/Tests/UnitTestsParallelizable/Drivers/OutputBufferWideCharTests.cs index d8da44b65..4e6a596af 100644 --- a/Tests/UnitTestsParallelizable/Drivers/OutputBufferWideCharTests.cs +++ b/Tests/UnitTestsParallelizable/Drivers/OutputBufferWideCharTests.cs @@ -1,14 +1,14 @@ 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. +/// 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) +public class OutputBufferWideCharTests { /// /// Tests that FillRect properly invalidates wide characters when overwriting them. @@ -100,7 +100,7 @@ public class OutputBufferWideCharTests (ITestOutputHelper output) // 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, 2].Grapheme, Glyphs.WideGlyphReplacement.ToString ()); Assert.Equal ("│", buffer.Contents [1, 3].Grapheme); Assert.True (buffer.Contents [1, 3].IsDirty); @@ -154,7 +154,7 @@ public class OutputBufferWideCharTests (ITestOutputHelper output) // 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.Equal (buffer.Contents [3, 7].Grapheme, Glyphs.WideGlyphReplacement.ToString ()); Assert.True (buffer.Contents [3, 7].IsDirty, "Invalidated wide char should be marked dirty"); // The border should be drawn at col 8 @@ -356,4 +356,189 @@ public class OutputBufferWideCharTests (ITestOutputHelper output) buffer.Contents [2, 7].IsDirty, "Adjacent cell should be dirty after wide char replacement"); } + + /// + /// Tests the edge case where a wide character's first column is outside the clip region + /// but the second column is inside. + /// IMPORTANT: This test documents that the code path in WriteWideGrapheme where: + /// - !Clip.Contains(col, row) is true (first column outside) + /// - Clip.Contains(col + 1, row) is true (second column inside) + /// is CURRENTLY UNREACHABLE because IsValidLocation checks Clip.Contains(col, row) and + /// returns false before WriteWideGrapheme is called. This test verifies the current behavior + /// (nothing is written when first column is outside clip). + /// If the behavior should change to write the second column with a replacement character, + /// the logic in IsValidLocation or AddGrapheme needs to be modified. + /// + [Fact] + [Trait ("Category", "Output")] + public void AddStr_WideChar_FirstColumnOutsideClip_SecondColumnInside_CurrentBehavior () + { + // Arrange + OutputBufferImpl buffer = new () + { + Rows = 5, + Cols = 10, + CurrentAttribute = new (Color.White, Color.Black) + }; + + // Set custom replacement characters to verify they're being used + Rune customColumn1Replacement = new ('◄'); + Rune customColumn2Replacement = new ('►'); + buffer.SetWideGlyphReplacement (customColumn1Replacement); + + // Set clip region that starts at column 3 (odd column) + // This creates a scenario where col 2 is outside clip, but col 3 is inside + buffer.Clip = new (new (3, 1, 5, 3)); + + // Clear initial contents to ensure clean state + for (var r = 0; r < buffer.Rows; r++) + { + for (var c = 0; c < buffer.Cols; c++) + { + buffer.Contents! [r, c].IsDirty = false; + buffer.Contents [r, c].Grapheme = " "; + } + } + + // Act - Try to draw a wide character at column 2 + // Column 2 is outside clip, but column 3 is inside clip + buffer.Move (2, 1); + buffer.AddStr ("你"); // Chinese character "you", 2 columns wide + + // Assert + // CURRENT BEHAVIOR: IsValidLocation returns false when col 2 is outside clip, + // so NOTHING is written - neither column 2 nor column 3 + Assert.Equal (" ", buffer.Contents! [1, 2].Grapheme); + Assert.False (buffer.Contents [1, 2].IsDirty, "Cell outside clip should not be marked dirty"); + + // Column 3 is also not written because IsValidLocation returned false + // The code path in WriteWideGrapheme that would write the replacement char + // to column 3 is never reached + Assert.Equal (" ", buffer.Contents [1, 3].Grapheme); + + Assert.False ( + buffer.Contents [1, 3].IsDirty, + "Currently, second column is not written when first column is outside clip"); + + // Verify Col has been advanced by only 1 (not by the wide character width) + // because the grapheme was not validated/processed when IsValidLocation returned false + Assert.Equal (3, buffer.Col); + } + + /// + /// Tests the complementary case: wide character's second column is outside clip + /// but first column is inside. This should use the column 1 replacement character. + /// + [Fact] + [Trait ("Category", "Output")] + public void AddStr_WideChar_SecondColumnOutsideClip_FirstColumnInside_UsesColumn1Replacement () + { + // Arrange + OutputBufferImpl buffer = new () + { + Rows = 5, + Cols = 10, + CurrentAttribute = new (Color.White, Color.Black) + }; + + // Set custom replacement characters + Rune customColumn1Replacement = new ('◄'); + Rune customColumn2Replacement = new ('►'); + buffer.SetWideGlyphReplacement (customColumn1Replacement); + + // Set clip region that ends at column 6 (even column) + // This creates a scenario where col 5 is inside, but col 6 is outside + buffer.Clip = new (new (0, 1, 6, 3)); + + // Clear initial contents + for (var r = 0; r < buffer.Rows; r++) + { + for (var c = 0; c < buffer.Cols; c++) + { + buffer.Contents! [r, c].IsDirty = false; + buffer.Contents [r, c].Grapheme = " "; + } + } + + // Act - Try to draw a wide character at column 5 + // Column 5 is inside clip, but column 6 is outside clip + buffer.Move (5, 1); + buffer.AddStr ("好"); // Chinese character, 2 columns wide + + // Assert + // The first column (col 5) is inside clip but second column (6) is outside + // Should use column 1 replacement char to indicate it can't fit + Assert.Equal ( + customColumn1Replacement.ToString (), + buffer.Contents! [1, 5].Grapheme); + + Assert.True ( + buffer.Contents [1, 5].IsDirty, + "First column should be marked dirty with replacement char when second column is clipped"); + + // The second column is outside clip boundaries entirely + Assert.Equal (" ", buffer.Contents [1, 6].Grapheme); + Assert.False (buffer.Contents [1, 6].IsDirty, "Cell outside clip should not be modified"); + + // Verify Col has been advanced by 2 (wide character width) + Assert.Equal (7, buffer.Col); + } + + /// + /// Tests that when both columns of a wide character are inside the clip, + /// the character is drawn normally without replacement characters. + /// + [Fact] + [Trait ("Category", "Output")] + public void AddStr_WideChar_BothColumnsInsideClip_DrawsNormally () + { + // Arrange + OutputBufferImpl buffer = new () + { + Rows = 5, + Cols = 10, + CurrentAttribute = new (Color.White, Color.Black) + }; + + // Set custom replacement characters (should NOT be used in this case) + Rune customColumn1Replacement = new ('◄'); + Rune customColumn2Replacement = new ('►'); + buffer.SetWideGlyphReplacement (customColumn1Replacement); + + // Set clip region that includes columns 2-7 + buffer.Clip = new (new (2, 1, 6, 3)); + + // Clear initial contents + for (var r = 0; r < buffer.Rows; r++) + { + for (var c = 0; c < buffer.Cols; c++) + { + buffer.Contents! [r, c].IsDirty = false; + buffer.Contents [r, c].Grapheme = " "; + } + } + + // Act - Draw a wide character at column 4 (both 4 and 5 are inside clip) + buffer.Move (4, 1); + buffer.AddStr ("山"); // Chinese character "mountain", 2 columns wide + + // Assert + // Both columns are inside clip, so the wide character should be drawn normally + Assert.Equal ("山", buffer.Contents! [1, 4].Grapheme); + Assert.True (buffer.Contents [1, 4].IsDirty, "First column should be marked dirty"); + + // The second column should NOT be marked dirty by WriteWideGrapheme + // The wide glyph naturally renders across both columns without modifying column N+1 + // See: https://github.com/gui-cs/Terminal.Gui/issues/4258 + Assert.False ( + buffer.Contents [1, 5].IsDirty, + "Adjacent cell should NOT be marked dirty when writing wide char (see #4258)"); + + // Verify no replacement characters were used + Assert.NotEqual (customColumn1Replacement.ToString (), buffer.Contents [1, 4].Grapheme); + Assert.NotEqual (customColumn2Replacement.ToString (), buffer.Contents [1, 5].Grapheme); + + // Verify Col has been advanced by 2 + Assert.Equal (6, buffer.Col); + } } diff --git a/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs index 7774d1886..e24280295 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/Draw/ViewDrawingClippingTests.cs @@ -574,6 +574,7 @@ public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBas }; superView.Add (viewWithBorderAtX0, viewWithBorderAtX1, viewWithBorderAtX2); + driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①'); app.Begin (superView); // Begin calls LayoutAndDraw, so no need to call it again here // app.LayoutAndDraw(); @@ -585,9 +586,9 @@ public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBas ┆viewWithBorderAtX0┆🍎🍎🍎 └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 - �┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎 - �┆viewWithBorderAtX1┆ 🍎🍎 - �└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ 🍎🍎 + ①┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎 + ①┆viewWithBorderAtX1┆ 🍎🍎 + ①└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ 🍎🍎 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎 🍎┆viewWithBorderAtX2┆🍎🍎 @@ -597,7 +598,7 @@ public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBas output, driver); - DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m┆viewWithBorderAtX0┆🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m�┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m�┆viewWithBorderAtX1┆ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m�└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎┆viewWithBorderAtX2┆🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m", + DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m┆viewWithBorderAtX0┆🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m①┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m①┆viewWithBorderAtX1┆ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m①└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘ 🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎┆viewWithBorderAtX2┆🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎└╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m \x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m", output, driver); DriverImpl? driverImpl = driver as DriverImpl; @@ -617,9 +618,9 @@ public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBas ┆viewWithBorderAtX0┆🍎🍎🍎 └╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘🍎🍎🍎 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 - �┌──────────────────┐ 🍎🍎 - �│viewWithBorderAtX1│ 🍎🍎 - �└──────────────────┘ 🍎🍎 + ①┌──────────────────┐ 🍎🍎 + ①│viewWithBorderAtX1│ 🍎🍎 + ①└──────────────────┘ 🍎🍎 🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎🍎 🍎┌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐🍎🍎 🍎┆viewWithBorderAtX2┆🍎🍎 @@ -675,18 +676,19 @@ public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBas }; superView.Add (viewWithBorder); + driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①'); app.Begin (superView); DriverAssert.AssertDriverContentsAre ( """ - �┌─┐🍎 - �│X│🍎 - �└─┘🍎 + ①┌─┐🍎 + ①│X│🍎 + ①└─┘🍎 """, output, driver); - DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m�┌─┐🍎�│X│🍎�└─┘🍎", + DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m①┌─┐🍎①│X│🍎①└─┘🍎", output, driver); DriverImpl? driverImpl = driver as DriverImpl; @@ -738,19 +740,21 @@ public class ViewDrawingClippingTests (ITestOutputHelper output) : FakeDriverBas Height = 3 }; + driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①'); + superView.Add (viewWithBorder); app.Begin (superView); DriverAssert.AssertDriverContentsAre ( """ - 🍎�┌─┐ - 🍎�│X│ - 🍎�└─┘ + 🍎①┌─┐ + 🍎①│X│ + 🍎①└─┘ """, output, driver); - DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎�┌─┐🍎�│X│🍎�└─┘", + DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;95;158;160m\x1b[48;2;54;69;79m🍎①┌─┐🍎①│X│🍎①└─┘", output, driver); DriverImpl? driverImpl = driver as DriverImpl;