chore: add emoji fallback font support for web

This commit is contained in:
Antoine Jaury
2026-09-30 11:18:54 +02:00
parent 3acba87728
commit 46dfe64493
10 changed files with 170 additions and 0 deletions
@@ -0,0 +1,7 @@
package bzh.ajaury.chombev.theme
import androidx.compose.runtime.Composable
/** No-op: Android supplies the system emoji font. */
@Composable
internal actual fun RegisterEmojiFallbackFont() = Unit
@@ -27,6 +27,7 @@ import bzh.ajaury.chombev.onboarding.ui.viewmodel.OnboardingViewModel
import bzh.ajaury.chombev.player.ui.PlayerScreen
import bzh.ajaury.chombev.records.ui.RecordsScreen
import bzh.ajaury.chombev.theme.ChomBevTheme
import bzh.ajaury.chombev.theme.RegisterEmojiFallbackFont
import bzh.ajaury.chombev.web.model.WebPage
import bzh.ajaury.chombev.web.ui.WebViewScreen
import org.koin.compose.KoinApplication
@@ -53,6 +54,8 @@ fun App() {
val darkTheme = isSystemInDarkTheme()
ChomBevTheme(darkTheme = darkTheme) {
RegisterEmojiFallbackFont()
val onboardingViewModel: OnboardingViewModel = koinViewModel()
val onboardingUiState by onboardingViewModel.uiState.collectAsStateWithLifecycle()
@@ -0,0 +1,15 @@
package bzh.ajaury.chombev.theme
import androidx.compose.runtime.Composable
/**
* Makes colour emoji renderable on platforms whose font manager has no emoji font of its own.
*
* Android, iOS and Desktop resolve emoji through the system fonts, so their implementations do
* nothing. Only the web needs a bundled font: Skia runs in the browser without any system font
* manager, and the default font baked into skiko has no emoji glyphs — they come out as tofu boxes.
*
* Emits nothing; call it once, high in the composition.
*/
@Composable
internal expect fun RegisterEmojiFallbackFont()
@@ -0,0 +1,7 @@
package bzh.ajaury.chombev.theme
import androidx.compose.runtime.Composable
/** No-op: iOS supplies Apple Color Emoji. */
@Composable
internal actual fun RegisterEmojiFallbackFont() = Unit
@@ -0,0 +1,7 @@
package bzh.ajaury.chombev.theme
import androidx.compose.runtime.Composable
/** No-op: The desktop JVM resolves emoji through the system font manager. */
@Composable
internal actual fun RegisterEmojiFallbackFont() = Unit
@@ -0,0 +1,34 @@
package bzh.ajaury.chombev.theme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.platform.Font
import bzh.ajaury.chombev.resources.generated.resources.Res
import bzh.ajaury.chombev.resources.generated.resources.noto_color_emoji
import org.jetbrains.compose.resources.getFontResourceBytes
import org.jetbrains.compose.resources.rememberResourceEnvironment
private const val EMOJI_FONT_IDENTITY = "NotoColorEmoji"
/**
* Loads the bundled Noto Color Emoji subset and hands it to the font resolver.
*
* Resolving a font is what registers its typeface with Skia's fallback provider — Compose builds
* its [org.jetbrains.skia.paragraph.FontCollection] on a `FontMgrWithFallback` fed by every font
* the app has loaded — so a single [androidx.compose.ui.text.font.FontFamily.Resolver.preload] is
* enough for emoji to appear in any `Text`, without touching the screens that display them.
*/
@Composable
internal actual fun RegisterEmojiFallbackFont() {
val resourceEnvironment = rememberResourceEnvironment()
val fontFamilyResolver = LocalFontFamilyResolver.current
LaunchedEffect(resourceEnvironment, fontFamilyResolver) {
val fontBytes = getFontResourceBytes(resourceEnvironment, Res.font.noto_color_emoji)
fontFamilyResolver.preload(
FontFamily(Font(identity = EMOJI_FONT_IDENTITY, data = fontBytes)),
)
}
}