Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
7 changes: 4 additions & 3 deletions Terminal.Gui/ViewBase/View.Drawing.Primitives.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
61 changes: 61 additions & 0 deletions Tests/UnitTestsParallelizable/ViewBase/Draw/DrawHotStringTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using UnitTests;

namespace ViewBaseTests.Drawing;

public class DrawHotStringTests (ITestOutputHelper output) : TestDriverBase
{
/// <summary>
/// Verifies that <see cref="View.DrawHotString(string, Attribute, Attribute)" /> 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.
/// </summary>
[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);
}

/// <summary>
/// Verifies that <see cref="View.DrawHotString(string, Attribute, Attribute)" /> 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.
/// </summary>
[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);
}
}
Loading