/*
* Copyright (C) 2014 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.tools.idea.ui;
import com.intellij.ui.SimpleTextAttributes;
import gnu.trove.TIntArrayList;
import gnu.trove.TIntIntHashMap;
import gnu.trove.TIntObjectHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.Before;
import org.junit.Test;
import java.awt.*;
import java.util.*;
import java.util.List;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
/**
* @author Denis Zhdanov
* @since 10/09/14
*/
public class WrapsAwareTextHelperTest {
public static final Font DUMMY_FONT = new Font("", Font.PLAIN, 16);
private static final int SYMBOL_WIDTH = 10;
private static final int SYMBOL_HEIGHT = 10;
private static final String LINE_BREAK_MARKER;
static {
List<String> buffer = new ArrayList<String>();
WrapsAwareTextHelper.appendLineBreak(buffer);
LINE_BREAK_MARKER = buffer.get(0);
}
@NotNull WrapsAwareTextHelper.DimensionCalculator myDefaultDimensionCalculator = new WrapsAwareTextHelper.DimensionCalculator() {
@Override
public void calculate(@NotNull String inText, @NotNull Font inFont, @NotNull Dimension outDimension) {
outDimension.width = inText.length() * SYMBOL_WIDTH;
outDimension.height = SYMBOL_HEIGHT;
}
};
@NotNull private final TIntIntHashMap myMinimumWidths = new TIntIntHashMap();
@NotNull private WrapsAwareTextHelper myCalculator;
@Before
public void setUp() {
myCalculator = new WrapsAwareTextHelper(myDefaultDimensionCalculator);
myMinimumWidths.clear();
}
@Test
public void wrap_singleLine_singleToken_noWraps() {
doWrapTest(Collections.singletonList("abc"), Collections.singletonList("abc"), 3);
}
@Test
public void wrap_singleLine_twoTokens_noWraps() {
doWrapTest(Arrays.asList("ab", "c"), Collections.singletonList("abc"), 3);
}
@Test
public void wrap_singleLine_twoTokens_singleWrap() {
doWrapTest(Arrays.asList("ab", "cd"), Arrays.asList("ab", "cd"), 2);
doWrapTest(Arrays.asList("ab", "cd"), Arrays.asList("abc", "d"), 3);
}
@Test
public void wrap_singleLine_manyTokens_manyWraps() {
doWrapTest(Arrays.asList("123", "4567", "8"), Arrays.asList("1234", "5678"), 4);
doWrapTest(Arrays.asList("123", "4567", "89"), Arrays.asList("1234", "5678", "9"), 4);
}
@Test
public void wrap_singleLine_singleToken_singleWrap() {
doWrapTest(Collections.singletonList("abc"), Arrays.asList("ab", "c"), 2);
}
@Test
public void wrap_singleLine_singleToken_twoWraps() {
doWrapTest(Collections.singletonList("12345678"), Arrays.asList("123", "456", "78"), 3);
doWrapTest(Collections.singletonList("123456789"), Arrays.asList("123", "456", "789"), 3);
}
@Test
public void wrap_noWidthLimit() {
doWrapTest(Arrays.asList("abc"), Arrays.asList("abc"), 0);
doWrapTest(Arrays.asList("abc"), Arrays.asList("abc"), -1);
doWrapTest(Arrays.asList("abc"), Arrays.asList("abc"), Integer.MIN_VALUE);
}
@Test
public void wrap_twoLines() {
doWrapTest(Arrays.asList("1234", LINE_BREAK_MARKER, "567"), Arrays.asList("123", "4", "567"), 3);
}
@Test
public void map_singleLine_singleFragment() {
doMapTest(Arrays.asList("abc"), 3, 0, 0, 0);
doMapTest(Arrays.asList("abc"), 3, 0, 1, 0);
doMapTest(Arrays.asList("abc"), 3, 0, 2, 0);
doMapTest(Arrays.asList("abc"), 3, 0, 3, -1);
doMapTest(Arrays.asList("abc"), 3, 1, 1, -1);
}
@Test
public void map_singleLine_multipleFragments_multipleWraps() {
doMapTest(Arrays.asList("abc", "def"), 2, 0, 0, 0);
doMapTest(Arrays.asList("abc", "def"), 2, 0, 1, 0);
doMapTest(Arrays.asList("abc", "def"), 2, 0, 2, -1);
doMapTest(Arrays.asList("abc", "def"), 2, 1, 0, 0);
doMapTest(Arrays.asList("abc", "def"), 2, 1, 2, -1);
doMapTest(Arrays.asList("abc", "def"), 2, 2, 0, 1);
doMapTest(Arrays.asList("abc", "def"), 2, 2, 1, 1);
doMapTest(Arrays.asList("abc", "def"), 2, 2, 2, -1);
doMapTest(Arrays.asList("abc", "def"), 2, 3, 0, -1);
doMapTest(Arrays.asList("abc", "def"), 2, 3, 1, -1);
doMapTest(Arrays.asList("abc", "def"), 2, 4, 1, -1);
}
@Test
public void map_lineBreak() {
doMapTest(Arrays.asList("ab", LINE_BREAK_MARKER, "de"), 3, 0, 0, 0);
doMapTest(Arrays.asList("ab", LINE_BREAK_MARKER, "de"), 3, 0, 1, 0);
doMapTest(Arrays.asList("ab", LINE_BREAK_MARKER, "de"), 3, 0, 2, -1);
doMapTest(Arrays.asList("ab", LINE_BREAK_MARKER, "de"), 3, 1, 0, 2);
doMapTest(Arrays.asList("ab", LINE_BREAK_MARKER, "de"), 3, 1, 1, 2);
doMapTest(Arrays.asList("ab", LINE_BREAK_MARKER, "de"), 3, 1, 2, -1);
}
@Test
public void map_minimumWidth() {
myMinimumWidths.put(0, 3 * SYMBOL_WIDTH);
doMapTest(Arrays.asList("ab", "cd"), 10, 0, 0, 0);
doMapTest(Arrays.asList("ab", "cd"), 10, 0, 1, 0);
doMapTest(Arrays.asList("ab", "cd"), 10, 0, 2, -1);
doMapTest(Arrays.asList("ab", "cd"), 10, 0, 3, 1);
doMapTest(Arrays.asList("ab", "cd"), 10, 0, 4, 1);
doMapTest(Arrays.asList("ab", "cd"), 10, 0, 5, -1);
doMapTest(Arrays.asList("ab", "cd"), 10, 1, 0, -1);
}
private void doWrapTest(@NotNull List<String> fragments, @NotNull List<String> expectedLines, int availableWidthInSymbols) {
// Verify that given data is consistent.
verifyTestData(fragments, expectedLines);
// Calculate the data.
List<SimpleTextAttributes> textAttributes = Collections.nCopies(fragments.size(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
Dimension dimension = new Dimension();
TIntObjectHashMap<TIntArrayList> breakOffsets = new TIntObjectHashMap<TIntArrayList>();
TIntIntHashMap lineHeights = new TIntIntHashMap();
int widthLimit = availableWidthInSymbols * SYMBOL_WIDTH;
myCalculator.wrap(fragments, textAttributes, DUMMY_FONT, myMinimumWidths, widthLimit, dimension, breakOffsets, lineHeights);
// Check calculated dimension vs expected.
int expectedWidthInPixels = 0;
for (String line : expectedLines) {
expectedWidthInPixels = Math.max(expectedWidthInPixels, line.length() * SYMBOL_WIDTH);
}
assertEquals("Target text dimension width doesn't match", expectedWidthInPixels, dimension.width);
assertEquals("Target text dimension height doesn't match", expectedLines.size() * SYMBOL_HEIGHT, dimension.height);
// Check line heights.
assertEquals("Target line heights don't match", buildExpectedLineHeights(expectedLines), lineHeights);
// Check calculated text break offsets vs expected.
TIntObjectHashMap<TIntArrayList> expectedBreakOffsets = buildExpectedBreakOffsets(fragments, expectedLines);
assertEquals(expectedBreakOffsets, breakOffsets);
}
@NotNull
private static TIntIntHashMap buildExpectedLineHeights(@NotNull List<String> expectedLines) {
TIntIntHashMap result = new TIntIntHashMap();
for (int i = 0; i < expectedLines.size(); i++) {
result.put(i, SYMBOL_HEIGHT);
}
return result;
}
@NotNull
private static TIntObjectHashMap<TIntArrayList> buildExpectedBreakOffsets(List<String> fragments, List<String> expectedLines) {
TIntObjectHashMap<TIntArrayList> expectedBreakOffsets = new TIntObjectHashMap<TIntArrayList>();
int currentLineOffset = 0;
int fragmentOffset;
expectedLines = new ArrayList<String>(expectedLines);
for (int fragmentIndex = 0; fragmentIndex < fragments.size(); fragmentIndex++) {
String fragmentText = fragments.get(fragmentIndex);
fragmentOffset = 0;
if (LINE_BREAK_MARKER.equals(fragmentText)) {
currentLineOffset = 0;
expectedLines.remove(0);
continue;
}
while (true) {
String s = expectedLines.get(0);
if (s.length() - currentLineOffset < fragmentText.length() - fragmentOffset) {
TIntArrayList list = expectedBreakOffsets.get(fragmentIndex);
if (list == null) {
expectedBreakOffsets.put(fragmentIndex, list = new TIntArrayList());
}
list.add(fragmentOffset += s.length() - currentLineOffset);
expectedLines.remove(0);
currentLineOffset = 0;
continue;
}
currentLineOffset += fragmentText.length() - fragmentOffset;
break;
}
}
return expectedBreakOffsets;
}
private static void verifyTestData(@NotNull List<String> fragments, @NotNull List<String> expectedLines) {
SymbolIterator initialIterator = new SymbolIterator(fragments);
SymbolIterator expectedIterator = new SymbolIterator(expectedLines);
StringBuilder buffer = new StringBuilder();
int offset = 0;
while (initialIterator.hasNext()) {
char c = initialIterator.next();
buffer.append(c);
if (!expectedIterator.hasNext()) {
throw new IllegalArgumentException(String.format(
"Given input text has at least one more symbol than expected (at offset %d) %n Input:%n%s %n Expected:%n%s", offset, fragments,
expectedLines));
}
char c1 = expectedIterator.next();
if (c != c1) {
throw new IllegalArgumentException(String.format(
"Given input text mismatches given expected text: input text has symbol '%c' at offset %d but expected has '%c' (%s) %n "
+ "Input:%n%s %n Expected:%n%s",
c, offset, c1, "..." + buffer.subSequence(Math.max(0, offset - 5), offset + 1), fragments, expectedLines
));
}
offset++;
}
if (expectedIterator.hasNext()) {
throw new IllegalArgumentException(String.format(
"Given expected text has at least one more symbol than initial (at offset %d) %n Input:%n%s %n Expected:%n%s",
offset, fragments, expectedLines
));
}
}
private void doMapTest(@NotNull List<String> fragments,
int availableWidthInSymbols,
int targetLine,
int targetColumn,
int expectedFragmentIndex)
{
// Prepare the data.
List<SimpleTextAttributes> textAttributes = Collections.nCopies(fragments.size(), SimpleTextAttributes.REGULAR_ATTRIBUTES);
Dimension dimension = new Dimension();
TIntObjectHashMap<TIntArrayList> breakOffsets = new TIntObjectHashMap<TIntArrayList>();
TIntIntHashMap lineHeights = new TIntIntHashMap();
int widthLimit = availableWidthInSymbols * SYMBOL_WIDTH;
myCalculator.wrap(fragments, textAttributes, DUMMY_FONT, myMinimumWidths, widthLimit, dimension, breakOffsets, lineHeights);
// Do test.
int actualFragmentIndex = myCalculator.mapFragment(fragments,
textAttributes,
myMinimumWidths,
breakOffsets,
lineHeights,
DUMMY_FONT,
targetColumn * SYMBOL_HEIGHT + SYMBOL_HEIGHT / 2,
targetLine * SYMBOL_HEIGHT + SYMBOL_HEIGHT / 2);
if (expectedFragmentIndex < 0 ^ actualFragmentIndex < 0) {
fail(String.format("Mapped fragment index mismatch for the input data: fragments=%s, available width=%d, target line=%d, "
+ "target column=%d, expected index=%d, actual index=%d",
fragments, availableWidthInSymbols, targetLine, targetColumn, expectedFragmentIndex, actualFragmentIndex));
}
assertEquals("Target fragments index mismatch", expectedFragmentIndex, actualFragmentIndex);
}
private static class SymbolIterator {
@NotNull private final Iterator<String> myDelegate;
@Nullable private String myCurrentString;
private int myCurrentStringOffset;
private SymbolIterator(@NotNull Iterable<String> strings) {
myDelegate = strings.iterator();
}
boolean hasNext() {
if (myCurrentString != null) {
return true;
}
if (!myDelegate.hasNext()) {
return false;
}
myCurrentString = myDelegate.next();
if (LINE_BREAK_MARKER.equals(myCurrentString)) {
myCurrentString = null;
return hasNext();
}
myCurrentStringOffset = 0;
return true;
}
char next() {
if (!hasNext()) {
throw new IllegalStateException();
}
assert myCurrentString != null;
char c = myCurrentString.charAt(myCurrentStringOffset++);
if (myCurrentStringOffset >= myCurrentString.length()) {
myCurrentString = null;
}
return c;
}
}
}