diff --git a/compose/mpp/demo/src/desktopMain/kotlin/androidx/compose/mpp/demo/Main.desktop.kt b/compose/mpp/demo/src/desktopMain/kotlin/androidx/compose/mpp/demo/Main.desktop.kt index ac30d114877b5..0de51c84d290c 100644 --- a/compose/mpp/demo/src/desktopMain/kotlin/androidx/compose/mpp/demo/Main.desktop.kt +++ b/compose/mpp/demo/src/desktopMain/kotlin/androidx/compose/mpp/demo/Main.desktop.kt @@ -16,10 +16,18 @@ package androidx.compose.mpp.demo +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalFontFamilyResolver +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.platform.Font import androidx.compose.ui.unit.dp import androidx.compose.ui.window.WindowState import androidx.compose.ui.window.singleWindowApplication +import java.io.IOException +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext fun main(args: Array) = singleWindowApplication( title = "Compose MPP demo", @@ -28,5 +36,29 @@ fun main(args: Array) = singleWindowApplication( val app = remember { App(initialScreenName = args.getOrNull(0)) } - app.Content() -} \ No newline at end of file + val fontFamilyResolver = LocalFontFamilyResolver.current + val fontsLoaded = remember { mutableStateOf(false) } + + if (fontsLoaded.value) { + app.Content() + } + + LaunchedEffect(Unit) { + val fontBytes = getResourceBytes("NotoColorEmoji.ttf")!! + val fontFamily = FontFamily(listOf(Font("NotoColorEmoji", fontBytes))) + fontFamilyResolver.preload(fontFamily) + fontsLoaded.value = true + } +} + +suspend fun getResourceBytes(resourceName: String): ByteArray? = withContext(Dispatchers.IO) { + val classLoader = Thread.currentThread().contextClassLoader + try { + classLoader.getResourceAsStream(resourceName).use { inputStream -> + return@withContext inputStream?.readBytes() + } + } catch (e: IOException) { + e.printStackTrace() + return@withContext null + } +} diff --git a/compose/mpp/demo/src/desktopMain/resources/NotoColorEmoji.ttf b/compose/mpp/demo/src/desktopMain/resources/NotoColorEmoji.ttf new file mode 100644 index 0000000000000..9d82f52226615 Binary files /dev/null and b/compose/mpp/demo/src/desktopMain/resources/NotoColorEmoji.ttf differ diff --git a/compose/mpp/demo/src/wasmJsMain/kotlin/androidx/compose/mpp/demo/main.js.kt b/compose/mpp/demo/src/wasmJsMain/kotlin/androidx/compose/mpp/demo/main.js.kt index d856532cd9c00..18cc5083a2c33 100644 --- a/compose/mpp/demo/src/wasmJsMain/kotlin/androidx/compose/mpp/demo/main.js.kt +++ b/compose/mpp/demo/src/wasmJsMain/kotlin/androidx/compose/mpp/demo/main.js.kt @@ -17,18 +17,74 @@ package androidx.compose.mpp.demo import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.platform.LocalFontFamilyResolver +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.platform.Font import androidx.compose.ui.window.ComposeViewport +import kotlin.wasm.unsafe.UnsafeWasmMemoryApi +import kotlin.wasm.unsafe.withScopedMemoryAllocator +import kotlinx.browser.window +import kotlinx.coroutines.await +import org.khronos.webgl.ArrayBuffer +import org.khronos.webgl.Int8Array +import org.w3c.fetch.Response + +private val notoColorEmoji = "./NotoColorEmoji.ttf" @OptIn(ExperimentalComposeUiApi::class) fun main() { ComposeViewport(viewportContainerId = "composeApplication") { + val fontFamilyResolver = LocalFontFamilyResolver.current + val fontsLoaded = remember { mutableStateOf(false) } val app = remember { App() } - app.Content() + + if (fontsLoaded.value) { + app.Content() + } + + LaunchedEffect(Unit) { + val notoEmojisBytes = loadRes(notoColorEmoji).toByteArray() + val fontFamily = FontFamily(listOf(Font("NotoColorEmoji", notoEmojisBytes))) + fontFamilyResolver.preload(fontFamily) + fontsLoaded.value = true + } + } } - @Composable -internal fun returnsNullable(): Any? = null \ No newline at end of file +internal fun returnsNullable(): Any? = null + +suspend fun loadRes(url: String): ArrayBuffer { + return window.fetch(url).await().arrayBuffer().await() +} + +fun ArrayBuffer.toByteArray(): ByteArray { + val source = Int8Array(this, 0, byteLength) + return jsInt8ArrayToKotlinByteArray(source) +} + +@JsFun( + """ (src, size, dstAddr) => { + const mem8 = new Int8Array(wasmExports.memory.buffer, dstAddr, size); + mem8.set(src); + } +""" +) +internal external fun jsExportInt8ArrayToWasm(src: Int8Array, size: Int, dstAddr: Int) + +internal fun jsInt8ArrayToKotlinByteArray(x: Int8Array): ByteArray { + val size = x.length + + @OptIn(UnsafeWasmMemoryApi::class) + return withScopedMemoryAllocator { allocator -> + val memBuffer = allocator.allocate(size) + val dstAddress = memBuffer.address.toInt() + jsExportInt8ArrayToWasm(x, size, dstAddress) + ByteArray(size) { i -> (memBuffer + i).loadByte() } + } +} \ No newline at end of file diff --git a/compose/mpp/demo/src/wasmJsMain/resources/NotoColorEmoji.ttf b/compose/mpp/demo/src/wasmJsMain/resources/NotoColorEmoji.ttf new file mode 100644 index 0000000000000..9d82f52226615 Binary files /dev/null and b/compose/mpp/demo/src/wasmJsMain/resources/NotoColorEmoji.ttf differ diff --git a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.skiko.kt b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.skiko.kt index 4072ae0e02a6c..802e5b148605e 100644 --- a/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.skiko.kt +++ b/compose/ui/ui-text/src/skikoMain/kotlin/androidx/compose/ui/text/platform/PlatformFont.skiko.kt @@ -31,8 +31,10 @@ import androidx.compose.ui.text.font.LoadedFontFamily import androidx.compose.ui.text.font.Typeface import androidx.compose.ui.text.font.createFontFamilyResolver import org.jetbrains.skia.FontMgr +import org.jetbrains.skia.FontMgrWithFallback import org.jetbrains.skia.paragraph.FontCollection import org.jetbrains.skia.paragraph.TypefaceFontProvider +import org.jetbrains.skia.paragraph.TypefaceFontProviderWithFallback expect sealed class PlatformFont() : Font { abstract val identity: String @@ -196,14 +198,14 @@ class FontLoadResult(val typeface: SkTypeface?, val aliases: List) internal class FontCache { internal val fonts = FontCollection() - private val fontProvider = TypefaceFontProvider() + private val fontProvider = TypefaceFontProviderWithFallback() private val registered: MutableSet = HashSet() private val typefacesCache: Cache = ExpireAfterAccessCache( 60_000_000_000 // 1 minute ) init { - fonts.setDefaultFontManager(FontMgr.default) + fonts.setDefaultFontManager(FontMgrWithFallback(fontProvider)) fonts.setAssetFontManager(fontProvider) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b381907cfd17c..b5bfe96c872da 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -54,7 +54,7 @@ moshi = "1.13.0" protobuf = "3.21.8" paparazzi = "1.0.0" paparazziNative = "2022.1.1-canary-f5f9f71" -skiko = "0.8.8" +skiko = "0.8.9" sqldelight = "1.3.0" retrofit = "2.7.2" wire = "4.5.1"