Browse Source

Add FontVariation.Settings support to the resources library (#5183)

Sample:
```kotlin
Text(
    modifier = Modifier.padding(16.dp),
    fontFamily = FontFamily(
        Font(
            Res.font.RobotoFlex_VariableFont,
            variationSettings = FontVariation.Settings(FontVariation.weight(400))
        ),
    ),
    text = "The quick brown fox jumps over the lazy dog"
)
```

Fixes https://youtrack.jetbrains.com/issue/CMP-7088

## Testing
Demo app:
`./gradlew :resources:demo:desktopApp:run`
<img width="600" alt="image"
src="https://github.com/user-attachments/assets/7fc0403b-6732-42bb-97dc-211136a82d44"
/>


## Release Notes
### Highlights - Resources
- Add FontVariation.Settings support to the resources library

Co-authored-by: adamglin <wwalkingg@gmail.com>
pull/4132/merge v1.8.0+dev1976
Konstantin 5 days ago committed by GitHub
parent
commit
93b2f1d482
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      components/gradle.properties
  2. BIN
      components/resources/demo/shared/src/commonMain/composeResources/font/RobotoFlex-VariableFont.ttf
  3. 66
      components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt
  4. 17
      components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt
  5. 17
      components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/ResourceState.blocking.kt
  6. 26
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt
  7. 16
      components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceState.kt
  8. 32
      components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt
  9. 49
      components/resources/library/src/skikoTest/kotlin/org/jetbrains/compose/resources/VariationFontCacheTest.kt
  10. 9
      components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/Resource.web.kt
  11. 20
      components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/ResourceState.web.kt
  12. 2
      gradle-plugins/gradle.properties

2
components/gradle.properties

@ -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

BIN
components/resources/demo/shared/src/commonMain/composeResources/font/RobotoFlex-VariableFont.ttf

Binary file not shown.

66
components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt

@ -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) {
@ -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
)
}
}
}

17
components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt

@ -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)
}

17
components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/ResourceState.blocking.kt

@ -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) }
)
}
}

26
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt

@ -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.
@ -28,6 +28,10 @@ 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,
@ -35,6 +39,26 @@ expect fun Font(
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.
*

16
components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceState.kt

@ -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>

32
components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt

@ -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
@ -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
@ -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(",")
}

49
components/resources/library/src/skikoTest/kotlin/org/jetbrains/compose/resources/VariationFontCacheTest.kt

@ -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)
}
}
}

9
components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/Resource.web.kt

@ -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
/**
@ -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.
*/
@ -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
}

20
components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/ResourceState.web.kt

@ -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
gradle-plugins/gradle.properties

@ -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__

Loading…
Cancel
Save