123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725 |
-
- // RichTextKit
- // Copyright © 2019-2020 Topten Software. All Rights Reserved.
- //
- // Licensed under the Apache License, Version 2.0 (the "License"); you may
- // not use this product except in compliance with the License. You may obtain
- // a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- // WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- // License for the specific language governing permissions and limitations
- // under the License.
- using SkiaSharp;
- using System;
- using System.Collections.Generic;
- using System.Diagnostics;
- using System.Linq;
- using Topten.RichTextKit.Utils;
- namespace Topten.RichTextKit
- {
- /// <summary>
- /// Represents a block of formatted, laid out and measurable text
- /// </summary>
- public class StyledText
- {
- /// <summary>
- /// Constructor
- /// </summary>
- public StyledText()
- {
- }
- /// <summary>
- /// Constructs a styled text block from unstyled text
- /// </summary>
- /// <param name="codePoints"></param>
- public StyledText(Slice<int> codePoints)
- {
- AddText(codePoints, null);
- }
- /// <summary>
- /// Clear the content of this text block
- /// </summary>
- public virtual void Clear()
- {
- // Reset everything
- _codePoints.Clear();
- StyleRun.Pool.Value.ReturnAndClear(_styleRuns);
- _hasTextDirectionOverrides = false;
- OnChanged();
- }
- /// <summary>
- /// The length of the added text in code points
- /// </summary>
- public int Length => _codePoints.Length;
- /// <summary>
- /// Get the code points of this text block
- /// </summary>
- public Utf32Buffer CodePoints => _codePoints;
- /// <summary>
- /// Get the text runs as added by AddText
- /// </summary>
- public IReadOnlyList<StyleRun> StyleRuns
- {
- get
- {
- return _styleRuns;
- }
- }
- /// <summary>
- /// Converts a code point index to a character index
- /// </summary>
- /// <param name="codePointIndex">The code point index to convert</param>
- /// <returns>The converted index</returns>
- public int CodePointToCharacterIndex(int codePointIndex)
- {
- return _codePoints.Utf32OffsetToUtf16Offset(codePointIndex);
- }
- /// <summary>
- /// Converts a character index to a code point index
- /// </summary>
- /// <param name="characterIndex">The character index to convert</param>
- /// <returns>The converted index</returns>
- public int CharacterToCodePointIndex(int characterIndex)
- {
- return _codePoints.Utf16OffsetToUtf32Offset(characterIndex);
- }
- /// <summary>
- /// Add text to this text block
- /// </summary>
- /// <remarks>
- /// The added text will be internally coverted to UTF32.
- ///
- /// Note that all text indicies returned by and accepted by this object will
- /// be UTF32 "code point indicies". To convert between UTF16 character indicies
- /// and UTF32 code point indicies use the <see cref="CodePointToCharacterIndex(int)"/>
- /// and <see cref="CharacterToCodePointIndex(int)"/> methods
- /// </remarks>
- /// <param name="text">The text to add</param>
- /// <param name="style">The style of the text</param>
- public void AddText(ReadOnlySpan<char> text, IStyle style)
- {
- // Quit if redundant
- if (text.Length == 0)
- return;
- // Add to buffer
- var utf32 = _codePoints.Add(text);
- // Create a run
- var run = StyleRun.Pool.Value.Get();
- run.CodePointBuffer = _codePoints;
- run.Start = utf32.Start;
- run.Length = utf32.Length;
- run.Style = style;
- if (style != null)
- _hasTextDirectionOverrides |= style.TextDirection != TextDirection.Auto;
- // Add run
- _styleRuns.Add(run);
- // Need new layout
- OnChanged();
- }
- /// <summary>
- /// Add text to this paragraph
- /// </summary>
- /// <param name="text">The text to add</param>
- /// <param name="style">The style of the text</param>
- public void AddText(Slice<int> text, IStyle style)
- {
- if (text.Length == 0)
- return;
- // Add to UTF-32 buffer
- var utf32 = _codePoints.Add(text);
- // Create a run
- var run = StyleRun.Pool.Value.Get();
- run.CodePointBuffer = _codePoints;
- run.Start = utf32.Start;
- run.Length = utf32.Length;
- run.Style = style;
- if (style != null)
- _hasTextDirectionOverrides |= style.TextDirection != TextDirection.Auto;
- // Add run
- _styleRuns.Add(run);
- // Need new layout
- OnChanged();
- }
- /// <summary>
- /// Add text to this text block
- /// </summary>
- /// <remarks>
- /// The added text will be internally coverted to UTF32.
- ///
- /// Note that all text indicies returned by and accepted by this object will
- /// be UTF32 "code point indicies". To convert between UTF16 character indicies
- /// and UTF32 code point indicies use the <see cref="CodePointToCharacterIndex(int)"/>
- /// and <see cref="CharacterToCodePointIndex(int)"/> methods
- /// </remarks>
- /// <param name="text">The text to add</param>
- /// <param name="style">The style of the text</param>
- public void AddText(string text, IStyle style)
- {
- AddText(text.AsSpan(), style);
- }
- /// <summary>
- /// Add all the text from another text block to this text block
- /// </summary>
- /// <param name="text">Text to add</param>
- public void AddText(StyledText text)
- {
- foreach (var sr in text.StyleRuns)
- {
- AddText(sr.CodePoints, sr.Style);
- }
- }
- /// <summary>
- /// Add all the text from another text block to this text block
- /// </summary>
- /// <param name="offset">The position at which to insert the text</param>
- /// <param name="text">Text to add</param>
- public void InsertText(int offset, StyledText text)
- {
- foreach (var sr in text.StyleRuns)
- {
- InsertText(offset, sr.CodePoints, sr.Style);
- offset += sr.CodePoints.Length;
- }
- }
- /// <summary>
- /// Add text to this text block
- /// </summary>
- /// <remarks>
- /// If the style is null, the new text will acquire the style of the character
- /// before the insertion point. If the text block is currently empty the style
- /// must be supplied. If inserting at the start of a non-empty text block the
- /// style will be that of the first existing style run
- /// </remarks>
- /// <param name="position">The position to insert the text</param>
- /// <param name="text">The text to add</param>
- /// <param name="style">The style of the text (optional)</param>
- public void InsertText(int position, Slice<int> text, IStyle style = null)
- {
- // Redundant?
- if (text.Length == 0)
- return;
- if (style == null && _styleRuns.Count == 0)
- throw new InvalidOperationException("Must supply style when inserting into an empty text block");
- // Add to UTF-32 buffer
- var utf32 = _codePoints.Insert(position, text);
- // Update style runs
- FinishInsert(utf32, style);
- }
- /// <summary>
- /// Add text to this text block
- /// </summary>
- /// <remarks>
- /// If the style is null, the new text will acquire the style of the character
- /// before the insertion point. If the text block is currently empty the style
- /// must be supplied. If inserting at the start of a non-empty text block the
- /// style will be that of the first existing style run
- /// </remarks>
- /// <param name="position">The position to insert the text</param>
- /// <param name="text">The text to add</param>
- /// <param name="style">The style of the text (optional)</param>
- public void InsertText(int position, ReadOnlySpan<char> text, IStyle style = null)
- {
- // Redundant?
- if (text.Length == 0)
- return;
- if (style == null && _styleRuns.Count == 0)
- throw new InvalidOperationException("Must supply style when inserting into an empty text block");
- // Add to UTF-32 buffer
- var utf32 = _codePoints.Insert(position, text);
- // Update style runs
- FinishInsert(utf32, style);
- }
- /// <summary>
- /// Add text to this text block
- /// </summary>
- /// <remarks>
- /// If the style is null, the new text will acquire the style of the character
- /// before the insertion point. If the text block is currently empty the style
- /// must be supplied. If inserting at the start of a non-empty text block the
- /// style will be that of the first existing style run
- /// </remarks>
- /// <param name="position">The position to insert the text</param>
- /// <param name="text">The text to add</param>
- /// <param name="style">The style of the text (optional)</param>
- public void InsertText(int position, string text, IStyle style = null)
- {
- // Redundant?
- if (text.Length == 0)
- return;
- if (style == null && _styleRuns.Count == 0)
- throw new InvalidOperationException("Must supply style when inserting into an empty text block");
- // Add to UTF-32 buffer
- var utf32 = _codePoints.Insert(position, text);
- // Update style runs
- FinishInsert(utf32, style);
- }
- /// <summary>
- /// Deletes text from this text block
- /// </summary>
- /// <param name="position">The code point index to delete from</param>
- /// <param name="length">The number of code points to delete</param>
- public void DeleteText(int position, int length)
- {
- if (length == 0)
- return;
- // Delete text from the code point buffer
- _codePoints.Delete(position, length);
- // Fix up style runs
- for (int i = 0; i < _styleRuns.Count; i++)
- {
- // Get the run
- var sr = _styleRuns[i];
- // Ignore runs before the deleted range
- if (sr.End <= position)
- continue;
- // Runs that start before the deleted range
- if (sr.Start < position)
- {
- if (sr.End <= position + length)
- {
- // Truncate runs the overlap with the start of the delete range
- sr.Length = position - sr.Start;
- continue;
- }
- else
- {
- // Shorten runs that completely cover the deleted range
- sr.Length -= length;
- continue;
- }
- }
- // Runs that start within the deleted range
- if (sr.Start < position + length)
- {
- if (sr.End <= position + length)
- {
- // Delete runs that are completely within the deleted range
- _styleRuns.RemoveAt(i);
- StyleRun.Pool.Value.Return(sr);
- i--;
- continue;
- }
- else
- {
- // Runs that overlap the end of the deleted range, just
- // keep the part past the deleted range
- sr.Length = sr.End - (position + length);
- sr.Start = position;
- continue;
- }
- }
- // Run is after the deleted range, shuffle it back
- sr.Start -= length;
- }
- // coalesc runs
- CoalescStyleRuns();
- // Need new layout
- OnChanged();
- }
- /// <summary>
- /// Overwrites the styles of existing text in the text block
- /// </summary>
- /// <param name="position">The code point index of the start of the text</param>
- /// <param name="length">The length of the text</param>
- /// <param name="style">The new style to be applied</param>
- public void ApplyStyle(int position, int length, IStyle style)
- {
- // Check args
- if (position < 0 || position + length > this.Length)
- throw new ArgumentException("Invalid range");
- if (style == null)
- throw new ArgumentNullException(nameof(style));
- // Redundant?
- if (length == 0)
- return;
- // Easy case when applying same style to entire text block
- if (position == 0 && length == this.Length)
- {
- // Remove excess runs
- while (_styleRuns.Count > 1)
- {
- StyleRun.Pool.Value.Return(_styleRuns[1]);
- _styleRuns.RemoveAt(1);
- }
- // Reconfigure the first
- _styleRuns[0].Start = 0;
- _styleRuns[0].Length = length;
- _styleRuns[0].Style = style;
- // Reset text direction overrides flag
- _hasTextDirectionOverrides = style.TextDirection != TextDirection.Auto;
- // Invalidate and done
- OnChanged();
- return;
- }
- // Get all intersecting runs
- int newRunPos = -1;
- foreach (var subRun in _styleRuns.GetIntersectingRunsReverse(position, length))
- {
- if (subRun.Partial)
- {
- var run = _styleRuns[subRun.Index];
- if (subRun.Offset == 0)
- {
- // Overlaps start of existing run, keep end
- run.Start += subRun.Length;
- run.Length -= subRun.Length;
- newRunPos = subRun.Index;
- }
- else if (subRun.Offset + subRun.Length == run.Length)
- {
- // Overlaps end of existing run, keep start
- run.Length = subRun.Offset;
- newRunPos = subRun.Index + 1;
- }
- else
- {
- // Internal to existing run, keep start and end
- // Create new run for end
- var endRun = StyleRun.Pool.Value.Get();
- endRun.CodePointBuffer = _codePoints;
- endRun.Start = run.Start + subRun.Offset + subRun.Length;
- endRun.Length = run.End - endRun.Start;
- endRun.Style = run.Style;
- _styleRuns.Insert(subRun.Index + 1, endRun);
- // Shorten the existing run to keep start
- run.Length = subRun.Offset;
- newRunPos = subRun.Index + 1;
- }
- }
- else
- {
- // Remove completely covered style runs
- StyleRun.Pool.Value.Return(_styleRuns[subRun.Index]);
- _styleRuns.RemoveAt(subRun.Index);
- newRunPos = subRun.Index;
- }
- }
- // Create style run for the new style
- var newRun = StyleRun.Pool.Value.Get();
- newRun.CodePointBuffer = _codePoints;
- newRun.Start = position;
- newRun.Length = length;
- newRun.Style = style;
- _hasTextDirectionOverrides |= style.TextDirection != TextDirection.Auto;
- // Insert it
- _styleRuns.Insert(newRunPos, newRun);
- // Coalesc
- CoalescStyleRuns();
- // Need to redo layout
- OnChanged();
- }
- /// <summary>
- /// Extract text from this styled text block
- /// </summary>
- /// <param name="from">The code point offset to extract from</param>
- /// <param name="length">The number of code points to extract</param>
- /// <returns>A new text block with the RHS split part of the text</returns>
- public StyledText Extract(int from, int length)
- {
- // Create a new text block with the same attributes as this one
- var other = new StyledText();
- // Copy text to the new paragraph
- foreach (var subRun in _styleRuns.GetInterectingRuns(from, length))
- {
- var sr = _styleRuns[subRun.Index];
- other.AddText(sr.CodePoints.SubSlice(subRun.Offset, subRun.Length), sr.Style);
- }
- return other;
- }
- /// <summary>
- /// Gets the style of the text at a specified offset
- /// </summary>
- /// <remarks>
- /// When on a style run boundary, returns the style of the preceeding run
- /// </remarks>
- /// <param name="offset">The code point offset in the text</param>
- /// <returns>An IStyle</returns>
- public IStyle GetStyleAtOffset(int offset)
- {
- if (Length == 0 || _styleRuns.Count == 0)
- return null;
- if (offset == 0)
- return _styleRuns[0].Style;
- int runIndex = _styleRuns.BinarySearch(offset, (sr, a) =>
- {
- if (a <= sr.Start)
- return 1;
- if (a > sr.End)
- return -1;
- return 0;
- });
- if (runIndex < 0)
- runIndex = ~runIndex;
- if (runIndex >= _styleRuns.Count)
- runIndex = _styleRuns.Count - 1;
- return _styleRuns[runIndex].Style;
- }
- /// <summary>
- /// Completes the insertion of text by inserting it's style run
- /// and updating the offsets of existing style runs.
- /// </summary>
- /// <param name="utf32">The utf32 slice that was inserted</param>
- /// <param name="style">The style of the inserted text</param>
- void FinishInsert(Slice<int> utf32, IStyle style)
- {
- // Update style runs
- int newRunIndex = 0;
- for (int i = 0; i < _styleRuns.Count; i++)
- {
- // Get the style run
- var sr = _styleRuns[i];
- // Before inserted text?
- if (sr.End < utf32.Start)
- continue;
- // Special case for inserting at very start of text block
- // with no supplied style.
- if (sr.Start == 0 && utf32.Start == 0 && style == null)
- {
- sr.Length += utf32.Length;
- continue;
- }
- // After inserted text?
- if (sr.Start >= utf32.Start)
- {
- sr.Start += utf32.Length;
- continue;
- }
- // Inserting exactly at the end of a style run?
- if (sr.End == utf32.Start)
- {
- if (style == null || style == sr.Style)
- {
- // Extend the existing run
- sr.Length += utf32.Length;
- // Force style to null to suppress later creation
- // of a style run for it.
- style = null;
- }
- else
- {
- // Remember this is where to insert the new
- // style run
- newRunIndex = i + 1;
- }
- continue;
- }
- Debug.Assert(sr.End > utf32.Start);
- Debug.Assert(sr.Start < utf32.Start);
- // Inserting inside an existing run
- if (style == null || style == sr.Style)
- {
- // Extend the existing style run to cover
- // the newly inserted text with the same style
- sr.Length += utf32.Length;
- // Force style to null to suppress later creation
- // of a style run for it.
- style = null;
- }
- else
- {
- // Split this run and insert the new style run between
- // Create the second part
- var split = StyleRun.Pool.Value.Get();
- split.CodePointBuffer = _codePoints;
- split.Start = utf32.Start + utf32.Length;
- split.Length = sr.End - utf32.Start;
- split.Style = sr.Style;
- _styleRuns.Insert(i + 1, split);
- // Shorten this part
- sr.Length = utf32.Start - sr.Start;
- // Insert the newly styled run after this one
- newRunIndex = i + 1;
- // Skip the second part of the split in this for loop
- // as we've already calculated it
- i++;
- }
- }
- // Create a new style run
- if (style != null)
- {
- var run = StyleRun.Pool.Value.Get();
- run.CodePointBuffer = _codePoints;
- run.Start = utf32.Start;
- run.Length = utf32.Length;
- run.Style = style;
- _hasTextDirectionOverrides |= style.TextDirection != TextDirection.Auto;
- _styleRuns.Insert(newRunIndex, run);
- }
- // Coalesc if necessary
- if ((newRunIndex > 0 && _styleRuns[newRunIndex - 1].Style == style) ||
- (newRunIndex + 1 < _styleRuns.Count && _styleRuns[newRunIndex + 1].Style == style))
- {
- CoalescStyleRuns();
- }
- // Need new layout
- OnChanged();
- }
- /// <summary>
- /// Combines any consecutive style runs with the same style
- /// into a single run
- /// </summary>
- void CoalescStyleRuns()
- {
- // Nothing to do if no style runs
- if (_styleRuns.Count == 0)
- {
- _hasTextDirectionOverrides = false;
- return;
- }
- // Since we're iterating the entire set of style runs, might as
- // we recalculate this flag while we're at it
- _hasTextDirectionOverrides = _styleRuns[0].Style.TextDirection != TextDirection.Auto;
- // No need to coalesc a single run
- if (_styleRuns.Count == 1)
- return;
- // Coalesc...
- var prev = _styleRuns[0];
- for (int i = 1; i < _styleRuns.Count; i++)
- {
- // Get the run
- var run = _styleRuns[i];
- // Update flag
- _hasTextDirectionOverrides |= run.Style.TextDirection != TextDirection.Auto;
- // Can run be coalesced?
- if (run.Style == prev.Style)
- {
- // Yes
- prev.Length += run.Length;
- StyleRun.Pool.Value.Return(run);
- _styleRuns.RemoveAt(i);
- i--;
- }
- else
- {
- // No, move on..
- prev = run;
- }
- }
- }
- /// <summary>
- /// Called whenever the content of this styled text block changes
- /// </summary>
- protected virtual void OnChanged()
- {
- }
- /// <summary>
- /// All code points as supplied by user, accumulated into a single buffer
- /// </summary>
- protected Utf32Buffer _codePoints = new Utf32Buffer();
- /// <summary>
- /// A list of style runs, as supplied by user
- /// </summary>
- protected List<StyleRun> _styleRuns = new List<StyleRun>();
- /// <summary>
- /// Set to true if any style runs have a directionality override.
- /// </summary>
- protected bool _hasTextDirectionOverrides = false;
- /// <inheritdoc />
- public override string ToString()
- {
- return Utf32Utils.FromUtf32(CodePoints.AsSlice());
- }
- }
- }
|