2
0

TextBlock.cs 26 KB

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