diff --git a/build.gradle.kts b/build.gradle.kts index d258804..3e1299f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -11,4 +11,6 @@ allprojects { tasks.configureEach { if (name == "kotlinStoreYarnLock") enabled = false } -} \ No newline at end of file +} + +val genEmojis = tasks.create("genEmojis") diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 2c34409..207fd57 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -4,11 +4,26 @@ plugins { repositories { mavenCentral() + + maven("https://css4j.github.io/maven/") { + mavenContent { releasesOnly() } + content { + includeGroup("com.github.css4j") + includeGroup("io.sf.carte") + includeGroup("io.sf.jclf") + } + } + } +//noinspection UseTomlInstead dependencies { implementation(gradleApi()) implementation(gradleKotlinDsl()) + implementation("com.squareup.moshi:moshi:1.15.1") implementation("com.squareup.moshi:moshi-kotlin:1.15.1") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + + implementation("com.github.weisj:jsvg:1.4.0") } diff --git a/buildSrc/src/main/kotlin/Annotations.kt b/buildSrc/src/main/kotlin/Annotations.kt index a8aecdb..f444440 100644 --- a/buildSrc/src/main/kotlin/Annotations.kt +++ b/buildSrc/src/main/kotlin/Annotations.kt @@ -21,7 +21,8 @@ data class AnnotatedForm( val altForms: List
, val emoticons: List, val aliases: List, - val notoAnimated: Boolean + val hasNotoImage: Boolean, + val hasNotoAnimation: Boolean ) @OptIn(ExperimentalStdlibApi::class) @@ -61,7 +62,8 @@ fun annotate(grouppedForms: GrouppedForms, notoJsonFile: File): List, intIndices: List>> -internal fun genEmojiFiles(outputDir: File, annotatedForms: List): AnnotatedFormTree { +internal fun genEmojiFiles(outputDir: File, annotatedForms: List, cacheDir: File): AnnotatedFormTree { val ids = LinkedHashMap>>() annotatedForms .forEach { annotatedForm -> @@ -28,6 +31,51 @@ internal fun genEmojiFiles(outputDir: File, annotatedForms: List) ids.getOrPut(groupId) { LinkedHashMap() }.getOrPut(subgroupId) { ArrayList() }.add(annotatedForm) val doubleSkinToneZWJ = annotatedForm.mainForm.doubleSkinToneZWJs["minimally-qualified"] ?: annotatedForm.mainForm.doubleSkinToneZWJs["fully-qualified"] val unqualifiedForm = annotatedForm.altForms.firstOrNull { it.entry.type == "unqualified" } + + val notoCode = annotatedForm.mainForm.entry.code.joinToString("_") { it.toString(radix = 16) } + + var notoImageRatio = 0f + if (annotatedForm.hasNotoImage) { + val file = cacheDir.resolve("$notoCode.svg") + val bytes = file.readBytes() + if (bytes.isNotEmpty()) { + val xml = bytes.toString(Charsets.UTF_8) + val svg = Regex("]+>").find(xml) + ?: error("${file.absolutePath}: Could not find svg markup") + val w = Regex("width=\"(?\\d+(?:\\.\\d+)?)(?:px)?\"").find(svg.value)?.groups?.get("w")?.value?.toFloat() + val h = Regex("height=\"(?\\d+(?:\\.\\d+)?)(?:px)?\"").find(svg.value)?.groups?.get("h")?.value?.toFloat() + if (w != null && h != null) { + notoImageRatio = w / h + } else { + val viewBox = Regex("viewBox=\"-?\\d+(?:\\.\\d+)? -?\\d+(?:\\.\\d+)? (?-?\\d+(?:\\.\\d+)?) (?-?\\d+(?:\\.\\d+)?)\"").find(svg.value) + if (viewBox != null) { + val vbW = viewBox.groups["w"]!!.value.toFloat() + val vbH = viewBox.groups["h"]!!.value.toFloat() + notoImageRatio = vbW / vbH + } else { + error("${file.absolutePath}: Could not find neither viewBox nor width & height.") + } + } + } + } + + var notoAnimationRatio = 0f + if (annotatedForm.hasNotoAnimation) { + val file = cacheDir.resolve("$notoCode.json") + val json = file.readText() + if (json.isNotEmpty()) { + @OptIn(ExperimentalStdlibApi::class) + val adapter = Moshi.Builder() + .addLast(KotlinJsonAdapterFactory()) + .build() + .adapter>() + val map = adapter.fromJson(json) ?: error("${file.absolutePath}: Could not parse JSON.") + val w = (map["w"] as? Number)?.toFloat() ?: error("${file.absolutePath}: Could not find w.") + val h = (map["h"] as? Number)?.toFloat() ?: error("${file.absolutePath}: Could not find h.") + notoAnimationRatio = w / h + } + } + val (itf, impl) = when { annotatedForm.mainForm.skinToneIndices == null && doubleSkinToneZWJ == null && unqualifiedForm?.skinToneIndices == null -> "Emoji" to "EmojiImpl" annotatedForm.mainForm.skinToneIndices == null && unqualifiedForm?.skinToneIndices?.size == 1 -> "SkinTone1Emoji" to "UnqualifiedSkinTone1EmojiImpl" @@ -64,7 +112,8 @@ internal fun genEmojiFiles(outputDir: File, annotatedForms: List) writer.appendLine(" unicodeVersion = UnicodeVersion(${annotatedForm.mainForm.entry.version[0]}, ${annotatedForm.mainForm.entry.version[1]}),") writer.appendLine(" aliases = listOf(\"${annotatedForm.mainForm.entry.description.kebabCase()}\", ${annotatedForm.aliases.joinToString { "\"$it\"" }}),") writer.appendLine(" emoticons = listOf(${emoticons.joinToString { "\"$it\"" }}),") - writer.appendLine(" notoAnimated = ${annotatedForm.notoAnimated},") + writer.appendLine(" notoImageRatio = ${notoImageRatio}f,") + writer.appendLine(" notoAnimationRatio = ${notoAnimationRatio}f,") writer.appendLine(" ),") when (impl) { "UnqualifiedSkinTone1EmojiImpl" -> { diff --git a/buildSrc/src/main/kotlin/GenEmojis.kt b/buildSrc/src/main/kotlin/GenEmojis.kt index bb31af9..30424b4 100644 --- a/buildSrc/src/main/kotlin/GenEmojis.kt +++ b/buildSrc/src/main/kotlin/GenEmojis.kt @@ -16,32 +16,40 @@ abstract class GenEmojis : DefaultTask() { abstract val notoJsonFile: RegularFileProperty @get:OutputDirectory - abstract val genDirectory: DirectoryProperty + abstract val genEmojiDirectory: DirectoryProperty + + @get:OutputDirectory + abstract val genNotoDirectory: DirectoryProperty + + @get:OutputDirectory + abstract val cacheDirectory: DirectoryProperty init { group = "build" - unicodeTextFile.convention(project.layout.projectDirectory.file("src/emoji/emoji-test.txt")) - notoJsonFile.convention(project.layout.projectDirectory.file("src/emoji/emoji_15_0_ordering.json")) - genDirectory.convention(project.layout.buildDirectory.dir("gen/emoji")) + unicodeTextFile.convention(project.layout.projectDirectory.file("definitions/emoji-test.txt")) + notoJsonFile.convention(project.layout.projectDirectory.file("definitions/emoji_15_0_ordering.json")) + genEmojiDirectory.convention(project.layout.buildDirectory.dir("gen/emoji")) + genNotoDirectory.convention(project.layout.buildDirectory.dir("gen/noto")) + cacheDirectory.convention(project.layout.buildDirectory.dir("cache/noto")) } - // Generates emojis - - @OptIn(ExperimentalStdlibApi::class) @TaskAction private fun execute() { val entries = getEntriesFromFile(unicodeTextFile.get().asFile) val forms = entriesToForms(entries) val annotatedForms = annotate(forms, notoJsonFile.get().asFile) - val outputDir = genDirectory.get().asFile - outputDir.deleteRecursively() - outputDir.mkdirs() + val cacheDir = cacheDirectory.get().asFile + cacheDir.mkdirs() + downloadNotoFiles(annotatedForms, cacheDir) - val tree = genEmojiFiles(outputDir, annotatedForms) + val emojiOutputDir = genEmojiDirectory.get().asFile + emojiOutputDir.deleteRecursively() + emojiOutputDir.mkdirs() - genCollections(outputDir, tree) + val tree = genEmojiFiles(emojiOutputDir, annotatedForms, cacheDir) + genCollections(emojiOutputDir, tree) } } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/NotoFiles.kt b/buildSrc/src/main/kotlin/NotoFiles.kt new file mode 100644 index 0000000..9a903a1 --- /dev/null +++ b/buildSrc/src/main/kotlin/NotoFiles.kt @@ -0,0 +1,45 @@ +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.File +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + + +private fun ExecutorService.download(client: OkHttpClient, url: String, file: File) { + if (file.exists()) return + + submit { + val request = Request.Builder() + .url(url) + .get() + .build() + file.outputStream().use { output -> + client.newCall(request).execute().use { response -> + if (!response.isSuccessful) error(response) + response.body!!.byteStream().use { input -> + input.copyTo(output) + } + } + } + } +} + +internal fun downloadNotoFiles(forms: List, cacheDir: File) { + + val httpClient = OkHttpClient() + val exec = Executors.newFixedThreadPool(16) + + forms.forEach { form -> + val code = form.mainForm.entry.code.joinToString("_") { it.toString(radix = 16) } + if (form.hasNotoImage) { + exec.download(httpClient, "https://fonts.gstatic.com/s/e/notoemoji/latest/$code/emoji.svg", cacheDir.resolve("$code.svg")) + } + if (form.hasNotoAnimation) { + exec.download(httpClient, "https://fonts.gstatic.com/s/e/notoemoji/latest/$code/lottie.json", cacheDir.resolve("$code.json")) + } + } + + exec.shutdown() + exec.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS) +} diff --git a/compose-demo/src/commonMain/kotlin/org/kodein/emoji/compose/demo/app.kt b/compose-demo/src/commonMain/kotlin/org/kodein/emoji/compose/demo/app.kt index 038bbcd..15d9eb8 100644 --- a/compose-demo/src/commonMain/kotlin/org/kodein/emoji/compose/demo/app.kt +++ b/compose-demo/src/commonMain/kotlin/org/kodein/emoji/compose/demo/app.kt @@ -2,6 +2,7 @@ package org.kodein.emoji.compose.demo import androidx.compose.foundation.layout.* import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.MaterialTheme import androidx.compose.material.ProvideTextStyle import androidx.compose.material.Text import androidx.compose.runtime.Composable @@ -46,9 +47,10 @@ fun App() { } NotoAnimatedEmoji( emoji = Emoji.ImpSmile, - modifier = Modifier.size(64.dp), + modifier = Modifier.height(92.dp), iterations = 2, - stopAt = 0.76f + stopAt = 0.76f, + placeholder = { Box(it) } ) } } diff --git a/emoji-kt/src/emoji/emoji-test.txt b/definitions/emoji-test.txt similarity index 100% rename from emoji-kt/src/emoji/emoji-test.txt rename to definitions/emoji-test.txt diff --git a/emoji-kt/src/emoji/emoji_15_0_ordering.json b/definitions/emoji_15_0_ordering.json similarity index 100% rename from emoji-kt/src/emoji/emoji_15_0_ordering.json rename to definitions/emoji_15_0_ordering.json diff --git a/emoji-compose-m2/src/commonMain/kotlin/org/kodein/emoji/compose/m2/text.kt b/emoji-compose-m2/src/commonMain/kotlin/org/kodein/emoji/compose/m2/text.kt index 1b10732..23c3220 100644 --- a/emoji-compose-m2/src/commonMain/kotlin/org/kodein/emoji/compose/m2/text.kt +++ b/emoji-compose-m2/src/commonMain/kotlin/org/kodein/emoji/compose/m2/text.kt @@ -50,11 +50,57 @@ public fun TextWithPlatformEmoji( minLines: Int = 1, onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current, - fixedEmojiSize: Boolean = false, ) { WithPlatformEmoji( text = text, - fixedImageSize = fixedEmojiSize, + ) { emojiAnnotatedString, emojiInlineContent -> + Text( + text = emojiAnnotatedString, + modifier = modifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + inlineContent = emojiInlineContent, + onTextLayout = onTextLayout, + style = style + ) + } +} + +@Deprecated("fixedSize is now ignored (size ratio is now part of emoji details)") +@Composable +public fun TextWithPlatformEmoji( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + fixedEmojiSize: Boolean, +) { + WithPlatformEmoji( + text = text, ) { emojiAnnotatedString, emojiInlineContent -> Text( text = emojiAnnotatedString, @@ -107,11 +153,58 @@ public fun TextWithPlatformEmoji( inlineContent: Map = mapOf(), onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current, - fixedEmojiSize: Boolean = false, ) { WithPlatformEmoji( text = text, - fixedImageSize = fixedEmojiSize, + ) { emojiAnnotatedString, emojiInlineContent -> + Text( + text = emojiAnnotatedString, + modifier = modifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + inlineContent = inlineContent + emojiInlineContent, + onTextLayout = onTextLayout, + style = style + ) + } +} + +@Deprecated("fixedSize is now ignored (size ratio is now part of emoji details)") +@Composable +public fun TextWithPlatformEmoji( + text: AnnotatedString, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + inlineContent: Map = mapOf(), + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + fixedEmojiSize: Boolean, +) { + WithPlatformEmoji( + text = text, ) { emojiAnnotatedString, emojiInlineContent -> Text( text = emojiAnnotatedString, @@ -161,11 +254,57 @@ public fun TextWithNotoImageEmoji( minLines: Int = 1, onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current, - fixedEmojiSize: Boolean = false, ) { WithNotoImageEmoji( text = text, - fixedSize = fixedEmojiSize, + ) { emojiAnnotatedString, emojiInlineContent -> + Text( + text = emojiAnnotatedString, + modifier = modifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + inlineContent = emojiInlineContent, + onTextLayout = onTextLayout, + style = style + ) + } +} + +@Deprecated("fixedSize is now ignored (size ratio is now part of emoji details)") +@Composable +public fun TextWithNotoImageEmoji( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + fixedEmojiSize: Boolean, +) { + WithNotoImageEmoji( + text = text, ) { emojiAnnotatedString, emojiInlineContent -> Text( text = emojiAnnotatedString, @@ -216,11 +355,58 @@ public fun TextWithNotoImageEmoji( inlineContent: Map = mapOf(), onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current, - fixedEmojiSize: Boolean = false, ) { WithNotoImageEmoji( text = text, - fixedSize = fixedEmojiSize, + ) { emojiAnnotatedString, emojiInlineContent -> + Text( + text = emojiAnnotatedString, + modifier = modifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + inlineContent = inlineContent + emojiInlineContent, + onTextLayout = onTextLayout, + style = style + ) + } +} + +@Deprecated("fixedSize is now ignored (size ratio is now part of emoji details)") +@Composable +public fun TextWithNotoImageEmoji( + text: AnnotatedString, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + inlineContent: Map = mapOf(), + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + fixedEmojiSize: Boolean, +) { + WithNotoImageEmoji( + text = text, ) { emojiAnnotatedString, emojiInlineContent -> Text( text = emojiAnnotatedString, @@ -274,13 +460,63 @@ public fun TextWithNotoAnimatedEmoji( style: TextStyle = LocalTextStyle.current, emojiAnimationIterations: Int = Int.MAX_VALUE, emojiAnimationSpeed: Float = 1f, - fixedEmojiSize: Boolean = false, ) { WithNotoAnimatedEmoji( text = text, iterations = emojiAnimationIterations, speed = emojiAnimationSpeed, - fixedSize = fixedEmojiSize, + ) { emojiAnnotatedString, emojiInlineContent -> + Text( + text = emojiAnnotatedString, + modifier = modifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + inlineContent = emojiInlineContent, + onTextLayout = onTextLayout, + style = style + ) + } +} + +@Deprecated("fixedSize is now ignored (size ratio is now part of emoji details)") +@Composable +public fun TextWithNotoAnimatedEmoji( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + emojiAnimationIterations: Int = Int.MAX_VALUE, + emojiAnimationSpeed: Float = 1f, + fixedEmojiSize: Boolean, +) { + WithNotoAnimatedEmoji( + text = text, + iterations = emojiAnimationIterations, + speed = emojiAnimationSpeed, ) { emojiAnnotatedString, emojiInlineContent -> Text( text = emojiAnnotatedString, @@ -333,7 +569,6 @@ public fun TextWithNotoAnimatedEmoji( inlineContent: Map = mapOf(), onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current, - fixedEmojiSize: Boolean = false, emojiAnimationIterations: Int = Int.MAX_VALUE, emojiAnimationSpeed: Float = 1f, ) { @@ -341,7 +576,6 @@ public fun TextWithNotoAnimatedEmoji( text = text, iterations = emojiAnimationIterations, speed = emojiAnimationSpeed, - fixedSize = fixedEmojiSize, ) { emojiAnnotatedString, emojiInlineContent -> Text( text = emojiAnnotatedString, @@ -364,4 +598,57 @@ public fun TextWithNotoAnimatedEmoji( style = style ) } -} \ No newline at end of file +} + +@Deprecated("fixedSize is now ignored (size ratio is now part of emoji details)") +@Composable +public fun TextWithNotoAnimatedEmoji( + text: AnnotatedString, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + inlineContent: Map = mapOf(), + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + fixedEmojiSize: Boolean, + emojiAnimationIterations: Int = Int.MAX_VALUE, + emojiAnimationSpeed: Float = 1f, +) { + WithNotoAnimatedEmoji( + text = text, + iterations = emojiAnimationIterations, + speed = emojiAnimationSpeed, + ) { emojiAnnotatedString, emojiInlineContent -> + Text( + text = emojiAnnotatedString, + modifier = modifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + inlineContent = inlineContent + emojiInlineContent, + onTextLayout = onTextLayout, + style = style + ) + } +} diff --git a/emoji-compose-m3/src/commonMain/kotlin/org/kodein/emoji/compose/m3/text.kt b/emoji-compose-m3/src/commonMain/kotlin/org/kodein/emoji/compose/m3/text.kt index fbbeea1..87cd190 100644 --- a/emoji-compose-m3/src/commonMain/kotlin/org/kodein/emoji/compose/m3/text.kt +++ b/emoji-compose-m3/src/commonMain/kotlin/org/kodein/emoji/compose/m3/text.kt @@ -46,11 +46,57 @@ public fun TextWithPlatformEmoji( minLines: Int = 1, onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current, - fixedEmojiSize: Boolean = false, ) { WithPlatformEmoji( text = text, - fixedImageSize = fixedEmojiSize, + ) { emojiAnnotatedString, emojiInlineContent -> + Text( + text = emojiAnnotatedString, + modifier = modifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + inlineContent = emojiInlineContent, + onTextLayout = onTextLayout, + style = style + ) + } +} + +@Deprecated("fixedSize is now ignored (size ratio is now part of emoji details)") +@Composable +public fun TextWithPlatformEmoji( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + fixedEmojiSize: Boolean, +) { + WithPlatformEmoji( + text = text, ) { emojiAnnotatedString, emojiInlineContent -> Text( text = emojiAnnotatedString, @@ -103,11 +149,58 @@ public fun TextWithPlatformEmoji( inlineContent: Map = mapOf(), onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current, - fixedEmojiSize: Boolean = false, ) { WithPlatformEmoji( text = text, - fixedImageSize = fixedEmojiSize, + ) { emojiAnnotatedString, emojiInlineContent -> + Text( + text = emojiAnnotatedString, + modifier = modifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + inlineContent = inlineContent + emojiInlineContent, + onTextLayout = onTextLayout, + style = style + ) + } +} + +@Deprecated("fixedSize is now ignored (size ratio is now part of emoji details)") +@Composable +public fun TextWithPlatformEmoji( + text: AnnotatedString, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + inlineContent: Map = mapOf(), + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + fixedEmojiSize: Boolean, +) { + WithPlatformEmoji( + text = text, ) { emojiAnnotatedString, emojiInlineContent -> Text( text = emojiAnnotatedString, @@ -157,11 +250,57 @@ public fun TextWithNotoImageEmoji( minLines: Int = 1, onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current, - fixedEmojiSize: Boolean = false, ) { WithNotoImageEmoji( text = text, - fixedSize = fixedEmojiSize, + ) { emojiAnnotatedString, emojiInlineContent -> + Text( + text = emojiAnnotatedString, + modifier = modifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + inlineContent = emojiInlineContent, + onTextLayout = onTextLayout, + style = style + ) + } +} + +@Deprecated("fixedSize is now ignored (size ratio is now part of emoji details)") +@Composable +public fun TextWithNotoImageEmoji( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + fixedEmojiSize: Boolean, +) { + WithNotoImageEmoji( + text = text, ) { emojiAnnotatedString, emojiInlineContent -> Text( text = emojiAnnotatedString, @@ -212,11 +351,58 @@ public fun TextWithNotoImageEmoji( inlineContent: Map = mapOf(), onTextLayout: (TextLayoutResult) -> Unit = {}, style: TextStyle = LocalTextStyle.current, - fixedEmojiSize: Boolean = false, ) { WithNotoImageEmoji( text = text, - fixedSize = fixedEmojiSize, + ) { emojiAnnotatedString, emojiInlineContent -> + Text( + text = emojiAnnotatedString, + modifier = modifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + inlineContent = inlineContent + emojiInlineContent, + onTextLayout = onTextLayout, + style = style + ) + } +} + +@Deprecated("fixedSize is now ignored (size ratio is now part of emoji details)") +@Composable +public fun TextWithNotoImageEmoji( + text: AnnotatedString, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + inlineContent: Map = mapOf(), + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + fixedEmojiSize: Boolean, +) { + WithNotoImageEmoji( + text = text, ) { emojiAnnotatedString, emojiInlineContent -> Text( text = emojiAnnotatedString, @@ -270,13 +456,63 @@ public fun TextWithNotoAnimatedEmoji( style: TextStyle = LocalTextStyle.current, emojiAnimationIterations: Int = Int.MAX_VALUE, emojiAnimationSpeed: Float = 1f, - fixedEmojiSize: Boolean = false, ) { WithNotoAnimatedEmoji( text = text, iterations = emojiAnimationIterations, speed = emojiAnimationSpeed, - fixedSize = fixedEmojiSize, + ) { emojiAnnotatedString, emojiInlineContent -> + Text( + text = emojiAnnotatedString, + modifier = modifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + inlineContent = emojiInlineContent, + onTextLayout = onTextLayout, + style = style + ) + } +} + +@Deprecated("fixedSize is now ignored (size ratio is now part of emoji details)") +@Composable +public fun TextWithNotoAnimatedEmoji( + text: String, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + emojiAnimationIterations: Int = Int.MAX_VALUE, + emojiAnimationSpeed: Float = 1f, + fixedEmojiSize: Boolean, +) { + WithNotoAnimatedEmoji( + text = text, + iterations = emojiAnimationIterations, + speed = emojiAnimationSpeed, ) { emojiAnnotatedString, emojiInlineContent -> Text( text = emojiAnnotatedString, @@ -331,13 +567,11 @@ public fun TextWithNotoAnimatedEmoji( style: TextStyle = LocalTextStyle.current, emojiAnimationIterations: Int = Int.MAX_VALUE, emojiAnimationSpeed: Float = 1f, - fixedEmojiSize: Boolean = false, ) { WithNotoAnimatedEmoji( text = text, iterations = emojiAnimationIterations, speed = emojiAnimationSpeed, - fixedSize = fixedEmojiSize, ) { emojiAnnotatedString, emojiInlineContent -> Text( text = emojiAnnotatedString, @@ -360,4 +594,57 @@ public fun TextWithNotoAnimatedEmoji( style = style ) } -} \ No newline at end of file +} + +@Deprecated("fixedSize is now ignored (size ratio is now part of emoji details)") +@Composable +public fun TextWithNotoAnimatedEmoji( + text: AnnotatedString, + modifier: Modifier = Modifier, + color: Color = Color.Unspecified, + fontSize: TextUnit = TextUnit.Unspecified, + fontStyle: FontStyle? = null, + fontWeight: FontWeight? = null, + fontFamily: FontFamily? = null, + letterSpacing: TextUnit = TextUnit.Unspecified, + textDecoration: TextDecoration? = null, + textAlign: TextAlign? = null, + lineHeight: TextUnit = TextUnit.Unspecified, + overflow: TextOverflow = TextOverflow.Clip, + softWrap: Boolean = true, + maxLines: Int = Int.MAX_VALUE, + minLines: Int = 1, + inlineContent: Map = mapOf(), + onTextLayout: (TextLayoutResult) -> Unit = {}, + style: TextStyle = LocalTextStyle.current, + emojiAnimationIterations: Int = Int.MAX_VALUE, + emojiAnimationSpeed: Float = 1f, + fixedEmojiSize: Boolean, +) { + WithNotoAnimatedEmoji( + text = text, + iterations = emojiAnimationIterations, + speed = emojiAnimationSpeed, + ) { emojiAnnotatedString, emojiInlineContent -> + Text( + text = emojiAnnotatedString, + modifier = modifier, + color = color, + fontSize = fontSize, + fontStyle = fontStyle, + fontWeight = fontWeight, + fontFamily = fontFamily, + letterSpacing = letterSpacing, + textDecoration = textDecoration, + textAlign = textAlign, + lineHeight = lineHeight, + overflow = overflow, + softWrap = softWrap, + maxLines = maxLines, + minLines = minLines, + inlineContent = inlineContent + emojiInlineContent, + onTextLayout = onTextLayout, + style = style + ) + } +} diff --git a/emoji-compose/src/androidMain/kotlin/org/kodein/emoji/compose/androidVectorImages.kt b/emoji-compose/src/androidMain/kotlin/org/kodein/emoji/compose/androidVectorImages.kt index b15c3fd..c0e8513 100644 --- a/emoji-compose/src/androidMain/kotlin/org/kodein/emoji/compose/androidVectorImages.kt +++ b/emoji-compose/src/androidMain/kotlin/org/kodein/emoji/compose/androidVectorImages.kt @@ -71,6 +71,9 @@ internal actual fun LottieAnimation( contentDescription: String, modifier: Modifier ) { + require(iterations > 0) { "Invalid iterations" } + require(stopAt in 0f..1f) { "Invalid stopAt" } + require(speed > 0f) { "Invalid speed" } val progress = remember { Animatable(0f) } LaunchedEffect(Unit) { repeat(iterations) { diff --git a/emoji-compose/src/commonMain/kotlin/org/kodein/emoji/compose/EmojiService.kt b/emoji-compose/src/commonMain/kotlin/org/kodein/emoji/compose/EmojiService.kt index d802143..3686cce 100644 --- a/emoji-compose/src/commonMain/kotlin/org/kodein/emoji/compose/EmojiService.kt +++ b/emoji-compose/src/commonMain/kotlin/org/kodein/emoji/compose/EmojiService.kt @@ -4,10 +4,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.produceState import kotlinx.coroutines.* -import org.kodein.emoji.Emoji -import org.kodein.emoji.EmojiFinder -import org.kodein.emoji.EmojiTemplateCatalog -import org.kodein.emoji.all +import org.kodein.emoji.* @OptIn(ExperimentalCoroutinesApi::class) @@ -45,7 +42,7 @@ public class EmojiService private constructor( if (!::deferred.isInitialized) { @OptIn(DelicateCoroutinesApi::class) deferred = GlobalScope.async { - val catalog = async(Dispatchers.Default) { EmojiTemplateCatalog(Emoji.all(), catalogBuilder) } + val catalog = async(Dispatchers.Default) { EmojiTemplateCatalog(Emoji.list(), catalogBuilder) } val finder = async(Dispatchers.Default) { EmojiFinder() } EmojiService(catalog.await(), finder.await()) } diff --git a/emoji-compose/src/commonMain/kotlin/org/kodein/emoji/compose/noto.kt b/emoji-compose/src/commonMain/kotlin/org/kodein/emoji/compose/noto.kt index 4c5c972..f0c9c84 100644 --- a/emoji-compose/src/commonMain/kotlin/org/kodein/emoji/compose/noto.kt +++ b/emoji-compose/src/commonMain/kotlin/org/kodein/emoji/compose/noto.kt @@ -1,5 +1,7 @@ package org.kodein.emoji.compose +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.height import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -9,6 +11,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import org.kodein.emoji.Emoji import kotlin.math.min @@ -42,19 +45,23 @@ internal fun EmojiFontPlaceholder(emoji: Emoji, modifier: Modifier) { @Composable internal expect fun PlatformEmojiPlaceholder(emoji: Emoji, modifier: Modifier) +private val defaultHeight = 24.dp + /** * Shows an Emoji as an image downloaded from the Noto image library. * * @param emoji The Emoji to render - * @param modifier The **madatory** modifier that **must define the size of the displayed emoji**, to be applied to the layout. + * @param modifier The modifier to be applied to the layout. * @param placeholder Composable that will be rendered in place during the download of the image. */ @Composable public fun NotoImageEmoji( emoji: Emoji, - modifier: Modifier, - placeholder: @Composable () -> Unit = { PlatformEmojiPlaceholder(emoji, modifier) } + modifier: Modifier = Modifier, + placeholder: @Composable (Modifier) -> Unit = { PlatformEmojiPlaceholder(emoji, it) } ) { + require(emoji.details.hasNotoImage) { "Emoji has not Noto image" } + val download = LocalEmojiDownloader.current var svg: SVGImage? by remember { mutableStateOf(null) } LaunchedEffect(emoji) { @@ -66,32 +73,23 @@ public fun NotoImageEmoji( } } + val imageModifier = modifier + .height(defaultHeight) + .aspectRatio(emoji.details.notoImageRatio) + if (svg != null) { - SVGImage(svg!!, "${emoji.details.description} emoji", modifier) + SVGImage(svg!!, "${emoji.details.description} emoji", imageModifier) } else { - placeholder() + placeholder(imageModifier) } } -@Deprecated( - message = "Modifier that defines size (width & height) is mandatory", - replaceWith = ReplaceWith("NotoImageEmoji(emoji, Modifier.size(16.dp), placeholder)"), - level = DeprecationLevel.ERROR -) -@Composable -public fun NotoImageEmoji( - emoji: Emoji, - placeholder: @Composable () -> Unit = { PlatformEmojiPlaceholder(emoji, Modifier) } -) { - NotoImageEmoji(emoji, Modifier, placeholder) -} - /** * Shows an animated Emoji if it is [Emoji.Details.notoAnimated], or defers to [NotoImageEmoji] if it is not. * * @param emoji The Emoji to render - * @param modifier The **madatory** modifier that **must define the size of the displayed emoji**, to be applied to the layout. + * @param modifier The modifier to be applied to the layout. * @param iterations The number of times that the animation will be played (default is infinite). * @param stopAt Progress that the emoji will stop at during its last iteration. * @param speed Speed at which the animation will be rendered. @@ -100,13 +98,15 @@ public fun NotoImageEmoji( @Composable public fun NotoAnimatedEmoji( emoji: Emoji, - modifier: Modifier, + modifier: Modifier = Modifier, iterations: Int = Int.MAX_VALUE, stopAt: Float = 1f, speed: Float = 1f, - placeholder: @Composable () -> Unit = { PlatformEmojiPlaceholder(emoji, modifier) } + placeholder: @Composable (Modifier) -> Unit = { PlatformEmojiPlaceholder(emoji, it) } ) { - if (!emoji.details.notoAnimated) { + require(emoji.details.hasNotoAnimation) { "Emoji has not Noto animation" } + + if (!emoji.details.hasNotoAnimation) { NotoImageEmoji(emoji, modifier, placeholder) return } @@ -124,29 +124,20 @@ public fun NotoAnimatedEmoji( } } - if (result != null) { - if (result!!.isSuccess) { - LottieAnimation(result!!.getOrThrow(), iterations, stopAt, speed, "${emoji.details.description} emoji", modifier) - } else { + val animationModifier = modifier + .height(defaultHeight) + .aspectRatio(emoji.details.notoAnimationRatio) + + val r = result + when { + r != null && r.isSuccess -> { + LottieAnimation(result!!.getOrThrow(), iterations, stopAt, speed, "${emoji.details.description} emoji", animationModifier) + } + r != null && r.isFailure && emoji.details.hasNotoImage -> { NotoImageEmoji(emoji, modifier, placeholder) } - } else { - placeholder() + else -> { + placeholder(animationModifier) + } } } - -@Deprecated( - message = "Modifier that defines size (width & height) is mandatory", - replaceWith = ReplaceWith("NotoAnimatedEmoji(emoji, Modifier.size(16.dp), iterations, stopAt, speed, placeholder)"), - level = DeprecationLevel.ERROR -) -@Composable -public fun NotoAnimatedEmoji( - emoji: Emoji, - iterations: Int = Int.MAX_VALUE, - stopAt: Float = 1f, - speed: Float = 1f, - placeholder: @Composable () -> Unit = { PlatformEmojiPlaceholder(emoji, Modifier) } -) { - NotoAnimatedEmoji(emoji, Modifier, iterations, stopAt, speed, placeholder) -} diff --git a/emoji-compose/src/commonMain/kotlin/org/kodein/emoji/compose/text.kt b/emoji-compose/src/commonMain/kotlin/org/kodein/emoji/compose/text.kt index 0a32624..6331e61 100644 --- a/emoji-compose/src/commonMain/kotlin/org/kodein/emoji/compose/text.kt +++ b/emoji-compose/src/commonMain/kotlin/org/kodein/emoji/compose/text.kt @@ -1,7 +1,6 @@ package org.kodein.emoji.compose import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.InlineTextContent import androidx.compose.foundation.text.appendInlineContent import androidx.compose.runtime.* @@ -10,11 +9,8 @@ import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.Placeholder import androidx.compose.ui.text.PlaceholderVerticalAlign import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.em -import kotlinx.coroutines.launch import org.kodein.emoji.Emoji -import org.kodein.emoji.FoundEmoji import org.kodein.emoji.findEmoji @@ -30,61 +26,12 @@ public fun String.withEmoji(): String { } @Composable -private fun WithDynamicSizedNotoEmoji( - text: CharSequence, - content: @Composable (AnnotatedString, Map) -> Unit, - createInlineTextContent: suspend (FoundEmoji) -> InlineTextContent? -) { - val service = EmojiService.get() ?: return - - val all = remember(text) { - service.finder.findEmoji(text) - .map { found -> - found to mutableStateOf(null) - } - .toList() - } - - LaunchedEffect(all) { - all.forEach { (found, inlineTextContent) -> - launch { - inlineTextContent.value = createInlineTextContent(found) - } - } - } - - val inlineContent = HashMap() - val annotatedString = buildAnnotatedString { - var start = 0 - all.forEach { (found, inlineTextContent) -> - if (text is AnnotatedString) - append(text.subSequence(start, found.start)) - else - append(text.substring(start, found.start)) - val itc = inlineTextContent.value - if (itc != null) { - val inlineContentID = "emoji:${found.emoji}" - inlineContent[inlineContentID] = itc - appendInlineContent(inlineContentID) - } else { - appendNotoPlaceholder(found.emoji, inlineContent) - } - start = found.end - } - if (text is AnnotatedString) - append(text.subSequence(start, text.length)) - else - append(text.substring(start, text.length)) - } - - content(annotatedString, inlineContent) -} - -@Composable -private fun WithFixedSizedNotoEmoji( +private fun WithNotoEmoji( text: CharSequence, + ratio: Emoji.() -> Float, + placeholder: @Composable (Emoji) -> Unit, + createDisplay: suspend (Emoji) -> (@Composable () -> Unit)?, content: @Composable (AnnotatedString, Map) -> Unit, - createInlineTextContent: suspend (FoundEmoji) -> InlineTextContent? ) { val service = EmojiService.get() ?: return @@ -98,16 +45,17 @@ private fun WithFixedSizedNotoEmoji( append(text.subSequence(start, found.start)) else append(text.substring(start, found.start)) + println(found.emoji) val inlineContentID = "emoji:${found.emoji}" - inlineContent[inlineContentID] = InlineTextContent(Placeholder(1.em, 1.em, PlaceholderVerticalAlign.Center)) { - var itc: InlineTextContent? by remember { mutableStateOf(null) } + inlineContent[inlineContentID] = InlineTextContent(Placeholder(found.emoji.ratio().em, 1.em, PlaceholderVerticalAlign.Center)) { + var display: (@Composable () -> Unit)? by remember { mutableStateOf(null) } LaunchedEffect(null) { - itc = createInlineTextContent(found) + display = createDisplay(found.emoji) } - if (itc == null) { - content(AnnotatedString(found.emoji.details.string), emptyMap()) + if (display == null) { + placeholder(found.emoji) } else { - itc!!.children("") + display!!.invoke() } } appendInlineContent(inlineContentID) @@ -122,27 +70,13 @@ private fun WithFixedSizedNotoEmoji( content(annotatedString, inlineContent) } -@Composable -private fun WithNotoEmoji( - text: CharSequence, - content: @Composable (AnnotatedString, Map) -> Unit, - createInlineTextContent: suspend (FoundEmoji) -> InlineTextContent?, - fixedSize: Boolean, -) { - if (fixedSize) WithFixedSizedNotoEmoji(text, content, createInlineTextContent) - else WithDynamicSizedNotoEmoji(text, content, createInlineTextContent) -} - -private suspend fun createNotoSvgInlineContent(emoji: Emoji, download: suspend (EmojiUrl) -> ByteArray): InlineTextContent? { +private suspend fun createNotoSvgContent(emoji: Emoji, download: suspend (EmojiUrl) -> ByteArray): (@Composable () -> Unit)? { try { val bytes = download(EmojiUrl.from(emoji, EmojiUrl.Type.SVG)) val svg = SVGImage.create(bytes) - return InlineTextContent( - placeholder = Placeholder(1.em, 1.em / svg.sizeRatio(), PlaceholderVerticalAlign.Center), - children = { - SVGImage(svg, "${emoji.details.description} emoji", Modifier.fillMaxSize()) - } - ) + return { + SVGImage(svg, "${emoji.details.description} emoji", Modifier.fillMaxSize()) + } } catch (t: Throwable) { println("${t::class.simpleName}: ${t.message}") return null @@ -154,44 +88,53 @@ private suspend fun createNotoSvgInlineContent(emoji: Emoji, download: suspend ( * Replaces all emojis with [NotoImageEmoji]. * * @param text The text to with Emoji UTF characters. - * @param fixedSize If true, then the emoji will not be resized once downloaded. * @param content A lambda that receives the `AnnotatedString` and its corresponding `InlineTextContent` map * These should be used to display: `{ astr, map -> Text(astr, inlineContent = map) }`. */ @Composable public fun WithNotoImageEmoji( text: CharSequence, - fixedSize: Boolean = false, + placeholder: @Composable (Emoji) -> Unit = { PlatformEmojiPlaceholder(it, Modifier.fillMaxSize()) }, content: @Composable (AnnotatedString, Map) -> Unit ) { val download = LocalEmojiDownloader.current WithNotoEmoji( text = text, - content = content, - createInlineTextContent = { found -> createNotoSvgInlineContent(found.emoji, download) }, - fixedSize = fixedSize, + ratio = { + details.notoImageRatio.takeIf { it > 0f } + ?: 1f + }, + placeholder = placeholder, + createDisplay = { emoji -> createNotoSvgContent(emoji, download) }, + content = content ) } -private suspend fun createNotoLottieInlineContent( +@Deprecated("fixedSize is now ignored (size ratio is now part of emoji details)") +@Composable +public fun WithNotoImageEmoji( + text: CharSequence, + fixedSize: Boolean, + content: @Composable (AnnotatedString, Map) -> Unit +): Unit = WithNotoImageEmoji(text = text, content = content) + + +private suspend fun createNotoLottieContent( emoji: Emoji, iterations: Int, speed: Float, download: suspend (EmojiUrl) -> ByteArray -): InlineTextContent? { - if (!emoji.details.notoAnimated) return createNotoSvgInlineContent(emoji, download) +): (@Composable () -> Unit)? { + if (!emoji.details.hasNotoAnimation) return createNotoSvgContent(emoji, download) try { val bytes = download(EmojiUrl.from(emoji, EmojiUrl.Type.Lottie)) val animation = LottieAnimation.create(bytes) - return InlineTextContent( - placeholder = Placeholder(1.em, 1.em / animation.sizeRatio(), PlaceholderVerticalAlign.Center), - children = { - LottieAnimation(animation, iterations, 1f, speed, "${emoji.details.description} emoji", Modifier.fillMaxSize()) - } - ) + return { + LottieAnimation(animation, iterations, 1f, speed, "${emoji.details.description} emoji", Modifier.fillMaxSize()) + } } catch (t: Throwable) { println("${t::class.simpleName}: ${t.message}") - return createNotoSvgInlineContent(emoji, download) + return createNotoSvgContent(emoji, download) } } @@ -202,7 +145,6 @@ private suspend fun createNotoLottieInlineContent( * @param text The text to with Emoji UTF characters. * @param iterations The number of times that the animations will be played (default is infinite). * @param speed Speed at which the animations will be rendered. - * @param fixedSize If true, then the emoji will not be resized once downloaded. * @param content A lambda that receives the `AnnotatedString` and its corresponding `InlineTextContent` map * These should be used to display: `{ astr, map -> Text(astr, inlineContent = map) }`. */ @@ -211,18 +153,32 @@ public fun WithNotoAnimatedEmoji( text: CharSequence, iterations: Int = Int.MAX_VALUE, speed: Float = 1f, - fixedSize: Boolean = false, + placeholder: @Composable (Emoji) -> Unit = { PlatformEmojiPlaceholder(it, Modifier.fillMaxSize()) }, content: @Composable (AnnotatedString, Map) -> Unit ) { val download = LocalEmojiDownloader.current WithNotoEmoji( text = text, + ratio = { + details.notoAnimationRatio.takeIf { it > 0f } + ?: details.notoImageRatio.takeIf { it > 0f } + ?: 1f + }, + placeholder = placeholder, + createDisplay = { emoji -> createNotoLottieContent(emoji, iterations, speed, download) }, content = content, - createInlineTextContent = { found -> createNotoLottieInlineContent(found.emoji, iterations, speed, download) }, - fixedSize = fixedSize ) } +@Deprecated("fixedSize is now ignored (size ratio is now part of emoji details)") +@Composable +public fun WithNotoAnimatedEmoji( + text: CharSequence, + iterations: Int = Int.MAX_VALUE, + speed: Float = 1f, + fixedSize: Boolean, + content: @Composable (AnnotatedString, Map) -> Unit +): Unit = WithNotoAnimatedEmoji(text = text, iterations = iterations, speed = speed, content = content) /** * Creates an annotated String and a `InlineTextContent` map from a text containing Emoji characters. * @@ -237,6 +193,13 @@ public fun WithNotoAnimatedEmoji( @Composable public expect fun WithPlatformEmoji( text: CharSequence, - fixedImageSize: Boolean = false, content: @Composable (AnnotatedString, Map) -> Unit ) + +@Deprecated("fixedSize is now ignored (size ratio is now part of emoji details)") +@Composable +public fun WithPlatformEmoji( + text: CharSequence, + fixedImageSize: Boolean, + content: @Composable (AnnotatedString, Map) -> Unit +): Unit = WithPlatformEmoji(text, content) diff --git a/emoji-compose/src/iosMain/kotlin/org/kodein/emoji/compose/iosPlatform.kt b/emoji-compose/src/iosMain/kotlin/org/kodein/emoji/compose/iosPlatform.kt index dcb8a3d..4118e59 100644 --- a/emoji-compose/src/iosMain/kotlin/org/kodein/emoji/compose/iosPlatform.kt +++ b/emoji-compose/src/iosMain/kotlin/org/kodein/emoji/compose/iosPlatform.kt @@ -36,7 +36,6 @@ internal actual suspend fun platformDownloadBytes(url: String): ByteArray { @Composable public actual fun WithPlatformEmoji( text: CharSequence, - fixedImageSize: Boolean, content: @Composable (AnnotatedString, Map) -> Unit ) { val annotatedString = remember(text) { AnnotatedString.Builder().append(text).toAnnotatedString() } diff --git a/emoji-compose/src/jvmBasedMain/kotlin/org/kodein/emoji/compose/jvmPlatform.kt b/emoji-compose/src/jvmBasedMain/kotlin/org/kodein/emoji/compose/jvmPlatform.kt index 90bf0be..4d5299a 100644 --- a/emoji-compose/src/jvmBasedMain/kotlin/org/kodein/emoji/compose/jvmPlatform.kt +++ b/emoji-compose/src/jvmBasedMain/kotlin/org/kodein/emoji/compose/jvmPlatform.kt @@ -19,7 +19,6 @@ internal actual suspend fun platformDownloadBytes(url: String): ByteArray = @Composable public actual fun WithPlatformEmoji( text: CharSequence, - fixedImageSize: Boolean, content: @Composable (AnnotatedString, Map) -> Unit ) { val annotatedString = remember(text) { AnnotatedString.Builder().append(text).toAnnotatedString() } diff --git a/emoji-compose/src/skiaMain/kotlin/org/kodein/emoji/compose/skiaVectorImages.kt b/emoji-compose/src/skiaMain/kotlin/org/kodein/emoji/compose/skiaVectorImages.kt index dd8e9e1..c15071d 100644 --- a/emoji-compose/src/skiaMain/kotlin/org/kodein/emoji/compose/skiaVectorImages.kt +++ b/emoji-compose/src/skiaMain/kotlin/org/kodein/emoji/compose/skiaVectorImages.kt @@ -85,6 +85,9 @@ internal actual fun LottieAnimation( contentDescription: String, modifier: Modifier ) { + require(iterations > 0) { "Invalid iterations" } + require(stopAt in 0f..1f) { "Invalid stopAt" } + require(speed > 0f) { "Invalid speed" } val time = remember { Animatable(0f) } LaunchedEffect(Unit) { repeat(iterations) { diff --git a/emoji-compose/src/wasmJsMain/kotlin/org/kodein/emoji/compose/platformWasm.kt b/emoji-compose/src/wasmJsMain/kotlin/org/kodein/emoji/compose/platformWasm.kt index d72a55a..19422bb 100644 --- a/emoji-compose/src/wasmJsMain/kotlin/org/kodein/emoji/compose/platformWasm.kt +++ b/emoji-compose/src/wasmJsMain/kotlin/org/kodein/emoji/compose/platformWasm.kt @@ -26,12 +26,10 @@ internal actual suspend fun platformDownloadBytes(url: String): ByteArray { @Composable public actual fun WithPlatformEmoji( text: CharSequence, - fixedImageSize: Boolean, content: @Composable (AnnotatedString, Map) -> Unit ) { WithNotoImageEmoji( text = text, - fixedSize = fixedImageSize, content = content ) } diff --git a/emoji-kt/build.gradle.kts b/emoji-kt/build.gradle.kts index 5e3c9c6..297ccf9 100644 --- a/emoji-kt/build.gradle.kts +++ b/emoji-kt/build.gradle.kts @@ -2,17 +2,17 @@ plugins { kodein.library.mpp } -val genEmojis = tasks.create("genEmojis") - kotlin.kodein { all { compilations.main { - compileTaskProvider { dependsOn(genEmojis) } + compileTaskProvider { dependsOn(":genEmojis") } } } common.main { - kotlin.srcDirs(genEmojis.genDirectory) + kotlin.srcDirs(provider { + rootProject.tasks.getByName("genEmojis").genEmojiDirectory + }) } } diff --git a/emoji-kt/src/commonMain/kotlin/org/kodein/emoji/Emoji.kt b/emoji-kt/src/commonMain/kotlin/org/kodein/emoji/Emoji.kt index 2c7f7c1..9cf3104 100644 --- a/emoji-kt/src/commonMain/kotlin/org/kodein/emoji/Emoji.kt +++ b/emoji-kt/src/commonMain/kotlin/org/kodein/emoji/Emoji.kt @@ -35,8 +35,15 @@ public sealed interface Emoji { val unicodeVersion: UnicodeVersion, val aliases: List, val emoticons: List, - val notoAnimated: Boolean - ) + val notoImageRatio: Float, + val notoAnimationRatio: Float + ) { + val hasNotoImage: Boolean get() = notoImageRatio != 0f + val hasNotoAnimation: Boolean get() = notoAnimationRatio != 0f + @Deprecated("Renamed hasNotoAnimation", ReplaceWith("hasNotoAnimation")) + val notoAnimated: Boolean get() = hasNotoAnimation + + } public companion object } @@ -163,7 +170,8 @@ internal open class SkinTone1EmojiImpl( unicodeVersion = details.unicodeVersion, aliases = details.aliases.map { it + "~${tone.alias}" }, emoticons = emptyList(), - notoAnimated = details.notoAnimated + notoImageRatio = details.notoImageRatio, + notoAnimationRatio = details.notoAnimationRatio ), original = this, tone1 = tone @@ -185,7 +193,8 @@ internal open class UnqualifiedSkinTone1EmojiImpl( unicodeVersion = details.unicodeVersion, aliases = details.aliases.map { it + "~${tone.alias}" }, emoticons = emptyList(), - notoAnimated = details.notoAnimated + notoImageRatio = details.notoImageRatio, + notoAnimationRatio = details.notoAnimationRatio ), original = this, tone1 = tone @@ -210,7 +219,8 @@ internal class SkinTone2EmojiZWJImpl internal constructor( unicodeVersion = zwjUnicodeVersion, aliases = details.aliases.map { it + "~${tone1.alias},${tone2.alias}" }, emoticons = emptyList(), - notoAnimated = details.notoAnimated + notoImageRatio = details.notoImageRatio, + notoAnimationRatio = details.notoAnimationRatio ), original = this, tone1 = tone1, @@ -233,7 +243,8 @@ internal class SkinTone2EmojiImpl internal constructor( unicodeVersion = details.unicodeVersion, aliases = details.aliases.map { it + "~${tone1.alias},${tone2.alias}" }, emoticons = emptyList(), - notoAnimated = details.notoAnimated + notoImageRatio = details.notoImageRatio, + notoAnimationRatio = details.notoAnimationRatio ), original = this, tone1 = tone1, diff --git a/emoji-kt/src/commonTest/kotlin/org/kodein/emoji/EmojiTests.kt b/emoji-kt/src/commonTest/kotlin/org/kodein/emoji/EmojiTests.kt index 054a95f..20682b8 100644 --- a/emoji-kt/src/commonTest/kotlin/org/kodein/emoji/EmojiTests.kt +++ b/emoji-kt/src/commonTest/kotlin/org/kodein/emoji/EmojiTests.kt @@ -22,7 +22,7 @@ class EmojiTests { @Test fun replaceEmojis() { - val catalog = EmojiTemplateCatalog(Emoji.all()) + val catalog = EmojiTemplateCatalog(Emoji.list()) assertEquals( expected = "When I see 🧑🏼‍🤝‍🧑🏾, my ❤️ goes 💥 😀!", actual = catalog.replace("When I see :people-holding-hands~medium-light,medium-dark:, my <3 goes :collision: :D!") @@ -43,6 +43,6 @@ class EmojiTests { @Test fun countEmoji() { - assertEquals(emojiCount, Emoji.all().size) + assertEquals(emojiCount, Emoji.list().size) } }