using System; using Amphora; using Glass.Typesetting; using UnitTestSharp; namespace Glass.UnitTests.Typesetting { public class CaretLocatorTests : TestFixture { public const int Roomy = GlyphPlacerTests.Roomy; /// /// Letters 10 wide, a space 4, and lines 10 apart. /// public static readonly SpriteFontDefinition Font = GlyphPlacerTests.Font; public static CaretPosition Locate(StringView text, int index, int containerWidth = Roomy, IFontMetrics font = null, Alignment.Horizontal alignment = Alignment.Horizontal.Left, TextOverflow overflow = TextOverflow.Clip, int? maxLines = null, TrailingSpaceMode trailingSpaces = TrailingSpaceMode.Hang, bool atLineEnd = false) { return CaretLocator.Locate(font ?? Font, text, index, containerWidth, alignment, maxHeight: null, overflow, maxLines, trailingSpaces, atLineEnd); } public static int IndexAt(StringView text, int x, int y, int containerWidth = Roomy, IFontMetrics font = null, Alignment.Horizontal alignment = Alignment.Horizontal.Left, TextOverflow overflow = TextOverflow.Clip, int? maxLines = null) { return IndexAtWithSide(text, x, y, containerWidth, font, alignment, overflow, maxLines).index; } public static (int index, bool atLineEnd) IndexAtWithSide(StringView text, int x, int y, int containerWidth = Roomy, IFontMetrics font = null, Alignment.Horizontal alignment = Alignment.Horizontal.Left, TextOverflow overflow = TextOverflow.Clip, int? maxLines = null) { int index = CaretLocator.IndexAt(font ?? Font, text, x, y, containerWidth, alignment, out bool atLineEnd, maxHeight: null, overflow, maxLines); return (index, atLineEnd); } public class LocateTests : TestFixture { public void EmptyText_IsAtTheStartOfTheFirstLine() { CheckEqual(new CaretPosition(0, 0, 0), Locate("", 0)); } public void EmptyText_FollowsTheAlignment() { CheckEqual(new CaretPosition(0, 100, 0), Locate("", 0, 100, alignment: Alignment.Horizontal.Right)); CheckEqual(new CaretPosition(0, 50, 0), Locate("", 0, 100, alignment: Alignment.Horizontal.Center)); } public void EachIndex_IsWhereTheNextGlyphStarts() { CheckEqual(new CaretPosition(0, 0, 0), Locate("abc", 0)); CheckEqual(new CaretPosition(0, 10, 0), Locate("abc", 1)); CheckEqual(new CaretPosition(0, 20, 0), Locate("abc", 2)); } public void TheEndOfTheText_IsAfterTheLastGlyph() { CheckEqual(new CaretPosition(0, 30, 0), Locate("abc", 3)); } public void Kerning_MovesTheStopWithTheGlyph() { CheckEqual(new CaretPosition(0, 9, 0), Locate("ab", 1, font: WordMeasurerTests.SharedFont)); } public void AWrappedLine_StartsWhereItsFirstGlyphIs() { CheckEqual(new CaretPosition(1, 0, 10), Locate("ab cd", 3, 25)); CheckEqual(new CaretPosition(1, 10, 10), Locate("ab cd", 4, 25)); } public void TheEndOfAWrappedLine_IsOnTheNextLine() { CheckEqual(new CaretPosition(0, 20, 0), Locate("ab cd", 2, 25)); CheckEqual(new CaretPosition(1, 0, 10), Locate("ab cd", 3, 25)); } public void AtLineEnd_DrawsTheStartOfAWrappedLineAtTheEndOfTheOneBefore() { CheckEqual(new CaretPosition(0, 24, 0), Locate("ab cd", 3, 25, atLineEnd: true)); } public void AtLineEnd_FollowsTheAlignment() { CheckEqual(new CaretPosition(0, 29, 0), Locate("ab cd", 3, 25, alignment: Alignment.Horizontal.Right, atLineEnd: true)); } public void AtLineEnd_AfterASoftHyphen_IsAfterTheHyphen() { string text = "b" + LineBreakIterator.SoftHyphen + "c"; CheckEqual(new CaretPosition(0, 17, 0), Locate(text, 2, 18, atLineEnd: true)); } public void AtLineEnd_DoesNothingToOtherIndices() { CheckEqual(new CaretPosition(0, 0, 0), Locate("ab cd", 0, 25, atLineEnd: true)); CheckEqual(new CaretPosition(0, 20, 0), Locate("ab cd", 2, 25, atLineEnd: true)); CheckEqual(new CaretPosition(1, 20, 10), Locate("ab cd", 5, 25, atLineEnd: true)); } public void AtLineEnd_DoesNothingAfterAHardBreak() { CheckEqual(new CaretPosition(1, 0, 10), Locate("a\nb", 2, atLineEnd: true)); CheckEqual(new CaretPosition(1, 0, 10), Locate("a\r\nb", 3, atLineEnd: true)); } public void AtLineEnd_DoesNothingOnAnEmptyLineAfterAHardBreak() { CheckEqual(new CaretPosition(1, 0, 10), Locate("a\n", 2, atLineEnd: true)); } public void TheEndOfTheLastLine_IsAfterItsLastGlyph() { CheckEqual(new CaretPosition(1, 20, 10), Locate("ab cd", 5, 25)); } public void HangingSpaces_AreStopsPastTheLinesVisibleEdge() { CheckEqual(new CaretPosition(0, 100, 0), Locate("a ", 1, 100, alignment: Alignment.Horizontal.Right)); CheckEqual(new CaretPosition(0, 104, 0), Locate("a ", 2, 100, alignment: Alignment.Horizontal.Right)); } public void BeforeAHardBreak_IsTheEndOfItsLine() { CheckEqual(new CaretPosition(0, 10, 0), Locate("a\nb", 1)); } public void AfterAHardBreak_IsTheStartOfTheNextLine() { CheckEqual(new CaretPosition(1, 0, 10), Locate("a\nb", 2)); } public void CarriageReturnLineFeed_IsOneBreak() { CheckEqual(new CaretPosition(0, 10, 0), Locate("a\r\nb", 1)); CheckEqual(new CaretPosition(0, 10, 0), Locate("a\r\nb", 2)); CheckEqual(new CaretPosition(1, 0, 10), Locate("a\r\nb", 3)); } public void AnEmptyLine_IsAtItsOwnStart() { CheckEqual(new CaretPosition(1, 0, 10), Locate("a\n\nb", 2)); CheckEqual(new CaretPosition(2, 0, 20), Locate("a\n\nb", 3)); } public void AnEmptyLine_FollowsTheAlignment() { CheckEqual(new CaretPosition(1, 100, 10), Locate("a\n\nb", 2, 100, alignment: Alignment.Horizontal.Right)); } public void TextEndingInAHardBreak_HasAnEmptyLineAfterIt() { CheckEqual(new CaretPosition(0, 10, 0), Locate("a\n", 1)); CheckEqual(new CaretPosition(1, 0, 10), Locate("a\n", 2)); } public void TextEndingInOnlyAHardBreak_HasAnEmptyLineAfterIt() { CheckEqual(new CaretPosition(0, 0, 0), Locate("\n", 0)); CheckEqual(new CaretPosition(1, 0, 10), Locate("\n", 1)); } public void CutShortText_HasNoEmptyLineAfterTheKeptLine() { CheckEqual(new CaretPosition(0, 20, 0), Locate("ab\ncd", 3, maxLines: 1)); CheckEqual(new CaretPosition(0, 20, 0), Locate("ab\ncd", 5, maxLines: 1)); } public void RightAligned_EachLineAlignsByItsOwnWidth() { const string text = "a\nbbb"; CheckEqual(new CaretPosition(0, 100, 0), Locate(text, 1, 100, alignment: Alignment.Horizontal.Right)); CheckEqual(new CaretPosition(1, 70, 10), Locate(text, 2, 100, alignment: Alignment.Horizontal.Right)); CheckEqual(new CaretPosition(1, 100, 10), Locate(text, 5, 100, alignment: Alignment.Horizontal.Right)); } public void Centered_RoundsDownToAWholePixel() { CheckEqual(new CaretPosition(0, 45, 0), Locate("a", 0, 101, alignment: Alignment.Horizontal.Center)); CheckEqual(new CaretPosition(0, 55, 0), Locate("a", 1, 101, alignment: Alignment.Horizontal.Center)); } public void SoftHyphenTaken_PutsTheCaretBeforeTheHyphen() { string text = "b" + LineBreakIterator.SoftHyphen + "c"; CheckEqual(new CaretPosition(0, 10, 0), Locate(text, 1, 18)); CheckEqual(new CaretPosition(1, 0, 10), Locate(text, 2, 18)); } public void Ellipsis_PutsTheCaretBeforeTheMarker() { CheckEqual(new CaretPosition(0, 10, 0), Locate("aaa bbb", 1, 34, overflow: TextOverflow.Ellipsis, maxLines: 1)); CheckEqual(new CaretPosition(0, 20, 0), Locate("aaa bbb", 2, 34, overflow: TextOverflow.Ellipsis, maxLines: 1)); } public void CutOffText_WaitsWhereTheKeptTextEnds() { CheckEqual(new CaretPosition(0, 20, 0), Locate("aaa bbb", 5, 34, overflow: TextOverflow.Ellipsis, maxLines: 1)); CheckEqual(new CaretPosition(0, 20, 0), Locate("aaa bbb", 7, 34, overflow: TextOverflow.Ellipsis, maxLines: 1)); } public void ASurrogatePair_IsOneGlyph() { string text = "a" + WordMeasurerTests.Emoji + "b"; CheckEqual(new CaretPosition(0, 10, 0), Locate(text, 1)); CheckEqual(new CaretPosition(0, 17, 0), Locate(text, 3)); } public void InsideASurrogatePair_IsAfterIt() { CheckEqual(new CaretPosition(0, 17, 0), Locate("a" + WordMeasurerTests.Emoji + "b", 2)); } public void AnInvisibleCharacter_TakesNoRoom() { string text = "a" + WordMeasurerTests.LanguageTag + "b"; CheckEqual(new CaretPosition(0, 10, 0), Locate(text, 1)); CheckEqual(new CaretPosition(0, 10, 0), Locate(text, 3)); } public void AViewIntoALongerString_IsLikeTheStandaloneString() { var view = new StringView("xab cdy", 1, 6); for (int index = 0; index <= view.Length; ++index) { CheckEqual(Locate("ab cd", index, 25), Locate(view, index, 25)); } } public void AViewThatStartsOnALineFeed_CountsOnlyTheLineFeed() { var view = new StringView("a\r\nb", 2, 4); CheckEqual(new CaretPosition(0, 0, 0), Locate(view, 0)); CheckEqual(new CaretPosition(1, 0, 10), Locate(view, 1)); } public void NegativeIndex_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); Locate("abc", -1); } public void IndexPastTheEnd_Throws() { CheckThrow(typeof(ArgumentOutOfRangeException)); Locate("abc", 4); } } public class IndexAtTests : TestFixture { public void EmptyText_IsZero() { CheckEqual(0, IndexAt("", 50, 50)); } public void APointOnAGlyph_TakesTheNearerStop() { CheckEqual(0, IndexAt("abc", 4, 0)); CheckEqual(1, IndexAt("abc", 6, 0)); CheckEqual(1, IndexAt("abc", 14, 0)); CheckEqual(2, IndexAt("abc", 16, 0)); } public void AMidwayPoint_TakesTheEarlierStop() { CheckEqual(0, IndexAt("abc", 5, 0)); } public void LeftOfTheText_IsTheFirstStop() { CheckEqual(0, IndexAt("abc", -50, 0)); } public void RightOfTheText_IsTheEnd() { CheckEqual(3, IndexAt("abc", 26, 0)); CheckEqual(3, IndexAt("abc", 1000, 0)); } public void AbovAndBelowTheText_GoToTheFirstAndLastLines() { CheckEqual(1, IndexAt("abc\ndef", 10, -50)); CheckEqual(5, IndexAt("abc\ndef", 10, 500)); } public void ALaterLine_IsFoundByItsRow() { CheckEqual(3, IndexAt("ab cd", 0, 12, 25)); CheckEqual(5, IndexAt("ab cd", 1000, 12, 25)); } public void EachLine_OwnsTheRowsFromItsTopToTheNextLinesTop() { CheckEqual(0, IndexAt("a\nb\nc", 0, 9)); CheckEqual(2, IndexAt("a\nb\nc", 0, 10)); CheckEqual(4, IndexAt("a\nb\nc", 0, 20)); } public void PastTheEndOfAWrappedLine_IsItsEnd() { CheckEqual((3, true), IndexAtWithSide("ab cd", 1000, 0, 25)); CheckEqual((2, true), IndexAtWithSide("abcd", 1000, 0, 25)); } public void TheStartOfAWrappedLine_IsNotItsEnd() { CheckEqual((3, false), IndexAtWithSide("ab cd", 0, 12, 25)); } public void TheEndOfALineThatDoesNotWrapOn_IsNotALineEnd() { CheckEqual((5, false), IndexAtWithSide("ab cd", 1000, 12, 25)); CheckEqual((1, false), IndexAtWithSide("a\nb", 100, 5)); CheckEqual((3, false), IndexAtWithSide("abc", 1000, 0)); } public void TheEndOfAWrappedLine_IsAtItsHangingSpaces() { CheckEqual((2, false), IndexAtWithSide("ab cd", 21, 0, 25)); CheckEqual((3, true), IndexAtWithSide("ab cd", 23, 0, 25)); } public void PastTheEndOfAHardBrokenLine_IsBeforeTheBreak() { CheckEqual(1, IndexAt("a\nb", 100, 5)); CheckEqual(1, IndexAt("a\r\nb", 100, 5)); } public void AfterAHardBreak_IsTheStartOfTheNextLine() { CheckEqual(2, IndexAt("a\nb", 0, 15)); CheckEqual(3, IndexAt("a\r\nb", 0, 15)); } public void AnEmptyLine_IsItsOwnStart() { CheckEqual(2, IndexAt("a\n\nb", 100, 15)); } public void BelowTextEndingInAHardBreak_IsTheEmptyLineAfterIt() { CheckEqual(2, IndexAt("a\n", 0, 15)); CheckEqual(2, IndexAt("a\n", 0, 500)); } public void RightAligned_UsesWhereTheLineSits() { const string text = "a\nbbb"; var right = Alignment.Horizontal.Right; CheckEqual(2, IndexAt(text, 0, 15, 100, alignment: right)); CheckEqual(3, IndexAt(text, 78, 15, 100, alignment: right)); CheckEqual(5, IndexAt(text, 1000, 15, 100, alignment: right)); } public void SoftHyphen_HasAStopBeforeTheHyphen() { string text = "b" + LineBreakIterator.SoftHyphen + "c"; CheckEqual(1, IndexAt(text, 7, 0, 18)); CheckEqual((2, true), IndexAtWithSide(text, 100, 0, 18)); CheckEqual(2, IndexAt(text, 0, 12, 18)); } public void Ellipsis_EndsAtTheKeptText() { CheckEqual(2, IndexAt("aaa bbb", 1000, 0, 34, overflow: TextOverflow.Ellipsis, maxLines: 1)); } public void AMarkerOfSeveralGlyphs_IsOneStop() { SpriteFontDefinition font = LineWrapperTests.FontWithLineHeight(ascent: 8, descent: 2, withEllipsis: false); CheckEqual(2, IndexAt("ab\ncd", 1000, 0, Roomy, font, overflow: TextOverflow.Ellipsis, maxLines: 1)); CheckEqual(2, IndexAt("ab\ncd", 22, 0, Roomy, font, overflow: TextOverflow.Ellipsis, maxLines: 1)); } public void AViewThatStartsOnALineFeed_IsRelativeToTheView() { var view = new StringView("a\r\nb", 2, 4); CheckEqual(0, IndexAt(view, 100, 5)); CheckEqual(1, IndexAt(view, 0, 15)); } public void AViewIntoALongerString_IsLikeTheStandaloneString() { var view = new StringView("xab cdy", 1, 6); CheckEqual(IndexAt("ab cd", 1000, 12, 25), IndexAt(view, 1000, 12, 25)); } public void LocatingAnyIndex_AndFindingThatPoint_ReturnsTheIndex() { const string text = "ab cd\nef gh\n\nij"; foreach (var alignment in new[] { Alignment.Horizontal.Left, Alignment.Horizontal.Center, Alignment.Horizontal.Right }) { for (int index = 0; index <= text.Length; ++index) { CaretPosition start = Locate(text, index, 25, alignment: alignment); CaretPosition end = Locate(text, index, 25, alignment: alignment, atLineEnd: true); CheckEqual((index, false), IndexAtWithSide(text, start.X, start.Y, 25, alignment: alignment)); CheckEqual((index, end != start), IndexAtWithSide(text, end.X, end.Y, 25, alignment: alignment)); } } } } } }