[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
This commit is contained in:
Sebastiano Poggi
2025-08-15 08:37:05 +00:00
committed by intellij-monorepo-bot
parent b638b1a99a
commit 3e352e3297
5 changed files with 184 additions and 1 deletions
@@ -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),
)
}
}
}
}
+3
View File
@@ -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
@@ -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()) {
@@ -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)
}
@@ -53,7 +53,7 @@ private fun Element.patchColorAttribute(attrName: String, pattern: Map<Color, Co
val alpha = opacity.toFloatOrNull() ?: 1.0f
val originalColor = tryParseColor(color, alpha) ?: return
val newColor = pattern[originalColor] ?: return
setAttribute(attrName, newColor.copy(alpha = 1.0f).toRgbaHexString())
setAttribute(attrName, newColor.copy(alpha = 1.0f).toRgbaHexString(omitAlphaWhenFullyOpaque = true))
if (newColor.alpha != alpha) {
setAttribute("$attrName-opacity", newColor.alpha.toString())
}