TextLayoutTests.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  1. using System;
  2. using System.Linq;
  3. using Avalonia.Media;
  4. using Avalonia.Media.TextFormatting;
  5. using Avalonia.Media.TextFormatting.Unicode;
  6. using Avalonia.UnitTests;
  7. using Xunit;
  8. namespace Avalonia.Skia.UnitTests
  9. {
  10. public class TextLayoutTests
  11. {
  12. private static readonly string s_singleLineText = "0123456789";
  13. private static readonly string s_multiLineText = "012345678\r\r0123456789";
  14. [Fact]
  15. public void Should_Apply_TextStyleSpan_To_Text_In_Between()
  16. {
  17. using (Start())
  18. {
  19. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  20. var spans = new[]
  21. {
  22. new TextStyleRun(
  23. new TextPointer(1, 2),
  24. new TextStyle(Typeface.Default, 12, foreground))
  25. };
  26. var layout = new TextLayout(
  27. s_multiLineText,
  28. Typeface.Default,
  29. 12.0f,
  30. Brushes.Black.ToImmutable(),
  31. textStyleOverrides : spans);
  32. var textLine = layout.TextLines[0];
  33. Assert.Equal(3, textLine.TextRuns.Count);
  34. var textRun = textLine.TextRuns[1];
  35. Assert.Equal(2, textRun.Text.Length);
  36. var actual = textRun.Text.Buffer.Span.ToString();
  37. Assert.Equal("12", actual);
  38. Assert.Equal(foreground, textRun.Style.Foreground);
  39. }
  40. }
  41. [Fact]
  42. public void Should_Not_Alter_Lines_After_TextStyleSpan_Was_Applied()
  43. {
  44. using (Start())
  45. {
  46. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  47. for (var i = 4; i < s_multiLineText.Length; i++)
  48. {
  49. var spans = new[]
  50. {
  51. new TextStyleRun(
  52. new TextPointer(0, i),
  53. new TextStyle(Typeface.Default, 12, foreground))
  54. };
  55. var expected = new TextLayout(
  56. s_multiLineText,
  57. Typeface.Default,
  58. 12.0f,
  59. Brushes.Black.ToImmutable(),
  60. textWrapping: TextWrapping.Wrap,
  61. maxWidth : 25);
  62. var actual = new TextLayout(
  63. s_multiLineText,
  64. Typeface.Default,
  65. 12.0f,
  66. Brushes.Black.ToImmutable(),
  67. textWrapping : TextWrapping.Wrap,
  68. maxWidth : 25,
  69. textStyleOverrides : spans);
  70. Assert.Equal(expected.TextLines.Count, actual.TextLines.Count);
  71. for (var j = 0; j < actual.TextLines.Count; j++)
  72. {
  73. Assert.Equal(expected.TextLines[j].Text.Length, actual.TextLines[j].Text.Length);
  74. Assert.Equal(expected.TextLines[j].TextRuns.Sum(x => x.Text.Length),
  75. actual.TextLines[j].TextRuns.Sum(x => x.Text.Length));
  76. }
  77. }
  78. }
  79. }
  80. [Fact]
  81. public void Should_Apply_TextStyleSpan_To_Text_At_Start()
  82. {
  83. using (Start())
  84. {
  85. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  86. var spans = new[]
  87. {
  88. new TextStyleRun(
  89. new TextPointer(0, 2),
  90. new TextStyle(Typeface.Default, 12, foreground))
  91. };
  92. var layout = new TextLayout(
  93. s_singleLineText,
  94. Typeface.Default,
  95. 12.0f,
  96. Brushes.Black.ToImmutable(),
  97. textStyleOverrides : spans);
  98. var textLine = layout.TextLines[0];
  99. Assert.Equal(2, textLine.TextRuns.Count);
  100. var textRun = textLine.TextRuns[0];
  101. Assert.Equal(2, textRun.Text.Length);
  102. var actual = s_singleLineText.Substring(textRun.Text.Start,
  103. textRun.Text.Length);
  104. Assert.Equal("01", actual);
  105. Assert.Equal(foreground, textRun.Style.Foreground);
  106. }
  107. }
  108. [Fact]
  109. public void Should_Apply_TextStyleSpan_To_Text_At_End()
  110. {
  111. using (Start())
  112. {
  113. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  114. var spans = new[]
  115. {
  116. new TextStyleRun(
  117. new TextPointer(8, 2),
  118. new TextStyle(Typeface.Default, 12, foreground))
  119. };
  120. var layout = new TextLayout(
  121. s_singleLineText,
  122. Typeface.Default,
  123. 12.0f,
  124. Brushes.Black.ToImmutable(),
  125. textStyleOverrides : spans);
  126. var textLine = layout.TextLines[0];
  127. Assert.Equal(2, textLine.TextRuns.Count);
  128. var textRun = textLine.TextRuns[1];
  129. Assert.Equal(2, textRun.Text.Length);
  130. var actual = textRun.Text.Buffer.Span.ToString();
  131. Assert.Equal("89", actual);
  132. Assert.Equal(foreground, textRun.Style.Foreground);
  133. }
  134. }
  135. [Fact]
  136. public void Should_Apply_TextStyleSpan_To_Single_Character()
  137. {
  138. using (Start())
  139. {
  140. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  141. var spans = new[]
  142. {
  143. new TextStyleRun(
  144. new TextPointer(0, 1),
  145. new TextStyle(Typeface.Default, 12, foreground))
  146. };
  147. var layout = new TextLayout(
  148. "0",
  149. Typeface.Default,
  150. 12.0f,
  151. Brushes.Black.ToImmutable(),
  152. textStyleOverrides : spans);
  153. var textLine = layout.TextLines[0];
  154. Assert.Equal(1, textLine.TextRuns.Count);
  155. var textRun = textLine.TextRuns[0];
  156. Assert.Equal(1, textRun.Text.Length);
  157. Assert.Equal(foreground, textRun.Style.Foreground);
  158. }
  159. }
  160. [Fact]
  161. public void Should_Apply_TextSpan_To_Unicode_String_In_Between()
  162. {
  163. using (Start())
  164. {
  165. const string text = "😄😄😄😄";
  166. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  167. var spans = new[]
  168. {
  169. new TextStyleRun(
  170. new TextPointer(2, 2),
  171. new TextStyle(Typeface.Default, 12, foreground))
  172. };
  173. var layout = new TextLayout(
  174. text,
  175. Typeface.Default,
  176. 12.0f,
  177. Brushes.Black.ToImmutable(),
  178. textStyleOverrides: spans);
  179. var textLine = layout.TextLines[0];
  180. Assert.Equal(3, textLine.TextRuns.Count);
  181. var textRun = textLine.TextRuns[1];
  182. Assert.Equal(2, textRun.Text.Length);
  183. var actual = textRun.Text.Buffer.Span.ToString();
  184. Assert.Equal("😄", actual);
  185. Assert.Equal(foreground, textRun.Style.Foreground);
  186. }
  187. }
  188. [Fact]
  189. public void TextLength_Should_Be_Equal_To_TextLine_Length_Sum()
  190. {
  191. using (Start())
  192. {
  193. var layout = new TextLayout(
  194. s_multiLineText,
  195. Typeface.Default,
  196. 12.0f,
  197. Brushes.Black.ToImmutable());
  198. Assert.Equal(s_multiLineText.Length, layout.TextLines.Sum(x => x.Text.Length));
  199. }
  200. }
  201. [Fact]
  202. public void TextLength_Should_Be_Equal_To_TextRun_TextLength_Sum()
  203. {
  204. using (Start())
  205. {
  206. var layout = new TextLayout(
  207. s_multiLineText,
  208. Typeface.Default,
  209. 12.0f,
  210. Brushes.Black.ToImmutable());
  211. Assert.Equal(
  212. s_multiLineText.Length,
  213. layout.TextLines.Select(textLine =>
  214. textLine.TextRuns.Sum(textRun => textRun.Text.Length))
  215. .Sum());
  216. }
  217. }
  218. [Fact]
  219. public void TextLength_Should_Be_Equal_To_TextRun_TextLength_Sum_After_Wrap_With_Style_Applied()
  220. {
  221. using (Start())
  222. {
  223. const string text =
  224. "Multiline TextBox with TextWrapping.\r\rLorem ipsum dolor sit amet, consectetur adipiscing elit. " +
  225. "Vivamus magna. Cras in mi at felis aliquet congue. Ut a est eget ligula molestie gravida. " +
  226. "Curabitur massa. Donec eleifend, libero at sagittis mollis, tellus est malesuada tellus, at luctus turpis elit sit amet quam. Vivamus pretium ornare est.";
  227. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  228. var spans = new[]
  229. {
  230. new TextStyleRun(
  231. new TextPointer(0, 24),
  232. new TextStyle(Typeface.Default, 12, foreground))
  233. };
  234. var layout = new TextLayout(
  235. text,
  236. Typeface.Default,
  237. 12.0f,
  238. Brushes.Black.ToImmutable(),
  239. textWrapping : TextWrapping.Wrap,
  240. maxWidth : 180,
  241. textStyleOverrides: spans);
  242. Assert.Equal(
  243. text.Length,
  244. layout.TextLines.Select(textLine =>
  245. textLine.TextRuns.Sum(textRun => textRun.Text.Length))
  246. .Sum());
  247. }
  248. }
  249. [Fact]
  250. public void Should_Apply_TextStyleSpan_To_MultiLine()
  251. {
  252. using (Start())
  253. {
  254. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  255. var spans = new[]
  256. {
  257. new TextStyleRun(
  258. new TextPointer(5, 20),
  259. new TextStyle(Typeface.Default, 12, foreground))
  260. };
  261. var layout = new TextLayout(
  262. s_multiLineText,
  263. Typeface.Default,
  264. 12.0f,
  265. Brushes.Black.ToImmutable(),
  266. maxWidth : 200,
  267. maxHeight : 125,
  268. textStyleOverrides: spans);
  269. Assert.Equal(foreground, layout.TextLines[0].TextRuns[1].Style.Foreground);
  270. Assert.Equal(foreground, layout.TextLines[1].TextRuns[0].Style.Foreground);
  271. Assert.Equal(foreground, layout.TextLines[2].TextRuns[0].Style.Foreground);
  272. }
  273. }
  274. [Fact]
  275. public void Should_Hit_Test_SurrogatePair()
  276. {
  277. using (Start())
  278. {
  279. const string text = "😄😄";
  280. var layout = new TextLayout(
  281. text,
  282. Typeface.Default,
  283. 12.0f,
  284. Brushes.Black.ToImmutable());
  285. var shapedRun = (ShapedTextRun)layout.TextLines[0].TextRuns[0];
  286. var glyphRun = shapedRun.GlyphRun;
  287. var width = glyphRun.Bounds.Width;
  288. var characterHit = glyphRun.GetCharacterHitFromDistance(width, out _);
  289. Assert.Equal(2, characterHit.FirstCharacterIndex);
  290. Assert.Equal(2, characterHit.TrailingLength);
  291. }
  292. }
  293. [Theory]
  294. [InlineData("☝🏿", new ushort[] { 0 })]
  295. [InlineData("☝🏿 ab", new ushort[] { 0, 3, 4, 5 })]
  296. [InlineData("ab ☝🏿", new ushort[] { 0, 1, 2, 3 })]
  297. public void Should_Create_Valid_Clusters_For_Text(string text, ushort[] clusters)
  298. {
  299. using (Start())
  300. {
  301. var layout = new TextLayout(
  302. text,
  303. Typeface.Default,
  304. 12.0f,
  305. Brushes.Black.ToImmutable());
  306. var textLine = layout.TextLines[0];
  307. var index = 0;
  308. foreach (var textRun in textLine.TextRuns)
  309. {
  310. var shapedRun = (ShapedTextRun)textRun;
  311. var glyphRun = shapedRun.GlyphRun;
  312. var glyphClusters = glyphRun.GlyphClusters;
  313. var expected = clusters.Skip(index).Take(glyphClusters.Length).ToArray();
  314. Assert.Equal(expected, glyphRun.GlyphClusters);
  315. index += glyphClusters.Length;
  316. }
  317. }
  318. }
  319. [Theory]
  320. [InlineData("abcde\r\n")]
  321. [InlineData("abcde\n\r")]
  322. public void Should_Break_With_BreakChar_Pair(string text)
  323. {
  324. using (Start())
  325. {
  326. var layout = new TextLayout(
  327. text,
  328. Typeface.Default,
  329. 12.0f,
  330. Brushes.Black.ToImmutable());
  331. Assert.Equal(2, layout.TextLines.Count);
  332. Assert.Equal(1, layout.TextLines[0].TextRuns.Count);
  333. Assert.Equal(7, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Length);
  334. Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[5]);
  335. Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]);
  336. }
  337. }
  338. [Fact]
  339. public void Should_Have_One_Run_With_Common_Script()
  340. {
  341. using (Start())
  342. {
  343. var layout = new TextLayout(
  344. "abcde\r\n",
  345. Typeface.Default,
  346. 12.0f,
  347. Brushes.Black.ToImmutable());
  348. Assert.Equal(1, layout.TextLines[0].TextRuns.Count);
  349. }
  350. }
  351. [Fact]
  352. public void Should_Layout_Corrupted_Text()
  353. {
  354. using (Start())
  355. {
  356. var text = new string(new[] { '\uD802', '\uD802', '\uD802', '\uD802', '\uD802', '\uD802', '\uD802' });
  357. var layout = new TextLayout(
  358. text,
  359. Typeface.Default,
  360. 12,
  361. Brushes.Black.ToImmutable());
  362. var textLine = layout.TextLines[0];
  363. var textRun = (ShapedTextRun)textLine.TextRuns[0];
  364. Assert.Equal(7, textRun.Text.Length);
  365. var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint);
  366. foreach (var glyph in textRun.GlyphRun.GlyphIndices)
  367. {
  368. Assert.Equal(replacementGlyph, glyph);
  369. }
  370. }
  371. }
  372. public static IDisposable Start()
  373. {
  374. var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
  375. .With(renderInterface: new PlatformRenderInterface(null),
  376. textShaperImpl: new TextShaperImpl(),
  377. fontManagerImpl : new CustomFontManagerImpl()));
  378. return disposable;
  379. }
  380. }
  381. }