diff --git a/components/gradle.properties b/components/gradle.properties index 79c1a9328e1..bacdef52520 100644 --- a/components/gradle.properties +++ b/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 diff --git a/components/resources/demo/shared/src/commonMain/composeResources/font/RobotoFlex-VariableFont.ttf b/components/resources/demo/shared/src/commonMain/composeResources/font/RobotoFlex-VariableFont.ttf new file mode 100644 index 00000000000..6f8db7ea090 Binary files /dev/null and b/components/resources/demo/shared/src/commonMain/composeResources/font/RobotoFlex-VariableFont.ttf differ diff --git a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt b/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt index 99b2c666bfe..ded70352db4 100644 --- a/components/resources/demo/shared/src/commonMain/kotlin/org/jetbrains/compose/resources/demo/shared/FontRes.kt +++ b/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 + ) + } } } \ No newline at end of file diff --git a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt b/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt index 64f87e54403..c9d285ee944 100644 --- a/components/resources/library/src/androidMain/kotlin/org/jetbrains/compose/resources/FontResources.android.kt +++ b/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) } \ No newline at end of file diff --git a/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/ResourceState.blocking.kt b/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/ResourceState.blocking.kt index 6621a723610..4b0aa69932e 100644 --- a/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/ResourceState.blocking.kt +++ b/components/resources/library/src/blockingMain/kotlin/org/jetbrains/compose/resources/ResourceState.blocking.kt @@ -46,4 +46,21 @@ internal actual fun rememberResourceState( runBlocking { block(environment) } ) } +} + +@Composable +internal actual fun rememberResourceState( + key1: Any, + key2: Any, + key3: Any, + key4: Any, + getDefault: () -> T, + block: suspend (ResourceEnvironment) -> T +): State { + val environment = LocalComposeEnvironment.current.rememberEnvironment() + return remember(key1, key2, key3, key4, environment) { + mutableStateOf( + runBlocking { block(environment) } + ) + } } \ No newline at end of file diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt index b39b2db4e35..ed08041bdc2 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/FontResources.kt +++ b/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): Resource(id, items) +@InternalResourceApi constructor(id: String, items: Set) : 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. * diff --git a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceState.kt b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceState.kt index e8e38d5e63c..56d411d9309 100644 --- a/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceState.kt +++ b/components/resources/library/src/commonMain/kotlin/org/jetbrains/compose/resources/ResourceState.kt @@ -40,4 +40,20 @@ internal expect fun rememberResourceState( key3: Any, getDefault: () -> T, block: suspend (ResourceEnvironment) -> T +): State + + +/** + * 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 rememberResourceState( + key1: Any, + key2: Any, + key3: Any, + key4: Any, + getDefault: () -> T, + block: suspend (ResourceEnvironment) -> T ): State \ No newline at end of file diff --git a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt b/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt index 6d697ae2530..c3a38a5537b 100644 --- a/components/resources/library/src/skikoMain/kotlin/org/jetbrains/compose/resources/FontResources.skiko.kt +++ b/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() 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(",") } \ No newline at end of file diff --git a/components/resources/library/src/skikoTest/kotlin/org/jetbrains/compose/resources/VariationFontCacheTest.kt b/components/resources/library/src/skikoTest/kotlin/org/jetbrains/compose/resources/VariationFontCacheTest.kt new file mode 100644 index 00000000000..1b344e6ac2b --- /dev/null +++ b/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( + "'axis' must be unique" + ) { + FontVariation.Settings(setting1, setting2) + } + } +} \ No newline at end of file diff --git a/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/Resource.web.kt b/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/Resource.web.kt index ee9416da3d3..e3b6f3aeb90 100644 --- a/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/Resource.web.kt +++ b/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 { - val resState = remember(resource, weight, style) { mutableStateOf(null) }.apply { - value = Font(resource, weight, style).takeIf { !it.isEmptyPlaceholder } + val resState = remember(resource, weight, style, variationSettings) { mutableStateOf(null) }.apply { + value = Font(resource, weight, style, variationSettings).takeIf { !it.isEmptyPlaceholder } } return resState } diff --git a/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/ResourceState.web.kt b/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/ResourceState.web.kt index 9386783106a..e9bdb2b0d7c 100644 --- a/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/ResourceState.web.kt +++ b/components/resources/library/src/webMain/kotlin/org/jetbrains/compose/resources/ResourceState.web.kt @@ -60,4 +60,24 @@ internal actual fun rememberResourceState( } mutableState } +} + +@Composable +internal actual fun rememberResourceState( + key1: Any, + key2: Any, + key3: Any, + key4: Any, + getDefault: () -> T, + block: suspend (ResourceEnvironment) -> T +): State { + 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 + } } \ No newline at end of file diff --git a/gradle-plugins/gradle.properties b/gradle-plugins/gradle.properties index e70f99e8100..3f4c6f1e45a 100644 --- a/gradle-plugins/gradle.properties +++ b/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__