using Azimuth; using System; using System.Collections.Generic; using Glass.Typesetting; using UnitTestSharp; namespace Glass.UnitTests.Typesetting { public class LineLayoutTests : TestFixture { // Only LineAscent/LineDescent/LineGap matter to SizeOf, so a fake covers the gap term without needing a // whole SpriteFontDefinition (whose own LineGap is always 0). private class GapOnlyFont : IFontMetrics { public int LineAscent { get; set; } public int LineDescent { get; set; } public int LineGap { get; set; } public GlyphMetricsTable MetricsTable => throw new NotSupportedException(); public KerningTable KerningTable => throw new NotSupportedException(); public bool HasGlyph(char glyph) => throw new NotSupportedException(); } public class FitAllTests : TestFixture { public void HoldsWhatLineWrapperYields() { LineLayout layout = LineLayout.FitAll(LineWrapperTests.Font(), "ab cd", maxWidth: 1000); CheckEqual(1, layout.Count); CheckEqual("0..5 width 38", LineWrapperTests.Describe(0, layout[0])); CheckNull(layout.Truncation); } public void MoreLinesThanFirstGuessed_AreAllKept() { string manyWords = string.Join(" ", new string('a', 40).ToCharArray()); // Narrow enough that every one-letter word gets its own line, well past the first guess. LineLayout layout = LineLayout.FitAll(LineWrapperTests.Font(), manyWords, maxWidth: 5); CheckEqual(40, layout.Count); } public void EmptyText_HasNoLines() { CheckEqual(0, LineLayout.FitAll(LineWrapperTests.Font(), "", maxWidth: 100).Count); } public void RunningOutOfLines_KeepsWhatFitAndReportsTheTruncation() { LineLayout layout = LineLayout.FitAll(LineWrapperTests.Font(), "aaaaaa", maxWidth: 25, maxLines: 2); CheckEqual(2, layout.Count); CheckEqual(TextOverflow.Clip, layout.Truncation); } } public class SizeOfTests : TestFixture { public void EmptyLayout_IsZero() { LineLayout layout = LineLayout.FitAll(LineWrapperTests.Font(), "", maxWidth: 100); CheckEqual(Vector.Zero, LineLayout.SizeOf(layout, LineWrapperTests.Font())); } public void OneLine_IsThatLinesWidthAndTheFontsAscentPlusDescent() { SpriteFontDefinition font = LineWrapperTests.FontWithLineHeight(ascent: 8, descent: 2); LineLayout layout = LineLayout.FitAll(font, "ab cd", maxWidth: 1000); CheckEqual(new Vector(TextWidth.Calculate(font, "ab cd"), 10), LineLayout.SizeOf(layout, font)); } public void SeveralLines_IsTheWidestLineAndTheirStackedHeight() { SpriteFontDefinition font = LineWrapperTests.FontWithLineHeight(ascent: 8, descent: 2); LineLayout layout = LineLayout.FitAll(font, "a\nbb\nc", maxWidth: 1000); int widest = Math.Max(TextWidth.Calculate(font, "a"), TextWidth.Calculate(font, "bb")); CheckEqual(new Vector(widest, 30), LineLayout.SizeOf(layout, font)); } public void CountsLineGapBetweenLinesButNotAfterTheLast() { var font = new GapOnlyFont { LineAscent = 8, LineDescent = 2, LineGap = 3 }; var layout = new LineLayout(new[] { new Line(1, 5), new Line(2, 5), new Line(3, 5) }, 3, null); CheckEqual(3 * 10 + 2 * 3, LineLayout.SizeOf(layout, font).Y); } } public class CalculateSizeTests : TestFixture { public void EmptyText_IsZero() { CheckEqual(Vector.Zero, LineLayout.CalculateSize(LineWrapperTests.Font(), "")); } public void Bounded_MatchesSizeOfFitAll() { SpriteFontDefinition font = LineWrapperTests.FontWithLineHeight(ascent: 8, descent: 2); const string text = "aaaaaa"; Vector expected = LineLayout.SizeOf(LineLayout.FitAll(font, text, maxWidth: 25), font); CheckEqual(expected, LineLayout.CalculateSize(font, text, maxWidth: 25)); } public void Bounded_ReportsTheGeneralPathsTruncation() { SpriteFontDefinition font = LineWrapperTests.FontWithLineHeight(ascent: 8, descent: 2); LineLayout.CalculateSize(font, "aaaaaa", maxWidth: 25, maxHeight: 12, TextOverflow.Clip, out TextOverflow? truncation); CheckEqual(TextOverflow.Clip, truncation); } public void Unconstrained_NoHardBreaks_MatchesTextWidthOnOneLine() { SpriteFontDefinition font = LineWrapperTests.FontWithLineHeight(ascent: 8, descent: 2); const string text = "ab cd"; CheckEqual(new Vector(TextWidth.Calculate(font, text), 10), LineLayout.CalculateSize(font, text)); } public void Unconstrained_HardBreaks_MatchesTheGeneralPath() { SpriteFontDefinition font = LineWrapperTests.FontWithLineHeight(ascent: 8, descent: 2); const string text = "aa\nbbb\nc"; Vector expected = LineLayout.SizeOf(LineLayout.FitAll(font, text, maxWidth: int.MaxValue), font); CheckEqual(expected, LineLayout.CalculateSize(font, text)); } public void ExplicitPositiveInfinityWidth_TakesTheFastPathTooLikeNullDoes() { SpriteFontDefinition font = LineWrapperTests.FontWithLineHeight(ascent: 8, descent: 2); const string text = "ab cd"; CheckEqual(LineLayout.CalculateSize(font, text, maxWidth: null), LineLayout.CalculateSize(font, text, maxWidth: Scalar.PositiveInfinity)); } public void MaxLinesSet_FallsBackToTheGeneralPathEvenWhenWidthAndHeightAreUnconstrained() { SpriteFontDefinition font = LineWrapperTests.FontWithLineHeight(ascent: 8, descent: 2); const string text = "a\nb\nc"; Vector expected = LineLayout.SizeOf(LineLayout.FitAll(font, text, maxWidth: int.MaxValue, maxLines: 2), font); CheckEqual(expected, LineLayout.CalculateSize(font, text, maxLines: 2)); } [CpuTimeout(3000)] public void Unconstrained_AgreesWithTheGeneralPath_OnRandomText() { SpriteFontDefinition font = LineWrapperTests.FontWithLineHeight(ascent: 8, descent: 2); var random = new Random(1); var firstMismatches = new List(); for (int trial = 0; trial < 20000 && firstMismatches.Count < 5; ++trial) { var text = new System.Text.StringBuilder(); int length = random.Next(12); for (int i = 0; i < length; ++i) { int codePoint = LineBreakIteratorTests.Representatives[ random.Next(LineBreakIteratorTests.Representatives.Length)]; text.Append(char.ConvertFromUtf32(codePoint)); } string candidate = text.ToString(); try { Vector fast = LineLayout.CalculateSize(font, candidate); Vector slow = LineLayout.SizeOf(LineLayout.FitAll(font, candidate, maxWidth: int.MaxValue), font); if (!fast.Equals(slow)) { firstMismatches.Add( $"[{LineBreakIteratorTests.CodePointsOf(candidate)}] fast {fast}, slow {slow}"); } } catch (Exception exception) { firstMismatches.Add( $"[{LineBreakIteratorTests.CodePointsOf(candidate)}] threw {exception.Message}"); } } CheckEqual("", string.Join("; ", firstMismatches)); } } } }