diff --git a/Terminal.Gui/ViewBase/View.Drawing.Primitives.cs b/Terminal.Gui/ViewBase/View.Drawing.Primitives.cs index c5971598c8..a9c5c877e4 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.Primitives.cs +++ b/Terminal.Gui/ViewBase/View.Drawing.Primitives.cs @@ -106,16 +106,17 @@ public void DrawHotString (string text, Attribute hotColor, Attribute normalColo Rune hotkeySpec = HotKeySpecifier == (Rune)0xffff ? (Rune)'_' : HotKeySpecifier; SetAttribute (normalColor); - foreach (Rune rune in text.EnumerateRunes ()) + foreach (string grapheme in GraphemeHelper.GetGraphemes (text)) { - if (rune == new Rune (hotkeySpec.Value)) + // The hotkey specifier is always a single simple character (e.g., '_') + if (grapheme.Length == 1 && new Rune (grapheme [0]) == new Rune (hotkeySpec.Value)) { SetAttribute (hotColor); continue; } - AddRune (rune); + AddStr (grapheme); SetAttribute (normalColor); } } diff --git a/Tests/UnitTestsParallelizable/ViewBase/Draw/DrawHotStringTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Draw/DrawHotStringTests.cs new file mode 100644 index 0000000000..781f9371f1 --- /dev/null +++ b/Tests/UnitTestsParallelizable/ViewBase/Draw/DrawHotStringTests.cs @@ -0,0 +1,61 @@ +using UnitTests; + +namespace ViewBaseTests.Drawing; + +public class DrawHotStringTests (ITestOutputHelper output) : TestDriverBase +{ + /// + /// Verifies that iterates by grapheme cluster, + /// not by rune. When iterating by rune, combining marks (e.g., acute accent U+0301) are sent individually + /// via AddRune and fail to compose with their base character. Iterating by grapheme and using AddStr + /// ensures the combining mark stays attached to its base. + /// + [Theory] + [InlineData ("e\u0301", "é")] // e + combining acute → é + [InlineData ("n\u0303o", "ño")] // n + combining tilde + o → ño + [InlineData ("Les Mise\u0301rables", "Les Misérables")] // combining acute inside word + public void DrawHotString_CombiningMarks (string input, string expectedRendered) + { + // setup + IDriver driver = CreateTestDriver (); + driver.Clip = new Region (driver.Screen); + var view = new View + { + Driver = driver, + Width = 20, Height = 1 + }; + + // execute + view.DrawHotString (input, Attribute.Default, Attribute.Default); + + // verify + DriverAssert.AssertDriverContentsWithFrameAre (expectedRendered, output, driver); + } + + /// + /// Verifies that correctly handles + /// the hotkey specifier when combined with grapheme clusters. The hotkey specifier ('_') should + /// switch to hot color, and subsequent grapheme clusters (including combining marks) should render + /// correctly. + /// + [Fact] + public void DrawHotString_HotkeyWithCombiningMarks () + { + // setup — "_Re\u0301sume\u0301" → hotkey on 'R', combining accents compose correctly + IDriver driver = CreateTestDriver (); + driver.Clip = new Region (driver.Screen); + var view = new View + { + Driver = driver, + Width = 20, Height = 1 + }; + + // execute + var hotColor = new Attribute (Color.Red, Color.Black); + var normalColor = new Attribute (Color.White, Color.Black); + view.DrawHotString ("_Re\u0301sume\u0301", hotColor, normalColor); + + // verify — the rendered text should show "Résumé" (without the underscore) + DriverAssert.AssertDriverContentsWithFrameAre ("Résumé", output, driver); + } +}