726 строки
25 KiB
C#
726 строки
25 KiB
C#
|
|
// 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());
|
|
}
|
|
}
|
|
}
|