Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@
bin/
gen/

# Gradle files
# Gradle & Kotlin build files
.gradle/
.kotlin/
build/

# Local configuration file (sdk path, etc)
Expand Down
2 changes: 2 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ buildscript {
dependencies {
classpath(libs.plugin.android.cache.fix)
classpath(libs.plugin.androidgradleplugin)
classpath(libs.plugin.compose)
classpath(libs.plugin.compose.multiplatform)
classpath(libs.plugin.dokka)
classpath(libs.plugin.kotlin)
classpath(libs.plugin.licensee)
Expand Down
10 changes: 10 additions & 0 deletions emoji-compose/api/current.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// Signature format: 4.0
package com.vanniktech.emoji.compose {

public final class EmojiPickerKt {
}

public final class EmojiPickerVariantPopupKt {
}

}
68 changes: 68 additions & 0 deletions emoji-compose/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
plugins {
id("org.jetbrains.dokka")
id("org.jetbrains.kotlin.multiplatform")
id("org.jetbrains.compose")
id("org.jetbrains.kotlin.plugin.compose")
id("com.android.library")
id("org.jetbrains.kotlin.plugin.parcelize")
id("me.tylerbwong.gradle.metalava")
id("com.vanniktech.maven.publish")
id("app.cash.licensee")
}

licensee {
allow("Apache-2.0")
}

metalava {
filename.set("api/current.txt")
}

kotlin {
applyDefaultHierarchyTemplate()

androidTarget {
publishLibraryVariants("release")
}

sourceSets {
val commonMain by getting {
dependencies {
api(project(":emoji"))
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
}
}

val commonTest by getting {
dependencies {
implementation(libs.kotlin.test)
}
}

val androidMain by getting {
dependencies {
}
}

val androidUnitTest by getting {
dependencies {
implementation(libs.kotlin.test.junit)
}
}
}
}

android {
namespace = "com.vanniktech.emoji.compose"

compileSdk = libs.versions.compileSdk.get().toInt()

defaultConfig {
minSdk = libs.versions.minSdk.get().toInt()
}

resourcePrefix = "emoji_"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/*
* Copyright (C) 2016 - Niklas Baudy, Ruben Gees, Mario Đanić and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.vanniktech.emoji.compose

import androidx.compose.foundation.Canvas
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
import androidx.compose.ui.graphics.nativeCanvas
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.sp
import com.vanniktech.emoji.Emoji
import com.vanniktech.emoji.EmojiAndroidProvider
import com.vanniktech.emoji.EmojiProvider
import com.vanniktech.emoji.preferredUnicode
import kotlin.math.roundToInt

/**
* Android actual implementation of [EmojiImage].
* Renders the provider drawable via Canvas, falling back to unicode text font.
*/
@Composable actual fun EmojiImage(
emoji: Emoji,
provider: EmojiProvider?,
modifier: Modifier,
) {
val context = LocalContext.current
val drawable = remember(emoji, provider, context) {
if (provider is EmojiAndroidProvider) {
try {
provider.getDrawable(emoji, context)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm can we use skia or the normal resource system to make this truly multiplatform?

} catch (_: Exception) { null }
} else null
}

if (drawable != null) {
Canvas(modifier = modifier) {
drawIntoCanvas { canvas ->
drawable.setBounds(0, 0, size.width.roundToInt(), size.height.roundToInt())
drawable.draw(canvas.nativeCanvas)
}
}
} else {
BoxWithConstraints(
modifier = modifier,
contentAlignment = Alignment.Center,
) {
val fontSize = (minOf(maxWidth, maxHeight).value * 0.75f).sp
Text(
text = emoji.preferredUnicode(),
fontSize = fontSize,
textAlign = TextAlign.Center,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*
* Copyright (C) 2016 - Niklas Baudy, Ruben Gees, Mario Đanić and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.vanniktech.emoji.compose
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.vanniktech.emoji.EmojiAndroidProvider
import com.vanniktech.emoji.EmojiCategory
import com.vanniktech.emoji.EmojiManager
import com.vanniktech.emoji.EmojiProvider
import com.vanniktech.emoji.recent.RecentEmoji
import com.vanniktech.emoji.recent.RecentEmojiManager
import com.vanniktech.emoji.variant.VariantEmoji
import com.vanniktech.emoji.variant.VariantEmojiManager

/**
* Returns the default [RecentEmojiManager] for Android using [LocalContext].
*/
@Composable actual fun rememberDefaultRecentEmoji(): RecentEmoji {
val context = LocalContext.current

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There must also be something for commonMain, right?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For commonMain, should we default to NoRecentEmoji / NoVariantEmoji or something like an in-memory session manager? And for other platforms native storage like NSUserDefaults for iOS and Preferences for Desktop?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NSUserDefaults for iOS, Preferences for Desktop and then we don't need the NoOp, right?

return remember(context) { RecentEmojiManager(context) }
}

/**
* Returns the default [VariantEmojiManager] for Android using [LocalContext].
*/
@Composable actual fun rememberDefaultVariantEmoji(): VariantEmoji {
val context = LocalContext.current
return remember(context) { VariantEmojiManager(context) }
}

/**
* Renders the category tab icon using Android vector resources or a text emoji fallback.
*/
@Composable actual fun CategoryTabIcon(

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is this in androidMain? I don't see anything that should not be avaiable from commonMain, the resource for the icon we can also put there!

category: EmojiCategory?,
isRecent: Boolean,
isSelected: Boolean,
colors: EmojiPickerColors,
fallbackEmoji: String,
provider: EmojiProvider?,
) {
val tint = if (isSelected) colors.tabSelectedColor else colors.tabUnselectedColor
val iconRes = remember(category, isRecent, provider) {
if (isRecent) {
com.vanniktech.emoji.R.drawable.emoji_recent
} else if (category != null && provider is EmojiAndroidProvider) {
try { provider.getIcon(category) } catch (_: Exception) { null }
} else null
}

if (iconRes != null && iconRes != 0) {
Icon(
painter = painterResource(iconRes),
contentDescription = null,
tint = tint,
modifier = Modifier.size(22.dp),
)
} else if (fallbackEmoji.isNotEmpty()) {
Text(
text = fallbackEmoji,
fontSize = 18.sp,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/*
* Copyright (C) 2016 - Niklas Baudy, Ruben Gees, Mario Đanić and contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.vanniktech.emoji.compose

import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.HorizontalDivider
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import com.vanniktech.emoji.Emoji
import com.vanniktech.emoji.EmojiCategory
import com.vanniktech.emoji.EmojiProvider
import com.vanniktech.emoji.preferredUnicode

/**
* Tab bar displaying category icons (and optional recents tab icon) for [EmojiPicker].
*/
@Composable
fun EmojiCategoryTabBar(
categories: Array<EmojiCategory>,
selectedTabIndex: Int,
onTabSelected: (Int) -> Unit,
modifier: Modifier = Modifier,
hasRecentsTab: Boolean = false,
recentEmojis: List<Emoji> = emptyList(),
colors: EmojiPickerColors = EmojiPickerDefaults.colors(),
emojiProvider: EmojiProvider? = null,
onCategoryIcon: ((EmojiCategory) -> String)? = null,
) {
Column(modifier = modifier) {
Row(
modifier = Modifier
.fillMaxWidth()
.height(44.dp)
.padding(horizontal = 4.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
if (hasRecentsTab) {
val isSelected = selectedTabIndex == 0
val recentFallbackEmoji = recentEmojis.firstOrNull()?.preferredUnicode() ?: ""
Box(
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(10.dp))
.background(
color = if (isSelected) colors.tabSelectedColor.copy(alpha = 0.12f) else Color.Transparent,
)
.clickable { onTabSelected(0) },
contentAlignment = Alignment.Center,
) {
CategoryTabIcon(
category = null,
isRecent = true,
isSelected = isSelected,
colors = colors,
fallbackEmoji = recentFallbackEmoji,
provider = emojiProvider,
)
}
}

categories.forEachIndexed { index, category ->
val tabIndex = if (hasRecentsTab) index + 1 else index
val isSelected = selectedTabIndex == tabIndex
val iconEmoji = onCategoryIcon?.invoke(category) ?: category.emojis.firstOrNull()?.preferredUnicode() ?: ""
Box(
modifier = Modifier
.size(36.dp)
.clip(RoundedCornerShape(10.dp))
.background(
color = if (isSelected) colors.tabSelectedColor.copy(alpha = 0.12f) else Color.Transparent,
)
.clickable { onTabSelected(tabIndex) },
contentAlignment = Alignment.Center,
) {
CategoryTabIcon(
category = category,
isRecent = false,
isSelected = isSelected,
colors = colors,
fallbackEmoji = iconEmoji,
provider = emojiProvider,
)
}
}
}

HorizontalDivider(
color = colors.dividerColor,
thickness = 1.dp,
)
}
}
Loading