RichString.cs 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383
  1. // RichTextKit
  2. // Copyright © 2019-2020 Topten Software. All Rights Reserved.
  3. //
  4. // Licensed under the Apache License, Version 2.0 (the "License"); you may
  5. // not use this product except in compliance with the License. You may obtain
  6. // a copy of the License at
  7. //
  8. // http://www.apache.org/licenses/LICENSE-2.0
  9. //
  10. // Unless required by applicable law or agreed to in writing, software
  11. // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  12. // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  13. // License for the specific language governing permissions and limitations
  14. // under the License.
  15. using SkiaSharp;
  16. using System;
  17. using System.Collections.Generic;
  18. using System.Linq;
  19. using System.Text;
  20. namespace Topten.RichTextKit
  21. {
  22. /// <summary>
  23. /// Represents a string decorated with rich text information including
  24. /// various helper methods for constructing rich text strings with a
  25. /// fluent-like API.
  26. /// </summary>
  27. public class RichString
  28. {
  29. /// <summary>
  30. /// Constructs a new rich text string
  31. /// </summary>
  32. /// <param name="str">An initial piece of text to append to the string</param>
  33. public RichString(string str = null)
  34. {
  35. _paragraphs.Add(new ParagraphInfo());
  36. if (str != null)
  37. Add(str);
  38. }
  39. /// <summary>
  40. /// Append text to this RichString
  41. /// </summary>
  42. /// <param name="text">The text to append</param>
  43. /// <returns></returns>
  44. public RichString Add(string text) => Append(new TextItem(text));
  45. /// <summary>
  46. /// Adds text with various style attributes changed
  47. /// </summary>
  48. /// <param name="text">The text to append</param>
  49. /// <param name="fontFamily">The new font family</param>
  50. /// <param name="fontSize">The new font size</param>
  51. /// <param name="fontWeight">The new font weight</param>
  52. /// <param name="fontWidth">The new font width</param>
  53. /// <param name="fontItalic">The new font italic</param>
  54. /// <param name="underline">The new underline style</param>
  55. /// <param name="strikeThrough">The new strike-through style</param>
  56. /// <param name="lineHeight">The new line height</param>
  57. /// <param name="textColor">The new text color</param>
  58. /// <param name="backgroundColor">The new background color</param>
  59. /// <param name="haloColor">The new halo color</param>
  60. /// <param name="haloWidth">The new halo width</param>
  61. /// <param name="haloBlur">The new halo blur width</param>
  62. /// <param name="letterSpacing">The new character spacing</param>
  63. /// <param name="fontVariant">The new font variant</param>
  64. /// <param name="textDirection">The new text direction</param>
  65. /// <returns>A reference to the same RichString instance</returns>
  66. public RichString Add(string text,
  67. string fontFamily = null,
  68. float? fontSize = null,
  69. int? fontWeight = null,
  70. SKFontStyleWidth? fontWidth = null,
  71. bool? fontItalic = null,
  72. UnderlineStyle? underline = null,
  73. StrikeThroughStyle? strikeThrough = null,
  74. float? lineHeight = null,
  75. SKColor? textColor = null,
  76. SKColor? backgroundColor = null,
  77. SKColor? haloColor = null,
  78. float? haloWidth = null,
  79. float? haloBlur = null,
  80. float? letterSpacing = null,
  81. FontVariant? fontVariant = null,
  82. TextDirection? textDirection = null
  83. )
  84. {
  85. if (string.IsNullOrEmpty(text))
  86. return this;
  87. Push();
  88. if (fontFamily != null) FontFamily(fontFamily);
  89. if (fontSize.HasValue) FontSize(fontSize.Value);
  90. if (fontWeight.HasValue) FontWeight(fontWeight.Value);
  91. if (fontWidth.HasValue) FontWidth(fontWidth.Value);
  92. if (fontItalic.HasValue) FontItalic(fontItalic.Value);
  93. if (underline.HasValue) Underline(underline.Value);
  94. if (strikeThrough.HasValue) StrikeThrough(strikeThrough.Value);
  95. if (lineHeight.HasValue) LineHeight(lineHeight.Value);
  96. if (textColor.HasValue) TextColor(textColor.Value);
  97. if (backgroundColor.HasValue) BackgroundColor(backgroundColor.Value);
  98. if (haloColor.HasValue) HaloColor(haloColor.Value);
  99. if (haloWidth.HasValue) HaloWidth(haloWidth.Value);
  100. if (haloBlur.HasValue) HaloBlur(haloBlur.Value);
  101. if (fontVariant.HasValue) FontVariant(fontVariant.Value);
  102. if (letterSpacing.HasValue) LetterSpacing(letterSpacing.Value);
  103. if (textDirection.HasValue) TextDirection(textDirection.Value);
  104. Add(text);
  105. Pop();
  106. return this;
  107. }
  108. /// <summary>
  109. /// Changes the font family
  110. /// </summary>
  111. /// <param name="value">The new font family</param>
  112. /// <returns>A reference to the same RichString instance</returns>
  113. public RichString FontFamily(string value) => Append(new FontFamilyItem(value));
  114. /// <summary>
  115. /// Changes the font size
  116. /// </summary>
  117. /// <param name="value">The new font size</param>
  118. /// <returns>A reference to the same RichString instance</returns>
  119. public RichString FontSize(float value) => Append(new FontSizeItem(value));
  120. /// <summary>
  121. /// Changes the font weight
  122. /// </summary>
  123. /// <param name="value">The new font weight</param>
  124. /// <returns>A reference to the same RichString instance</returns>
  125. public RichString FontWeight(int value) => Append(new FontWeightItem(value));
  126. /// <summary>
  127. /// Changes the font weight to bold or normal
  128. /// </summary>
  129. /// <param name="value">The new font bold setting</param>
  130. /// <returns>A reference to the same RichString instance</returns>
  131. public RichString Bold(bool value = true) => Append(new FontWeightItem(value ? 700 : 400));
  132. /// <summary>
  133. /// Changes the font width
  134. /// </summary>
  135. /// <param name="value">The new font width</param>
  136. /// <returns>A reference to the same RichString instance</returns>
  137. public RichString FontWidth(SKFontStyleWidth value) => Append(new FontWidthItem(value));
  138. /// <summary>
  139. /// Changes the font italic setting
  140. /// </summary>
  141. /// <param name="value">The new font italic setting</param>
  142. /// <returns>A reference to the same RichString instance</returns>
  143. public RichString FontItalic(bool value = true) => Append(new FontItalicItem(value));
  144. /// <summary>
  145. /// Changes the underline style
  146. /// </summary>
  147. /// <param name="value">The new underline style</param>
  148. /// <returns>A reference to the same RichString instance</returns>
  149. public RichString Underline(UnderlineStyle value = UnderlineStyle.Gapped) => Append(new UnderlineItem(value));
  150. /// <summary>
  151. /// Changes the strike-through style
  152. /// </summary>
  153. /// <param name="value">The new strike through style</param>
  154. /// <returns>A reference to the same RichString instance</returns>
  155. public RichString StrikeThrough(StrikeThroughStyle value = StrikeThroughStyle.Solid) => Append(new StrikeThroughItem(value));
  156. /// <summary>
  157. /// Changes the line height
  158. /// </summary>
  159. /// <param name="value">The new line height</param>
  160. /// <returns>A reference to the same RichString instance</returns>
  161. public RichString LineHeight(float value) => Append(new LineHeightItem(value));
  162. /// <summary>
  163. /// Changes the text color
  164. /// </summary>
  165. /// <param name="value">The new text color</param>
  166. /// <returns>A reference to the same RichString instance</returns>
  167. public RichString TextColor(SKColor value) => Append(new TextColorItem(value));
  168. /// <summary>
  169. /// Changes the background color
  170. /// </summary>
  171. /// <param name="value">The new background color</param>
  172. /// <returns>A reference to the same RichString instance</returns>
  173. public RichString BackgroundColor(SKColor value) => Append(new BackgroundColorItem(value));
  174. /// <summary>
  175. /// Changes the halo color
  176. /// </summary>
  177. /// <param name="value">The new halo color</param>
  178. /// <returns>A reference to the same RichString instance</returns>
  179. public RichString HaloColor(SKColor value) => Append(new HaloColorItem(value));
  180. /// <summary>
  181. /// Changes the halo width
  182. /// </summary>
  183. /// <param name="value">The new halo width</param>
  184. /// <returns>A reference to the same RichString instance</returns>
  185. public RichString HaloWidth(float value) => Append(new HaloWidthItem(value));
  186. /// <summary>
  187. /// Changes the halo blur width
  188. /// </summary>
  189. /// <param name="value">The new halo blur width</param>
  190. /// <returns>A reference to the same RichString instance</returns>
  191. public RichString HaloBlur(float value) => Append(new HaloBlurItem(value));
  192. /// <summary>
  193. /// Changes the character spacing
  194. /// </summary>
  195. /// <param name="value">The new character spacing</param>
  196. /// <returns>A reference to the same RichString instance</returns>
  197. public RichString LetterSpacing(float value) => Append(new LetterSpacingItem(value));
  198. /// <summary>
  199. /// Changes the font variant
  200. /// </summary>
  201. /// <param name="value">The new font variant</param>
  202. /// <returns>A reference to the same RichString instance</returns>
  203. public RichString FontVariant(FontVariant value) => Append(new FontVariantItem(value));
  204. /// <summary>
  205. /// Changes the text direction
  206. /// </summary>
  207. /// <param name="value">The new text direction</param>
  208. /// <returns>A reference to the same RichString instance</returns>
  209. public RichString TextDirection(TextDirection value) => Append(new TextDirectionItem(value));
  210. /// <summary>
  211. /// Saves the current style to an internal stack
  212. /// </summary>
  213. /// <returns>A reference to the same RichString instance</returns>
  214. public RichString Push() => Append(new PushItem());
  215. /// <summary>
  216. /// Resets to normal font (normal weight, italic off, underline off, strike through off, font variant off
  217. /// </summary>
  218. /// <returns>A reference to the same RichString instance</returns>
  219. public RichString Normal() => Append(new NormalItem());
  220. /// <summary>
  221. /// Restores a previous saved style from the internal stack
  222. /// </summary>
  223. /// <returns>A reference to the same RichString instance</returns>
  224. public RichString Pop() => Append(new PopItem());
  225. /// <summary>
  226. /// Starts a new text paragraph
  227. /// </summary>
  228. /// <returns>A reference to the same RichString instance</returns>
  229. public RichString Paragraph()
  230. {
  231. // End the previous paragraph with a carriage return
  232. Add("\n");
  233. // Start new paragraph
  234. _paragraphs.Add(new ParagraphInfo(_paragraphs[_paragraphs.Count - 1]));
  235. Invalidate();
  236. return this;
  237. }
  238. /// <summary>
  239. /// Sets the left margin of the current paragraph
  240. /// </summary>
  241. /// <param name="value">The margin width</param>
  242. /// <returns>A reference to the same RichString instance</returns>
  243. public RichString MarginLeft(float value)
  244. {
  245. _paragraphs[_paragraphs.Count - 1].MarginLeft = value;
  246. Invalidate();
  247. return this;
  248. }
  249. /// <summary>
  250. /// Sets the right margin of the current paragraph
  251. /// </summary>
  252. /// <param name="value">The margin width</param>
  253. /// <returns>A reference to the same RichString instance</returns>
  254. public RichString MarginRight(float value)
  255. {
  256. _paragraphs[_paragraphs.Count - 1].MarginRight = value;
  257. Invalidate();
  258. return this;
  259. }
  260. /// <summary>
  261. /// Sets the top margin of the current paragraph
  262. /// </summary>
  263. /// <param name="value">The margin height</param>
  264. /// <returns>A reference to the same RichString instance</returns>
  265. public RichString MarginTop(float value)
  266. {
  267. _paragraphs[_paragraphs.Count - 1].MarginTop = value;
  268. Invalidate();
  269. return this;
  270. }
  271. /// <summary>
  272. /// Sets the bottom margin of the current paragraph
  273. /// </summary>
  274. /// <param name="value">The margin height</param>
  275. /// <returns>A reference to the same RichString instance</returns>
  276. public RichString MarginBottom(float value)
  277. {
  278. _paragraphs[_paragraphs.Count - 1].MarginBottom = value;
  279. Invalidate();
  280. return this;
  281. }
  282. /// <summary>
  283. /// Sets the text alignment of the current paragraph
  284. /// </summary>
  285. /// <param name="value">The text alignment</param>
  286. /// <returns>A reference to the same RichString instance</returns>
  287. public RichString Alignment(TextAlignment value)
  288. {
  289. _paragraphs[_paragraphs.Count - 1].TextAlignment = value;
  290. Invalidate();
  291. return this;
  292. }
  293. /// <summary>
  294. /// Sets the base text direction of the current paragraph
  295. /// </summary>
  296. /// <param name="value">The base text direction</param>
  297. /// <returns>A reference to the same RichString instance</returns>
  298. public RichString BaseDirection(TextDirection value)
  299. {
  300. _paragraphs[_paragraphs.Count - 1].BaseDirection = value;
  301. Invalidate();
  302. return this;
  303. }
  304. /// <summary>
  305. /// The max width property sets the maximum width of a line, after which
  306. /// the line will be wrapped onto the next line.
  307. /// </summary>
  308. /// <remarks>
  309. /// This property can be set to null, in which case lines won't be wrapped.
  310. /// </remarks>
  311. public float? MaxWidth
  312. {
  313. get => _maxWidth;
  314. set
  315. {
  316. if (value.HasValue && value.Value < 0)
  317. value = 0;
  318. if (_maxWidth != value)
  319. {
  320. _maxWidth = value;
  321. Invalidate();
  322. }
  323. }
  324. }
  325. /// <summary>
  326. /// The maximum height of the TextBlock after which lines will be
  327. /// truncated and the final line will be appended with an
  328. /// ellipsis (`...`) character.
  329. /// </summary>
  330. /// <remarks>
  331. /// This property can be set to null, in which case the vertical height of the text block
  332. /// won't be capped.
  333. /// </remarks>
  334. public float? MaxHeight
  335. {
  336. get => _maxHeight;
  337. set
  338. {
  339. if (value.HasValue && value.Value < 0)
  340. value = 0;
  341. if (value != _maxHeight)
  342. {
  343. _maxHeight = value;
  344. Invalidate();
  345. }
  346. }
  347. }
  348. /// <summary>
  349. /// The maximum number of lines after which lines will be
  350. /// truncated and the final line will be appended with an
  351. /// ellipsis (`...`) character.
  352. /// </summary>
  353. /// <remarks>
  354. /// This property can be set to null, in which case the vertical height of
  355. /// the text block won't be capped.
  356. /// </remarks>
  357. public int? MaxLines
  358. {
  359. get => _maxLines;
  360. set
  361. {
  362. if (value.HasValue && value.Value < 0)
  363. value = 0;
  364. if (value != _maxLines)
  365. {
  366. _maxLines = value;
  367. Invalidate();
  368. }
  369. }
  370. }
  371. /// <summary>
  372. /// Sets the default text alignment for cases where
  373. /// the rich text doesn't specify an alignment
  374. /// </summary>
  375. public TextAlignment DefaultAlignment
  376. {
  377. get => _textAlignment;
  378. set
  379. {
  380. if (_textAlignment != value)
  381. {
  382. _textAlignment = value;
  383. Invalidate();
  384. }
  385. }
  386. }
  387. /// <summary>
  388. /// The default base text direction for cases where the rich text
  389. /// doesn't explicitly specify a text direction
  390. /// </summary>
  391. public TextDirection DefaultDirection
  392. {
  393. get => _baseDirection;
  394. set
  395. {
  396. if (_baseDirection != value)
  397. {
  398. _baseDirection = value;
  399. Invalidate();
  400. }
  401. }
  402. }
  403. /// <summary>
  404. /// The default text style to be used as the current style at the start of the rich string.
  405. /// Subsequent formatting operations will be applied over this base style.
  406. /// </summary>
  407. public IStyle DefaultStyle
  408. {
  409. get => _baseStyle;
  410. set
  411. {
  412. if (!_baseStyle.IsSame(value))
  413. {
  414. _needsFullLayout = true;
  415. Invalidate();
  416. }
  417. _baseStyle = value;
  418. }
  419. }
  420. /// <summary>
  421. /// Paint this text block
  422. /// </summary>
  423. /// <param name="canvas">The Skia canvas to paint to</param>
  424. /// <param name="options">Options controlling the paint operation</param>
  425. public void Paint(
  426. SKCanvas canvas,
  427. TextPaintOptions options = null)
  428. {
  429. Paint(canvas, SKPoint.Empty, options);
  430. }
  431. /// <summary>
  432. /// Paint this text block
  433. /// </summary>
  434. /// <param name="canvas">The Skia canvas to paint to</param>
  435. /// <param name="position">The top left position within the canvas to draw at</param>
  436. /// <param name="options">Options controlling the paint operation</param>
  437. public void Paint(
  438. SKCanvas canvas,
  439. SKPoint position,
  440. TextPaintOptions options = null)
  441. {
  442. Layout();
  443. var ctx = new PaintContext()
  444. {
  445. owner = this,
  446. canvas = canvas,
  447. paintPosition = position,
  448. renderWidth = _maxWidth ?? _measuredWidth,
  449. textPaintOptions = options,
  450. };
  451. foreach (var p in _paragraphs)
  452. {
  453. p.Paint(ref ctx);
  454. }
  455. }
  456. /// <summary>
  457. /// Discards all internal layout structures
  458. /// </summary>
  459. public void DiscardLayout()
  460. {
  461. _needsLayout = true;
  462. foreach (var p in _paragraphs)
  463. {
  464. p.DiscardLayout();
  465. }
  466. }
  467. /// <summary>
  468. /// The total height of all lines.
  469. /// </summary>
  470. public float MeasuredHeight
  471. {
  472. get
  473. {
  474. Layout();
  475. return _measuredHeight;
  476. }
  477. }
  478. /// <summary>
  479. /// The width of the widest line of text.
  480. /// </summary>
  481. /// <remarks>
  482. /// The returned width does not include any overhang.
  483. /// </remarks>
  484. public float MeasuredWidth
  485. {
  486. get
  487. {
  488. Layout();
  489. return _measuredWidth;
  490. }
  491. }
  492. /// <summary>
  493. /// The number of lines in the text
  494. /// </summary>
  495. public int LineCount
  496. {
  497. get
  498. {
  499. Layout();
  500. return _measuredLines;
  501. }
  502. }
  503. /// <summary>
  504. /// Indicates if the text was truncated due to max height or max lines
  505. /// constraints
  506. /// </summary>
  507. public bool Truncated
  508. {
  509. get
  510. {
  511. Layout();
  512. return _truncated;
  513. }
  514. }
  515. /// <summary>
  516. /// Gets the total length of the string in code points
  517. /// </summary>
  518. public int Length
  519. {
  520. get
  521. {
  522. Layout();
  523. return _totalLength;
  524. }
  525. }
  526. /// <summary>
  527. /// Gets the measured length of the string up to the truncation point
  528. /// in code points
  529. /// </summary>
  530. public int MeasuredLength
  531. {
  532. get
  533. {
  534. Layout();
  535. return _measuredLength;
  536. }
  537. }
  538. /// <summary>
  539. /// Returns the revision number of the content of this rich text string
  540. /// </summary>
  541. /// <remarks>
  542. /// If the revision number of a text string has not changed then painting it
  543. /// again will result in the exact same representation as the previous time.
  544. /// </remarks>
  545. public uint Revision
  546. {
  547. get
  548. {
  549. if (!_revisionValid)
  550. {
  551. _revision = (uint)System.Threading.Interlocked.Increment(ref _nextRevision);
  552. _revisionValid = true;
  553. }
  554. return _revision;
  555. }
  556. }
  557. /// <summary>
  558. /// Provides the plain-text equivalent of this RichString
  559. /// </summary>
  560. /// <returns>A plain-text string</returns>
  561. public override string ToString()
  562. {
  563. var sb = new StringBuilder();
  564. foreach (var p in _paragraphs)
  565. {
  566. p.Build(sb);
  567. }
  568. return sb.ToString();
  569. }
  570. /// <summary>
  571. /// Hit test this string
  572. /// </summary>
  573. /// <param name="x">The x-coordinate relative to top left of the string</param>
  574. /// <param name="y">The x-coordinate relative to top left of the string</param>
  575. /// <returns>A HitTestResult</returns>
  576. public HitTestResult HitTest(float x, float y)
  577. {
  578. // Make sure layout is up to date
  579. Layout();
  580. // Find the closest paragraph
  581. var para = FindClosestParagraph(y);
  582. // Get it's paint positio
  583. var paintPos = para.TextBlockPaintPosition(this);
  584. // Hit test
  585. var htr = para.TextBlock.HitTest(x - paintPos.X, y - paintPos.Y);
  586. // Convert the hit test record from TextBlock relative indices
  587. // to rich string relative indicies
  588. htr.ClosestLine += para.LineOffset;
  589. htr.ClosestCodePointIndex += para.CodePointOffset;
  590. if (htr.OverLine >= 0)
  591. htr.OverLine += para.LineOffset;
  592. if (htr.OverCodePointIndex >= 0)
  593. htr.OverCodePointIndex += para.CodePointOffset;
  594. // Done
  595. return htr;
  596. }
  597. ParagraphInfo FindClosestParagraph(float y)
  598. {
  599. // Work out which text block is closest
  600. ParagraphInfo pPrev = null;
  601. foreach (var p in _paragraphs)
  602. {
  603. // Ignore truncated paragraphs
  604. if (p.Truncated)
  605. break;
  606. // Is it before this paragraph's text block?
  607. if (y < p.yPosition + p.MarginTop && pPrev != null)
  608. {
  609. // Is it closer to this paragraph or the previous
  610. // NB: We compare the text block coords, not the paragraphs
  611. // so that regardless of paragraph margins we always
  612. // hit test against the closer text block
  613. var distPrev = y - (pPrev.yPosition + pPrev.TextBlock.MeasuredHeight);
  614. var distThis = y - (p.yPosition + p.MarginTop);
  615. if (Math.Abs(distPrev) < Math.Abs(distThis))
  616. return pPrev;
  617. else
  618. return p;
  619. }
  620. // Is it within this paragraph's textblock?
  621. if (y < p.yPosition + p.MarginTop + p.TextBlock.MeasuredHeight)
  622. {
  623. return p;
  624. }
  625. // Store the previous paragraph
  626. pPrev = p;
  627. }
  628. return pPrev;
  629. }
  630. /// <inheritdoc cref="TextBlock.GetCaretInfo(CaretPosition)"/>
  631. public CaretInfo GetCaretInfo(CaretPosition position)
  632. {
  633. Layout();
  634. // Is it outside the displayed range?
  635. if (position.CodePointIndex < 0 || position.CodePointIndex > MeasuredLength)
  636. return CaretInfo.None;
  637. // Find the paragraph containing that code point
  638. ParagraphInfo p;
  639. if (position.CodePointIndex == MeasuredLength)
  640. {
  641. // Special case for after the last displayed paragraph
  642. p = _paragraphs.LastOrDefault(x => !x.Truncated);
  643. }
  644. else
  645. {
  646. p = ParagraphForCodePointIndex(position.CodePointIndex);
  647. }
  648. // Get the caret info
  649. var ci = p.TextBlock.GetCaretInfo(new CaretPosition(position.CodePointIndex - p.CodePointOffset, position.AltPosition));
  650. // Adjust it
  651. ci.CodePointIndex += p.CodePointOffset;
  652. // Get it's paint position
  653. var paintPos = p.TextBlockPaintPosition(this);
  654. ci.CaretXCoord += paintPos.X;
  655. ci.CaretRectangle.Offset(paintPos);
  656. return ci;
  657. }
  658. ParagraphInfo ParagraphForCodePointIndex(int index)
  659. {
  660. for (int i=1; i<_paragraphs.Count; i++)
  661. {
  662. if (index < _paragraphs[i].CodePointOffset)
  663. return _paragraphs[i - 1];
  664. }
  665. return _paragraphs[_paragraphs.Count - 1];
  666. }
  667. void Invalidate()
  668. {
  669. _needsLayout = true;
  670. _revisionValid = false;
  671. }
  672. void Layout()
  673. {
  674. // Full layout needed?
  675. if (_needsFullLayout)
  676. {
  677. _needsFullLayout = false;
  678. DiscardLayout();
  679. }
  680. // Needed?
  681. if (!_needsLayout)
  682. return;
  683. _needsLayout = false;
  684. // Create a layout context
  685. var lctx = new LayoutContext()
  686. {
  687. yPosition = 0,
  688. maxWidth = _maxWidth,
  689. maxHeight = _maxHeight,
  690. maxLines = _maxLines,
  691. textAlignment = _textAlignment,
  692. baseDirection = _baseDirection,
  693. styleManager = StyleManager.Default.Value,
  694. previousParagraph = null,
  695. };
  696. // Setup style manager
  697. lctx.styleManager.Reset();
  698. if (_baseStyle != null)
  699. lctx.styleManager.CurrentStyle = _baseStyle;
  700. // Layout each paragraph
  701. _measuredWidth = 0;
  702. foreach (var p in _paragraphs)
  703. {
  704. // Layout the paragraph
  705. p.Layout(ref lctx);
  706. // If this paragraph wasn't completely truncated, then update the measured width
  707. if (!p.Truncated)
  708. {
  709. if (p.TextBlock.MeasuredWidth > _measuredWidth)
  710. _measuredWidth = p.TextBlock.MeasuredWidth;
  711. }
  712. // Store the this paragraph as the previous so a fully truncated subsequent
  713. // paragraph can add the ellipsis to this one
  714. lctx.previousParagraph = p;
  715. }
  716. _measuredHeight = lctx.yPosition;
  717. _measuredLines = lctx.lineCount;
  718. _truncated = lctx.Truncated;
  719. _measuredLength = lctx.MeasuredLength;
  720. _totalLength = lctx.TotalLength;
  721. }
  722. struct PaintContext
  723. {
  724. public RichString owner;
  725. public SKCanvas canvas;
  726. public SKPoint paintPosition;
  727. public float renderWidth;
  728. public TextPaintOptions textPaintOptions;
  729. }
  730. struct LayoutContext
  731. {
  732. public float yPosition;
  733. public int lineCount;
  734. public bool Truncated;
  735. public float? maxWidth;
  736. public float? maxHeight;
  737. public int? maxLines;
  738. public TextAlignment? textAlignment;
  739. public TextDirection? baseDirection;
  740. public StyleManager styleManager;
  741. public ParagraphInfo previousParagraph;
  742. public int MeasuredLength;
  743. public int TotalLength;
  744. }
  745. // Append an item to the current paragraph
  746. RichString Append(Item item)
  747. {
  748. _paragraphs[_paragraphs.Count - 1]._items.Add(item);
  749. _needsFullLayout = true;
  750. Invalidate();
  751. return this;
  752. }
  753. static int _nextRevision = 0;
  754. bool _revisionValid = false;
  755. uint _revision = 0;
  756. bool _needsLayout = true;
  757. bool _needsFullLayout = true;
  758. float? _maxWidth;
  759. float? _maxHeight;
  760. int? _maxLines;
  761. TextAlignment _textAlignment;
  762. TextDirection _baseDirection;
  763. IStyle _baseStyle;
  764. float _measuredWidth;
  765. float _measuredHeight;
  766. int _measuredLines;
  767. bool _truncated;
  768. int _measuredLength;
  769. int _totalLength;
  770. List<ParagraphInfo> _paragraphs = new List<ParagraphInfo>();
  771. class ParagraphInfo
  772. {
  773. public ParagraphInfo()
  774. {
  775. }
  776. public ParagraphInfo(ParagraphInfo other)
  777. {
  778. MarginLeft = other.MarginLeft;
  779. MarginRight = other.MarginRight;
  780. MarginTop = other.MarginTop;
  781. MarginBottom = other.MarginBottom;
  782. TextAlignment = other.TextAlignment;
  783. BaseDirection = other.BaseDirection;
  784. }
  785. public void DiscardLayout()
  786. {
  787. TextBlock = null;
  788. Truncated = false;
  789. }
  790. // The position at which to paint this text block
  791. // relative to the top left of the entire string
  792. public SKPoint TextBlockPaintPosition(RichString owner)
  793. {
  794. // Adjust x-position according to resolved text alignment to prevent
  795. // having to re-calculate the TextBlock's layout
  796. float yPos = this.yPosition + MarginTop;
  797. float xPos = MarginLeft;
  798. if (!owner.MaxWidth.HasValue)
  799. {
  800. switch (TextBlock.ResolveTextAlignment())
  801. {
  802. case RichTextKit.TextAlignment.Center:
  803. xPos += (owner.MeasuredWidth - TextBlock.MeasuredWidth) / 2;
  804. break;
  805. case RichTextKit.TextAlignment.Right:
  806. xPos += owner.MeasuredWidth - TextBlock.MeasuredWidth;
  807. break;
  808. }
  809. }
  810. return new SKPoint(xPos, yPos);
  811. }
  812. public void Build(StringBuilder sb)
  813. {
  814. foreach (var i in _items)
  815. {
  816. i.Build(sb);
  817. }
  818. }
  819. public void Paint(ref PaintContext ctx)
  820. {
  821. if (Truncated)
  822. return;
  823. TextRange? oldSel = null;
  824. if (ctx.textPaintOptions != null)
  825. {
  826. // Save old selection ranges
  827. oldSel = ctx.textPaintOptions.Selection;
  828. if (ctx.textPaintOptions.Selection.HasValue)
  829. {
  830. ctx.textPaintOptions.Selection = ctx.textPaintOptions.Selection.Value.Offset(-this.CodePointOffset);
  831. }
  832. }
  833. // Paint it
  834. TextBlock.Paint(ctx.canvas, ctx.paintPosition + TextBlockPaintPosition(ctx.owner), ctx.textPaintOptions);
  835. // Restore selection indicies
  836. if (oldSel.HasValue)
  837. {
  838. ctx.textPaintOptions.Selection = oldSel;
  839. }
  840. }
  841. public int CodePointOffset;
  842. public int LineOffset;
  843. // Layout this paragraph
  844. public void Layout(ref LayoutContext ctx)
  845. {
  846. // Store y position of this block
  847. this.yPosition = ctx.yPosition;
  848. // Create the text block
  849. if (TextBlock == null)
  850. {
  851. TextBlock = new TextBlock();
  852. var buildContext = new BuildContext()
  853. {
  854. StyleManager = ctx.styleManager,
  855. TextBlock = TextBlock,
  856. };
  857. foreach (var i in _items)
  858. {
  859. i.Build(buildContext);
  860. }
  861. }
  862. // Store code point offset of this paragraph
  863. CodePointOffset = ctx.TotalLength;
  864. LineOffset = ctx.lineCount;
  865. ctx.TotalLength += TextBlock.Length;
  866. // Text already truncated?
  867. Truncated = ctx.Truncated;
  868. if (Truncated)
  869. return;
  870. // Set the TextBlock
  871. TextBlock.Alignment = TextAlignment ?? ctx.textAlignment ?? RichTextKit.TextAlignment.Auto;
  872. TextBlock.BaseDirection = BaseDirection ?? ctx.baseDirection ?? RichTextKit.TextDirection.Auto;
  873. // Setup max width
  874. if (ctx.maxWidth.HasValue)
  875. {
  876. TextBlock.MaxWidth = ctx.maxWidth.Value - (MarginLeft + MarginRight);
  877. }
  878. else
  879. {
  880. TextBlock.MaxWidth = null;
  881. }
  882. // Set max height
  883. if (ctx.maxHeight.HasValue)
  884. {
  885. TextBlock.MaxHeight = ctx.maxHeight.Value - (ctx.yPosition) - (MarginTop + MarginBottom);
  886. }
  887. else
  888. {
  889. TextBlock.MaxHeight = null;
  890. }
  891. // Set max lines
  892. if (ctx.maxLines.HasValue)
  893. {
  894. TextBlock.MaxLines = ctx.maxLines.Value - ctx.lineCount;
  895. }
  896. else
  897. {
  898. TextBlock.MaxLines = null;
  899. }
  900. // Truncated?
  901. if (TextBlock.MaxLines == 0 || TextBlock.MaxHeight == 0)
  902. {
  903. TextBlock = null;
  904. Truncated = true;
  905. ctx.Truncated = true;
  906. return;
  907. }
  908. // Update the yPosition and stop further processing if truncated
  909. ctx.yPosition += TextBlock.MeasuredHeight + MarginTop;
  910. ctx.lineCount += TextBlock.Lines.Count;
  911. ctx.MeasuredLength += TextBlock.MeasuredLength;
  912. if (!TextBlock.Truncated)
  913. {
  914. // Only add the bottom margin if it wasn't truncated
  915. ctx.yPosition += MarginBottom;
  916. }
  917. else
  918. {
  919. if (TextBlock.Lines.Count == 0 && ctx.previousParagraph != null)
  920. {
  921. ctx.previousParagraph.TextBlock.AddEllipsis();
  922. }
  923. // All following blocks should be truncated
  924. ctx.Truncated = true;
  925. }
  926. }
  927. public TextBlock TextBlock;
  928. public float MarginLeft;
  929. public float MarginRight;
  930. public float MarginTop;
  931. public float MarginBottom;
  932. public TextAlignment? TextAlignment;
  933. public TextDirection? BaseDirection;
  934. public bool Truncated;
  935. public float yPosition; // Laid out y-position
  936. public List<Item> _items = new List<Item>();
  937. }
  938. class BuildContext
  939. {
  940. public TextBlock TextBlock;
  941. public StyleManager StyleManager;
  942. }
  943. abstract class Item
  944. {
  945. public abstract void Build(BuildContext ctx);
  946. public virtual void Build(StringBuilder sb) { }
  947. }
  948. class TextItem : Item
  949. {
  950. public TextItem(string str)
  951. {
  952. _text = str;
  953. }
  954. string _text;
  955. public override void Build(BuildContext ctx)
  956. {
  957. ctx.TextBlock.AddText(_text, ctx.StyleManager.CurrentStyle);
  958. }
  959. public override void Build(StringBuilder sb)
  960. {
  961. sb.Append(_text);
  962. }
  963. }
  964. class FontFamilyItem : Item
  965. {
  966. public FontFamilyItem(string value)
  967. {
  968. _value = value;
  969. }
  970. string _value;
  971. public override void Build(BuildContext ctx)
  972. {
  973. ctx.StyleManager.FontFamily(_value);
  974. }
  975. }
  976. class FontSizeItem : Item
  977. {
  978. public FontSizeItem(float value)
  979. {
  980. _value = value;
  981. }
  982. float _value;
  983. public override void Build(BuildContext ctx)
  984. {
  985. ctx.StyleManager.FontSize(_value);
  986. }
  987. }
  988. class FontWeightItem : Item
  989. {
  990. public FontWeightItem(int value)
  991. {
  992. _value = value;
  993. }
  994. int _value;
  995. public override void Build(BuildContext ctx)
  996. {
  997. ctx.StyleManager.FontWeight(_value);
  998. }
  999. }
  1000. class FontWidthItem : Item
  1001. {
  1002. public FontWidthItem(SKFontStyleWidth value)
  1003. {
  1004. _value = value;
  1005. }
  1006. SKFontStyleWidth _value;
  1007. public override void Build(BuildContext ctx)
  1008. {
  1009. ctx.StyleManager.FontWidth(_value);
  1010. }
  1011. }
  1012. class FontItalicItem : Item
  1013. {
  1014. public FontItalicItem(bool value)
  1015. {
  1016. _value = value;
  1017. }
  1018. bool _value;
  1019. public override void Build(BuildContext ctx)
  1020. {
  1021. ctx.StyleManager.FontItalic(_value);
  1022. }
  1023. }
  1024. class UnderlineItem : Item
  1025. {
  1026. public UnderlineItem(UnderlineStyle value)
  1027. {
  1028. _value = value;
  1029. }
  1030. UnderlineStyle _value;
  1031. public override void Build(BuildContext ctx)
  1032. {
  1033. ctx.StyleManager.Underline(_value);
  1034. }
  1035. }
  1036. class StrikeThroughItem : Item
  1037. {
  1038. public StrikeThroughItem(StrikeThroughStyle value)
  1039. {
  1040. _value = value;
  1041. }
  1042. StrikeThroughStyle _value;
  1043. public override void Build(BuildContext ctx)
  1044. {
  1045. ctx.StyleManager.StrikeThrough(_value);
  1046. }
  1047. }
  1048. class LineHeightItem : Item
  1049. {
  1050. public LineHeightItem(float value)
  1051. {
  1052. _value = value;
  1053. }
  1054. float _value;
  1055. public override void Build(BuildContext ctx)
  1056. {
  1057. ctx.StyleManager.LineHeight(_value);
  1058. }
  1059. }
  1060. class TextColorItem : Item
  1061. {
  1062. public TextColorItem(SKColor value)
  1063. {
  1064. _value = value;
  1065. }
  1066. SKColor _value;
  1067. public override void Build(BuildContext ctx)
  1068. {
  1069. ctx.StyleManager.TextColor(_value);
  1070. }
  1071. }
  1072. class BackgroundColorItem : Item
  1073. {
  1074. public BackgroundColorItem(SKColor value)
  1075. {
  1076. _value = value;
  1077. }
  1078. SKColor _value;
  1079. public override void Build(BuildContext ctx)
  1080. {
  1081. ctx.StyleManager.BackgroundColor(_value);
  1082. }
  1083. }
  1084. class HaloColorItem : Item
  1085. {
  1086. public HaloColorItem(SKColor value)
  1087. {
  1088. _value = value;
  1089. }
  1090. SKColor _value;
  1091. public override void Build(BuildContext ctx)
  1092. {
  1093. ctx.StyleManager.HaloColor(_value);
  1094. }
  1095. }
  1096. class HaloWidthItem : Item
  1097. {
  1098. public HaloWidthItem(float value)
  1099. {
  1100. _value = value;
  1101. }
  1102. float _value;
  1103. public override void Build(BuildContext ctx)
  1104. {
  1105. ctx.StyleManager.HaloWidth(_value);
  1106. }
  1107. }
  1108. class HaloBlurItem : Item
  1109. {
  1110. public HaloBlurItem(float value)
  1111. {
  1112. _value = value;
  1113. }
  1114. float _value;
  1115. public override void Build(BuildContext ctx)
  1116. {
  1117. ctx.StyleManager.HaloBlur(_value);
  1118. }
  1119. }
  1120. class LetterSpacingItem : Item
  1121. {
  1122. public LetterSpacingItem(float value)
  1123. {
  1124. _value = value;
  1125. }
  1126. float _value;
  1127. public override void Build(BuildContext ctx)
  1128. {
  1129. ctx.StyleManager.LetterSpacing(_value);
  1130. }
  1131. }
  1132. class FontVariantItem : Item
  1133. {
  1134. public FontVariantItem(FontVariant value)
  1135. {
  1136. _value = value;
  1137. }
  1138. FontVariant _value;
  1139. public override void Build(BuildContext ctx)
  1140. {
  1141. ctx.StyleManager.FontVariant(_value);
  1142. }
  1143. }
  1144. class TextDirectionItem : Item
  1145. {
  1146. public TextDirectionItem(TextDirection value)
  1147. {
  1148. _value = value;
  1149. }
  1150. TextDirection _value;
  1151. public override void Build(BuildContext ctx)
  1152. {
  1153. ctx.StyleManager.TextDirection(_value);
  1154. }
  1155. }
  1156. class PushItem : Item
  1157. {
  1158. public PushItem()
  1159. {
  1160. }
  1161. public override void Build(BuildContext ctx)
  1162. {
  1163. ctx.StyleManager.Push();
  1164. }
  1165. }
  1166. class PopItem : Item
  1167. {
  1168. public PopItem()
  1169. {
  1170. }
  1171. public override void Build(BuildContext ctx)
  1172. {
  1173. ctx.StyleManager.Pop();
  1174. }
  1175. }
  1176. class NormalItem : Item
  1177. {
  1178. public NormalItem()
  1179. {
  1180. }
  1181. public override void Build(BuildContext ctx)
  1182. {
  1183. ctx.StyleManager.Bold(false);
  1184. ctx.StyleManager.FontItalic(false);
  1185. ctx.StyleManager.Underline(UnderlineStyle.None);
  1186. ctx.StyleManager.StrikeThrough(StrikeThroughStyle.None);
  1187. ctx.StyleManager.FontVariant(RichTextKit.FontVariant.Normal);
  1188. }
  1189. }
  1190. }
  1191. }