blob: 7cd86878068dab6dbae553e085413b8be87b667f [file] [log] [blame]
/*
* Copyright 2023 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.foundation.text.modifiers
import android.content.Context
import android.graphics.Typeface
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.verticalScroll
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.AndroidFont
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontLoadingStrategy
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.font.createFontFamilyResolver
import androidx.compose.ui.text.font.toFontFamily
import androidx.compose.ui.unit.sp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import androidx.test.platform.app.InstrumentationRegistry
import com.google.common.truth.Truth
import java.util.concurrent.atomic.AtomicInteger
import kotlin.test.fail
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import org.junit.Ignore
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@MediumTest
@RunWith(AndroidJUnit4::class)
class TextStringSimpleNodeTest {
@get:Rule
val rule = createComposeRule()
val context: Context = InstrumentationRegistry.getInstrumentation().context
@Test
fun draw_whenNotAttached_doesNotCrash() {
val subject = TextStringSimpleNode(
"text", TextStyle.Default, createFontFamilyResolver(context)
)
rule.setContent {
Canvas(Modifier.fillMaxSize()) {
val contentDrawScope = object : ContentDrawScope, DrawScope by this {
override fun drawContent() {
fail("Not used")
}
} as ContentDrawScope
with(subject) {
contentDrawScope.draw()
}
}
}
rule.waitForIdle()
}
@Test
fun exceedsMaxConstraintSize_doesNotCrash() {
rule.setContent {
val state = rememberScrollState()
Column(Modifier.verticalScroll(state)) {
BasicText(
text = "text\n".repeat(10_000),
style = TextStyle(fontSize = 50.sp),
)
}
}
}
// TODO(b/279797016) re-enable this test, and add a path for AnnotatedString
@Ignore("b/279797016 drawBehind is currently broken in tot")
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun asyncTextResolution_causesRedraw() {
val loadDeferred = CompletableDeferred<Unit>()
val drawChannel = Channel<Unit>(capacity = Channel.UNLIMITED)
val drawCount = AtomicInteger(0)
val asyncFont = makeAsyncFont(loadDeferred)
val subject = TextStringSimpleElement(
"til",
TextStyle.Default.copy(fontFamily = asyncFont.toFontFamily()),
createFontFamilyResolver(context)
)
val modifier = Modifier.fillMaxSize() then subject then Modifier.drawBehind {
drawRect(Color.Magenta, size = Size(100f, 100f))
drawCount.incrementAndGet()
drawChannel.trySend(Unit)
}
rule.setContent {
Layout(modifier) { _, constraints ->
layout(constraints.maxWidth, constraints.maxHeight) {}
}
}
rule.waitForIdle()
runBlocking {
// empty the draw channel here. sentContent already ensured that draw ran.
// we just need this for sequencing AtomicInteger read/write/read later
Truth.assertThat(drawChannel.isEmpty).isFalse()
while (!drawChannel.isEmpty) {
drawChannel.receive()
}
}
val initialCount = drawCount.get()
Truth.assertThat(initialCount).isGreaterThan(0)
// this may take a while to make compose non-idle, so wait for drawChannel explicit sync
loadDeferred.complete(Unit)
runBlocking { withTimeout(1_000L) {
drawChannel.receive()
}
}
rule.waitForIdle()
Truth.assertThat(drawCount.get()).isGreaterThan(initialCount)
}
private fun makeAsyncFont(loadDeferred: Deferred<Unit>): Font {
val typefaceLoader = object : AndroidFont.TypefaceLoader {
override fun loadBlocking(context: Context, font: AndroidFont): Typeface? {
TODO("Not yet implemented")
}
override suspend fun awaitLoad(context: Context, font: AndroidFont): Typeface? {
loadDeferred.await()
return Typeface.create("cursive", 0)
}
}
return object : AndroidFont(
FontLoadingStrategy.Async,
typefaceLoader,
FontVariation.Settings()
) {
override val weight: FontWeight
get() = FontWeight.Normal
override val style: FontStyle
get() = FontStyle.Normal
}
}
}