Browse Source

web resources: Use the skiko-bundled font as a default instead of an empty font (#5456)

Partially addresses https://youtrack.jetbrains.com/issue/CMP-9075

But it doesn't ultimately fix the FOUC problem. In many cases font
preloading will be necessary to improve the UX.

Describe proposed changes and the issue being fixed


## Testing
- This should be tested by QA

## Release Notes
### Fixes - Resources
- Use the non-empty font as a default when awaiting for a asynchronous
request completion on web
ok/test_gradle_plugin_with_1.10.0+dev3087
Oleksandr Karpovich 2 months ago committed by GitHub
parent
commit
6ca69e77de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 47
      components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt
  2. 2
      components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/Resource.web.kt
  3. 37
      components/resources/library/src/webTest/kotlin/org/jetbrains/compose/resources/TestResourcePreloading.kt

47
components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt

@ -2,37 +2,31 @@ package org.jetbrains.compose.resources @@ -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 '<EFBFBD>' 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<String, Font>()
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()}]" @@ -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 @@ -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( @@ -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) {

2
components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/Resource.web.kt

@ -104,7 +104,7 @@ fun preloadFont( @@ -104,7 +104,7 @@ fun preloadFont(
variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style),
): State<Font?> {
val resState = remember(resource, weight, style, variationSettings) { mutableStateOf<Font?>(null) }.apply {
value = Font(resource, weight, style, variationSettings).takeIf { !it.isEmptyPlaceholder }
value = Font(resource, weight, style, variationSettings).takeIf { !it.isDefault }
}
return resState
}

37
components/resources/library/src/webTest/kotlin/org/jetbrains/compose/resources/TestResourcePreloading.kt

@ -9,11 +9,12 @@ import androidx.compose.ui.test.runComposeUiTest @@ -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 { @@ -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)
}
}
Loading…
Cancel
Save