using System;
using System.Collections.Generic;
using System.Drawing;
using Annulus;
using Azimuth;
using UnitTestSharp;
using GlyphDefinition = Glass.SpriteFontDefinition.GlyphDefinition;
namespace Glass.UnitTests.Materials
{
public class BakedTextMaterialTests : TestFixture
{
public const int LetterWidth = 10;
public const int SpaceWidth = 4;
public const int LineSpacing = 10;
///
/// Letters 10 wide whose images are offset (1, 2) from the pen, each at its own atlas column so a test can
/// tell them apart; a space 4 wide with no image; lines apart.
///
public static SpriteFontDefinition Font(int letterWidth = LetterWidth)
{
var glyphs = new Dictionary();
for (char letter = 'a'; letter <= 'z'; ++letter)
{
glyphs[letter] = new GlyphDefinition
{
Width = letterWidth,
Size = new Point(8, 10),
Offset = new Point(1, 2),
Origin = new Point((letter - 'a') * 10, 0),
};
}
glyphs[' '] = new GlyphDefinition { Width = SpaceWidth };
return new SpriteFontDefinition(new SpriteFontDefinition.Data
{
GlyphDefinitions = glyphs,
InvalidGlyph = new GlyphDefinition { Width = letterWidth },
Kernings = new Dictionary<(short, short), short>(),
Baseline = 8,
LineHeight = LineSpacing,
});
}
///
/// Built once, since building a font mid-test would count against an allocation test.
///
public static readonly SpriteFontDefinition SharedFont = Font();
public static TextStyle Style()
{
return new TextStyle { Font = SharedFont, HorizontalMargin = 0, VerticalMargin = 0 };
}
///
/// Each baked glyph as "letter@x,y".
///
public static List Glyphs(BakedTextMaterial material)
{
var returnMe = new List();
for (int i = 0; i < material.GlyphCount; ++i)
{
BakedGlyph glyph = material[i];
returnMe.Add($"{(char)('a' + glyph.Glyph.Origin.X / 10)}@{glyph.X},{glyph.Y}");
}
return returnMe;
}
public static BakedTextMaterial BakedAt(TextStyle style, string text, Vector materialSize)
{
var returnMe = new BakedTextMaterial(style, text);
returnMe.Bake(materialSize);
return returnMe;
}
public class CtorTests : TestFixture
{
public void NullStyle_Throws()
{
CheckThrow(typeof(ArgumentNullException));
new BakedTextMaterial(null, "a");
}
public void NoFont_MeasuresNothing()
{
CheckEqual(0, new BakedTextMaterial(new TextStyle(), "ab").MeasureCount);
}
public void NoFont_BakesNoGlyphsAtAnySize()
{
BakedTextMaterial material = BakedAt(new TextStyle(), "ab", new Vector(100, 100));
CheckEqual(0, material.GlyphCount);
CheckEqual(0, material.TextHeight);
CheckNull(material.Truncation);
}
public void NoFont_CopiesWithoutMeasuring()
{
BakedTextMaterial copy = BakedAt(new TextStyle(), "ab", new Vector(100, 100)) with { };
CheckEqual(0, copy.MeasureCount);
CheckEqual(0, copy.GlyphCount);
}
public void KeepsTheStyleAndText()
{
TextStyle style = Style();
var material = new BakedTextMaterial(style, "ab");
CheckTrue(ReferenceEquals(style, material.Style));
CheckEqual("ab", material.Text.ToString());
}
public void BeforeASize_HasNoGlyphs()
{
CheckEqual(0, new BakedTextMaterial(Style(), "ab").GlyphCount);
}
public void MeasuresOnce()
{
CheckEqual(1, new BakedTextMaterial(Style(), "ab").MeasureCount);
}
public void With_BakesTheCopyAtTheOriginalsSize()
{
BakedTextMaterial original = BakedAt(Style(), "ab cd", new Vector(25, 100));
BakedTextMaterial copy = original with { };
CheckEqual("ab cd", copy.Text.ToString());
CheckEqual(Glyphs(original), Glyphs(copy));
}
public void With_MeasuresTheCopyOnce()
{
BakedTextMaterial original = BakedAt(Style(), "ab", new Vector(100, 10));
original.SetText("cd");
CheckEqual(1, (original with { }).MeasureCount);
}
public void With_NewTextOnTheCopy_LeavesTheOriginalAlone()
{
BakedTextMaterial original = BakedAt(Style(), "ab cd", new Vector(25, 100));
BakedTextMaterial copy = original with { };
copy.SetText("ef");
CheckEqual("ab cd", original.Text.ToString());
CheckEqual(new[] { "a@1,2", "b@11,2", "c@1,12", "d@11,12" }, Glyphs(original));
CheckEqual(new[] { "e@1,2", "f@11,2" }, Glyphs(copy));
}
public void With_NewStyleOnTheCopy_LeavesTheOriginalAlone()
{
BakedTextMaterial original = BakedAt(Style(), "ab", new Vector(100, 10));
BakedTextMaterial copy = original with { };
copy.SetStyle(Style() with { Font = Font(letterWidth: 20) });
CheckEqual(new[] { "a@1,2", "b@11,2" }, Glyphs(original));
CheckEqual(new[] { "a@1,2", "b@21,2" }, Glyphs(copy));
}
}
public class EffectiveScaleTests : TestFixture
{
public void IsTheMaterialsScaleTimesTheStyles()
{
var material = new BakedTextMaterial(Style() with { Scale = 3 }, "a") { Scale = 2 };
CheckEqual((Scalar)6, material.EffectiveScale);
}
}
public class BakeTests : TestFixture
{
public void PlacesEachGlyphAtItsPenPlusItsOffset()
{
CheckEqual(new[] { "a@1,2", "b@11,2" }, Glyphs(BakedAt(Style(), "ab", new Vector(100, 10))));
}
public void GlyphsWithNoImage_AreLeftOut()
{
CheckEqual(new[] { "a@1,2", "b@15,2" }, Glyphs(BakedAt(Style(), "a b", new Vector(100, 10))));
}
public void WrapsAtTheBakedWidth()
{
CheckEqual(new[] { "a@1,2", "b@11,2", "c@1,12", "d@11,12" },
Glyphs(BakedAt(Style(), "ab cd", new Vector(25, 100))));
}
public void VerticalAlignment_IsInTheBakedY()
{
TextStyle style = Style() with { VerticalAlignment = Alignment.Vertical.Center };
CheckEqual(new[] { "a@1,12", "b@11,12" }, Glyphs(BakedAt(style, "ab", new Vector(100, 30))));
}
public void HorizontalAlignment_IsInTheBakedX()
{
TextStyle style = Style() with { HorizontalAlignment = Alignment.Horizontal.Right };
CheckEqual(new[] { "a@81,2", "b@91,2" }, Glyphs(BakedAt(style, "ab", new Vector(100, 10))));
}
public void Margins_NarrowTheWidthWrappedTo()
{
TextStyle style = Style() with { HorizontalMargin = 5 };
CheckEqual(new[] { "a@1,2", "b@11,2", "c@1,12", "d@11,12" },
Glyphs(BakedAt(style, "ab cd", new Vector(35, 100))));
}
public void MaterialScale_WrapsAtTheWidthInModelSpace()
{
var material = new BakedTextMaterial(Style(), "ab cd") { Scale = 2 };
material.Bake(new Vector(50, 200));
CheckEqual(new[] { "a@1,2", "b@11,2", "c@1,12", "d@11,12" }, Glyphs(material));
}
public void StyleScale_WrapsAtTheWidthInModelSpace()
{
CheckEqual(new[] { "a@1,2", "b@11,2", "c@1,12", "d@11,12" },
Glyphs(BakedAt(Style() with { Scale = 2 }, "ab cd", new Vector(50, 200))));
}
public void SingleLine_BakesOnlyTheFirstLine()
{
TextStyle style = Style() with { Multiline = false };
CheckEqual(new[] { "a@1,2", "b@11,2" }, Glyphs(BakedAt(style, "ab cd", new Vector(25, 100))));
}
public void TextHeight_IsHowTallTheLinesStack()
{
CheckEqual(2 * LineSpacing, BakedAt(Style(), "ab cd", new Vector(25, 100)).TextHeight);
}
public void EmptySize_BakesNothing()
{
BakedTextMaterial material = BakedAt(Style(), "ab", new Vector(100, 10));
material.Bake(new Vector(0, 10));
CheckEqual(0, material.GlyphCount);
CheckEqual(0, material.TextHeight);
}
public void TextThatFits_IsNotTruncated()
{
CheckNull(BakedAt(Style(), "ab cd", new Vector(25, 100)).Truncation);
}
public void TextCutShort_IsTruncatedTheStylesWay()
{
TextStyle style = Style() with { OverflowBehavior = TextOverflow.Clip };
CheckEqual(TextOverflow.Clip, BakedAt(style, "ab cd", new Vector(25, 10)).Truncation);
}
public void EmptySize_IsNotTruncated()
{
BakedTextMaterial material = BakedAt(Style(), "ab cd", new Vector(25, 10));
material.Bake(new Vector(0, 10));
CheckNull(material.Truncation);
}
public void Again_ForgetsTheLastTruncation()
{
BakedTextMaterial material = BakedAt(Style(), "ab cd", new Vector(25, 10));
material.Bake(new Vector(25, 100));
CheckNull(material.Truncation);
}
public void Again_StartsOver()
{
BakedTextMaterial material = BakedAt(Style(), "ab cd", new Vector(25, 100));
material.Bake(new Vector(100, 100));
CheckEqual(new[] { "a@1,2", "b@11,2", "c@25,2", "d@35,2" }, Glyphs(material));
}
public void MoreGlyphsThanFirstRoomFor_AreAllKept()
{
CheckEqual(40, BakedAt(Style(), new string('a', 40), new Vector(1000, 10)).GlyphCount);
}
public void CountsEveryBake()
{
BakedTextMaterial material = BakedAt(Style(), "ab", new Vector(100, 10));
material.Bake(new Vector(100, 10));
CheckEqual(3, material.BakeCount);
}
public static readonly BakedTextMaterial AllocationMaterial =
BakedAt(Style(), "ab cd ef", new Vector(25, 100));
[AllocationTest]
public void Again_AllocatesNothing()
{
AllocationMaterial.Bake(new Vector(25, 100));
CheckNoAllocations();
CheckEqual(6, AllocationMaterial.GlyphCount);
}
}
public class BlockSizeForTests : TestFixture
{
public void TakesTheMarginsOffEachSide()
{
TextStyle style = Style() with { HorizontalMargin = 5, VerticalMargin = 3 };
CheckEqual(new Vector(90, 24), new BakedTextMaterial(style, "a").BlockSizeFor(new Vector(100, 30)));
}
}
public class IsBakedForTests : TestFixture
{
public static BakedTextMaterial BakedAt100By30() => BakedAt(Style(), "ab", new Vector(100, 30));
public void TheBakedSize_IsTrue()
{
BakedTextMaterial material = BakedAt100By30();
CheckTrue(material.IsBakedFor(material.BlockSizeFor(new Vector(100, 30))));
}
public void AnotherWidth_IsFalse()
{
BakedTextMaterial material = BakedAt100By30();
CheckFalse(material.IsBakedFor(material.BlockSizeFor(new Vector(101, 30))));
}
public void AnotherHeight_IsFalse()
{
BakedTextMaterial material = BakedAt100By30();
CheckFalse(material.IsBakedFor(material.BlockSizeFor(new Vector(100, 31))));
}
public void TheSameWholePixels_IsTrue()
{
BakedTextMaterial material = BakedAt(Style(), "ab", new Vector(100.2, 30.2));
CheckTrue(material.IsBakedFor(material.BlockSizeFor(new Vector(100.7, 30.7))));
}
}
public class SetTextTests : TestFixture
{
public void MeasuresAndBakesTheNewTextAtTheLastSize()
{
BakedTextMaterial material = BakedAt(Style(), "ab", new Vector(25, 100));
material.SetText("cd ef");
CheckEqual("cd ef", material.Text.ToString());
CheckEqual(new[] { "c@1,2", "d@11,2", "e@1,12", "f@11,12" }, Glyphs(material));
}
public static readonly BakedTextMaterial AllocationMaterial =
BakedAt(Style(), "ab cd ef gh ij kl", new Vector(25, 100));
[AllocationTest]
public void NoLongerText_AllocatesNothing()
{
AllocationMaterial.SetText("ab cd");
CheckNoAllocations();
CheckEqual(4, AllocationMaterial.GlyphCount);
}
}
public class SetStyleTests : TestFixture
{
public void Null_Throws()
{
var material = new BakedTextMaterial(Style(), "a");
CheckThrow(typeof(ArgumentNullException));
material.SetStyle(null);
}
public void NoFont_ClearsTheGlyphs()
{
BakedTextMaterial material = BakedAt(Style(), "ab", new Vector(100, 10));
material.SetStyle(new TextStyle { HorizontalMargin = 0, VerticalMargin = 0 });
CheckEqual(0, material.GlyphCount);
}
public void AFontAfterNone_MeasuresAndBakesAtTheLastSize()
{
BakedTextMaterial material = BakedAt(new TextStyle(), "ab", new Vector(100, 10));
material.SetStyle(Style());
CheckEqual(1, material.MeasureCount);
CheckEqual(new[] { "a@1,2", "b@11,2" }, Glyphs(material));
}
public void TextSetWithNoFont_IsMeasuredOnceAFontArrives()
{
BakedTextMaterial material = BakedAt(new TextStyle(), "ab", new Vector(100, 10));
material.SetText("cd");
material.SetStyle(Style());
CheckEqual(new[] { "c@1,2", "d@11,2" }, Glyphs(material));
}
public void KeepsTheNewStyle()
{
var material = new BakedTextMaterial(Style(), "a");
TextStyle style = Style() with { TintColor = Color.Red };
material.SetStyle(style);
CheckTrue(ReferenceEquals(style, material.Style));
}
public void OnlyAColorChange_NeitherMeasuresNorBakes()
{
BakedTextMaterial material = BakedAt(Style(), "ab", new Vector(100, 10));
material.SetStyle(Style() with { TintColor = Color.Red });
CheckEqual(1, material.MeasureCount);
CheckEqual(2, material.BakeCount);
}
public void ALayoutChange_BakesAtTheLastSizeWithoutMeasuring()
{
BakedTextMaterial material = BakedAt(Style(), "ab", new Vector(100, 10));
material.SetStyle(Style() with { HorizontalAlignment = Alignment.Horizontal.Right });
CheckEqual(1, material.MeasureCount);
CheckEqual(new[] { "a@81,2", "b@91,2" }, Glyphs(material));
}
public void ANewFont_MeasuresWithItAndBakes()
{
BakedTextMaterial material = BakedAt(Style(), "ab", new Vector(100, 10));
material.SetStyle(Style() with { Font = Font(letterWidth: 20) });
CheckEqual(2, material.MeasureCount);
CheckEqual(new[] { "a@1,2", "b@21,2" }, Glyphs(material));
}
}
public class PreferredSizeTests : TestFixture
{
public static TextMaterial Unbaked(TextStyle style, string text) =>
new TextMaterial { Text = text, Style = style };
public void Unbounded_MatchesTheUnbakedMaterials()
{
TextStyle style = Style() with { HorizontalMargin = 3, VerticalMargin = 2 };
CheckEqual(Unbaked(style, "ab cd\nef").PreferredSize(Constraints.Unbounded),
new BakedTextMaterial(style, "ab cd\nef").PreferredSize(Constraints.Unbounded));
}
public void Wrapped_MatchesTheUnbakedMaterials()
{
TextStyle style = Style() with { HorizontalMargin = 3, VerticalMargin = 2 };
Constraints narrow = Constraints.Loose(new Vector(31, Scalar.PositiveInfinity));
CheckEqual(Unbaked(style, "ab cd ef").PreferredSize(narrow),
new BakedTextMaterial(style, "ab cd ef").PreferredSize(narrow));
}
public void StyleScale_MatchesTheUnbakedMaterials()
{
TextStyle style = Style() with { Scale = 2 };
CheckEqual(Unbaked(style, "ab").PreferredSize(Constraints.Unbounded),
new BakedTextMaterial(style, "ab").PreferredSize(Constraints.Unbounded));
}
public void NoFont_IsJustTheMarginsLikeTheUnbakedMaterials()
{
var style = new TextStyle { HorizontalMargin = 3, VerticalMargin = 2 };
CheckEqual(new Vector(6, 4), new BakedTextMaterial(style, "ab").PreferredSize(Constraints.Unbounded));
CheckEqual(Unbaked(style, "ab").PreferredSize(Constraints.Unbounded),
new BakedTextMaterial(style, "ab").PreferredSize(Constraints.Unbounded));
}
public void AfterSetText_IsTheNewTexts()
{
var material = new BakedTextMaterial(Style(), "ab");
material.SetText("abcd");
CheckEqual(Unbaked(Style(), "abcd").PreferredSize(Constraints.Unbounded),
material.PreferredSize(Constraints.Unbounded));
}
}
}
}