StyledText.cs 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725
  1. 
  2. // RichTextKit
  3. // Copyright © 2019-2020 Topten Software. All Rights Reserved.
  4. //
  5. // Licensed under the Apache License, Version 2.0 (the "License"); you may
  6. // not use this product except in compliance with the License. You may obtain
  7. // a copy of the License at
  8. //
  9. // http://www.apache.org/licenses/LICENSE-2.0
  10. //
  11. // Unless required by applicable law or agreed to in writing, software
  12. // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  13. // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  14. // License for the specific language governing permissions and limitations
  15. // under the License.
  16. using SkiaSharp;
  17. using System;
  18. using System.Collections.Generic;
  19. using System.Diagnostics;
  20. using System.Linq;
  21. using Topten.RichTextKit.Utils;
  22. namespace Topten.RichTextKit
  23. {
  24. /// <summary>
  25. /// Represents a block of formatted, laid out and measurable text
  26. /// </summary>
  27. public class StyledText
  28. {
  29. /// <summary>
  30. /// Constructor
  31. /// </summary>
  32. public StyledText()
  33. {
  34. }
  35. /// <summary>
  36. /// Constructs a styled text block from unstyled text
  37. /// </summary>
  38. /// <param name="codePoints"></param>
  39. public StyledText(Slice<int> codePoints)
  40. {
  41. AddText(codePoints, null);
  42. }
  43. /// <summary>
  44. /// Clear the content of this text block
  45. /// </summary>
  46. public virtual void Clear()
  47. {
  48. // Reset everything
  49. _codePoints.Clear();
  50. StyleRun.Pool.Value.ReturnAndClear(_styleRuns);
  51. _hasTextDirectionOverrides = false;
  52. OnChanged();
  53. }
  54. /// <summary>
  55. /// The length of the added text in code points
  56. /// </summary>
  57. public int Length => _codePoints.Length;
  58. /// <summary>
  59. /// Get the code points of this text block
  60. /// </summary>
  61. public Utf32Buffer CodePoints => _codePoints;
  62. /// <summary>
  63. /// Get the text runs as added by AddText
  64. /// </summary>
  65. public IReadOnlyList<StyleRun> StyleRuns
  66. {
  67. get
  68. {
  69. return _styleRuns;
  70. }
  71. }
  72. /// <summary>
  73. /// Converts a code point index to a character index
  74. /// </summary>
  75. /// <param name="codePointIndex">The code point index to convert</param>
  76. /// <returns>The converted index</returns>
  77. public int CodePointToCharacterIndex(int codePointIndex)
  78. {
  79. return _codePoints.Utf32OffsetToUtf16Offset(codePointIndex);
  80. }
  81. /// <summary>
  82. /// Converts a character index to a code point index
  83. /// </summary>
  84. /// <param name="characterIndex">The character index to convert</param>
  85. /// <returns>The converted index</returns>
  86. public int CharacterToCodePointIndex(int characterIndex)
  87. {
  88. return _codePoints.Utf16OffsetToUtf32Offset(characterIndex);
  89. }
  90. /// <summary>
  91. /// Add text to this text block
  92. /// </summary>
  93. /// <remarks>
  94. /// The added text will be internally coverted to UTF32.
  95. ///
  96. /// Note that all text indicies returned by and accepted by this object will
  97. /// be UTF32 "code point indicies". To convert between UTF16 character indicies
  98. /// and UTF32 code point indicies use the <see cref="CodePointToCharacterIndex(int)"/>
  99. /// and <see cref="CharacterToCodePointIndex(int)"/> methods
  100. /// </remarks>
  101. /// <param name="text">The text to add</param>
  102. /// <param name="style">The style of the text</param>
  103. public void AddText(ReadOnlySpan<char> text, IStyle style)
  104. {
  105. // Quit if redundant
  106. if (text.Length == 0)
  107. return;
  108. // Add to buffer
  109. var utf32 = _codePoints.Add(text);
  110. // Create a run
  111. var run = StyleRun.Pool.Value.Get();
  112. run.CodePointBuffer = _codePoints;
  113. run.Start = utf32.Start;
  114. run.Length = utf32.Length;
  115. run.Style = style;
  116. if (style != null)
  117. _hasTextDirectionOverrides |= style.TextDirection != TextDirection.Auto;
  118. // Add run
  119. _styleRuns.Add(run);
  120. // Need new layout
  121. OnChanged();
  122. }
  123. /// <summary>
  124. /// Add text to this paragraph
  125. /// </summary>
  126. /// <param name="text">The text to add</param>
  127. /// <param name="style">The style of the text</param>
  128. public void AddText(Slice<int> text, IStyle style)
  129. {
  130. if (text.Length == 0)
  131. return;
  132. // Add to UTF-32 buffer
  133. var utf32 = _codePoints.Add(text);
  134. // Create a run
  135. var run = StyleRun.Pool.Value.Get();
  136. run.CodePointBuffer = _codePoints;
  137. run.Start = utf32.Start;
  138. run.Length = utf32.Length;
  139. run.Style = style;
  140. if (style != null)
  141. _hasTextDirectionOverrides |= style.TextDirection != TextDirection.Auto;
  142. // Add run
  143. _styleRuns.Add(run);
  144. // Need new layout
  145. OnChanged();
  146. }
  147. /// <summary>
  148. /// Add text to this text block
  149. /// </summary>
  150. /// <remarks>
  151. /// The added text will be internally coverted to UTF32.
  152. ///
  153. /// Note that all text indicies returned by and accepted by this object will
  154. /// be UTF32 "code point indicies". To convert between UTF16 character indicies
  155. /// and UTF32 code point indicies use the <see cref="CodePointToCharacterIndex(int)"/>
  156. /// and <see cref="CharacterToCodePointIndex(int)"/> methods
  157. /// </remarks>
  158. /// <param name="text">The text to add</param>
  159. /// <param name="style">The style of the text</param>
  160. public void AddText(string text, IStyle style)
  161. {
  162. AddText(text.AsSpan(), style);
  163. }
  164. /// <summary>
  165. /// Add all the text from another text block to this text block
  166. /// </summary>
  167. /// <param name="text">Text to add</param>
  168. public void AddText(StyledText text)
  169. {
  170. foreach (var sr in text.StyleRuns)
  171. {
  172. AddText(sr.CodePoints, sr.Style);
  173. }
  174. }
  175. /// <summary>
  176. /// Add all the text from another text block to this text block
  177. /// </summary>
  178. /// <param name="offset">The position at which to insert the text</param>
  179. /// <param name="text">Text to add</param>
  180. public void InsertText(int offset, StyledText text)
  181. {
  182. foreach (var sr in text.StyleRuns)
  183. {
  184. InsertText(offset, sr.CodePoints, sr.Style);
  185. offset += sr.CodePoints.Length;
  186. }
  187. }
  188. /// <summary>
  189. /// Add text to this text block
  190. /// </summary>
  191. /// <remarks>
  192. /// If the style is null, the new text will acquire the style of the character
  193. /// before the insertion point. If the text block is currently empty the style
  194. /// must be supplied. If inserting at the start of a non-empty text block the
  195. /// style will be that of the first existing style run
  196. /// </remarks>
  197. /// <param name="position">The position to insert the text</param>
  198. /// <param name="text">The text to add</param>
  199. /// <param name="style">The style of the text (optional)</param>
  200. public void InsertText(int position, Slice<int> text, IStyle style = null)
  201. {
  202. // Redundant?
  203. if (text.Length == 0)
  204. return;
  205. if (style == null && _styleRuns.Count == 0)
  206. throw new InvalidOperationException("Must supply style when inserting into an empty text block");
  207. // Add to UTF-32 buffer
  208. var utf32 = _codePoints.Insert(position, text);
  209. // Update style runs
  210. FinishInsert(utf32, style);
  211. }
  212. /// <summary>
  213. /// Add text to this text block
  214. /// </summary>
  215. /// <remarks>
  216. /// If the style is null, the new text will acquire the style of the character
  217. /// before the insertion point. If the text block is currently empty the style
  218. /// must be supplied. If inserting at the start of a non-empty text block the
  219. /// style will be that of the first existing style run
  220. /// </remarks>
  221. /// <param name="position">The position to insert the text</param>
  222. /// <param name="text">The text to add</param>
  223. /// <param name="style">The style of the text (optional)</param>
  224. public void InsertText(int position, ReadOnlySpan<char> text, IStyle style = null)
  225. {
  226. // Redundant?
  227. if (text.Length == 0)
  228. return;
  229. if (style == null && _styleRuns.Count == 0)
  230. throw new InvalidOperationException("Must supply style when inserting into an empty text block");
  231. // Add to UTF-32 buffer
  232. var utf32 = _codePoints.Insert(position, text);
  233. // Update style runs
  234. FinishInsert(utf32, style);
  235. }
  236. /// <summary>
  237. /// Add text to this text block
  238. /// </summary>
  239. /// <remarks>
  240. /// If the style is null, the new text will acquire the style of the character
  241. /// before the insertion point. If the text block is currently empty the style
  242. /// must be supplied. If inserting at the start of a non-empty text block the
  243. /// style will be that of the first existing style run
  244. /// </remarks>
  245. /// <param name="position">The position to insert the text</param>
  246. /// <param name="text">The text to add</param>
  247. /// <param name="style">The style of the text (optional)</param>
  248. public void InsertText(int position, string text, IStyle style = null)
  249. {
  250. // Redundant?
  251. if (text.Length == 0)
  252. return;
  253. if (style == null && _styleRuns.Count == 0)
  254. throw new InvalidOperationException("Must supply style when inserting into an empty text block");
  255. // Add to UTF-32 buffer
  256. var utf32 = _codePoints.Insert(position, text);
  257. // Update style runs
  258. FinishInsert(utf32, style);
  259. }
  260. /// <summary>
  261. /// Deletes text from this text block
  262. /// </summary>
  263. /// <param name="position">The code point index to delete from</param>
  264. /// <param name="length">The number of code points to delete</param>
  265. public void DeleteText(int position, int length)
  266. {
  267. if (length == 0)
  268. return;
  269. // Delete text from the code point buffer
  270. _codePoints.Delete(position, length);
  271. // Fix up style runs
  272. for (int i = 0; i < _styleRuns.Count; i++)
  273. {
  274. // Get the run
  275. var sr = _styleRuns[i];
  276. // Ignore runs before the deleted range
  277. if (sr.End <= position)
  278. continue;
  279. // Runs that start before the deleted range
  280. if (sr.Start < position)
  281. {
  282. if (sr.End <= position + length)
  283. {
  284. // Truncate runs the overlap with the start of the delete range
  285. sr.Length = position - sr.Start;
  286. continue;
  287. }
  288. else
  289. {
  290. // Shorten runs that completely cover the deleted range
  291. sr.Length -= length;
  292. continue;
  293. }
  294. }
  295. // Runs that start within the deleted range
  296. if (sr.Start < position + length)
  297. {
  298. if (sr.End <= position + length)
  299. {
  300. // Delete runs that are completely within the deleted range
  301. _styleRuns.RemoveAt(i);
  302. StyleRun.Pool.Value.Return(sr);
  303. i--;
  304. continue;
  305. }
  306. else
  307. {
  308. // Runs that overlap the end of the deleted range, just
  309. // keep the part past the deleted range
  310. sr.Length = sr.End - (position + length);
  311. sr.Start = position;
  312. continue;
  313. }
  314. }
  315. // Run is after the deleted range, shuffle it back
  316. sr.Start -= length;
  317. }
  318. // coalesc runs
  319. CoalescStyleRuns();
  320. // Need new layout
  321. OnChanged();
  322. }
  323. /// <summary>
  324. /// Overwrites the styles of existing text in the text block
  325. /// </summary>
  326. /// <param name="position">The code point index of the start of the text</param>
  327. /// <param name="length">The length of the text</param>
  328. /// <param name="style">The new style to be applied</param>
  329. public void ApplyStyle(int position, int length, IStyle style)
  330. {
  331. // Check args
  332. if (position < 0 || position + length > this.Length)
  333. throw new ArgumentException("Invalid range");
  334. if (style == null)
  335. throw new ArgumentNullException(nameof(style));
  336. // Redundant?
  337. if (length == 0)
  338. return;
  339. // Easy case when applying same style to entire text block
  340. if (position == 0 && length == this.Length)
  341. {
  342. // Remove excess runs
  343. while (_styleRuns.Count > 1)
  344. {
  345. StyleRun.Pool.Value.Return(_styleRuns[1]);
  346. _styleRuns.RemoveAt(1);
  347. }
  348. // Reconfigure the first
  349. _styleRuns[0].Start = 0;
  350. _styleRuns[0].Length = length;
  351. _styleRuns[0].Style = style;
  352. // Reset text direction overrides flag
  353. _hasTextDirectionOverrides = style.TextDirection != TextDirection.Auto;
  354. // Invalidate and done
  355. OnChanged();
  356. return;
  357. }
  358. // Get all intersecting runs
  359. int newRunPos = -1;
  360. foreach (var subRun in _styleRuns.GetIntersectingRunsReverse(position, length))
  361. {
  362. if (subRun.Partial)
  363. {
  364. var run = _styleRuns[subRun.Index];
  365. if (subRun.Offset == 0)
  366. {
  367. // Overlaps start of existing run, keep end
  368. run.Start += subRun.Length;
  369. run.Length -= subRun.Length;
  370. newRunPos = subRun.Index;
  371. }
  372. else if (subRun.Offset + subRun.Length == run.Length)
  373. {
  374. // Overlaps end of existing run, keep start
  375. run.Length = subRun.Offset;
  376. newRunPos = subRun.Index + 1;
  377. }
  378. else
  379. {
  380. // Internal to existing run, keep start and end
  381. // Create new run for end
  382. var endRun = StyleRun.Pool.Value.Get();
  383. endRun.CodePointBuffer = _codePoints;
  384. endRun.Start = run.Start + subRun.Offset + subRun.Length;
  385. endRun.Length = run.End - endRun.Start;
  386. endRun.Style = run.Style;
  387. _styleRuns.Insert(subRun.Index + 1, endRun);
  388. // Shorten the existing run to keep start
  389. run.Length = subRun.Offset;
  390. newRunPos = subRun.Index + 1;
  391. }
  392. }
  393. else
  394. {
  395. // Remove completely covered style runs
  396. StyleRun.Pool.Value.Return(_styleRuns[subRun.Index]);
  397. _styleRuns.RemoveAt(subRun.Index);
  398. newRunPos = subRun.Index;
  399. }
  400. }
  401. // Create style run for the new style
  402. var newRun = StyleRun.Pool.Value.Get();
  403. newRun.CodePointBuffer = _codePoints;
  404. newRun.Start = position;
  405. newRun.Length = length;
  406. newRun.Style = style;
  407. _hasTextDirectionOverrides |= style.TextDirection != TextDirection.Auto;
  408. // Insert it
  409. _styleRuns.Insert(newRunPos, newRun);
  410. // Coalesc
  411. CoalescStyleRuns();
  412. // Need to redo layout
  413. OnChanged();
  414. }
  415. /// <summary>
  416. /// Extract text from this styled text block
  417. /// </summary>
  418. /// <param name="from">The code point offset to extract from</param>
  419. /// <param name="length">The number of code points to extract</param>
  420. /// <returns>A new text block with the RHS split part of the text</returns>
  421. public StyledText Extract(int from, int length)
  422. {
  423. // Create a new text block with the same attributes as this one
  424. var other = new StyledText();
  425. // Copy text to the new paragraph
  426. foreach (var subRun in _styleRuns.GetInterectingRuns(from, length))
  427. {
  428. var sr = _styleRuns[subRun.Index];
  429. other.AddText(sr.CodePoints.SubSlice(subRun.Offset, subRun.Length), sr.Style);
  430. }
  431. return other;
  432. }
  433. /// <summary>
  434. /// Gets the style of the text at a specified offset
  435. /// </summary>
  436. /// <remarks>
  437. /// When on a style run boundary, returns the style of the preceeding run
  438. /// </remarks>
  439. /// <param name="offset">The code point offset in the text</param>
  440. /// <returns>An IStyle</returns>
  441. public IStyle GetStyleAtOffset(int offset)
  442. {
  443. if (Length == 0 || _styleRuns.Count == 0)
  444. return null;
  445. if (offset == 0)
  446. return _styleRuns[0].Style;
  447. int runIndex = _styleRuns.BinarySearch(offset, (sr, a) =>
  448. {
  449. if (a <= sr.Start)
  450. return 1;
  451. if (a > sr.End)
  452. return -1;
  453. return 0;
  454. });
  455. if (runIndex < 0)
  456. runIndex = ~runIndex;
  457. if (runIndex >= _styleRuns.Count)
  458. runIndex = _styleRuns.Count - 1;
  459. return _styleRuns[runIndex].Style;
  460. }
  461. /// <summary>
  462. /// Completes the insertion of text by inserting it's style run
  463. /// and updating the offsets of existing style runs.
  464. /// </summary>
  465. /// <param name="utf32">The utf32 slice that was inserted</param>
  466. /// <param name="style">The style of the inserted text</param>
  467. void FinishInsert(Slice<int> utf32, IStyle style)
  468. {
  469. // Update style runs
  470. int newRunIndex = 0;
  471. for (int i = 0; i < _styleRuns.Count; i++)
  472. {
  473. // Get the style run
  474. var sr = _styleRuns[i];
  475. // Before inserted text?
  476. if (sr.End < utf32.Start)
  477. continue;
  478. // Special case for inserting at very start of text block
  479. // with no supplied style.
  480. if (sr.Start == 0 && utf32.Start == 0 && style == null)
  481. {
  482. sr.Length += utf32.Length;
  483. continue;
  484. }
  485. // After inserted text?
  486. if (sr.Start >= utf32.Start)
  487. {
  488. sr.Start += utf32.Length;
  489. continue;
  490. }
  491. // Inserting exactly at the end of a style run?
  492. if (sr.End == utf32.Start)
  493. {
  494. if (style == null || style == sr.Style)
  495. {
  496. // Extend the existing run
  497. sr.Length += utf32.Length;
  498. // Force style to null to suppress later creation
  499. // of a style run for it.
  500. style = null;
  501. }
  502. else
  503. {
  504. // Remember this is where to insert the new
  505. // style run
  506. newRunIndex = i + 1;
  507. }
  508. continue;
  509. }
  510. Debug.Assert(sr.End > utf32.Start);
  511. Debug.Assert(sr.Start < utf32.Start);
  512. // Inserting inside an existing run
  513. if (style == null || style == sr.Style)
  514. {
  515. // Extend the existing style run to cover
  516. // the newly inserted text with the same style
  517. sr.Length += utf32.Length;
  518. // Force style to null to suppress later creation
  519. // of a style run for it.
  520. style = null;
  521. }
  522. else
  523. {
  524. // Split this run and insert the new style run between
  525. // Create the second part
  526. var split = StyleRun.Pool.Value.Get();
  527. split.CodePointBuffer = _codePoints;
  528. split.Start = utf32.Start + utf32.Length;
  529. split.Length = sr.End - utf32.Start;
  530. split.Style = sr.Style;
  531. _styleRuns.Insert(i + 1, split);
  532. // Shorten this part
  533. sr.Length = utf32.Start - sr.Start;
  534. // Insert the newly styled run after this one
  535. newRunIndex = i + 1;
  536. // Skip the second part of the split in this for loop
  537. // as we've already calculated it
  538. i++;
  539. }
  540. }
  541. // Create a new style run
  542. if (style != null)
  543. {
  544. var run = StyleRun.Pool.Value.Get();
  545. run.CodePointBuffer = _codePoints;
  546. run.Start = utf32.Start;
  547. run.Length = utf32.Length;
  548. run.Style = style;
  549. _hasTextDirectionOverrides |= style.TextDirection != TextDirection.Auto;
  550. _styleRuns.Insert(newRunIndex, run);
  551. }
  552. // Coalesc if necessary
  553. if ((newRunIndex > 0 && _styleRuns[newRunIndex - 1].Style == style) ||
  554. (newRunIndex + 1 < _styleRuns.Count && _styleRuns[newRunIndex + 1].Style == style))
  555. {
  556. CoalescStyleRuns();
  557. }
  558. // Need new layout
  559. OnChanged();
  560. }
  561. /// <summary>
  562. /// Combines any consecutive style runs with the same style
  563. /// into a single run
  564. /// </summary>
  565. void CoalescStyleRuns()
  566. {
  567. // Nothing to do if no style runs
  568. if (_styleRuns.Count == 0)
  569. {
  570. _hasTextDirectionOverrides = false;
  571. return;
  572. }
  573. // Since we're iterating the entire set of style runs, might as
  574. // we recalculate this flag while we're at it
  575. _hasTextDirectionOverrides = _styleRuns[0].Style.TextDirection != TextDirection.Auto;
  576. // No need to coalesc a single run
  577. if (_styleRuns.Count == 1)
  578. return;
  579. // Coalesc...
  580. var prev = _styleRuns[0];
  581. for (int i = 1; i < _styleRuns.Count; i++)
  582. {
  583. // Get the run
  584. var run = _styleRuns[i];
  585. // Update flag
  586. _hasTextDirectionOverrides |= run.Style.TextDirection != TextDirection.Auto;
  587. // Can run be coalesced?
  588. if (run.Style == prev.Style)
  589. {
  590. // Yes
  591. prev.Length += run.Length;
  592. StyleRun.Pool.Value.Return(run);
  593. _styleRuns.RemoveAt(i);
  594. i--;
  595. }
  596. else
  597. {
  598. // No, move on..
  599. prev = run;
  600. }
  601. }
  602. }
  603. /// <summary>
  604. /// Called whenever the content of this styled text block changes
  605. /// </summary>
  606. protected virtual void OnChanged()
  607. {
  608. }
  609. /// <summary>
  610. /// All code points as supplied by user, accumulated into a single buffer
  611. /// </summary>
  612. protected Utf32Buffer _codePoints = new Utf32Buffer();
  613. /// <summary>
  614. /// A list of style runs, as supplied by user
  615. /// </summary>
  616. protected List<StyleRun> _styleRuns = new List<StyleRun>();
  617. /// <summary>
  618. /// Set to true if any style runs have a directionality override.
  619. /// </summary>
  620. protected bool _hasTextDirectionOverrides = false;
  621. /// <inheritdoc />
  622. public override string ToString()
  623. {
  624. return Utf32Utils.FromUtf32(CodePoints.AsSlice());
  625. }
  626. }
  627. }