diff --git a/src/Uno.UI.Tests/Windows_UI_Xaml_Media/Given_FontMatching.cs b/src/Uno.UI.Tests/Windows_UI_Xaml_Media/Given_FontMatching.cs new file mode 100644 index 000000000000..990fa7c4aa06 --- /dev/null +++ b/src/Uno.UI.Tests/Windows_UI_Xaml_Media/Given_FontMatching.cs @@ -0,0 +1,534 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Uno.UI.Xaml.Media; +using Windows.UI.Text; + +namespace Uno.UI.Tests.Windows_UI_Xaml_Media; + +[TestClass] +public class Given_FontManifest +{ + [TestMethod] + public void When_Stretch_Matches_Exactly() + { + FontInfo[] infos = + [ + new FontInfo() + { + FamilyName = "ms-appx:///test1.ttf", + FontStretch = FontStretch.ExtraExpanded, + FontStyle = FontStyle.Italic, + FontWeight = FontWeights.Bold.Weight, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = FontWeights.Normal.Weight, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test3.ttf", + FontStretch = FontStretch.SemiExpanded, + FontStyle = FontStyle.Normal, + FontWeight = FontWeights.Normal.Weight, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test4.ttf", + FontStretch = FontStretch.SemiCondensed, + FontStyle = FontStyle.Normal, + FontWeight = FontWeights.Normal.Weight, + }, + ]; + + PermuteAndAssert(infos, manifest => + { + var actual = FontManifestHelpers.GetFamilyNameFromManifest(manifest, FontWeights.Normal, FontStyle.Normal, FontStretch.ExtraExpanded); + Assert.AreEqual("ms-appx:///test1.ttf", actual); + }); + } + + [TestMethod] + public void When_Stretch_Is_Expanded_Prefer_Expanded() + { + FontInfo[] infos = + [ + new FontInfo() + { + FamilyName = "ms-appx:///test1.ttf", + FontStretch = FontStretch.ExtraExpanded, + FontStyle = FontStyle.Italic, + FontWeight = FontWeights.Bold.Weight, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.UltraExpanded, + FontStyle = FontStyle.Normal, + FontWeight = FontWeights.Normal.Weight, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = FontWeights.Normal.Weight, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test3.ttf", + FontStretch = FontStretch.SemiCondensed, + FontStyle = FontStyle.Normal, + FontWeight = FontWeights.Normal.Weight, + }, + ]; + + PermuteAndAssert(infos, manifest => + { + var actual = FontManifestHelpers.GetFamilyNameFromManifest(manifest, FontWeights.Normal, FontStyle.Normal, FontStretch.SemiExpanded); + Assert.AreEqual("ms-appx:///test1.ttf", actual); + }); + } + + [TestMethod] + public void When_Style_Matches_Exactly() + { + FontInfo[] infos = + [ + new FontInfo() + { + FamilyName = "ms-appx:///test1.ttf", + FontStretch = FontStretch.SemiExpanded, + FontStyle = FontStyle.Italic, + FontWeight = FontWeights.Bold.Weight, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.SemiExpanded, + FontStyle = FontStyle.Normal, + FontWeight = FontWeights.Normal.Weight, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test3.ttf", + FontStretch = FontStretch.SemiExpanded, + FontStyle = FontStyle.Oblique, + FontWeight = FontWeights.Bold.Weight, + }, + ]; + + PermuteAndAssert(infos, manifest => + { + var actual = FontManifestHelpers.GetFamilyNameFromManifest(manifest, FontWeights.Normal, FontStyle.Italic, FontStretch.ExtraExpanded); + Assert.AreEqual("ms-appx:///test1.ttf", actual); + + actual = FontManifestHelpers.GetFamilyNameFromManifest(manifest, FontWeights.Normal, FontStyle.Oblique, FontStretch.ExtraExpanded); + Assert.AreEqual("ms-appx:///test3.ttf", actual); + + actual = FontManifestHelpers.GetFamilyNameFromManifest(manifest, FontWeights.Normal, FontStyle.Normal, FontStretch.ExtraExpanded); + Assert.AreEqual("ms-appx:///test2.ttf", actual); + }); + } + + [TestMethod] + public void When_Style_Not_Matched_Requested_Oblique() + { + FontInfo[] infos = + [ + new FontInfo() + { + FamilyName = "ms-appx:///test1.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Italic, + FontWeight = FontWeights.Normal.Weight, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = FontWeights.Normal.Weight, + }, + ]; + + PermuteAndAssert(infos, manifest => + { + var actual = FontManifestHelpers.GetFamilyNameFromManifest(manifest, FontWeights.Normal, FontStyle.Oblique, FontStretch.SemiExpanded); + Assert.AreEqual("ms-appx:///test1.ttf", actual); + }); + } + + [TestMethod] + public void When_Style_Not_Matched_Requested_Italic() + { + FontInfo[] infos = + [ + new FontInfo() + { + FamilyName = "ms-appx:///test1.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Oblique, + FontWeight = FontWeights.Normal.Weight, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = FontWeights.Normal.Weight, + }, + ]; + + PermuteAndAssert(infos, manifest => + { + var actual = FontManifestHelpers.GetFamilyNameFromManifest(manifest, FontWeights.Normal, FontStyle.Italic, FontStretch.SemiExpanded); + Assert.AreEqual("ms-appx:///test1.ttf", actual); + }); + } + + [TestMethod] + public void When_Style_Not_Matched_Requested_Normal() + { + FontInfo[] infos = + [ + new FontInfo() + { + FamilyName = "ms-appx:///test1.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Oblique, + FontWeight = FontWeights.Normal.Weight, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Italic, + FontWeight = FontWeights.Normal.Weight, + }, + ]; + + PermuteAndAssert(infos, manifest => + { + var actual = FontManifestHelpers.GetFamilyNameFromManifest(manifest, FontWeights.Normal, FontStyle.Normal, FontStretch.SemiExpanded); + Assert.AreEqual("ms-appx:///test1.ttf", actual); + }); + } + + [TestMethod] + public void When_Weight_Requested_Less_Than_400_Has_Match() + { + FontInfo[] infos = + [ + new FontInfo() + { + FamilyName = "ms-appx:///test1.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 350, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 300, + }, + ]; + + PermuteAndAssert(infos, manifest => + { + var actual = FontManifestHelpers.GetFamilyNameFromManifest(manifest, new FontWeight(350), FontStyle.Normal, FontStretch.Normal); + Assert.AreEqual("ms-appx:///test1.ttf", actual); + }); + } + + [TestMethod] + public void When_Weight_Requested_Less_Than_400_No_Match_Prefer_Lighter() + { + FontInfo[] infos = + [ + new FontInfo() + { + FamilyName = "ms-appx:///test1.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 300, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 400, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test3.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 500, + }, + ]; + + PermuteAndAssert(infos, manifest => + { + var actual = FontManifestHelpers.GetFamilyNameFromManifest(manifest, new FontWeight(350), FontStyle.Normal, FontStretch.Normal); + Assert.AreEqual("ms-appx:///test1.ttf", actual); + }); + } + + [TestMethod] + public void When_Weight_Requested_Less_Than_400_No_Lighter() + { + FontInfo[] infos = + [ + new FontInfo() + { + FamilyName = "ms-appx:///test1.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 370, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 400, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test3.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 500, + }, + ]; + + PermuteAndAssert(infos, manifest => + { + var actual = FontManifestHelpers.GetFamilyNameFromManifest(manifest, new FontWeight(350), FontStyle.Normal, FontStretch.Normal); + Assert.AreEqual("ms-appx:///test1.ttf", actual); + }); + } + + [TestMethod] + public void When_Weight_Requested_Greater_Than_500_Prefer_Bolder() + { + FontInfo[] infos = + [ + new FontInfo() + { + FamilyName = "ms-appx:///test1.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 600, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 540, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test3.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 500, + }, + ]; + + PermuteAndAssert(infos, manifest => + { + var actual = FontManifestHelpers.GetFamilyNameFromManifest(manifest, new FontWeight(550), FontStyle.Normal, FontStretch.Normal); + Assert.AreEqual("ms-appx:///test1.ttf", actual); + }); + } + + [TestMethod] + public void When_Weight_Requested_Greater_Than_500_No_Bolder() + { + FontInfo[] infos = + [ + new FontInfo() + { + FamilyName = "ms-appx:///test1.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 540, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 500, + }, + ]; + + PermuteAndAssert(infos, manifest => + { + var actual = FontManifestHelpers.GetFamilyNameFromManifest(manifest, new FontWeight(550), FontStyle.Normal, FontStretch.Normal); + Assert.AreEqual("ms-appx:///test1.ttf", actual); + }); + } + + [TestMethod] + public void When_Weight_Requested_Is_400_Prefer_500() + { + FontInfo[] infos = + [ + new FontInfo() + { + FamilyName = "ms-appx:///test1.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 500, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 450, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 350, + }, + ]; + + PermuteAndAssert(infos, manifest => + { + var actual = FontManifestHelpers.GetFamilyNameFromManifest(manifest, new FontWeight(400), FontStyle.Normal, FontStretch.Normal); + Assert.AreEqual("ms-appx:///test1.ttf", actual); + }); + } + + [TestMethod] + public void When_Weight_Requested_Is_500_Prefer_400() + { + FontInfo[] infos = + [ + new FontInfo() + { + FamilyName = "ms-appx:///test1.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 400, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 450, + }, + new FontInfo() + { + FamilyName = "ms-appx:///test2.ttf", + FontStretch = FontStretch.Normal, + FontStyle = FontStyle.Normal, + FontWeight = 550, + }, + ]; + + PermuteAndAssert(infos, manifest => + { + var actual = FontManifestHelpers.GetFamilyNameFromManifest(manifest, new FontWeight(500), FontStyle.Normal, FontStretch.Normal); + Assert.AreEqual("ms-appx:///test1.ttf", actual); + }); + } + + [TestMethod] + // Equivalent cases + [DataRow(FontStretch.Normal, FontStretch.Normal, (int)FontManifestHelpers.FontMatchingResult.Equivalent)] + [DataRow(FontStretch.UltraCondensed, FontStretch.UltraCondensed, (int)FontManifestHelpers.FontMatchingResult.Equivalent)] + [DataRow(FontStretch.UltraExpanded, FontStretch.UltraExpanded, (int)FontManifestHelpers.FontMatchingResult.Equivalent)] + + // Prefer condensed over expanded cases + [DataRow(FontStretch.Condensed, FontStretch.SemiExpanded, (int)FontManifestHelpers.FontMatchingResult.CandidateIsBetter)] + + // Prefer condensed that's closest to Normal + [DataRow(FontStretch.SemiCondensed, FontStretch.Condensed, (int)FontManifestHelpers.FontMatchingResult.CandidateIsBetter)] + + // Prefer expanded that's closest to Normal + [DataRow(FontStretch.SemiExpanded, FontStretch.Expanded, (int)FontManifestHelpers.FontMatchingResult.CandidateIsBetter)] + public void When_FontStretch_Requested_Is_Normal(FontStretch candidate, FontStretch bestSoFar, int expectedResult) + { + Assert.AreEqual((FontManifestHelpers.FontMatchingResult)expectedResult, FontManifestHelpers.IsBetterStretch(candidate, bestSoFar, FontStretch.Normal)); + + var reversedExpected = expectedResult switch + { + (int)FontManifestHelpers.FontMatchingResult.CandidateIsBetter => FontManifestHelpers.FontMatchingResult.CandidateIsWorse, + (int)FontManifestHelpers.FontMatchingResult.CandidateIsWorse => FontManifestHelpers.FontMatchingResult.CandidateIsBetter, + _ => FontManifestHelpers.FontMatchingResult.Equivalent + }; + Assert.AreEqual(reversedExpected, FontManifestHelpers.IsBetterStretch(bestSoFar, candidate, FontStretch.Normal)); + } + + [TestMethod] + // Equivalent cases + [DataRow(FontStretch.Normal, FontStretch.Normal, (int)FontManifestHelpers.FontMatchingResult.Equivalent)] + [DataRow(FontStretch.UltraCondensed, FontStretch.UltraCondensed, (int)FontManifestHelpers.FontMatchingResult.Equivalent)] + [DataRow(FontStretch.UltraExpanded, FontStretch.UltraExpanded, (int)FontManifestHelpers.FontMatchingResult.Equivalent)] + + // Prefer narrower + [DataRow(FontStretch.ExtraCondensed, FontStretch.UltraCondensed, (int)FontManifestHelpers.FontMatchingResult.CandidateIsBetter)] + [DataRow(FontStretch.ExtraCondensed, FontStretch.SemiCondensed, (int)FontManifestHelpers.FontMatchingResult.CandidateIsBetter)] + + // No narrower, take nearest wider + [DataRow(FontStretch.SemiCondensed, FontStretch.Normal, (int)FontManifestHelpers.FontMatchingResult.CandidateIsBetter)] + [DataRow(FontStretch.SemiCondensed, FontStretch.SemiExpanded, (int)FontManifestHelpers.FontMatchingResult.CandidateIsBetter)] + [DataRow(FontStretch.Normal, FontStretch.SemiExpanded, (int)FontManifestHelpers.FontMatchingResult.CandidateIsBetter)] + [DataRow(FontStretch.SemiExpanded, FontStretch.Expanded, (int)FontManifestHelpers.FontMatchingResult.CandidateIsBetter)] + public void When_FontStretch_Requested_Is_Condensed(FontStretch candidate, FontStretch bestSoFar, int expectedResult) + { + Assert.AreEqual((FontManifestHelpers.FontMatchingResult)expectedResult, FontManifestHelpers.IsBetterStretch(candidate, bestSoFar, FontStretch.Condensed)); + + var reversedExpected = expectedResult switch + { + (int)FontManifestHelpers.FontMatchingResult.CandidateIsBetter => FontManifestHelpers.FontMatchingResult.CandidateIsWorse, + (int)FontManifestHelpers.FontMatchingResult.CandidateIsWorse => FontManifestHelpers.FontMatchingResult.CandidateIsBetter, + _ => FontManifestHelpers.FontMatchingResult.Equivalent + }; + Assert.AreEqual(reversedExpected, FontManifestHelpers.IsBetterStretch(bestSoFar, candidate, FontStretch.Condensed)); + } + + // https://github.com/dotnet/roslyn/blob/19b5e961ecb97b008106f1b646c077e0bffde4a7/src/Compilers/Core/CodeAnalysisTest/Collections/TemporaryArrayTests.cs#L239 + private static void PermuteAndAssert(FontInfo[] values, Action assert) + { + doPermute(0, values.Length - 1); + + void doPermute(int start, int end) + { + if (start == end) + { + // We have one of our possible n! solutions, + // add it to the list. + assert(new FontManifest() { Fonts = values }); + } + else + { + for (var i = start; i <= end; i++) + { + (values[start], values[i]) = (values[i], values[start]); + doPermute(start + 1, end); + (values[start], values[i]) = (values[i], values[start]); + } + } + } + } +} diff --git a/src/Uno.UI/UI/Xaml/FontManifestHelpers.cs b/src/Uno.UI/UI/Xaml/FontManifestHelpers.cs index 3220178e03de..58b5b6d1cd8c 100644 --- a/src/Uno.UI/UI/Xaml/FontManifestHelpers.cs +++ b/src/Uno.UI/UI/Xaml/FontManifestHelpers.cs @@ -1,6 +1,7 @@ #nullable enable using System; +using System.Diagnostics; using System.IO; using System.Text.Json; using System.Text.Json.Serialization; @@ -10,6 +11,13 @@ namespace Uno.UI.Xaml.Media; internal static class FontManifestHelpers { + internal enum FontMatchingResult + { + CandidateIsBetter, + CandidateIsWorse, + Equivalent, + } + private static readonly JsonSerializerOptions _options = new JsonSerializerOptions() { AllowTrailingCommas = true, @@ -39,24 +47,260 @@ internal static string GetFamilyNameFromManifest(FontManifest? manifest, FontWei for (int i = 1; i < manifest.Fonts.Length; i++) { var candidateMatch = manifest.Fonts[i]; - if (candidateMatch.FontWeight != bestSoFar.FontWeight && Math.Abs(candidateMatch.FontWeight - weight.Weight) < Math.Abs(bestSoFar.FontWeight - weight.Weight)) + var stretchResult = IsBetterStretch(candidateMatch.FontStretch, bestSoFar.FontStretch, stretch); + if (stretchResult == FontMatchingResult.CandidateIsBetter) { - // candidateMatch is a better match than bestSoFar. So it's now our new bestSoFar. bestSoFar = candidateMatch; } - else if (candidateMatch.FontStyle != bestSoFar.FontStyle && candidateMatch.FontStyle == style) + else if (stretchResult == FontMatchingResult.Equivalent) { - // The current bestSoFar - bestSoFar = candidateMatch; + var styleResult = IsBetterStyle(candidateMatch.FontStyle, bestSoFar.FontStyle, style); + if (styleResult == FontMatchingResult.CandidateIsBetter) + { + bestSoFar = candidateMatch; + } + else if (styleResult == FontMatchingResult.Equivalent) + { + var weightResult = IsBetterWeight(candidateMatch.FontWeight, bestSoFar.FontWeight, weight.Weight); + if (weightResult == FontMatchingResult.CandidateIsBetter) + { + bestSoFar = candidateMatch; + } + } + } + } + + return bestSoFar.FamilyName; + } + + private static FontMatchingResult? GenericComparison(int candidate, int bestSoFar, int requested) + { + if (candidate == bestSoFar) + { + return FontMatchingResult.Equivalent; + } + else if (bestSoFar == requested) + { + return FontMatchingResult.CandidateIsWorse; + } + else if (candidate == requested) + { + return FontMatchingResult.CandidateIsBetter; + } + + return null; + } + + + internal static FontMatchingResult IsBetterStretch(FontStretch candidate, FontStretch bestSoFar, FontStretch requestedStretch) + { + // https://www.w3.org/TR/css-fonts-3/#font-style-matching + // SPEC: 'font-stretch' is tried first. If the matching set contains faces with width values + // SPEC: matching the 'font-stretch' value, faces with other width values are removed from the + // SPEC: matching set. If there is no face that exactly matches the width value the nearest width + // SPEC: is used instead. If the value of 'font-stretch' is 'normal' or one of the condensed + // SPEC: values, narrower width values are checked first, then wider values. If the value + // SPEC: of 'font-stretch' is one of the expanded values, wider values are checked first, followed + // SPEC: by narrower values. Once the closest matching width has been determined by this + // SPEC: process, faces with other widths are removed from the matching set. + + var result = GenericComparison((int)candidate, (int)bestSoFar, (int)requestedStretch); + if (result.HasValue) + { + return result.Value; + } + + if (requestedStretch <= FontStretch.Normal) + { + // Prefer narrower + if (candidate < requestedStretch && bestSoFar > requestedStretch) + { + // Candidate is narrower, while bestSoFar is wider + // Candidate is better. + return FontMatchingResult.CandidateIsBetter; } - else if (stretch != FontStretch.Undefined && candidateMatch.FontStretch != bestSoFar.FontStretch && Math.Abs(candidateMatch.FontStretch - stretch) < Math.Abs(bestSoFar.FontStretch - stretch)) + else if (candidate > requestedStretch && bestSoFar < requestedStretch) { - // candidateMatch is a better match than bestSoFar. So it's now our new bestSoFar. - bestSoFar = candidateMatch; + // Candidate is wider, while bestSoFar is narrower. + // bestSoFar was better. + return FontMatchingResult.CandidateIsWorse; + } + else if (candidate > requestedStretch) + { + // Both candidate and bestSoFar are wider from requestedStretch + Debug.Assert(bestSoFar > requestedStretch); + return candidate < bestSoFar ? FontMatchingResult.CandidateIsBetter : FontMatchingResult.CandidateIsWorse; + } + else + { + Debug.Assert(bestSoFar < requestedStretch); + return candidate > bestSoFar ? FontMatchingResult.CandidateIsBetter : FontMatchingResult.CandidateIsWorse; + } + } + else + { + // Prefer wider + if (candidate > requestedStretch && bestSoFar < requestedStretch) + { + // Candidate is wider, while bestSoFar is narrower + // Candidate is better. + return FontMatchingResult.CandidateIsBetter; + } + else if (candidate < requestedStretch && bestSoFar > requestedStretch) + { + // Candidate is narrower, while bestSoFar is wider. + // bestSoFar was better. + return FontMatchingResult.CandidateIsWorse; + } + else if (candidate > requestedStretch) + { + // Both candidate and bestSoFar are wider from requestedStretch + Debug.Assert(bestSoFar > requestedStretch); + return candidate < bestSoFar ? FontMatchingResult.CandidateIsBetter : FontMatchingResult.CandidateIsWorse; + } + else + { + Debug.Assert(bestSoFar < requestedStretch); + return candidate > bestSoFar ? FontMatchingResult.CandidateIsBetter : FontMatchingResult.CandidateIsWorse; } } + } - return bestSoFar.FamilyName; + internal static FontMatchingResult IsBetterStyle(FontStyle candidate, FontStyle bestSoFar, FontStyle requestedStyle) + { + // https://www.w3.org/TR/css-fonts-3/#font-style-matching + // SPEC: 'font-style' is tried next. If the value of 'font-style' is 'italic', italic faces are checked first, then + // SPEC: oblique, then normal faces. If the value is 'oblique', oblique faces are checked first, then italic + // SPEC: faces and then normal faces. If the value is 'normal', normal faces are checked first, then oblique + // SPEC: faces, then italic faces. Faces with other style values are excluded from the matching set. + // SPEC: User agents are permitted to distinguish between italic and oblique faces within platform font families but this + // SPEC: is not required, so all italic or oblique faces may be treated as italic faces. However, within font families + // SPEC: defined via @font-face rules, italic and oblique faces must be distinguished using the value of + // SPEC: the 'font-style' descriptor. For families that lack any italic or oblique faces, user agents may + // SPEC: create artificial oblique faces, if this is permitted by the value of the 'font-synthesis' property. + + var result = GenericComparison((int)candidate, (int)bestSoFar, (int)requestedStyle); + if (result.HasValue) + { + return result.Value; + } + + if ((requestedStyle == FontStyle.Italic && candidate == FontStyle.Oblique) || + (requestedStyle == FontStyle.Oblique && candidate == FontStyle.Italic) || + (requestedStyle == FontStyle.Normal && candidate == FontStyle.Oblique)) + { + return FontMatchingResult.CandidateIsBetter; + } + else + { + return FontMatchingResult.CandidateIsWorse; + } } + private static FontMatchingResult FontWeightRuleLessThan400(ushort candidate, ushort bestSoFar, ushort requestedWeight) + { + // SPEC: If the desired weight is less than 400, weights below the desired weight are checked in descending order followed by weights above the desired weight in ascending order until a match is found. + if (candidate < requestedWeight && bestSoFar < requestedWeight) + { + return candidate > bestSoFar ? FontMatchingResult.CandidateIsBetter : FontMatchingResult.CandidateIsWorse; + } + else if (candidate > requestedWeight && bestSoFar > requestedWeight) + { + return candidate < bestSoFar ? FontMatchingResult.CandidateIsBetter : FontMatchingResult.CandidateIsWorse; + } + else + { + return candidate < requestedWeight ? FontMatchingResult.CandidateIsBetter : FontMatchingResult.CandidateIsWorse; + } + } + + internal static FontMatchingResult IsBetterWeight(ushort candidate, ushort bestSoFar, ushort requestedWeight) + { + // https://www.w3.org/TR/css-fonts-3/#font-style-matching + // SPEC: 'font-weight' is matched next, so it will always reduce the matching set to a single font face. + // SPEC: If bolder/lighter relative weights are used, the effective weight is calculated based on the inherited weight value, as described + // SPEC: in the definition of the 'font-weight' property. Given the desired weight and the weights of faces in the matching set after the + // SPEC: steps above, if the desired weight is available that face matches. Otherwise, a weight is chosen using the rules below: + // SPEC: If the desired weight is less than 400, weights below the desired weight are checked in descending order followed by weights above the desired weight in ascending order until a match is found. + // SPEC: If the desired weight is greater than 500, weights above the desired weight are checked in ascending order followed by weights below the desired weight in descending order until a match is found. + // SPEC: If the desired weight is 400, 500 is checked first and then the rule for desired weights less than 400 is used. + // SPEC: If the desired weight is 500, 400 is checked first and then the rule for desired weights less than 400 is used. + + var result = GenericComparison(candidate, bestSoFar, requestedWeight); + if (result.HasValue) + { + return result.Value; + } + + if (requestedWeight < 400) + { + return FontWeightRuleLessThan400(candidate, bestSoFar, requestedWeight); + } + else if (requestedWeight > 500) + { + if (candidate > requestedWeight && bestSoFar > requestedWeight) + { + return candidate < bestSoFar ? FontMatchingResult.CandidateIsBetter : FontMatchingResult.CandidateIsWorse; + } + else if (candidate < requestedWeight && bestSoFar < requestedWeight) + { + return candidate > bestSoFar ? FontMatchingResult.CandidateIsBetter : FontMatchingResult.CandidateIsWorse; + } + else + { + return candidate > requestedWeight ? FontMatchingResult.CandidateIsBetter : FontMatchingResult.CandidateIsWorse; + } + } + else if (requestedWeight == 400) + { + if (candidate == 500) + { + return FontMatchingResult.CandidateIsBetter; + } + else if (bestSoFar == 500) + { + return FontMatchingResult.CandidateIsWorse; + } + + return FontWeightRuleLessThan400(candidate, bestSoFar, requestedWeight); + } + else if (requestedWeight == 500) + { + if (candidate == 400) + { + return FontMatchingResult.CandidateIsBetter; + } + else if (bestSoFar == 400) + { + return FontMatchingResult.CandidateIsWorse; + } + + return FontWeightRuleLessThan400(candidate, bestSoFar, requestedWeight); + } + else + { + // Here, requestedWeight is somewhere between 400 and 500 + // The spec doesn't define the behavior for this case. + // We do this as "best-effort" + if (candidate < requestedWeight && bestSoFar > requestedWeight) + { + // If candidate is lighter, prefer it. + return FontMatchingResult.CandidateIsBetter; + } + else if (candidate > requestedWeight && bestSoFar < requestedWeight) + { + // bestSoFar is lighter. + // It is not better than the new candidate. + return FontMatchingResult.CandidateIsWorse; + } + else if (candidate > requestedWeight) + { + return candidate < bestSoFar ? FontMatchingResult.CandidateIsBetter : FontMatchingResult.CandidateIsWorse; + } + else + { + return candidate > bestSoFar ? FontMatchingResult.CandidateIsBetter : FontMatchingResult.CandidateIsWorse; + } + } + } }