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
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
┌──────────┬────────┬──────────────────────┐
│ hearts │ length │ LengthInTextElements │
├──────────┼────────┼──────────────────────┤
│ wide ❤️ │ 2 │ 1 │
│ normal ♥ │ 1 │ 1 │
└──────────┴────────┴──────────────────────┘
20 changes: 20 additions & 0 deletions src/Spectre.Console.Tests/Unit/CellTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
namespace Spectre.Console.Tests.Unit;

public sealed class CellTests
{
[Theory]
[InlineData("A", 1)] // ASCII
[InlineData("中", 2)] // CJK wide
[InlineData("♥", 1)] // U+2665 text heart, no variation selector
[InlineData("❤️", 2)] // U+2764 + U+FE0F emoji variation selector
[InlineData("👨‍👩‍👧", 2)] // ZWJ family sequence
[InlineData("❤️‍🔥", 2)] // U+2764 + FE0F + ZWJ + U+1F525 (heart on fire)
[InlineData("🇩🇪", 2)] // Regional Indicator pair (flag)
[InlineData("", 0)] // empty string
[InlineData("Hello World", 11)]
public void GetCellLength_Returns_Correct_Display_Width(string text, int expectedWidth)
{
Cell.GetCellLength(text).ShouldBe(expectedWidth);
}

}
19 changes: 19 additions & 0 deletions src/Spectre.Console.Tests/Unit/Widgets/Table/TableTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1034,4 +1034,23 @@ public void Should_Not_Throw_When_Rendering_Multiple_Long_CJK_Headers_In_Narrow_
// Then
result.ShouldBeNull();
}

[Fact]
[Expectation("Render_Wide_Emoji_Hearts")]
[GitHubIssue("https://github.com/spectreconsole/spectre.console/issues/1847")]
public Task Should_Render_Table_With_Wide_Emoji_Correctly()
{
// Given
var console = new TestConsole();
var table = new Table();
table.AddColumns("hearts", "length", "LengthInTextElements");
table.AddRow("wide ❤️", "2", "1");
table.AddRow("normal ♥", "1", "1");

// When
console.Write(table);

// Then
return Verifier.Verify(console.Output);
}
}
11 changes: 8 additions & 3 deletions src/Spectre.Console/Internal/Cell.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,24 @@ static Cell()
#endif
}

public static int GetCellLength(string text) => GetCellLength(text.AsSpan());

public static int GetCellLength(ReadOnlySpan<char> text)
public static int GetCellLength(string text)
{
#if !NETSTANDARD2_0
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this obviosly is now not fixed for .net standard 2.0. UnicodeCalculator.GetWidth(string) is not available for .net standard?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are completely correct. I look forward to drop netstandard2.0 at some point (around the 2.0 release of Spectre.Console). For now, I'm OK with having two different implementations (and potentially erroneous rendering on .NET Framework). What do you think?

return UnicodeCalculator.GetWidth(text);
#else
var sum = 0;
foreach (var rune in text)
{
sum += GetCellLength(rune);
}

return sum;
#endif
}

public static int GetCellLength(ReadOnlySpan<char> text)
=> GetCellLength(text.ToString());

public static int GetCellLength(char rune)
{
// TODO: We need to figure out why Segment.SplitLines fails
Expand Down
36 changes: 24 additions & 12 deletions src/Spectre.Console/Rendering/Segment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ public int CellCount()
return 0;
}

if (Text == "\n")
{
return 1;
}

return Cell.GetCellLength(Text);
}

Expand Down Expand Up @@ -160,10 +165,12 @@ public Segment StripLineEndings()
if (offset > 0)
{
var accumulated = 0;
foreach (var character in Text)
var enumerator = StringInfo.GetTextElementEnumerator(Text);
while (enumerator.MoveNext())
{
index++;
accumulated += Cell.GetCellLength(character);
var cluster = enumerator.GetTextElement();
accumulated += Cell.GetCellLength(cluster);
index += cluster.Length;
if (accumulated >= offset)
{
break;
Expand Down Expand Up @@ -429,16 +436,18 @@ public static List<Segment> Truncate(IEnumerable<Segment> segments, int maxWidth

var builder = new StringBuilder();
var accumulatedCellWidth = 0;
foreach (var character in segment.Text)
var truncateEnumerator = StringInfo.GetTextElementEnumerator(segment.Text);
while (truncateEnumerator.MoveNext())
{
var characterWidth = UnicodeCalculator.GetWidth(character);
if (accumulatedCellWidth + characterWidth > maxWidth)
var cluster = truncateEnumerator.GetTextElement();
var clusterWidth = Cell.GetCellLength(cluster);
if (accumulatedCellWidth + clusterWidth > maxWidth)
{
break;
}

builder.Append(character);
accumulatedCellWidth += characterWidth;
builder.Append(cluster);
accumulatedCellWidth += clusterWidth;
}

if (builder.Length == 0)
Expand Down Expand Up @@ -571,17 +580,20 @@ internal static List<string> SplitSegment(string text, int maxCellLength)

var length = 0;
var sb = new StringBuilder();
foreach (var ch in text)
var splitEnumerator = StringInfo.GetTextElementEnumerator(text);
while (splitEnumerator.MoveNext())
{
if (length + UnicodeCalculator.GetWidth(ch) > maxCellLength)
var cluster = splitEnumerator.GetTextElement();
var clusterWidth = Cell.GetCellLength(cluster);
if (length + clusterWidth > maxCellLength)
{
list.Add(sb.ToString());
sb.Clear();
length = 0;
}

length += UnicodeCalculator.GetWidth(ch);
sb.Append(ch);
length += clusterWidth;
sb.Append(cluster);
}

list.Add(sb.ToString());
Expand Down