using System.Collections.Generic; using Amphora; using Glass.Typesetting; using UnitTestSharp; using GlyphDefinition = Glass.SpriteFontDefinition.GlyphDefinition; namespace Glass.UnitTests.Typesetting { public class FacedMeasuringTests : TestFixture { /// /// Lowercase letters, a space, a hyphen, a period and an ellipsis, each the same width in every font that /// doesn't say otherwise, with lines 10 apart. /// public static SpriteFontDefinition Face(int letterWidth, int spaceWidth = 4, int hyphenWidth = 3, int ellipsisWidth = 5, int invalidWidth = WordMeasurerTests.InvalidGlyphWidth, int ascent = 8, int descent = 2, string page = null, Dictionary<(short, short), short> kernings = null) { var glyphs = new Dictionary(); for (char letter = 'a'; letter <= 'z'; ++letter) { glyphs[letter] = new GlyphDefinition { Width = letterWidth }; } glyphs[' '] = new GlyphDefinition { Width = spaceWidth }; glyphs['-'] = new GlyphDefinition { Width = hyphenWidth }; glyphs['.'] = new GlyphDefinition { Width = LineWrapperTests.PeriodWidth }; glyphs[TruncationMarker.EllipsisGlyph] = new GlyphDefinition { Width = ellipsisWidth }; return new SpriteFontDefinition(new SpriteFontDefinition.Data { GlyphDefinitions = glyphs, InvalidGlyph = new GlyphDefinition { Width = invalidWidth }, Kernings = kernings ?? new Dictionary<(short, short), short>(), Baseline = (ushort)ascent, LineHeight = (ushort)(ascent + descent), Pages = page == null ? null : new[] { page }, }); } private static (short, short) Pair(char left, char right) => WordMeasurerTests.Pair(left, right); /// /// Letters 10 wide with no kerning. /// public static readonly SpriteFontDefinition Narrow = Face(letterWidth: 10); /// /// Letters 14 wide with a wider space, hyphen and ellipsis than , and no kerning. /// public static readonly SpriteFontDefinition Wide = Face(letterWidth: 14, spaceWidth: 6, hyphenWidth: 5, ellipsisWidth: 8, invalidWidth: 9); /// /// Letters 10 wide like , with 4 more pixels above the baseline. /// public static readonly SpriteFontDefinition Tall = Face(letterWidth: 10, ascent: 12, descent: 2); /// /// Letters 10 wide like , with 3 fewer pixels above the baseline. /// public static readonly SpriteFontDefinition Short = Face(letterWidth: 10, ascent: 5, descent: 2); /// /// with a kerning pair of each kind a test crosses a face change with. /// public static readonly SpriteFontDefinition Kerned = Face(letterWidth: 10, kernings: new Dictionary<(short, short), short> { [Pair('a', 'b')] = -1, [Pair('b', 'c')] = -4, [Pair('b', '-')] = -1, [Pair('b', TruncationMarker.EllipsisGlyph)] = -1, [Pair('b', ' ')] = -2, [Pair(' ', 'c')] = -3, [Pair(' ', ' ')] = 1, }); /// /// with kerning that differs from 's for the same pairs. /// public static readonly SpriteFontDefinition KernedWide = Face(letterWidth: 14, spaceWidth: 6, hyphenWidth: 5, ellipsisWidth: 8, invalidWidth: 9, kernings: new Dictionary<(short, short), short> { [Pair('a', 'b')] = -3, [Pair('b', 'c')] = -6, [Pair('b', '-')] = -2, [Pair('b', TruncationMarker.EllipsisGlyph)] = -2, [Pair('b', ' ')] = -5, [Pair(' ', 'c')] = -5, [Pair(' ', ' ')] = 2, }); public static FaceRuns Runs(IFontMetrics baseFont, params (int start, int end, IFontMetrics font)[] runs) { var returnMe = new FaceRuns(baseFont); foreach ((int start, int end, IFontMetrics font) in runs) { returnMe.Add(start, end, font); } return returnMe; } public static string Describe(int start, in MeasuredWord word) { return $"{start}..{word.ContentEnd}..{word.End}{(word.IsMandatory ? "!" : "")} " + $"lead {word.LeadingKerning} content {word.ContentWidth} end {word.EndKerning}"; } public static List Words(StringView text, IFontMetrics baseFont, params (int start, int end, IFontMetrics font)[] runs) { var measured = new MeasuredWords(baseFont, text, Runs(baseFont, runs)); var returnMe = new List(); while (measured.MoveNext()) { returnMe.Add(Describe(measured.CurrentStart, measured.Current)); } return returnMe; } /// /// The first word of `text` measured with `runs`, for a test of what ends it. /// public static MeasuredWord FirstWord(StringView text, IFontMetrics baseFont, params (int start, int end, IFontMetrics font)[] runs) { var measured = new MeasuredWords(baseFont, text, Runs(baseFont, runs)); measured.MoveNext(); return measured.Current; } public class MeasureTests : TestFixture { public void TextInTheBaseFont_MeasuresInIt() { CheckEqual(new[] { "0..2..3 lead 0 content 20 end 0", "3..5..5! lead 0 content 20 end 0" }, Words("ab cd", Narrow, (7, 8, Wide))); } public void WordInAnotherFont_MeasuresInIt() { CheckEqual(new[] { "0..2..3 lead 0 content 20 end 0", "3..5..5! lead 0 content 28 end 0" }, Words("ab cd", Narrow, (3, 5, Wide))); } public void WordStraddlingTwoFonts_IsTheSumOfItsGlyphs() { CheckEqual(new[] { "0..4..4! lead 0 content 48 end 0" }, Words("abcd", Narrow, (1, 3, Wide))); } public void KerningInsideARun_UsesThatFontsTable() { CheckEqual(new[] { "0..2..2! lead 0 content 19 end 0" }, Words("ab", KernedWide, (0, 2, Kerned))); } public void KerningAcrossAFaceChange_IsNone() { CheckEqual(new[] { "0..2..2! lead 0 content 24 end 0" }, Words("bc", Kerned, (1, 2, KernedWide))); } public void KerningWithinTheBaseFont_IsItsOwn() { CheckEqual(new[] { "0..2..2! lead 0 content 16 end 0" }, Words("bc", Kerned, (5, 6, KernedWide))); } public void LeadingKerningAcrossAFaceChange_IsNone() { CheckEqual(new[] { "0..1..2 lead 0 content 10 end -2", "2..3..3! lead 0 content 14 end 0" }, Words("b c", Kerned, (2, 3, KernedWide))); } public void LeadingKerningWithinTheBaseFont_IsItsOwn() { CheckEqual(new[] { "0..1..2 lead 0 content 10 end -2", "2..3..3! lead -3 content 10 end 0" }, Words("b c", Kerned, (5, 6, KernedWide))); } public void LeadingKerningAfterASpaceInAnotherFont_IsNone() { CheckEqual(new[] { "0..1..2 lead 0 content 10 end 0", "2..3..3! lead 0 content 10 end 0" }, Words("b c", Kerned, (1, 2, KernedWide))); } public void TrailingSpace_KernsAgainstTheContentInTheSameFont() { CheckEqual("0..1..2 lead 0 content 10 end -2", Describe(0, FirstWord("b c", Kerned, (5, 6, KernedWide)))); } public void TrailingSpaceInAnotherFont_KernsAsAtTheStartOfALine() { CheckEqual("0..1..2 lead 0 content 10 end 0", Describe(0, FirstWord("b c", Kerned, (1, 2, KernedWide)))); } public void SoftHyphenInTheSameFont_KernsAgainstTheContent() { string text = $"b{LineBreakIterator.SoftHyphen}c"; CheckEqual(new[] { "0..2..2 lead 0 content 10 end -1", "2..3..3! lead -4 content 10 end 0" }, Words(text, Kerned, (5, 6, KernedWide))); } public void SoftHyphenInAnotherFont_KernsAsAtTheStartOfALine() { string text = $"b{LineBreakIterator.SoftHyphen}c"; CheckEqual(new[] { "0..2..2 lead 0 content 10 end 0", "2..3..3! lead -4 content 10 end 0" }, Words(text, Kerned, (1, 2, KernedWide))); } public void SoftHyphenInAnotherFont_LeavesTheKerningBetweenTheGlyphsAround() { string text = $"a{LineBreakIterator.SoftHyphen}b"; CheckEqual(new[] { "0..2..2 lead 0 content 10 end 0", "2..3..3! lead -1 content 10 end 0" }, Words(text, Kerned, (1, 2, KernedWide))); } public void HardBreak_StartsKerningOver() { CheckEqual(new[] { "0..1..2! lead 0 content 10 end 0", "2..3..3! lead 0 content 10 end 0" }, Words("b\nc", Kerned, (5, 6, KernedWide))); } public void SurrogatePair_IsMeasuredInTheFontAtItsFirstCodeUnit() { CheckEqual(new[] { "0..2..2! lead 0 content 9 end 0" }, Words(WordMeasurerTests.Emoji, Narrow, (0, 1, Wide))); } public void SurrogatePairInTheBaseFont_IsMeasuredInIt() { CheckEqual(new[] { "0..2..2! lead 0 content 7 end 0" }, Words(WordMeasurerTests.Emoji, Narrow, (1, 2, Wide))); } public void CharacterThatDrawsNothing_AddsNoWidth() { string text = $"a{LineBreakIterator.SoftHyphen}{WordMeasurerTests.LanguageTag}b"; CheckEqual(new[] { "0..4..4 lead 0 content 10 end 0", "4..5..5! lead 0 content 10 end 0" }, Words(text, Narrow, (7, 8, Wide))); } public void EmptyText_HasNoWords() { CheckEqual(0, Words("", Narrow, (0, 1, Wide)).Count); } public void ReplacesWhatWordsHeld() { var words = new DynamicArray(); words.Add(MeasuredWordsTests.WordEndingAt(7)); FacedMeasuring.Measure("ab", Runs(Narrow, (0, 1, Wide)), words); CheckEqual(1, words.Count); CheckEqual(24, words[0].ContentWidth); } } public class HyphenFaceOfTests : TestFixture { public void IsTheFontTheSoftHyphenIsSetIn() { var pen = new FacedPen(Runs(Narrow, (1, 2, Wide))); var lineBreak = new LineBreak(2, isMandatory: false, ending: LineBreakEnding.SoftHyphen); CheckTrue(ReferenceEquals(Wide, FacedMeasuring.HyphenFaceOf(ref pen, lineBreak))); } public void ASoftHyphenInTheBaseFont_IsTheBaseFont() { var pen = new FacedPen(Runs(Narrow, (0, 1, Wide))); var lineBreak = new LineBreak(2, isMandatory: false, ending: LineBreakEnding.SoftHyphen); CheckTrue(ReferenceEquals(Narrow, FacedMeasuring.HyphenFaceOf(ref pen, lineBreak))); } } public class HyphenWidthOfTests : TestFixture { private static readonly string SoftHyphenated = $"b{LineBreakIterator.SoftHyphen}c"; public void InTheSameFont_IsItsHyphenKernedAgainstTheContent() { FaceRuns faces = Runs(Kerned, (5, 6, KernedWide)); MeasuredWord word = FirstWord(SoftHyphenated, Kerned, (5, 6, KernedWide)); CheckEqual(2, FacedMeasuring.HyphenWidthOf(faces, in word)); } public void InAnotherFont_IsThatFontsHyphenWithNoKerning() { FaceRuns faces = Runs(Kerned, (1, 2, KernedWide)); MeasuredWord word = FirstWord(SoftHyphenated, Kerned, (1, 2, KernedWide)); CheckEqual(5, FacedMeasuring.HyphenWidthOf(faces, in word)); } public void WordNotEndingAtASoftHyphen_IsZero() { FaceRuns faces = Runs(Narrow, (5, 6, Wide)); MeasuredWord word = FirstWord("ab cd", Narrow, (5, 6, Wide)); CheckEqual(0, FacedMeasuring.HyphenWidthOf(faces, in word)); } public void WordEndingTheText_IsZeroEvenAtASoftHyphen() { string text = $"b{LineBreakIterator.SoftHyphen}"; FaceRuns faces = Runs(Narrow, (5, 6, Wide)); MeasuredWord word = FirstWord(text, Narrow, (5, 6, Wide)); CheckEqual(0, FacedMeasuring.HyphenWidthOf(faces, in word)); } } public class WidthOfSpacesTests : TestFixture { public void NoSpaces_IsZero() { CheckEqual(0, FacedMeasuring.WidthOfSpaces(Runs(Kerned, (7, 8, KernedWide)), start: 1, count: 0, firstKerning: 3)); } public void SpacesInTheBaseFont_AreKernedBetweenThemselves() { CheckEqual(14, FacedMeasuring.WidthOfSpaces(Runs(Kerned, (7, 8, KernedWide)), start: 1, count: 3, firstKerning: 0)); } public void FirstKerning_IsAddedBeforeTheFirstSpace() { CheckEqual(19, FacedMeasuring.WidthOfSpaces(Runs(Kerned, (7, 8, KernedWide)), start: 1, count: 3, firstKerning: 5)); } public void OnlyTheFirstCountSpaces_AreWidened() { CheckEqual(9, FacedMeasuring.WidthOfSpaces(Runs(Kerned, (7, 8, KernedWide)), start: 1, count: 2, firstKerning: 0)); } public void EachSpace_IsInItsOwnFont() { CheckEqual(14, FacedMeasuring.WidthOfSpaces(Runs(Kerned, (2, 3, KernedWide)), start: 1, count: 3, firstKerning: 0)); } public void SpacesInAnotherFont_AreItsWidthAndKerning() { CheckEqual(6 + 2 + 6 + 2 + 6, FacedMeasuring.WidthOfSpaces(Runs(Kerned, (1, 4, KernedWide)), start: 1, count: 3, firstKerning: 0)); } } public class SpacesThatFitTests : TestFixture { // Three spaces from index 1, all in the base font unless a test says otherwise. private static int Fit(FaceRuns faces, int room, int available = 3, int firstKerning = 0) { return FacedMeasuring.SpacesThatFit(faces, start: 1, available, room, firstKerning); } private static FaceRuns AllInTheBaseFont => Runs(Kerned, (7, 8, KernedWide)); public void RoomForNone_IsZero() { CheckEqual(0, Fit(AllInTheBaseFont, room: 3)); } public void RoomForOne_IsOne() { CheckEqual(1, Fit(AllInTheBaseFont, room: 8)); } public void RoomExactlyForTwo_IsTwo() { CheckEqual(2, Fit(AllInTheBaseFont, room: 9)); } public void RoomForAll_IsAllAvailable() { CheckEqual(3, Fit(AllInTheBaseFont, room: 100)); } public void RoomForMoreThanAvailable_IsAvailable() { CheckEqual(2, Fit(AllInTheBaseFont, room: 100, available: 2)); } public void FirstKerning_TakesRoomFromTheFirstSpace() { CheckEqual(2, Fit(AllInTheBaseFont, room: 7, firstKerning: -2)); CheckEqual(1, Fit(AllInTheBaseFont, room: 6, firstKerning: -2)); } public void EachSpace_IsInItsOwnFont() { FaceRuns middleOneWide = Runs(Kerned, (2, 3, KernedWide)); CheckEqual(1, Fit(middleOneWide, room: 9)); CheckEqual(2, Fit(middleOneWide, room: 10)); } public void ASpaceThatDoesNotAdvance_AlwaysFits() { SpriteFontDefinition flat = Face(letterWidth: 10, spaceWidth: 0); CheckEqual(3, FacedMeasuring.SpacesThatFit(Runs(flat, (7, 8, Wide)), start: 1, available: 3, room: -10, firstKerning: 0)); } } public class WidthOfMarkerTests : TestFixture { public void Ellipsis_IsItsWidthInTheFont() { CheckEqual(8, FacedMeasuring.WidthOfMarker(Wide, TruncationMarker.HorizontalEllipsis, rowBefore: 0)); } public void Ellipsis_KernsAgainstTheGlyphBeforeIt() { int row = Kerned.MetricsTable['b'].LeftKerningRow; CheckEqual(4, FacedMeasuring.WidthOfMarker(Kerned, TruncationMarker.HorizontalEllipsis, row)); } public void ThreePeriods_AreEachTheirWidth() { CheckEqual(3 * LineWrapperTests.PeriodWidth, FacedMeasuring.WidthOfMarker(Narrow, TruncationMarker.ThreePeriods, rowBefore: 0)); } } } }