TextBlock.cs 26 KB

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