diff --git a/src/Avalonia.Base/Media/FontFamily.cs b/src/Avalonia.Base/Media/FontFamily.cs index aa2ce05bd4..a34321ef98 100644 --- a/src/Avalonia.Base/Media/FontFamily.cs +++ b/src/Avalonia.Base/Media/FontFamily.cs @@ -131,53 +131,55 @@ namespace Avalonia.Media { var result = new FrugalStructList(1); - var segments = name.Split(','); - - for (int i = 0; i < segments.Length; i++) + int commaIndex = -1; + do { - var segment = segments[i]; - var innerSegments = segment.Split('#'); + // Look for a comma separator to carve out a single segment. + int segmentStart = commaIndex + 1; + commaIndex = name.IndexOf(',', segmentStart); + int segmentEnd = commaIndex == -1 + ? name.Length + : commaIndex; + + var segment = name.AsSpan(segmentStart..segmentEnd).Trim(); - FontSourceIdentifier identifier = new FontSourceIdentifier(name, null); + FontSourceIdentifier? identifier = null; - switch (innerSegments.Length) + // Check if there is exactly one '#' (i.e., segment is in the format "path#innerName"). + int separatorIndex = segment.IndexOf('#'); + if (separatorIndex != -1 && segment[(separatorIndex + 1)..].IndexOf('#') == -1) { - case 1: + var pathSpan = segment[..separatorIndex].Trim(); + var innerName = segment[(separatorIndex + 1)..].Trim(); + + if (pathSpan.IsEmpty) + { + identifier = new FontSourceIdentifier(innerName.ToString(), null); + } + else + { + string path = pathSpan.ToString(); + if (pathSpan.Contains('/') && Uri.TryCreate(path, UriKind.Relative, out var source)) { - identifier = new FontSourceIdentifier(innerSegments[0].Trim(), null); - break; + identifier = new FontSourceIdentifier(innerName.ToString(), source); } - - case 2: + else { - var path = innerSegments[0].Trim(); - var innerName = innerSegments[1].Trim(); - - if (string.IsNullOrEmpty(path)) - { - identifier = new FontSourceIdentifier(innerName, null); - } - else + if (Uri.TryCreate(path, UriKind.Absolute, out source)) { - if (path.Contains('/') && Uri.TryCreate(path, UriKind.Relative, out var source)) - { - identifier = new FontSourceIdentifier(innerName, source); - } - else - { - if (Uri.TryCreate(path, UriKind.Absolute, out source)) - { - identifier = new FontSourceIdentifier(innerName, source); - } - } + identifier = new FontSourceIdentifier(innerName.ToString(), source); } - - break; } + } } - result.Add(identifier); - } + // If we didn't manage to match it to any known format, treat the entire segment as the font name. + identifier ??= new FontSourceIdentifier( + segment.Length == name.Length ? name : segment.ToString(), + null); + + result.Add(identifier.Value); + } while (commaIndex != -1); return result; } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDi.trie.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDi.trie.cs index 47a3c3ccab..e3ddfc2b1b 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDi.trie.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDi.trie.cs @@ -16,10 +16,18 @@ internal static class BidiTrie { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Data, 0x00100000, 0x00000000); - } + } - private static ReadOnlySpan Data => new uint[] - { + // For Debug builds, we store the data in a separate uint[] field to avoid RuntimeFieldInfoStub allocations. + // See also: https://github.com/AvaloniaUI/Avalonia/pull/20175 + +#if DEBUG + public static ReadOnlySpan Data => s_data; + private static uint[] s_data = +#else + public static ReadOnlySpan Data => +#endif + [ 0x0000038D, 0x00000395, 0x0000039D, 0x000003A5, 0x000003BD, 0x000003C5, 0x000003CD, 0x000003D5, 0x000003AD, 0x000003B5, 0x000003AD, 0x000003B5, 0x000003AD, 0x000003B5, 0x000003AD, 0x000003B5, 0x000003AD, 0x000003B5, 0x000003AD, 0x000003B5, 0x000003DB, 0x000003E3, 0x000003EB, 0x000003F3, 0x000003FB, 0x00000403, 0x000003FF, 0x00000407, 0x0000040F, 0x00000417, 0x00000412, 0x0000041A, 0x000003AD, 0x000003B5, 0x000003AD, 0x000003B5, @@ -1104,5 +1112,5 @@ internal static class BidiTrie 0x00000000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00000000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00040000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000, 0x00000000 - }; + ]; } diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/EastAsianWidth.trie.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/EastAsianWidth.trie.cs index d0ddce660a..f951e31330 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/EastAsianWidth.trie.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/EastAsianWidth.trie.cs @@ -18,7 +18,7 @@ internal static class EastAsianWidthTrie get => new(Data, 0x00110000, 0x00000000); } - private static ReadOnlySpan Data => new uint[] + private static readonly uint[] Data = new uint[] { 0x000003E7, 0x000003EF, 0x000003F7, 0x000003FF, 0x0000041F, 0x00000427, 0x0000042F, 0x00000437, 0x0000043F, 0x00000447, 0x0000044F, 0x00000457, 0x00000417, 0x0000041F, 0x0000045C, 0x00000464, 0x00000417, 0x0000041F, 0x00000468, 0x00000470, 0x00000417, 0x0000041F, 0x00000477, 0x0000047F, diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeBreak.trie.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeBreak.trie.cs index a9b2e92fbe..c901123591 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeBreak.trie.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/GraphemeBreak.trie.cs @@ -18,7 +18,7 @@ internal static class GraphemeBreakTrie get => new(Data, 0x000E1000, 0x00000000); } - private static ReadOnlySpan Data => new uint[] + private static readonly uint[] Data = new uint[] { 0x0000035A, 0x00000362, 0x0000036A, 0x00000372, 0x0000038A, 0x00000392, 0x00000362, 0x0000036A, 0x00000362, 0x0000036A, 0x00000362, 0x0000036A, 0x00000362, 0x0000036A, 0x00000362, 0x0000036A, 0x00000362, 0x0000036A, 0x00000362, 0x0000036A, 0x00000362, 0x0000036A, 0x00000362, 0x0000036A, diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index 5b8bf0673c..02b8cfcd06 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -12,7 +12,7 @@ namespace Avalonia.Media.TextFormatting.Unicode private static readonly BreakUnit s_eot = new() { EndOfText = true }; public readonly ReadOnlySpan _text; - private readonly LineBreakState _state; + private LineBreakState _state; public LineBreakEnumerator(ReadOnlySpan text) { @@ -20,7 +20,7 @@ namespace Avalonia.Media.TextFormatting.Unicode _state = new LineBreakState(); } - public readonly bool MoveNext([NotNullWhen(true)] out LineBreak lineBreak) + public bool MoveNext([NotNullWhen(true)] out LineBreak lineBreak) { lineBreak = default; @@ -35,7 +35,7 @@ namespace Avalonia.Media.TextFormatting.Unicode { _state.Read(_text); - result = ExecuteRules(_text, _state); + result = ExecuteRules(_text, ref _state); } if (result == null) @@ -120,11 +120,11 @@ namespace Avalonia.Media.TextFormatting.Unicode return from; } - private static LineBreak? ExecuteRules(ReadOnlySpan text, LineBreakState state) + private static LineBreak? ExecuteRules(ReadOnlySpan text, ref LineBreakState state) { foreach (var rule in s_rules) { - var res = rule.Invoke(text, state); + var res = rule.Invoke(text, ref state); switch (res) { @@ -143,7 +143,7 @@ namespace Avalonia.Media.TextFormatting.Unicode return null; } - private static RuleResult QuotationAndRegionalIndicator(ReadOnlySpan text, LineBreakState state) + private static RuleResult QuotationAndRegionalIndicator(ReadOnlySpan text, ref LineBreakState state) { if (state.Current.Inherited) { @@ -176,7 +176,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB3: Always break at the end of text. /// - private static RuleResult LB03(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB03(ReadOnlySpan text, ref LineBreakState state) { if (state.Current.EndOfText) { @@ -189,7 +189,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB4: Always break after hard line breaks. /// - private static RuleResult LB04(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB04(ReadOnlySpan text, ref LineBreakState state) { // BK ! if (state.Current.LineBreakClass == LineBreakClass.MandatoryBreak) @@ -203,7 +203,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB5: Treat CR followed by LF, as well as CR, LF, and NL as hard line /// - private static RuleResult LB05(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB05(ReadOnlySpan text, ref LineBreakState state) { switch (state.Current.LineBreakClass) { @@ -227,7 +227,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// LB6: Do not break before hard line breaks. /// /// - private static RuleResult LB06(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB06(ReadOnlySpan text, ref LineBreakState state) { // × ( BK | CR | LF | NL ) if (IsBreakClass(state.Next(text).LineBreakClass)) @@ -241,7 +241,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB7: Do not break before spaces or zero width space. /// - private static RuleResult LB07(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB07(ReadOnlySpan text, ref LineBreakState state) { // × SP // × ZW @@ -261,7 +261,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB8: Break before any character following a zero-width space, even if one or more spaces intervene. /// - private static RuleResult LB08(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB08(ReadOnlySpan text, ref LineBreakState state) { if (state.LastBeforeSpace.LineBreakClass == LineBreakClass.ZWSpace && state.Next(text).LineBreakClass != LineBreakClass.Space) { @@ -274,7 +274,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB8a: Do not break after a zero width joiner. /// - private static RuleResult LB08a(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB08a(ReadOnlySpan text, ref LineBreakState state) { // ZWJ × if (state.Current.LineBreakClass == LineBreakClass.ZWJ) @@ -290,7 +290,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// treat it as if it has the line breaking class of the base character in all of the following rules. /// Treat ZWJ as if it were CM. /// - private static RuleResult LB09(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB09(ReadOnlySpan text, ref LineBreakState state) { // Treat X (CM | ZWJ)* as if it were X. // where X is any line break class except BK, CR, LF, NL, SP, or ZW. @@ -319,7 +319,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB10: Treat any remaining combining mark or ZWJ as AL. /// - private static RuleResult LB10(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB10(ReadOnlySpan text, ref LineBreakState state) { if (state.Current.LineBreakClass == LineBreakClass.CombiningMark) { @@ -338,7 +338,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB11: Do not break before or after Word joiner and related characters. /// - private static RuleResult LB11(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB11(ReadOnlySpan text, ref LineBreakState state) { if (state.Next(text).LineBreakClass == LineBreakClass.WordJoiner /* × WJ */ || state.Current.LineBreakClass == LineBreakClass.WordJoiner /* WJ × */) @@ -352,7 +352,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB12: Do not break after NBSP and related characters. /// - private static RuleResult LB12(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB12(ReadOnlySpan text, ref LineBreakState state) { // GL × if (state.Current.LineBreakClass == LineBreakClass.Glue) @@ -366,7 +366,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB12a: Do not break before NBSP and related characters, except after spaces and hyphens. /// - private static RuleResult LB12a(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB12a(ReadOnlySpan text, ref LineBreakState state) { // [^SP BA HY] × GL if (state.Next(text).LineBreakClass == LineBreakClass.Glue) @@ -388,7 +388,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB13: Do not break before ‘]’ or ‘!’ or ‘;’ or ‘/’, even after spaces. /// - private static RuleResult LB13(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB13(ReadOnlySpan text, ref LineBreakState state) { // × CL // × CP @@ -409,7 +409,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB14: Do not break after ‘[’, even after spaces. /// - private static RuleResult LB14(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB14(ReadOnlySpan text, ref LineBreakState state) { // OP SP* × if (state.LastBeforeWhitespace.LineBreakClass == LineBreakClass.OpenPunctuation) @@ -424,7 +424,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// LB15a: Do not break after an unresolved initial punctuation that lies at the start of the line, /// after a space, after opening punctuation, or after an unresolved quotation mark, even after spaces. /// - private static RuleResult LB15a(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB15a(ReadOnlySpan text, ref LineBreakState state) { // (sot | BK | CR | LF | NL | OP | QU | GL | SP | ZW) [\p{Pi}&QU] SP* × if (state.Quotation > 0 && state.LastBeforeWhitespace.Codepoint.GeneralCategory == GeneralCategory.InitialPunctuation && @@ -487,7 +487,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// LB15b: Do not break before an unresolved final punctuation that lies at the end of the line, /// before a space, before a prohibited break, or before an unresolved quotation mark, even after spaces. /// - private static RuleResult LB15b(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB15b(ReadOnlySpan text, ref LineBreakState state) { // × [\p{Pf}&QU] ( SP | GL | WJ | CL | QU | CP | EX | IS | SY | BK | CR | LF | NL | ZW | eot) if (state.Next(text).Codepoint.GeneralCategory == GeneralCategory.FinalPunctuation && (state.Next(text).LineBreakClass == LineBreakClass.Quotation)) @@ -526,7 +526,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB15c: Break before a decimal mark that follows a space, for instance, in ‘subtract .5’. /// - private static RuleResult LB15c(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB15c(ReadOnlySpan text, ref LineBreakState state) { // SP ÷ IS NU if (state.Current.LineBreakClass == LineBreakClass.Space) @@ -546,7 +546,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB15d: Otherwise, do not break before ‘;’, ‘,’, or ‘.’, even after spaces. /// - private static RuleResult LB15d(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB15d(ReadOnlySpan text, ref LineBreakState state) { // × IS if (state.Next(text).LineBreakClass == LineBreakClass.InfixNumeric) @@ -562,7 +562,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// LB16: Do not break between closing punctuation and a nonstarter (lb=NS), /// even with intervening spaces. /// - private static RuleResult LB16(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB16(ReadOnlySpan text, ref LineBreakState state) { switch (state.LastBeforeWhitespace.LineBreakClass) { @@ -590,7 +590,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB17: Do not break within ‘——’, even with intervening spaces. /// - private static RuleResult LB17(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB17(ReadOnlySpan text, ref LineBreakState state) { // B2 SP* × B2 if (state.LastBeforeWhitespace.LineBreakClass == LineBreakClass.BreakBoth) @@ -607,7 +607,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB18: Break after spaces. /// - private static RuleResult LB18(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB18(ReadOnlySpan text, ref LineBreakState state) { // SP ÷ if (state.Current.LineBreakClass == LineBreakClass.Space) @@ -621,7 +621,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB19: Do not break before or after quotation marks, such as ‘ ” ’. /// - private static RuleResult LB19(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB19(ReadOnlySpan text, ref LineBreakState state) { var next = state.Next(text); @@ -677,7 +677,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB20: Break before and after unresolved CB. /// - private static RuleResult LB20(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB20(ReadOnlySpan text, ref LineBreakState state) { // ÷ CB // CB ÷ @@ -692,7 +692,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB20a: Do not break after a word-initial hyphen. /// - private static RuleResult LB20a(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB20a(ReadOnlySpan text, ref LineBreakState state) { // (sot | BK | CR | LF | NL | SP | ZW | CB | GL)(HY | [\u2010]) × AL if (IsMatch(state.Previous) && state.Next(text).LineBreakClass == LineBreakClass.Alphabetic) @@ -733,7 +733,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB21: Do not break before hyphen-minus, other hyphens, fixed-width spaces, small kana, and other non-starters, or after acute accents. /// - private static RuleResult LB21(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB21(ReadOnlySpan text, ref LineBreakState state) { // × (BA | HY | NS) switch (state.Next(text).LineBreakClass) @@ -760,7 +760,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB21a: Don't break after Hebrew + Hyphen. /// - private static RuleResult LB21a(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB21a(ReadOnlySpan text, ref LineBreakState state) { if(state.Next(text).LineBreakClass != LineBreakClass.HebrewLetter) { @@ -778,7 +778,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB21b: Don’t break between Solidus and Hebrew letters. /// - private static RuleResult LB21b(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB21b(ReadOnlySpan text, ref LineBreakState state) { // [21.2] SY × HL if ((state.Current.LineBreakClass == LineBreakClass.BreakSymbols) && (state.Next(text).LineBreakClass == LineBreakClass.HebrewLetter)) @@ -792,7 +792,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB22: Do not break before ellipses. /// - private static RuleResult LB22(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB22(ReadOnlySpan text, ref LineBreakState state) { // × IN if (state.Next(text).LineBreakClass == LineBreakClass.Inseparable) @@ -806,7 +806,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB23: Do not break between digits and letters. /// - private static RuleResult LB23(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB23(ReadOnlySpan text, ref LineBreakState state) { switch (state.Current.LineBreakClass) { @@ -842,7 +842,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// LB23a: Do not break between numeric prefixes and ideographs, or between /// ideographs and numeric postfixes. /// - private static RuleResult LB23a(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB23a(ReadOnlySpan text, ref LineBreakState state) { // PR × (ID | EB | EM) if ((state.Current.LineBreakClass == LineBreakClass.PrefixNumeric) @@ -878,7 +878,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// LB24: Do not break between numeric prefix/postfix and letters, or between /// letters and prefix/postfix. /// - private static RuleResult LB24(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB24(ReadOnlySpan text, ref LineBreakState state) { // (PR | PO) × (AL | HL) if (state.Current.LineBreakClass is LineBreakClass.PrefixNumeric or LineBreakClass.PostfixNumeric @@ -900,7 +900,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB25: Do not break between the following pairs of classes relevant to numbers /// - private static RuleResult LB25(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB25(ReadOnlySpan text, ref LineBreakState state) { switch (state.Next(text).LineBreakClass) { @@ -1128,7 +1128,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB26: Do not break a Korean syllable. /// - private static RuleResult LB26(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB26(ReadOnlySpan text, ref LineBreakState state) { switch (state.Current.LineBreakClass) { @@ -1176,7 +1176,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB27: Treat a Korean Syllable Block the same as ID. /// - private static RuleResult LB27(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB27(ReadOnlySpan text, ref LineBreakState state) { switch (state.Current.LineBreakClass) { @@ -1216,7 +1216,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB28: Do not break between alphabetics (“at”). /// - private static RuleResult LB28(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB28(ReadOnlySpan text, ref LineBreakState state) { // [28.0] (AL | HL) × (AL | HL) switch (state.Current.LineBreakClass) @@ -1243,7 +1243,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB28a: Do not break inside the orthographic syllables of Brahmic scripts. /// - private static RuleResult LB28a(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB28a(ReadOnlySpan text, ref LineBreakState state) { // [28.11] AP × (AK | DottedCircle | AS) if ((state.Current.LineBreakClass == LineBreakClass.AksaraPrebase) && isMatch(state.Next(text))) @@ -1286,7 +1286,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB29: Do not break between numeric punctuation and alphabetics (“e.g.”). /// - private static RuleResult LB29(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB29(ReadOnlySpan text, ref LineBreakState state) { // IS × (AL | HL) if ((state.Current.LineBreakClass == LineBreakClass.InfixNumeric) @@ -1301,7 +1301,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB30: Do not break between letters, numbers, or ordinary symbols and opening or closing parentheses. /// - private static RuleResult LB30(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB30(ReadOnlySpan text, ref LineBreakState state) { switch (state.Current.LineBreakClass) { @@ -1346,7 +1346,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// break. /// /// - private static RuleResult LB30a(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB30a(ReadOnlySpan text, ref LineBreakState state) { if (state.RegionalIndicator > 0 && state.Next(text).LineBreakClass == LineBreakClass.RegionalIndicator) { @@ -1363,7 +1363,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// LB30b: Do not break between an emoji base (or potential emoji) and an emoji modifier. /// /// - private static RuleResult LB30b(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB30b(ReadOnlySpan text, ref LineBreakState state) { // EB × EM if ((state.Current.LineBreakClass == LineBreakClass.EBase) && (state.Next(text).LineBreakClass == LineBreakClass.EModifier)) @@ -1392,7 +1392,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// LB31: Break everywhere else. /// - private static RuleResult LB31(ReadOnlySpan text, LineBreakState state) + private static RuleResult LB31(ReadOnlySpan text, ref LineBreakState state) { return RuleResult.MayBreak; } @@ -1491,7 +1491,7 @@ namespace Avalonia.Media.TextFormatting.Unicode } } - private class LineBreakState + private ref struct LineBreakState { private BreakUnit? _next; private BreakUnit _previous; @@ -1651,7 +1651,7 @@ namespace Avalonia.Media.TextFormatting.Unicode } } - private delegate RuleResult BreakUnitDelegate(ReadOnlySpan text, LineBreakState state); + private delegate RuleResult BreakUnitDelegate(ReadOnlySpan text, ref LineBreakState state); private static readonly BreakUnitDelegate[] s_rules = [ QuotationAndRegionalIndicator, diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeData.trie.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeData.trie.cs index b84003a646..16a797f4af 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeData.trie.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/UnicodeData.trie.cs @@ -16,10 +16,18 @@ internal static class UnicodeDataTrie { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Data, 0x00100000, 0x00000000); - } - - private static ReadOnlySpan Data => new uint[] - { + } + + // For Debug builds, we store the data in a separate uint[] field to avoid RuntimeFieldInfoStub allocations. + // See also: https://github.com/AvaloniaUI/Avalonia/pull/20175 + +#if DEBUG + public static ReadOnlySpan Data => s_data; + private static uint[] s_data = +#else + public static ReadOnlySpan Data => +#endif + [ 0x00000482, 0x0000048A, 0x00000492, 0x0000049A, 0x000004B2, 0x000004BA, 0x000004C2, 0x000004CA, 0x000004D2, 0x000004DA, 0x000004E0, 0x000004E8, 0x000004F0, 0x000004F8, 0x00000500, 0x00000508, 0x0000050E, 0x00000516, 0x0000051E, 0x00000526, 0x00000529, 0x00000531, 0x00000539, 0x00000541, 0x00000549, 0x00000551, 0x00000556, 0x0000055E, 0x00000566, 0x0000056E, 0x00000573, 0x0000057B, 0x00000583, 0x0000058B, 0x0000058F, 0x00000597, @@ -2351,5 +2359,5 @@ internal static class UnicodeDataTrie 0x00038061, 0x00038061, 0x00038061, 0x00038061, 0x00038061, 0x00074061, 0x00074061, 0x00074061, 0x00038061, 0x00074061, 0x00074061, 0x00074061, 0x00074061, 0x00074061, 0x00074061, 0x00074061, 0x00074061, 0x00074061, 0x00074061, 0x00074061, 0x00074061, 0x00074061, 0x00038061, 0x00038061, 0x00000000, 0x00000000, 0x00000000, 0x00000000 - }; + ]; } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index efce67e90b..0cc1a5d2aa 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -43,7 +43,10 @@ namespace Avalonia.Skia var usedCulture = culture ?? CultureInfo.CurrentCulture; - buffer.Language = s_cachedLanguage.GetOrAdd(usedCulture.LCID, _ => new Language(usedCulture)); + buffer.Language = s_cachedLanguage.GetOrAdd( + usedCulture.LCID, + static (_, culture) => new Language(culture), + usedCulture); var font = ((GlyphTypefaceImpl)typeface).Font; diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs index 3a0255f564..3b558e600b 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/UnicodeDataGenerator.cs @@ -67,7 +67,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting public static void GenerateTrieClass(string name, UnicodeTrie trie) { - using var fileStream = File.Create($"Generated\\{name}.trie.cs"); + using var fileStream = File.Create(Path.Combine("Generated", $"{name}.trie.cs")); using var writer = new StreamWriter(fileStream); writer.Write( @@ -90,10 +90,18 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new(Data, 0x{{trie.HighStart:X8}}, 0x{{trie.ErrorValue:X8}}); - } - - private static ReadOnlySpan Data => new uint[] - { + } + + // For Debug builds, we store the data in a separate uint[] field to avoid RuntimeFieldInfoStub allocations. + // See also: https://github.com/AvaloniaUI/Avalonia/pull/20175 + + #if DEBUG + public static ReadOnlySpan Data => s_data; + private static uint[] s_data = + #else + public static ReadOnlySpan Data => + #endif + [ """); for (int i = 0; i < trie.Data.Length; ++i) @@ -114,7 +122,7 @@ namespace Avalonia.Base.UnitTests.Media.TextFormatting writer.Write( """ - }; + ]; } """);