Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FontVariation.Settings support to the resources library #5183

Merged
merged 1 commit into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion components/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ android.useAndroidX=true
#Versions
kotlin.version=1.9.24
agp.version=8.2.2
compose.version=1.7.0
compose.version=1.8.0-alpha01
deploy.version=0.1.0-SNAPSHOT

#Compose
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,33 @@ package org.jetbrains.compose.resources.demo.shared

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedCard
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.unit.dp
import components.resources.demo.shared.generated.resources.Res
import components.resources.demo.shared.generated.resources.*
import components.resources.demo.shared.generated.resources.RobotoFlex_VariableFont
import components.resources.demo.shared.generated.resources.Workbench_Regular
import components.resources.demo.shared.generated.resources.font_awesome
import org.jetbrains.compose.resources.Font
import kotlin.math.roundToInt

@Composable
fun FontRes(paddingValues: PaddingValues) {
Expand Down Expand Up @@ -74,5 +87,56 @@ fun FontRes(paddingValues: PaddingValues) {
style = MaterialTheme.typography.headlineLarge,
text = "\uf1ba \uf238 \uf21a \uf1bb \uf1b8 \uf09b \uf269 \uf1d0 \uf15a \uf293 \uf1c6"
)

var variableFontWeight by remember { mutableStateOf(200) }
val variableFont = Font(
resource = Res.font.RobotoFlex_VariableFont,
variationSettings = FontVariation.Settings(FontVariation.weight(variableFontWeight))
)
OutlinedCard(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
shape = RoundedCornerShape(4.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.primaryContainer)
) {
Text(
modifier = Modifier.padding(8.dp),
text = """
Text(
modifier = Modifier.padding(16.dp),
fontFamily = FontFamily(
Font(
Res.font.RobotoFlex_VariableFont,
variationSettings = FontVariation.Settings(
FontVariation.weight($variableFontWeight)
)
),
),
text = "The quick brown fox jumps over the lazy dog"
)
""".trimIndent(),
color = MaterialTheme.colorScheme.onPrimaryContainer,
softWrap = false
)
}

Text(
modifier = Modifier.padding(16.dp),
fontFamily = FontFamily(variableFont),
text = "The quick brown fox jumps over the lazy dog"
)

Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 16.dp)
) {
Text(text = "Weight:")
Spacer(modifier = Modifier.size(16.dp))
Slider(
value = variableFontWeight.toFloat(),
onValueChange = { variableFontWeight = it.roundToInt() },
valueRange = 100f..1000f,
steps = 9
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,27 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.*

@Deprecated(
message = "Use the new Font function with variationSettings instead.",
level = DeprecationLevel.HIDDEN
)
@Composable
actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font {
val environment = LocalComposeEnvironment.current.rememberEnvironment()
val path = remember(environment, resource) { resource.getResourceItemByEnvironment(environment).path }
val assets = LocalContext.current.assets
return Font(path, assets, weight, style)
}

@Composable
actual fun Font(
resource: FontResource,
weight: FontWeight,
style: FontStyle,
variationSettings: FontVariation.Settings,
): Font {
val environment = LocalComposeEnvironment.current.rememberEnvironment()
val path = remember(environment, resource) { resource.getResourceItemByEnvironment(environment).path }
val assets = LocalContext.current.assets
return Font(path, assets, weight, style, variationSettings)
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,21 @@ internal actual fun <T> rememberResourceState(
runBlocking { block(environment) }
)
}
}

@Composable
internal actual fun <T> rememberResourceState(
key1: Any,
key2: Any,
key3: Any,
key4: Any,
getDefault: () -> T,
block: suspend (ResourceEnvironment) -> T
): State<T> {
val environment = LocalComposeEnvironment.current.rememberEnvironment()
return remember(key1, key2, key3, key4, environment) {
mutableStateOf(
runBlocking { block(environment) }
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import androidx.compose.ui.text.font.*
*/
@Immutable
class FontResource
@InternalResourceApi constructor(id: String, items: Set<ResourceItem>): Resource(id, items)
@InternalResourceApi constructor(id: String, items: Set<ResourceItem>) : Resource(id, items)

/**
* Creates a font using the specified font resource, weight, and style.
Expand All @@ -28,13 +28,37 @@ class FontResource
*
* @throws NotFoundException if the specified resource ID is not found.
*/
@Deprecated(
message = "Use the new Font function with variationSettings instead.",
level = DeprecationLevel.HIDDEN
)
@Composable
expect fun Font(
resource: FontResource,
weight: FontWeight = FontWeight.Normal,
style: FontStyle = FontStyle.Normal
): Font

/**
* Creates a font using the specified font resource, weight, and style.
*
* @param resource The font resource to be used.
* @param weight The weight of the font. Default value is [FontWeight.Normal].
* @param style The style of the font. Default value is [FontStyle.Normal].
* @param variationSettings Custom variation settings for the font, with a default value derived from the specified [weight] and [style].
*
* @return The created [Font] object.
*
* @throws NotFoundException if the specified resource ID is not found.
*/
@Composable
expect fun Font(
resource: FontResource,
weight: FontWeight = FontWeight.Normal,
style: FontStyle = FontStyle.Normal,
variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style)
): Font

/**
* Retrieves the byte array of the font resource.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,20 @@ internal expect fun <T> rememberResourceState(
key3: Any,
getDefault: () -> T,
block: suspend (ResourceEnvironment) -> T
): State<T>


/**
* This is a platform-specific function that calculates and remembers a state.
* For all platforms except a JS it is a blocking function.
* On the JS platform it loads the state asynchronously and uses `getDefault` as an initial state value.
*/
@Composable
internal expect fun <T> rememberResourceState(
key1: Any,
key2: Any,
key3: Any,
key4: Any,
getDefault: () -> T,
block: suspend (ResourceEnvironment) -> T
): State<T>
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.text.font.*
import androidx.compose.ui.text.platform.Font
import androidx.compose.ui.unit.Density
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi

Expand Down Expand Up @@ -33,6 +34,10 @@ private val fontCache = AsyncCache<String, Font>()
internal val Font.isEmptyPlaceholder: Boolean
get() = this == defaultEmptyFont

@Deprecated(
message = "Use the new Font function with variationSettings instead.",
level = DeprecationLevel.HIDDEN
)
@Composable
actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): Font {
val resourceReader = LocalResourceReader.currentOrPreview
Expand All @@ -45,4 +50,31 @@ actual fun Font(resource: FontResource, weight: FontWeight, style: FontStyle): F
}
}
return fontFile
}

