using System; using System.Collections.Generic; using Glass.Typesetting; using UnitTestSharp; using GlyphDefinition = Glass.SpriteFontDefinition.GlyphDefinition; namespace Glass.UnitTests.Typesetting { public class LineWrapperTests : TestFixture { /// /// Letters and a space, all with zero line height, so height/maxLines cutoffs never trigger unless a test /// opts into instead. /// public static SpriteFontDefinition Font() => WordMeasurerTests.TestFont(); /// /// A font with a real line height, for tests of the maxHeight cutoff, which can't reach /// since SpriteFontDefinition reports zero ascent/descent unless Data sets them. /// public static SpriteFontDefinition FontWithLineHeight(int ascent, int descent) { var glyphs = new Dictionary(); for (char letter = 'a'; letter <= 'z'; ++letter) { glyphs[letter] = new GlyphDefinition { Width = WordMeasurerTests.LetterWidth }; } return new SpriteFontDefinition(new SpriteFontDefinition.Data { GlyphDefinitions = glyphs, InvalidGlyph = new GlyphDefinition { Width = WordMeasurerTests.InvalidGlyphWidth }, Kernings = new Dictionary<(short, short), short>(), Baseline = (ushort)ascent, LineHeight = (ushort)(ascent + descent), }); } // Line carries no Start of its own (like MeasuredWord), so the caller tracks it: it's wherever the line // before it ended. public static string Describe(int start, in Line line) { string flags = line.Flags == LineFlags.None ? "" : $" {line.Flags}"; return $"{start}..{line.End} width {line.VisibleWidth}{flags}"; } public static LineWrapper Wrap(StringView text, int maxWidth, IFontMetrics font = null, int? maxHeight = null, TextOverflow overflow = TextOverflow.Clip, int? maxLines = null) { var words = new WordMeasurer(font ?? Font(), new LineBreakIterator(text)); return new LineWrapper(words, maxWidth, maxHeight, overflow, maxLines); } public static List Lines(StringView text, int maxWidth, IFontMetrics font = null, int? maxHeight = null, TextOverflow overflow = TextOverflow.Clip, int? maxLines = null) { LineWrapper wrapper = Wrap(text, maxWidth, font, maxHeight, overflow, maxLines); var returnMe = new List(); int start = 0; while (wrapper.MoveNext()) { returnMe.Add(Describe(start, wrapper.Current)); start = wrapper.Current.End; } return returnMe; } public static TextOverflow? TruncationOf(StringView text, int maxWidth, IFontMetrics font = null, int? maxHeight = null, TextOverflow overflow = TextOverflow.Clip, int? maxLines = null) { LineWrapper wrapper = Wrap(text, maxWidth, font, maxHeight, overflow, maxLines); while (wrapper.MoveNext()) { } return wrapper.Truncation; } public class CtorTests : TestFixture { public void MaxLinesLessThanOne_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); new LineWrapper(new WordMeasurer(Font(), new LineBreakIterator("a")), maxWidth: 100, maxLines: 0); } } public class MoveNextTests : TestFixture { public void EmptyText_HasNoLines() { CheckEqual(new string[0], Lines("", maxWidth: 100)); } public void WordsThatFitTogether_StayOnOneLine() { CheckEqual(new[] { "0..5 width 38" }, Lines("ab cd", maxWidth: 1000)); } public void WordsThatDoNotFitTogether_WrapToANewLine_AndTheSpaceHangs() { CheckEqual(new[] { "0..3 width 19", "3..5 width 20" }, Lines("ab cd", maxWidth: 25)); } public void MandatoryBreak_EndsTheLineEvenWithRoomToSpare() { CheckEqual(new[] { "0..2 width 10", "2..4 width 20" }, Lines("a\nbb", maxWidth: 1000)); } public void SoftHyphenNotTaken_ShowsNoHyphenAndKerningCarriesAcross() { CheckEqual(new[] { "0..3 width 15" }, Lines($"a{LineBreakIterator.SoftHyphen}c", maxWidth: 1000)); } public void SoftHyphenTaken_ShowsTheHyphenAndItsWidth() { string text = $"a{LineBreakIterator.SoftHyphen}c"; CheckEqual(new[] { "0..2 width 13 EndsWithHyphen", "2..3 width 10" }, Lines(text, maxWidth: 12)); } public void WordWiderThanMaxWidth_SplitsIntoCharacterFitChunks() { CheckEqual(new[] { "0..2 width 20", "2..4 width 20", "4..6 width 20" }, Lines("aaaaaa", maxWidth: 25)); } public void SplitWordFollowedByMoreWords_TheLastChunkStaysOpenForThem() { CheckEqual(new[] { "0..2 width 20", "2..5 width 24" }, Lines("aaa a", maxWidth: 25)); } public void OversizedWordAfterAnotherWord_ClosesTheLineBeforeSplitting() { CheckEqual(new[] { "0..2 width 10", "2..4 width 20", "4..6 width 20", "6..8 width 20" }, Lines("a aaaaaa", maxWidth: 25)); } public void SoftHyphenEndingTheText_NeedsNoHyphen() { CheckEqual(new[] { "0..3 width 19" }, Lines($"ab{LineBreakIterator.SoftHyphen}", maxWidth: 1000)); } public void SingleCharacterWiderThanMaxWidth_OverflowsSidewaysAndIsClipped() { CheckEqual(new[] { "0..1 width 10" }, Lines("a", maxWidth: 5)); CheckEqual(TextOverflow.Clip, TruncationOf("a", maxWidth: 5)); } public void MaxLinesReached_StopsEmittingAndMarksClipped() { CheckEqual(new[] { "0..2 width 20", "2..4 width 20" }, Lines("aaaaaa", maxWidth: 25, maxLines: 2)); CheckEqual(TextOverflow.Clip, TruncationOf("aaaaaa", maxWidth: 25, maxLines: 2)); } public void FirstLine_AlwaysShowsEvenWithNoHeightBudget() { SpriteFontDefinition font = FontWithLineHeight(ascent: 8, descent: 2); CheckEqual(new[] { "0..1 width 10" }, Lines("a", maxWidth: 1000, font, maxHeight: 0)); CheckNull(TruncationOf("a", maxWidth: 1000, font, maxHeight: 0)); } public void ClipOverflow_AllowsAPartlyVisibleLineButNotOneEntirelyOutOfBounds() { SpriteFontDefinition font = FontWithLineHeight(ascent: 8, descent: 2); CheckEqual(new[] { "0..2 width 10", "2..4 width 10" }, Lines("a\nb\nc", maxWidth: 1000, font, maxHeight: 15, TextOverflow.Clip)); CheckEqual(TextOverflow.Clip, TruncationOf("a\nb\nc", maxWidth: 1000, font, maxHeight: 15, TextOverflow.Clip)); } public void EllipsisOverflow_NeedsTheWholeNextLineInsideTheBudget() { SpriteFontDefinition font = FontWithLineHeight(ascent: 8, descent: 2); CheckEqual(new[] { "0..2 width 10" }, Lines("a\nb\nc", maxWidth: 1000, font, maxHeight: 15, TextOverflow.Ellipsis)); } public void EllipsisOverflow_AllowsALineThatFitsEntirely() { SpriteFontDefinition font = FontWithLineHeight(ascent: 8, descent: 2); CheckEqual(new[] { "0..2 width 10", "2..4 width 10" }, Lines("a\nb\nc", maxWidth: 1000, font, maxHeight: 20, TextOverflow.Ellipsis)); } public void HeightBudget_CanRunOutMidSplit() { SpriteFontDefinition font = FontWithLineHeight(ascent: 8, descent: 2); CheckEqual(new[] { "0..2 width 20", "2..4 width 20", "4..6 width 20" }, Lines("aaaaaaaa", maxWidth: 25, font, maxHeight: 25)); CheckEqual(TextOverflow.Clip, TruncationOf("aaaaaaaa", maxWidth: 25, font, maxHeight: 25)); } } } }