Files
sbox-public/engine/ThirdParty/Topten.RichTextKit/FontFallback/FontFallback.cs
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

191 lines
6.1 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 Topten.RichTextKit.Utils;
namespace Topten.RichTextKit
{
/// <summary>
/// Helper to split a run of code points based on a particular typeface
/// into a series of runs where unsupported code points are mapped to a
/// fallback font.
/// </summary>
public class FontFallback
{
/// <summary>
/// Specified details about a font fallback run
/// </summary>
public struct Run
{
/// <summary>
/// The starting code point index of this run
/// </summary>
public int Start;
/// <summary>
/// The length of this run in code points
/// </summary>
public int Length;
/// <summary>
/// The typeface to be used for this run
/// </summary>
public SKTypeface Typeface;
}
/// <summary>
/// Specifies the instance of the character matcher to be used for font fallback
/// </summary>
/// <remarks>
/// This instance is shared by all TextBlock instances and should be thread safe
/// if used in a multi-threaded environment.
/// </remarks>
public static ICharacterMatcher CharacterMatcher = new DefaultCharacterMatcher();
/// <summary>
/// Splits a sequence of code points into a series of runs with font fallback applied
/// </summary>
/// <param name="codePoints">The code points</param>
/// <param name="typeface">The preferred typeface</param>
/// <param name="replacementCharacter">The replacement character to be used for the run</param>
/// <returns>A sequence of runs with unsupported code points replaced by a selected font fallback</returns>
public static IEnumerable<Run> GetFontRuns( Slice<int> codePoints, SKTypeface typeface, char replacementCharacter = '\0' )
{
var font = new SKFont( typeface );
if ( replacementCharacter != '\0' )
{
var glyph = font.GetGlyph( replacementCharacter );
if ( glyph == 0 )
{
var fallbackTypeface = CharacterMatcher.MatchCharacter( typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, replacementCharacter );
if ( fallbackTypeface != null )
typeface = fallbackTypeface;
}
yield return new Run()
{
Start = 0,
Length = codePoints.Length,
Typeface = typeface,
};
yield break;
}
// Get glyphs using the top-level typeface
var glyphs = new ushort[codePoints.Length];
font.GetGlyphs( codePoints.AsSpan(), glyphs );
// Look for subspans that need font fallback (where glyphs are zero)
int runStart = 0;
for ( int i = 0; i < codePoints.Length; i++ )
{
// Do we need fallback for this character?
if ( glyphs[i] == 0 )
{
// Check if there's a fallback available, if not, might as well continue with the current top-level typeface
var subSpanTypeface = CharacterMatcher.MatchCharacter( typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, codePoints[i] );
if ( subSpanTypeface == null )
continue;
// Don't fallback for whitespace characters
if ( UnicodeClasses.BoundaryGroup( codePoints[i] ) == WordBoundaryClass.Space )
continue;
// Must be a cluster boundary
if ( !GraphemeClusterAlgorithm.IsBoundary( codePoints, i ) )
continue;
// We can do font fallback...
// Flush the current top-level run
if ( i > runStart )
{
yield return new Run()
{
Start = runStart,
Length = i - runStart,
Typeface = typeface,
};
}
// Count how many unmatched characters
var unmatchedStart = i;
var unmatchedEnd = i + 1;
while ( unmatchedEnd < codePoints.Length &&
(glyphs[unmatchedEnd] == 0 || !GraphemeClusterAlgorithm.IsBoundary( codePoints, unmatchedEnd )) )
{
unmatchedEnd++;
}
var unmatchedLength = unmatchedEnd - unmatchedStart;
// Match the missing characters
while ( unmatchedLength > 0 )
{
// Find the font fallback using the first character
subSpanTypeface = CharacterMatcher.MatchCharacter( typeface.FamilyName, typeface.FontWeight, typeface.FontWidth, typeface.FontSlant, null, codePoints[unmatchedStart] );
if ( subSpanTypeface == null )
{
unmatchedEnd = unmatchedStart;
break;
}
var subSpanFont = new SKFont( subSpanTypeface );
// Get the glyphs over the current unmatched range
subSpanFont.GetGlyphs( codePoints.SubSlice( unmatchedStart, unmatchedLength ).AsSpan(), new Span<ushort>( glyphs, unmatchedStart, unmatchedLength ) );
// Count how many characters were matched
var fallbackStart = unmatchedStart;
var fallbackEnd = unmatchedStart + 1;
while ( fallbackEnd < unmatchedEnd && glyphs[fallbackEnd] != 0 )
fallbackEnd++;
var fallbackLength = fallbackEnd - fallbackStart;
// Yield this font fallback run
yield return new Run()
{
Start = fallbackStart,
Length = fallbackLength,
Typeface = subSpanTypeface,
};
// Continue selecting font fallbacks until the entire unmatched ranges has been matched
unmatchedStart += fallbackLength;
unmatchedLength -= fallbackLength;
}
// Move onto the next top level span
i = unmatchedEnd - 1; // account for i++ on for loop
runStart = unmatchedEnd;
}
}
// Flush find run
if ( codePoints.Length > runStart )
{
yield return new Run()
{
Start = runStart,
Length = codePoints.Length - runStart,
Typeface = typeface,
};
}
}
}
}