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));
}
}
}
}