Files
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

726 lines
20 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() );
}
}
}