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