diff --git a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt index 4ebc287002..d133b7eda0 100644 --- a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt +++ b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt @@ -2,37 +2,31 @@ package org.jetbrains.compose.resources import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.ui.text.ExperimentalTextApi import androidx.compose.ui.text.font.* import androidx.compose.ui.text.platform.Font +import androidx.compose.ui.text.platform.SystemFont import androidx.compose.ui.unit.Density -import kotlin.io.encoding.Base64 -import kotlin.io.encoding.ExperimentalEncodingApi -private val emptyFontBase64 = - "T1RUTwAJAIAAAwAQQ0ZGIML7MfIAAAQIAAAA2U9TLzJmMV8PAAABAAAAAGBjbWFwANUAVwAAA6QAAABEaGVhZCMuU7" + - "IAAACcAAAANmhoZWECvgAmAAAA1AAAACRobXR4Az4AAAAABOQAAAAQbWF4cAAEUAAAAAD4AAAABm5hbWUpw3nbAAABYAAAAkNwb3N0AAMA" + - "AAAAA+gAAAAgAAEAAAABAADs7nftXw889QADA+gAAAAA4WWJaQAAAADhZYlpAAAAAAFNAAAAAAADAAIAAAAAAAAAAQAAArz+1AAAAU0AAA" + - "AAAAAAAQAAAAAAAAAAAAAAAAAAAAQAAFAAAAQAAAADAHwB9AAFAAACigK7AAAAjAKKArsAAAHfADEBAgAAAAAAAAAAAAAAAAAAAAEAAAAA" + - "AAAAAAAAAABYWFhYAEAAIABfArz+1AAAAAAAAAAAAAEAAAAAAV4AAAAgACAAAAAAACIBngABAAAAAAAAAAIAbwABAAAAAAABAAUAAAABAA" + - "AAAAACAAcADwABAAAAAAADABAAdQABAAAAAAAEAA0AJAABAAAAAAAFAAIAbwABAAAAAAAGAAwASwABAAAAAAAHAAIAbwABAAAAAAAIAAIA" + - "bwABAAAAAAAJAAIAbwABAAAAAAAKAAIAbwABAAAAAAALAAIAbwABAAAAAAAMAAIAbwABAAAAAAANAAIAbwABAAAAAAAOAAIAbwABAAAAAA" + - "AQAAUAAAABAAAAAAARAAcADwADAAEECQAAAAQAcQADAAEECQABAAoABQADAAEECQACAA4AFgADAAEECQADACAAhQADAAEECQAEABoAMQAD" + - "AAEECQAFAAQAcQADAAEECQAGABgAVwADAAEECQAHAAQAcQADAAEECQAIAAQAcQADAAEECQAJAAQAcQADAAEECQAKAAQAcQADAAEECQALAA" + - "QAcQADAAEECQAMAAQAcQADAAEECQANAAQAcQADAAEECQAOAAQAcQADAAEECQAQAAoABQADAAEECQARAA4AFmVtcHR5AGUAbQBwAHQAeVJl" + - "Z3VsYXIAUgBlAGcAdQBsAGEAcmVtcHR5IFJlZ3VsYXIAZQBtAHAAdAB5ACAAUgBlAGcAdQBsAGEAcmVtcHR5UmVndWxhcgBlAG0AcAB0AH" + - "kAUgBlAGcAdQBsAGEAciIiACIAIiIiOmVtcHR5IFJlZ3VsYXIAIgAiADoAZQBtAHAAdAB5ACAAUgBlAGcAdQBsAGEAcgAAAAABAAMAAQAA" + - "AAwABAA4AAAACgAIAAIAAgAAACAAQQBf//8AAAAAACAAQQBf//8AAP/h/8H/pAABAAAAAAAAAAAAAAADAAAAAAAAAAAAAAAAAAAAAAAAAA" + - "AAAAAAAAAAAAAAAQAEAQABAQENZW1wdHlSZWd1bGFyAAEBASf4GwD4HAL4HQP4HgSLi/lQ9+EFHQAAAHgPHQAAAH8Rix0AAADZEgAHAQED" + - "EBUcISIsIiJlbXB0eSBSZWd1bGFyZW1wdHlSZWd1bGFyc3BhY2VBdW5kZXJzY29yZQAAAAGLAYwBjQAEAQFMT1FT+F2f+TcVi4uL/TeLiw" + - "iLi/g1i4uLCIuLi/k3i4sIi4v8NYuLiwi7/QcVi4uL+NeLiwiLi/fUi4uLCIuLi/zXi4sIi4v71IuLiwgO9+EOnw6fDgAAAAHJAAABTQAA" + - "ABQAAAAUAAA=" +private const val defaultFontIdentity = "org.jetbrains.compose.resources.defaultFont" -@OptIn(ExperimentalEncodingApi::class) -private val defaultEmptyFont by lazy { Font("org.jetbrains.compose.emptyFont", Base64.decode(emptyFontBase64)) } +// It's mainly necessary for the web. +// A meaningful default font is used while a requested font is being loaded asynchronously. +// Check out skiko for the default font details - it's bundled into skiko.wasm +// https://github.com/JetBrains/skiko/blob/master/skiko/src/webMain/cpp/Roboto-Regular.ttf.cc +// +// Notes: +// - On the web, the default font provided by skiko has limited glyph coverage. +// When encountering unknown glyphs, it will display '�' or tofu (empty box) characters (for example, for emojis). +// - the default font doesn't support font styles and weights customization. +@OptIn(ExperimentalTextApi::class) +private val defaultFont: Font = SystemFont(defaultFontIdentity) private val fontCache = AsyncCache() -internal val Font.isEmptyPlaceholder: Boolean - get() = this == defaultEmptyFont + +internal val Font.isDefault: Boolean + @OptIn(ExperimentalTextApi::class) + get() = (this as? SystemFont)?.identity == defaultFontIdentity private fun ByteArray.footprint() = "[$size:${lastOrNull()?.toInt()}]" @@ -44,7 +38,7 @@ private fun ByteArray.footprint() = "[$size:${lastOrNull()?.toInt()}]" @Composable actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font { val resourceReader = LocalResourceReader.currentOrPreview - val fontFile by rememberResourceState(resource, weight, style, { defaultEmptyFont }) { env -> + val fontFile by rememberResourceState(resource, weight, style, { defaultFont }) { env -> val path = resource.getResourceItemByEnvironment(env).path val key = "$path:$weight:$style" fontCache.getOrLoad(key) { @@ -54,7 +48,6 @@ actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): F } return fontFile } - @Composable actual fun Font( resource: FontResource, @@ -63,7 +56,7 @@ actual fun Font( variationSettings: FontVariation.Settings, ): Font { val resourceReader = LocalResourceReader.currentOrPreview - val fontFile by rememberResourceState(resource, weight, style, variationSettings, { defaultEmptyFont }) { env -> + val fontFile by rememberResourceState(resource, weight, style, variationSettings, { defaultFont }) { env -> val path = resource.getResourceItemByEnvironment(env).path val key = "$path:$weight:$style:${variationSettings.getCacheKey()}" fontCache.getOrLoad(key) { diff --git a/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/Resource.web.kt b/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/Resource.web.kt index e3b6f3aeb9..ce2d0519e2 100644 --- a/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/Resource.web.kt +++ b/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/Resource.web.kt @@ -104,7 +104,7 @@ fun preloadFont( variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style), ): State { val resState = remember(resource, weight, style, variationSettings) { mutableStateOf(null) }.apply { - value = Font(resource, weight, style, variationSettings).takeIf { !it.isEmptyPlaceholder } + value = Font(resource, weight, style, variationSettings).takeIf { !it.isDefault } } return resState } diff --git a/components/resources/library/src/webTest/kotlin/org/jetbrains/compose/resources/TestResourcePreloading.kt b/components/resources/library/src/webTest/kotlin/org/jetbrains/compose/resources/TestResourcePreloading.kt index 3d481d8a60..79bca661fc 100644 --- a/components/resources/library/src/webTest/kotlin/org/jetbrains/compose/resources/TestResourcePreloading.kt +++ b/components/resources/library/src/webTest/kotlin/org/jetbrains/compose/resources/TestResourcePreloading.kt @@ -9,11 +9,12 @@ import androidx.compose.ui.test.runComposeUiTest import androidx.compose.ui.text.font.Font import kotlinx.coroutines.CancellableContinuation import kotlinx.coroutines.suspendCancellableCoroutine -import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue @OptIn(ExperimentalTestApi::class, ExperimentalEncodingApi::class) class TestResourcePreloading { @@ -73,4 +74,38 @@ class TestResourcePreloading { assertEquals(font, font2, "font2 is expected to be loaded from cache") assertEquals(null, loadContinuation, "expected no more ResourceReader usages") } + + @Test + fun testIsDefaultCheck() = runComposeUiTest { + val resLoader = object : ResourceReader { + override suspend fun read(path: String): ByteArray { + return suspendCancellableCoroutine { + // suspend indefinitely for test purpose + } + } + + override suspend fun readPart(path: String, offset: Long, size: Long): ByteArray { + TODO("Not yet implemented") + } + + override fun getUri(path: String): String { + TODO("Not yet implemented") + } + } + + var font: Font? = null + + setContent { + CompositionLocalProvider( + LocalComposeEnvironment provides TestComposeEnvironment, + LocalResourceReader provides resLoader + ) { + font = Font(TestFontResource("sometestfont2")) + } + } + + waitForIdle() + assertNotNull(font) + assertTrue(font.isDefault) + } } \ No newline at end of file