TextBlock.cs 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. using System;
  2. using System.Collections.Concurrent;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using QuestPDF.Drawing;
  6. using QuestPDF.Drawing.Exceptions;
  7. using QuestPDF.Elements.Text.Items;
  8. using QuestPDF.Helpers;
  9. using QuestPDF.Infrastructure;
  10. using QuestPDF.Skia;
  11. using QuestPDF.Skia.Text;
  12. namespace QuestPDF.Elements.Text
  13. {
  14. internal sealed class TextBlock : Element, IStateful, IContentDirectionAware, IDisposable
  15. {
  16. // content
  17. public List<ITextBlockItem> Items { get; set; } = new();
  18. // configuration
  19. public TextHorizontalAlignment? Alignment { get; set; }
  20. public ContentDirection ContentDirection { get; set; }
  21. public int? LineClamp { get; set; }
  22. public string LineClampEllipsis { get; set; }
  23. public float ParagraphSpacing { get; set; }
  24. public float ParagraphFirstLineIndentation { get; set; }
  25. public TextStyle DefaultTextStyle { get; set; } = TextStyle.Default;
  26. // cache
  27. private bool RebuildParagraphForEveryPage { get; set; }
  28. private bool AreParagraphMetricsValid { get; set; }
  29. private bool AreParagraphItemsTransformedWithSpacingAndIndentation { get; set; }
  30. private SkSize[] LineMetrics { get; set; }
  31. private float WidthForLineMetricsCalculation { get; set; }
  32. private float MaximumWidth { get; set; }
  33. private SkRect[] PlaceholderPositions { get; set; }
  34. private bool? ContainsOnlyWhiteSpace { get; set; }
  35. // native objects
  36. private SkParagraph Paragraph { get; set; }
  37. internal bool ClearInternalCacheAfterFullRender { get; set; } = true;
  38. public string Text => string.Join(" ", Items.OfType<TextBlockSpan>().Select(x => x.Text));
  39. ~TextBlock()
  40. {
  41. if (Paragraph == null)
  42. return;
  43. this.WarnThatFinalizerIsReached();
  44. Dispose();
  45. }
  46. public void Dispose()
  47. {
  48. Paragraph?.Dispose();
  49. foreach (var textBlockElement in Items.OfType<TextBlockElement>())
  50. textBlockElement.Element.ReleaseDisposableChildren();
  51. GC.SuppressFinalize(this);
  52. }
  53. internal override SpacePlan Measure(Size availableSpace)
  54. {
  55. if (Items.Count == 0)
  56. return SpacePlan.Empty();
  57. if (IsRendered)
  58. return SpacePlan.Empty();
  59. if (availableSpace.IsNegative())
  60. return SpacePlan.Wrap("The available space is negative.");
  61. // if the text block does not contain any items, or all items are null, return SpacePlan.Empty
  62. // but if the text block contains only whitespace, return SpacePlan.FullRender with zero width and font-based height
  63. ContainsOnlyWhiteSpace ??= CheckIfContainsOnlyWhiteSpace();
  64. if (ContainsOnlyWhiteSpace == true)
  65. {
  66. var requiredHeight = MeasureHeightOfParagraphContainingOnlyWhiteSpace();
  67. return requiredHeight < availableSpace.Height + Size.Epsilon
  68. ? SpacePlan.FullRender(0, requiredHeight)
  69. : SpacePlan.Wrap("The available vertical space is not sufficient to render even a single line of text.");
  70. }
  71. if (availableSpace.Width < Size.Epsilon || availableSpace.Height < Size.Epsilon)
  72. return SpacePlan.Wrap("The available space is not sufficient to render even a single line of text.");
  73. Initialize();
  74. CalculateParagraphMetrics(availableSpace);
  75. if (availableSpace.Width < MaximumWidth - Size.Epsilon)
  76. return SpacePlan.Wrap($"The available space is not sufficient to render even a single character.");
  77. if (MaximumWidth == 0)
  78. return SpacePlan.FullRender(Size.Zero);
  79. var totalHeight = 0f;
  80. var totalLines = 0;
  81. for (var lineIndex = CurrentLineIndex; lineIndex < LineMetrics.Length; lineIndex++)
  82. {
  83. var lineMetric = LineMetrics[lineIndex];
  84. var newTotalHeight = totalHeight + lineMetric.Height;
  85. if (newTotalHeight > availableSpace.Height + Size.Epsilon)
  86. break;
  87. totalHeight = newTotalHeight;
  88. totalLines++;
  89. }
  90. if (totalLines == 0)
  91. return SpacePlan.Wrap("The available space is not sufficient to render even a single line of text.");
  92. var requiredArea = new Size(
  93. Math.Min(MaximumWidth, availableSpace.Width),
  94. Math.Min(totalHeight, availableSpace.Height));
  95. if (CurrentLineIndex + totalLines < LineMetrics.Length)
  96. return SpacePlan.PartialRender(requiredArea);
  97. return SpacePlan.FullRender(requiredArea);
  98. }
  99. internal override void Draw(Size availableSpace)
  100. {
  101. if (Items.Count == 0)
  102. return;
  103. if (IsRendered)
  104. return;
  105. if (ContainsOnlyWhiteSpace == true)
  106. return;
  107. CalculateParagraphMetrics(availableSpace);
  108. if (MaximumWidth == 0)
  109. return;
  110. var (linesToDraw, takenHeight) = DetermineLinesToDraw();
  111. DrawParagraph();
  112. CurrentLineIndex += linesToDraw;
  113. CurrentTopOffset += takenHeight;
  114. if (CurrentLineIndex == LineMetrics.Length)
  115. IsRendered = true;
  116. if (IsRendered && ClearInternalCacheAfterFullRender)
  117. {
  118. Paragraph?.Dispose();
  119. Paragraph = null;
  120. }
  121. return;
  122. (int linesToDraw, float takenHeight) DetermineLinesToDraw()
  123. {
  124. var linesToDraw = 0;
  125. var takenHeight = 0f;
  126. for (var lineIndex = CurrentLineIndex; lineIndex < LineMetrics.Length; lineIndex++)
  127. {
  128. var lineMetric = LineMetrics[lineIndex];
  129. var newTotalHeight = takenHeight + lineMetric.Height;
  130. if (newTotalHeight > availableSpace.Height + Size.Epsilon)
  131. break;
  132. takenHeight = newTotalHeight;
  133. linesToDraw++;
  134. }
  135. return (linesToDraw, takenHeight);
  136. }
  137. void DrawParagraph()
  138. {
  139. var takesMultiplePages = linesToDraw != LineMetrics.Length;
  140. if (takesMultiplePages)
  141. {
  142. Canvas.Save();
  143. Canvas.Translate(new Position(0, -CurrentTopOffset));
  144. }
  145. Canvas.DrawParagraph(Paragraph, CurrentLineIndex, CurrentLineIndex + linesToDraw - 1);
  146. if (takesMultiplePages)
  147. Canvas.ClipRectangle(new SkRect(0, CurrentTopOffset, availableSpace.Width, takenHeight + CurrentTopOffset));
  148. DrawInjectedElements();
  149. DrawHyperlinks();
  150. DrawSectionLinks();
  151. if (takesMultiplePages)
  152. Canvas.Restore();
  153. }
  154. void DrawInjectedElements()
  155. {
  156. foreach (var textBlockElement in Items.OfType<TextBlockElement>())
  157. {
  158. var placeholder = PlaceholderPositions[textBlockElement.ParagraphBlockIndex];
  159. textBlockElement.ConfigureElement(PageContext, Canvas);
  160. var offset = new Position(placeholder.Left, placeholder.Top);
  161. if (!IsPositionVisible(offset))
  162. continue;
  163. Canvas.Translate(offset);
  164. textBlockElement.Element.Draw(new Size(placeholder.Width, placeholder.Height));
  165. Canvas.Translate(offset.Reverse());
  166. }
  167. }
  168. void DrawHyperlinks()
  169. {
  170. foreach (var hyperlink in Items.OfType<TextBlockHyperlink>())
  171. {
  172. var positions = Paragraph.GetTextRangePositions(hyperlink.ParagraphBeginIndex, hyperlink.ParagraphBeginIndex + hyperlink.Text.Length);
  173. foreach (var position in positions)
  174. {
  175. var offset = new Position(position.Left, position.Top);
  176. if (!IsPositionVisible(offset))
  177. continue;
  178. Canvas.Translate(offset);
  179. Canvas.DrawHyperlink(new Size(position.Width, position.Height), hyperlink.Url, hyperlink.Text);
  180. Canvas.Translate(offset.Reverse());
  181. }
  182. }
  183. }
  184. void DrawSectionLinks()
  185. {
  186. foreach (var sectionLink in Items.OfType<TextBlockSectionLink>())
  187. {
  188. var positions = Paragraph.GetTextRangePositions(sectionLink.ParagraphBeginIndex, sectionLink.ParagraphBeginIndex + sectionLink.Text.Length);
  189. var targetName = PageContext.GetDocumentLocationName(sectionLink.SectionName);
  190. foreach (var position in positions)
  191. {
  192. var offset = new Position(position.Left, position.Top);
  193. if (!IsPositionVisible(offset))
  194. continue;
  195. Canvas.Translate(offset);
  196. Canvas.DrawSectionLink(new Size(position.Width, position.Height), targetName, sectionLink.Text);
  197. Canvas.Translate(offset.Reverse());
  198. }
  199. }
  200. }
  201. bool IsPositionVisible(Position position)
  202. {
  203. return CurrentTopOffset <= position.Y || position.Y <= CurrentTopOffset + takenHeight;
  204. }
  205. }
  206. private void Initialize()
  207. {
  208. if (Paragraph != null && !RebuildParagraphForEveryPage)
  209. return;
  210. if (!AreParagraphItemsTransformedWithSpacingAndIndentation)
  211. {
  212. Items = ApplyParagraphSpacingToTextBlockItems().ToList();
  213. AreParagraphItemsTransformedWithSpacingAndIndentation = true;
  214. }
  215. RebuildParagraphForEveryPage = Items.Any(x => x is TextBlockPageNumber);
  216. BuildParagraph();
  217. AreParagraphMetricsValid = false;
  218. }
  219. private void BuildParagraph()
  220. {
  221. Alignment ??= TextHorizontalAlignment.Start;
  222. var paragraphStyle = new ParagraphStyle
  223. {
  224. Alignment = MapAlignment(Alignment.Value),
  225. Direction = MapDirection(ContentDirection),
  226. MaxLinesVisible = LineClamp ?? 1_000_000,
  227. LineClampEllipsis = LineClampEllipsis
  228. };
  229. if (Paragraph != null)
  230. {
  231. Paragraph.Dispose();
  232. Paragraph = null;
  233. }
  234. var builder = SkParagraphBuilderPoolManager.Get(paragraphStyle);
  235. try
  236. {
  237. Paragraph = CreateParagraph(builder);
  238. }
  239. finally
  240. {
  241. SkParagraphBuilderPoolManager.Return(builder);
  242. }
  243. static ParagraphStyleConfiguration.TextAlign MapAlignment(TextHorizontalAlignment alignment)
  244. {
  245. return alignment switch
  246. {
  247. TextHorizontalAlignment.Left => ParagraphStyleConfiguration.TextAlign.Left,
  248. TextHorizontalAlignment.Center => ParagraphStyleConfiguration.TextAlign.Center,
  249. TextHorizontalAlignment.Right => ParagraphStyleConfiguration.TextAlign.Right,
  250. TextHorizontalAlignment.Justify => ParagraphStyleConfiguration.TextAlign.Justify,
  251. TextHorizontalAlignment.Start => ParagraphStyleConfiguration.TextAlign.Start,
  252. TextHorizontalAlignment.End => ParagraphStyleConfiguration.TextAlign.End,
  253. _ => throw new Exception()
  254. };
  255. }
  256. static ParagraphStyleConfiguration.TextDirection MapDirection(ContentDirection direction)
  257. {
  258. return direction switch
  259. {
  260. ContentDirection.LeftToRight => ParagraphStyleConfiguration.TextDirection.Ltr,
  261. ContentDirection.RightToLeft => ParagraphStyleConfiguration.TextDirection.Rtl,
  262. _ => throw new Exception()
  263. };
  264. }
  265. static SkPlaceholderStyle.PlaceholderAlignment MapInjectedTextAlignment(TextInjectedElementAlignment alignment)
  266. {
  267. return alignment switch
  268. {
  269. TextInjectedElementAlignment.AboveBaseline => SkPlaceholderStyle.PlaceholderAlignment.AboveBaseline,
  270. TextInjectedElementAlignment.BelowBaseline => SkPlaceholderStyle.PlaceholderAlignment.BelowBaseline,
  271. TextInjectedElementAlignment.Top => SkPlaceholderStyle.PlaceholderAlignment.Top,
  272. TextInjectedElementAlignment.Bottom => SkPlaceholderStyle.PlaceholderAlignment.Bottom,
  273. TextInjectedElementAlignment.Middle => SkPlaceholderStyle.PlaceholderAlignment.Middle,
  274. _ => throw new Exception()
  275. };
  276. }
  277. SkParagraph CreateParagraph(SkParagraphBuilder builder)
  278. {
  279. var currentTextIndex = 0;
  280. var currentBlockIndex = 0;
  281. if (!Items.Any(x => x is TextBlockSpan))
  282. builder.AddText("\u200B", DefaultTextStyle.GetSkTextStyle());
  283. foreach (var textBlockItem in Items)
  284. {
  285. if (textBlockItem is TextBlockSpan textBlockSpan)
  286. {
  287. if (textBlockItem is TextBlockSectionLink textBlockSectionLink)
  288. textBlockSectionLink.ParagraphBeginIndex = currentTextIndex;
  289. else if (textBlockItem is TextBlockHyperlink textBlockHyperlink)
  290. textBlockHyperlink.ParagraphBeginIndex = currentTextIndex;
  291. else if (textBlockItem is TextBlockPageNumber textBlockPageNumber)
  292. textBlockPageNumber.UpdatePageNumberText(PageContext);
  293. var textStyle = textBlockSpan.Style.GetSkTextStyle();
  294. var text = textBlockSpan.Text?.Replace("\r", "") ?? "";
  295. builder.AddText(text, textStyle);
  296. currentTextIndex += text.Length;
  297. }
  298. else if (textBlockItem is TextBlockElement textBlockElement)
  299. {
  300. textBlockElement.ConfigureElement(PageContext, Canvas);
  301. textBlockElement.UpdateElementSize();
  302. textBlockElement.ParagraphBlockIndex = currentBlockIndex;
  303. builder.AddPlaceholder(new SkPlaceholderStyle
  304. {
  305. Width = textBlockElement.ElementSize.Width,
  306. Height = textBlockElement.ElementSize.Height,
  307. Alignment = MapInjectedTextAlignment(textBlockElement.Alignment),
  308. Baseline = SkPlaceholderStyle.PlaceholderBaseline.Alphabetic,
  309. BaselineOffset = 0
  310. });
  311. currentTextIndex++;
  312. currentBlockIndex++;
  313. }
  314. else if (textBlockItem is TextBlockParagraphSpacing spacing)
  315. {
  316. builder.AddPlaceholder(new SkPlaceholderStyle
  317. {
  318. Width = spacing.Width,
  319. Height = spacing.Height,
  320. Alignment = SkPlaceholderStyle.PlaceholderAlignment.Middle,
  321. Baseline = SkPlaceholderStyle.PlaceholderBaseline.Alphabetic,
  322. BaselineOffset = 0
  323. });
  324. currentTextIndex++;
  325. currentBlockIndex++;
  326. }
  327. }
  328. return builder.CreateParagraph();
  329. }
  330. }
  331. private IEnumerable<ITextBlockItem> ApplyParagraphSpacingToTextBlockItems()
  332. {
  333. if (ParagraphSpacing < Size.Epsilon && ParagraphFirstLineIndentation < Size.Epsilon)
  334. return Items;
  335. var result = new List<ITextBlockItem>();
  336. AddParagraphFirstLineIndentation();
  337. foreach (var textBlockItem in Items)
  338. {
  339. if (textBlockItem is not TextBlockSpan textBlockSpan)
  340. {
  341. result.Add(textBlockItem);
  342. continue;
  343. }
  344. if (textBlockItem is TextBlockPageNumber)
  345. {
  346. result.Add(textBlockItem);
  347. continue;
  348. }
  349. if (textBlockSpan.Text == "\n")
  350. {
  351. AddParagraphSpacing();
  352. AddParagraphFirstLineIndentation();
  353. continue;
  354. }
  355. var textFragments = textBlockSpan.Text.Split('\n');
  356. foreach (var textFragment in textFragments)
  357. {
  358. AddClonedTextBlockSpanWithTextFragment(textBlockSpan, textFragment);
  359. if (textFragment == textFragments.Last())
  360. continue;
  361. AddParagraphSpacing();
  362. AddParagraphFirstLineIndentation();
  363. }
  364. }
  365. return result;
  366. void AddClonedTextBlockSpanWithTextFragment(TextBlockSpan originalSpan, string textFragment)
  367. {
  368. TextBlockSpan newItem;
  369. if (originalSpan is TextBlockSectionLink textBlockSectionLink)
  370. newItem = new TextBlockSectionLink { SectionName = textBlockSectionLink.SectionName };
  371. else if (originalSpan is TextBlockHyperlink textBlockHyperlink)
  372. newItem = new TextBlockHyperlink { Url = textBlockHyperlink.Url };
  373. else if (originalSpan is TextBlockPageNumber textBlockPageNumber)
  374. newItem = textBlockPageNumber;
  375. else
  376. newItem = new TextBlockSpan();
  377. newItem.Text = textFragment;
  378. newItem.Style = originalSpan.Style;
  379. result.Add(newItem);
  380. }
  381. void AddParagraphSpacing()
  382. {
  383. if (ParagraphSpacing <= Size.Epsilon)
  384. return;
  385. // space ensure proper line spacing
  386. result.Add(new TextBlockSpan() { Text = "\n ", Style = TextStyle.ParagraphSpacing });
  387. result.Add(new TextBlockParagraphSpacing(0, ParagraphSpacing));
  388. result.Add(new TextBlockSpan() { Text = " \n", Style = TextStyle.ParagraphSpacing });
  389. }
  390. void AddParagraphFirstLineIndentation()
  391. {
  392. if (ParagraphFirstLineIndentation <= Size.Epsilon)
  393. return;
  394. result.Add(new TextBlockSpan() { Text = "\n", Style = TextStyle.ParagraphSpacing });
  395. result.Add(new TextBlockParagraphSpacing(ParagraphFirstLineIndentation, 0));
  396. }
  397. }
  398. /// <summary>
  399. /// Adjusts the concurrency level for the SkParagraph.PlanLayout method to optimize performance.
  400. ///
  401. /// While the Skia implementation is thread-safe, it appears to contain internal locks that hinder scalability.
  402. /// Consequently, using multithreading for document rendering can reduce performance. This includes increased memory usage
  403. /// and slower generation times—potentially even worse than rendering documents sequentially.
  404. ///
  405. /// TODO: investigate further on how to improve scalability and remove this mutex
  406. /// </summary>
  407. private static readonly object PlanLayoutLock = new();
  408. private void CalculateParagraphMetrics(Size availableSpace)
  409. {
  410. if (Math.Abs(WidthForLineMetricsCalculation - availableSpace.Width) > Size.Epsilon)
  411. AreParagraphMetricsValid = false;
  412. if (AreParagraphMetricsValid)
  413. return;
  414. WidthForLineMetricsCalculation = availableSpace.Width;
  415. lock (PlanLayoutLock)
  416. Paragraph.PlanLayout(availableSpace.Width);
  417. CheckUnresolvedGlyphs();
  418. LineMetrics = Paragraph.GetLineMetrics();
  419. PlaceholderPositions = Paragraph.GetPlaceholderPositions();
  420. MaximumWidth = LineMetrics.Any() ? LineMetrics.Max(x => x.Width) : 0;
  421. AreParagraphMetricsValid = true;
  422. }
  423. private void CheckUnresolvedGlyphs()
  424. {
  425. if (!Settings.CheckIfAllTextGlyphsAreAvailable)
  426. return;
  427. var unsupportedGlyphs = Paragraph.GetUnresolvedCodepoints();
  428. if (!unsupportedGlyphs.Any())
  429. return;
  430. var formattedGlyphs = unsupportedGlyphs
  431. .Select(codepoint =>
  432. {
  433. var character = char.ConvertFromUtf32(codepoint);
  434. return $"U-{codepoint:X4} '{character}'";
  435. });
  436. var glyphs = string.Join("\n", formattedGlyphs);
  437. throw new DocumentDrawingException(
  438. $"Could not find an appropriate font fallback for the following glyphs: \n" +
  439. $"${glyphs} \n\n" +
  440. $"Possible solutions: \n" +
  441. $"1) Install fonts that contain missing glyphs in your runtime environment. \n" +
  442. $"2) Configure the fallback TextStyle using the 'TextStyle.FontFamilyFallback' method. \n" +
  443. $"3) Register additional application specific fonts using the 'FontManager.RegisterFont' method. \n\n" +
  444. $"You can disable this check by setting the 'Settings.CheckIfAllTextGlyphsAreAvailable' option to 'false'. \n" +
  445. $"However, this may result with text glyphs being incorrectly rendered without any warning.");
  446. }
  447. #region Handling Of Text Blocks With Only With Space
  448. private static ConcurrentDictionary<int, float> ParagraphContainingOnlyWhiteSpaceHeightCache { get; } = new(); // key: TextStyle.Id
  449. private bool CheckIfContainsOnlyWhiteSpace()
  450. {
  451. foreach (var textBlockItem in Items)
  452. {
  453. // TextBlockPageNumber needs to be checked first, as it derives from TextBlockSpan,
  454. // and before the generation starts, its Text property is empty
  455. if (textBlockItem is TextBlockPageNumber)
  456. return false;
  457. if (textBlockItem is TextBlockSpan textBlockSpan && !string.IsNullOrWhiteSpace(textBlockSpan.Text))
  458. return false;
  459. if (textBlockItem is TextBlockElement)
  460. return false;
  461. }
  462. return true;
  463. }
  464. private float MeasureHeightOfParagraphContainingOnlyWhiteSpace()
  465. {
  466. return Items
  467. .OfType<TextBlockSpan>()
  468. .Select(x => ParagraphContainingOnlyWhiteSpaceHeightCache.GetOrAdd(x.Style.Id, Measure))
  469. .DefaultIfEmpty(0)
  470. .Max();
  471. static float Measure(int textStyleId)
  472. {
  473. var paragraphStyle = new ParagraphStyle
  474. {
  475. Alignment = ParagraphStyleConfiguration.TextAlign.Start,
  476. Direction = ParagraphStyleConfiguration.TextDirection.Ltr,
  477. MaxLinesVisible = 1_000_000,
  478. LineClampEllipsis = string.Empty
  479. };
  480. var builder = SkParagraphBuilderPoolManager.Get(paragraphStyle);
  481. try
  482. {
  483. var textStyle = TextStyleManager.GetTextStyle(textStyleId).GetSkTextStyle();
  484. builder.AddText("\u00A0", textStyle); // non-breaking space
  485. using var paragraph = builder.CreateParagraph();
  486. paragraph.PlanLayout(1000);
  487. return paragraph.GetLineMetrics().First().Height;
  488. }
  489. finally
  490. {
  491. SkParagraphBuilderPoolManager.Return(builder);
  492. }
  493. }
  494. }
  495. #endregion
  496. #region IStateful
  497. private bool IsRendered { get; set; }
  498. private int CurrentLineIndex { get; set; }
  499. private float CurrentTopOffset { get; set; }
  500. public struct TextBlockState
  501. {
  502. public bool IsRendered;
  503. public int CurrentLineIndex;
  504. public float CurrentTopOffset;
  505. }
  506. public void ResetState(bool hardReset = false)
  507. {
  508. IsRendered = false;
  509. CurrentLineIndex = 0;
  510. CurrentTopOffset = 0;
  511. }
  512. public object GetState()
  513. {
  514. return new TextBlockState
  515. {
  516. IsRendered = IsRendered,
  517. CurrentLineIndex = CurrentLineIndex,
  518. CurrentTopOffset = CurrentTopOffset
  519. };
  520. }
  521. public void SetState(object state)
  522. {
  523. var textBlockState = (TextBlockState) state;
  524. IsRendered = textBlockState.IsRendered;
  525. CurrentLineIndex = textBlockState.CurrentLineIndex;
  526. CurrentTopOffset = textBlockState.CurrentTopOffset;
  527. }
  528. #endregion
  529. internal override string? GetCompanionHint() => Text.Substring(0, Math.Min(Text.Length, 50));
  530. internal override string? GetCompanionSearchableContent() => Text;
  531. }
  532. }