[go: nahoru, domu]

blob: a21ee43c7bf99079dd5bda119e054226cc595385 [file] [log] [blame]
/*
* Copyright 2019 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 androidx.ui.text
import androidx.test.filters.SdkSuppress
import androidx.test.filters.SmallTest
import androidx.test.filters.Suppress
import androidx.test.platform.app.InstrumentationRegistry
import androidx.ui.geometry.Rect
import androidx.ui.graphics.Path
import androidx.ui.graphics.PathOperation
import androidx.ui.text.FontTestData.Companion.BASIC_MEASURE_FONT
import androidx.ui.text.font.asFontFamily
import androidx.ui.text.style.TextAlign
import androidx.ui.text.style.TextDirection
import androidx.ui.text.style.TextDirectionAlgorithm
import androidx.ui.text.style.TextIndent
import androidx.ui.unit.Density
import androidx.ui.unit.PxPosition
import androidx.ui.unit.TextUnit
import androidx.ui.unit.px
import androidx.ui.unit.sp
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
@SmallTest
class MultiParagraphIntegrationTest {
private val fontFamilyMeasureFont = BASIC_MEASURE_FONT.asFontFamily()
private val context = InstrumentationRegistry.getInstrumentation().context
private val defaultDensity = Density(density = 1f)
private val cursorWidth = 4f
@Test
fun didExceedMaxLines_withLineFeed() {
// The text should be rendered with 3 lines:
// a
// b
// c
val text = createAnnotatedString("a\nb", "c")
// maxLines be 1 or 2, smaller than the line count 3
for (i in 1..2) {
val paragraph = simpleMultiParagraph(
text = text,
maxLines = i
)
assertWithMessage("text has 3 lines, maxLines = $i")
.that(paragraph.didExceedMaxLines).isTrue()
}
// maxLines be 3, 4, 5 larger than the line count 3
for (i in 3..5) {
val paragraph = simpleMultiParagraph(
text = text,
maxLines = i
)
assertWithMessage("text has 3 lines, maxLines = $i")
.that(paragraph.didExceedMaxLines).isFalse()
}
}
@Test
fun didExceedMaxLines_withLineWrap() {
with(defaultDensity) {
val fontSize = 50.sp
// Each line has the space only for 1 character
val width = fontSize.toPx().value
// The text should be rendered with 3 lines:
// a
// b
// c
val text = createAnnotatedString("ab", "c")
for (i in 1..2) {
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
maxLines = i,
width = width
)
assertWithMessage("text has 3 lines, maxLines = $i")
.that(paragraph.didExceedMaxLines).isTrue()
}
for (i in 3..5) {
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
maxLines = i
)
assertWithMessage("text has 3 lines, maxLines = $i")
.that(paragraph.didExceedMaxLines).isFalse()
}
}
}
@Test
fun textOverflow_exceedMaxLines_singleParagraph() {
val text = createAnnotatedString("a\nb")
val paragraph = simpleMultiParagraph(text = text, maxLines = 1)
assertThat(paragraph.paragraphInfoList[0].paragraph.didExceedMaxLines).isTrue()
}
@Test
fun textOverflow_exceedMaxLinesInMiddle_multiParagraph() {
val text = createAnnotatedString("a\nb", "a\nb")
val paragraph = simpleMultiParagraph(text = text, maxLines = 3)
assertThat(paragraph.paragraphInfoList[1].paragraph.didExceedMaxLines).isTrue()
}
@Test
fun textOverflow_exceedMaxLinesInGap_multiParagraph() {
val text = createAnnotatedString("a\nb", "a")
val paragraph = simpleMultiParagraph(text = text, maxLines = 2)
assertThat(paragraph.paragraphInfoList.size).isEqualTo(1)
}
@Test
fun getPathForRange() {
with(defaultDensity) {
val text = createAnnotatedString("ab", "c", "de")
val fontSize = 20.sp
val fontSizeInPx = fontSize.toPx().value
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize
)
// Select "bcd"
val actualPath = paragraph.getPathForRange(1, 4)
val expectedPath = Path()
// path covering "b"
expectedPath.addRect(
Rect(fontSizeInPx, 0f, fontSizeInPx * 2, fontSizeInPx)
)
// path covering "c"
expectedPath.addRect(
Rect(0f, fontSizeInPx, fontSizeInPx, fontSizeInPx * 2)
)
// path covering "d"
expectedPath.addRect(
Rect(0f, fontSizeInPx * 2, fontSizeInPx, fontSizeInPx * 3)
)
val diff = Path.combine(PathOperation.difference, expectedPath, actualPath).getBounds()
assertThat(diff).isEqualTo(Rect.zero)
}
}
@Test(expected = IllegalArgumentException::class)
fun getPathForRange_throws_exception_if_start_larger_than_end() {
val text = "abc"
val textStart = 0
val textEnd = text.length
val paragraph = simpleMultiParagraph(text = text)
paragraph.getPathForRange(textEnd, textStart)
}
@Test(expected = IllegalArgumentException::class)
fun getPathForRange_throws_exception_if_start_is_smaller_than_zero() {
val text = "abc"
val textStart = 0
val textEnd = text.length
val paragraph = simpleMultiParagraph(text = text)
paragraph.getPathForRange(textStart - 2, textEnd - 1)
}
@Test(expected = IllegalArgumentException::class)
fun getPathForRange_throws_exception_if_end_is_larger_than_text_length() {
val text = "abc"
val textStart = 0
val textEnd = text.length
val paragraph = simpleMultiParagraph(text = text)
paragraph.getPathForRange(textStart, textEnd + 1)
}
@Test
fun getOffsetForPosition() {
with(defaultDensity) {
val lineLength = 2
val text = createAnnotatedString(List(3) { "a".repeat(lineLength) })
val fontSize = 50.sp
val fontSizeInPx = fontSize.toIntPx().value
// each line contains 2 character
val width = 2 * fontSizeInPx
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
width = width.toFloat()
)
// The text should be rendered as:
// aa
// aa
// aa
for (i in 0 until text.length) {
val row = i / lineLength
val y = fontSizeInPx / 2 + fontSizeInPx * row
val col = i % lineLength
val x = fontSizeInPx * col
val actualOffset = paragraph.getOffsetForPosition(PxPosition(x.px, y.px))
assertWithMessage("getOffsetForPosition($x, $y) failed")
.that(actualOffset).isEqualTo(i)
}
}
}
@Test
fun getBoundingBox() {
with(defaultDensity) {
val lineLength = 2
val text = createAnnotatedString(List(3) { "a".repeat(lineLength) })
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx().value
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
width = text.length * fontSizeInPx
)
// The text should be rendered as:
// aa
// aa
// aa
for (i in 0 until text.length) {
val row = i / lineLength
val col = i % lineLength
val expectedBox = Rect(
left = col * fontSizeInPx,
right = (col + 1) * fontSizeInPx,
top = row * fontSizeInPx,
bottom = (row + 1) * fontSizeInPx
)
val actualBox = paragraph.getBoundingBox(i)
assertWithMessage("getBoundingBox($i) failed")
.that(actualBox).isEqualTo(expectedBox)
}
}
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getBoundingBox_offset_negative() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getBoundingBox(-1)
}
@Suppress
@Test(expected = java.lang.IllegalArgumentException::class)
fun getBoundingBox_offset_larger_than_length_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getBoundingBox(text.length + 1)
}
@Test
fun getHorizontalPosition() {
with(defaultDensity) {
val paragraphCount = 3
val lineLength = 2
val text = createAnnotatedString(List(paragraphCount) { "a".repeat(lineLength) })
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx().value
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize
)
for (i in 0 until text.length) {
val col = i % lineLength
val expectPos = fontSizeInPx * col
val actualPos = paragraph.getHorizontalPosition(i, true)
assertWithMessage("getHorizontalPosition($i) failed")
.that(actualPos).isEqualTo(expectPos)
}
val expectPos = fontSizeInPx * lineLength
val actualPos = paragraph.getHorizontalPosition(text.length, true)
assertWithMessage("getHorizontalPosition(${text.length}) failed")
.that(actualPos).isEqualTo(expectPos)
}
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getHorizontalPosition_negative_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getHorizontalPosition(-1, true)
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getHorizontalPosition_larger_than_length_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getHorizontalPosition(text.length + 1, true)
}
@Test
fun getParagraphDirection_textDirection_Default() {
val text = createAnnotatedString("a", "\u05D0", " ")
val paragraph = simpleMultiParagraph(text = text)
assertThat(paragraph.getParagraphDirection(0)).isEqualTo(TextDirection.Ltr)
assertThat(paragraph.getParagraphDirection(1)).isEqualTo(TextDirection.Rtl)
assertThat(paragraph.getParagraphDirection(2)).isEqualTo(TextDirection.Ltr)
assertThat(paragraph.getParagraphDirection(3)).isEqualTo(TextDirection.Ltr)
}
@Test
fun getParagraphDirection_textDirection_ContentOrLtr() {
val text = createAnnotatedString("a", "\u05D0", " ")
val paragraph = simpleMultiParagraph(
text = text,
style = TextStyle(
textDirectionAlgorithm = TextDirectionAlgorithm.ContentOrLtr
)
)
assertThat(paragraph.getParagraphDirection(0)).isEqualTo(TextDirection.Ltr)
assertThat(paragraph.getParagraphDirection(1)).isEqualTo(TextDirection.Rtl)
assertThat(paragraph.getParagraphDirection(2)).isEqualTo(TextDirection.Ltr)
assertThat(paragraph.getParagraphDirection(3)).isEqualTo(TextDirection.Ltr)
}
@Test
fun getParagraphDirection_textDirection_ContentOrRtl() {
val text = createAnnotatedString("a", "\u05D0", " ")
val paragraph = simpleMultiParagraph(
text = text,
style = TextStyle(
textDirectionAlgorithm = TextDirectionAlgorithm.ContentOrRtl
)
)
assertThat(paragraph.getParagraphDirection(0)).isEqualTo(TextDirection.Ltr)
assertThat(paragraph.getParagraphDirection(1)).isEqualTo(TextDirection.Rtl)
assertThat(paragraph.getParagraphDirection(2)).isEqualTo(TextDirection.Rtl)
assertThat(paragraph.getParagraphDirection(3)).isEqualTo(TextDirection.Rtl)
}
@Test
fun getParagraphDirection_textDirection_ForceLtr() {
val text = createAnnotatedString("a", "\u05D0", " ")
val paragraph = simpleMultiParagraph(
text = text,
style = TextStyle(
textDirectionAlgorithm = TextDirectionAlgorithm.ForceLtr
)
)
for (i in 0..text.length) {
assertWithMessage("getParagraphDirection($i) failed")
.that(paragraph.getParagraphDirection(i)).isEqualTo(TextDirection.Ltr)
}
}
@Test
fun getParagraphDirection_textDirection_ForceRtl() {
val text = createAnnotatedString("a", "\u05D0", " ")
val paragraph = simpleMultiParagraph(
text = text,
style = TextStyle(
textDirectionAlgorithm = TextDirectionAlgorithm.ForceRtl
)
)
for (i in 0..text.length) {
assertWithMessage("getParagraphDirection($i) failed")
.that(paragraph.getParagraphDirection(i)).isEqualTo(TextDirection.Rtl)
}
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getParagraphDirection_negative_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getParagraphDirection(-1)
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getParagraphDirection_larger_than_length_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getParagraphDirection(text.length + 1)
}
@Test
fun getBidiRunDirection() {
with(defaultDensity) {
val text = createAnnotatedString("a\u05D0", "\u05D0a")
val paragraph = simpleMultiParagraph(text = text)
assertThat(paragraph.getBidiRunDirection(0)).isEqualTo(TextDirection.Ltr)
assertThat(paragraph.getBidiRunDirection(1)).isEqualTo(TextDirection.Rtl)
assertThat(paragraph.getBidiRunDirection(2)).isEqualTo(TextDirection.Rtl)
assertThat(paragraph.getBidiRunDirection(3)).isEqualTo(TextDirection.Ltr)
}
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getBidiRunDirection_negative_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getBidiRunDirection(-1)
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getBidiRunDirection_larger_than_length_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getBidiRunDirection(text.length + 1)
}
@Test
fun getWordBoundary() {
val text = createAnnotatedString("ab cd", "e f")
val paragraph = simpleMultiParagraph(text = text)
val textString = text.text
assertThat(paragraph.getWordBoundary(textString.indexOf('a')))
.isEqualTo(
TextRange(
textString.indexOf('a'),
textString.indexOf('b') + 1
)
)
assertThat(paragraph.getWordBoundary(textString.indexOf('d')))
.isEqualTo(
TextRange(
textString.indexOf('c'),
textString.indexOf('d') + 1
)
)
assertThat(paragraph.getWordBoundary(textString.indexOf('e')))
.isEqualTo(
TextRange(
textString.indexOf('e'),
textString.indexOf('e') + 1
)
)
assertThat(paragraph.getWordBoundary(textString.indexOf('f')))
.isEqualTo(
TextRange(
textString.indexOf('f'),
textString.indexOf('f') + 1
)
)
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getWordBoundary_negative_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getWordBoundary(-1)
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getWordBoundary_larger_than_length_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getWordBoundary(text.length + 1)
}
@Test
fun getCursorRect() {
with(defaultDensity) {
val paragraphCount = 3
val lineLength = 2
// A text with 3 lines and each line has 2 characters.
val text = createAnnotatedString(List(paragraphCount) { "a".repeat(lineLength) })
val fontSize = 10.sp
val fontSizeInPx = fontSize.toPx().value
val width = 2 * fontSizeInPx
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
width = width
)
for (i in 0 until text.length) {
val row = i / lineLength
val col = i % lineLength
val top = row * fontSizeInPx
val cursorXOffset = col * fontSizeInPx
val expectRect = Rect(
left = cursorXOffset - cursorWidth / 2,
top = top,
right = cursorXOffset + cursorWidth / 2,
bottom = top + fontSizeInPx
)
val actualRect = paragraph.getCursorRect(i)
assertWithMessage("getCursorRect($i) failed")
.that(actualRect).isEqualTo(expectRect)
}
// Last cursor position is the end of the last line.
val row = paragraph.lineCount - 1
val col = lineLength
val top = row * fontSizeInPx
val cursorXOffset = col * fontSizeInPx
val expectRect = Rect(
left = cursorXOffset - cursorWidth / 2,
top = top,
right = cursorXOffset + cursorWidth / 2,
bottom = top + fontSizeInPx
)
val actualRect = paragraph.getCursorRect(text.length)
assertWithMessage("getCursorRect(${text.length}) failed")
.that(actualRect).isEqualTo(expectRect)
}
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getCursorRect_negative_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getCursorRect(-1)
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getCursorRect_larger_than_length_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getCursorRect(text.length + 1)
}
@Test
fun getLineForOffset() {
val text = createAnnotatedString("a", "a\na")
val paragraph = simpleMultiParagraph(text = text)
assertThat(paragraph.getLineForOffset(0)).isEqualTo(0)
assertThat(paragraph.getLineForOffset(1)).isEqualTo(1)
// '\n' is not checked because it's Paragraph's implementation
assertThat(paragraph.getLineForOffset(3)).isEqualTo(2)
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getLineForOffset_negative_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getLineForOffset(-1)
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getLineForOffset_larger_than_length_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getLineForOffset(text.length + 1)
}
@Test
fun getLineLeft() {
with(defaultDensity) {
val text = createAnnotatedString("aa", "\u05D0\u05D0")
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx().value
val width = simpleMultiParagraphIntrinsics(text, fontSize).maxIntrinsicWidth * 2
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
width = width
)
assertThat(paragraph.getLineLeft(0)).isEqualTo(0)
assertThat(paragraph.getLineLeft(1)).isEqualTo(width - 2 * fontSizeInPx)
}
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getLineLeft_negative_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getLineLeft(-1)
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getLineLeft_greaterThanOrEqual_lineCount_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getLineLeft(paragraph.lineCount)
}
@Test
fun getLineRight() {
with(defaultDensity) {
val text = createAnnotatedString("aa", "\u05D0\u05D0")
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx().value
val width = simpleMultiParagraphIntrinsics(text, fontSize).maxIntrinsicWidth * 2
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
width = width
)
assertThat(paragraph.getLineRight(0)).isEqualTo(2 * fontSizeInPx)
assertThat(paragraph.getLineRight(1)).isEqualTo(width)
}
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getLineRight_negative_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getLineRight(-1)
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getLineRight_greaterThanOrEqual_lineCount_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getLineRight(paragraph.lineCount)
}
@Test
fun getLineBottom() {
with(defaultDensity) {
val text = createAnnotatedString("a", "a", "a")
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx().value
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize
)
for (i in 0 until paragraph.lineCount) {
assertWithMessage("bottom of line $i doesn't match")
.that(paragraph.getLineBottom(i))
.isEqualTo(fontSizeInPx * (i + 1))
}
}
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getLineBottom_negative_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getLineBottom(-1)
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getLineBottom_greaterThanOrEqual_lineCount_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getLineBottom(paragraph.lineCount)
}
@Test
fun getLineHeight() {
with(defaultDensity) {
val text = createAnnotatedString("a", "a", "a")
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx().value
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize
)
for (i in 0 until paragraph.lineCount) {
assertWithMessage("getLineHeight($i) failed")
.that(paragraph.getLineHeight(i)).isEqualTo(fontSizeInPx)
}
}
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getLineHeight_negative_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getLineHeight(-1)
}
@Test(expected = java.lang.IllegalArgumentException::class)
fun getLineHeight_greaterThanOrEqual_lineCount_throw_exception() {
val text = "abc"
val paragraph = simpleMultiParagraph(text = text)
paragraph.getLineHeight(paragraph.lineCount)
}
@Test
fun textAlign_defaultValue_alignsStart() {
val textLtr = "aa"
val textRtl = "\u05D0\u05D0"
val text = createAnnotatedString(textLtr, textRtl)
// Width should be sufficient to make each paragraph one line.
val width = 2 * simpleMultiParagraphIntrinsics(text).maxIntrinsicWidth
val paragraph = simpleMultiParagraph(text = text, width = width)
// When text align to start, Ltr text aligns to left, line left should be 0.
assertThat(paragraph.getLineLeft(0)).isZero()
// When text align to start, Rtl text aligns to right, line right should be width.
assertThat(paragraph.getLineRight(1)).isEqualTo(width)
}
@Test
fun textAlign_left_returnsZeroForGetLineLeft() {
val textLtr = "aa"
val textRtl = "\u05D0\u05D0"
val text = createAnnotatedString(textLtr, textRtl)
// Width should be sufficient to make each paragraph one line.
val width = 2 * simpleMultiParagraphIntrinsics(text).maxIntrinsicWidth
val paragraph = simpleMultiParagraph(
text = text,
width = width,
style = TextStyle(textAlign = TextAlign.Left)
)
// When text align to left, line left should be 0 for both Ltr and Rtl text.
assertThat(paragraph.getLineLeft(0)).isZero()
assertThat(paragraph.getLineLeft(1)).isZero()
}
@Test
fun textAlign_right_returnsWidthForGetLineRight() {
val textLtr = "aa"
val textRtl = "\u05D0\u05D0"
val text = createAnnotatedString(textLtr, textRtl)
// Width should be sufficient to make each paragraph one line.
val width = 2 * simpleMultiParagraphIntrinsics(text).maxIntrinsicWidth
val paragraph = simpleMultiParagraph(
text = text,
width = width,
style = TextStyle(textAlign = TextAlign.Right)
)
// When text align to right, line right should be width for both Ltr and Rtl text.
assertThat(paragraph.getLineRight(0)).isEqualTo(width)
assertThat(paragraph.getLineRight(1)).isEqualTo(width)
}
@Test
fun textAlign_center_textIsCentered() {
with(defaultDensity) {
val textLtr = "aa"
val textRtl = "\u05D0\u05D0"
val text = createAnnotatedString(textLtr, textRtl)
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx().value
// Width should be sufficient to make each paragraph one line.
val width = 2 * simpleMultiParagraphIntrinsics(text, fontSize).maxIntrinsicWidth
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
width = width,
style = TextStyle(textAlign = TextAlign.Center)
)
val expectedLineLeft = width / 2 - (fontSizeInPx * textLtr.length) / 2
val expectedLineRight = width / 2 + (fontSizeInPx * textLtr.length) / 2
assertThat(paragraph.getLineLeft(0)).isEqualTo(expectedLineLeft)
assertThat(paragraph.getLineRight(0)).isEqualTo(expectedLineRight)
assertThat(paragraph.getLineLeft(1)).isEqualTo(expectedLineLeft)
assertThat(paragraph.getLineRight(1)).isEqualTo(expectedLineRight)
}
}
@Test
@SdkSuppress(minSdkVersion = 28)
// We have to test strict justification above API 28 because of this bug b/68009059, where
// devices before API 28 may have an extra space at the end of line.
fun textAlign_justify_justifies() {
val textLtr = "a a a"
val textRtl = "\u05D0 \u05D0 \u05D0"
val text = createAnnotatedString(textLtr, textRtl)
// Justify only works for soft wrapped lines, so width is made insufficient.
val width = simpleMultiParagraphIntrinsics(text).maxIntrinsicWidth - 1f
val paragraph = simpleMultiParagraph(
text = text,
style = TextStyle(textAlign = TextAlign.Justify),
width = width
)
// When text is justified, line left is 0 while line right is width
assertThat(paragraph.getLineLeft(0)).isZero()
assertThat(paragraph.getLineRight(0)).isEqualTo(width)
assertThat(paragraph.getLineLeft(2)).isZero()
assertThat(paragraph.getLineRight(2)).isEqualTo(width)
}
@Test
@SdkSuppress(maxSdkVersion = 27, minSdkVersion = 26)
fun textAlign_justify_justifies_underApi28() {
with(defaultDensity) {
val textLtr = "a a a"
val textRtl = "\u05D0 \u05D0 \u05D0"
val text = createAnnotatedString(textLtr, textRtl)
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx().value
// Justify only works for soft wrapped lines, so width is made insufficient.
val width = simpleMultiParagraphIntrinsics(text, fontSize).maxIntrinsicWidth - 1f
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
style = TextStyle(textAlign = TextAlign.Justify),
width = width
)
// When Ltr text is justified, line left is 0.
assertThat(paragraph.getLineLeft(0)).isZero()
// When Ltr text is justified, line right is greater than when it's align left. We
// can only assert a weaker condition due to bug b/68009059, where extra space is
// added at the end of the line.
assertThat(paragraph.getLineRight(0))
.isGreaterThan("a a".length * fontSizeInPx)
// When Rtl text is justified, line right is width.
assertThat(paragraph.getLineRight(2)).isEqualTo(width)
// Similar to Ltr text, when Rtl text is justified, line left is less than when it's
// align right.
assertThat(paragraph.getLineLeft(2))
.isLessThan(width - "\u05D0 \u05D0".length * fontSizeInPx)
}
}
@Test
fun textAlign_start_alignsStart() {
val textLtr = "aa"
val textRtl = "\u05D0\u05D0"
val text = createAnnotatedString(textLtr, textRtl)
// Width should be sufficient to make each paragraph one line.
val width = 2 * simpleMultiParagraphIntrinsics(text).maxIntrinsicWidth
val paragraph = simpleMultiParagraph(
text = text,
style = TextStyle(textAlign = TextAlign.Start),
width = width
)
// When text align to start, Ltr text aligns to left, line left should be 0.
assertThat(paragraph.getLineLeft(0)).isZero()
// When text align to start, Rtl text aligns to right, line right should be width.
assertThat(paragraph.getLineRight(1)).isEqualTo(width)
}
@Test
fun textAlign_end_alignsEnd() {
val textLtr = "aa"
val textRtl = "\u05D0\u05D0"
val text = createAnnotatedString(textLtr, textRtl)
// Width should be sufficient to make each paragraph one line.
val width = 2 * simpleMultiParagraphIntrinsics(text).maxIntrinsicWidth
val paragraph = simpleMultiParagraph(
text = text,
style = TextStyle(textAlign = TextAlign.End),
width = width
)
// When text align to start, Ltr text aligns to right, line right should be width.
assertThat(paragraph.getLineRight(0)).isEqualTo(width)
// When text align to start, Rtl text aligns to left, line left should 0.
assertThat(paragraph.getLineLeft(1)).isZero()
}
@Test
fun textDirectionAlgorithm_defaultValue() {
with(defaultDensity) {
val textLtr = "a ."
val textRtl = "\u05D0 ."
val textNeutral = " ."
val text = createAnnotatedString(textLtr, textRtl, textNeutral)
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx().value
val width = simpleMultiParagraphIntrinsics(text, fontSize).maxIntrinsicWidth
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
width = width
)
// First paragraph should be rendered as: "a .", dot is visually after "a ".
assertThat(paragraph.getHorizontalPosition(2, true))
.isEqualTo("a ".length * fontSizeInPx)
// Second paragraph should be rendered as: ". א", dot is visually before " א".
assertThat(paragraph.getHorizontalPosition(5, true))
.isEqualTo(width - "\u05D0 ".length * fontSizeInPx)
// Third paragraph should be rendered as: " .", dot is visually after " ".
assertThat(paragraph.getHorizontalPosition(8, true))
.isEqualTo(" ".length * fontSizeInPx)
}
}
@Test
fun textDirectionAlgorithm_contentOrLtr() {
with(defaultDensity) {
val textLtr = "a ."
val textRtl = "\u05D0 ."
val textNeutral = " ."
val text = createAnnotatedString(textLtr, textRtl, textNeutral)
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx().value
val width = simpleMultiParagraphIntrinsics(text, fontSize).maxIntrinsicWidth
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
style = TextStyle(textDirectionAlgorithm = TextDirectionAlgorithm.ContentOrLtr),
width = width
)
// First paragraph should be rendered as: "a .", dot is visually after "a ".
assertThat(paragraph.getHorizontalPosition(2, true))
.isEqualTo("a ".length * fontSizeInPx)
// Second paragraph should be rendered as: ". א", dot is visually before " א".
assertThat(paragraph.getHorizontalPosition(5, true))
.isEqualTo(width - "\u05D0 ".length * fontSizeInPx)
// Third paragraph should be rendered as: " .", dot is visually after " ".
assertThat(paragraph.getHorizontalPosition(8, true))
.isEqualTo(" ".length * fontSizeInPx)
}
}
@Test
fun textDirectionAlgorithm_contentOrRtl() {
with(defaultDensity) {
val textLtr = "a ."
val textRtl = "\u05D0 ."
val textNeutral = " ."
val text = createAnnotatedString(textLtr, textRtl, textNeutral)
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx().value
val width = simpleMultiParagraphIntrinsics(text, fontSize).maxIntrinsicWidth
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
style = TextStyle(textDirectionAlgorithm = TextDirectionAlgorithm.ContentOrRtl),
width = width
)
// First paragraph should be rendered as: "a .", dot is visually after "a ".
assertThat(paragraph.getHorizontalPosition(2, true))
.isEqualTo("a ".length * fontSizeInPx)
// Second paragraph should be rendered as: ". א", dot is visually before " א".
assertThat(paragraph.getHorizontalPosition(5, true))
.isEqualTo(width - "\u05D0 ".length * fontSizeInPx)
// Third paragraph should be rendered as: ". ", dot is visually before " ".
assertThat(paragraph.getHorizontalPosition(8, true))
.isEqualTo(width - " ".length * fontSizeInPx)
}
}
@Test
fun textDirectionAlgorithm_forceLtr() {
with(defaultDensity) {
val textLtr = "a ."
val textRtl = "\u05D0 ."
val textNeutral = " ."
val text = createAnnotatedString(textLtr, textRtl, textNeutral)
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx().value
val width = simpleMultiParagraphIntrinsics(text, fontSize).maxIntrinsicWidth
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
style = TextStyle(textDirectionAlgorithm = TextDirectionAlgorithm.ForceLtr),
width = width
)
// First paragraph should be rendered as: "a .", dot is visually after "a ".
assertThat(paragraph.getHorizontalPosition(2, true))
.isEqualTo("a ".length * fontSizeInPx)
// Second paragraph should be rendered as: "א .", dot is visually after "א ".
assertThat(paragraph.getHorizontalPosition(5, true))
.isEqualTo("\u05D0 ".length * fontSizeInPx)
// Third paragraph should be rendered as: " .", dot is visually after " ".
assertThat(paragraph.getHorizontalPosition(8, true))
.isEqualTo(" ".length * fontSizeInPx)
}
}
@Test
fun textDirectionAlgorithm_forceRtl() {
with(defaultDensity) {
val textLtr = "a ."
val textRtl = "\u05D0 ."
val textNeutral = " ."
val text = createAnnotatedString(textLtr, textRtl, textNeutral)
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx().value
val width = simpleMultiParagraphIntrinsics(text, fontSize).maxIntrinsicWidth
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
style = TextStyle(textDirectionAlgorithm = TextDirectionAlgorithm.ForceRtl),
width = width
)
// First paragraph should be rendered as: ". a", dot is visually before " a".
assertThat(paragraph.getHorizontalPosition(2, true))
.isEqualTo(width - "a ".length * fontSizeInPx)
// Second paragraph should be rendered as: ". א", dot is visually before " א".
assertThat(paragraph.getHorizontalPosition(5, true))
.isEqualTo(width - "\u05D0 ".length * fontSizeInPx)
// Third paragraph should be rendered as: ". ", dot is visually before " ".
assertThat(paragraph.getHorizontalPosition(8, true))
.isEqualTo(width - " ".length * fontSizeInPx)
}
}
@Test
fun lineHeight_returnsSameAsGiven() {
with(defaultDensity) {
val text = createAnnotatedString("a\na\na", "a\na\na")
// Need to specify font size in case the asserted line height happens to be the default
// line height corresponding to the font size.
val fontSize = 50.sp
val lineHeight = 80.sp
val lineHeightInPx = lineHeight.toPx().value
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
style = TextStyle(lineHeight = lineHeight)
)
// Height of first and last line in each paragraph is influenced by includePadding.
// So we only assert the inner paragraph lines' height.
assertThat(paragraph.getLineHeight(1)).isEqualTo(lineHeightInPx)
assertThat(paragraph.getLineHeight(4)).isEqualTo(lineHeightInPx)
}
}
@Test
fun textIndent_onFirstLine() {
with(defaultDensity) {
val text = createAnnotatedString("aaa", "\u05D0\u05D0\u05D0")
val indent = 20.sp
val indentInPx = indent.toPx().value
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx().value
// Width is the space needed by 2 characters
val width = 2 * fontSizeInPx
val paragraph = simpleMultiParagraph(
text = text,
style = TextStyle(textIndent = TextIndent(firstLine = indent)),
fontSize = fontSize,
width = width
)
// The paragraph should be rendered as:
// a
// aa
// א
// אא
assertThat(paragraph.getHorizontalPosition(0, true)).isEqualTo(indentInPx)
assertThat(paragraph.getHorizontalPosition(1, true)).isZero()
assertThat(paragraph.getHorizontalPosition(3, true)).isEqualTo(width - indentInPx)
assertThat(paragraph.getHorizontalPosition(4, true)).isEqualTo(width)
}
}
@Test
fun textIndent_onRestLine() {
with(defaultDensity) {
val text = createAnnotatedString("aaa", "\u05D0\u05D0\u05D0")
val indent = 20.sp
val indentInPx = indent.toPx().value
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx().value
// Width is the space needed by 2 characters
val width = 2 * fontSizeInPx
val paragraph = simpleMultiParagraph(
text = text,
fontSize = fontSize,
style = TextStyle(textIndent = TextIndent(restLine = indent)),
width = width
)
// The paragraph should be rendered as:
// aa
// a
// אא
// א
assertThat(paragraph.getHorizontalPosition(0, true)).isZero()
assertThat(paragraph.getHorizontalPosition(2, true)).isEqualTo(indentInPx)
assertThat(paragraph.getHorizontalPosition(3, true)).isEqualTo(width)
assertThat(paragraph.getHorizontalPosition(5, true)).isEqualTo(width - indentInPx)
}
}
@Test(expected = IllegalArgumentException::class)
fun constructor_throwsException_ifTextDirectionAlgorithmIsNotSet() {
MultiParagraph(
annotatedString = createAnnotatedString(""),
style = TextStyle(),
constraints = ParagraphConstraints(Float.MAX_VALUE),
density = defaultDensity,
resourceLoader = TestFontResourceLoader(context)
)
}
@Test
fun annotatedString_haveParagraphStyle_withoutTextDirection() {
val textDirectionAlgorithm = TextDirectionAlgorithm.ForceRtl
// Provide an LTR text
val text = AnnotatedString(
text = "ab",
paragraphStyles = listOf(
ParagraphStyleItem(
item = ParagraphStyle(
textDirectionAlgorithm = TextDirectionAlgorithm.ContentOrLtr
),
start = 0,
end = "a".length
),
ParagraphStyleItem(
// skip setting [TextDirectionAlgorithm] on purpose, should inherit from the
// main [ParagraphStyle]
item = ParagraphStyle(),
start = "a".length,
end = "ab".length
)
)
)
val paragraph = MultiParagraph(
annotatedString = text,
style = TextStyle(textDirectionAlgorithm = textDirectionAlgorithm),
constraints = ParagraphConstraints(Float.MAX_VALUE),
density = defaultDensity,
resourceLoader = TestFontResourceLoader(context)
)
// the first character uses TextDirectionAlgorithm.ContentOrLtr
assertThat(paragraph.getParagraphDirection(0)).isEqualTo(TextDirection.Ltr)
// the second character should use TextDirectionAlgorithm.ForceRtlsince it should inherit
// from main [ParagraphStyle]
assertThat(paragraph.getParagraphDirection(1)).isEqualTo(TextDirection.Rtl)
}
/**
* Helper function which creates an AnnotatedString where each input string becomes a paragraph.
*/
private fun createAnnotatedString(vararg paragraphs: String) =
createAnnotatedString(paragraphs.toList())
/**
* Helper function which creates an AnnotatedString where each input string becomes a paragraph.
*/
private fun createAnnotatedString(paragraphs: List<String>): AnnotatedString {
return AnnotatedString {
for (paragraph in paragraphs) {
pushStyle(ParagraphStyle())
append(paragraph)
popStyle()
}
}
}
private fun simpleMultiParagraphIntrinsics(
text: AnnotatedString,
fontSize: TextUnit = TextUnit.Inherit
): MultiParagraphIntrinsics {
return MultiParagraphIntrinsics(
text,
style = TextStyle(
fontFamily = fontFamilyMeasureFont,
fontSize = fontSize,
textDirectionAlgorithm = TextDirectionAlgorithm.ContentOrLtr
),
density = defaultDensity,
resourceLoader = TestFontResourceLoader(context)
)
}
private fun simpleMultiParagraph(
text: String,
style: TextStyle? = null,
fontSize: TextUnit = TextUnit.Inherit,
maxLines: Int = Int.MAX_VALUE,
width: Float = Float.MAX_VALUE
): MultiParagraph {
return MultiParagraph(
annotatedString = createAnnotatedString(text),
style = TextStyle(
fontFamily = fontFamilyMeasureFont,
fontSize = fontSize,
textDirectionAlgorithm = TextDirectionAlgorithm.ContentOrLtr
).merge(style),
maxLines = maxLines,
constraints = ParagraphConstraints(width),
density = defaultDensity,
resourceLoader = TestFontResourceLoader(context)
)
}
private fun simpleMultiParagraph(
text: AnnotatedString,
style: TextStyle? = null,
fontSize: TextUnit = TextUnit.Inherit,
maxLines: Int = Int.MAX_VALUE,
width: Float = Float.MAX_VALUE
): MultiParagraph {
return MultiParagraph(
annotatedString = text,
style = TextStyle(
fontFamily = fontFamilyMeasureFont,
fontSize = fontSize,
textDirectionAlgorithm = TextDirectionAlgorithm.ContentOrLtr
).merge(style),
maxLines = maxLines,
constraints = ParagraphConstraints(width),
density = defaultDensity,
resourceLoader = TestFontResourceLoader(context)
)
}
}