TextLineTests.cs 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009
  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.UnitTests;
  9. using Avalonia.Utilities;
  10. using Xunit;
  11. namespace Avalonia.Skia.UnitTests.Media.TextFormatting
  12. {
  13. public class TextLineTests
  14. {
  15. private const string s_multiLineText = "012345678\r\r0123456789";
  16. [Fact]
  17. public void Should_Get_First_CharacterHit()
  18. {
  19. using (Start())
  20. {
  21. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  22. var textSource = new SingleBufferTextSource(s_multiLineText, defaultProperties);
  23. var formatter = new TextFormatterImpl();
  24. var currentIndex = 0;
  25. while (currentIndex < s_multiLineText.Length)
  26. {
  27. var textLine =
  28. formatter.FormatLine(textSource, currentIndex, double.PositiveInfinity,
  29. new GenericTextParagraphProperties(defaultProperties));
  30. var firstCharacterHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(int.MinValue));
  31. Assert.Equal(textLine.FirstTextSourceIndex, firstCharacterHit.FirstCharacterIndex);
  32. currentIndex += textLine.Length;
  33. }
  34. }
  35. }
  36. [Fact]
  37. public void Should_Get_Last_CharacterHit()
  38. {
  39. using (Start())
  40. {
  41. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  42. var textSource = new SingleBufferTextSource(s_multiLineText, defaultProperties);
  43. var formatter = new TextFormatterImpl();
  44. var currentIndex = 0;
  45. while (currentIndex < s_multiLineText.Length)
  46. {
  47. var textLine =
  48. formatter.FormatLine(textSource, currentIndex, double.PositiveInfinity,
  49. new GenericTextParagraphProperties(defaultProperties));
  50. var lastCharacterHit = textLine.GetNextCaretCharacterHit(new CharacterHit(int.MaxValue));
  51. Assert.Equal(textLine.FirstTextSourceIndex + textLine.Length,
  52. lastCharacterHit.FirstCharacterIndex + lastCharacterHit.TrailingLength);
  53. currentIndex += textLine.Length;
  54. }
  55. }
  56. }
  57. [Fact]
  58. public void Should_Get_Next_Caret_CharacterHit_Bidi()
  59. {
  60. const string text = "אבג 1 ABC";
  61. using (Start())
  62. {
  63. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  64. var textSource = new SingleBufferTextSource(text, defaultProperties);
  65. var formatter = new TextFormatterImpl();
  66. var textLine =
  67. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  68. new GenericTextParagraphProperties(defaultProperties));
  69. var clusters = new List<int>();
  70. foreach (var textRun in textLine.TextRuns.OrderBy(x => TextTestHelper.GetStartCharIndex(x.Text)))
  71. {
  72. var shapedRun = (ShapedTextRun)textRun;
  73. var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster);
  74. clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters);
  75. }
  76. var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]);
  77. foreach (var cluster in clusters)
  78. {
  79. Assert.Equal(cluster, nextCharacterHit.FirstCharacterIndex);
  80. nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit);
  81. }
  82. var lastCharacterHit = nextCharacterHit;
  83. nextCharacterHit = textLine.GetNextCaretCharacterHit(lastCharacterHit);
  84. Assert.Equal(lastCharacterHit.FirstCharacterIndex, nextCharacterHit.FirstCharacterIndex);
  85. Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength);
  86. }
  87. }
  88. [Fact]
  89. public void Should_Get_Previous_Caret_CharacterHit_Bidi()
  90. {
  91. const string text = "אבג 1 ABC";
  92. using (Start())
  93. {
  94. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  95. var textSource = new SingleBufferTextSource(text, defaultProperties);
  96. var formatter = new TextFormatterImpl();
  97. var textLine =
  98. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  99. new GenericTextParagraphProperties(defaultProperties));
  100. var clusters = new List<int>();
  101. foreach (var textRun in textLine.TextRuns.OrderBy(x => TextTestHelper.GetStartCharIndex(x.Text)))
  102. {
  103. var shapedRun = (ShapedTextRun)textRun;
  104. var runClusters = shapedRun.ShapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster);
  105. clusters.AddRange(shapedRun.IsReversed ? runClusters.Reverse() : runClusters);
  106. }
  107. clusters.Reverse();
  108. var nextCharacterHit = new CharacterHit(text.Length - 1);
  109. foreach (var cluster in clusters)
  110. {
  111. var currentCaretIndex = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength;
  112. Assert.Equal(cluster, currentCaretIndex);
  113. nextCharacterHit = textLine.GetPreviousCaretCharacterHit(nextCharacterHit);
  114. }
  115. var lastCharacterHit = nextCharacterHit;
  116. nextCharacterHit = textLine.GetPreviousCaretCharacterHit(lastCharacterHit);
  117. Assert.Equal(lastCharacterHit.FirstCharacterIndex, nextCharacterHit.FirstCharacterIndex);
  118. Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength);
  119. }
  120. }
  121. [InlineData("𐐷𐐷𐐷𐐷𐐷")]
  122. [InlineData("01234567🎉\n")]
  123. [InlineData("𐐷1234")]
  124. [Theory]
  125. public void Should_Get_Next_Caret_CharacterHit(string text)
  126. {
  127. using (Start())
  128. {
  129. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  130. var textSource = new SingleBufferTextSource(text, defaultProperties);
  131. var formatter = new TextFormatterImpl();
  132. var textLine =
  133. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  134. new GenericTextParagraphProperties(defaultProperties));
  135. var clusters = BuildGlyphClusters(textLine);
  136. var nextCharacterHit = new CharacterHit(0);
  137. for (var i = 0; i < clusters.Count; i++)
  138. {
  139. var expectedCluster = clusters[i];
  140. var actualCluster = nextCharacterHit.FirstCharacterIndex;
  141. Assert.Equal(expectedCluster, actualCluster);
  142. nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit);
  143. }
  144. var lastCharacterHit = nextCharacterHit;
  145. nextCharacterHit = textLine.GetNextCaretCharacterHit(lastCharacterHit);
  146. Assert.Equal(lastCharacterHit.FirstCharacterIndex, nextCharacterHit.FirstCharacterIndex);
  147. Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength);
  148. nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]);
  149. foreach (var cluster in clusters)
  150. {
  151. Assert.Equal(cluster, nextCharacterHit.FirstCharacterIndex);
  152. nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit);
  153. }
  154. lastCharacterHit = nextCharacterHit;
  155. nextCharacterHit = textLine.GetNextCaretCharacterHit(lastCharacterHit);
  156. Assert.Equal(lastCharacterHit.FirstCharacterIndex, nextCharacterHit.FirstCharacterIndex);
  157. Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength);
  158. }
  159. }
  160. [InlineData("𐐷𐐷𐐷𐐷𐐷")]
  161. [InlineData("01234567🎉\n")]
  162. [InlineData("𐐷1234")]
  163. [Theory]
  164. public void Should_Get_Previous_Caret_CharacterHit(string text)
  165. {
  166. using (Start())
  167. {
  168. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  169. var textSource = new SingleBufferTextSource(text, defaultProperties);
  170. var formatter = new TextFormatterImpl();
  171. var textLine =
  172. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  173. new GenericTextParagraphProperties(defaultProperties));
  174. var clusters = textLine.TextRuns
  175. .Cast<ShapedTextRun>()
  176. .SelectMany(x => x.ShapedBuffer.GlyphInfos, (_, glyph) => glyph.GlyphCluster)
  177. .ToArray();
  178. var previousCharacterHit = new CharacterHit(text.Length);
  179. for (var i = clusters.Length - 1; i >= 0; i--)
  180. {
  181. previousCharacterHit = textLine.GetPreviousCaretCharacterHit(previousCharacterHit);
  182. Assert.Equal(clusters[i],
  183. previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength);
  184. }
  185. var firstCharacterHit = previousCharacterHit;
  186. previousCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit);
  187. Assert.Equal(firstCharacterHit.FirstCharacterIndex, previousCharacterHit.FirstCharacterIndex);
  188. Assert.Equal(0, previousCharacterHit.TrailingLength);
  189. previousCharacterHit = new CharacterHit(clusters[^1], text.Length - clusters[^1]);
  190. for (var i = clusters.Length - 1; i > 0; i--)
  191. {
  192. previousCharacterHit = textLine.GetPreviousCaretCharacterHit(previousCharacterHit);
  193. Assert.Equal(clusters[i],
  194. previousCharacterHit.FirstCharacterIndex + previousCharacterHit.TrailingLength);
  195. }
  196. firstCharacterHit = previousCharacterHit;
  197. firstCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit);
  198. previousCharacterHit = textLine.GetPreviousCaretCharacterHit(firstCharacterHit);
  199. Assert.Equal(firstCharacterHit.FirstCharacterIndex, previousCharacterHit.FirstCharacterIndex);
  200. Assert.Equal(0, previousCharacterHit.TrailingLength);
  201. }
  202. }
  203. [Fact]
  204. public void Should_Get_Distance_From_CharacterHit()
  205. {
  206. using (Start())
  207. {
  208. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  209. var textSource = new SingleBufferTextSource(s_multiLineText, defaultProperties);
  210. var formatter = new TextFormatterImpl();
  211. var textLine =
  212. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  213. new GenericTextParagraphProperties(defaultProperties));
  214. var currentDistance = 0.0;
  215. foreach (var run in textLine.TextRuns)
  216. {
  217. var textRun = (ShapedTextRun)run;
  218. var glyphRun = textRun.GlyphRun;
  219. for (var i = 0; i < glyphRun.GlyphInfos.Count; i++)
  220. {
  221. var cluster = glyphRun.GlyphInfos[i].GlyphCluster;
  222. var advance = glyphRun.GlyphInfos[i].GlyphAdvance;
  223. var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster));
  224. Assert.Equal(currentDistance, distance);
  225. currentDistance += advance;
  226. }
  227. }
  228. var actualDistance = textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length));
  229. Assert.Equal(currentDistance, actualDistance);
  230. }
  231. }
  232. [InlineData("ABC012345")] //LeftToRight
  233. [InlineData("זה כיף סתם לשמוע איך תנצח קרפד עץ טוב בגן")] //RightToLeft
  234. [Theory]
  235. public void Should_Get_CharacterHit_From_Distance(string text)
  236. {
  237. using (Start())
  238. {
  239. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  240. var textSource = new SingleBufferTextSource(text, defaultProperties);
  241. var formatter = new TextFormatterImpl();
  242. var textLine =
  243. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  244. new GenericTextParagraphProperties(defaultProperties));
  245. var isRightToLeft = IsRightToLeft(textLine);
  246. var rects = BuildRects(textLine);
  247. var glyphClusters = BuildGlyphClusters(textLine);
  248. for (var i = 0; i < rects.Count; i++)
  249. {
  250. var cluster = glyphClusters[i];
  251. var rect = rects[i];
  252. var characterHit = textLine.GetCharacterHitFromDistance(rect.Left);
  253. Assert.Equal(isRightToLeft ? cluster + 1 : cluster,
  254. characterHit.FirstCharacterIndex + characterHit.TrailingLength);
  255. }
  256. }
  257. }
  258. public static IEnumerable<object[]> CollapsingData
  259. {
  260. get
  261. {
  262. yield return CreateData("01234 01234 01234", 120, TextTrimming.PrefixCharacterEllipsis, "01234 01\u20264 01234");
  263. yield return CreateData("01234 01234", 58, TextTrimming.CharacterEllipsis, "01234 0\u2026");
  264. yield return CreateData("01234 01234", 58, TextTrimming.WordEllipsis, "01234\u2026");
  265. yield return CreateData("01234", 9, TextTrimming.CharacterEllipsis, "\u2026");
  266. yield return CreateData("01234", 2, TextTrimming.CharacterEllipsis, "");
  267. object[] CreateData(string text, double width, TextTrimming mode, string expected)
  268. {
  269. return new object[]
  270. {
  271. text, width, mode, expected
  272. };
  273. }
  274. }
  275. }
  276. [MemberData(nameof(CollapsingData))]
  277. [Theory]
  278. public void Should_Collapse_Line(string text, double width, TextTrimming trimming, string expected)
  279. {
  280. using (Start())
  281. {
  282. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  283. var textSource = new SingleBufferTextSource(text, defaultProperties);
  284. var formatter = new TextFormatterImpl();
  285. var textLine =
  286. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  287. new GenericTextParagraphProperties(defaultProperties));
  288. Assert.False(textLine.HasCollapsed);
  289. TextCollapsingProperties collapsingProperties = trimming.CreateCollapsingProperties(new TextCollapsingCreateInfo(width, defaultProperties));
  290. var collapsedLine = textLine.Collapse(collapsingProperties);
  291. Assert.True(collapsedLine.HasCollapsed);
  292. var trimmedText = collapsedLine.TextRuns.SelectMany(x => x.Text.ToString()).ToArray();
  293. Assert.Equal(expected.Length, trimmedText.Length);
  294. for (var i = 0; i < expected.Length; i++)
  295. {
  296. Assert.Equal(expected[i], trimmedText[i]);
  297. }
  298. }
  299. }
  300. [Fact]
  301. public void Should_Get_Next_CharacterHit_For_Drawable_Runs()
  302. {
  303. using (Start())
  304. {
  305. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  306. var textSource = new DrawableRunTextSource();
  307. var formatter = new TextFormatterImpl();
  308. var textLine =
  309. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  310. new GenericTextParagraphProperties(defaultProperties));
  311. Assert.Equal(4, textLine.TextRuns.Count);
  312. var currentHit = textLine.GetNextCaretCharacterHit(new CharacterHit(0));
  313. Assert.Equal(1, currentHit.FirstCharacterIndex);
  314. Assert.Equal(0, currentHit.TrailingLength);
  315. currentHit = textLine.GetNextCaretCharacterHit(currentHit);
  316. Assert.Equal(2, currentHit.FirstCharacterIndex);
  317. Assert.Equal(0, currentHit.TrailingLength);
  318. currentHit = textLine.GetNextCaretCharacterHit(currentHit);
  319. Assert.Equal(3, currentHit.FirstCharacterIndex);
  320. Assert.Equal(0, currentHit.TrailingLength);
  321. currentHit = textLine.GetNextCaretCharacterHit(currentHit);
  322. Assert.Equal(4, currentHit.FirstCharacterIndex);
  323. Assert.Equal(0, currentHit.TrailingLength);
  324. }
  325. }
  326. [Fact]
  327. public void Should_Get_Previous_CharacterHit_For_Drawable_Runs()
  328. {
  329. using (Start())
  330. {
  331. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  332. var textSource = new DrawableRunTextSource();
  333. var formatter = new TextFormatterImpl();
  334. var textLine =
  335. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  336. new GenericTextParagraphProperties(defaultProperties));
  337. Assert.Equal(4, textLine.TextRuns.Count);
  338. var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1));
  339. Assert.Equal(2, currentHit.FirstCharacterIndex);
  340. Assert.Equal(1, currentHit.TrailingLength);
  341. currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
  342. Assert.Equal(1, currentHit.FirstCharacterIndex);
  343. Assert.Equal(1, currentHit.TrailingLength);
  344. currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
  345. Assert.Equal(0, currentHit.FirstCharacterIndex);
  346. Assert.Equal(1, currentHit.TrailingLength);
  347. currentHit = textLine.GetPreviousCaretCharacterHit(currentHit);
  348. Assert.Equal(0, currentHit.FirstCharacterIndex);
  349. Assert.Equal(0, currentHit.TrailingLength);
  350. }
  351. }
  352. [Fact]
  353. public void Should_Get_CharacterHit_From_Distance_For_Drawable_Runs()
  354. {
  355. using (Start())
  356. {
  357. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  358. var textSource = new DrawableRunTextSource();
  359. var formatter = new TextFormatterImpl();
  360. var textLine =
  361. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  362. new GenericTextParagraphProperties(defaultProperties));
  363. var characterHit = textLine.GetCharacterHitFromDistance(50);
  364. Assert.Equal(5, characterHit.FirstCharacterIndex);
  365. Assert.Equal(1, characterHit.TrailingLength);
  366. characterHit = textLine.GetCharacterHitFromDistance(32);
  367. Assert.Equal(3, characterHit.FirstCharacterIndex);
  368. Assert.Equal(0, characterHit.TrailingLength);
  369. }
  370. }
  371. [Fact]
  372. public void Should_Get_Distance_From_CharacterHit_Drawable_Runs()
  373. {
  374. using (Start())
  375. {
  376. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  377. var textSource = new DrawableRunTextSource();
  378. var formatter = new TextFormatterImpl();
  379. var textLine =
  380. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  381. new GenericTextParagraphProperties(defaultProperties));
  382. var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(1));
  383. Assert.Equal(14, distance);
  384. distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(2));
  385. Assert.True(distance > 14);
  386. }
  387. }
  388. [Fact]
  389. public void Should_Get_Distance_From_CharacterHit_Mixed_TextBuffer()
  390. {
  391. using (Start())
  392. {
  393. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  394. var textSource = new MixedTextBufferTextSource();
  395. var formatter = new TextFormatterImpl();
  396. var textLine =
  397. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  398. new GenericTextParagraphProperties(defaultProperties));
  399. var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(10));
  400. Assert.Equal(72.01171875, distance);
  401. distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(20));
  402. Assert.Equal(144.0234375, distance);
  403. distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(30));
  404. Assert.Equal(216.03515625, distance);
  405. distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(40));
  406. Assert.Equal(textLine.WidthIncludingTrailingWhitespace, distance);
  407. }
  408. }
  409. [Fact]
  410. public void Should_Get_TextBounds_From_Mixed_TextBuffer()
  411. {
  412. using (Start())
  413. {
  414. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  415. var textSource = new MixedTextBufferTextSource();
  416. var formatter = new TextFormatterImpl();
  417. var textLine =
  418. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  419. new GenericTextParagraphProperties(defaultProperties));
  420. var textBounds = textLine.GetTextBounds(0, 10);
  421. Assert.Equal(1, textBounds.Count);
  422. Assert.Equal(72.01171875, textBounds[0].Rectangle.Width);
  423. textBounds = textLine.GetTextBounds(0, 20);
  424. Assert.Equal(2, textBounds.Count);
  425. Assert.Equal(144.0234375, textBounds.Sum(x => x.Rectangle.Width));
  426. textBounds = textLine.GetTextBounds(0, 30);
  427. Assert.Equal(3, textBounds.Count);
  428. Assert.Equal(216.03515625, textBounds.Sum(x => x.Rectangle.Width));
  429. textBounds = textLine.GetTextBounds(0, 40);
  430. Assert.Equal(4, textBounds.Count);
  431. Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
  432. }
  433. }
  434. [Fact]
  435. public void Should_GetTextRange()
  436. {
  437. var text = "שדגככעיחדגכAישדגשדגחייטYDASYWIWחיחלדשSAטויליHUHIUHUIDWKLאא'ק'קחליק/'וקןגגגלךשף'/קפוכדגכשדגשיח'/קטאגשד";
  438. using (Start())
  439. {
  440. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  441. var textSource = new SingleBufferTextSource(text, defaultProperties);
  442. var formatter = new TextFormatterImpl();
  443. var textLine =
  444. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  445. new GenericTextParagraphProperties(defaultProperties));
  446. var textRuns = textLine.TextRuns.Cast<ShapedTextRun>().ToList();
  447. var lineWidth = textLine.WidthIncludingTrailingWhitespace;
  448. var textBounds = textLine.GetTextBounds(0, text.Length);
  449. TextBounds lastBounds = null;
  450. var runBounds = textBounds.SelectMany(x => x.TextRunBounds).ToList();
  451. Assert.Equal(textRuns.Count, runBounds.Count);
  452. for (var i = 0; i < textRuns.Count; i++)
  453. {
  454. var run = textRuns[i];
  455. var bounds = runBounds[i];
  456. Assert.Equal(TextTestHelper.GetStartCharIndex(run.Text), bounds.TextSourceCharacterIndex);
  457. Assert.Equal(run, bounds.TextRun);
  458. Assert.Equal(run.Size.Width, bounds.Rectangle.Width);
  459. }
  460. for (var i = 0; i < textBounds.Count; i++)
  461. {
  462. var currentBounds = textBounds[i];
  463. if (lastBounds != null)
  464. {
  465. Assert.Equal(lastBounds.Rectangle.Right, currentBounds.Rectangle.Left);
  466. }
  467. var sumOfRunWidth = currentBounds.TextRunBounds.Sum(x => x.Rectangle.Width);
  468. Assert.Equal(sumOfRunWidth, currentBounds.Rectangle.Width);
  469. lastBounds = currentBounds;
  470. }
  471. var sumOfBoundsWidth = textBounds.Sum(x => x.Rectangle.Width);
  472. Assert.Equal(lineWidth, sumOfBoundsWidth);
  473. }
  474. }
  475. private class MixedTextBufferTextSource : ITextSource
  476. {
  477. public TextRun? GetTextRun(int textSourceIndex)
  478. {
  479. switch (textSourceIndex)
  480. {
  481. case 0:
  482. return new TextCharacters("aaaaaaaaaa", new GenericTextRunProperties(Typeface.Default));
  483. case 10:
  484. return new TextCharacters("bbbbbbbbbb", new GenericTextRunProperties(Typeface.Default));
  485. case 20:
  486. return new TextCharacters("cccccccccc", new GenericTextRunProperties(Typeface.Default));
  487. case 30:
  488. return new TextCharacters("dddddddddd", new GenericTextRunProperties(Typeface.Default));
  489. default:
  490. return null;
  491. }
  492. }
  493. }
  494. private class DrawableRunTextSource : ITextSource
  495. {
  496. private const string Text = "_A_A";
  497. public TextRun GetTextRun(int textSourceIndex)
  498. {
  499. switch (textSourceIndex)
  500. {
  501. case 0:
  502. return new CustomDrawableRun();
  503. case 1:
  504. return new TextCharacters(Text, new GenericTextRunProperties(Typeface.Default));
  505. case 5:
  506. return new CustomDrawableRun();
  507. case 6:
  508. return new TextCharacters(Text, new GenericTextRunProperties(Typeface.Default));
  509. default:
  510. return null;
  511. }
  512. }
  513. }
  514. private class CustomDrawableRun : DrawableTextRun
  515. {
  516. public override Size Size => new(14, 14);
  517. public override double Baseline => 14;
  518. public override void Draw(DrawingContext drawingContext, Point origin)
  519. {
  520. }
  521. }
  522. private static bool IsRightToLeft(TextLine textLine)
  523. {
  524. return textLine.TextRuns.Cast<ShapedTextRun>().Any(x => !x.ShapedBuffer.IsLeftToRight);
  525. }
  526. private static List<int> BuildGlyphClusters(TextLine textLine)
  527. {
  528. var glyphClusters = new List<int>();
  529. var shapedTextRuns = textLine.TextRuns.Cast<ShapedTextRun>().ToList();
  530. var lastCluster = -1;
  531. foreach (var textRun in shapedTextRuns)
  532. {
  533. var shapedBuffer = textRun.ShapedBuffer;
  534. var currentClusters = shapedBuffer.GlyphInfos.Select(glyph => glyph.GlyphCluster).ToList();
  535. foreach (var currentCluster in currentClusters)
  536. {
  537. if (lastCluster == currentCluster)
  538. {
  539. continue;
  540. }
  541. glyphClusters.Add(currentCluster);
  542. lastCluster = currentCluster;
  543. }
  544. }
  545. return glyphClusters;
  546. }
  547. private static List<Rect> BuildRects(TextLine textLine)
  548. {
  549. var rects = new List<Rect>();
  550. var height = textLine.Height;
  551. var currentX = 0d;
  552. var lastCluster = -1;
  553. var shapedTextRuns = textLine.TextRuns.Cast<ShapedTextRun>().ToList();
  554. foreach (var textRun in shapedTextRuns)
  555. {
  556. var shapedBuffer = textRun.ShapedBuffer;
  557. for (var index = 0; index < shapedBuffer.GlyphInfos.Length; index++)
  558. {
  559. var currentCluster = shapedBuffer.GlyphInfos[index].GlyphCluster;
  560. var advance = shapedBuffer.GlyphInfos[index].GlyphAdvance;
  561. if (lastCluster != currentCluster)
  562. {
  563. rects.Add(new Rect(currentX, 0, advance, height));
  564. }
  565. else
  566. {
  567. var rect = rects[index - 1];
  568. rects.Remove(rect);
  569. rect = rect.WithWidth(rect.Width + advance);
  570. rects.Add(rect);
  571. }
  572. currentX += advance;
  573. lastCluster = currentCluster;
  574. }
  575. }
  576. return rects;
  577. }
  578. [Fact]
  579. public void Should_Get_TextBounds_Mixed()
  580. {
  581. using (Start())
  582. {
  583. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  584. var text = "0123";
  585. var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture);
  586. var firstRun = new ShapedTextRun(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties);
  587. var textRuns = new List<TextRun>
  588. {
  589. new CustomDrawableRun(),
  590. firstRun,
  591. new CustomDrawableRun(),
  592. new ShapedTextRun(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties),
  593. new CustomDrawableRun(),
  594. new ShapedTextRun(TextShaper.Current.ShapeText(text, shaperOption), defaultProperties)
  595. };
  596. var textSource = new FixedRunsTextSource(textRuns);
  597. var formatter = new TextFormatterImpl();
  598. var textLine =
  599. formatter.FormatLine(textSource, 0, double.PositiveInfinity,
  600. new GenericTextParagraphProperties(defaultProperties));
  601. var textBounds = textLine.GetTextBounds(0, textLine.Length);
  602. Assert.Equal(6, textBounds.Count);
  603. Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
  604. textBounds = textLine.GetTextBounds(0, 1);
  605. Assert.Equal(1, textBounds.Count);
  606. Assert.Equal(14, textBounds[0].Rectangle.Width);
  607. textBounds = textLine.GetTextBounds(0, firstRun.Length + 1);
  608. Assert.Equal(2, textBounds.Count);
  609. Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
  610. textBounds = textLine.GetTextBounds(1, firstRun.Length);
  611. Assert.Equal(1, textBounds.Count);
  612. Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width);
  613. textBounds = textLine.GetTextBounds(0, 1 + firstRun.Length);
  614. Assert.Equal(2, textBounds.Count);
  615. Assert.Equal(firstRun.Size.Width + 14, textBounds.Sum(x => x.Rectangle.Width));
  616. }
  617. }
  618. [Fact]
  619. public void Should_Get_TextBounds_BiDi_LeftToRight()
  620. {
  621. using (Start())
  622. {
  623. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  624. var text = "אאא AAA";
  625. var textSource = new SingleBufferTextSource(text, defaultProperties);
  626. var formatter = new TextFormatterImpl();
  627. var textLine =
  628. formatter.FormatLine(textSource, 0, 200,
  629. new GenericTextParagraphProperties(FlowDirection.LeftToRight, TextAlignment.Left,
  630. true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0));
  631. var textBounds = textLine.GetTextBounds(0, 3);
  632. var firstRun = textLine.TextRuns[0] as ShapedTextRun;
  633. Assert.Equal(1, textBounds.Count);
  634. Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
  635. textBounds = textLine.GetTextBounds(3, 4);
  636. var secondRun = textLine.TextRuns[1] as ShapedTextRun;
  637. Assert.Equal(1, textBounds.Count);
  638. Assert.Equal(secondRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
  639. textBounds = textLine.GetTextBounds(0, 4);
  640. Assert.Equal(2, textBounds.Count);
  641. Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width);
  642. Assert.Equal(7.201171875, textBounds[1].Rectangle.Width);
  643. Assert.Equal(firstRun.Size.Width, textBounds[1].Rectangle.Left);
  644. textBounds = textLine.GetTextBounds(0, text.Length);
  645. Assert.Equal(2, textBounds.Count);
  646. Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
  647. }
  648. }
  649. [Fact]
  650. public void Should_Get_TextBounds_BiDi_RightToLeft()
  651. {
  652. using (Start())
  653. {
  654. var defaultProperties = new GenericTextRunProperties(Typeface.Default);
  655. var text = "אאא AAA";
  656. var textSource = new SingleBufferTextSource(text, defaultProperties);
  657. var formatter = new TextFormatterImpl();
  658. var textLine =
  659. formatter.FormatLine(textSource, 0, 200,
  660. new GenericTextParagraphProperties(FlowDirection.RightToLeft, TextAlignment.Left,
  661. true, true, defaultProperties, TextWrapping.NoWrap, 0, 0, 0));
  662. var textBounds = textLine.GetTextBounds(0, 4);
  663. var secondRun = textLine.TextRuns[1] as ShapedTextRun;
  664. Assert.Equal(1, textBounds.Count);
  665. Assert.Equal(secondRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
  666. textBounds = textLine.GetTextBounds(4, 3);
  667. var firstRun = textLine.TextRuns[0] as ShapedTextRun;
  668. Assert.Equal(1, textBounds.Count);
  669. Assert.Equal(3, textBounds[0].TextRunBounds.Sum(x => x.Length));
  670. Assert.Equal(firstRun.Size.Width, textBounds.Sum(x => x.Rectangle.Width));
  671. textBounds = textLine.GetTextBounds(0, 5);
  672. Assert.Equal(2, textBounds.Count);
  673. Assert.Equal(5, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length)));
  674. Assert.Equal(secondRun.Size.Width, textBounds[1].Rectangle.Width);
  675. Assert.Equal(7.201171875, textBounds[0].Rectangle.Width);
  676. Assert.Equal(textLine.Start + 7.201171875, textBounds[0].Rectangle.Right);
  677. Assert.Equal(textLine.Start + firstRun.Size.Width, textBounds[1].Rectangle.Left);
  678. textBounds = textLine.GetTextBounds(0, text.Length);
  679. Assert.Equal(2, textBounds.Count);
  680. Assert.Equal(7, textBounds.Sum(x => x.TextRunBounds.Sum(x => x.Length)));
  681. Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width));
  682. }
  683. }
  684. private class FixedRunsTextSource : ITextSource
  685. {
  686. private readonly IReadOnlyList<TextRun> _textRuns;
  687. public FixedRunsTextSource(IReadOnlyList<TextRun> textRuns)
  688. {
  689. _textRuns = textRuns;
  690. }
  691. public TextRun GetTextRun(int textSourceIndex)
  692. {
  693. var currentPosition = 0;
  694. foreach (var textRun in _textRuns)
  695. {
  696. if (currentPosition == textSourceIndex)
  697. {
  698. return textRun;
  699. }
  700. currentPosition += textRun.Length;
  701. }
  702. return null;
  703. }
  704. }
  705. private static IDisposable Start()
  706. {
  707. var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
  708. .With(renderInterface: new PlatformRenderInterface(null),
  709. textShaperImpl: new TextShaperImpl(),
  710. fontManagerImpl: new CustomFontManagerImpl()));
  711. return disposable;
  712. }
  713. }
  714. }