Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion Terminal.Gui/Views/CharMap/CharMap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -822,6 +822,22 @@ void RenderGrapheme (string grapheme, int width, int scalar)

break;

// Modifier symbol (e.g. emoji skin tone modifiers U+1F3FB-U+1F3FF) that reports
// width of 0 because it is designed to combine with a preceding base character.
case UnicodeCategory.ModifierSymbol:
if (width > 0)
{
AddStr (grapheme);
}
else
{
SetAttributeForRole (VisualRole.Highlight);
AddStr ("M");
SetAttributeForRole (VisualRole.Normal);
}

break;

case UnicodeCategory.OtherLetter:
AddStr (grapheme);

Expand All @@ -841,7 +857,11 @@ void RenderGrapheme (string grapheme, int width, int scalar)
}
else
{
throw new InvalidOperationException ($"The Rune \"{grapheme}\" (U+{Rune.GetRuneAt (grapheme, 0).Value:x6}) has zero width and no special-case UnicodeCategory logic applies.");
// Fallback for any zero-width rune whose UnicodeCategory is not explicitly handled above.
// Display a highlighted placeholder to avoid crashes for future Unicode categories.
SetAttributeForRole (VisualRole.Highlight);
AddStr ("?");
SetAttributeForRole (VisualRole.Normal);
}

break;
Expand Down
42 changes: 41 additions & 1 deletion Tests/UnitTestsParallelizable/Views/CharMapTests.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
using System.Diagnostics.CodeAnalysis;
using System.Text;
using UnitTests;

namespace ViewsTests;

/// <summary>
/// Tests for <see cref="CharMap"/> command handling and IValue implementation.
/// </summary>
public class CharMapTests
public class CharMapTests : TestDriverBase
{
/// <summary>
/// Verifies that <see cref="CharMap.ValueChangedUntyped"/> is raised when <see cref="CharMap.SelectedCodePoint"/> changes.
Expand Down Expand Up @@ -129,4 +130,43 @@ public void Value_Property_Change_Raises_ValueChangedUntyped ()

charMap.Dispose ();
}

/// <summary>
/// Verifies that drawing a <see cref="CharMap"/> with a ModifierSymbol code point (e.g. U+1F3FB, emoji skin tone
/// modifier) does not throw <see cref="InvalidOperationException"/>.
/// </summary>
[Fact]
[RequiresUnreferencedCode ("AOT")]
[RequiresDynamicCode ("AOT")]
public void Draw_ModifierSymbol_CodePoint_Does_Not_Throw ()
{
// U+1F3FB is EMOJI MODIFIER FITZPATRICK TYPE-1-2, classified as UnicodeCategory.ModifierSymbol
// with a column width of 0 (designed to combine with a preceding base emoji).
const int modifierSymbolCodePoint = 0x1F3FB;

IDriver driver = CreateTestDriver (80, 25);
driver.Clip = new Region (driver.Screen);

CharMap charMap = new ()
{
Driver = driver,
X = 0,
Y = 0,
Width = 80,
Height = 25,
StartCodePoint = modifierSymbolCodePoint & ~0xF // row containing U+1F3FB
};

charMap.SelectedCodePoint = modifierSymbolCodePoint;

charMap.BeginInit ();
charMap.EndInit ();
charMap.LayoutSubViews ();

// This must not throw InvalidOperationException
Exception? exception = Record.Exception (() => charMap.Draw ());
Assert.Null (exception);

charMap.Dispose ();
}
}
Loading