TextLayoutTests.cs 34 KB

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