From 3e352e32970b0819fcecaa77c876bb297e5185ad Mon Sep 17 00:00:00 2001 From: Sebastiano Poggi Date: Wed, 13 Aug 2025 20:48:15 +0200 Subject: [PATCH] [JEWEL-947] Add Image composable This adds a new `Image` composable, similar to `Icon` but less prescriptive, while still allowing users to take advantage of Jewel's icon loading pipeline. closes https://github.com/JetBrains/intellij-community/pull/3180 GitOrigin-RevId: 24682400286f468ac8cb4bd31563c6c5b79607f2 --- .../samples/showcase/components/Icons.kt | 24 ++++ platform/jewel/ui/api-dump.txt | 3 + .../org/jetbrains/jewel/ui/component/Icon.kt | 52 +++++++++ .../org/jetbrains/jewel/ui/component/Image.kt | 104 ++++++++++++++++++ .../hints/ColorBasedPaletteReplacement.kt | 2 +- 5 files changed, 184 insertions(+), 1 deletion(-) create mode 100644 platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Image.kt diff --git a/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Icons.kt b/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Icons.kt index 6916cc58ebe5..013394ab921a 100644 --- a/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Icons.kt +++ b/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/components/Icons.kt @@ -7,7 +7,9 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -21,6 +23,7 @@ import androidx.compose.ui.unit.dp import org.jetbrains.jewel.foundation.theme.JewelTheme import org.jetbrains.jewel.samples.showcase.ShowcaseIcons import org.jetbrains.jewel.ui.component.Icon +import org.jetbrains.jewel.ui.component.Image import org.jetbrains.jewel.ui.component.Text import org.jetbrains.jewel.ui.icons.AllIconsKeys import org.jetbrains.jewel.ui.painter.badge.DotBadgeShape @@ -96,5 +99,26 @@ public fun Icons(modifier: Modifier = Modifier) { } } } + + Column { + Text("Images:") + + Spacer(Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + ) { + Image(ShowcaseIcons.jewelLogo, contentDescription = "Jewel Logo", modifier = Modifier.size(96.dp)) + + // Note: this currently looks identical to the hint-less image due to JEWEL-971 + Image( + iconKey = ShowcaseIcons.jewelLogo, + contentDescription = "Jewel Logo with hint", + hints = arrayOf(Stroke(Color.Red)), + modifier = Modifier.size(96.dp), + ) + } + } } } diff --git a/platform/jewel/ui/api-dump.txt b/platform/jewel/ui/api-dump.txt index 3f6e612aa139..fdb4f12236fa 100644 --- a/platform/jewel/ui/api-dump.txt +++ b/platform/jewel/ui/api-dump.txt @@ -396,6 +396,9 @@ f:org.jetbrains.jewel.ui.component.IconKt - sf:Icon-ww6aTOc(androidx.compose.ui.graphics.painter.Painter,java.lang.String,androidx.compose.ui.Modifier,J,androidx.compose.runtime.Composer,I,I):V - sf:Icon-ww6aTOc(androidx.compose.ui.graphics.vector.ImageVector,java.lang.String,androidx.compose.ui.Modifier,J,androidx.compose.runtime.Composer,I,I):V - sf:painterResource(java.lang.String,androidx.compose.runtime.Composer,I):androidx.compose.ui.graphics.painter.Painter +f:org.jetbrains.jewel.ui.component.ImageKt +- sf:Image(org.jetbrains.jewel.ui.icon.IconKey,java.lang.String,androidx.compose.ui.Modifier,java.lang.Class,androidx.compose.ui.Alignment,androidx.compose.ui.layout.ContentScale,F,androidx.compose.ui.graphics.ColorFilter,androidx.compose.runtime.Composer,I,I):V +- sf:Image(org.jetbrains.jewel.ui.icon.IconKey,java.lang.String,org.jetbrains.jewel.ui.painter.PainterHint[],androidx.compose.ui.Modifier,java.lang.Class,androidx.compose.ui.Alignment,androidx.compose.ui.layout.ContentScale,F,androidx.compose.ui.graphics.ColorFilter,androidx.compose.runtime.Composer,I,I):V f:org.jetbrains.jewel.ui.component.InfoTextKt - sf:InfoText-F-Jr8PA(androidx.compose.ui.text.AnnotatedString,androidx.compose.ui.Modifier,J,J,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,J,androidx.compose.ui.text.style.TextDecoration,I,J,I,Z,I,java.util.Map,kotlin.jvm.functions.Function1,androidx.compose.ui.text.TextStyle,androidx.compose.runtime.Composer,I,I,I):V - sf:InfoText-bAzTDeA(java.lang.String,androidx.compose.ui.Modifier,J,J,androidx.compose.ui.text.font.FontStyle,androidx.compose.ui.text.font.FontWeight,androidx.compose.ui.text.font.FontFamily,J,androidx.compose.ui.text.style.TextDecoration,I,J,I,Z,I,kotlin.jvm.functions.Function1,androidx.compose.ui.text.TextStyle,androidx.compose.runtime.Composer,I,I,I):V diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Icon.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Icon.kt index 133fa37ab41f..0355422a62f1 100644 --- a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Icon.kt +++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Icon.kt @@ -38,6 +38,18 @@ import org.jetbrains.jewel.ui.icon.newUiChecker import org.jetbrains.jewel.ui.painter.PainterHint import org.jetbrains.jewel.ui.painter.rememberResourcePainterProvider +/** + * Icon component that draws an icon from an [IconKey]. + * + * @param key The [IconKey] to resolve the icon from. + * @param contentDescription text used by accessibility services to describe what this icon represents. This should + * always be provided unless this icon is used for decorative purposes, and does not represent a meaningful action + * that a user can take. + * @param modifier optional [Modifier] for this Icon. + * @param iconClass The class to use for resolving the icon resource. Defaults to `key.iconClass`. + * @param tint tint to be applied to the icon. If [Color.Unspecified] is provided, then no tint is applied. + * @param hints [PainterHint]s to be used by the painter. + */ @Suppress("ComposableParamOrder") // It doesn't like the vararg @Composable public fun Icon( @@ -56,6 +68,18 @@ public fun Icon( Icon(painter = painter, contentDescription = contentDescription, modifier = modifier, tint = tint) } +/** + * Icon component that draws an icon from an [IconKey] using a [tint]. + * + * @param key The [IconKey] to resolve the icon from. + * @param contentDescription text used by accessibility services to describe what this icon represents. This should + * always be provided unless this icon is used for decorative purposes, and does not represent a meaningful action + * that a user can take. + * @param modifier optional [Modifier] for this Icon. + * @param iconClass The class to use for resolving the icon resource. Defaults to `key.iconClass`. + * @param tint tint to be applied to the icon. If [Color.Unspecified] is provided, then no tint is applied. + * @param hint [PainterHint] to be passed to the painter. + */ @Suppress("ComposableParamOrder") // To fix in JEWEL-929 @Composable public fun Icon( @@ -75,6 +99,18 @@ public fun Icon( Icon(painter = painter, contentDescription = contentDescription, modifier = modifier, tint = tint) } +/** + * Icon component that draws an icon from an [IconKey] using a [colorFilter]. + * + * @param key The [IconKey] to resolve the icon from. + * @param contentDescription text used by accessibility services to describe what this icon represents. This should + * always be provided unless this icon is used for decorative purposes, and does not represent a meaningful action + * that a user can take. + * @param modifier optional [Modifier] for this Icon. + * @param iconClass The class to use for resolving the icon resource. Defaults to `key.iconClass`. + * @param colorFilter [ColorFilter] to be applied to the icon. + * @param hint [PainterHint] to be passed to the painter. + */ @Suppress("ComposableParamOrder") // To fix in JEWEL-929 @Composable public fun Icon( @@ -93,6 +129,18 @@ public fun Icon( Icon(painter = painter, contentDescription = contentDescription, modifier = modifier, colorFilter = colorFilter) } +/** + * Icon component that draws an icon from an [IconKey] using a [colorFilter]. + * + * @param key The [IconKey] to resolve the icon from. + * @param contentDescription text used by accessibility services to describe what this icon represents. This should + * always be provided unless this icon is used for decorative purposes, and does not represent a meaningful action + * that a user can take. + * @param modifier optional [Modifier] for this Icon. + * @param iconClass The class to use for resolving the icon resource. Defaults to `key.iconClass`. + * @param colorFilter [ColorFilter] to be applied to the icon. + * @param hints [PainterHint]s to be used by the painter. + */ @Suppress("ComposableParamOrder") // To fix in JEWEL-929 @Composable public fun Icon( @@ -214,6 +262,10 @@ public fun Icon( ) } +@Deprecated( + "Please use iconKeys with the Icon or Image components instead. " + + "This will prevent issues when running in the IntelliJ Platform." +) @Composable public fun painterResource(resourcePath: String): Painter = when (resourcePath.substringAfterLast(".").lowercase()) { diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Image.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Image.kt new file mode 100644 index 000000000000..d55ad47d3103 --- /dev/null +++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Image.kt @@ -0,0 +1,104 @@ +// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.jewel.ui.component + +import androidx.compose.foundation.Image +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ColorFilter +import androidx.compose.ui.graphics.DefaultAlpha +import androidx.compose.ui.layout.ContentScale +import org.jetbrains.jewel.foundation.theme.JewelTheme +import org.jetbrains.jewel.ui.icon.IconKey +import org.jetbrains.jewel.ui.icon.newUiChecker +import org.jetbrains.jewel.ui.painter.PainterHint +import org.jetbrains.jewel.ui.painter.rememberResourcePainterProvider + +/** + * A Composable that lays out and draws a given [IconKey]. + * + * This component is similar to [Icon], but it does not have a default size, and it exposes more control over the + * image's appearance, like [contentScale], [alignment], and [alpha]. This will attempt to size the composable according + * to the [IconKey]'s intrinsic size. However, an optional [Modifier] can be provided to adjust the sizing or draw + * additional content (e.g., a background). + * + * @param iconKey The [IconKey] to draw. + * @param contentDescription Text used by accessibility services to describe what this image represents. This should + * always be provided unless this image is used for decorative purposes, and does not represent a meaningful action + * that a user can take. This text should be localized, such as by using `stringResource` or similar. + * @param modifier [Modifier] for this image. + * @param iconClass The class to use for resolving the icon resource. Defaults to `iconKey.iconClass`. + * @param alignment Alignment parameter used to place the [iconKey] in the given bounds. + * @param contentScale Scale parameter used to determine the aspect ratio scaling to be used if the bounds are a + * different size from the intrinsic size of the [iconKey]. + * @param alpha Opacity to be applied to the [iconKey] when it is rendered on screen. + * @param colorFilter [ColorFilter] to apply for the [iconKey] when it is rendered onscreen. + * @see Icon + */ +@Composable +public fun Image( + iconKey: IconKey, + contentDescription: String?, + modifier: Modifier = Modifier, + iconClass: Class<*> = iconKey.iconClass, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, +) { + Image( + iconKey, + contentDescription, + hints = emptyArray(), + modifier, + iconClass, + alignment, + contentScale, + alpha, + colorFilter, + ) +} + +/** + * A Composable that lays out and draws a given [IconKey]. + * + * This component is similar to [Icon], but it does not have a default size, and it exposes more control over the + * image's appearance, like [contentScale], [alignment], and [alpha]. This will attempt to size the composable according + * to the [IconKey]'s intrinsic size. However, an optional [Modifier] can be provided to adjust the sizing or draw + * additional content (e.g., a background). + * + * @param iconKey The [IconKey] to draw. + * @param contentDescription Text used by accessibility services to describe what this image represents. This should + * always be provided unless this image is used for decorative purposes, and does not represent a meaningful action + * that a user can take. This text should be localized, such as by using `stringResource` or similar. + * @param hints [PainterHint]s to be used by the painter. + * @param modifier [Modifier] for this image. + * @param iconClass The class to use for resolving the icon resource. Defaults to `iconKey.iconClass`. + * @param alignment Alignment parameter used to place the [iconKey] in the given bounds. + * @param contentScale Scale parameter used to determine the aspect ratio scaling to be used if the bounds are a + * different size from the intrinsic size of the [iconKey]. + * @param alpha Opacity to be applied to the [iconKey] when it is rendered on screen. + * @param colorFilter [ColorFilter] to apply for the [iconKey] when it is rendered onscreen. + * @see Icon + */ +@Composable +public fun Image( + iconKey: IconKey, + contentDescription: String?, + vararg hints: PainterHint, + modifier: Modifier = Modifier, + iconClass: Class<*> = iconKey.iconClass, + alignment: Alignment = Alignment.Center, + contentScale: ContentScale = ContentScale.Fit, + alpha: Float = DefaultAlpha, + colorFilter: ColorFilter? = null, +) { + val isNewUi = JewelTheme.newUiChecker.isNewUi() + val path = remember(iconKey, isNewUi) { iconKey.path(isNewUi) } + val painterProvider = rememberResourcePainterProvider(path, iconClass) + val painter by painterProvider.getPainter(*hints) + + Image(painter, contentDescription, modifier, alignment, contentScale, alpha, colorFilter) +} diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/ColorBasedPaletteReplacement.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/ColorBasedPaletteReplacement.kt index 30fc2a203bdc..5040925dee3d 100644 --- a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/ColorBasedPaletteReplacement.kt +++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/ColorBasedPaletteReplacement.kt @@ -53,7 +53,7 @@ private fun Element.patchColorAttribute(attrName: String, pattern: Map