@Composable
actual fun Font(
resource: FontResource,
weight: FontWeight,
style: FontStyle,
variationSettings: FontVariation.Settings,
): Font {
val resourceReader = LocalResourceReader.currentOrPreview
val fontFile by rememberResourceState(resource, weight, style, variationSettings, { defaultEmptyFont }) { env ->
val path = resource.getResourceItemByEnvironment(env).path
val key = "$path:$weight:$style:${variationSettings.getCacheKey()}"
fontCache.getOrLoad(key) {
val fontBytes = resourceReader.read(path)
Font(key, fontBytes, weight, style, variationSettings)
}
}
return fontFile
}

internal fun FontVariation.Settings.getCacheKey(): String {
val defaultDensity = Density(1f)
return settings
.map { "${it::class.simpleName}(${it.axisName},${it.toVariationValue(defaultDensity)})" }
.sorted()
.joinToString(",")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package org.jetbrains.compose.resources

import androidx.compose.ui.text.font.FontVariation
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

class VariationFontCacheTest {

@Test
fun `getCacheKey should return an empty string for an empty settings list`() {
val settings = FontVariation.Settings()
val cacheKey = settings.getCacheKey()
assertEquals("", cacheKey, "Cache key for empty settings list should be an empty string")
}

@Test
fun `getCacheKey should return a correct key for a single setting`() {
val setting = FontVariation.Setting("wght", 700f)
val settings = FontVariation.Settings(setting)
val cacheKey = settings.getCacheKey()
assertEquals("SettingFloat(wght,700.0)", cacheKey, "Cache key for a single setting is incorrect")
}

@Test
fun `getCacheKey should correctly sort settings by class name and axis name`() {
val setting1 = FontVariation.Setting("wght", 400f)
val setting2 = FontVariation.Setting("ital", 1f)
val settings = FontVariation.Settings(setting1, setting2)
val cacheKey = settings.getCacheKey()
assertEquals(
"SettingFloat(ital,1.0),SettingFloat(wght,400.0)",
cacheKey,
"Cache key should sort settings by class name and axis name"
)
}

@Test
fun `getCacheKey should throw an exception when there are duplicate settings`() {
val setting1 = FontVariation.Setting("wght", 400f)
val setting2 = FontVariation.Setting("wght", 700f)

assertFailsWith<IllegalArgumentException>(
"'axis' must be unique"
) {
FontVariation.Settings(setting1, setting2)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.text.font.FontWeight

/**
Expand Down Expand Up @@ -90,6 +91,7 @@ internal fun getResourceUrl(windowOrigin: String, windowPathname: String, resour
* @param resource The font resource to be used.
* @param weight The weight of the font. Default value is [FontWeight.Normal].
* @param style The style of the font. Default value is [FontStyle.Normal].
* @param variationSettings Custom variation settings for the font, with a default value derived from the specified [weight] and [style].
* @return A [State]<[Font]?> object that holds the loaded [Font] when available,
* or `null` if the font is not yet ready.
*/
Expand All @@ -98,10 +100,11 @@ internal fun getResourceUrl(windowOrigin: String, windowPathname: String, resour
fun preloadFont(
resource: FontResource,
weight: FontWeight = FontWeight.Normal,
style: FontStyle = FontStyle.Normal
style: FontStyle = FontStyle.Normal,
variationSettings: FontVariation.Settings = FontVariation.Settings(weight, style),
): State<Font?> {
val resState = remember(resource, weight, style) { mutableStateOf<Font?>(null) }.apply {
value = Font(resource, weight, style).takeIf { !it.isEmptyPlaceholder }
val resState = remember(resource, weight, style, variationSettings) { mutableStateOf<Font?>(null) }.apply {
value = Font(resource, weight, style, variationSettings).takeIf { !it.isEmptyPlaceholder }
}
return resState
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,24 @@ internal actual fun <T> rememberResourceState(
}
mutableState
}
}

@Composable
internal actual fun <T> rememberResourceState(
key1: Any,
key2: Any,
key3: Any,
key4: Any,
getDefault: () -> T,
block: suspend (ResourceEnvironment) -> T
): State<T> {
val environment = LocalComposeEnvironment.current.rememberEnvironment()
val scope = rememberCoroutineScope()
return remember(key1, key2, key3, key4, environment) {
val mutableState = mutableStateOf(getDefault())
scope.launch(start = CoroutineStart.UNDISPATCHED) {
mutableState.value = block(environment)
}
mutableState
}
}
2 changes: 1 addition & 1 deletion gradle-plugins/gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ kotlin.code.style=official
dev.junit.parallel=false

# Default version of Compose Libraries used by Gradle plugin
compose.version=1.7.1
compose.version=1.8.0-alpha01
# The latest version of Kotlin compatible with compose.tests.compiler.version. Used only in tests/CI.
compose.tests.kotlin.version=2.0.0
# __SUPPORTED_GRADLE_VERSIONS__
Expand Down
Loading