// 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 { /// /// Represents a block of formatted, laid out and measurable text /// public class StyledText { /// /// Constructor /// public StyledText() { } /// /// Constructs a styled text block from unstyled text /// /// public StyledText(Slice codePoints) { AddText(codePoints, null); } /// /// Clear the content of this text block /// public virtual void Clear() { // Reset everything _codePoints.Clear(); StyleRun.Pool.Value.ReturnAndClear(_styleRuns); _hasTextDirectionOverrides = false; OnChanged(); } /// /// The length of the added text in code points /// public int Length => _codePoints.Length; /// /// Get the code points of this text block /// public Utf32Buffer CodePoints => _codePoints; /// /// Get the text runs as added by AddText /// public IReadOnlyList StyleRuns { get { return _styleRuns; } } /// /// Converts a code point index to a character index /// /// The code point index to convert /// The converted index public int CodePointToCharacterIndex(int codePointIndex) { return _codePoints.Utf32OffsetToUtf16Offset(codePointIndex); } /// /// Converts a character index to a code point index /// /// The character index to convert /// The converted index public int CharacterToCodePointIndex(int characterIndex) { return _codePoints.Utf16OffsetToUtf32Offset(characterIndex); } /// /// Add text to this text block /// /// /// 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 /// and methods /// /// The text to add /// The style of the text public void AddText(ReadOnlySpan 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(); } /// /// Add text to this paragraph /// /// The text to add /// The style of the text public void AddText(Slice 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(); } /// /// Add text to this text block /// /// /// 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 /// and methods /// /// The text to add /// The style of the text public void AddText(string text, IStyle style) { AddText(text.AsSpan(), style); } /// /// Add all the text from another text block to this text block /// /// Text to add public void AddText(StyledText text) { foreach (var sr in text.StyleRuns) { AddText(sr.CodePoints, sr.Style); } } /// /// Add all the text from another text block to this text block /// /// The position at which to insert the text /// Text to add public void InsertText(int offset, StyledText text) { foreach (var sr in text.StyleRuns) { InsertText(offset, sr.CodePoints, sr.Style); offset += sr.CodePoints.Length; } } /// /// Add text to this text block /// /// /// 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 /// /// The position to insert the text /// The text to add /// The style of the text (optional) public void InsertText(int position, Slice 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); } /// /// Add text to this text block /// /// /// 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 /// /// The position to insert the text /// The text to add /// The style of the text (optional) public void InsertText(int position, ReadOnlySpan 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); } /// /// Add text to this text block /// /// /// 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 /// /// The position to insert the text /// The text to add /// The style of the text (optional) 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); } /// /// Deletes text from this text block /// /// The code point index to delete from /// The number of code points to delete 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(); } /// /// Overwrites the styles of existing text in the text block /// /// The code point index of the start of the text /// The length of the text /// The new style to be applied 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(); } /// /// Extract text from this styled text block /// /// The code point offset to extract from /// The number of code points to extract /// A new text block with the RHS split part of the text 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; } /// /// Gets the style of the text at a specified offset /// /// /// When on a style run boundary, returns the style of the preceeding run /// /// The code point offset in the text /// An IStyle 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; } /// /// Completes the insertion of text by inserting it's style run /// and updating the offsets of existing style runs. /// /// The utf32 slice that was inserted /// The style of the inserted text void FinishInsert(Slice 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(); } /// /// Combines any consecutive style runs with the same style /// into a single run /// 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; } } } /// /// Called whenever the content of this styled text block changes /// protected virtual void OnChanged() { } /// /// All code points as supplied by user, accumulated into a single buffer /// protected Utf32Buffer _codePoints = new Utf32Buffer(); /// /// A list of style runs, as supplied by user /// protected List _styleRuns = new List(); /// /// Set to true if any style runs have a directionality override. /// protected bool _hasTextDirectionOverrides = false; /// public override string ToString() { return Utf32Utils.FromUtf32(CodePoints.AsSlice()); } } }