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