blob: e110330c9b6663e4886d7b547e59c376ee72897a [file] [log] [blame]
/*
* Copyright 2020 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.compose.ui.text
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Canvas
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.platform.Font
import androidx.compose.ui.text.style.TextDirection
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.sp
import com.google.common.truth.Truth
import org.junit.Assume.assumeTrue
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.JUnit4
@RunWith(JUnit4::class)
class DesktopParagraphTest {
@get:Rule
val rule = createComposeRule()
private val fontFamilyResolver = createFontFamilyResolver()
private val defaultDensity = Density(density = 1f)
private val fontFamilyMeasureFont =
FontFamily(
Font(
"font/sample_font.ttf",
weight = FontWeight.Normal,
style = FontStyle.Normal
)
)
@Test
@Ignore("b/271123970 Fails in AOSP. Will be fixed after upstreaming Compose for Desktop")
fun getBoundingBox_basic() {
with(defaultDensity) {
val text = "abc"
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx()
val paragraph = simpleParagraph(
text = text,
style = TextStyle(fontSize = fontSize)
)
for (i in 0..text.length - 1) {
val box = paragraph.getBoundingBox(i)
Truth.assertThat(box.left).isEqualTo(i * fontSizeInPx)
Truth.assertThat(box.right).isEqualTo((i + 1) * fontSizeInPx)
Truth.assertThat(box.top).isZero()
Truth.assertThat(box.bottom).isEqualTo(fontSizeInPx + 10)
}
}
}
@Test
@Ignore("b/271123970 Fails in AOSP. Will be fixed after upstreaming Compose for Desktop")
fun getBoundingBox_multicodepoints() {
assumeTrue(isLinux)
with(defaultDensity) {
val text = "h\uD83E\uDDD1\uD83C\uDFFF\u200D\uD83E\uDDB0"
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx()
val paragraph = simpleParagraph(
text = text,
style = TextStyle(fontSize = 50.sp)
)
Truth.assertThat(paragraph.getBoundingBox(0))
.isEqualTo(Rect(0f, 0f, fontSizeInPx, 60f))
Truth.assertThat(paragraph.getBoundingBox(1))
.isEqualTo(Rect(fontSizeInPx, 0f, fontSizeInPx * 2.5f, 60f))
Truth.assertThat(paragraph.getBoundingBox(5))
.isEqualTo(Rect(fontSizeInPx, 0f, fontSizeInPx * 2.5f, 60f))
}
}
@Test
fun getLineForOffset() {
val text = "ab\na"
val paragraph = simpleParagraph(
text = text,
style = TextStyle(fontSize = 50.sp)
)
Truth.assertThat(paragraph.getLineForOffset(2))
.isEqualTo(0)
Truth.assertThat(paragraph.getLineForOffset(3))
.isEqualTo(1)
}
@Test
fun getLineEnd() {
with(defaultDensity) {
val text = ""
val paragraph = simpleParagraph(
text = text,
style = TextStyle(fontSize = 50.sp)
)
Truth.assertThat(paragraph.getLineEnd(0, true))
.isEqualTo(0)
}
with(defaultDensity) {
val text = "ab\n\nc"
val paragraph = simpleParagraph(
text = text,
style = TextStyle(fontSize = 50.sp)
)
Truth.assertThat(paragraph.getLineEnd(0, true))
.isEqualTo(2)
Truth.assertThat(paragraph.getLineEnd(1, true))
.isEqualTo(3)
Truth.assertThat(paragraph.getLineEnd(2, true))
.isEqualTo(5)
}
with(defaultDensity) {
val text = "ab\n"
val paragraph = simpleParagraph(
text = text,
style = TextStyle(fontSize = 50.sp)
)
Truth.assertThat(paragraph.getLineEnd(0, true))
.isEqualTo(2)
Truth.assertThat(paragraph.getLineEnd(1, true))
.isEqualTo(3)
}
}
@Test
fun getLineBaseline() {
val text = "abc\nabc\nabc"
val paragraph = simpleParagraph(text = text)
Truth.assertThat(paragraph.getLineBaseline(0)).isEqualTo(paragraph.firstBaseline)
Truth.assertThat(paragraph.getLineBaseline(2)).isEqualTo(paragraph.lastBaseline)
}
@Test
fun getHorizontalPositionForOffset_primary_Bidi_singleLine_textDirectionDefault() {
with(defaultDensity) {
val ltrText = "abc"
val rtlText = "\u05D0\u05D1\u05D2"
val text = ltrText + rtlText
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx()
val width = text.length * fontSizeInPx
val paragraph = simpleParagraph(
text = text,
style = TextStyle(fontSize = fontSize),
width = width
)
for (i in 0..ltrText.length) {
Truth.assertThat(paragraph.getHorizontalPosition(i, true))
.isEqualTo(fontSizeInPx * i)
}
for (i in 1 until rtlText.length) {
Truth.assertThat(paragraph.getHorizontalPosition(i + ltrText.length, true))
.isEqualTo(width - fontSizeInPx * i)
}
}
}
@Test
fun getHorizontalPositionForOffset_notPrimary_Bidi_singleLine_textDirectionLtr() {
with(defaultDensity) {
val ltrText = "abc"
val rtlText = "\u05D0\u05D1\u05D2"
val text = ltrText + rtlText
val fontSize = 50.sp
val fontSizeInPx = fontSize.toPx()
val width = text.length * fontSizeInPx
val paragraph = simpleParagraph(
text = text,
style = TextStyle(
fontSize = fontSize,
textDirection = TextDirection.Ltr
),
width = width
)
for (i in ltrText.indices) {
Truth.assertThat(paragraph.getHorizontalPosition(i, false))
.isEqualTo(fontSizeInPx * i)
}
for (i in rtlText.indices) {
Truth.assertThat(paragraph.getHorizontalPosition(i + ltrText.length, false))
.isEqualTo(width - fontSizeInPx * i)
}
Truth.assertThat(paragraph.getHorizontalPosition(text.length, false))
.isEqualTo(width - rtlText.length * fontSizeInPx)
}
}
@Test
fun getWordBoundary_spaces() {
val text = "ab cd e"
val paragraph = simpleParagraph(
text = text,
style = TextStyle(
fontFamily = fontFamilyMeasureFont,
fontSize = 20.sp
)
)
val singleSpaceStartResult = paragraph.getWordBoundary(text.indexOf('b') + 1)
Truth.assertThat(singleSpaceStartResult.start).isEqualTo(text.indexOf('a'))
Truth.assertThat(singleSpaceStartResult.end).isEqualTo(text.indexOf('b') + 1)
val singleSpaceEndResult = paragraph.getWordBoundary(text.indexOf('c'))
Truth.assertThat(singleSpaceEndResult.start).isEqualTo(text.indexOf('c'))
Truth.assertThat(singleSpaceEndResult.end).isEqualTo(text.indexOf('d') + 1)
val doubleSpaceResult = paragraph.getWordBoundary(text.indexOf('d') + 2)
Truth.assertThat(doubleSpaceResult.start).isEqualTo(text.indexOf('d') + 2)
Truth.assertThat(doubleSpaceResult.end).isEqualTo(text.indexOf('d') + 2)
}
@Test
fun two_paragraphs_use_common_intrinsics() {
fun Paragraph.testOffset() = getOffsetForPosition(Offset(0f, 100000f))
fun Paragraph.paint() = paint(Canvas(ImageBitmap(100, 100)))
val intrinsics = simpleIntrinsics((1..1000).joinToString(" "))
val paragraph1 = simpleParagraph(intrinsics, width = 100f)
val offset1 = paragraph1.testOffset()
val paragraph2 = simpleParagraph(intrinsics, width = 100000f)
val offset2 = paragraph2.testOffset()
Truth.assertThat(paragraph1.testOffset()).isEqualTo(offset1)
Truth.assertThat(paragraph2.testOffset()).isEqualTo(offset2)
paragraph2.paint()
Truth.assertThat(paragraph1.testOffset()).isEqualTo(offset1)
Truth.assertThat(paragraph2.testOffset()).isEqualTo(offset2)
paragraph1.paint()
Truth.assertThat(paragraph1.testOffset()).isEqualTo(offset1)
Truth.assertThat(paragraph2.testOffset()).isEqualTo(offset2)
paragraph2.paint()
Truth.assertThat(paragraph1.testOffset()).isEqualTo(offset1)
Truth.assertThat(paragraph2.testOffset()).isEqualTo(offset2)
}
@Test
fun `line heights`() {
val paragraph = simpleParagraph(
text = "aaa\n\naaa\n\n\naaa\n \naaa",
style = TextStyle(fontSize = 50.sp)
)
val firstLineHeight = paragraph.getLineHeight(0)
for (i in 1 until paragraph.lineCount) {
Truth.assertThat(paragraph.getLineHeight(i)).isEqualTo(firstLineHeight)
}
}
private fun simpleParagraph(
text: String = "",
style: TextStyle? = null,
maxLines: Int = Int.MAX_VALUE,
ellipsis: Boolean = false,
spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
density: Density? = null,
width: Float = 2000f
): Paragraph {
return Paragraph(
text = text,
spanStyles = spanStyles,
style = TextStyle(
fontFamily = fontFamilyMeasureFont
).merge(style),
maxLines = maxLines,
ellipsis = ellipsis,
constraints = Constraints(maxWidth = width.ceilToInt()),
density = density ?: defaultDensity,
fontFamilyResolver = fontFamilyResolver
)
}
private fun simpleIntrinsics(
text: String = "",
style: TextStyle? = null,
spanStyles: List<AnnotatedString.Range<SpanStyle>> = listOf(),
density: Density? = null
): ParagraphIntrinsics {
return ParagraphIntrinsics(
text = text,
spanStyles = spanStyles,
style = TextStyle(
fontFamily = fontFamilyMeasureFont
).merge(style),
density = density ?: defaultDensity,
fontFamilyResolver = fontFamilyResolver
)
}
private fun simpleParagraph(
intrinsics: ParagraphIntrinsics,
maxLines: Int = Int.MAX_VALUE,
ellipsis: Boolean = false,
width: Float = 2000f
): Paragraph {
return Paragraph(
paragraphIntrinsics = intrinsics,
maxLines = maxLines,
ellipsis = ellipsis,
constraints = Constraints(maxWidth = width.ceilToInt()),
)
}
}