TextLayoutTests.cs 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Linq;
  5. using System.Runtime.InteropServices;
  6. using Avalonia.Media;
  7. using Avalonia.Media.TextFormatting;
  8. using Avalonia.Media.TextFormatting.Unicode;
  9. using Avalonia.UnitTests;
  10. using Avalonia.Utilities;
  11. using Xunit;
  12. namespace Avalonia.Skia.UnitTests.Media.TextFormatting
  13. {
  14. public class TextLayoutTests
  15. {
  16. private const string SingleLineText = "0123456789";
  17. private const string MultiLineText = "01 23 45 678\r\rabc def gh ij";
  18. private const string RightToLeftText = "זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן";
  19. [InlineData("01234\r01234\r", 3)]
  20. [InlineData("01234\r01234", 2)]
  21. [Theory]
  22. public void Should_Break_Lines(string text, int numberOfLines)
  23. {
  24. using (Start())
  25. {
  26. var layout = new TextLayout(
  27. text,
  28. Typeface.Default,
  29. 12.0f,
  30. Brushes.Black);
  31. Assert.Equal(numberOfLines, layout.TextLines.Count);
  32. }
  33. }
  34. [Fact]
  35. public void Should_Apply_TextStyleSpan_To_Text_In_Between()
  36. {
  37. using (Start())
  38. {
  39. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  40. var spans = new[]
  41. {
  42. new ValueSpan<TextRunProperties>(1, 2,
  43. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  44. };
  45. var layout = new TextLayout(
  46. MultiLineText,
  47. Typeface.Default,
  48. 12.0f,
  49. Brushes.Black.ToImmutable(),
  50. textStyleOverrides: spans);
  51. var textLine = layout.TextLines[0];
  52. Assert.Equal(3, textLine.TextRuns.Count);
  53. var textRun = textLine.TextRuns[1];
  54. Assert.Equal(2, textRun.Length);
  55. var actual = textRun.Text.ToString();
  56. Assert.Equal("1 ", actual);
  57. Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
  58. }
  59. }
  60. [InlineData(27)]
  61. [InlineData(22)]
  62. [Theory]
  63. public void Should_Wrap_And_Apply_Style(int length)
  64. {
  65. using (Start())
  66. {
  67. var text = "Multiline TextBox with TextWrapping.";
  68. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  69. var expected = new TextLayout(
  70. text,
  71. Typeface.Default,
  72. 12.0f,
  73. Brushes.Black.ToImmutable(),
  74. textWrapping: TextWrapping.Wrap,
  75. maxWidth: 200);
  76. var expectedLines = expected.TextLines.Select(x => text.Substring(x.FirstTextSourceIndex,
  77. x.Length)).ToList();
  78. var spans = new[]
  79. {
  80. new ValueSpan<TextRunProperties>(0, length,
  81. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  82. };
  83. var actual = new TextLayout(
  84. text,
  85. Typeface.Default,
  86. 12.0f,
  87. Brushes.Black.ToImmutable(),
  88. textWrapping: TextWrapping.Wrap,
  89. maxWidth: 200,
  90. textStyleOverrides: spans);
  91. var actualLines = actual.TextLines.Select(x => text.Substring(x.FirstTextSourceIndex,
  92. x.Length)).ToList();
  93. Assert.Equal(expectedLines.Count, actualLines.Count);
  94. for (var j = 0; j < actual.TextLines.Count; j++)
  95. {
  96. var expectedText = expectedLines[j];
  97. var actualText = actualLines[j];
  98. Assert.Equal(expectedText, actualText);
  99. }
  100. }
  101. }
  102. [Fact]
  103. public void Should_Not_Alter_Lines_After_TextStyleSpan_Was_Applied()
  104. {
  105. using (Start())
  106. {
  107. const string text = "אחד !\ntwo !\nשְׁלוֹשָׁה !";
  108. var red = new SolidColorBrush(Colors.Red).ToImmutable();
  109. var black = Brushes.Black.ToImmutable();
  110. var expected = new TextLayout(
  111. text,
  112. Typeface.Default,
  113. 12.0f,
  114. black,
  115. textWrapping: TextWrapping.Wrap);
  116. var expectedGlyphs = GetGlyphs(expected);
  117. var outer = new GraphemeEnumerator(text);
  118. var inner = new GraphemeEnumerator(text);
  119. var i = 0;
  120. var j = 0;
  121. while (true)
  122. {
  123. while (inner.MoveNext())
  124. {
  125. j += inner.Current.Text.Length;
  126. if (j + i > text.Length)
  127. {
  128. break;
  129. }
  130. var spans = new[]
  131. {
  132. new ValueSpan<TextRunProperties>(i, j,
  133. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: red))
  134. };
  135. var actual = new TextLayout(
  136. text,
  137. Typeface.Default,
  138. 12.0f,
  139. black,
  140. textWrapping: TextWrapping.Wrap,
  141. textStyleOverrides: spans);
  142. var actualGlyphs = GetGlyphs(actual);
  143. Assert.Equal(expectedGlyphs.Count, actualGlyphs.Count);
  144. for (var k = 0; k < expectedGlyphs.Count; k++)
  145. {
  146. Assert.Equal(expectedGlyphs[k], actualGlyphs[k]);
  147. }
  148. }
  149. if (!outer.MoveNext())
  150. {
  151. break;
  152. }
  153. inner = new GraphemeEnumerator(text);
  154. i += outer.Current.Text.Length;
  155. }
  156. }
  157. static List<string> GetGlyphs(TextLayout textLayout)
  158. => textLayout.TextLines
  159. .Select(line => string.Join('|', line.TextRuns
  160. .Cast<ShapedTextRun>()
  161. .SelectMany(run => run.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphIndex)))
  162. .ToList();
  163. }
  164. [Fact]
  165. public void Should_Apply_TextStyleSpan_To_Text_At_Start()
  166. {
  167. using (Start())
  168. {
  169. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  170. var spans = new[]
  171. {
  172. new ValueSpan<TextRunProperties>(0, 2,
  173. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  174. };
  175. var layout = new TextLayout(
  176. SingleLineText,
  177. Typeface.Default,
  178. 12.0f,
  179. Brushes.Black.ToImmutable(),
  180. textStyleOverrides: spans);
  181. var textLine = layout.TextLines[0];
  182. Assert.Equal(2, textLine.TextRuns.Count);
  183. var textRun = textLine.TextRuns[0];
  184. Assert.Equal(2, textRun.Length);
  185. var actual = SingleLineText[..textRun.Length];
  186. Assert.Equal("01", actual);
  187. Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
  188. }
  189. }
  190. [Fact]
  191. public void Should_Apply_TextStyleSpan_To_Text_At_End()
  192. {
  193. using (Start())
  194. {
  195. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  196. var spans = new[]
  197. {
  198. new ValueSpan<TextRunProperties>(8, 2,
  199. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)),
  200. };
  201. var layout = new TextLayout(
  202. SingleLineText,
  203. Typeface.Default,
  204. 12.0f,
  205. Brushes.Black.ToImmutable(),
  206. textStyleOverrides: spans);
  207. var textLine = layout.TextLines[0];
  208. Assert.Equal(2, textLine.TextRuns.Count);
  209. var textRun = textLine.TextRuns[1];
  210. Assert.Equal(2, textRun.Length);
  211. var actual = textRun.Text.ToString();
  212. Assert.Equal("89", actual);
  213. Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
  214. }
  215. }
  216. [Fact]
  217. public void Should_Apply_TextStyleSpan_To_Single_Character()
  218. {
  219. using (Start())
  220. {
  221. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  222. var spans = new[]
  223. {
  224. new ValueSpan<TextRunProperties>(0, 1,
  225. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  226. };
  227. var layout = new TextLayout(
  228. "0",
  229. Typeface.Default,
  230. 12.0f,
  231. Brushes.Black.ToImmutable(),
  232. textStyleOverrides: spans);
  233. var textLine = layout.TextLines[0];
  234. Assert.Equal(1, textLine.TextRuns.Count);
  235. var textRun = textLine.TextRuns[0];
  236. Assert.Equal(1, textRun.Length);
  237. Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
  238. }
  239. }
  240. [Fact]
  241. public void Should_Apply_TextSpan_To_Unicode_String_In_Between()
  242. {
  243. using (Start())
  244. {
  245. const string text = "😄😄😄😄";
  246. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  247. var spans = new[]
  248. {
  249. new ValueSpan<TextRunProperties>(2, 2,
  250. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  251. };
  252. var layout = new TextLayout(
  253. text,
  254. Typeface.Default,
  255. 12.0f,
  256. Brushes.Black.ToImmutable(),
  257. textStyleOverrides: spans);
  258. var textLine = layout.TextLines[0];
  259. Assert.Equal(3, textLine.TextRuns.Count);
  260. var textRun = textLine.TextRuns[1];
  261. Assert.Equal(2, textRun.Length);
  262. var actual = textRun.Text.ToString();
  263. Assert.Equal("😄", actual);
  264. Assert.Equal(foreground, textRun.Properties.ForegroundBrush);
  265. }
  266. }
  267. [Fact]
  268. public void TextLength_Should_Be_Equal_To_TextLine_Length_Sum()
  269. {
  270. using (Start())
  271. {
  272. var layout = new TextLayout(
  273. MultiLineText,
  274. Typeface.Default,
  275. 12.0f,
  276. Brushes.Black.ToImmutable());
  277. Assert.Equal(MultiLineText.Length, layout.TextLines.Sum(x => x.Length));
  278. }
  279. }
  280. [Fact]
  281. public void TextLength_Should_Be_Equal_To_TextRun_TextLength_Sum()
  282. {
  283. using (Start())
  284. {
  285. var layout = new TextLayout(
  286. MultiLineText,
  287. Typeface.Default,
  288. 12.0f,
  289. Brushes.Black.ToImmutable());
  290. Assert.Equal(
  291. MultiLineText.Length,
  292. layout.TextLines.Select(textLine =>
  293. textLine.TextRuns.Sum(textRun => textRun.Length))
  294. .Sum());
  295. }
  296. }
  297. [Fact]
  298. public void TextLength_Should_Be_Equal_To_TextRun_TextLength_Sum_After_Wrap_With_Style_Applied()
  299. {
  300. using (Start())
  301. {
  302. const string text =
  303. "Multiline TextBox with TextWrapping.\r\rLorem ipsum dolor sit amet";
  304. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  305. var spans = new[]
  306. {
  307. new ValueSpan<TextRunProperties>(0, 24,
  308. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  309. };
  310. var layout = new TextLayout(
  311. text,
  312. Typeface.Default,
  313. 12.0f,
  314. Brushes.Black.ToImmutable(),
  315. textWrapping: TextWrapping.Wrap,
  316. maxWidth: 180,
  317. textStyleOverrides: spans);
  318. Assert.Equal(
  319. text.Length,
  320. layout.TextLines.Select(textLine =>
  321. textLine.TextRuns.Sum(textRun => textRun.Length))
  322. .Sum());
  323. }
  324. }
  325. [Fact]
  326. public void Should_Apply_TextStyleSpan_To_MultiLine()
  327. {
  328. using (Start())
  329. {
  330. var foreground = new SolidColorBrush(Colors.Red).ToImmutable();
  331. var spans = new[]
  332. {
  333. new ValueSpan<TextRunProperties>(5, 20,
  334. new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground))
  335. };
  336. var layout = new TextLayout(
  337. MultiLineText,
  338. Typeface.Default,
  339. 12.0f,
  340. Brushes.Black.ToImmutable(),
  341. maxWidth: 200,
  342. maxHeight: 125,
  343. textStyleOverrides: spans);
  344. Assert.Equal(foreground, layout.TextLines[0].TextRuns[1].Properties.ForegroundBrush);
  345. Assert.Equal(foreground, layout.TextLines[1].TextRuns[0].Properties.ForegroundBrush);
  346. Assert.Equal(foreground, layout.TextLines[2].TextRuns[0].Properties.ForegroundBrush);
  347. }
  348. }
  349. [Fact]
  350. public void Should_Hit_Test_SurrogatePair()
  351. {
  352. using (Start())
  353. {
  354. const string text = "😄😄";
  355. var layout = new TextLayout(
  356. text,
  357. Typeface.Default,
  358. 12.0f,
  359. Brushes.Black.ToImmutable());
  360. var shapedRun = (ShapedTextRun)layout.TextLines[0].TextRuns[0];
  361. var glyphRun = shapedRun.GlyphRun;
  362. var width = glyphRun.Size.Width;
  363. var characterHit = glyphRun.GetCharacterHitFromDistance(width, out _);
  364. Assert.Equal(2, characterHit.FirstCharacterIndex);
  365. Assert.Equal(2, characterHit.TrailingLength);
  366. }
  367. }
  368. [Theory]
  369. [InlineData("☝🏿", new int[] { 0 })]
  370. [InlineData("☝🏿 ab", new int[] { 0, 3, 4, 5 })]
  371. [InlineData("ab ☝🏿", new int[] { 0, 1, 2, 3 })]
  372. public void Should_Create_Valid_Clusters_For_Text(string text, int[] clusters)
  373. {
  374. using (Start())
  375. {
  376. var layout = new TextLayout(
  377. text,
  378. Typeface.Default,
  379. 12.0f,
  380. Brushes.Black.ToImmutable());
  381. var textLine = layout.TextLines[0];
  382. var index = 0;
  383. foreach (var textRun in textLine.TextRuns)
  384. {
  385. var shapedRun = (ShapedTextRun)textRun;
  386. var glyphClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToArray();
  387. var expected = clusters.Skip(index).Take(glyphClusters.Length).ToArray();
  388. Assert.Equal(expected, glyphClusters);
  389. index += glyphClusters.Length;
  390. }
  391. }
  392. }
  393. [Theory]
  394. [InlineData("abcde\r\n", 7)] // Carriage Return + Line Feed
  395. [InlineData("abcde\u000A", 6)] // Line Feed
  396. [InlineData("abcde\u000B", 6)] // Vertical Tab
  397. [InlineData("abcde\u000C", 6)] // Form Feed
  398. [InlineData("abcde\u000D", 6)] // Carriage Return
  399. public void Should_Break_With_BreakChar(string text, int expectedLength)
  400. {
  401. using (Start())
  402. {
  403. var layout = new TextLayout(
  404. text,
  405. Typeface.Default,
  406. 12.0f,
  407. Brushes.Black.ToImmutable());
  408. Assert.Equal(2, layout.TextLines.Count);
  409. Assert.Equal(1, layout.TextLines[0].TextRuns.Count);
  410. Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphInfos.Count);
  411. Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[5].GlyphCluster);
  412. if (expectedLength == 7)
  413. {
  414. Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).ShapedBuffer.GlyphInfos[6].GlyphCluster);
  415. }
  416. }
  417. }
  418. [Fact]
  419. public void Should_Have_One_Run_With_Common_Script()
  420. {
  421. using (Start())
  422. {
  423. var layout = new TextLayout(
  424. "abcde\r\n",
  425. Typeface.Default,
  426. 12.0f,
  427. Brushes.Black.ToImmutable());
  428. Assert.Equal(1, layout.TextLines[0].TextRuns.Count);
  429. }
  430. }
  431. [Fact]
  432. public void Should_Layout_Corrupted_Text()
  433. {
  434. using (Start())
  435. {
  436. var text = new string(new[] { '\uD802', '\uD802', '\uD802', '\uD802', '\uD802', '\uD802', '\uD802' });
  437. var layout = new TextLayout(
  438. text,
  439. Typeface.Default,
  440. 12,
  441. Brushes.Black.ToImmutable());
  442. var textLine = layout.TextLines[0];
  443. var textRun = (ShapedTextRun)textLine.TextRuns[0];
  444. Assert.Equal(7, textRun.Length);
  445. var replacementGlyph = Typeface.Default.GlyphTypeface.GetGlyph(Codepoint.ReplacementCodepoint);
  446. foreach (var glyphInfo in textRun.GlyphRun.GlyphInfos)
  447. {
  448. Assert.Equal(replacementGlyph, glyphInfo.GlyphIndex);
  449. }
  450. }
  451. }
  452. [InlineData("0123456789\r0123456789")]
  453. [InlineData("0123456789")]
  454. [Theory]
  455. public void Should_Include_First_Line_When_Constraint_Is_Surpassed(string text)
  456. {
  457. using (Start())
  458. {
  459. var glyphTypeface = Typeface.Default.GlyphTypeface;
  460. var emHeight = glyphTypeface.Metrics.DesignEmHeight;
  461. var lineHeight = glyphTypeface.Metrics.LineSpacing * (12.0 / emHeight);
  462. var layout = new TextLayout(
  463. text,
  464. Typeface.Default,
  465. 12,
  466. Brushes.Black.ToImmutable(),
  467. maxHeight: lineHeight - lineHeight * 0.5);
  468. Assert.Equal(1, layout.TextLines.Count);
  469. Assert.Equal(lineHeight, layout.Bounds.Height);
  470. }
  471. }
  472. [InlineData("0123456789\r\n0123456789\r\n0123456789", 0, 3)]
  473. [InlineData("0123456789\r\n0123456789\r\n0123456789", 1, 1)]
  474. [InlineData("0123456789\r\n0123456789\r\n0123456789", 4, 3)]
  475. [Theory]
  476. public void Should_Not_Exceed_MaxLines(string text, int maxLines, int expectedLines)
  477. {
  478. using (Start())
  479. {
  480. var layout = new TextLayout(
  481. text,
  482. Typeface.Default,
  483. 12,
  484. Brushes.Black,
  485. maxWidth: 50,
  486. maxLines: maxLines);
  487. Assert.Equal(expectedLines, layout.TextLines.Count);
  488. }
  489. }
  490. [Fact]
  491. public void Should_Produce_Fixed_Height_Lines()
  492. {
  493. using (Start())
  494. {
  495. var layout = new TextLayout(
  496. MultiLineText,
  497. Typeface.Default,
  498. 12,
  499. Brushes.Black,
  500. lineHeight: 50);
  501. foreach (var line in layout.TextLines)
  502. {
  503. Assert.Equal(50, line.Height);
  504. }
  505. }
  506. }
  507. private const string Text = "日本でTest一番読まれている英字新聞・ジャパンタイムズが発信する国内外ニュースと、様々なジャンルの特集記事。";
  508. [Fact(Skip = "Only used for profiling.")]
  509. public void Should_Wrap()
  510. {
  511. using (Start())
  512. {
  513. for (var i = 0; i < 2000; i++)
  514. {
  515. var layout = new TextLayout(
  516. Text,
  517. Typeface.Default,
  518. 12,
  519. Brushes.Black,
  520. textWrapping: TextWrapping.Wrap,
  521. maxWidth: 50);
  522. }
  523. }
  524. }
  525. [Fact]
  526. public void Should_Process_Multiple_NewLines_Properly()
  527. {
  528. using (Start())
  529. {
  530. var text = "123\r\n\r\n456\r\n\r\n";
  531. var layout = new TextLayout(
  532. text,
  533. Typeface.Default,
  534. 12.0f,
  535. Brushes.Black);
  536. Assert.Equal(5, layout.TextLines.Count);
  537. Assert.Equal("123\r\n", layout.TextLines[0].TextRuns[0].Text.ToString());
  538. Assert.Equal("\r\n", layout.TextLines[1].TextRuns[0].Text.ToString());
  539. Assert.Equal("456\r\n", layout.TextLines[2].TextRuns[0].Text.ToString());
  540. Assert.Equal("\r\n", layout.TextLines[3].TextRuns[0].Text.ToString());
  541. }
  542. }
  543. [Fact]
  544. public void Should_Wrap_Min_OneCharacter_EveryLine()
  545. {
  546. using (Start())
  547. {
  548. var layout = new TextLayout(
  549. SingleLineText,
  550. Typeface.Default,
  551. 12,
  552. Brushes.Black,
  553. textWrapping: TextWrapping.Wrap,
  554. maxWidth: 3);
  555. //every character should be new line as there not enough space for even one character
  556. Assert.Equal(SingleLineText.Length, layout.TextLines.Count);
  557. }
  558. }
  559. [Fact]
  560. public void Should_HitTestTextRange_RightToLeft()
  561. {
  562. using (Start())
  563. {
  564. const int start = 0;
  565. const int length = 10;
  566. var layout = new TextLayout(
  567. RightToLeftText,
  568. Typeface.Default,
  569. 12,
  570. Brushes.Black);
  571. var selectedText = new TextLayout(
  572. RightToLeftText.Substring(start, length),
  573. Typeface.Default,
  574. 12,
  575. Brushes.Black);
  576. var rects = layout.HitTestTextRange(start, length).ToArray();
  577. Assert.Equal(1, rects.Length);
  578. var selectedRect = rects[0];
  579. Assert.Equal(selectedText.Bounds.Width, selectedRect.Width);
  580. }
  581. }
  582. [Fact]
  583. public void Should_HitTestTextRange_BiDi()
  584. {
  585. const string text = "זה כיףabcDEFזה כיף";
  586. using (Start())
  587. {
  588. var layout = new TextLayout(
  589. text,
  590. Typeface.Default,
  591. 12.0f,
  592. Brushes.Black.ToImmutable());
  593. var textLine = layout.TextLines[0];
  594. var start = textLine.GetDistanceFromCharacterHit(new CharacterHit(5, 1));
  595. var end = textLine.GetDistanceFromCharacterHit(new CharacterHit(6, 1));
  596. var rects = layout.HitTestTextRange(0, 7).ToArray();
  597. Assert.Equal(1, rects.Length);
  598. var expected = rects[0];
  599. Assert.Equal(expected.Left, start);
  600. Assert.Equal(expected.Right, end);
  601. }
  602. }
  603. [Fact]
  604. public void Should_HitTestTextRange()
  605. {
  606. using (Start())
  607. {
  608. var layout = new TextLayout(
  609. SingleLineText,
  610. Typeface.Default,
  611. 12.0f,
  612. Brushes.Black.ToImmutable());
  613. var lineRects = layout.HitTestTextRange(0, SingleLineText.Length).ToList();
  614. Assert.Equal(layout.TextLines.Count, lineRects.Count);
  615. for (var i = 0; i < layout.TextLines.Count; i++)
  616. {
  617. var textLine = layout.TextLines[i];
  618. var rect = lineRects[i];
  619. Assert.Equal(textLine.WidthIncludingTrailingWhitespace, rect.Width);
  620. }
  621. var rects = layout.TextLines
  622. .SelectMany(x => x.TextRuns.Cast<ShapedTextRun>())
  623. .SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance)
  624. .ToArray();
  625. for (var i = 0; i < SingleLineText.Length; i++)
  626. {
  627. for (var j = 1; i + j < SingleLineText.Length; j++)
  628. {
  629. var expected = rects.AsSpan(i, j).ToArray().Sum();
  630. var actual = layout.HitTestTextRange(i, j).Sum(x => x.Width);
  631. Assert.Equal(expected, actual);
  632. }
  633. }
  634. }
  635. }
  636. [Fact]
  637. public void Should_Wrap_RightToLeft()
  638. {
  639. const string text =
  640. "يَجِبُ عَلَى الإنْسَانِ أن يَكُونَ أمِيْنَاً وَصَادِقَاً مَعَ نَفْسِهِ وَمَعَ أَهْلِهِ وَجِيْرَانِهِ وَأَنْ يَبْذُلَ كُلَّ جُهْدٍ فِي إِعْلاءِ شَأْنِ الوَطَنِ وَأَنْ يَعْمَلَ عَلَى مَا يَجْلِبُ السَّعَادَةَ لِلنَّاسِ . ولَن يَتِمَّ لَهُ ذلِك إِلا بِأَنْ يُقَدِّمَ المَنْفَعَةَ العَامَّةَ عَلَى المَنْفَعَةِ الخَاصَّةِ وَهذَا مِثَالٌ لِلتَّضْحِيَةِ .";
  641. using (Start())
  642. {
  643. for (var maxWidth = 366; maxWidth < 900; maxWidth += 33)
  644. {
  645. var layout = new TextLayout(
  646. text,
  647. Typeface.Default,
  648. 12.0f,
  649. Brushes.Black.ToImmutable(),
  650. textWrapping: TextWrapping.Wrap,
  651. flowDirection: FlowDirection.RightToLeft,
  652. maxWidth: maxWidth);
  653. foreach (var textLine in layout.TextLines)
  654. {
  655. Assert.True(textLine.Width <= maxWidth);
  656. var actual = new string(textLine.TextRuns.Cast<ShapedTextRun>()
  657. .OrderBy(x => TextTestHelper.GetStartCharIndex(x.Text))
  658. .SelectMany(x => x.Text.ToString())
  659. .ToArray());
  660. var expected = text.Substring(textLine.FirstTextSourceIndex, textLine.Length);
  661. Assert.Equal(expected, actual);
  662. }
  663. }
  664. }
  665. }
  666. [Fact]
  667. public void Should_Layout_Empty_String()
  668. {
  669. using (Start())
  670. {
  671. var layout = new TextLayout(
  672. string.Empty,
  673. Typeface.Default,
  674. 12,
  675. Brushes.Black);
  676. Assert.True(layout.Bounds.Height > 0);
  677. }
  678. }
  679. [Fact]
  680. public void Should_HitTestPoint_RightToLeft()
  681. {
  682. using (Start())
  683. {
  684. var text = "אאא AAA";
  685. var layout = new TextLayout(
  686. text,
  687. Typeface.Default,
  688. 12,
  689. Brushes.Black,
  690. flowDirection: FlowDirection.RightToLeft);
  691. var firstRun = layout.TextLines[0].TextRuns[0] as ShapedTextRun;
  692. var hit = layout.HitTestPoint(new Point());
  693. Assert.Equal(4, hit.TextPosition);
  694. var currentX = 0.0;
  695. for (var i = 0; i < firstRun.GlyphRun.GlyphInfos.Count; i++)
  696. {
  697. var cluster = firstRun.GlyphRun.GlyphInfos[i].GlyphCluster;
  698. var advance = firstRun.GlyphRun.GlyphInfos[i].GlyphAdvance;
  699. hit = layout.HitTestPoint(new Point(currentX, 0));
  700. Assert.Equal(cluster, hit.TextPosition);
  701. var hitRange = layout.HitTestTextRange(hit.TextPosition, 1);
  702. var distance = hitRange.First().Left;
  703. Assert.Equal(currentX, distance);
  704. currentX += advance;
  705. }
  706. var secondRun = layout.TextLines[0].TextRuns[1] as ShapedTextRun;
  707. hit = layout.HitTestPoint(new Point(firstRun.Size.Width, 0));
  708. Assert.Equal(7, hit.TextPosition);
  709. hit = layout.HitTestPoint(new Point(layout.TextLines[0].WidthIncludingTrailingWhitespace, 0));
  710. Assert.Equal(0, hit.TextPosition);
  711. currentX = firstRun.Size.Width + 0.5;
  712. for (var i = 0; i < secondRun.GlyphRun.GlyphInfos.Count; i++)
  713. {
  714. var cluster = secondRun.GlyphRun.GlyphInfos[i].GlyphCluster;
  715. var advance = secondRun.GlyphRun.GlyphInfos[i].GlyphAdvance;
  716. hit = layout.HitTestPoint(new Point(currentX, 0));
  717. Assert.Equal(cluster, hit.CharacterHit.FirstCharacterIndex);
  718. var hitRange = layout.HitTestTextRange(hit.CharacterHit.FirstCharacterIndex, hit.CharacterHit.TrailingLength);
  719. var distance = hitRange.First().Left + 0.5;
  720. Assert.Equal(currentX, distance);
  721. currentX += advance;
  722. }
  723. }
  724. }
  725. [Fact]
  726. public void Should_Get_CharacterHit_From_Distance_RTL()
  727. {
  728. using (Start())
  729. {
  730. var text = "أَبْجَدِيَّة عَرَبِيَّة";
  731. var layout = new TextLayout(
  732. text,
  733. Typeface.Default,
  734. 12,
  735. Brushes.Black);
  736. var textLine = layout.TextLines[0];
  737. var firstRun = (ShapedTextRun)textLine.TextRuns[0];
  738. var firstCluster = firstRun.ShapedBuffer.GlyphInfos[0].GlyphCluster;
  739. var characterHit = textLine.GetCharacterHitFromDistance(0);
  740. Assert.Equal(firstCluster, characterHit.FirstCharacterIndex);
  741. Assert.Equal(text.Length, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
  742. var distance = textLine.GetDistanceFromCharacterHit(characterHit);
  743. Assert.Equal(0, distance);
  744. distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex));
  745. var firstAdvance = firstRun.ShapedBuffer.GlyphInfos[0].GlyphAdvance;
  746. Assert.Equal(firstAdvance, distance, 5);
  747. var rect = layout.HitTestTextPosition(22);
  748. Assert.Equal(firstAdvance, rect.Left, 5);
  749. rect = layout.HitTestTextPosition(23);
  750. Assert.Equal(0, rect.Left, 5);
  751. }
  752. }
  753. [Fact]
  754. public void Should_Get_CharacterHit_From_Distance_RTL_With_TextStyles()
  755. {
  756. using (Start())
  757. {
  758. var text = "أَبْجَدِيَّة عَرَبِيَّة";
  759. var i = 0;
  760. var graphemeEnumerator = new GraphemeEnumerator(text);
  761. while (graphemeEnumerator.MoveNext())
  762. {
  763. var grapheme = graphemeEnumerator.Current;
  764. var textStyleOverrides = new[] { new ValueSpan<TextRunProperties>(i, grapheme.Text.Length, new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Red)) };
  765. i += grapheme.Text.Length;
  766. var layout = new TextLayout(
  767. text,
  768. Typeface.Default,
  769. 12,
  770. Brushes.Black,
  771. textStyleOverrides: textStyleOverrides);
  772. var textLine = layout.TextLines[0];
  773. var shapedRuns = textLine.TextRuns.Cast<ShapedTextRun>().ToList();
  774. var clusters = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster).ToList();
  775. var glyphAdvances = shapedRuns.SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphAdvance).ToList();
  776. var currentX = 0.0;
  777. var cluster = text.Length;
  778. for (int j = 0; j < clusters.Count - 1; j++)
  779. {
  780. var glyphAdvance = glyphAdvances[j];
  781. var characterHit = textLine.GetCharacterHitFromDistance(currentX);
  782. Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength);
  783. var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster));
  784. Assert.Equal(currentX, distance, 5);
  785. currentX += glyphAdvance;
  786. if(glyphAdvance > 0)
  787. {
  788. cluster = clusters[j];
  789. }
  790. }
  791. }
  792. }
  793. }
  794. private static IDisposable Start()
  795. {
  796. var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
  797. .With(renderInterface: new PlatformRenderInterface(null),
  798. textShaperImpl: new TextShaperImpl(),
  799. fontManagerImpl: new CustomFontManagerImpl()));
  800. return disposable;
  801. }
  802. }
  803. }