, generator: () -> TBitmap): TBitmap
+}
diff --git a/platform/icons-api/src/org/jetbrains/icons/DeferredIcon.kt b/platform/icons-api/src/org/jetbrains/icons/DeferredIcon.kt
new file mode 100644
index 0000000000000..7f4cd00af8735
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/DeferredIcon.kt
@@ -0,0 +1,18 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons
+
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * Deferred Icon takes time to resolve; therefore, it is postponed to be resolved later.
+ * Placeholder icon can be set to provide a visual representation while the actual icon is being resolved.
+ * Unlike the older API, to force evaluation or get the resolved icon, IconManager should be used.
+ *
+ * @see IconManager.deferredIcon
+ * @see IconManager.forceEvaluationnnnn
+ */
+@ApiStatus.Experimental
+interface DeferredIcon: Icon {
+ val id: IconIdentifier
+ val placeholder: Icon?
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/Icon.kt b/platform/icons-api/src/org/jetbrains/icons/Icon.kt
new file mode 100644
index 0000000000000..5d15bd7428d6f
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/Icon.kt
@@ -0,0 +1,19 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Polymorphic
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * This is universal Icon interface that can be used across different environments with different graphics api.
+ * (or without one)
+ *
+ * For serialization, use getSerializersModule() on IconManager
+ */
+@ApiStatus.Experimental
+interface Icon
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/IconIdentifier.kt b/platform/icons-api/src/org/jetbrains/icons/IconIdentifier.kt
new file mode 100644
index 0000000000000..8e992085b8e47
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/IconIdentifier.kt
@@ -0,0 +1,4 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons
+
+interface IconIdentifier
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/IconManager.kt b/platform/icons-api/src/org/jetbrains/icons/IconManager.kt
new file mode 100644
index 0000000000000..95fb7d7764f52
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/IconManager.kt
@@ -0,0 +1,88 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons
+
+import kotlinx.serialization.modules.SerializersModule
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.design.IconDesigner
+import org.jetbrains.icons.modifiers.IconModifier
+import java.util.ServiceLoader
+
+@ApiStatus.Experimental
+interface IconManager {
+ /**
+ * Creates new Icon "description", this is a cheap operation, to render the Icon, use Icon.createRenderer() function.
+ * Use convenience top-level method icon {} instead if possible.
+ */
+ fun icon(designer: IconDesigner.() -> Unit): Icon
+
+ /**
+ * @see org.jetbrains.icons.deferredIcon
+ */
+ fun deferredIcon(placeholder: Icon?, identifier: String? = null, classLoader: ClassLoader? = null, evaluator: suspend () -> Icon): Icon
+
+ suspend fun forceEvaluation(icon: DeferredIcon): Icon
+
+ /**
+ * Converts specific Icon to swing Icon.
+ * ! This is an expensive operation and can include image loading, reuse the instance if possible. !
+ */
+ fun toSwingIcon(icon: Icon): javax.swing.Icon
+
+ fun getSerializersModule(): SerializersModule
+
+ companion object {
+ @Volatile
+ private var instance: IconManager? = null
+
+ @JvmStatic
+ fun getInstance(): IconManager = instance ?: loadFromSPI()
+
+ private fun loadFromSPI(): IconManager =
+ ServiceLoader.load(IconManager::class.java).firstOrNull()
+ ?: error("IconManager instance is not set and there is no SPI service on classpath.")
+
+
+ fun activate(manager: IconManager) {
+ instance = manager
+ }
+ }
+}
+
+/**
+ * Creates new Icon
+ * The result should be serializable and contains description of what the Icon should look like.
+ *
+ * To render the Icon, renderer has to be obtained first using createRenderer(), however this should be only done
+ * from inside components. (check intellij.platform.icons.api.rendering module), for usage inside swing,
+ * use toSwingIcon method.
+ *
+ * Usage:
+ * '''
+ * icon {
+ * image("icons/icon.svg", MyClass::class.java.classLoader)
+ * }
+ * '''
+ *
+ * Check the designer interface for layer options. Also check intellij.platform.icons.api.legacyIconSupport module
+ * to find out how to convert old icons and new icons.
+ *
+ * @see IconManager.toSwingIcon
+ */
+fun icon(designer: IconDesigner.() -> Unit): Icon = IconManager.getInstance().icon(designer)
+
+/**
+ * Deferred icon allows apis to return an Icon that takes some time to compute;
+ * optional placeholder can be included to allow rendering it before the actual icon is ready.
+ *
+ * To cache such icons and synchronize them over the network, some identifier should be given.
+ * Implementations might try to prefix the identifier with the source pluginId/moduleId to avoid clashes.
+ *
+ * If the identifier is not passed, an automatic one is created, however, this will prevent the result
+ * from being cached, as a new one is generated per each deferredIcon() call.
+ *
+ * @param classLoader This classLoader might be used to prevent id clashes (prefix with pluginId & moduleId if possible for example)
+ */
+fun deferredIcon(placeholder: Icon?, identifier: String? = null, classLoader: ClassLoader? = null, evaluator: suspend () -> Icon): Icon =
+ IconManager.getInstance().deferredIcon(placeholder, identifier, classLoader, evaluator)
+
+fun imageIcon(path: String, classLoader: ClassLoader? = null, modifier: IconModifier = IconModifier): Icon = icon { image(path, classLoader, modifier) }
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/ImageResourceLocation.kt b/platform/icons-api/src/org/jetbrains/icons/ImageResourceLocation.kt
new file mode 100644
index 0000000000000..4ac3d7ed22a29
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/ImageResourceLocation.kt
@@ -0,0 +1,18 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons
+
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * Represents a place from which image resource can be loaded.
+ *
+ * For example:
+ * - path
+ * - pluginId
+ * - moduleId
+ *
+ * When creating new locations, ensure to register new ImageResourceLoader (rendering api) for the specific location.
+ * Check current implementation on how to register extensions.
+ */
+@ApiStatus.Experimental
+interface ImageResourceLocation
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/design/BlendMode.kt b/platform/icons-api/src/org/jetbrains/icons/design/BlendMode.kt
new file mode 100644
index 0000000000000..e545e500a0c2b
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/design/BlendMode.kt
@@ -0,0 +1,16 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.design
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Experimental
+@Serializable
+enum class BlendMode {
+ SrcIn,
+ Hue,
+ Saturation,
+ Luminosity,
+ Color,
+ Multiply
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/design/Color.kt b/platform/icons-api/src/org/jetbrains/icons/design/Color.kt
new file mode 100644
index 0000000000000..c3f03ff2d1e58
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/design/Color.kt
@@ -0,0 +1,81 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.design
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+import kotlin.math.roundToInt
+
+@ApiStatus.Experimental
+@Serializable
+sealed interface Color {
+ fun toHex(): String
+
+ companion object {
+ val Transparent: Color = RGBA(0f, 0f, 0f, 0f)
+ }
+}
+
+@ApiStatus.Experimental
+@Serializable
+class RGBA(
+ val red: Float,
+ val green: Float,
+ val blue: Float,
+ val alpha: Float
+): Color {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as RGBA
+
+ if (red != other.red) return false
+ if (green != other.green) return false
+ if (blue != other.blue) return false
+ if (alpha != other.alpha) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = red.hashCode()
+ result = 31 * result + green.hashCode()
+ result = 31 * result + blue.hashCode()
+ result = 31 * result + alpha.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "RGBA(red=$red, green=$green, blue=$blue, alpha=$alpha)"
+ }
+
+ override fun toHex(): String {
+ val r = Integer.toHexString((red * 255).roundToInt())
+ val g = Integer.toHexString((green * 255).roundToInt())
+ val b = Integer.toHexString((blue * 255).roundToInt())
+ val intAlpha = (alpha * 255).roundToInt()
+
+ return formatColorRgbaHexString(r, g, b, intAlpha, true, true)
+ }
+
+ private fun formatColorRgbaHexString(
+ rString: String,
+ gString: String,
+ bString: String,
+ alphaInt: Int,
+ includeHashSymbol: Boolean,
+ omitAlphaWhenFullyOpaque: Boolean,
+ ): String = buildString {
+ if (includeHashSymbol) append('#')
+
+ append(rString.padStart(2, '0'))
+ append(gString.padStart(2, '0'))
+ append(bString.padStart(2, '0'))
+
+ if (alphaInt < 255 || !omitAlphaWhenFullyOpaque) {
+ val a = Integer.toHexString(alphaInt)
+ append(a.padStart(2, '0'))
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/design/IconAlign.kt b/platform/icons-api/src/org/jetbrains/icons/design/IconAlign.kt
new file mode 100644
index 0000000000000..2031d2aaeed0e
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/design/IconAlign.kt
@@ -0,0 +1,61 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.design
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+
+@Suppress("unused")
+@Serializable
+@ApiStatus.Experimental
+class IconAlign(
+ val verticalAlign: IconVerticalAlign,
+ val horizontalAlign: IconHorizontalAlign
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as IconAlign
+
+ if (verticalAlign != other.verticalAlign) return false
+ if (horizontalAlign != other.horizontalAlign) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = verticalAlign.hashCode()
+ result = 31 * result + horizontalAlign.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "IconAlign(verticalAlign=$verticalAlign, horizontalAlign=$horizontalAlign)"
+ }
+
+ companion object {
+ val TopLeft: IconAlign = IconAlign(IconVerticalAlign.Top, IconHorizontalAlign.Left)
+ val TopCenter: IconAlign = IconAlign(IconVerticalAlign.Top, IconHorizontalAlign.Center)
+ val TopRight: IconAlign = IconAlign(IconVerticalAlign.Top, IconHorizontalAlign.Right)
+ val CenterLeft: IconAlign = IconAlign(IconVerticalAlign.Center, IconHorizontalAlign.Left)
+ val Center: IconAlign = IconAlign(IconVerticalAlign.Center, IconHorizontalAlign.Center)
+ val CenterRight: IconAlign = IconAlign(IconVerticalAlign.Center, IconHorizontalAlign.Right)
+ val BottomLeft: IconAlign = IconAlign(IconVerticalAlign.Bottom, IconHorizontalAlign.Left)
+ val BottomCenter: IconAlign = IconAlign(IconVerticalAlign.Bottom, IconHorizontalAlign.Center)
+ val BottomRight: IconAlign = IconAlign(IconVerticalAlign.Bottom, IconHorizontalAlign.Right)
+ }
+}
+
+@Serializable
+enum class IconVerticalAlign {
+ Top,
+ Center,
+ Bottom
+}
+
+@Serializable
+enum class IconHorizontalAlign {
+ Left,
+ Center,
+ Right
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/design/IconDesigner.kt b/platform/icons-api/src/org/jetbrains/icons/design/IconDesigner.kt
new file mode 100644
index 0000000000000..63b959cef14d2
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/design/IconDesigner.kt
@@ -0,0 +1,49 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.design
+
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.ImageResourceLocation
+import org.jetbrains.icons.layers.IconLayer
+import org.jetbrains.icons.modifiers.IconModifier
+import org.jetbrains.icons.modifiers.align
+import org.jetbrains.icons.modifiers.cutoutMargin
+import org.jetbrains.icons.modifiers.size
+
+/**
+ * Individual methods act as layers and the order dictates in which order they are rendered.
+ *
+ * @param IconModifier Modifications that should be performed on the Layer, like sizing, margin, color filters etc. (order-dependant)
+ */
+@ApiStatus.Experimental
+interface IconDesigner {
+ fun image(resourceLoader: ImageResourceLocation, modifier: IconModifier = IconModifier)
+ fun image(path: String, classLoader: ClassLoader? = null, modifier: IconModifier = IconModifier)
+ fun icon(icon: Icon, modifier: IconModifier = IconModifier)
+ fun box(modifier: IconModifier = IconModifier, builder: IconDesigner.() -> Unit)
+ fun row(spacing: IconUnit = 0.px, modifier: IconModifier = IconModifier, builder: IconDesigner.() -> Unit)
+ fun column(spacing: IconUnit = 0.px, modifier: IconModifier = IconModifier, builder: IconDesigner.() -> Unit)
+ fun animation(modifier: IconModifier = IconModifier, builder: IconAnimationDesigner.() -> Unit)
+ fun shape(color: Color, shape: Shape, modifier: IconModifier)
+ /**
+ * Adds custom layer type to this designer, keep in mind that additional registration of serializers/renderers is needed
+ * for the layer to be used. Check the specific Icon Manager used to see registration details.
+ */
+ fun custom(iconLayer: IconLayer)
+}
+
+fun IconDesigner.badge(
+ color: Color,
+ shape: Shape,
+ size: IconUnit = (3.5 * 2).dp relativeTo 20.dp,
+ align: IconAlign = IconAlign.TopRight,
+ cutout: IconUnit = 1.5.dp relativeTo 20.dp,
+ modifier: IconModifier = IconModifier,
+) {
+ shape(color, shape, modifier.size(size).align(align).cutoutMargin(cutout))
+}
+
+@ApiStatus.Experimental
+interface IconAnimationDesigner {
+ fun frame(duration: Long, builder: IconDesigner.() -> Unit)
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/design/IconMargin.kt b/platform/icons-api/src/org/jetbrains/icons/design/IconMargin.kt
new file mode 100644
index 0000000000000..decbdb2206374
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/design/IconMargin.kt
@@ -0,0 +1,44 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.design
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+
+@Serializable
+@ApiStatus.Experimental
+class IconMargin(
+ val top: IconUnit,
+ val left: IconUnit,
+ val bottom: IconUnit,
+ val right: IconUnit,
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as IconMargin
+
+ if (top != other.top) return false
+ if (left != other.left) return false
+ if (bottom != other.bottom) return false
+ if (right != other.right) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = top.hashCode()
+ result = 31 * result + left.hashCode()
+ result = 31 * result + bottom.hashCode()
+ result = 31 * result + right.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "IconMargin(top=$top, left=$left, bottom=$bottom, right=$right)"
+ }
+
+ companion object {
+ val Zero: IconMargin = IconMargin(0.dp, 0.dp, 0.dp, 0.dp)
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/design/IconUnits.kt b/platform/icons-api/src/org/jetbrains/icons/design/IconUnits.kt
new file mode 100644
index 0000000000000..d75289462deaa
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/design/IconUnits.kt
@@ -0,0 +1,153 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.design
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * Samples:
+ *
+ * 20.px
+ * * 100.percent
+ * * 0.5.fraction
+ * * 5.dp
+ * * AutoIconUnit - fills max width
+ *
+ */
+@Serializable
+@ApiStatus.Experimental
+sealed interface IconUnit {
+ companion object {
+ val Zero: IconUnit = 0.dp
+ val Auto: IconUnit = MaxIconUnit
+ }
+}
+
+@Serializable
+@ApiStatus.Experimental
+class DisplayPointIconUnit(val value: Double) : IconUnit {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as DisplayPointIconUnit
+
+ return value == other.value
+ }
+
+ override fun hashCode(): Int {
+ return value.hashCode()
+ }
+
+ override fun toString(): String {
+ return "DisplayPointIconUnit(value=$value)"
+ }
+
+}
+
+@Serializable
+@ApiStatus.Experimental
+class PixelIconUnit(val value: Int) : IconUnit {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as PixelIconUnit
+
+ return value == other.value
+ }
+
+ override fun hashCode(): Int {
+ return value
+ }
+
+ override fun toString(): String {
+ return "PixelIconUnit(value=$value)"
+ }
+}
+
+@Serializable
+@ApiStatus.Experimental
+class PercentIconUnit(val value: Double) : IconUnit {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as PercentIconUnit
+
+ return value == other.value
+ }
+
+ override fun hashCode(): Int {
+ return value.hashCode()
+ }
+
+ override fun toString(): String {
+ return "PercentIconUnit(value=$value)"
+ }
+}
+
+@Serializable
+@ApiStatus.Experimental
+object MaxIconUnit : IconUnit
+
+val Int.dp: DisplayPointIconUnit
+ get() = DisplayPointIconUnit(this.toDouble())
+
+val Double.dp: DisplayPointIconUnit
+ get() = DisplayPointIconUnit(this)
+
+val Float.dp: DisplayPointIconUnit
+ get() = DisplayPointIconUnit(this.toDouble())
+
+val Int.px: PixelIconUnit
+ get() = PixelIconUnit(this)
+
+/**
+ * The resulting value is divided by 100, so the number before this represents actual percentage (not fraction)
+ * @see fraction
+ */
+val Int.percent: PercentIconUnit
+ get() = this.toDouble().percent
+
+/**
+ * The resulting value is divided by 100, so the number before this represents actual percentage (not fraction)
+ * @see fraction
+ */
+val Double.percent: PercentIconUnit
+ get() = PercentIconUnit(this / 100.0)
+
+/**
+ * The resulting value is divided by 100, so the number before this represents actual percentage (not fraction)
+ * @see fraction
+ */
+val Float.percent: PercentIconUnit
+ get() = this.toDouble().percent
+/**
+ * The resulting value is not divided by 100, acts as a percentage of bounds
+ * @see percent
+ */
+val Int.fraction: PercentIconUnit
+ get() = this.toDouble().fraction
+
+/**
+ * The resulting value is not divided by 100, acts as a percentage of bounds
+ * @see percent
+ */
+val Double.fraction: PercentIconUnit
+ get() = PercentIconUnit(this)
+
+/**
+ * The resulting value is not divided by 100, acts as a percentage of bounds
+ * @see percent
+ */
+val Float.fraction: PercentIconUnit
+ get() = this.toDouble().fraction
+
+infix fun PixelIconUnit.relativeTo(bound: PixelIconUnit): PercentIconUnit {
+ return (this.value / bound.value).fraction
+}
+
+infix fun DisplayPointIconUnit.relativeTo(bound: DisplayPointIconUnit): PercentIconUnit {
+ return (this.value / bound.value).fraction
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/design/Shape.kt b/platform/icons-api/src/org/jetbrains/icons/design/Shape.kt
new file mode 100644
index 0000000000000..3296909b54636
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/design/Shape.kt
@@ -0,0 +1,27 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.design
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Experimental
+@Serializable
+sealed interface Shape {
+ companion object {
+ }
+}
+
+@ApiStatus.Experimental
+@Serializable
+object Circle: Shape {
+ override fun toString(): String {
+ return "Circle"
+ }
+
+}
+
+@ApiStatus.Experimental
+@Serializable
+object Rectangle: Shape {
+
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/design/SvgPatcherDesigner.kt b/platform/icons-api/src/org/jetbrains/icons/design/SvgPatcherDesigner.kt
new file mode 100644
index 0000000000000..1df720ce7ce69
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/design/SvgPatcherDesigner.kt
@@ -0,0 +1,55 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.design
+
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.patchers.SvgPatchOperation
+import org.jetbrains.icons.patchers.SvgPatcher
+import org.jetbrains.icons.patchers.SvgPathFilteredOperations
+
+@ApiStatus.Experimental
+public class SvgPatcherDesigner {
+ private val operations = mutableListOf()
+ private val filteredOperations = mutableListOf()
+
+ fun replace(name: String, newValue: String) {
+ operations.add(SvgPatchOperation(name, newValue, false, false, null, SvgPatchOperation.Operation.Replace))
+ }
+
+ fun replaceIfMatches(name: String, expectedValue: String, newValue: String) {
+ operations.add(SvgPatchOperation(name, newValue, true, false, expectedValue, SvgPatchOperation.Operation.Replace))
+ }
+
+ fun replaceUnlessMatches(name: String, expectedValue: String, newValue: String) {
+ operations.add(SvgPatchOperation(name, newValue, true, true, expectedValue, SvgPatchOperation.Operation.Replace))
+ }
+
+ fun removeIfMatches(name: String, expectedValue: String) {
+ operations.add(SvgPatchOperation(name, null, true, false, expectedValue, SvgPatchOperation.Operation.Remove))
+ }
+
+ fun removeUnlessMatches(name: String, expectedValue: String) {
+ operations.add(SvgPatchOperation(name, null, true, true, expectedValue, SvgPatchOperation.Operation.Remove))
+ }
+
+ fun remove(name: String) {
+ operations.add(SvgPatchOperation(name, null, false, false, null, SvgPatchOperation.Operation.Remove))
+ }
+
+ fun set(name: String, value: String) {
+ operations.add(SvgPatchOperation(name, value, false, false, null, SvgPatchOperation.Operation.Add))
+ }
+
+ fun add(name: String, value: String) {
+ operations.add(SvgPatchOperation(name, value, false, false, null, SvgPatchOperation.Operation.Add))
+ }
+
+ fun filter(path: String, svgPatcherDesigner: SvgPatcherDesigner.() -> Unit) {
+ val designer = SvgPatcherDesigner()
+ svgPatcherDesigner.invoke(designer)
+ // TODO Support recursive filters
+ filteredOperations.add(SvgPathFilteredOperations(path, designer.build().operations))
+ }
+
+ @ApiStatus.Internal
+ internal fun build(): SvgPatcher = SvgPatcher(operations, filteredOperations)
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/filters/ColorFilter.kt b/platform/icons-api/src/org/jetbrains/icons/filters/ColorFilter.kt
new file mode 100644
index 0000000000000..652cef6bc0fca
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/filters/ColorFilter.kt
@@ -0,0 +1,9 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.filters
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Experimental
+@Serializable
+sealed interface ColorFilter
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/filters/TintColorFilter.kt b/platform/icons-api/src/org/jetbrains/icons/filters/TintColorFilter.kt
new file mode 100644
index 0000000000000..361d2baed6c27
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/filters/TintColorFilter.kt
@@ -0,0 +1,15 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.filters
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.design.BlendMode
+import org.jetbrains.icons.design.Color
+
+@ApiStatus.Experimental
+@Serializable
+class TintColorFilter(
+ val color: Color,
+ val blendMode: BlendMode = BlendMode.SrcIn
+): ColorFilter {
+}
diff --git a/platform/icons-api/src/org/jetbrains/icons/layers/IconLayer.kt b/platform/icons-api/src/org/jetbrains/icons/layers/IconLayer.kt
new file mode 100644
index 0000000000000..851a6af8d36a6
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/layers/IconLayer.kt
@@ -0,0 +1,13 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.layers
+
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.modifiers.IconModifier
+
+/**
+ * To serialize IconLayer, serializersModule from IconManager might be used.
+ */
+@ApiStatus.Experimental
+interface IconLayer {
+ val modifier: IconModifier
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/modifiers/AlignIconModifier.kt b/platform/icons-api/src/org/jetbrains/icons/modifiers/AlignIconModifier.kt
new file mode 100644
index 0000000000000..9bd82963650f0
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/modifiers/AlignIconModifier.kt
@@ -0,0 +1,33 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.modifiers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.design.IconAlign
+
+@Serializable
+@ApiStatus.Experimental
+class AlignIconModifier(
+ val align: IconAlign
+): IconModifier {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as AlignIconModifier
+
+ return align == other.align
+ }
+
+ override fun hashCode(): Int {
+ return align.hashCode()
+ }
+
+ override fun toString(): String {
+ return "AlignIconModifier(align=$align)"
+ }
+}
+
+fun IconModifier.align(align: IconAlign): IconModifier {
+ return this then AlignIconModifier(align)
+}
diff --git a/platform/icons-api/src/org/jetbrains/icons/modifiers/AlphaIconModifier.kt b/platform/icons-api/src/org/jetbrains/icons/modifiers/AlphaIconModifier.kt
new file mode 100644
index 0000000000000..8b33601265a6a
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/modifiers/AlphaIconModifier.kt
@@ -0,0 +1,32 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.modifiers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+
+@Serializable
+@ApiStatus.Experimental
+class AlphaIconModifier(
+ val alpha: Float
+): IconModifier {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as AlphaIconModifier
+
+ return alpha == other.alpha
+ }
+
+ override fun hashCode(): Int {
+ return alpha.hashCode()
+ }
+
+ override fun toString(): String {
+ return "AlphaIconModifier(alpha=$alpha)"
+ }
+}
+
+fun IconModifier.alpha(alpha: Float): IconModifier {
+ return this then AlphaIconModifier(alpha)
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/modifiers/ColorFilterModifier.kt b/platform/icons-api/src/org/jetbrains/icons/modifiers/ColorFilterModifier.kt
new file mode 100644
index 0000000000000..e7f2efa178de0
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/modifiers/ColorFilterModifier.kt
@@ -0,0 +1,41 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.modifiers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.design.BlendMode
+import org.jetbrains.icons.design.Color
+import org.jetbrains.icons.filters.ColorFilter
+import org.jetbrains.icons.filters.TintColorFilter
+
+@Serializable
+@ApiStatus.Experimental
+class ColorFilterModifier(
+ val colorFilter: ColorFilter
+): IconModifier {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ColorFilterModifier
+
+ return colorFilter == other.colorFilter
+ }
+
+ override fun hashCode(): Int {
+ return colorFilter.hashCode()
+ }
+
+ override fun toString(): String {
+ return "ColorFilterModifier(colorFilter=$colorFilter)"
+ }
+
+}
+
+fun IconModifier.colorFilter(colorFilter: ColorFilter): IconModifier {
+ return this then ColorFilterModifier(colorFilter)
+}
+
+fun IconModifier.tintColor(color: Color, blendMode: BlendMode): IconModifier {
+ return colorFilter(TintColorFilter(color, blendMode))
+}
diff --git a/platform/icons-api/src/org/jetbrains/icons/modifiers/CombinedIconModifier.kt b/platform/icons-api/src/org/jetbrains/icons/modifiers/CombinedIconModifier.kt
new file mode 100644
index 0000000000000..674fd63f5b0f2
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/modifiers/CombinedIconModifier.kt
@@ -0,0 +1,35 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.modifiers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+
+@Serializable
+@ApiStatus.Experimental
+class CombinedIconModifier(
+ val root: IconModifier,
+ val other: IconModifier,
+): IconModifier {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as CombinedIconModifier
+
+ if (root != other.root) return false
+ if (other != other.other) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = root.hashCode()
+ result = 31 * result + other.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "CombinedIconModifier(root=$root, other=$other)"
+ }
+
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/modifiers/CutoutMarginModifier.kt b/platform/icons-api/src/org/jetbrains/icons/modifiers/CutoutMarginModifier.kt
new file mode 100644
index 0000000000000..6dd71c315f69a
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/modifiers/CutoutMarginModifier.kt
@@ -0,0 +1,38 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.modifiers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.design.IconUnit
+
+@Serializable
+@ApiStatus.Experimental
+/**
+ * Add cutout margin to the specific layer, which will clear the surrounding area
+ * Currently supported only by shape and image layers, image layer will not consider
+ * internal svg shape and will cut out rectangular area.
+ */
+class CutoutMarginModifier(
+ val size: IconUnit
+): IconModifier {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as CutoutMarginModifier
+
+ return size == other.size
+ }
+
+ override fun hashCode(): Int {
+ return size.hashCode()
+ }
+
+ override fun toString(): String {
+ return "CutoutMarginModifier(size=$size)"
+ }
+}
+
+fun IconModifier.cutoutMargin(size: IconUnit): IconModifier {
+ return this then CutoutMarginModifier(size)
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/modifiers/HeightIconModifier.kt b/platform/icons-api/src/org/jetbrains/icons/modifiers/HeightIconModifier.kt
new file mode 100644
index 0000000000000..9c933a6f7da31
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/modifiers/HeightIconModifier.kt
@@ -0,0 +1,42 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.modifiers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.design.MaxIconUnit
+import org.jetbrains.icons.design.IconUnit
+
+@Serializable
+@ApiStatus.Experimental
+class HeightIconModifier(
+ val height: IconUnit
+): IconModifier {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as HeightIconModifier
+
+ return height == other.height
+ }
+
+ override fun hashCode(): Int {
+ return height.hashCode()
+ }
+
+ override fun toString(): String {
+ return "HeightIconModifier(height=$height)"
+ }
+}
+
+fun IconModifier.height(height: IconUnit): IconModifier {
+ return this then HeightIconModifier(height)
+}
+
+fun IconModifier.size(size: IconUnit): IconModifier {
+ return this.width(size).height(size)
+}
+
+fun IconModifier.fillMaxSize(): IconModifier {
+ return this.size(MaxIconUnit)
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/modifiers/IconModifier.kt b/platform/icons-api/src/org/jetbrains/icons/modifiers/IconModifier.kt
new file mode 100644
index 0000000000000..7432ab82cf282
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/modifiers/IconModifier.kt
@@ -0,0 +1,35 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.modifiers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+
+/**
+ * Modifications that should be performed on the Layer, like sizing, margin, color filters etc. (order-dependant)
+ *
+ * Sample usage:
+ * '''
+ * icon {
+ * image("icons/icon.svg", IconModifier.margin(20.px))
+ * }
+ * '''
+ *
+ * @see AlignIconModifier
+ * @see MarginIconModifier
+ * @see HeightIconModifier
+ * @see WidthIconModifier
+ */
+@Serializable
+@ApiStatus.Experimental
+sealed interface IconModifier {
+ companion object: IconModifier
+}
+
+/**
+ * Concatenates this modifier with another.
+ *
+ * Returns a [IconModifier] representing this modifier followed by [other] in sequence.
+ */
+@ApiStatus.Experimental
+infix fun IconModifier.then(other: IconModifier): IconModifier =
+ if (other === IconModifier) this else CombinedIconModifier(this, other)
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/modifiers/MarginIconModifier.kt b/platform/icons-api/src/org/jetbrains/icons/modifiers/MarginIconModifier.kt
new file mode 100644
index 0000000000000..77bf72121fbe1
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/modifiers/MarginIconModifier.kt
@@ -0,0 +1,56 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.modifiers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.design.IconUnit
+
+@Serializable
+@ApiStatus.Experimental
+class MarginIconModifier(
+ val left: IconUnit,
+ val top: IconUnit,
+ val right: IconUnit,
+ val bottom: IconUnit
+): IconModifier {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as MarginIconModifier
+
+ if (left != other.left) return false
+ if (top != other.top) return false
+ if (right != other.right) return false
+ if (bottom != other.bottom) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = left.hashCode()
+ result = 31 * result + top.hashCode()
+ result = 31 * result + right.hashCode()
+ result = 31 * result + bottom.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "MarginIconModifier(left=$left, top=$top, right=$right, bottom=$bottom)"
+ }
+}
+
+@ApiStatus.Experimental
+fun IconModifier.margin(left: IconUnit, top: IconUnit, right: IconUnit, bottom: IconUnit): IconModifier {
+ return this then MarginIconModifier(left, top, right, bottom)
+}
+
+@ApiStatus.Experimental
+fun IconModifier.margin(all: IconUnit): IconModifier {
+ return margin(all, all, all, all)
+}
+
+@ApiStatus.Experimental
+fun IconModifier.margin(vertical: IconUnit, horizontal: IconUnit): IconModifier {
+ return margin(horizontal, vertical, horizontal, vertical)
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/modifiers/StrokeModifier.kt b/platform/icons-api/src/org/jetbrains/icons/modifiers/StrokeModifier.kt
new file mode 100644
index 0000000000000..5797d7c6dfb9f
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/modifiers/StrokeModifier.kt
@@ -0,0 +1,35 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.modifiers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.design.Color
+
+@Serializable
+@ApiStatus.Experimental
+class StrokeModifier(
+ val color: Color
+): IconModifier {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as StrokeModifier
+
+ return color == other.color
+ }
+
+ override fun hashCode(): Int {
+ return color.hashCode()
+ }
+
+ override fun toString(): String {
+ return "StrokeIconModifier(color=$color)"
+ }
+
+}
+
+@ApiStatus.Experimental
+fun IconModifier.stroke(color: Color): IconModifier {
+ return this then StrokeModifier(color)
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/modifiers/SvgPatcherModifier.kt b/platform/icons-api/src/org/jetbrains/icons/modifiers/SvgPatcherModifier.kt
new file mode 100644
index 0000000000000..b1d95c649406c
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/modifiers/SvgPatcherModifier.kt
@@ -0,0 +1,44 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.modifiers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.design.SvgPatcherDesigner
+import org.jetbrains.icons.patchers.SvgPatcher
+
+@Serializable
+@ApiStatus.Experimental
+class SvgPatcherModifier(
+ val svgPatcher: SvgPatcher
+): IconModifier {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as SvgPatcherModifier
+
+ return svgPatcher == other.svgPatcher
+ }
+
+ override fun hashCode(): Int {
+ return svgPatcher.hashCode()
+ }
+
+ override fun toString(): String {
+ return "SvgPatcherModifier(svgPatcher=$svgPatcher)"
+ }
+
+}
+
+@ApiStatus.Experimental
+fun IconModifier.patchSvg(svgPatcher: SvgPatcher): IconModifier {
+ return this then SvgPatcherModifier(svgPatcher)
+}
+
+@ApiStatus.Experimental
+fun IconModifier.patchSvg(svgPatcherBuilder: SvgPatcherDesigner.() -> Unit): IconModifier {
+ return this.patchSvg(svgPatcher(svgPatcherBuilder))
+}
+
+@ApiStatus.Experimental
+fun svgPatcher(svgPatcherBuilder: SvgPatcherDesigner.() -> Unit): SvgPatcher = SvgPatcherDesigner().apply(svgPatcherBuilder).build()
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/modifiers/WidthIconModifier.kt b/platform/icons-api/src/org/jetbrains/icons/modifiers/WidthIconModifier.kt
new file mode 100644
index 0000000000000..cc158e10843fb
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/modifiers/WidthIconModifier.kt
@@ -0,0 +1,34 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.modifiers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.design.IconUnit
+
+@Serializable
+@ApiStatus.Experimental
+class WidthIconModifier(
+ val width: IconUnit
+): IconModifier {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as WidthIconModifier
+
+ return width == other.width
+ }
+
+ override fun hashCode(): Int {
+ return width.hashCode()
+ }
+
+ override fun toString(): String {
+ return "WidthIconModifier(width=$width)"
+ }
+}
+
+@ApiStatus.Experimental
+fun IconModifier.width(width: IconUnit): IconModifier {
+ return this then WidthIconModifier(width)
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/patchers/SvgPatcher.kt b/platform/icons-api/src/org/jetbrains/icons/patchers/SvgPatcher.kt
new file mode 100644
index 0000000000000..acd3ab56022e5
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/patchers/SvgPatcher.kt
@@ -0,0 +1,123 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.patchers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+
+@ApiStatus.Experimental
+@Serializable
+public class SvgPatcher(
+ val operations: List,
+ val filteredOperations: List
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as SvgPatcher
+
+ if (operations != other.operations) return false
+ if (filteredOperations != other.filteredOperations) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = operations.hashCode()
+ result = 31 * result + filteredOperations.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "SvgPatcher(operations=$operations, filteredOperations=$filteredOperations)"
+ }
+
+}
+
+@ApiStatus.Experimental
+infix fun SvgPatcher?.combineWith(other: SvgPatcher?): SvgPatcher? {
+ if (this == null && other == null) return null
+ if (this == null) return other!!
+ if (other == null) return this
+ return SvgPatcher(operations + other.operations, filteredOperations + other.filteredOperations)
+}
+
+@ApiStatus.Experimental
+@Serializable
+public class SvgPathFilteredOperations(
+ val path: String,
+ val operations: List
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as SvgPathFilteredOperations
+
+ if (path != other.path) return false
+ if (operations != other.operations) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = path.hashCode()
+ result = 31 * result + operations.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "SvgPathFilteredOperations(path='$path', operations=$operations)"
+ }
+
+}
+
+@ApiStatus.Experimental
+@Serializable
+public class SvgPatchOperation(
+ val attributeName: String,
+ val value: String?,
+ val conditional: Boolean,
+ val negatedCondition: Boolean,
+ val expectedValue: String?,
+ val operation: Operation
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as SvgPatchOperation
+
+ if (conditional != other.conditional) return false
+ if (negatedCondition != other.negatedCondition) return false
+ if (attributeName != other.attributeName) return false
+ if (value != other.value) return false
+ if (expectedValue != other.expectedValue) return false
+ if (operation != other.operation) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = conditional.hashCode()
+ result = 31 * result + negatedCondition.hashCode()
+ result = 31 * result + attributeName.hashCode()
+ result = 31 * result + (value.hashCode() ?: 0)
+ result = 31 * result + (expectedValue.hashCode() ?: 0)
+ result = 31 * result + operation.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "SvgPatchOperation(attributeName='$attributeName', value=$value, conditional=$conditional, negatedCondition=$negatedCondition, expectedValue=$expectedValue, operation=$operation)"
+ }
+
+ @Serializable
+ enum class Operation {
+ Add,
+ Replace,
+ Remove,
+ Set
+ }
+
+}
diff --git a/platform/icons-api/src/org/jetbrains/icons/swing/SwingIcon.kt b/platform/icons-api/src/org/jetbrains/icons/swing/SwingIcon.kt
new file mode 100644
index 0000000000000..3bdaaff10ee16
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/swing/SwingIcon.kt
@@ -0,0 +1,36 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.swing
+
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.IconManager
+import org.jetbrains.icons.design.IconDesigner
+import org.jetbrains.icons.icon
+import org.jetbrains.icons.modifiers.IconModifier
+
+/**
+ * Layer for embedding swing icons.
+ */
+@ApiStatus.Experimental
+fun IconDesigner.swingIcon(legacyIcon: javax.swing.Icon, modifier: IconModifier = IconModifier) {
+ custom(SwingIconLayer(legacyIcon, modifier))
+}
+
+/**
+ * Shorthand for creating a new icon from swing icon, uses IconDesigner.swingIcon().
+ */
+@ApiStatus.Experimental
+fun javax.swing.Icon.toNewIcon(modifier: IconModifier = IconModifier): Icon {
+ return icon {
+ swingIcon(this@toNewIcon, modifier)
+ }
+}
+
+/**
+ * Converts specific Icon to swing Icon.
+ * ! This is an expensive operation and can include image loading, reuse the instance if possible. !
+ */
+@ApiStatus.Experimental
+fun Icon.toSwingIcon(): javax.swing.Icon {
+ return IconManager.getInstance().toSwingIcon(this)
+}
\ No newline at end of file
diff --git a/platform/icons-api/src/org/jetbrains/icons/swing/SwingIconLayer.kt b/platform/icons-api/src/org/jetbrains/icons/swing/SwingIconLayer.kt
new file mode 100644
index 0000000000000..a52253294f04f
--- /dev/null
+++ b/platform/icons-api/src/org/jetbrains/icons/swing/SwingIconLayer.kt
@@ -0,0 +1,15 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.swing
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.layers.IconLayer
+import org.jetbrains.icons.modifiers.IconModifier
+import javax.swing.Icon
+
+@ApiStatus.Experimental
+@Serializable
+class SwingIconLayer(
+ val legacyIcon: Icon,
+ override val modifier: IconModifier
+) : IconLayer
\ No newline at end of file
diff --git a/platform/icons-impl/.gitignore b/platform/icons-impl/.gitignore
new file mode 100644
index 0000000000000..af272e31f1de3
--- /dev/null
+++ b/platform/icons-impl/.gitignore
@@ -0,0 +1,111 @@
+### macOS template
+# General
+.DS_Store
+.AppleDouble
+.LSOverride
+
+# Thumbnails
+._*
+
+### Gradle template
+.gradle
+build/
+
+### Terraform template
+# Local .terraform directories
+**/.terraform/*
+
+# .tfstate files
+*.tfstate
+*.tfstate.*
+
+### JetBrains template
+# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm
+# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
+
+*.ipr
+*.iws
+.idea/*
+out/
+local.properties
+
+# IDEA/Android Studio project settings ignore exceptions
+!.idea/codeStyles/
+!.idea/copyright/
+!.idea/dataSources.xml
+!.idea/detekt.xml
+!.idea/encodings.xml
+!.idea/externalDependencies.xml
+!.idea/fileTemplates/
+!.idea/icon.svg
+!.idea/icon.png
+!.idea/icon_dark.png
+!.idea/inspectionProfiles/
+!.idea/ktfmt.xml
+!.idea/ktlint.xml
+!.idea/ktlint-plugin.xml
+!.idea/runConfigurations/
+!.idea/scopes/
+!.idea/vcs.xml
+
+### Kotlin template
+# Compiled class file
+*.class
+
+# Log file
+*.log
+
+# Package Files #
+*.jar
+*.war
+*.nar
+*.ear
+*.zip
+*.tar.gz
+*.rar
+
+# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
+hs_err_pid*
+
+### Windows template
+# Windows thumbnail cache files
+Thumbs.db
+ehthumbs.db
+ehthumbs_vista.db
+
+# Dump file
+*.stackdump
+
+# Folder config file
+[Dd]esktop.ini
+
+# Recycle Bin used on file shares
+$RECYCLE.BIN/
+
+# Windows Installer files
+*.cab
+*.msi
+*.msix
+*.msm
+*.msp
+
+# Windows shortcuts
+*.lnk
+
+### Misc
+
+# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
+!gradle-wrapper.jar
+
+# Ignore IJP temp folder
+/.intellijPlatform
+
+# Ignore release patch generator output
+/this-release.txt
+
+# Ignore Kotlin compiler sessions
+/.kotlin
+/buildSrc/.kotlin
+/foundation/bin/*
+/samples/showcase/bin/*
+/new_release_notes.md
diff --git a/platform/icons-impl/BUILD.bazel b/platform/icons-impl/BUILD.bazel
new file mode 100644
index 0000000000000..fd2dbda804948
--- /dev/null
+++ b/platform/icons-impl/BUILD.bazel
@@ -0,0 +1,44 @@
+### auto-generated section `build intellij.platform.icons.impl` start
+load("@rules_jvm//:jvm.bzl", "jvm_library")
+
+jvm_library(
+ name = "icons-impl",
+ module_name = "intellij.platform.icons.impl",
+ visibility = ["//visibility:public"],
+ srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True),
+ deps = [
+ "@lib//:kotlin-stdlib",
+ "//libraries/kotlinx/coroutines/core",
+ "//platform/icons-api",
+ "//platform/icons-api/rendering",
+ "//libraries/kotlinx/serialization/core",
+ "@lib//:jetbrains-annotations",
+ ],
+ exports = [
+ "//platform/icons-api",
+ "//platform/icons-api/rendering",
+ ]
+)
+
+jvm_library(
+ name = "icons-impl_test_lib",
+ module_name = "intellij.platform.icons.impl",
+ visibility = ["//visibility:public"],
+ srcs = glob([], allow_empty = True),
+ exports = [
+ "//platform/icons-api",
+ "//platform/icons-api:icons-api_test_lib",
+ "//platform/icons-api/rendering",
+ "//platform/icons-api/rendering:rendering_test_lib",
+ ],
+ runtime_deps = [
+ ":icons-impl",
+ "//libraries/kotlinx/coroutines/core:core_test_lib",
+ "//platform/icons-api:icons-api_test_lib",
+ "//platform/icons-api/rendering:rendering_test_lib",
+ "//platform/icons",
+ "//platform/icons:icons_test_lib",
+ "//libraries/kotlinx/serialization/core:core_test_lib",
+ ]
+)
+### auto-generated section `build intellij.platform.icons.impl` end
\ No newline at end of file
diff --git a/platform/icons-impl/README.md b/platform/icons-impl/README.md
new file mode 100644
index 0000000000000..bf3d86a460a24
--- /dev/null
+++ b/platform/icons-impl/README.md
@@ -0,0 +1,3 @@
+## Structure
+- root - General implementation, reused
+- intellij - IntelliJ specific implementations
\ No newline at end of file
diff --git a/platform/icons-impl/build.gradle.kts b/platform/icons-impl/build.gradle.kts
new file mode 100644
index 0000000000000..b1306f2897fe1
--- /dev/null
+++ b/platform/icons-impl/build.gradle.kts
@@ -0,0 +1,28 @@
+// This file is used by Jewel gradle script, check community/platform/jewel
+
+plugins {
+ jewel
+ alias(libs.plugins.kotlinx.serialization)
+}
+
+sourceSets {
+ main {
+ kotlin {
+ setSrcDirs(listOf("src"))
+ }
+ }
+
+ test {
+ kotlin {
+ setSrcDirs(listOf("test"))
+ }
+ }
+}
+
+dependencies {
+ api("org.jetbrains:annotations:26.0.2")
+ api(project(":jb-icons-api"))
+ api(project(":jb-icons-api-rendering"))
+ api(libs.kotlinx.serialization.core)
+ api(libs.kotlinx.coroutines.core)
+}
diff --git a/platform/icons-impl/intellij.platform.icons.impl.iml b/platform/icons-impl/intellij.platform.icons.impl.iml
new file mode 100644
index 0000000000000..f8d8181fe53d2
--- /dev/null
+++ b/platform/icons-impl/intellij.platform.icons.impl.iml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/BUILD.bazel b/platform/icons-impl/intellij/BUILD.bazel
new file mode 100644
index 0000000000000..22f96bae67184
--- /dev/null
+++ b/platform/icons-impl/intellij/BUILD.bazel
@@ -0,0 +1,90 @@
+### auto-generated section `build intellij.platform.icons.impl.intellij` start
+load("@rules_jvm//:jvm.bzl", "jvm_library", "jvm_provided_library", "resourcegroup")
+
+resourcegroup(
+ name = "intellij_resources",
+ srcs = glob(["resources/**/*"]),
+ strip_prefix = "resources"
+)
+
+jvm_provided_library(
+ name = "platform_icons-api_provided",
+ lib = "//platform/icons-api"
+)
+
+jvm_provided_library(
+ name = "platform_icons-api_rendering_provided",
+ lib = "//platform/icons-api/rendering"
+)
+
+jvm_provided_library(
+ name = "platform_icons-impl_provided",
+ lib = "//platform/icons-impl"
+)
+
+jvm_library(
+ name = "intellij",
+ module_name = "intellij.platform.icons.impl.intellij",
+ visibility = ["//visibility:public"],
+ srcs = glob(["src/**/*.kt", "src/**/*.java", "src/**/*.form"], allow_empty = True),
+ resources = [":intellij_resources"],
+ deps = [
+ "@lib//:kotlin-stdlib",
+ "//libraries/kotlinx/coroutines/core",
+ "//libraries/kotlinx/serialization/core",
+ "//platform/util",
+ "//platform/util:util-ui",
+ "//platform/core-api:core",
+ "//platform/platform-impl/rpc",
+ "//fleet/util/core",
+ "//platform/core-impl",
+ "//platform/platform-impl:ide-impl",
+ "//platform/core-ui",
+ "@lib//:jetbrains-annotations",
+ "//platform/util/concurrency",
+ ":platform_icons-api_provided",
+ ":platform_icons-api_rendering_provided",
+ ":platform_icons-impl_provided",
+ ],
+ exports = [
+ "//platform/icons-api",
+ "//platform/icons-api/rendering",
+ "//platform/icons-impl",
+ ]
+)
+
+jvm_library(
+ name = "intellij_test_lib",
+ module_name = "intellij.platform.icons.impl.intellij",
+ visibility = ["//visibility:public"],
+ srcs = glob([], allow_empty = True),
+ exports = [
+ "//platform/icons-api",
+ "//platform/icons-api:icons-api_test_lib",
+ "//platform/icons-api/rendering",
+ "//platform/icons-api/rendering:rendering_test_lib",
+ "//platform/icons-impl",
+ "//platform/icons-impl:icons-impl_test_lib",
+ ],
+ runtime_deps = [
+ ":intellij",
+ "//libraries/kotlinx/coroutines/core:core_test_lib",
+ "//libraries/kotlinx/serialization/core:core_test_lib",
+ "//platform/icons-api",
+ "//platform/icons-api:icons-api_test_lib",
+ "//platform/icons-api/rendering",
+ "//platform/icons-api/rendering:rendering_test_lib",
+ "//platform/icons-impl",
+ "//platform/icons-impl:icons-impl_test_lib",
+ "//platform/util:util_test_lib",
+ "//platform/util:util-ui_test_lib",
+ "//platform/core-api:core_test_lib",
+ "//platform/platform-impl/rpc:rpc_test_lib",
+ "//fleet/util/core:core_test_lib",
+ "//platform/core-impl:core-impl_test_lib",
+ "//platform/platform-impl:ide-impl_test_lib",
+ "//platform/core-ui:core-ui_test_lib",
+ "//platform/util/concurrency:concurrency_test_lib",
+ ]
+)
+### auto-generated section `build intellij.platform.icons.impl.intellij` end
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/intellij.platform.icons.impl.intellij.iml b/platform/icons-impl/intellij/intellij.platform.icons.impl.intellij.iml
new file mode 100644
index 0000000000000..2e4055576f9e2
--- /dev/null
+++ b/platform/icons-impl/intellij/intellij.platform.icons.impl.intellij.iml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $KOTLIN_BUNDLED$/lib/kotlinx-serialization-compiler-plugin.jar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/module-content.yaml b/platform/icons-impl/intellij/module-content.yaml
new file mode 100644
index 0000000000000..3f0be96e40100
--- /dev/null
+++ b/platform/icons-impl/intellij/module-content.yaml
@@ -0,0 +1,3 @@
+- name: dist.all/lib/intellij.platform.icons.impl.intellij.jar
+ modules:
+ - name: intellij.platform.icons.impl.intellij
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/resources/intellij.platform.icons.impl.intellij.xml b/platform/icons-impl/intellij/resources/intellij.platform.icons.impl.intellij.xml
new file mode 100644
index 0000000000000..64d37ff3a6822
--- /dev/null
+++ b/platform/icons-impl/intellij/resources/intellij.platform.icons.impl.intellij.xml
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/IntelliJDeferredIconResolverService.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/IntelliJDeferredIconResolverService.kt
new file mode 100644
index 0000000000000..17c9a2180d915
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/IntelliJDeferredIconResolverService.kt
@@ -0,0 +1,29 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij
+
+import com.intellij.ide.PowerSaveMode
+import com.intellij.openapi.components.Service
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.DeferredIcon
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.IconIdentifier
+import org.jetbrains.icons.impl.DefaultDeferredIcon
+import org.jetbrains.icons.impl.DeferredIconResolver
+import org.jetbrains.icons.impl.DeferredIconResolverService
+import java.lang.ref.ReferenceQueue
+import java.lang.ref.WeakReference
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.time.Duration.Companion.seconds
+
+@ApiStatus.Internal
+@Service(Service.Level.APP)
+class IntelliJDeferredIconResolverService(scope: CoroutineScope): DeferredIconResolverService(scope) {
+ override fun scheduleEvaluation(icon: DeferredIcon) {
+ if (!PowerSaveMode.isEnabled()) {
+ super.scheduleEvaluation(icon)
+ }
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/IntelliJIconManager.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/IntelliJIconManager.kt
new file mode 100644
index 0000000000000..f25a296979c0b
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/IntelliJIconManager.kt
@@ -0,0 +1,123 @@
+// 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.icons.impl.intellij
+
+import com.intellij.ide.plugins.cl.PluginAwareClassLoader
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.ModalityState
+import com.intellij.openapi.application.UI
+import com.intellij.openapi.application.asContextElement
+import com.intellij.openapi.components.service
+import com.intellij.util.messages.Topic
+import com.intellij.util.messages.Topic.BroadcastDirection
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import kotlinx.serialization.modules.SerializersModuleBuilder
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.IconIdentifier
+import org.jetbrains.icons.ImageResourceLocation
+import org.jetbrains.icons.design.IconDesigner
+import org.jetbrains.icons.impl.DefaultDeferredIcon
+import org.jetbrains.icons.impl.DefaultIconManager
+import org.jetbrains.icons.impl.DeferredIconResolver
+import org.jetbrains.icons.impl.intellij.custom.CustomIconLayerRegistration
+import org.jetbrains.icons.impl.intellij.custom.CustomLegacyIconSerializer
+import org.jetbrains.icons.impl.intellij.design.IntelliJIconDesigner
+import org.jetbrains.icons.impl.intellij.rendering.IntelliJIconRendererManager
+import org.jetbrains.icons.impl.intellij.rendering.SwingIcon
+import org.jetbrains.icons.impl.intellij.rendering.images.IntelliJImageResourceProvider
+import org.jetbrains.icons.rendering.IconRendererManager
+import org.jetbrains.icons.rendering.ImageResourceProvider
+import java.lang.ref.WeakReference
+import kotlin.getValue
+
+class IntelliJIconManager : DefaultIconManager() {
+ override val resolverService: IntelliJDeferredIconResolverService by lazy {
+ service()
+ }
+
+ override fun generateDeferredIconIdentifier(id: String?, classLoader: ClassLoader?): IconIdentifier {
+ if (classLoader != null) {
+ val (pluginId, moduleId) = getPluginAndModuleId(classLoader)
+ return ModuleIconIdentifier(pluginId, moduleId, super.generateDeferredIconIdentifier(id, classLoader))
+ }
+ return super.generateDeferredIconIdentifier(id, classLoader)
+ }
+
+ override fun createDeferredIconResolver(
+ id: IconIdentifier,
+ ref: WeakReference,
+ evaluator: (suspend () -> Icon)?
+ ): DeferredIconResolver {
+ if (evaluator == null) {
+ throw NotImplementedError("Remote Icon evaluation is not supported")
+ }
+ else {
+ return super.createDeferredIconResolver(id, ref, evaluator)
+ }
+ }
+
+ override fun icon(designer: IconDesigner.() -> Unit): Icon {
+ val ijIconDesigner = IntelliJIconDesigner()
+ ijIconDesigner.designer()
+ return ijIconDesigner.build()
+ }
+
+ override fun toSwingIcon(icon: Icon): javax.swing.Icon {
+ return SwingIcon(icon)
+ }
+
+ override fun markDeferredIconUnused(id: IconIdentifier) {
+ // TODO delete unused deferred icons
+ }
+
+ override suspend fun sendDeferredNotifications(id: IconIdentifier, result: Icon) {
+ val deferredIconListener = ApplicationManager.getApplication().messageBus.syncPublisher(DeferredIconListener.TOPIC)
+ withContext(Dispatchers.UI + ModalityState.any().asContextElement()) {
+ deferredIconListener.evaluated(id, result)
+ }
+ }
+
+ override fun SerializersModuleBuilder.buildCustomSerializers() {
+ CustomLegacyIconSerializer.registerSerializersTo(this)
+ CustomIconLayerRegistration.registerSerializersTo(this)
+ polymorphic(
+ ImageResourceLocation::class,
+ ModuleImageResourceLocation::class,
+ ModuleImageResourceLocation.serializer()
+ )
+ polymorphic(
+ IconIdentifier::class,
+ ModuleIconIdentifier::class,
+ ModuleIconIdentifier.serializer()
+ )
+ }
+
+ companion object {
+ fun activate() {
+ org.jetbrains.icons.IconManager.activate(IntelliJIconManager())
+ IconRendererManager.activate(IntelliJIconRendererManager())
+ ImageResourceProvider.activate(IntelliJImageResourceProvider())
+ }
+
+ internal fun getPluginAndModuleId(classLoader: ClassLoader): Pair {
+ if (classLoader is PluginAwareClassLoader) {
+ return classLoader.pluginId.idString to classLoader.moduleId
+ }
+ else {
+ return "com.intellij" to null
+ }
+ }
+ }
+}
+
+@ApiStatus.Internal
+interface DeferredIconListener {
+ fun evaluated(id: IconIdentifier, result: Icon)
+
+ companion object {
+ @JvmField
+ @Topic.AppLevel
+ val TOPIC: Topic = Topic(DeferredIconListener::class.java, BroadcastDirection.NONE)
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/ModuleIconIdentifier.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/ModuleIconIdentifier.kt
new file mode 100644
index 0000000000000..be17a3bd05756
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/ModuleIconIdentifier.kt
@@ -0,0 +1,36 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.icons.IconIdentifier
+
+@Serializable
+class ModuleIconIdentifier(
+ val pluginId: String,
+ val moduleId: String?,
+ val uniqueId: IconIdentifier
+): IconIdentifier {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ModuleIconIdentifier
+
+ if (pluginId != other.pluginId) return false
+ if (moduleId != other.moduleId) return false
+ if (uniqueId != other.uniqueId) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = pluginId.hashCode()
+ result = 31 * result + moduleId.hashCode()
+ result = 31 * result + uniqueId.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "ModuleIconIdentifier(pluginId='$pluginId', moduleId='$moduleId', uniqueId='$uniqueId')"
+ }
+}
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/ModuleImageResourceLocation.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/ModuleImageResourceLocation.kt
new file mode 100644
index 0000000000000..68c4133ceb06c
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/ModuleImageResourceLocation.kt
@@ -0,0 +1,45 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.icons.ImageResourceLocation
+
+@Serializable
+class ModuleImageResourceLocation(
+ @JvmField val path: String,
+ @JvmField val pluginId: String,
+ @JvmField val moduleId: String?,
+): ImageResourceLocation {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ModuleImageResourceLocation
+
+ if (path != other.path) return false
+ if (pluginId != other.pluginId) return false
+ if (moduleId != other.moduleId) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = path.hashCode()
+ result = 31 * result + pluginId.hashCode()
+ result = 31 * result + (moduleId?.hashCode() ?: 0)
+ return result
+ }
+
+ override fun toString(): String {
+ return "ModuleImageResourceLoader(path='$path', pluginId='$pluginId', moduleId=$moduleId)"
+ }
+
+ companion object {
+ fun fromClassLoader(path: String, classLoader: ClassLoader): ImageResourceLocation {
+ val (pluginId, moduleId) = IntelliJIconManager.getPluginAndModuleId(classLoader)
+ return ModuleImageResourceLocation(path, pluginId, moduleId)
+ }
+
+ }
+
+}
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/custom/CustomIconLayerRegistration.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/custom/CustomIconLayerRegistration.kt
new file mode 100644
index 0000000000000..65e4fa4808aa6
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/custom/CustomIconLayerRegistration.kt
@@ -0,0 +1,16 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.custom
+
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.layers.IconLayer
+import kotlin.reflect.KClass
+
+abstract class CustomIconLayerRegistration(
+ klass: KClass
+): CustomSerializableRegistration(klass) {
+ @ApiStatus.Internal
+ companion object: CustomSerializableRegistration.Companion>(
+ IconLayer ::class,
+ "com.intellij.icons.customLayer"
+ )
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/custom/CustomLegacyIconSerializer.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/custom/CustomLegacyIconSerializer.kt
new file mode 100644
index 0000000000000..2d052b64ed862
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/custom/CustomLegacyIconSerializer.kt
@@ -0,0 +1,16 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.custom
+
+import org.jetbrains.annotations.ApiStatus
+import javax.swing.Icon
+import kotlin.reflect.KClass
+
+abstract class CustomLegacyIconSerializer(
+ klass: KClass
+): CustomSerializableRegistration(klass) {
+ @ApiStatus.Internal
+ companion object: CustomSerializableRegistration.Companion>(
+ Icon::class,
+ "com.intellij.icons.customLegacyIconSerializer"
+ )
+}
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/custom/CustomSerializableRegistration.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/custom/CustomSerializableRegistration.kt
new file mode 100644
index 0000000000000..19eb0f81441a6
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/custom/CustomSerializableRegistration.kt
@@ -0,0 +1,41 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.custom
+
+import com.intellij.openapi.extensions.ExtensionPointName
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.modules.SerializersModuleBuilder
+import kotlin.reflect.KClass
+
+abstract class CustomSerializableRegistration(
+ internal val klass: KClass
+) {
+ abstract fun createSerializer(): KSerializer
+
+ abstract class Companion>(
+ private val baseClass: KClass,
+ extensionName: String
+ ) {
+ fun registerSerializersTo(builder: SerializersModuleBuilder) {
+ EP_NAME.extensionList.forEach { instance ->
+ val serializer = instance.createSerializer()
+ @Suppress("UNCHECKED_CAST")
+ builder.addCustom(instance as CustomSerializableRegistration)
+ onRegistration(instance, serializer)
+ }
+ }
+
+ private fun SerializersModuleBuilder.addCustom(instance: CustomSerializableRegistration) {
+ polymorphic(
+ baseClass,
+ instance.klass,
+ instance.createSerializer()
+ )
+ }
+
+ open fun onRegistration(ep: TEP, serializer: KSerializer<*>) {
+ // Do nothing in default implementation
+ }
+
+ val EP_NAME: ExtensionPointName = ExtensionPointName(extensionName)
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/design/IntelliJIconDesigner.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/design/IntelliJIconDesigner.kt
new file mode 100644
index 0000000000000..1230183d85db6
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/design/IntelliJIconDesigner.kt
@@ -0,0 +1,17 @@
+// 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.icons.impl.intellij.design
+
+import org.jetbrains.icons.modifiers.IconModifier
+import org.jetbrains.icons.impl.design.DefaultIconDesigner
+import org.jetbrains.icons.impl.intellij.ModuleImageResourceLocation
+
+class IntelliJIconDesigner: DefaultIconDesigner() {
+ override fun image(path: String, classLoader: ClassLoader?, modifier: IconModifier) {
+ if (classLoader == null) error("Specifying classloader for icon image is required in IntelliJ.")
+ image(ModuleImageResourceLocation.fromClassLoader(path, classLoader), modifier)
+ }
+
+ override fun createNestedDesigner(): DefaultIconDesigner {
+ return IntelliJIconDesigner()
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/Convertors.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/Convertors.kt
new file mode 100644
index 0000000000000..c3c8faa66a7fd
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/Convertors.kt
@@ -0,0 +1,107 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering
+
+import com.intellij.ui.svg.SvgAttributePatcher
+import com.intellij.util.SVGLoader
+import org.jetbrains.icons.design.BlendMode
+import org.jetbrains.icons.filters.ColorFilter
+import org.jetbrains.icons.filters.TintColorFilter
+import org.jetbrains.icons.patchers.SvgPatchOperation
+import org.jetbrains.icons.patchers.SvgPatcher
+import java.awt.Color
+import java.awt.image.RGBImageFilter
+
+internal fun ColorFilter.toAwtFilter(): RGBImageFilter {
+ return when (this) {
+ is TintColorFilter -> {
+ AwtColorFilter.fromColorAndBlendMode(color, blendMode)
+ }
+ }
+}
+
+internal fun SvgPatcher.toIJPatcher(): SVGLoader.SvgElementColorPatcherProvider {
+ return ProxySvgPatcher(this)
+}
+
+private class ProxySvgPatcher(
+ private val patcher: SvgPatcher
+): SVGLoader.SvgElementColorPatcherProvider, SvgAttributePatcher {
+ override fun attributeForPath(path: String): SvgAttributePatcher = this
+ override fun digest(): LongArray {
+ return longArrayOf(patcher.hashCode().toLong())
+ }
+
+ override fun patchColors(attributes: MutableMap) {
+ // TODO Support filtered operations - not possible with current IJ svg loader
+ for (operation in patcher.operations) {
+ when (operation.operation) {
+ SvgPatchOperation.Operation.Add -> {
+ if (!attributes.containsKey(operation.attributeName)) {
+ attributes[operation.attributeName] = operation.value!!
+ }
+ }
+ SvgPatchOperation.Operation.Replace -> {
+ if (operation.conditional) {
+ val matches = attributes[operation.attributeName] == operation.expectedValue
+ if (matches == !operation.negatedCondition) {
+ attributes.replace(operation.attributeName, operation.value!!)
+ }
+ } else {
+ attributes.replace(operation.attributeName, operation.value!!)
+ }
+ }
+ SvgPatchOperation.Operation.Remove -> {
+ if (operation.conditional) {
+ val matches = attributes[operation.attributeName] == operation.expectedValue
+ if (matches == !operation.negatedCondition) {
+ attributes.remove(operation.attributeName)
+ }
+ } else {
+ attributes.remove(operation.attributeName)
+ }
+ }
+ SvgPatchOperation.Operation.Set -> attributes[operation.attributeName] = operation.value!!
+ }
+ }
+ }
+}
+
+private class AwtColorFilter(val color: Color, val keepGray: Boolean, val keepBrightness: Boolean) : RGBImageFilter() {
+ private val base = Color.RGBtoHSB(color.red, color.green, color.blue, null)
+
+ override fun filterRGB(x: Int, y: Int, rgba: Int): Int {
+ val r = rgba shr 16 and 0xff
+ val g = rgba shr 8 and 0xff
+ val b = rgba and 0xff
+ val hsb = FloatArray(3)
+ Color.RGBtoHSB(r, g, b, hsb)
+ val rgb = Color.HSBtoRGB(base[0],
+ base[1] * if (keepGray) hsb[1] else 1.0f,
+ base[2] * if (keepBrightness) hsb[2] else 1.0f)
+ return rgba and -0x1000000 or (rgb and 0xffffff)
+ }
+
+ companion object {
+ fun fromColorAndBlendMode(color: org.jetbrains.icons.design.Color, blendMode: BlendMode): AwtColorFilter {
+ var keepGray = true
+ var keepBrightness = true
+ when (blendMode) {
+ BlendMode.Hue -> {
+ keepGray = false
+ keepBrightness = false
+ }
+ BlendMode.Saturation -> {
+ keepGray = false
+ }
+ else -> {
+ // Do nothing
+ }
+ }
+ return AwtColorFilter(
+ color.toAwtColor(),
+ keepGray,
+ keepBrightness
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/IconUpdateService.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/IconUpdateService.kt
new file mode 100644
index 0000000000000..420877aa3ccb2
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/IconUpdateService.kt
@@ -0,0 +1,29 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering
+
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.service
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.launch
+import org.jetbrains.annotations.ApiStatus
+
+@Service(Service.Level.APP)
+@ApiStatus.Internal
+class IconUpdateService(val scope: CoroutineScope) {
+ @ApiStatus.Experimental
+ fun scheduleDelayedUpdate(delay: Long, updateId: Int, flow: MutableSharedFlow, updateCallback: (Int) -> Unit, rateLimiter: () -> Boolean = { false }) {
+ scope.launch {
+ delay(delay)
+ if (rateLimiter()) return@launch
+ flow.emit(updateId)
+ updateCallback(updateId)
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ fun getInstance(): IconUpdateService = service()
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/ImageResourceLoaderExtension.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/ImageResourceLoaderExtension.kt
new file mode 100644
index 0000000000000..9b0e753b39ada
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/ImageResourceLoaderExtension.kt
@@ -0,0 +1,29 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering
+
+import com.intellij.openapi.extensions.ExtensionPointName
+import com.intellij.util.xmlb.annotations.Attribute
+import org.jetbrains.icons.ImageResourceLocation
+import org.jetbrains.icons.rendering.GenericImageResourceLoader
+
+
+internal class ImageResourceLoaderExtension {
+ @Attribute("loader")
+ lateinit var loader: GenericImageResourceLoader
+
+ @Attribute("location")
+ lateinit var location: String
+
+ companion object {
+ fun getLoaderFor(location: ImageResourceLocation): GenericImageResourceLoader? {
+ for (extension in LOADER_EP_NAME.extensionList) {
+ if (location::class.qualifiedName == extension.location) {
+ return extension.loader
+ }
+ }
+ return null
+ }
+
+ val LOADER_EP_NAME: ExtensionPointName = ExtensionPointName("com.intellij.icons.imageResourceLoader")
+ }
+}
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/IntelliJIconRendererManager.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/IntelliJIconRendererManager.kt
new file mode 100644
index 0000000000000..fa89fe1888725
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/IntelliJIconRendererManager.kt
@@ -0,0 +1,50 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering
+
+import com.intellij.util.ui.StartupUiUtil
+import kotlinx.coroutines.CoroutineScope
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.impl.intellij.rendering.custom.CustomIconLayerRendererProvider
+import org.jetbrains.icons.rendering.ImageModifiers
+import org.jetbrains.icons.rendering.MutableIconUpdateFlow
+import org.jetbrains.icons.rendering.RenderingContext
+import org.jetbrains.icons.impl.rendering.CoroutineBasedMutableIconUpdateFlow
+import org.jetbrains.icons.impl.rendering.DefaultIconRendererManager
+import org.jetbrains.icons.layers.IconLayer
+import org.jetbrains.icons.impl.rendering.DefaultImageModifiers
+import org.jetbrains.icons.impl.rendering.layers.IconLayerRenderer
+
+@Suppress("UNCHECKED_CAST")
+class IntelliJIconRendererManager: DefaultIconRendererManager() {
+ override fun createRenderer(layer: IconLayer, renderingContext: RenderingContext): IconLayerRenderer {
+ val defaultRenderer = createRendererOrNull(layer, renderingContext)
+ if (defaultRenderer != null) return defaultRenderer
+
+ return CustomIconLayerRendererProvider.createRendererFor(layer, renderingContext)
+ ?: error("No renderer found for Icon Layer type: $layer\nMake sure that the corresponding renderer is properly registered.")
+ }
+
+ override fun createUpdateFlow(scope: CoroutineScope?, updateCallback: (Int) -> Unit): MutableIconUpdateFlow {
+ if (scope != null) {
+ return CoroutineBasedMutableIconUpdateFlow(scope, updateCallback)
+ } else {
+ return IntelliJMutableIconUpdateFlowImpl(updateCallback)
+ }
+ }
+
+ override fun createRenderingContext(
+ updateFlow: MutableIconUpdateFlow,
+ defaultImageModifiers: ImageModifiers?,
+ ): RenderingContext {
+ val knownModifiers = defaultImageModifiers as? DefaultImageModifiers
+ return RenderingContext(
+ updateFlow,
+ DefaultImageModifiers(
+ defaultImageModifiers?.colorFilter,
+ defaultImageModifiers?.svgPatcher,
+ StartupUiUtil.isDarkTheme,
+ knownModifiers?.stroke
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/IntelliJMutableIconUpdateFlowImpl.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/IntelliJMutableIconUpdateFlowImpl.kt
new file mode 100644
index 0000000000000..bf5d3d5cabce0
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/IntelliJMutableIconUpdateFlowImpl.kt
@@ -0,0 +1,24 @@
+// 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.icons.impl.intellij.rendering
+
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.launch
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.impl.rendering.MutableIconUpdateFlowBase
+
+internal class IntelliJMutableIconUpdateFlowImpl(
+ updateCallback: (Int) -> Unit
+) : MutableIconUpdateFlowBase(updateCallback) {
+ override fun MutableSharedFlow.emitDelayed(delay: Long, value: Int) {
+ IconUpdateService.getInstance().scheduleDelayedUpdate(delay, value, this@emitDelayed, updateCallback) {
+ handleRateLimiting()
+ }
+ }
+
+ override fun collectDynamic(flow: Flow, handler: (Icon) -> Unit) {
+ IconUpdateService.getInstance().scope.launch {
+ flow.collect { handler(it) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/SwingIcon.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/SwingIcon.kt
new file mode 100644
index 0000000000000..643453710cea6
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/SwingIcon.kt
@@ -0,0 +1,77 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering
+
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.rendering.IconRendererManager
+import org.jetbrains.icons.rendering.RenderingContext
+import org.jetbrains.icons.rendering.ScalingContext
+import org.jetbrains.icons.impl.rendering.layers.applyTo
+import org.jetbrains.icons.rendering.Dimensions
+import java.awt.Component
+import java.awt.Graphics
+import java.awt.Graphics2D
+import java.awt.image.BufferedImage
+
+@ApiStatus.Experimental
+class SwingIcon(
+ val icon: Icon
+): javax.swing.Icon {
+ private val renderer by lazy {
+ IconRendererManager.getInstance().createRenderer(icon, RenderingContext.Empty) // TODO listen for updates to redraw Icon
+ }
+ private val dimensions by lazy {
+ renderer.calculateExpectedDimensions(SwingScalingContext(1f))
+ }
+
+ override fun paintIcon(c: Component?, g: Graphics, x: Int, y: Int) {
+ val scaling = getScaling(g)
+ val boundsSize = if (c != null) {
+ Dimensions(c.width, c.height)
+ } else Dimensions(dimensions.width, dimensions.height)
+ withLayer(scaling, boundsSize, g, x, y) { newGraphics ->
+ val swingApi = SwingPaintingApi(
+ c,
+ newGraphics,
+ 0,
+ 0,
+ scaling = scaling,
+ customWidth = boundsSize.width,
+ customHeight = boundsSize.height
+ )
+ renderer.render(swingApi)
+ }
+ }
+
+ private fun withLayer(scaling: ScalingContext, boundsSize: Dimensions, g: Graphics, x: Int, y: Int, painting: (Graphics2D) -> Unit) {
+ val w = scaling.applyTo(boundsSize.width - x)
+ val h = scaling.applyTo(boundsSize.height - y)
+ if (w < 0 || h < 0) return
+ val img = BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB)
+ val sublayer = img.createGraphics()
+ try {
+ painting(sublayer as Graphics2D)
+ g.drawImage(img, x, y, boundsSize.width, boundsSize.height, 0, 0, img.width, img.height, null)
+ } finally {
+ sublayer.dispose()
+ }
+ }
+
+ private fun Graphics2D.clearTransform() {
+ scale(1 / transform.scaleX, 1 / transform.scaleY)
+ }
+
+ private fun getScaling(g: Graphics?): SwingScalingContext {
+ if (g is Graphics2D) {
+ return SwingScalingContext(g.transform.scaleX.toFloat())
+ } else return SwingScalingContext(1f)
+ }
+
+ override fun getIconWidth(): Int {
+ return dimensions.width
+ }
+
+ override fun getIconHeight(): Int {
+ return dimensions.height
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/SwingIconLayerRegistration.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/SwingIconLayerRegistration.kt
new file mode 100644
index 0000000000000..96282a9995edd
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/SwingIconLayerRegistration.kt
@@ -0,0 +1,12 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering
+
+import kotlinx.serialization.KSerializer
+import org.jetbrains.icons.impl.intellij.custom.CustomIconLayerRegistration
+import org.jetbrains.icons.swing.SwingIconLayer
+
+internal class SwingIconLayerRegistration: CustomIconLayerRegistration(SwingIconLayer::class) {
+ override fun createSerializer(): KSerializer {
+ return SwingIconLayer.serializer()
+ }
+}
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/SwingIconLayerRenderer.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/SwingIconLayerRenderer.kt
new file mode 100644
index 0000000000000..d5b744dc4ad41
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/SwingIconLayerRenderer.kt
@@ -0,0 +1,110 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering
+
+import com.intellij.ide.PowerSaveMode
+import com.intellij.openapi.application.ApplicationManager
+import com.intellij.openapi.application.ModalityState
+import com.intellij.openapi.application.UI
+import com.intellij.openapi.application.asContextElement
+import com.intellij.ui.DeferredIcon
+import com.intellij.ui.DeferredIconListener
+import com.intellij.util.messages.impl.subscribeAsFlow
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.suspendCancellableCoroutine
+import kotlinx.coroutines.withContext
+import org.jetbrains.icons.impl.intellij.rendering.custom.CustomIconLayerRendererProvider
+import org.jetbrains.icons.impl.rendering.DefaultImageResourceProvider
+import org.jetbrains.icons.rendering.Dimensions
+import org.jetbrains.icons.rendering.ImageResource
+import org.jetbrains.icons.rendering.ImageResourceProvider
+import org.jetbrains.icons.impl.rendering.layers.BaseImageIconLayerRenderer
+import org.jetbrains.icons.impl.rendering.layers.IconLayerRenderer
+import org.jetbrains.icons.rendering.RenderingContext
+import org.jetbrains.icons.rendering.ScalingContext
+import org.jetbrains.icons.impl.rendering.layers.applyTo
+import org.jetbrains.icons.impl.rendering.layers.generateImageModifiers
+import org.jetbrains.icons.layers.IconLayer
+import org.jetbrains.icons.rendering.PaintingApi
+import org.jetbrains.icons.swing.SwingIconLayer
+import java.util.concurrent.atomic.AtomicBoolean
+import javax.swing.Icon
+import kotlin.coroutines.resume
+
+class SwingIconLayerRendererProvider: CustomIconLayerRendererProvider {
+ override fun handles(layer: IconLayer): Boolean {
+ return layer is SwingIconLayer
+ }
+
+ override fun createRenderer(layer: IconLayer, renderingContext: RenderingContext): IconLayerRenderer {
+ val swingLayer = layer as SwingIconLayer
+ val renderer = SwingIconLayerRenderer(
+ swingLayer,
+ renderingContext
+ )
+ return renderer
+ }
+}
+
+class SwingIconLayerRenderer(
+ override val layer: SwingIconLayer,
+ override val renderingContext: RenderingContext
+): BaseImageIconLayerRenderer() {
+ private val deferredIcon = layer.legacyIcon as? DeferredIcon?
+ private var isDone = deferredIcon?.isDone ?: false
+ override var image: ImageResource = createImageResource()
+ private val isPending = AtomicBoolean(false)
+
+ override fun render(api: PaintingApi) {
+ checkForUpdatesIfNeeded()
+ super.render(api)
+ }
+
+ private fun checkForUpdatesIfNeeded() {
+ if (deferredIcon != null && !PowerSaveMode.isEnabled() && !isDone) {
+ if (!isPending.getAndSet(true)) {
+ if (!deferredIcon.isDone) {
+ IconUpdateService.getInstance().scope.launch {
+ withContext(Dispatchers.UI + ModalityState.any().asContextElement()) {
+ suspendUntilDeferredIconIsDone()
+ isDone = true
+ image = createImageResource()
+ }
+ }
+ } else {
+ isDone = true
+ image = createImageResource()
+ }
+ }
+ }
+ }
+
+ private fun createImageResource(): ImageResource {
+ val provider = ImageResourceProvider.getInstance() as? DefaultImageResourceProvider
+ if (provider == null) error("Swing Icon fallback is only supported with DefaultImageResourceProvider")
+ return provider.fromSwingIcon(layer.legacyIcon, layer.generateImageModifiers(renderingContext))
+ }
+
+ override fun calculateExpectedDimensions(scaling: ScalingContext): Dimensions {
+ return Dimensions(scaling.applyTo(image.width) ?: 16, scaling.applyTo(image.height) ?: 16)
+ }
+
+ private suspend fun suspendUntilDeferredIconIsDone() {
+ val connection = ApplicationManager.getApplication().messageBus.simpleConnect()
+ try {
+ suspendCancellableCoroutine { continuation ->
+ val listener = object : DeferredIconListener {
+ override fun evaluated(deferred: DeferredIcon, result: Icon) {
+ if (deferred === this@SwingIconLayerRenderer.deferredIcon) {
+ continuation.resume(Unit)
+ }
+ }
+ }
+ connection.subscribe(DeferredIconListener.TOPIC, listener)
+ }
+ }
+ finally {
+ connection.disconnect()
+ }
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/SwingPaintingApi.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/SwingPaintingApi.kt
new file mode 100644
index 0000000000000..c02cd5da1dff0
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/SwingPaintingApi.kt
@@ -0,0 +1,230 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering
+
+import org.jetbrains.icons.design.Color
+import org.jetbrains.icons.filters.ColorFilter
+import org.jetbrains.icons.impl.intellij.rendering.images.awtImage
+import org.jetbrains.icons.rendering.BitmapImageResource
+import org.jetbrains.icons.rendering.Bounds
+import org.jetbrains.icons.rendering.DrawMode
+import org.jetbrains.icons.rendering.FitAreaScale
+import org.jetbrains.icons.rendering.ImageResource
+import org.jetbrains.icons.rendering.PaintingApi
+import org.jetbrains.icons.rendering.RescalableImageResource
+import org.jetbrains.icons.rendering.ScalingContext
+import java.awt.AlphaComposite
+import java.awt.Component
+import java.awt.Graphics
+import java.awt.Graphics2D
+import java.awt.Image
+import java.awt.RenderingHints
+import java.awt.Shape
+import java.awt.geom.Ellipse2D
+import java.awt.geom.Rectangle2D
+
+class SwingPaintingApi(
+ val c: Component?,
+ val g: Graphics,
+ val x: Int,
+ val y: Int,
+ private val customX: Int? = null,
+ private val customY: Int? = null,
+ private val customWidth: Int? = null,
+ private val customHeight: Int? = null,
+ private val overrideColorFilter: ColorFilter? = null,
+ override val scaling: ScalingContext = SwingScalingContext(1f),
+) : PaintingApi {
+ override val bounds: Bounds
+ get() {
+ if (c == null) return Bounds(0, 0, customWidth ?: 0, customHeight ?: 0)
+ return Bounds(
+ customX ?: (x * scaling.display).toInt(),
+ customY ?: (y * scaling.display).toInt(),
+ customWidth ?: (c.width * scaling.display).toInt(),
+ customHeight ?: (c.height * scaling.display).toInt()
+ )
+ }
+
+ override fun getUsedBounds(): Bounds = bounds
+
+ override fun withCustomContext(bounds: Bounds, overrideColorFilter: ColorFilter?): PaintingApi {
+ return SwingPaintingApi(
+ c,
+ g,
+ x,
+ y,
+ bounds.x,
+ bounds.y,
+ bounds.width,
+ bounds.height,
+ overrideColorFilter ?: this.overrideColorFilter,
+ scaling
+ )
+ }
+
+ override fun drawCircle(color: Color, x: Int, y: Int, radius: Float, alpha: Float, mode: DrawMode) {
+ val r = radius.toDouble()
+ drawShape(color, Ellipse2D.Double(x - r, y - r, r + r, r + r), alpha, mode)
+ }
+
+ override fun drawRect(color: Color, x: Int, y: Int, width: Int, height: Int, alpha: Float, mode: DrawMode) {
+ drawShape(color, Rectangle2D.Double(x.toDouble(), y.toDouble(), width.toDouble(), height.toDouble()), alpha, mode)
+ }
+
+ private fun drawShape(color: Color, shape: Shape, alpha: Float, mode: DrawMode) {
+ setDrawingDefaults()
+ if (g is Graphics2D) {
+ val oldComposite = g.composite
+ val oldPaint = g.paint
+ try {
+ if (mode == DrawMode.Clear) {
+ g.composite = AlphaComposite.Clear
+ }
+ g.color = color.toAwtColor()
+ if (mode == DrawMode.Stroke) {
+ g.draw(shape)
+ }
+ else {
+ g.fill(shape)
+ }
+ }
+ finally {
+ g.composite = oldComposite
+ g.paint = oldPaint
+ }
+ }
+ }
+
+ override fun drawImage(
+ image: ImageResource,
+ x: Int,
+ y: Int,
+ width: Int?,
+ height: Int?,
+ srcX: Int,
+ srcY: Int,
+ srcWidth: Int?,
+ srcHeight: Int?,
+ alpha: Float,
+ colorFilter: ColorFilter?,
+ ) {
+ when (image) {
+ is BitmapImageResource -> {
+ drawImage(image, x, y, width, height, srcX, srcY, srcWidth, srcHeight, alpha, colorFilter)
+ }
+ is RescalableImageResource -> {
+ drawImage(image, x, y, width, height, srcX, srcY, srcWidth, srcHeight, alpha, colorFilter)
+ }
+ }
+ }
+
+ private fun setDrawingDefaults() {
+ if (g !is Graphics2D) return
+ g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
+ g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE)
+ }
+
+ private fun drawImage(
+ image: RescalableImageResource,
+ x: Int,
+ y: Int,
+ width: Int?,
+ height: Int?,
+ srcX: Int,
+ srcY: Int,
+ srcWidth: Int?,
+ srcHeight: Int?,
+ alpha: Float,
+ colorFilter: ColorFilter?,
+ ) {
+ val swingImage = image.scale(FitAreaScale(width ?: bounds.width, height ?: bounds.height))
+ drawImage(swingImage, x, y, width, height, srcX, srcY, srcWidth, srcHeight, alpha, colorFilter)
+ }
+
+ private fun drawImage(
+ image: BitmapImageResource,
+ x: Int,
+ y: Int,
+ width: Int?,
+ height: Int?,
+ srcX: Int,
+ srcY: Int,
+ srcWidth: Int?,
+ srcHeight: Int?,
+ alpha: Float,
+ colorFilter: ColorFilter?,
+ ) {
+ val swingImage = image.awtImage()
+
+ drawImage(
+ swingImage,
+ x,
+ y,
+ width,
+ height,
+ srcX,
+ srcY,
+ srcWidth,
+ srcHeight,
+ alpha,
+ colorFilter
+ )
+ }
+
+ private fun drawImage(
+ image: Image,
+ x: Int,
+ y: Int,
+ width: Int?,
+ height: Int?,
+ srcX: Int,
+ srcY: Int,
+ srcWidth: Int?,
+ srcHeight: Int?,
+ alpha: Float,
+ colorFilter: ColorFilter?,
+ ) {
+ // TODO apply alpha & color filters
+
+ val imageWidth = image.getWidth(null)
+ val imageHeight = image.getHeight(null)
+
+ if (imageWidth == 0 || imageHeight == 0) return
+ setDrawingDefaults()
+ g.drawImage(
+ image,
+ x,
+ y,
+ x + (width ?: imageWidth),
+ y + (height ?: imageHeight),
+ srcX,
+ srcY,
+ srcX + (srcWidth ?: imageWidth),
+ srcY + (srcHeight ?: imageHeight),
+ null,
+ )
+ }
+}
+
+fun PaintingApi.swing(): SwingPaintingApi? = this as? SwingPaintingApi
+
+internal class SwingScalingContext(
+ override val display: Float,
+) : ScalingContext {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as SwingScalingContext
+
+ return display == other.display
+ }
+
+ override fun hashCode(): Int {
+ return display.hashCode()
+ }
+
+ override fun toString(): String {
+ return "SwingScalingContext(display=$display)"
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/custom/CustomIconLayerRendererProvider.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/custom/CustomIconLayerRendererProvider.kt
new file mode 100644
index 0000000000000..49f3f995dc067
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/custom/CustomIconLayerRendererProvider.kt
@@ -0,0 +1,25 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering.custom
+
+import com.intellij.openapi.extensions.ExtensionPointName
+import org.jetbrains.icons.impl.intellij.rendering.SwingIconLayerRenderer
+import org.jetbrains.icons.impl.rendering.layers.IconLayerRenderer
+import org.jetbrains.icons.layers.IconLayer
+import org.jetbrains.icons.swing.SwingIconLayer
+import org.jetbrains.icons.rendering.RenderingContext
+
+interface CustomIconLayerRendererProvider {
+ fun handles(layer: IconLayer): Boolean
+ fun createRenderer(layer: IconLayer, renderingContext: RenderingContext): IconLayerRenderer
+
+ companion object {
+ fun createRendererFor(layer: IconLayer, renderingContext: RenderingContext): IconLayerRenderer? {
+ for (extension in EP_NAME.extensionList) {
+ if (extension.handles(layer)) return SwingIconLayerRenderer(layer as SwingIconLayer, renderingContext)
+ }
+ return null
+ }
+
+ val EP_NAME: ExtensionPointName = ExtensionPointName("com.intellij.icons.customLayerRendererProvider")
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/AwtImageResource.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/AwtImageResource.kt
new file mode 100644
index 0000000000000..24b4bc5b50b32
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/AwtImageResource.kt
@@ -0,0 +1,85 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering.images
+
+import org.jetbrains.icons.impl.rendering.CachedGPUImageResourceHolder
+import org.jetbrains.icons.rendering.BitmapImageResource
+import org.jetbrains.icons.rendering.lowlevel.GPUImageResourceHolder
+import java.awt.Image
+import java.awt.Transparency
+import java.awt.color.ColorSpace
+import java.awt.image.BufferedImage
+import java.awt.image.ComponentColorModel
+import java.awt.image.DataBuffer
+import java.awt.image.Raster
+
+class AwtImageResource(
+ val image: Image
+) : CachedGPUImageResourceHolder(), BitmapImageResource {
+ private val bufferedIamge by lazy {
+ if (image is BufferedImage) return@lazy image
+ val bi = BufferedImage(
+ image.getWidth(null),
+ image.getHeight(null),
+ BufferedImage.TYPE_INT_ARGB
+ )
+ val g = bi.createGraphics()
+ g.drawImage(image, 0, 0, null)
+ g.dispose()
+ return@lazy bi
+ }
+
+ override fun getRGBPixels(): IntArray {
+ return bufferedIamge.getRGB(0, 0, bufferedIamge.raster.width, bufferedIamge.raster.height, null, 0, bufferedIamge.raster.width)
+ }
+
+ override fun readPrefetchedPixel(pixels: IntArray, x: Int, y: Int): Int? {
+ return pixels.getOrNull(y * bufferedIamge.raster.width + x)
+ }
+
+ override fun getBandOffsetsToSRGB(): IntArray {
+ return intArrayOf(0, 1, 2, 3)
+ }
+
+ override val width: Int = image.getWidth(null)
+ override val height: Int = image.getHeight(null)
+}
+
+fun BitmapImageResource.awtImage(): Image {
+ if (this is AwtImageResource) return image
+ val cache = (this as? GPUImageResourceHolder)
+ return cache?.getOrGenerateBitmap(Image::class) {
+ awtImageWithoutCaching()
+ } ?: awtImageWithoutCaching()
+}
+
+private fun BitmapImageResource.awtImageWithoutCaching(): Image {
+ val pxs = getRGBPixels()
+ val order = getBandOffsetsToSRGB()
+ val raster = Raster.createInterleavedRaster(
+ DirectDataBuffer(pxs),
+ this.width,
+ this.height,
+ this.width * 4,
+ 4,
+ order,
+ null
+ )
+ val colorModel = ComponentColorModel(
+ ColorSpace.getInstance(ColorSpace.CS_sRGB),
+ true,
+ false,
+ Transparency.TRANSLUCENT,
+ DataBuffer.TYPE_BYTE
+ )
+ return BufferedImage(colorModel, raster!!, false, null)
+}
+
+private class DirectDataBuffer(val pixels: IntArray) : DataBuffer(TYPE_BYTE, pixels.size) {
+ override fun getElem(bank: Int, index: Int): Int {
+ return pixels[index]
+ }
+
+ override fun setElem(bank: Int, index: Int, value: Int) {
+ throw UnsupportedOperationException("no write access")
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/DataLoaderImageResourceHolder.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/DataLoaderImageResourceHolder.kt
new file mode 100644
index 0000000000000..7f976592319cc
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/DataLoaderImageResourceHolder.kt
@@ -0,0 +1,27 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering.images
+
+import com.intellij.ui.icons.ImageDataLoader
+import com.intellij.ui.scale.ScaleContext
+import org.jetbrains.icons.rendering.Dimensions
+import org.jetbrains.icons.rendering.ImageModifiers
+import java.awt.Image
+
+internal class DataLoaderImageResourceHolder(
+ val dataLoader: ImageDataLoader
+) : SwingImageResourceHolder {
+ override fun getImage(
+ scale: ScaleContext,
+ imageModifiers: ImageModifiers?,
+ ): Image? {
+ return dataLoader.loadImage(
+ parameters = imageModifiers.toLoadParameters(),
+ scaleContext = scale
+ )
+ }
+
+ override fun getExpectedDimensions(): Dimensions {
+ val img = getImage(ScaleContext.create(), null) ?: return Dimensions(0, 0)
+ return Dimensions(img.getWidth(null), img.getHeight(null))
+ }
+}
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/IntelliJImageResource.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/IntelliJImageResource.kt
new file mode 100644
index 0000000000000..6f0c39e21990f
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/IntelliJImageResource.kt
@@ -0,0 +1,40 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering.images
+
+import com.intellij.ui.scale.ScaleContext
+import com.intellij.ui.scale.ScaleType
+import com.intellij.util.JBHiDPIScaledImage
+import org.jetbrains.icons.impl.rendering.CachedGPUImageResourceHolder
+import org.jetbrains.icons.rendering.BitmapImageResource
+import org.jetbrains.icons.rendering.Bounds
+import org.jetbrains.icons.rendering.EmptyBitmapImageResource
+import org.jetbrains.icons.rendering.ImageModifiers
+import org.jetbrains.icons.rendering.ImageScale
+import org.jetbrains.icons.rendering.RescalableImageResource
+
+internal class IntelliJImageResource(
+ private val holder: SwingImageResourceHolder,
+ private val imageModifiers: ImageModifiers? = null
+): RescalableImageResource, CachedGPUImageResourceHolder() {
+ private val expectedDimensions by lazy { holder.getExpectedDimensions() }
+
+ override fun scale(scale: ImageScale): BitmapImageResource {
+ val objScale = scale.calculateScalingFactorByOriginalDimensions(width, height)
+ val scaleContext = ScaleContext.of(arrayOf(
+ ScaleType.OBJ_SCALE.of(objScale),
+ ScaleType.USR_SCALE.of(1.0),
+ ScaleType.SYS_SCALE.of(1.0),
+ ))
+ val rawImage = holder.getImage(scaleContext, imageModifiers) ?: return EmptyBitmapImageResource
+ val awtImage = (rawImage as? JBHiDPIScaledImage)?.delegate ?: rawImage
+ return AwtImageResource(awtImage)
+ }
+
+ override fun calculateExpectedDimensions(scale: ImageScale): Bounds {
+ val ijScale = scale.calculateScalingFactorByOriginalDimensions(width, height)
+ return Bounds(0, 0, width = (width * ijScale).toInt(), height = (height * ijScale).toInt())
+ }
+
+ override val width: Int by lazy { expectedDimensions.width }
+ override val height: Int by lazy { expectedDimensions.height }
+}
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/IntelliJImageResourceProvider.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/IntelliJImageResourceProvider.kt
new file mode 100644
index 0000000000000..003831782addb
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/IntelliJImageResourceProvider.kt
@@ -0,0 +1,26 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering.images
+
+import org.jetbrains.icons.rendering.ImageModifiers
+import org.jetbrains.icons.rendering.ImageResource
+import org.jetbrains.icons.ImageResourceLocation
+import org.jetbrains.icons.impl.intellij.ModuleImageResourceLocation
+import org.jetbrains.icons.impl.intellij.rendering.ImageResourceLoaderExtension
+import org.jetbrains.icons.impl.rendering.DefaultImageResourceProvider
+import javax.swing.Icon
+
+class IntelliJImageResourceProvider: DefaultImageResourceProvider() {
+ override fun loadImage(location: ImageResourceLocation, imageModifiers: ImageModifiers?): ImageResource {
+ val loader = if (location is ModuleImageResourceLocation) {
+ ModuleImageResourceLoader()
+ } else {
+ ImageResourceLoaderExtension.getLoaderFor(location)
+ }
+ if (loader == null) error("Cannot find loader for location: $location")
+ return loader.loadGenericImage(location, imageModifiers) ?: error("Cannot load image for location: $location")
+ }
+
+ override fun fromSwingIcon(icon: Icon, imageModifiers: ImageModifiers?): ImageResource {
+ return IntelliJImageResource(LegacyIconImageResourceHolder(icon), imageModifiers)
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/LegacyIconImageResourceHolder.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/LegacyIconImageResourceHolder.kt
new file mode 100644
index 0000000000000..86e23d39b2d1b
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/LegacyIconImageResourceHolder.kt
@@ -0,0 +1,34 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering.images
+
+import com.intellij.openapi.util.IconLoader
+import com.intellij.ui.icons.CachedImageIcon
+import com.intellij.ui.icons.RgbImageFilterSupplier
+import com.intellij.ui.scale.ScaleContext
+import org.jetbrains.icons.rendering.Dimensions
+import org.jetbrains.icons.rendering.ImageModifiers
+import java.awt.Image
+import java.awt.image.RGBImageFilter
+import javax.swing.Icon
+
+internal class LegacyIconImageResourceHolder(
+ private val backingLegacyIcon: Icon
+): SwingImageResourceHolder {
+ override fun getExpectedDimensions(): Dimensions {
+ return Dimensions(backingLegacyIcon.iconWidth, backingLegacyIcon.iconHeight)
+ }
+
+ override fun getImage(scale: ScaleContext, imageModifiers: ImageModifiers?): Image? {
+ val params = imageModifiers.toLoadParameters()
+ val svgPatcher = params.colorPatcher
+ val icon = if (backingLegacyIcon is CachedImageIcon && svgPatcher != null) {
+ backingLegacyIcon.createWithPatcher(svgPatcher, isDark = params.isDark, useStroke = params.isStroke)
+ } else backingLegacyIcon
+ val filtered = if (params.filters.isNotEmpty()) {
+ IconLoader.filterIcon(icon = icon, filterSupplier = object : RgbImageFilterSupplier {
+ override fun getFilter() = params.filters.first() as RGBImageFilter
+ })
+ } else icon
+ return IconLoader.toImage(filtered, scale)
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/ModuleImageResourceLoader.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/ModuleImageResourceLoader.kt
new file mode 100644
index 0000000000000..e9305e485168b
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/ModuleImageResourceLoader.kt
@@ -0,0 +1,35 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering.images
+
+import com.intellij.ide.plugins.PluginManagerCore
+import com.intellij.ide.plugins.contentModules
+import com.intellij.openapi.extensions.PluginId
+import com.intellij.openapi.util.IntellijInternalApi
+import com.intellij.ui.icons.findIconLoaderByPath
+import org.jetbrains.icons.impl.intellij.ModuleImageResourceLocation
+import org.jetbrains.icons.rendering.ImageModifiers
+import org.jetbrains.icons.rendering.ImageResource
+import org.jetbrains.icons.rendering.ImageResourceLoader
+
+class ModuleImageResourceLoader: ImageResourceLoader {
+ override fun loadImage(
+ location: ModuleImageResourceLocation,
+ imageModifiers: ImageModifiers?,
+ ): ImageResource {
+ val classLoader = getClassLoader(location.pluginId, location.moduleId)
+ ?: error("Cannot recover classloader for plugin: ${location.pluginId} module: ${location.moduleId}")
+ val dataLoader = findIconLoaderByPath(location.path, classLoader)
+ return IntelliJImageResource(DataLoaderImageResourceHolder(dataLoader), imageModifiers)
+ }
+
+ @OptIn(IntellijInternalApi::class)
+ private fun getClassLoader(pluginId: String, moduleId: String?): ClassLoader? {
+ val plugin = PluginManagerCore.findPlugin(PluginId.Companion.getId(pluginId)) ?: return null
+ if (moduleId == null) {
+ return plugin.classLoader
+ }
+ else {
+ return plugin.contentModules.firstOrNull { it.moduleId.name == moduleId }?.classLoader
+ }
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/SwingImageResourceHolder.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/SwingImageResourceHolder.kt
new file mode 100644
index 0000000000000..47b7e4a7f7578
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/images/SwingImageResourceHolder.kt
@@ -0,0 +1,41 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering.images
+
+import com.intellij.ui.icons.LoadIconParameters
+import com.intellij.ui.scale.ScaleContext
+import org.jetbrains.icons.impl.intellij.rendering.toAwtFilter
+import org.jetbrains.icons.impl.intellij.rendering.toIJPatcher
+import org.jetbrains.icons.impl.rendering.DefaultImageModifiers
+import org.jetbrains.icons.modifiers.svgPatcher
+import org.jetbrains.icons.patchers.combineWith
+import org.jetbrains.icons.rendering.Dimensions
+import org.jetbrains.icons.rendering.ImageModifiers
+import java.awt.Image
+import java.awt.image.ImageFilter
+
+internal interface SwingImageResourceHolder {
+ fun getImage(scale: ScaleContext, imageModifiers: ImageModifiers?): Image?
+ fun getExpectedDimensions(): Dimensions
+}
+
+internal fun ImageModifiers?.toLoadParameters(): LoadIconParameters {
+ val filters = mutableListOf()
+ val colorFilter = this?.colorFilter
+ if (colorFilter != null) {
+ filters.add(colorFilter.toAwtFilter())
+ }
+ val knownModifiers = this as? DefaultImageModifiers
+ val strokePatcher = knownModifiers?.stroke?.let { stroke ->
+ svgPatcher {
+ replace("fill", "transparent")
+ add("stroke", stroke.toHex())
+ }
+ }
+ val colorPatcher = (this?.svgPatcher combineWith strokePatcher)?.toIJPatcher()
+ return LoadIconParameters(
+ filters = filters,
+ isDark = knownModifiers?.isDark ?: false,
+ colorPatcher = colorPatcher,
+ isStroke = knownModifiers?.stroke != null
+ )
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/toAwtColor.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/toAwtColor.kt
new file mode 100644
index 0000000000000..8ed0475bb784c
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/rendering/toAwtColor.kt
@@ -0,0 +1,12 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.rendering
+
+import org.jetbrains.icons.design.Color
+import org.jetbrains.icons.design.RGBA
+
+fun Color.toAwtColor(): java.awt.Color {
+ @Suppress("UseJBColor")
+ when (this) {
+ is RGBA -> return java.awt.Color(red, green, blue, alpha)
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/serializers/CachedImageIconSerializer.kt b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/serializers/CachedImageIconSerializer.kt
new file mode 100644
index 0000000000000..8481bd669758b
--- /dev/null
+++ b/platform/icons-impl/intellij/src/org/jetbrains/icons/impl/intellij/serializers/CachedImageIconSerializer.kt
@@ -0,0 +1,35 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij.serializers
+
+import com.intellij.ui.icons.CachedImageIcon
+import com.intellij.ui.icons.decodeCachedImageIconFromByteArray
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import org.jetbrains.icons.impl.intellij.custom.CustomLegacyIconSerializer
+
+class CustomCachedImageIconSerializer: CustomLegacyIconSerializer(CachedImageIcon::class) {
+ override fun createSerializer(): KSerializer = CachedImageIconSerializer
+}
+
+object CachedImageIconSerializer: KSerializer {
+ private val actualSerializer = SerializedIconDataHolder.serializer()
+
+ override val descriptor: SerialDescriptor = actualSerializer.descriptor
+
+ override fun serialize(encoder: Encoder, value: CachedImageIcon) {
+ actualSerializer.serialize(encoder, SerializedIconDataHolder(value.encodeToByteArray()))
+ }
+
+ override fun deserialize(decoder: Decoder): CachedImageIcon {
+ val byteArray = actualSerializer.deserialize(decoder).data
+ return decodeCachedImageIconFromByteArray(byteArray) as? CachedImageIcon ?: error("Unable to restore CachedImageIcon from byte array")
+ }
+}
+
+@Serializable
+class SerializedIconDataHolder(
+ val data: ByteArray
+)
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/tests/BUILD.bazel b/platform/icons-impl/intellij/tests/BUILD.bazel
new file mode 100644
index 0000000000000..5b098bec845f6
--- /dev/null
+++ b/platform/icons-impl/intellij/tests/BUILD.bazel
@@ -0,0 +1,75 @@
+### auto-generated section `build intellij.platform.icons.impl.intellij.tests` start
+load("@rules_jvm//:jvm.bzl", "jvm_library")
+
+jvm_library(
+ name = "tests",
+ visibility = ["//visibility:public"],
+ srcs = glob([], allow_empty = True),
+ exports = [
+ "//platform/icons-api",
+ "//platform/icons-api/rendering",
+ "//platform/icons-impl",
+ "//platform/icons-impl/intellij",
+ ],
+ runtime_deps = [
+ "@lib//:kotlin-stdlib",
+ "//libraries/kotlinx/coroutines/core",
+ "//platform/icons-api",
+ "//platform/icons-api/rendering",
+ "//platform/icons-impl",
+ "//platform/icons-impl/intellij",
+ ]
+)
+
+jvm_library(
+ name = "tests_test_lib",
+ module_name = "intellij.platform.icons.impl.intellij.tests",
+ visibility = ["//visibility:public"],
+ srcs = glob(["test/**/*.kt", "test/**/*.java", "test/**/*.form"], allow_empty = True),
+ deps = [
+ "@lib//:kotlin-stdlib",
+ "//libraries/kotlinx/coroutines/core",
+ "//libraries/kotlinx/coroutines/core:core_test_lib",
+ "//platform/icons-api",
+ "//platform/icons-api:icons-api_test_lib",
+ "//platform/icons-api/rendering",
+ "//platform/icons-api/rendering:rendering_test_lib",
+ "//platform/icons-impl",
+ "//platform/icons-impl:icons-impl_test_lib",
+ "//platform/icons-impl/intellij",
+ "//platform/icons-impl/intellij:intellij_test_lib",
+ "//libraries/junit5",
+ "//libraries/junit5:junit5_test_lib",
+ "//libraries/kotlinx/serialization/json",
+ "//libraries/kotlinx/serialization/json:json_test_lib",
+ "@lib//:assert_j",
+ "@lib//:junit5",
+ "//platform/testFramework/junit5",
+ "//platform/testFramework/junit5:junit5_test_lib",
+ "//platform/icons",
+ "//platform/icons:icons_test_lib",
+ "//platform/testFramework",
+ "//platform/testFramework:testFramework_test_lib",
+ ],
+ exports = [
+ "//platform/icons-api",
+ "//platform/icons-api:icons-api_test_lib",
+ "//platform/icons-api/rendering",
+ "//platform/icons-api/rendering:rendering_test_lib",
+ "//platform/icons-impl",
+ "//platform/icons-impl:icons-impl_test_lib",
+ "//platform/icons-impl/intellij",
+ "//platform/icons-impl/intellij:intellij_test_lib",
+ ],
+ runtime_deps = [":tests"]
+)
+### auto-generated section `build intellij.platform.icons.impl.intellij.tests` end
+
+### auto-generated section `test intellij.platform.icons.impl.intellij.tests` start
+load("@community//build:tests-options.bzl", "jps_test")
+
+jps_test(
+ name = "tests_test",
+ runtime_deps = [":tests_test_lib"]
+)
+### auto-generated section `test intellij.platform.icons.impl.intellij.tests` end
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/tests/intellij.platform.icons.impl.intellij.tests.iml b/platform/icons-impl/intellij/tests/intellij.platform.icons.impl.intellij.tests.iml
new file mode 100644
index 0000000000000..794e2ad44fe6f
--- /dev/null
+++ b/platform/icons-impl/intellij/tests/intellij.platform.icons.impl.intellij.tests.iml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ $USER_HOME$/Library/Caches/JetBrains/IntelliJIdea2025.3/kotlin-dist-for-ide/2.3.20-RC2/lib/kotlinx-serialization-compiler-plugin.jar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/platform/icons-impl/intellij/tests/test/org/jetbrains/icons/impl/intellij/IconTest.kt b/platform/icons-impl/intellij/tests/test/org/jetbrains/icons/impl/intellij/IconTest.kt
new file mode 100644
index 0000000000000..9c16d71f984d4
--- /dev/null
+++ b/platform/icons-impl/intellij/tests/test/org/jetbrains/icons/impl/intellij/IconTest.kt
@@ -0,0 +1,26 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.intellij
+
+import com.intellij.testFramework.junit5.TestApplication
+import org.jetbrains.icons.imageIcon
+import org.jetbrains.icons.swing.toSwingIcon
+import org.junit.jupiter.api.Test
+import java.awt.image.BufferedImage
+
+@TestApplication
+class IconTest {
+ @Test
+ fun `should render icon to buffered image`() {
+ IntelliJIconManager.activate()
+ val icon = imageIcon("actions/addFile.svg", IconTest::class.java.classLoader)
+
+ val swingIcon = icon.toSwingIcon()
+ val image = BufferedImage(swingIcon.iconWidth, swingIcon.iconHeight, BufferedImage.TYPE_INT_ARGB)
+ val g2 = image.createGraphics()
+ try {
+ swingIcon.paintIcon(null, g2, 0, 0)
+ } finally {
+ g2.dispose()
+ }
+ }
+}
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/DefaultDeferredIcon.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/DefaultDeferredIcon.kt
new file mode 100644
index 0000000000000..9b4bd57c77873
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/DefaultDeferredIcon.kt
@@ -0,0 +1,75 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.serializer
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.DeferredIcon
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.IconIdentifier
+import java.lang.ref.WeakReference
+
+@Serializable
+open class DefaultDeferredIcon(
+ override val id: IconIdentifier,
+ /**
+ * Placeholder that is renderer when the icon is not resolved yet.
+ * Keep in mind that implementation might change this to null when
+ * the icon is resolved to reduce memory footprint when serialized.
+ */
+ override var placeholder: Icon?
+): DeferredIcon {
+ @Transient
+ private val listeners = mutableListOf>()
+
+ @ApiStatus.Internal
+ fun addDoneListener(listener: DeferredIconEventHandler) {
+ listeners.add(WeakReference(listener))
+ }
+
+ @ApiStatus.Internal
+ fun markDone(resolvedIcon: Icon) {
+ this.placeholder = null
+ for (listener in listeners) {
+ listener.get()?.whenDone(this, resolvedIcon)
+ }
+ listeners.clear()
+ }
+
+}
+
+class DefaultDeferredIconSerializer(
+ private val manager: DefaultIconManager
+) : KSerializer {
+ private val delegate = DefaultDeferredIcon.serializer()
+
+ override val descriptor: SerialDescriptor = delegate.descriptor
+
+ override fun serialize(encoder: Encoder, value: DefaultDeferredIcon) {
+ delegate.serialize(encoder, value)
+ }
+
+ override fun deserialize(decoder: Decoder): DefaultDeferredIcon {
+ val result = delegate.deserialize(decoder)
+ return manager.registerDeserializedDeferredIcon(result)
+ }
+}
+
+interface DeferredIconEventHandler {
+ fun whenDone(deferredIcon: DeferredIcon, resolvedIcon: Icon)
+}
+
+/**
+ * Responsible for resolving deferred icons,
+ * and also synchronization between instances and backend/frontend.
+ */
+interface DeferredIconResolver {
+ val id: IconIdentifier
+ val deferredIcon: WeakReference
+ suspend fun resolve(): Icon
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/DefaultIconManager.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/DefaultIconManager.kt
new file mode 100644
index 0000000000000..3c5935360b87a
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/DefaultIconManager.kt
@@ -0,0 +1,143 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.descriptors.serialDescriptor
+import kotlinx.serialization.encoding.CompositeDecoder
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.encoding.decodeStructure
+import kotlinx.serialization.encoding.encodeStructure
+import kotlinx.serialization.modules.SerializersModule
+import kotlinx.serialization.modules.SerializersModuleBuilder
+import org.jetbrains.icons.DeferredIcon
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.IconIdentifier
+import org.jetbrains.icons.IconManager
+import org.jetbrains.icons.impl.layers.AnimatedIconLayer
+import org.jetbrains.icons.impl.layers.IconIconLayer
+import org.jetbrains.icons.impl.layers.ImageIconLayer
+import org.jetbrains.icons.impl.layers.LayoutIconLayer
+import org.jetbrains.icons.impl.layers.ShapeIconLayer
+import org.jetbrains.icons.modifiers.IconModifier
+import java.lang.ref.WeakReference
+import java.util.concurrent.atomic.AtomicInteger
+
+abstract class DefaultIconManager: IconManager {
+ protected abstract val resolverService: DeferredIconResolverService
+
+ private val deferredIconDeserializer by lazy {
+ DefaultDeferredIconSerializer(this)
+ }
+
+ override fun deferredIcon(placeholder: Icon?, identifier: String?, classLoader: ClassLoader?, evaluator: suspend () -> Icon): Icon {
+ return resolverService.getOrCreateDeferredIcon(
+ generateDeferredIconIdentifier(identifier, classLoader),
+ placeholder
+ ) { id, ref ->
+ createDeferredIconResolver(id, ref, evaluator)
+ }
+ }
+
+ internal fun registerDeserializedDeferredIcon(icon: DefaultDeferredIcon): DefaultDeferredIcon {
+ return resolverService.register(icon) { id, ref ->
+ createDeferredIconResolver(id, ref, null)
+ }
+ }
+
+ protected open fun generateDeferredIconIdentifier(id: String?, classLoader: ClassLoader? = null): IconIdentifier {
+ if (id != null) return StringIconIdentifier(id)
+ return StringIconIdentifier("dynamicIcon_" + dynamicIconNextId.getAndIncrement().toString())
+ }
+
+ override suspend fun forceEvaluation(icon: DeferredIcon): Icon {
+ return resolverService.forceEvaluation(icon)
+ }
+
+ fun scheduleEvaluation(icon: DeferredIcon) {
+ resolverService.scheduleEvaluation(icon)
+ }
+
+ protected open fun createDeferredIconResolver(
+ id: IconIdentifier,
+ ref: WeakReference,
+ evaluator: (suspend () -> Icon)?,
+ ): DeferredIconResolver {
+ if (evaluator == null) error("Evaluator is not specified for icon $id")
+ return InPlaceDeferredIconResolver(resolverService, id, ref, evaluator)
+ }
+
+ open fun SerializersModuleBuilder.buildCustomSerializers() {
+ // Add nothing by default
+ }
+ abstract suspend fun sendDeferredNotifications(id: IconIdentifier, result: Icon)
+ abstract fun markDeferredIconUnused(id: IconIdentifier)
+
+ override fun getSerializersModule(): SerializersModule {
+ return SerializersModule {
+ polymorphic(Icon::class, DefaultLayeredIcon::class, DefaultLayeredIcon.serializer())
+ polymorphic(Icon::class, DefaultDeferredIcon::class, deferredIconDeserializer)
+ polymorphic(DeferredIcon::class, DefaultDeferredIcon::class, deferredIconDeserializer)
+ polymorphic(
+ IconModifier::class,
+ IconModifier.Companion::class,
+ IconModifierConstSerializer
+ )
+ polymorphic(
+ IconIdentifier::class,
+ StringIconIdentifier::class,
+ StringIconIdentifier.serializer()
+ )
+
+ iconLayer(AnimatedIconLayer::class)
+ iconLayer(IconIconLayer::class)
+ iconLayer(ImageIconLayer::class)
+ iconLayer(LayoutIconLayer::class)
+ iconLayer(ShapeIconLayer::class)
+
+ buildCustomSerializers()
+ }
+ }
+
+ override fun toSwingIcon(icon: Icon): javax.swing.Icon {
+ error("Swing Icons are not supported.")
+ }
+
+ companion object {
+ fun getDefaultManagerInstance(): DefaultIconManager {
+ return IconManager.getInstance() as? DefaultIconManager ?: error("IconManager is not DefaultIconManager.")
+ }
+
+ private val dynamicIconNextId = AtomicInteger()
+ }
+}
+
+private object IconModifierConstSerializer : KSerializer {
+ override val descriptor: SerialDescriptor =
+ buildClassSerialDescriptor("IconModifier.Companion") {
+ element("isEmpty", serialDescriptor())
+ }
+
+ override fun serialize(encoder: Encoder, value: IconModifier.Companion) {
+ encoder.encodeStructure(descriptor) {
+ encodeBooleanElement(descriptor, 0, true)
+ }
+ }
+
+ override fun deserialize(decoder: Decoder): IconModifier.Companion {
+ var v: Boolean? = null
+ decoder.decodeStructure(descriptor) {
+ while (true) {
+ when (val index = decodeElementIndex(descriptor)) {
+ CompositeDecoder.DECODE_DONE -> break
+ 0 -> v = decodeBooleanElement(descriptor, 0)
+ else -> error("Unexpected element index: $index")
+ }
+ }
+ }
+ require(v == true) { "Unexpected value: '$v'" }
+ return IconModifier
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/DefaultLayeredIcon.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/DefaultLayeredIcon.kt
new file mode 100644
index 0000000000000..8e747ba3baf37
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/DefaultLayeredIcon.kt
@@ -0,0 +1,29 @@
+// 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.icons.impl
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.layers.IconLayer
+
+@Serializable
+class DefaultLayeredIcon(
+ val layers: List
+): Icon {
+ override fun toString(): String {
+ return "DefaultIcon(layers=$layers)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as DefaultLayeredIcon
+
+ return layers == other.layers
+ }
+
+ override fun hashCode(): Int {
+ return layers.hashCode()
+ }
+}
+
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/DeferredIconResolverService.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/DeferredIconResolverService.kt
new file mode 100644
index 0000000000000..962c2f18dcb64
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/DeferredIconResolverService.kt
@@ -0,0 +1,82 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.launch
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.DeferredIcon
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.IconIdentifier
+import java.lang.ref.ReferenceQueue
+import java.lang.ref.WeakReference
+import java.util.concurrent.ConcurrentHashMap
+import kotlin.time.Duration.Companion.seconds
+
+@ApiStatus.Internal
+open class DeferredIconResolverService(
+ protected val scope: CoroutineScope
+) {
+ protected val iconReferenceQueue = ReferenceQueue()
+ protected val resolvers = ConcurrentHashMap()
+
+ init {
+ scope.launch {
+ while (true) {
+ delay(5.seconds)
+ cleanUnusedIcons()
+ }
+ }
+ }
+
+ open fun getOrCreateDeferredIcon(
+ identifier: IconIdentifier,
+ placeholder: Icon?,
+ resolverBuilder: (IconIdentifier, WeakReference) -> DeferredIconResolver
+ ): Icon {
+ val resolver = resolvers.getOrPut(identifier) {
+ val icon = DefaultDeferredIcon(identifier, placeholder)
+ resolverBuilder(icon.id, IdentifiedDeferredIconWeakReference(icon, iconReferenceQueue))
+ }
+ return resolver?.deferredIcon?.get() ?: DefaultDeferredIcon(identifier, placeholder)
+ }
+
+ open fun register(
+ icon: DefaultDeferredIcon,
+ resolverBuilder: (IconIdentifier, WeakReference) -> DeferredIconResolver
+ ): DefaultDeferredIcon {
+ return resolvers.getOrPut(icon.id) {
+ resolverBuilder(icon.id, IdentifiedDeferredIconWeakReference(icon, iconReferenceQueue))
+ }?.deferredIcon?.get() ?: icon
+ }
+
+ open fun scheduleEvaluation(icon: DeferredIcon) {
+ scope.launch {
+ forceEvaluation(icon)
+ }
+ }
+
+ open suspend fun forceEvaluation(icon: DeferredIcon): Icon {
+ val resolver = resolvers[icon.id] ?: error("Cannot find resolver for icon: $icon")
+ return resolver.resolve()
+ }
+
+ open fun cleanIcon(id: IconIdentifier) {
+ resolvers.remove(id)
+ }
+
+ protected open fun cleanUnusedIcons() {
+ while (true) {
+ val reference = iconReferenceQueue.poll() ?: break
+ val id = (reference as IdentifiedDeferredIconWeakReference).id ?: continue
+ DefaultIconManager.getDefaultManagerInstance().markDeferredIconUnused(id)
+ }
+ }
+
+ protected open class IdentifiedDeferredIconWeakReference(
+ instance: DefaultDeferredIcon,
+ queue: ReferenceQueue,
+ ): WeakReference(instance, queue) {
+ val id = instance.id
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/IconAnimationFrame.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/IconAnimationFrame.kt
new file mode 100644
index 0000000000000..d7bc85c44cabb
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/IconAnimationFrame.kt
@@ -0,0 +1,33 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.icons.layers.IconLayer
+
+@Serializable
+class IconAnimationFrame(
+ val layers: List,
+ val duration: Long
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as IconAnimationFrame
+
+ if (duration != other.duration) return false
+ if (layers != other.layers) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = duration.hashCode()
+ result = 31 * result + layers.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "IconAnimationFrame(duration=$duration, layers=$layers)"
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/InPlaceDeferredIconResolver.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/InPlaceDeferredIconResolver.kt
new file mode 100644
index 0000000000000..7cdcd0237c28b
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/InPlaceDeferredIconResolver.kt
@@ -0,0 +1,36 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl
+
+import kotlinx.coroutines.CompletableDeferred
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.IconIdentifier
+import java.lang.ref.WeakReference
+import java.util.concurrent.atomic.AtomicBoolean
+
+@ApiStatus.Internal
+class InPlaceDeferredIconResolver(
+ val service: DeferredIconResolverService,
+ override val id: IconIdentifier,
+ override val deferredIcon: WeakReference,
+ val evaluator: suspend () -> Icon
+): DeferredIconResolver {
+ var resolvedIcon: Icon? = null
+ private val deferredValue = CompletableDeferred()
+ private val isPending = AtomicBoolean(false)
+
+ override suspend fun resolve(): Icon {
+ val resolved = resolvedIcon
+ if (resolved != null) return resolved
+ if (!isPending.getAndSet(true)) {
+ val result = evaluator()
+ deferredValue.complete(result)
+ resolvedIcon = result
+ deferredIcon.get()?.markDone(result)
+ DefaultIconManager.getDefaultManagerInstance().sendDeferredNotifications(id, result)
+ return result
+ }
+ return deferredValue.await()
+ }
+}
+
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/SerializersBuilder.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/SerializersBuilder.kt
new file mode 100644
index 0000000000000..8905b5cecda49
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/SerializersBuilder.kt
@@ -0,0 +1,14 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl
+
+import kotlinx.serialization.InternalSerializationApi
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.modules.SerializersModuleBuilder
+import kotlinx.serialization.serializer
+import org.jetbrains.icons.layers.IconLayer
+import kotlin.reflect.KClass
+
+@OptIn(InternalSerializationApi::class)
+fun SerializersModuleBuilder.iconLayer(klass: KClass, serializer: KSerializer? = null) {
+ polymorphic(IconLayer::class, klass, serializer ?: klass.serializer())
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/StringIconIdentifier.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/StringIconIdentifier.kt
new file mode 100644
index 0000000000000..85436758af324
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/StringIconIdentifier.kt
@@ -0,0 +1,27 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.icons.IconIdentifier
+
+@Serializable
+class StringIconIdentifier(
+ val value: String
+): IconIdentifier {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as StringIconIdentifier
+
+ return value == other.value
+ }
+
+ override fun hashCode(): Int {
+ return value.hashCode()
+ }
+
+ override fun toString(): String {
+ return value
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/design/DefaultIconAnimationDesigner.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/design/DefaultIconAnimationDesigner.kt
new file mode 100644
index 0000000000000..bd2c92a214676
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/design/DefaultIconAnimationDesigner.kt
@@ -0,0 +1,22 @@
+// 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.icons.impl.design
+
+import org.jetbrains.icons.design.IconAnimationDesigner
+import org.jetbrains.icons.design.IconDesigner
+import org.jetbrains.icons.impl.IconAnimationFrame
+
+class DefaultIconAnimationDesigner(
+ val rootDesigner: DefaultIconDesigner
+): IconAnimationDesigner {
+ private val frames = mutableListOf()
+
+ override fun frame(duration: Long, builder: IconDesigner.() -> Unit) {
+ val designer = rootDesigner.createNestedDesigner()
+ designer.builder()
+ frames.add(IconAnimationFrame(designer.buildLayers(), duration))
+ }
+
+ fun build(): List {
+ return frames
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/design/DefaultIconDesigner.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/design/DefaultIconDesigner.kt
new file mode 100644
index 0000000000000..00f5c71055abf
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/design/DefaultIconDesigner.kt
@@ -0,0 +1,70 @@
+// 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.icons.impl.design
+
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.design.Shape
+import org.jetbrains.icons.design.Color
+import org.jetbrains.icons.design.IconAnimationDesigner
+import org.jetbrains.icons.design.IconDesigner
+import org.jetbrains.icons.design.IconUnit
+import org.jetbrains.icons.modifiers.IconModifier
+import org.jetbrains.icons.impl.DefaultLayeredIcon
+import org.jetbrains.icons.impl.layers.AnimatedIconLayer
+import org.jetbrains.icons.impl.layers.IconIconLayer
+import org.jetbrains.icons.layers.IconLayer
+import org.jetbrains.icons.impl.layers.ImageIconLayer
+import org.jetbrains.icons.impl.layers.LayoutIconLayer
+import org.jetbrains.icons.ImageResourceLocation
+import org.jetbrains.icons.impl.layers.ShapeIconLayer
+
+abstract class DefaultIconDesigner: IconDesigner {
+ private val layers = mutableListOf()
+
+ override fun image(resourceLoader: ImageResourceLocation, modifier: IconModifier) {
+ layers.add(ImageIconLayer(resourceLoader, modifier))
+ }
+
+ override fun icon(icon: Icon, modifier: IconModifier) {
+ layers.add(IconIconLayer(icon, modifier))
+ }
+
+ override fun custom(iconLayer: IconLayer) {
+ layers.add(iconLayer)
+ }
+
+ override fun box(modifier: IconModifier, builder: IconDesigner.() -> Unit) {
+ layout(LayoutIconLayer.LayoutDirection.Box, IconUnit.Zero, modifier, builder)
+ }
+
+ override fun row(spacing: IconUnit, modifier: IconModifier, builder: IconDesigner.() -> Unit) {
+ layout(LayoutIconLayer.LayoutDirection.Row, spacing, modifier, builder)
+ }
+
+ override fun column(spacing: IconUnit, modifier: IconModifier, builder: IconDesigner.() -> Unit) {
+ layout(LayoutIconLayer.LayoutDirection.Column, spacing, modifier, builder)
+ }
+
+ private fun layout(direction: LayoutIconLayer.LayoutDirection, spacing: IconUnit, modifier: IconModifier, builder: IconDesigner.() -> Unit) {
+ val nestedIconDesigner = createNestedDesigner()
+ nestedIconDesigner.builder()
+ layers.add(LayoutIconLayer(nestedIconDesigner.buildLayers(), direction, spacing, modifier))
+ }
+
+ override fun animation(modifier: IconModifier, builder: IconAnimationDesigner.() -> Unit) {
+ val designer = DefaultIconAnimationDesigner(this)
+ designer.builder()
+ layers.add(AnimatedIconLayer(designer.build(), modifier))
+ }
+
+ override fun shape(color: Color, shape: Shape, modifier: IconModifier) {
+ layers.add(ShapeIconLayer(color, shape, modifier))
+ }
+
+ abstract fun createNestedDesigner(): DefaultIconDesigner
+
+ fun build(): DefaultLayeredIcon {
+ return DefaultLayeredIcon(buildLayers())
+ }
+
+ fun buildLayers(): List = layers.toList()
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/layers/AnimatedIconLayer.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/layers/AnimatedIconLayer.kt
new file mode 100644
index 0000000000000..f8af43a9d736e
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/layers/AnimatedIconLayer.kt
@@ -0,0 +1,35 @@
+// 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.icons.impl.layers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.icons.modifiers.IconModifier
+import org.jetbrains.icons.impl.IconAnimationFrame
+import org.jetbrains.icons.layers.IconLayer
+
+@Serializable
+class AnimatedIconLayer(
+ val frames: List,
+ override val modifier: IconModifier
+): IconLayer {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as AnimatedIconLayer
+
+ if (frames != other.frames) return false
+ if (modifier != other.modifier) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = frames.hashCode()
+ result = 31 * result + modifier.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "AnimatedIconLayer(frames=$frames, modifier=$modifier)"
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/layers/IconIconLayer.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/layers/IconIconLayer.kt
new file mode 100644
index 0000000000000..cf4245faf8c6e
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/layers/IconIconLayer.kt
@@ -0,0 +1,35 @@
+// 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.icons.impl.layers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.layers.IconLayer
+import org.jetbrains.icons.modifiers.IconModifier
+
+@Serializable
+class IconIconLayer(
+ val icon: Icon,
+ override val modifier: IconModifier
+): IconLayer {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as IconIconLayer
+
+ if (icon != other.icon) return false
+ if (modifier != other.modifier) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = icon.hashCode()
+ result = 31 * result + modifier.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "IconIconLayer(icon=$icon, modifier=$modifier)"
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/layers/IconLayerConstraints.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/layers/IconLayerConstraints.kt
new file mode 100644
index 0000000000000..9d9be222cc778
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/layers/IconLayerConstraints.kt
@@ -0,0 +1,44 @@
+// 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.icons.impl.layers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.icons.design.IconAlign
+import org.jetbrains.icons.design.IconMargin
+import org.jetbrains.icons.design.IconUnit
+
+@Serializable
+class IconLayerConstraints(
+ val align: IconAlign,
+ val width: IconUnit,
+ val height: IconUnit,
+ val margin: IconMargin,
+ val alpha: Float = 1.0f
+) {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as IconLayerConstraints
+
+ if (align != other.align) return false
+ if (width != other.width) return false
+ if (height != other.height) return false
+ if (margin != other.margin) return false
+ if (alpha != other.alpha) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = align.hashCode()
+ result = 31 * result + width.hashCode()
+ result = 31 * result + height.hashCode()
+ result = 31 * result + margin.hashCode()
+ result = 31 * result + alpha.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "IconLayerConstraints(align=$align, width=$width, height=$height, margin=$margin, opacity=$alpha)"
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/layers/ImageIconLayer.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/layers/ImageIconLayer.kt
new file mode 100644
index 0000000000000..40ec711a75ecb
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/layers/ImageIconLayer.kt
@@ -0,0 +1,35 @@
+// 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.icons.impl.layers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.icons.layers.IconLayer
+import org.jetbrains.icons.modifiers.IconModifier
+import org.jetbrains.icons.ImageResourceLocation
+
+@Serializable
+class ImageIconLayer(
+ val loader: ImageResourceLocation,
+ override val modifier: IconModifier
+) : IconLayer {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ImageIconLayer
+
+ if (loader != other.loader) return false
+ if (modifier != other.modifier) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = loader.hashCode()
+ result = 31 * result + modifier.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "ImageIconLayer(loader=$loader, modifier=$modifier)"
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/layers/LayoutIconLayer.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/layers/LayoutIconLayer.kt
new file mode 100644
index 0000000000000..9a6b3da829c72
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/layers/LayoutIconLayer.kt
@@ -0,0 +1,45 @@
+// 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.icons.impl.layers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.icons.design.IconUnit
+import org.jetbrains.icons.layers.IconLayer
+import org.jetbrains.icons.modifiers.IconModifier
+
+@Serializable
+class LayoutIconLayer(
+ val nestedLayers: List,
+ val direction: LayoutDirection,
+ val spacing: IconUnit,
+ override val modifier: IconModifier
+) : IconLayer {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as LayoutIconLayer
+
+ if (nestedLayers != other.nestedLayers) return false
+ if (direction != other.direction) return false
+ if (modifier != other.modifier) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = nestedLayers.hashCode()
+ result = 31 * result + direction.hashCode()
+ result = 31 * result + modifier.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "ColumnIconLayer(nestedLayers=$nestedLayers, direction=$direction, modifier=$modifier)"
+ }
+
+ enum class LayoutDirection {
+ Row,
+ Column,
+ Box
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/layers/ModifierHelpers.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/layers/ModifierHelpers.kt
new file mode 100644
index 0000000000000..d6ecdbdf70192
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/layers/ModifierHelpers.kt
@@ -0,0 +1,32 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.layers
+
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.layers.IconLayer
+import org.jetbrains.icons.modifiers.CombinedIconModifier
+import org.jetbrains.icons.modifiers.IconModifier
+
+@ApiStatus.Internal
+inline fun IconLayer.findModifier(): TModifier? {
+ var output: TModifier? = null
+ traverseModifiers(modifier) {
+ if (it is TModifier) {
+ output = it
+ return@traverseModifiers false
+ }
+ return@traverseModifiers true
+ }
+ return output
+}
+
+@ApiStatus.Internal
+fun traverseModifiers(modifier: IconModifier, traverser: (IconModifier) -> Boolean): Boolean {
+ if (traverser(modifier)) {
+ if (modifier is CombinedIconModifier) {
+ if (traverseModifiers(modifier.other, traverser)) {
+ return traverseModifiers(modifier.root, traverser)
+ } else return false
+ } else return true
+ }
+ return false
+}
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/layers/ShapeIconLayer.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/layers/ShapeIconLayer.kt
new file mode 100644
index 0000000000000..dd13333c49d6d
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/layers/ShapeIconLayer.kt
@@ -0,0 +1,40 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.layers
+
+import kotlinx.serialization.Serializable
+import org.jetbrains.icons.design.Shape
+import org.jetbrains.icons.design.Color
+import org.jetbrains.icons.layers.IconLayer
+import org.jetbrains.icons.modifiers.IconModifier
+
+@Serializable
+class ShapeIconLayer(
+ val color: Color,
+ val shape: Shape,
+ override val modifier: IconModifier
+): IconLayer {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ShapeIconLayer
+
+ if (color != other.color) return false
+ if (shape != other.shape) return false
+ if (modifier != other.modifier) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = color.hashCode()
+ result = 31 * result + shape.hashCode()
+ result = 31 * result + modifier.hashCode()
+ return result
+ }
+
+ override fun toString(): String {
+ return "BadgeIconLayer(color=$color, badgeShape=$shape, modifier=$modifier)"
+ }
+
+}
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/CachedGPUImageResourceHolder.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/CachedGPUImageResourceHolder.kt
new file mode 100644
index 0000000000000..599b4c09c67bd
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/CachedGPUImageResourceHolder.kt
@@ -0,0 +1,18 @@
+// 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.icons.impl.rendering
+
+import org.jetbrains.icons.rendering.lowlevel.GPUImageResourceHolder
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.ConcurrentMap
+import kotlin.reflect.KClass
+
+open class CachedGPUImageResourceHolder: GPUImageResourceHolder {
+ private val cache: ConcurrentMap, Any> = ConcurrentHashMap()
+
+ override fun getOrGenerateBitmap(bitmapClass: KClass, generator: () -> TBitmap): TBitmap {
+ val bitmap = cache.computeIfAbsent(bitmapClass) { generator() }
+ if (bitmap == null || !bitmapClass.isInstance(bitmap)) error("Unexpected type of cached bitmap")
+ @Suppress("UNCHECKED_CAST")
+ return bitmap as TBitmap
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/CoroutineBasedMutableIconUpdateFlow.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/CoroutineBasedMutableIconUpdateFlow.kt
new file mode 100644
index 0000000000000..f0102b588321a
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/CoroutineBasedMutableIconUpdateFlow.kt
@@ -0,0 +1,29 @@
+// 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.icons.impl.rendering
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.launch
+import org.jetbrains.icons.Icon
+
+class CoroutineBasedMutableIconUpdateFlow(
+ private val coroutineScope: CoroutineScope,
+ updateCallback: (Int) -> Unit
+): MutableIconUpdateFlowBase(updateCallback) {
+ override fun MutableSharedFlow.emitDelayed(delay: Long, value: Int) {
+ coroutineScope.launch {
+ delay(delay)
+ if (handleRateLimiting()) return@launch
+ emit(value)
+ updateCallback(value)
+ }
+ }
+
+ override fun collectDynamic(flow: Flow, handler: (Icon) -> Unit) {
+ coroutineScope.launch {
+ flow.collect { handler(it) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/DefaultDeferredIconRenderer.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/DefaultDeferredIconRenderer.kt
new file mode 100644
index 0000000000000..19aec75288f69
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/DefaultDeferredIconRenderer.kt
@@ -0,0 +1,49 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.rendering
+
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.DeferredIcon
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.IconManager
+import org.jetbrains.icons.impl.DefaultDeferredIcon
+import org.jetbrains.icons.impl.DefaultIconManager
+import org.jetbrains.icons.impl.DeferredIconEventHandler
+import org.jetbrains.icons.rendering.Dimensions
+import org.jetbrains.icons.rendering.IconRenderer
+import org.jetbrains.icons.rendering.LoadingStrategy
+import org.jetbrains.icons.rendering.PaintingApi
+import org.jetbrains.icons.rendering.RenderingContext
+import org.jetbrains.icons.rendering.ScalingContext
+import org.jetbrains.icons.rendering.createRenderer
+
+internal class DefaultDeferredIconRenderer(
+ override val icon: DefaultDeferredIcon,
+ val renderingContext: RenderingContext,
+ val loadingStrategy: LoadingStrategy
+): IconRenderer, DeferredIconEventHandler {
+ private var isDone = false
+ private var renderer = icon.placeholder?.createRenderer(renderingContext, loadingStrategy)
+
+ override fun whenDone(deferredIcon: DeferredIcon, resolvedIcon: Icon) {
+ val oldRenderer = renderer
+ val strategy = if (oldRenderer != null) {
+ LoadingStrategy.RenderPlaceholder(oldRenderer)
+ } else loadingStrategy
+ renderer = resolvedIcon.createRenderer(renderingContext, strategy)
+ isDone = true
+ renderingContext.updateFlow.triggerUpdate()
+ }
+
+ @ApiStatus.Internal
+ override fun render(api: PaintingApi) {
+ if (!isDone) {
+ DefaultIconManager.getDefaultManagerInstance().scheduleEvaluation(icon)
+ }
+ renderer?.render(api)
+ }
+
+ @ApiStatus.Internal
+ override fun calculateExpectedDimensions(scaling: ScalingContext): Dimensions {
+ return renderer?.calculateExpectedDimensions(scaling) ?: Dimensions(0, 0)
+ }
+}
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/DefaultIconRenderer.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/DefaultIconRenderer.kt
new file mode 100644
index 0000000000000..7f67a23de54ea
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/DefaultIconRenderer.kt
@@ -0,0 +1,52 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.rendering
+
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.rendering.Dimensions
+import org.jetbrains.icons.rendering.IconRenderer
+import org.jetbrains.icons.rendering.LoadingStrategy
+import org.jetbrains.icons.rendering.PaintingApi
+import org.jetbrains.icons.rendering.RenderingContext
+import org.jetbrains.icons.rendering.ScalingContext
+import org.jetbrains.icons.impl.DefaultLayeredIcon
+import org.jetbrains.icons.impl.rendering.layers.IconLayerRenderer
+import org.jetbrains.icons.impl.rendering.layers.IconLayerManager
+import kotlin.compareTo
+
+class DefaultIconRenderer(
+ val iconInstance: DefaultLayeredIcon,
+ private val context: RenderingContext,
+ private val loadingStrategy: LoadingStrategy
+) : IconRenderer {
+ override val icon: Icon = iconInstance
+ private var isLoaded = false
+ private val layerRenderers = createRenderers()
+
+ private fun createRenderers(): List {
+ val manager = IconLayerManager.getInstance()
+ val renderers = iconInstance.layers.map { manager.createRenderer(it, context) }
+ isLoaded = true
+ return renderers
+ }
+
+ override fun render(api: PaintingApi) {
+ for (layer in layerRenderers) {
+ layer.render(api)
+ }
+ }
+
+ override fun calculateExpectedDimensions(scaling: ScalingContext): Dimensions {
+ var width = 0
+ var height = 0
+ for (layer in layerRenderers) {
+ val dimensions = layer.calculateExpectedDimensions(scaling)
+ if (dimensions.width > width) {
+ width = dimensions.width
+ }
+ if (dimensions.height > height) {
+ height = dimensions.height
+ }
+ }
+ return Dimensions(width, height)
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/DefaultIconRendererManager.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/DefaultIconRendererManager.kt
new file mode 100644
index 0000000000000..cec0da3d961cb
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/DefaultIconRendererManager.kt
@@ -0,0 +1,72 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.rendering
+
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.rendering.IconRenderer
+import org.jetbrains.icons.rendering.IconRendererManager
+import org.jetbrains.icons.rendering.LoadingStrategy
+import org.jetbrains.icons.impl.layers.AnimatedIconLayer
+import org.jetbrains.icons.impl.DefaultLayeredIcon
+import org.jetbrains.icons.impl.layers.IconIconLayer
+import org.jetbrains.icons.impl.layers.ImageIconLayer
+import org.jetbrains.icons.impl.layers.LayoutIconLayer
+import org.jetbrains.icons.impl.rendering.layers.AnimatedIconLayerRenderer
+import org.jetbrains.icons.impl.rendering.layers.IconIconLayerRenderer
+import org.jetbrains.icons.layers.IconLayer
+import org.jetbrains.icons.impl.rendering.layers.IconLayerRenderer
+import org.jetbrains.icons.impl.rendering.layers.IconLayerManager
+import org.jetbrains.icons.rendering.RenderingContext
+import org.jetbrains.icons.impl.DefaultDeferredIcon
+import org.jetbrains.icons.impl.layers.ShapeIconLayer
+import org.jetbrains.icons.impl.rendering.layers.ShapeIconLayerRenderer
+import org.jetbrains.icons.impl.rendering.layers.ImageIconLayerRenderer
+import org.jetbrains.icons.impl.rendering.layers.LayoutIconLayerRenderer
+
+abstract class DefaultIconRendererManager: IconRendererManager, IconLayerManager {
+ init {
+ IconLayerManager.setInstance(this)
+ }
+
+ override fun createRenderer(icon: Icon, context: RenderingContext, loadingStrategy: LoadingStrategy): IconRenderer {
+ return createRendererOrNull(icon, context, loadingStrategy) ?: error("Unsupported icon type: $icon")
+ }
+
+ protected fun createRendererOrNull(icon: Icon, context: RenderingContext, loadingStrategy: LoadingStrategy): IconRenderer? {
+ return when (icon) {
+ is DefaultLayeredIcon -> DefaultIconRenderer(icon, context, loadingStrategy)
+ is DefaultDeferredIcon -> createDeferredIconRenderer(icon, context, loadingStrategy)
+ else -> null
+ }
+ }
+
+ private fun createDeferredIconRenderer(icon: DefaultDeferredIcon, context: RenderingContext, loadingStrategy: LoadingStrategy): IconRenderer {
+ val renderer = DefaultDeferredIconRenderer(icon, context, loadingStrategy)
+ icon.addDoneListener(renderer)
+ return renderer
+ }
+
+ override fun createRenderer(layer: IconLayer, renderingContext: RenderingContext): IconLayerRenderer {
+ return createRendererOrNull(layer, renderingContext) ?: error("Unsupported icon layer type: $layer")
+ }
+
+ protected fun createRendererOrNull(layer: IconLayer, renderingContext: RenderingContext): IconLayerRenderer? {
+ return when (layer) {
+ is ImageIconLayer -> {
+ ImageIconLayerRenderer(layer, renderingContext)
+ }
+ is IconIconLayer -> {
+ IconIconLayerRenderer(layer, renderingContext)
+ }
+ is LayoutIconLayer -> {
+ LayoutIconLayerRenderer(layer, renderingContext)
+ }
+ is AnimatedIconLayer -> {
+ AnimatedIconLayerRenderer(layer, renderingContext)
+ }
+ is ShapeIconLayer -> {
+ ShapeIconLayerRenderer(layer, renderingContext)
+ }
+ else -> null
+ }
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/DefaultImageModifiers.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/DefaultImageModifiers.kt
new file mode 100644
index 0000000000000..7bfa66f9f83bd
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/DefaultImageModifiers.kt
@@ -0,0 +1,42 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.rendering
+
+import org.jetbrains.icons.design.Color
+import org.jetbrains.icons.filters.ColorFilter
+import org.jetbrains.icons.patchers.SvgPatcher
+import org.jetbrains.icons.rendering.ImageModifiers
+
+class DefaultImageModifiers(
+ override val colorFilter: ColorFilter? = null,
+ override val svgPatcher: SvgPatcher? = null,
+ val isDark: Boolean = false,
+ val stroke: Color? = null
+): ImageModifiers {
+ override fun toString(): String {
+ return "DefaultImageModifiers(colorFilter=$colorFilter, svgPatcher=$svgPatcher, isDark=$isDark, stroke=$stroke)"
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as DefaultImageModifiers
+
+ if (isDark != other.isDark) return false
+ if (colorFilter != other.colorFilter) return false
+ if (svgPatcher != other.svgPatcher) return false
+ if (stroke != other.stroke) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = isDark.hashCode()
+ result = 31 * result + (colorFilter?.hashCode() ?: 0)
+ result = 31 * result + (svgPatcher?.hashCode() ?: 0)
+ result = 31 * result + (stroke?.hashCode() ?: 0)
+ return result
+ }
+
+
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/DefaultImageResourceProvider.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/DefaultImageResourceProvider.kt
new file mode 100644
index 0000000000000..e87e0ee7caeab
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/DefaultImageResourceProvider.kt
@@ -0,0 +1,11 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.rendering
+
+import org.jetbrains.icons.rendering.ImageModifiers
+import org.jetbrains.icons.rendering.ImageResource
+import org.jetbrains.icons.rendering.ImageResourceProvider
+import javax.swing.Icon
+
+abstract class DefaultImageResourceProvider: ImageResourceProvider {
+ abstract fun fromSwingIcon(icon: Icon, imageModifiers: ImageModifiers? = null): ImageResource
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/MutableIconUpdateFlowBase.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/MutableIconUpdateFlowBase.kt
new file mode 100644
index 0000000000000..f8ac597c2fddd
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/MutableIconUpdateFlowBase.kt
@@ -0,0 +1,39 @@
+// 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.icons.impl.rendering
+
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.MutableSharedFlow
+import org.jetbrains.icons.rendering.MutableIconUpdateFlow
+import java.util.concurrent.atomic.AtomicInteger
+
+abstract class MutableIconUpdateFlowBase(
+ protected val updateCallback: (Int) -> Unit
+): MutableIconUpdateFlow {
+ private val updateCounter = AtomicInteger()
+ private val underlayingFlow = MutableSharedFlow()
+ protected var stopwatch = System.currentTimeMillis()
+ protected val minimalUpdateMillis = 1000L / 60L // Use actual fps?
+
+ protected fun handleRateLimiting(): Boolean {
+ if (System.currentTimeMillis() - stopwatch < minimalUpdateMillis) return true
+ stopwatch = System.currentTimeMillis()
+ return false
+ }
+
+ override fun triggerUpdate() {
+ if (handleRateLimiting()) return
+ val updateId = updateCounter.incrementAndGet()
+ underlayingFlow.tryEmit(updateId)
+ updateCallback(updateId)
+ }
+
+ override fun triggerDelayedUpdate(delay: Long) {
+ underlayingFlow.emitDelayed(delay, updateCounter.incrementAndGet())
+ }
+
+ protected abstract fun MutableSharedFlow.emitDelayed(delay: Long, value: Int)
+
+ override suspend fun collect(collector: FlowCollector) {
+ underlayingFlow.collect(collector)
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/AnimatedIconLayerRenderer.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/AnimatedIconLayerRenderer.kt
new file mode 100644
index 0000000000000..baf7e7d0b677b
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/AnimatedIconLayerRenderer.kt
@@ -0,0 +1,89 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.rendering.layers
+
+import org.jetbrains.icons.rendering.Bounds
+import org.jetbrains.icons.rendering.Dimensions
+import org.jetbrains.icons.rendering.PaintingApi
+import org.jetbrains.icons.rendering.RenderingContext
+import org.jetbrains.icons.rendering.ScalingContext
+import org.jetbrains.icons.impl.layers.AnimatedIconLayer
+import org.jetbrains.icons.impl.rendering.layers.IconLayerManager
+import org.jetbrains.icons.impl.IconAnimationFrame
+import org.jetbrains.icons.impl.rendering.modifiers.applyTo
+
+class AnimatedIconLayerRenderer(
+ private val layer: AnimatedIconLayer,
+ private val renderingContext: RenderingContext
+) : IconLayerRenderer {
+ private val currentContext = renderingContext.adjustTo(layer)
+ private val frameRenderers = layer.frames.map { AnimatedIconFrameRenderer(it, currentContext) }
+ private var lastFrame = FrameData(System.currentTimeMillis(), 0, 0)
+
+ override fun render(api: PaintingApi) {
+ val layout = DefaultLayerLayout(
+ Bounds(
+ 0,
+ 0,
+ api.bounds.width,
+ api.bounds.height,
+ ),
+ api.bounds
+ )
+ val nestedLayerApi = api.withCustomContext(layer.modifier.applyTo(layout, api.scaling).calculateFinalBounds())
+ val currentFrameData = calculateAndSetNewFrameData()
+ frameRenderers[currentFrameData.frame].render(nestedLayerApi)
+ if (currentFrameData.remainingDuration > 0L) {
+ renderingContext.updateFlow.triggerDelayedUpdate(currentFrameData.remainingDuration)
+ }
+ }
+
+ override fun calculateExpectedDimensions(scaling: ScalingContext): Dimensions {
+ return frameRenderers[lastFrame.frame].calculateExpectedDimensions(scaling)
+ }
+
+ private fun calculateAndSetNewFrameData(): FrameData {
+ val currentLastFrame = lastFrame
+ val elapsedMillis = System.currentTimeMillis() - lastFrame.timestamp
+ if (elapsedMillis > currentLastFrame.remainingDuration) {
+ val index = (currentLastFrame.frame + 1) % frameRenderers.size
+ val remaining = frameRenderers[index].frame.duration
+ val newData = FrameData(System.currentTimeMillis(), index, remaining)
+ lastFrame = newData
+ return newData
+ } else return lastFrame
+ }
+
+ private class FrameData(
+ val timestamp: Long,
+ val frame: Int,
+ val remainingDuration: Long
+ )
+
+ private class AnimatedIconFrameRenderer(
+ val frame: IconAnimationFrame,
+ renderingContext: RenderingContext
+ ) {
+ private val renderers = IconLayerManager.createRenderers(frame.layers, renderingContext)
+
+ fun render(api: PaintingApi) {
+ for (layer in renderers) {
+ layer.render(api)
+ }
+ }
+
+ fun calculateExpectedDimensions(scalingContext: ScalingContext): Dimensions {
+ var width = 0
+ var height = 0
+ for (layer in renderers) {
+ val dimensions = layer.calculateExpectedDimensions(scalingContext)
+ if (dimensions.width > width) {
+ width = dimensions.width
+ }
+ if (dimensions.height > height) {
+ height = dimensions.height
+ }
+ }
+ return Dimensions(width, height)
+ }
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/DefaultLayerLayout.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/DefaultLayerLayout.kt
new file mode 100644
index 0000000000000..0a9160b7e7555
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/DefaultLayerLayout.kt
@@ -0,0 +1,91 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.rendering.layers
+
+import org.jetbrains.icons.design.Color
+import org.jetbrains.icons.filters.ColorFilter
+import org.jetbrains.icons.rendering.Bounds
+import kotlin.hashCode
+
+interface LayerLayout {
+ val layerBounds: Bounds
+ val parentBounds: Bounds
+ val colorFilter: ColorFilter?
+ val alpha: Float
+ val cutoutMargin: Float?
+ val stroke: Color?
+
+ fun copy(
+ layerBounds: Bounds = this.layerBounds,
+ parentBounds: Bounds = this.parentBounds,
+ colorFilter: ColorFilter? = this.colorFilter,
+ alpha: Float = this.alpha,
+ cutoutMargin: Float? = this.cutoutMargin,
+ stroke: Color? = this.stroke
+ ): LayerLayout
+
+ fun calculateFinalBounds(): Bounds
+}
+
+open class DefaultLayerLayout(
+ override val layerBounds: Bounds,
+ override val parentBounds: Bounds,
+ override val colorFilter: ColorFilter? = null,
+ override val alpha: Float = 1f,
+ override val cutoutMargin: Float? = null,
+ override val stroke: Color? = null
+): LayerLayout {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as DefaultLayerLayout
+
+ if (alpha != other.alpha) return false
+ if (layerBounds != other.layerBounds) return false
+ if (parentBounds != other.parentBounds) return false
+ if (colorFilter != other.colorFilter) return false
+ if (cutoutMargin != other.cutoutMargin) return false
+ if (stroke != other.stroke) return false
+
+ return true
+ }
+
+ override fun hashCode(): Int {
+ var result = alpha.hashCode()
+ result = 31 * result + layerBounds.hashCode()
+ result = 31 * result + parentBounds.hashCode()
+ result = 31 * result + (colorFilter?.hashCode() ?: 0)
+ return result
+ }
+
+ override fun toString(): String {
+ return "LayerLayout(layerBounds=$layerBounds, parentBounds=$parentBounds, colorFilter=$colorFilter, alpha=$alpha, stroke=$stroke)"
+ }
+
+ override fun calculateFinalBounds(): Bounds {
+ return Bounds(
+ layerBounds.x + parentBounds.x,
+ layerBounds.y + parentBounds.y,
+ layerBounds.width,
+ layerBounds.height
+ )
+ }
+
+ override fun copy(
+ layerBounds: Bounds,
+ parentBounds: Bounds,
+ colorFilter: ColorFilter?,
+ alpha: Float,
+ cutoutMargin: Float?,
+ stroke: Color?
+ ): DefaultLayerLayout {
+ return DefaultLayerLayout(
+ layerBounds,
+ parentBounds,
+ colorFilter,
+ alpha,
+ cutoutMargin,
+ stroke
+ )
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/IconIconLayerRenderer.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/IconIconLayerRenderer.kt
new file mode 100644
index 0000000000000..a5f0b3f0a446b
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/IconIconLayerRenderer.kt
@@ -0,0 +1,37 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.rendering.layers
+
+import org.jetbrains.icons.rendering.Bounds
+import org.jetbrains.icons.rendering.Dimensions
+import org.jetbrains.icons.rendering.PaintingApi
+import org.jetbrains.icons.rendering.RenderingContext
+import org.jetbrains.icons.rendering.ScalingContext
+import org.jetbrains.icons.rendering.createRenderer
+import org.jetbrains.icons.impl.layers.IconIconLayer
+import org.jetbrains.icons.impl.rendering.modifiers.applyTo
+
+class IconIconLayerRenderer(
+ private val layer: IconIconLayer,
+ private val renderingContext: RenderingContext
+) : IconLayerRenderer {
+ private val renderer = layer.icon.createRenderer(renderingContext.adjustTo(layer))
+
+ override fun render(api: PaintingApi) {
+ val layout = DefaultLayerLayout(
+ Bounds(
+ 0,
+ 0,
+ api.bounds.width,
+ api.bounds.height,
+ ),
+ api.bounds
+ )
+ val appliedLayout = layer.modifier.applyTo(layout, api.scaling)
+ val boundApi = api.withCustomContext(appliedLayout.calculateFinalBounds())
+ renderer.render(boundApi)
+ }
+
+ override fun calculateExpectedDimensions(scaling: ScalingContext): Dimensions {
+ return renderer.calculateExpectedDimensions(scaling)
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/IconLayerManager.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/IconLayerManager.kt
new file mode 100644
index 0000000000000..c03a6d15c9690
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/IconLayerManager.kt
@@ -0,0 +1,29 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.rendering.layers
+
+import org.jetbrains.icons.layers.IconLayer
+import org.jetbrains.icons.rendering.RenderingContext
+
+interface IconLayerManager {
+ fun createRenderer(layer: IconLayer, renderingContext: RenderingContext): IconLayerRenderer
+
+ companion object {
+ @Volatile
+ private var instance: IconLayerManager? = null
+
+ @JvmStatic
+ fun getInstance(): IconLayerManager = instance ?: error("IconLayerRendererManager is not initialized")
+
+ fun setInstance(manager: IconLayerManager) {
+ instance = manager
+ }
+
+ fun createRenderers(
+ layers: List,
+ renderingContext: RenderingContext,
+ ): List {
+ val instance = getInstance()
+ return layers.map { instance.createRenderer(it, renderingContext) }
+ }
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/IconLayerRenderer.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/IconLayerRenderer.kt
new file mode 100644
index 0000000000000..a5f9ff45ebfc8
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/IconLayerRenderer.kt
@@ -0,0 +1,12 @@
+// 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.icons.impl.rendering.layers
+
+import org.jetbrains.icons.rendering.Dimensions
+import org.jetbrains.icons.rendering.PaintingApi
+import org.jetbrains.icons.rendering.ScalingContext
+
+interface IconLayerRenderer {
+ fun render(api: PaintingApi)
+ fun calculateExpectedDimensions(scaling: ScalingContext): Dimensions
+}
+
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/ImageIconLayerRenderer.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/ImageIconLayerRenderer.kt
new file mode 100644
index 0000000000000..4ab215d400cee
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/ImageIconLayerRenderer.kt
@@ -0,0 +1,48 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.rendering.layers
+
+import org.jetbrains.icons.rendering.Bounds
+import org.jetbrains.icons.rendering.Dimensions
+import org.jetbrains.icons.rendering.ImageResource
+import org.jetbrains.icons.rendering.PaintingApi
+import org.jetbrains.icons.rendering.RenderingContext
+import org.jetbrains.icons.rendering.ScalingContext
+import org.jetbrains.icons.rendering.imageResource
+import org.jetbrains.icons.layers.IconLayer
+import org.jetbrains.icons.impl.layers.ImageIconLayer
+import org.jetbrains.icons.impl.rendering.layers.applyTo
+import org.jetbrains.icons.impl.rendering.modifiers.applyTo
+
+class ImageIconLayerRenderer(
+ override val layer: ImageIconLayer,
+ override val renderingContext: RenderingContext
+) : BaseImageIconLayerRenderer() {
+ override var image: ImageResource = imageResource(layer.loader, layer.generateImageModifiers(renderingContext))
+ override fun calculateExpectedDimensions(scaling: ScalingContext): Dimensions {
+ return Dimensions(scaling.applyTo(image.width ?: 16), scaling.applyTo(image.height ?: 16))
+ }
+}
+
+abstract class BaseImageIconLayerRenderer: IconLayerRenderer {
+ abstract var image: ImageResource
+ abstract val layer: IconLayer
+ protected abstract val renderingContext: RenderingContext
+
+ override fun render(api: PaintingApi) {
+ val currentImage = image
+ val w = api.scaling.applyTo(currentImage.width)
+ val h = api.scaling.applyTo(currentImage.height)
+ val layout = DefaultLayerLayout(
+ Bounds(
+ 0,
+ 0,
+ api.bounds.width.coerceAtMost(w ?: Integer.MAX_VALUE),
+ api.bounds.height.coerceAtMost(h ?: Integer.MAX_VALUE)
+ ),
+ api.bounds
+ )
+ val appliedLayout = layer.modifier.applyTo(layout, api.scaling)
+ val finalBounds = appliedLayout.calculateFinalBounds()
+ api.drawImage(currentImage, finalBounds.x, finalBounds.y, finalBounds.width, finalBounds.height, alpha = appliedLayout.alpha)
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/LayerHelpers.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/LayerHelpers.kt
new file mode 100644
index 0000000000000..aa159c4651b0a
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/LayerHelpers.kt
@@ -0,0 +1,41 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.rendering.layers
+
+import org.jetbrains.icons.modifiers.ColorFilterModifier
+import org.jetbrains.icons.modifiers.StrokeModifier
+import org.jetbrains.icons.modifiers.SvgPatcherModifier
+import org.jetbrains.icons.rendering.ImageModifiers
+import org.jetbrains.icons.rendering.RenderingContext
+import org.jetbrains.icons.rendering.ScalingContext
+import org.jetbrains.icons.layers.IconLayer
+import org.jetbrains.icons.impl.layers.findModifier
+import org.jetbrains.icons.impl.rendering.DefaultImageModifiers
+import kotlin.math.ceil
+
+fun IconLayer.generateImageModifiers(renderingContext: RenderingContext? = null): ImageModifiers {
+ val defaults = renderingContext?.defaultImageModifiers
+ val knownDefaults = defaults as? DefaultImageModifiers
+ return DefaultImageModifiers(
+ colorFilter = findModifier()?.colorFilter ?: defaults?.colorFilter,
+ svgPatcher = findModifier()?.svgPatcher ?: defaults?.svgPatcher,
+ isDark = knownDefaults?.isDark ?: false,
+ stroke = findModifier()?.color ?: knownDefaults?.stroke
+ )
+}
+
+fun RenderingContext.adjustTo(layer: IconLayer): RenderingContext {
+ return copy(defaultImageModifiers = layer.generateImageModifiers(this))
+}
+
+fun ScalingContext.applyTo(px: Int): Int {
+ return applyTo(px.toDouble())
+}
+
+fun ScalingContext.applyTo(px: Double): Int {
+ return ceil(px * display).toInt()
+}
+
+fun ScalingContext.applyTo(px: Int?): Int? {
+ if (px == null) return null
+ return applyTo(px)
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/LayoutIconLayerRenderer.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/LayoutIconLayerRenderer.kt
new file mode 100644
index 0000000000000..21f5c4debaf39
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/LayoutIconLayerRenderer.kt
@@ -0,0 +1,100 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.rendering.layers
+
+import org.jetbrains.icons.rendering.Bounds
+import org.jetbrains.icons.rendering.Dimensions
+import org.jetbrains.icons.rendering.PaintingApi
+import org.jetbrains.icons.rendering.RenderingContext
+import org.jetbrains.icons.rendering.ScalingContext
+import org.jetbrains.icons.impl.rendering.layers.IconLayerManager
+import org.jetbrains.icons.impl.layers.LayoutIconLayer
+import org.jetbrains.icons.impl.rendering.modifiers.applyTo
+import org.jetbrains.icons.impl.rendering.modifiers.asPixels
+import kotlin.compareTo
+
+class LayoutIconLayerRenderer(
+ private val layoutLayer: LayoutIconLayer,
+ renderingContext: RenderingContext,
+) : IconLayerRenderer {
+ private val currentContext = renderingContext.adjustTo(layoutLayer)
+ private val nestedRenderers = createNestedRenderers(layoutLayer)
+
+ private fun createNestedRenderers(layer: LayoutIconLayer): List {
+ return IconLayerManager.createRenderers(layer.nestedLayers, currentContext)
+ }
+
+ override fun render(api: PaintingApi) {
+ val layout = DefaultLayerLayout(
+ Bounds(
+ 0,
+ 0,
+ api.bounds.width,
+ api.bounds.height,
+ ),
+ api.bounds,
+ )
+ val appliedLayout = layoutLayer.modifier.applyTo(layout, api.scaling)
+ val finalBounds = appliedLayout.calculateFinalBounds()
+
+ when (layoutLayer.direction) {
+ LayoutIconLayer.LayoutDirection.Row -> {
+ val spacingPx = layoutLayer.spacing.asPixels(api.scaling, api.bounds, true)
+ val remainingSize = finalBounds.width - spacingPx * (nestedRenderers.count() - 1)
+ val size = remainingSize / nestedRenderers.count()
+ var offset = 0
+ for (nestedRenderer in nestedRenderers) {
+ val nestedApi = api.withCustomContext(Bounds(finalBounds.x + offset, finalBounds.y, size, finalBounds.height))
+ nestedRenderer.render(nestedApi)
+ offset += size + spacingPx
+ }
+ }
+ LayoutIconLayer.LayoutDirection.Column -> {
+ val spacingPx = layoutLayer.spacing.asPixels(api.scaling, api.bounds, false)
+ val remainingSize = finalBounds.height - spacingPx * (nestedRenderers.count() - 1)
+ val size = remainingSize / nestedRenderers.count()
+ var offset = 0
+ for (nestedRenderer in nestedRenderers) {
+ val nestedApi = api.withCustomContext(Bounds(finalBounds.x, finalBounds.y + offset, finalBounds.width, size))
+ nestedRenderer.render(nestedApi)
+ offset += size + spacingPx
+ }
+ }
+ else -> {
+ for (nestedRenderer in nestedRenderers) {
+ nestedRenderer.render(api)
+ }
+ }
+ }
+ }
+
+ override fun calculateExpectedDimensions(scaling: ScalingContext): Dimensions {
+ var width = 0
+ var height = 0
+ for (layer in nestedRenderers) {
+ val dimensions = layer.calculateExpectedDimensions(scaling)
+ when (layoutLayer.direction) {
+ LayoutIconLayer.LayoutDirection.Row -> {
+ width += dimensions.width
+ if (dimensions.height > height) {
+ height = dimensions.height
+ }
+ }
+ LayoutIconLayer.LayoutDirection.Column -> {
+ height += dimensions.height
+ if (dimensions.width > width) {
+ width = dimensions.width
+ }
+ }
+ else -> {
+ if (dimensions.width > width) {
+ width = dimensions.width
+ }
+ if (dimensions.height > height) {
+ height = dimensions.height
+ }
+ }
+ }
+ }
+ return Dimensions(width, height)
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/ShapeIconLayerRenderer.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/ShapeIconLayerRenderer.kt
new file mode 100644
index 0000000000000..b7a8f895cdb10
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/layers/ShapeIconLayerRenderer.kt
@@ -0,0 +1,70 @@
+// 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.icons.impl.rendering.layers
+
+import org.jetbrains.icons.design.Circle
+import org.jetbrains.icons.design.Rectangle
+import org.jetbrains.icons.rendering.Bounds
+import org.jetbrains.icons.rendering.Dimensions
+import org.jetbrains.icons.rendering.DrawMode
+import org.jetbrains.icons.rendering.PaintingApi
+import org.jetbrains.icons.rendering.RenderingContext
+import org.jetbrains.icons.rendering.ScalingContext
+import org.jetbrains.icons.impl.layers.ShapeIconLayer
+import org.jetbrains.icons.impl.rendering.modifiers.applyTo
+import kotlin.math.roundToInt
+
+class ShapeIconLayerRenderer(
+ val layer: ShapeIconLayer,
+ val renderingContext: RenderingContext
+) : IconLayerRenderer {
+ override fun render(api: PaintingApi) {
+ val layout = DefaultLayerLayout(
+ Bounds(
+ 0,
+ 0,
+ api.bounds.width,
+ api.bounds.height,
+ ),
+ api.bounds
+ )
+ val appliedLayout = layer.modifier.applyTo(layout, api.scaling)
+ val finalBounds = appliedLayout.calculateFinalBounds()
+ val cutoutMargin = appliedLayout.cutoutMargin ?: 0f
+ when (layer.shape) {
+ Circle -> {
+ val radius = finalBounds.width.coerceAtMost(finalBounds.height) / 2.0f
+ val roundRadius = radius.roundToInt()
+ if (cutoutMargin > 0f) {
+ api.drawCircle(
+ layer.color,
+ finalBounds.x + roundRadius,
+ finalBounds.y + roundRadius,
+ radius + cutoutMargin + 1,
+ 1f,
+ DrawMode.Clear
+ )
+ }
+ api.drawCircle(layer.color, finalBounds.x + roundRadius, finalBounds.y + roundRadius, radius, 1f)
+ }
+ Rectangle -> {
+ if (cutoutMargin > 0f) {
+ val cutoutMargin2 = cutoutMargin + cutoutMargin
+ api.drawRect(
+ layer.color,
+ finalBounds.x - cutoutMargin.roundToInt(),
+ finalBounds.y - cutoutMargin.roundToInt(),
+ cutoutMargin2.roundToInt(),
+ cutoutMargin2.roundToInt(),
+ 1f,
+ DrawMode.Clear
+ )
+ }
+ api.drawRect(layer.color, finalBounds.x, finalBounds.y, finalBounds.width, finalBounds.height, 1f)
+ }
+ }
+ }
+
+ override fun calculateExpectedDimensions(scaling: ScalingContext): Dimensions {
+ return Dimensions(0, 0)
+ }
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/modifiers/ColorFilterModifier.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/modifiers/ColorFilterModifier.kt
new file mode 100644
index 0000000000000..362d7268986b3
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/modifiers/ColorFilterModifier.kt
@@ -0,0 +1,9 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.rendering.modifiers
+
+import org.jetbrains.icons.modifiers.ColorFilterModifier
+import org.jetbrains.icons.impl.rendering.layers.DefaultLayerLayout
+
+internal fun applyColorFilterModifier(modifier: ColorFilterModifier, layout: DefaultLayerLayout, displayScale: Float): DefaultLayerLayout {
+ return layout.copy(colorFilter = modifier.colorFilter)
+}
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/modifiers/IconModifier.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/modifiers/IconModifier.kt
new file mode 100644
index 0000000000000..fc652da4539ed
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/modifiers/IconModifier.kt
@@ -0,0 +1,120 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.rendering.modifiers
+
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.design.MaxIconUnit
+import org.jetbrains.icons.design.DisplayPointIconUnit
+import org.jetbrains.icons.design.IconUnit
+import org.jetbrains.icons.design.IconVerticalAlign
+import org.jetbrains.icons.design.IconHorizontalAlign
+import org.jetbrains.icons.design.PercentIconUnit
+import org.jetbrains.icons.design.PixelIconUnit
+import org.jetbrains.icons.modifiers.AlignIconModifier
+import org.jetbrains.icons.modifiers.AlphaIconModifier
+import org.jetbrains.icons.modifiers.ColorFilterModifier
+import org.jetbrains.icons.modifiers.CombinedIconModifier
+import org.jetbrains.icons.modifiers.CutoutMarginModifier
+import org.jetbrains.icons.modifiers.HeightIconModifier
+import org.jetbrains.icons.modifiers.IconModifier
+import org.jetbrains.icons.modifiers.MarginIconModifier
+import org.jetbrains.icons.modifiers.StrokeModifier
+import org.jetbrains.icons.modifiers.SvgPatcherModifier
+import org.jetbrains.icons.modifiers.WidthIconModifier
+import org.jetbrains.icons.rendering.Bounds
+import org.jetbrains.icons.rendering.ScalingContext
+import org.jetbrains.icons.impl.rendering.layers.LayerLayout
+import org.jetbrains.icons.impl.rendering.layers.applyTo
+import kotlin.math.roundToInt
+
+@ApiStatus.Internal
+fun IconModifier.applyTo(layout: LayerLayout, scaling: ScalingContext): LayerLayout {
+ return when (this) {
+ is CombinedIconModifier -> {
+ other.applyTo(root.applyTo(layout, scaling), scaling)
+ }
+ is WidthIconModifier -> {
+ layout.copy(
+ layerBounds = layout.layerBounds.copy(
+ width = width.asPixels(scaling, layout.parentBounds, true)
+ )
+ )
+ }
+ is HeightIconModifier -> {
+ layout.copy(
+ layerBounds = layout.layerBounds.copy(
+ height = height.asPixels(scaling, layout.parentBounds, false)
+ )
+ )
+ }
+ is AlignIconModifier -> {
+ val x = when (align.horizontalAlign) {
+ IconHorizontalAlign.Left -> {
+ layout.layerBounds.x
+ }
+ IconHorizontalAlign.Right -> {
+ layout.parentBounds.width - layout.layerBounds.width
+ }
+ IconHorizontalAlign.Center -> {
+ (layout.parentBounds.width / 2) - (layout.layerBounds.width / 2)
+ }
+ }
+ val y = when (align.verticalAlign) {
+ IconVerticalAlign.Top -> {
+ layout.layerBounds.y
+ }
+ IconVerticalAlign.Bottom -> {
+ layout.parentBounds.height - layout.layerBounds.height
+ }
+ IconVerticalAlign.Center -> {
+ (layout.parentBounds.height / 2) - (layout.layerBounds.height / 2)
+ }
+ }
+ layout.copy(
+ layerBounds = layout.layerBounds.copy(
+ x = x,
+ y = y
+ )
+ )
+ }
+ is MarginIconModifier -> applyMarginIconModifier(this, layout, scaling)
+ is AlphaIconModifier -> layout.copy(alpha = alpha)
+ is ColorFilterModifier -> layout // applyColorFilterModifier(this, layout, displayScale) // ImageModifier
+ is SvgPatcherModifier -> layout // ImageModifier
+ is CutoutMarginModifier -> applyCutoutMarginModifier(this, layout, scaling)
+ is StrokeModifier -> layout
+ IconModifier.Companion -> layout
+ }
+}
+
+fun applyStrokeModifier(modifier: StrokeModifier, layout: LayerLayout): LayerLayout {
+ return layout.copy(stroke = modifier.color)
+}
+
+fun applyCutoutMarginModifier(modifier: CutoutMarginModifier, layout: LayerLayout, scaling: ScalingContext): LayerLayout {
+ val final = layout.calculateFinalBounds()
+ val size = modifier.size.asFractionalPixels(scaling, final, true)
+ return layout.copy(cutoutMargin = size)
+}
+
+fun IconUnit.asFractionalPixels(scaling: ScalingContext, bounds: Bounds, isWidth: Boolean = false): Float {
+ return when (this) {
+ is PixelIconUnit -> value
+ is PercentIconUnit -> if (isWidth) {
+ bounds.width * value
+ } else {
+ bounds.height * value
+ }
+ is DisplayPointIconUnit -> scaling.applyTo(value)
+ is MaxIconUnit -> {
+ if (isWidth) {
+ bounds.width
+ } else {
+ bounds.height
+ }
+ }
+ }.toFloat()
+}
+
+fun IconUnit.asPixels(scaling: ScalingContext, bounds: Bounds, isWidth: Boolean = false): Int {
+ return asFractionalPixels(scaling, bounds, isWidth).roundToInt()
+}
\ No newline at end of file
diff --git a/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/modifiers/MarginIconModifier.kt b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/modifiers/MarginIconModifier.kt
new file mode 100644
index 0000000000000..7427d1fb03c60
--- /dev/null
+++ b/platform/icons-impl/src/org/jetbrains/icons/impl/rendering/modifiers/MarginIconModifier.kt
@@ -0,0 +1,25 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.icons.impl.rendering.modifiers
+
+import org.jetbrains.icons.modifiers.MarginIconModifier
+import org.jetbrains.icons.rendering.ScalingContext
+import org.jetbrains.icons.impl.rendering.layers.LayerLayout
+
+internal fun applyMarginIconModifier(modifier: MarginIconModifier, layout: LayerLayout, scaling: ScalingContext): LayerLayout {
+ val leftPx = modifier.left.asPixels(scaling, layout.parentBounds, true)
+ val topPx = modifier.top.asPixels(scaling, layout.parentBounds, false)
+ val rightPx = modifier.right.asPixels(scaling, layout.parentBounds, true)
+ val bottomPx = modifier.bottom.asPixels(scaling, layout.parentBounds, false)
+
+ val finalX = maxOf(layout.layerBounds.x, leftPx)
+ val finalY = maxOf(layout.layerBounds.y, topPx)
+
+ return layout.copy(
+ layerBounds = layout.layerBounds.copy(
+ x = finalX,
+ y = finalY,
+ width = minOf(layout.parentBounds.width - finalX - rightPx, layout.layerBounds.width),
+ height = minOf(layout.parentBounds.height - finalY - bottomPx, layout.layerBounds.height)
+ )
+ )
+}
diff --git a/platform/ide-core-impl/src/com/intellij/ui/icons/CoreIconManager.kt b/platform/ide-core-impl/src/com/intellij/ui/icons/CoreIconManager.kt
index 2cc8cc2e6a51b..5bf8abb38ee24 100644
--- a/platform/ide-core-impl/src/com/intellij/ui/icons/CoreIconManager.kt
+++ b/platform/ide-core-impl/src/com/intellij/ui/icons/CoreIconManager.kt
@@ -249,6 +249,9 @@ class CoreIconManager : IconManager, CoreAwareIconManager {
return hasher.asLong
}
+ /**
+ * Ensure to also change ModuleImageResourceLocation in icons-impl/intellij
+ */
override fun getPluginAndModuleId(classLoader: ClassLoader): Pair {
if (classLoader is PluginAwareClassLoader) {
return classLoader.pluginId.idString to classLoader.moduleId
@@ -258,6 +261,9 @@ class CoreIconManager : IconManager, CoreAwareIconManager {
}
}
+ /**
+ * Ensure to also change ModuleImageResourceLoader in icons-impl/intellij/rendering
+ */
override fun getClassLoader(pluginId: String, moduleId: String?): ClassLoader? {
val plugin = PluginManagerCore.findPlugin(PluginId.getId(pluginId)) ?: return null
if (moduleId == null) {
diff --git a/platform/ide-core/resources/META-INF/intellij.moduleSets.core.platform.xml b/platform/ide-core/resources/META-INF/intellij.moduleSets.core.platform.xml
index 7e4afe5b8938e..349830d11cf60 100644
--- a/platform/ide-core/resources/META-INF/intellij.moduleSets.core.platform.xml
+++ b/platform/ide-core/resources/META-INF/intellij.moduleSets.core.platform.xml
@@ -118,6 +118,7 @@
+
diff --git a/platform/jewel/build.gradle.kts b/platform/jewel/build.gradle.kts
index 07dc37ee3c1eb..c52e74aa98200 100644
--- a/platform/jewel/build.gradle.kts
+++ b/platform/jewel/build.gradle.kts
@@ -6,6 +6,8 @@ plugins {
`jewel-linting`
}
+
+
tasks {
register("clean") { delete(rootProject.layout.buildDirectory) }
diff --git a/platform/jewel/gradle/libs.versions.toml b/platform/jewel/gradle/libs.versions.toml
index abb7e33534606..927cdea6bc99e 100644
--- a/platform/jewel/gradle/libs.versions.toml
+++ b/platform/jewel/gradle/libs.versions.toml
@@ -17,6 +17,7 @@ kotlin = "2.2.20"
kotlinpoet = "2.1.0"
kotlinterGradlePlugin = "5.2.0"
kotlinxSerialization = "1.9.0"
+kotlinxCoroutines = "1.10.2"
ktfmtGradlePlugin = "0.23.0"
metalava = "1.0.0-alpha13"
jsoup = "1.21.2"
@@ -43,8 +44,9 @@ junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "jun
junit-platform-engine = { module = "org.junit.platform:junit-platform-engine", version.ref = "junitPlatform" }
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junitPlatform" }
kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
+kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core", version.ref = "kotlinxSerialization" }
+kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinxCoroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" }
-kotlinx-serialization-core = { module = "org.jetbrains.kotlinx:kotlinx-serialization-core-jvm", version.ref = "kotlinxSerialization" }
ktor-client-java = { module = "io.ktor:ktor-client-java", version = "3.0.3" }
metalava = { module = "com.android.tools.metalava:metalava", version.ref = "metalava" }
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
diff --git a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/JewelComposePanelWrapper.kt b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/JewelComposePanelWrapper.kt
index 0d5e8eefba380..3406d6c88ef5b 100644
--- a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/JewelComposePanelWrapper.kt
+++ b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/JewelComposePanelWrapper.kt
@@ -75,7 +75,7 @@ public fun JewelComposePanel(
SwingBridgeTheme {
CompositionLocalProvider(
LocalComponentFoundation provides this@createJewelComposePanel,
- LocalPopupRenderer provides JBPopupRenderer,
+ LocalPopupRenderer provides JBPopupRenderer
) {
ComponentDataProviderBridge(jewelPanel, content = content)
}
diff --git a/platform/jewel/ide-laf-bridge/src/main/resources/intellij.platform.jewel.ideLafBridge.xml b/platform/jewel/ide-laf-bridge/src/main/resources/intellij.platform.jewel.ideLafBridge.xml
index 2212d890c6c5b..9cb39b531c856 100644
--- a/platform/jewel/ide-laf-bridge/src/main/resources/intellij.platform.jewel.ideLafBridge.xml
+++ b/platform/jewel/ide-laf-bridge/src/main/resources/intellij.platform.jewel.ideLafBridge.xml
@@ -1,7 +1,9 @@
-
-
-
+
+
+
+
+
diff --git a/platform/jewel/int-ui/int-ui-standalone/BUILD.bazel b/platform/jewel/int-ui/int-ui-standalone/BUILD.bazel
index 4f6699ecedb36..130728a6e92c2 100644
--- a/platform/jewel/int-ui/int-ui-standalone/BUILD.bazel
+++ b/platform/jewel/int-ui/int-ui-standalone/BUILD.bazel
@@ -38,6 +38,8 @@ jvm_library(
"//platform/jewel/foundation",
"//libraries/jbr",
"@lib//:jna",
+ "//platform/icons-api",
+ "//platform/icons-impl",
],
plugins = ["@lib//:compose-plugin"]
)
@@ -57,6 +59,8 @@ jvm_library(
"//libraries/compose-runtime-desktop:compose-runtime-desktop_test_lib",
"//platform/jewel/foundation:foundation_test_lib",
"//libraries/jbr:jbr_test_lib",
+ "//platform/icons-api:icons-api_test_lib",
+ "//platform/icons-impl:icons-impl_test_lib",
],
plugins = ["@lib//:compose-plugin"]
)
diff --git a/platform/jewel/int-ui/int-ui-standalone/build.gradle.kts b/platform/jewel/int-ui/int-ui-standalone/build.gradle.kts
index 7defc34d3b345..226c8cb01a834 100644
--- a/platform/jewel/int-ui/int-ui-standalone/build.gradle.kts
+++ b/platform/jewel/int-ui/int-ui-standalone/build.gradle.kts
@@ -13,6 +13,9 @@ plugins {
dependencies {
api(projects.ui)
+ api(project(":jb-icons-api"))
+ api(project(":jb-icons-api-rendering"))
+ api(project(":jb-icons-impl"))
implementation(libs.jbr.api)
implementation(libs.jna.core)
}
diff --git a/platform/jewel/int-ui/int-ui-standalone/intellij.platform.jewel.intUi.standalone.iml b/platform/jewel/int-ui/int-ui-standalone/intellij.platform.jewel.intUi.standalone.iml
index 37d796bbcb81e..353c9fd888bfc 100644
--- a/platform/jewel/int-ui/int-ui-standalone/intellij.platform.jewel.intUi.standalone.iml
+++ b/platform/jewel/int-ui/int-ui-standalone/intellij.platform.jewel.intUi.standalone.iml
@@ -42,5 +42,7 @@
+
+
\ No newline at end of file
diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/icon/StandaloneIconDesigner.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/icon/StandaloneIconDesigner.kt
new file mode 100644
index 0000000000000..66d7af7b6e99a
--- /dev/null
+++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/icon/StandaloneIconDesigner.kt
@@ -0,0 +1,14 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.jewel.intui.standalone.icon
+
+import org.jetbrains.icons.impl.design.DefaultIconDesigner
+import org.jetbrains.icons.modifiers.IconModifier
+import org.jetbrains.jewel.ui.icon.PathImageResourceLocation
+
+internal class StandaloneIconDesigner : DefaultIconDesigner() {
+ override fun image(path: String, classLoader: ClassLoader?, modifier: IconModifier) {
+ image(PathImageResourceLocation(path, classLoader), modifier)
+ }
+
+ override fun createNestedDesigner(): StandaloneIconDesigner = StandaloneIconDesigner()
+}
diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/icon/StandaloneIconManager.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/icon/StandaloneIconManager.kt
new file mode 100644
index 0000000000000..808972654342c
--- /dev/null
+++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/icon/StandaloneIconManager.kt
@@ -0,0 +1,29 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.jewel.intui.standalone.icon
+
+import kotlinx.coroutines.CoroutineScope
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.IconIdentifier
+import org.jetbrains.icons.design.IconDesigner
+import org.jetbrains.icons.impl.DefaultIconManager
+import org.jetbrains.icons.impl.DeferredIconResolverService
+
+internal class StandaloneIconManager(scope: CoroutineScope) : DefaultIconManager() {
+ override val resolverService: DeferredIconResolverService = DeferredIconResolverService(scope)
+
+ override suspend fun sendDeferredNotifications(id: IconIdentifier, result: Icon) {
+ // Do nothing
+ }
+
+ override fun markDeferredIconUnused(id: IconIdentifier) {
+ resolverService.cleanIcon(id)
+ }
+
+ override fun icon(designer: IconDesigner.() -> Unit): Icon {
+ val iconDesigner = StandaloneIconDesigner()
+ iconDesigner.designer()
+ return iconDesigner.build()
+ }
+}
+
+internal class StandaloneDeferredIconResolverService(scope: CoroutineScope) : DeferredIconResolverService(scope)
diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/icon/StandaloneIconRendererManager.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/icon/StandaloneIconRendererManager.kt
new file mode 100644
index 0000000000000..556c217c43c2a
--- /dev/null
+++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/icon/StandaloneIconRendererManager.kt
@@ -0,0 +1,45 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+
+package org.jetbrains.jewel.intui.standalone.icon
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.FlowCollector
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.impl.rendering.CoroutineBasedMutableIconUpdateFlow
+import org.jetbrains.icons.impl.rendering.DefaultIconRendererManager
+import org.jetbrains.icons.rendering.ImageModifiers
+import org.jetbrains.icons.rendering.MutableIconUpdateFlow
+import org.jetbrains.icons.rendering.RenderingContext
+
+internal class StandaloneIconRendererManager : DefaultIconRendererManager() {
+ override fun createUpdateFlow(scope: CoroutineScope?, updateCallback: (Int) -> Unit): MutableIconUpdateFlow {
+ if (scope == null) return EmptyMutableIconUpdateFlow()
+ return CoroutineBasedMutableIconUpdateFlow(scope, updateCallback)
+ }
+
+ override fun createRenderingContext(
+ updateFlow: MutableIconUpdateFlow,
+ defaultImageModifiers: ImageModifiers?,
+ ): RenderingContext {
+ return RenderingContext(updateFlow, defaultImageModifiers)
+ }
+}
+
+private class EmptyMutableIconUpdateFlow : MutableIconUpdateFlow {
+ override fun triggerUpdate() {
+ // Do nothing
+ }
+
+ override fun triggerDelayedUpdate(delay: Long) {
+ // Do nothing
+ }
+
+ override suspend fun collect(collector: FlowCollector) {
+ // Do nothing
+ }
+
+ override fun collectDynamic(flow: Flow, handler: (Icon) -> Unit) {
+ // Do nothing
+ }
+}
diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt
index 8e390b5498221..25a41d4feabf2 100644
--- a/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt
+++ b/platform/jewel/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/theme/IntUiTheme.kt
@@ -5,8 +5,10 @@ package org.jetbrains.jewel.intui.standalone.theme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
+import org.jetbrains.icons.IconManager
import org.jetbrains.jewel.foundation.DisabledAppearanceValues
import org.jetbrains.jewel.foundation.GlobalColors
import org.jetbrains.jewel.foundation.GlobalMetrics
@@ -20,6 +22,7 @@ import org.jetbrains.jewel.intui.standalone.IntUiMessageResourceResolver
import org.jetbrains.jewel.intui.standalone.IntUiTypography
import org.jetbrains.jewel.intui.standalone.StandalonePainterHintsProvider
import org.jetbrains.jewel.intui.standalone.StandalonePlatformCursorController
+import org.jetbrains.jewel.intui.standalone.icon.StandaloneIconManager
import org.jetbrains.jewel.intui.standalone.icon.StandaloneNewUiChecker
import org.jetbrains.jewel.intui.standalone.menuShortcut.StandaloneMenuItemShortcutHintProvider
import org.jetbrains.jewel.intui.standalone.menuShortcut.StandaloneShortcutProvider
@@ -1075,6 +1078,8 @@ public fun IntUiTheme(
swingCompatMode: Boolean = false,
content: @Composable () -> Unit,
) {
+ val managerScope = rememberCoroutineScope()
+ IconManager.activate(StandaloneIconManager(managerScope))
BaseJewelTheme(theme, ComponentStyling.default().with(styling), swingCompatMode) {
CompositionLocalProvider(
LocalPainterHintsProvider provides StandalonePainterHintsProvider(theme),
diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/resources/META-INF/services/org.jetbrains.icons.rendering.IconRendererManager b/platform/jewel/int-ui/int-ui-standalone/src/main/resources/META-INF/services/org.jetbrains.icons.rendering.IconRendererManager
new file mode 100644
index 0000000000000..8ea2a0a84dfc4
--- /dev/null
+++ b/platform/jewel/int-ui/int-ui-standalone/src/main/resources/META-INF/services/org.jetbrains.icons.rendering.IconRendererManager
@@ -0,0 +1 @@
+org.jetbrains.jewel.intui.standalone.icon.StandaloneIconRendererManager
diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/resources/META-INF/services/org.jetbrains.icons.rendering.ImageResourceProvider b/platform/jewel/int-ui/int-ui-standalone/src/main/resources/META-INF/services/org.jetbrains.icons.rendering.ImageResourceProvider
new file mode 100644
index 0000000000000..e94e176216edf
--- /dev/null
+++ b/platform/jewel/int-ui/int-ui-standalone/src/main/resources/META-INF/services/org.jetbrains.icons.rendering.ImageResourceProvider
@@ -0,0 +1 @@
+org.jetbrains.jewel.ui.icon.ComposeImageResourceProvider
diff --git a/platform/jewel/int-ui/int-ui-standalone/src/main/resources/intellij.platform.jewel.intUi.standalone.xml b/platform/jewel/int-ui/int-ui-standalone/src/main/resources/intellij.platform.jewel.intUi.standalone.xml
index 038db731b91b5..ef1eb97a61734 100644
--- a/platform/jewel/int-ui/int-ui-standalone/src/main/resources/intellij.platform.jewel.intUi.standalone.xml
+++ b/platform/jewel/int-ui/int-ui-standalone/src/main/resources/intellij.platform.jewel.intUi.standalone.xml
@@ -1,6 +1,8 @@
-
+
+
+
diff --git a/platform/jewel/samples/showcase/BUILD.bazel b/platform/jewel/samples/showcase/BUILD.bazel
index b839524d27971..873fe8d6f28e2 100644
--- a/platform/jewel/samples/showcase/BUILD.bazel
+++ b/platform/jewel/samples/showcase/BUILD.bazel
@@ -9,6 +9,7 @@ create_kotlinc_options(
"androidx.compose.foundation.ExperimentalFoundationApi",
"org.jetbrains.jewel.foundation.ExperimentalJewelApi",
"org.jetbrains.jewel.foundation.InternalJewelApi",
+ "org.jetbrains.icons.ExperimentalIconsApi",
],
plugin_options = ["plugin:androidx.compose.compiler.plugins.kotlin:generateFunctionKeyMetaAnnotations=true"],
x_context_parameters = True,
diff --git a/platform/jewel/samples/showcase/api-dump.txt b/platform/jewel/samples/showcase/api-dump.txt
index 907bffeb202f9..66c6bda6f2d84 100644
--- a/platform/jewel/samples/showcase/api-dump.txt
+++ b/platform/jewel/samples/showcase/api-dump.txt
@@ -2,11 +2,15 @@ f:org.jetbrains.jewel.samples.showcase.ShowcaseIcons
- sf:$stable:I
- sf:INSTANCE:org.jetbrains.jewel.samples.showcase.ShowcaseIcons
- f:getComponentsMenu():org.jetbrains.jewel.ui.icon.PathIconKey
-- f:getGitHub():org.jetbrains.jewel.ui.icon.PathIconKey
-- f:getJewelLogo():org.jetbrains.jewel.ui.icon.PathIconKey
+- f:getGitHub():org.jetbrains.icons.Icon
+- f:getGitHubKey():org.jetbrains.jewel.ui.icon.PathIconKey
+- f:getJewelLogo():org.jetbrains.icons.Icon
+- f:getJewelLogoKey():org.jetbrains.jewel.ui.icon.PathIconKey
+- f:getLayeredIcon():org.jetbrains.icons.Icon
- f:getMarkdown():org.jetbrains.jewel.ui.icon.PathIconKey
- f:getSunny():org.jetbrains.jewel.ui.icon.PathIconKey
-- f:getThemeDark():org.jetbrains.jewel.ui.icon.PathIconKey
+- f:getThemeDark():org.jetbrains.icons.Icon
+- f:getThemeDarkKey():org.jetbrains.jewel.ui.icon.PathIconKey
- f:getThemeLight():org.jetbrains.jewel.ui.icon.PathIconKey
- f:getThemeLightWithLightHeader():org.jetbrains.jewel.ui.icon.PathIconKey
- f:getThemeSystem():org.jetbrains.jewel.ui.icon.PathIconKey
diff --git a/platform/jewel/samples/showcase/exposed-third-party-api.txt b/platform/jewel/samples/showcase/exposed-third-party-api.txt
index 168de8a3b0bb9..9f3f1e91d6e47 100644
--- a/platform/jewel/samples/showcase/exposed-third-party-api.txt
+++ b/platform/jewel/samples/showcase/exposed-third-party-api.txt
@@ -1,2 +1,3 @@
androidx/compose/**
kotlin/jvm/internal/DefaultConstructorMarker
+org/jetbrains/icons/api/Icon
diff --git a/platform/jewel/samples/showcase/intellij.platform.jewel.samples.showcase.iml b/platform/jewel/samples/showcase/intellij.platform.jewel.samples.showcase.iml
index f95ea989b5614..bb519850b1005 100644
--- a/platform/jewel/samples/showcase/intellij.platform.jewel.samples.showcase.iml
+++ b/platform/jewel/samples/showcase/intellij.platform.jewel.samples.showcase.iml
@@ -4,7 +4,7 @@
-
+
diff --git a/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/ShowcaseIcons.kt b/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/ShowcaseIcons.kt
index 02bd15b92f536..cbc18260e37bd 100644
--- a/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/ShowcaseIcons.kt
+++ b/platform/jewel/samples/showcase/src/main/kotlin/org/jetbrains/jewel/samples/showcase/ShowcaseIcons.kt
@@ -1,12 +1,22 @@
// 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.samples.showcase
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.design.IconAlign
+import org.jetbrains.icons.design.percent
+import org.jetbrains.icons.icon
+import org.jetbrains.icons.imageIcon
+import org.jetbrains.icons.modifiers.IconModifier
+import org.jetbrains.icons.modifiers.align
+import org.jetbrains.icons.modifiers.fillMaxSize
+import org.jetbrains.icons.modifiers.margin
+import org.jetbrains.icons.modifiers.size
import org.jetbrains.jewel.ui.icon.PathIconKey
public object ShowcaseIcons {
public val componentsMenu: PathIconKey = PathIconKey("icons/structure.svg", ShowcaseIcons::class.java)
- public val gitHub: PathIconKey = PathIconKey("icons/github.svg", ShowcaseIcons::class.java)
public val jewelLogo: PathIconKey = PathIconKey("icons/jewel-logo.svg", ShowcaseIcons::class.java)
+ public val gitHub: PathIconKey = PathIconKey("icons/github.svg", ShowcaseIcons::class.java)
public val markdown: PathIconKey = PathIconKey("icons/markdown.svg", ShowcaseIcons::class.java)
public val themeDark: PathIconKey = PathIconKey("icons/darkTheme.svg", ShowcaseIcons::class.java)
public val themeLight: PathIconKey = PathIconKey("icons/lightTheme.svg", ShowcaseIcons::class.java)
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 3e1e0769339b9..cc85e7e7bd1da 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
@@ -24,12 +24,18 @@ import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.unit.dp
+import org.jetbrains.icons.design.Circle
+import org.jetbrains.icons.modifiers.IconModifier
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.samples.showcase.ShowcaseIcons
import org.jetbrains.jewel.ui.component.CheckboxRow
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.icon.badge
+import org.jetbrains.jewel.ui.icon.iconKey
+import org.jetbrains.jewel.ui.icon.size
+import org.jetbrains.jewel.ui.icon.stroke
import org.jetbrains.jewel.ui.icons.AllIconsKeys
import org.jetbrains.jewel.ui.painter.badge.DotBadgeShape
import org.jetbrains.jewel.ui.painter.hints.Badge
@@ -106,6 +112,55 @@ public fun Icons(modifier: Modifier = Modifier) {
}
}
+ Column {
+ Text("Icon Modifiers & Layers: (new api)")
+
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
+ horizontalArrangement = Arrangement.spacedBy(16.dp),
+ ) {
+ Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) {
+ Icon(contentDescription = "taskGroup") {
+ iconKey(AllIconsKeys.Nodes.ConfigFolder)
+ }
+ }
+ Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) {
+ Icon(contentDescription = "taskGroup") {
+ iconKey(AllIconsKeys.Nodes.ConfigFolder)
+ badge(Color.Red, Circle)
+ }
+ }
+ val backgroundColor =
+ if (JewelTheme.isDark) {
+ JewelTheme.colorPalette.blueOrNull(4) ?: Color(0xFF375FAD)
+ } else {
+ JewelTheme.colorPalette.blueOrNull(4) ?: Color(0xFF3574F0)
+ }
+ Box(
+ Modifier.size(24.dp).background(backgroundColor, shape = RoundedCornerShape(4.dp)),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(contentDescription = "taskGroup") {
+ iconKey(AllIconsKeys.Nodes.ConfigFolder, modifier = IconModifier.stroke(Color.White))
+ }
+ }
+ Box(
+ Modifier.size(24.dp).background(backgroundColor, shape = RoundedCornerShape(4.dp)),
+ contentAlignment = Alignment.Center,
+ ) {
+ Icon(contentDescription = "taskGroup") {
+ iconKey(AllIconsKeys.Nodes.ConfigFolder, modifier = IconModifier.stroke(Color.White))
+ badge(Color.Red, Circle)
+ }
+ }
+ Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) {
+ Icon(contentDescription = "taskGroup") {
+ iconKey(AllIconsKeys.Nodes.ConfigFolder, modifier = IconModifier.size(20.dp))
+ }
+ }
+ }
+ }
+
Column {
var checked by remember { mutableStateOf(true) }
diff --git a/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/TitleBarView.kt b/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/TitleBarView.kt
index d541dc423cf39..6ddb2500679f2 100644
--- a/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/TitleBarView.kt
+++ b/platform/jewel/samples/standalone/src/main/kotlin/org/jetbrains/jewel/samples/standalone/view/TitleBarView.kt
@@ -12,6 +12,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import java.awt.Desktop
import java.net.URI
+import org.jetbrains.icons.design.px
+import org.jetbrains.icons.icon
+import org.jetbrains.icons.modifiers.IconModifier
+import org.jetbrains.icons.modifiers.size
import org.jetbrains.jewel.samples.showcase.ShowcaseIcons
import org.jetbrains.jewel.samples.showcase.views.forCurrentOs
import org.jetbrains.jewel.samples.standalone.IntUiThemes
@@ -117,7 +121,7 @@ internal fun DecoratedWindowScope.TitleBarView() {
)
IntUiThemes.Dark ->
- Icon(key = ShowcaseIcons.themeDark, contentDescription = "Dark", hints = arrayOf(Size(20)))
+ Icon(ShowcaseIcons.themeDark, contentDescription = "Dark")
IntUiThemes.System ->
Icon(
diff --git a/platform/jewel/settings.gradle.kts b/platform/jewel/settings.gradle.kts
index 2931a57bff13f..1b7165d7cc2c4 100644
--- a/platform/jewel/settings.gradle.kts
+++ b/platform/jewel/settings.gradle.kts
@@ -51,8 +51,16 @@ include(
":samples:standalone",
":ui",
":ui-tests",
+ ":jb-icons-api",
+ ":jb-icons-api-rendering",
+ ":jb-icons-impl"
)
+project(":jb-icons-api").projectDir = file("../icons-api")
+project(":jb-icons-api-rendering").projectDir = file("../icons-api/rendering")
+project(":jb-icons-impl").projectDir = file("../icons-impl")
+
+
develocity {
buildScan {
publishing.onlyIf { System.getenv("CI") == "true" }
diff --git a/platform/jewel/ui/BUILD.bazel b/platform/jewel/ui/BUILD.bazel
index e49e1987ff17b..fa68743554ee3 100644
--- a/platform/jewel/ui/BUILD.bazel
+++ b/platform/jewel/ui/BUILD.bazel
@@ -43,12 +43,17 @@ jvm_library(
"@lib//:platform-jewel-ui-org-jetbrains-compose-components-components-resources-desktop",
"//libraries/compose-foundation-desktop",
"//libraries/compose-runtime-desktop",
+ "//platform/icons-api",
+ "//platform/icons-api/rendering",
+ "//platform/icons-impl",
],
exports = [
"//platform/jewel/foundation",
"@lib//:platform-jewel-ui-org-jetbrains-compose-components-components-resources",
"@lib//:platform-jewel-ui-org-jetbrains-compose-components-components-resources-desktop",
"//libraries/compose-foundation-desktop",
+ "//platform/icons-api",
+ "//platform/icons-api/rendering",
],
plugins = ["@lib//:compose-plugin"]
)
@@ -77,6 +82,12 @@ jvm_library(
"//libraries/compose-runtime-desktop:compose-runtime-desktop_test_lib",
"//libraries/junit4",
"//libraries/junit4:junit4_test_lib",
+ "//platform/icons-api",
+ "//platform/icons-api:icons-api_test_lib",
+ "//platform/icons-api/rendering",
+ "//platform/icons-api/rendering:rendering_test_lib",
+ "//platform/icons-impl",
+ "//platform/icons-impl:icons-impl_test_lib",
"//libraries/compose-foundation-desktop-junit",
"//libraries/compose-foundation-desktop-junit:compose-foundation-desktop-junit_test_lib",
],
@@ -87,6 +98,10 @@ jvm_library(
"@lib//:platform-jewel-ui-org-jetbrains-compose-components-components-resources-desktop",
"//libraries/compose-foundation-desktop",
"//libraries/compose-foundation-desktop:compose-foundation-desktop_test_lib",
+ "//platform/icons-api",
+ "//platform/icons-api:icons-api_test_lib",
+ "//platform/icons-api/rendering",
+ "//platform/icons-api/rendering:rendering_test_lib",
],
plugins = ["@lib//:compose-plugin"]
)
diff --git a/platform/jewel/ui/api-dump-experimental.txt b/platform/jewel/ui/api-dump-experimental.txt
index 0529b6ab214f5..92d5c7a262891 100644
--- a/platform/jewel/ui/api-dump-experimental.txt
+++ b/platform/jewel/ui/api-dump-experimental.txt
@@ -96,6 +96,14 @@ f:org.jetbrains.jewel.ui.component.search.SpeedSearchableTreeKt
- *sf:SpeedSearchableTree(org.jetbrains.jewel.ui.component.SpeedSearchScope,org.jetbrains.jewel.foundation.lazy.tree.Tree,kotlin.jvm.functions.Function1,androidx.compose.ui.Modifier,org.jetbrains.jewel.foundation.lazy.tree.TreeState,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1,kotlin.jvm.functions.Function1,org.jetbrains.jewel.foundation.lazy.tree.KeyActions,org.jetbrains.jewel.ui.component.styling.LazyTreeStyle,kotlinx.coroutines.CoroutineDispatcher,kotlin.jvm.functions.Function4,androidx.compose.runtime.Composer,I,I,I):V
f:org.jetbrains.jewel.ui.component.styling.IconButtonStylingKt
- *sf:getLocalTransparentIconButtonStyle():androidx.compose.runtime.ProvidableCompositionLocal
+f:org.jetbrains.jewel.ui.icon.IconUtilsKt
+- *sf:tintColor-4MyTwCo(org.jetbrains.icons.api.modifiers.IconModifier,J,I):org.jetbrains.icons.api.modifiers.IconModifier
+- *bs:tintColor-4MyTwCo$default(org.jetbrains.icons.api.modifiers.IconModifier,J,I,I,java.lang.Object):org.jetbrains.icons.api.modifiers.IconModifier
+- *sf:toCompose(org.jetbrains.icons.api.design.BlendMode):I
+- *sf:toCompose(org.jetbrains.icons.api.design.Color):J
+- *sf:toCompose(org.jetbrains.icons.api.filters.ColorFilter):androidx.compose.ui.graphics.ColorFilter
+- *sf:toIconsBlendMode-s9anfk8(I):org.jetbrains.icons.api.design.BlendMode
+- *sf:toIconsColor-8_81llA(J):org.jetbrains.icons.api.design.RGBA
*f:org.jetbrains.jewel.ui.painter.hints.EmbeddedToInlineCssStyleSvgPatchHint
- org.jetbrains.jewel.ui.painter.PainterSvgPatchHint
- sf:$stable:I
diff --git a/platform/jewel/ui/api-dump.txt b/platform/jewel/ui/api-dump.txt
index e7d09117cd855..450312e1eabd7 100644
--- a/platform/jewel/ui/api-dump.txt
+++ b/platform/jewel/ui/api-dump.txt
@@ -381,6 +381,8 @@ f:org.jetbrains.jewel.ui.component.IconButtonState$Companion
- bs:of-3OtLUoY$default(org.jetbrains.jewel.ui.component.IconButtonState$Companion,Z,Z,Z,Z,Z,I,java.lang.Object):J
f:org.jetbrains.jewel.ui.component.IconKt
- sf:Icon(androidx.compose.ui.graphics.painter.Painter,java.lang.String,androidx.compose.ui.graphics.ColorFilter,androidx.compose.ui.Modifier,androidx.compose.runtime.Composer,I,I):V
+- sf:Icon(java.lang.String,androidx.compose.ui.Modifier,org.jetbrains.icons.rendering.LoadingStrategy,kotlin.jvm.functions.Function1,androidx.compose.runtime.Composer,I,I):V
+- sf:Icon(org.jetbrains.icons.Icon,java.lang.String,androidx.compose.ui.Modifier,org.jetbrains.icons.rendering.LoadingStrategy,androidx.compose.runtime.Composer,I,I):V
- sf:Icon(org.jetbrains.jewel.ui.icon.IconKey,java.lang.String,androidx.compose.ui.Modifier,java.lang.Class,androidx.compose.ui.graphics.ColorFilter,org.jetbrains.jewel.ui.painter.PainterHint,androidx.compose.runtime.Composer,I,I):V
- sf:Icon(org.jetbrains.jewel.ui.icon.IconKey,java.lang.String,androidx.compose.ui.Modifier,java.lang.Class,androidx.compose.ui.graphics.ColorFilter,org.jetbrains.jewel.ui.painter.PainterHint[],androidx.compose.runtime.Composer,I,I):V
- sf:Icon-FHprtrg(org.jetbrains.jewel.ui.icon.IconKey,java.lang.String,androidx.compose.ui.Modifier,java.lang.Class,J,org.jetbrains.jewel.ui.painter.PainterHint,androidx.compose.runtime.Composer,I,I):V
@@ -2360,6 +2362,10 @@ f:org.jetbrains.jewel.ui.graphics.CssLinearGradientBrushKt
org.jetbrains.jewel.ui.icon.IconKey
- a:getIconClass():java.lang.Class
- a:path(Z):java.lang.String
+f:org.jetbrains.jewel.ui.icon.IconKeyKt
+- sf:iconKey(org.jetbrains.icons.design.IconDesigner,org.jetbrains.jewel.ui.icon.IconKey,org.jetbrains.icons.modifiers.IconModifier):V
+- bs:iconKey$default(org.jetbrains.icons.design.IconDesigner,org.jetbrains.jewel.ui.icon.IconKey,org.jetbrains.icons.modifiers.IconModifier,I,java.lang.Object):V
+f:org.jetbrains.jewel.ui.icon.IconUtilsKt
f:org.jetbrains.jewel.ui.icon.IntelliJIconKey
- org.jetbrains.jewel.ui.icon.IconKey
- sf:$stable:I
diff --git a/platform/jewel/ui/build.gradle.kts b/platform/jewel/ui/build.gradle.kts
index 0c10b7c6a557b..87569ff734071 100644
--- a/platform/jewel/ui/build.gradle.kts
+++ b/platform/jewel/ui/build.gradle.kts
@@ -13,6 +13,9 @@ plugins {
dependencies {
api(projects.foundation)
+ api(project(":jb-icons-api"))
+ api(project(":jb-icons-api-rendering"))
+ api(project(":jb-icons-impl"))
implementation(compose.components.resources)
testImplementation(compose.desktop.uiTestJUnit4)
testImplementation(compose.desktop.currentOs) { exclude(group = "org.jetbrains.compose.material") }
diff --git a/platform/jewel/ui/exposed-third-party-api.txt b/platform/jewel/ui/exposed-third-party-api.txt
index cbaeb3beca2d6..e855ffd4be5ac 100644
--- a/platform/jewel/ui/exposed-third-party-api.txt
+++ b/platform/jewel/ui/exposed-third-party-api.txt
@@ -1,4 +1,5 @@
androidx/compose/**
kotlin/jvm/internal/DefaultConstructorMarker
kotlin/ranges/ClosedFloatingPointRange
+org/jetbrains/icons/api/**
org/w3c/dom/Element
diff --git a/platform/jewel/ui/intellij.platform.jewel.ui.iml b/platform/jewel/ui/intellij.platform.jewel.ui.iml
index d34fb87e2ae61..19206e157e095 100644
--- a/platform/jewel/ui/intellij.platform.jewel.ui.iml
+++ b/platform/jewel/ui/intellij.platform.jewel.ui.iml
@@ -82,6 +82,9 @@
+
+
+
\ No newline at end of file
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 0355422a62f19..4bd76d72c8248 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
@@ -7,7 +7,11 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
+import androidx.compose.runtime.key
+import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.paint
import androidx.compose.ui.geometry.Size
@@ -31,9 +35,17 @@ import org.jetbrains.compose.resources.ExperimentalResourceApi
import org.jetbrains.compose.resources.decodeToImageBitmap
import org.jetbrains.compose.resources.decodeToImageVector
import org.jetbrains.compose.resources.decodeToSvgPainter
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.design.IconDesigner
+import org.jetbrains.icons.icon
+import org.jetbrains.icons.rendering.IconRendererManager
+import org.jetbrains.icons.rendering.LoadingStrategy
+import org.jetbrains.icons.rendering.createRenderer
+import org.jetbrains.icons.impl.rendering.DefaultImageModifiers
import org.jetbrains.jewel.foundation.modifier.thenIf
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.icon.IconKey
+import org.jetbrains.jewel.ui.icon.RendererBasedIconPainter
import org.jetbrains.jewel.ui.icon.newUiChecker
import org.jetbrains.jewel.ui.painter.PainterHint
import org.jetbrains.jewel.ui.painter.rememberResourcePainterProvider
@@ -76,11 +88,51 @@ public fun Icon(
* 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.
+ * @param loadingStrategy specifies if the function should block the thread while loading the icon or show placeholder
+ * or render blank instead.
*/
-@Suppress("ComposableParamOrder") // To fix in JEWEL-929
+@Composable
+public fun Icon(
+ icon: Icon,
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ loadingStrategy: LoadingStrategy = LoadingStrategy.BlockThread
+) {
+ val scope = rememberCoroutineScope()
+ val scalingContext = RendererBasedIconPainter.inferScalingContext()
+ var updateIndex by remember { mutableStateOf(0) }
+ val context = IconRendererManager.createRenderingContext(
+ updateFlow = IconRendererManager.createUpdateFlow(scope) { update ->
+ updateIndex = update
+ },
+ defaultImageModifiers = DefaultImageModifiers(
+ isDark = JewelTheme.isDark
+ )
+ )
+ val renderer = remember(icon) {
+ icon.createRenderer(context)
+ }
+ val painter = remember(icon, renderer) {
+ RendererBasedIconPainter(
+ renderer,
+ scaling = scalingContext
+ )
+ }
+ key(updateIndex) {
+ Icon(painter = painter, contentDescription = contentDescription, modifier = modifier)
+ }
+}
+
+@Composable
+public fun Icon(
+ contentDescription: String?,
+ modifier: Modifier = Modifier,
+ loadingStrategy: LoadingStrategy = LoadingStrategy.BlockThread,
+ designer: IconDesigner.() -> Unit
+) {
+ Icon(icon(designer), contentDescription, modifier, loadingStrategy)
+}
+
@Composable
public fun Icon(
key: IconKey,
@@ -298,8 +350,8 @@ private object ResourceLoader
private fun readResourceBytes(resourcePath: String) =
checkNotNull(ResourceLoader.javaClass.classLoader.getResourceAsStream(resourcePath)) {
- "Could not load resource $resourcePath: it does not exist or can't be read."
- }
+ "Could not load resource $resourcePath: it does not exist or can't be read."
+ }
.readAllBytes()
private fun Modifier.defaultSizeFor(painter: Painter) =
diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposeBitmapImageResource.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposeBitmapImageResource.kt
new file mode 100644
index 0000000000000..e26324f9d888d
--- /dev/null
+++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposeBitmapImageResource.kt
@@ -0,0 +1,145 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.jewel.ui.icon
+
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.asComposeImageBitmap
+import androidx.compose.ui.graphics.asSkiaBitmap
+import org.jetbrains.icons.rendering.BitmapImageResource
+import org.jetbrains.icons.rendering.Bounds
+import org.jetbrains.icons.rendering.lowlevel.GPUImageResourceHolder
+import org.jetbrains.icons.rendering.ImageScale
+import org.jetbrains.icons.rendering.RescalableImageResource
+import org.jetbrains.skia.Bitmap
+import org.jetbrains.skia.ColorAlphaType
+import org.jetbrains.skia.ImageInfo
+import org.jetbrains.icons.impl.rendering.CachedGPUImageResourceHolder
+import org.jetbrains.icons.rendering.Dimensions
+import org.jetbrains.skia.ColorType
+import org.jetbrains.skia.impl.BufferUtil
+
+internal class ComposeBitmapImageResource(
+ public val imageBitmap: ImageBitmap
+): BitmapImageResource, CachedGPUImageResourceHolder() {
+ override fun getRGBPixels(): IntArray {
+ val skia = imageBitmap.asSkiaBitmap()
+ val pixelsNativePointer = skia.peekPixels()!!.addr
+ val pixelsBuffer = BufferUtil.getByteBufferFromPointer(pixelsNativePointer, skia.rowBytes * skia.height)
+ return pixelsBuffer.asIntBuffer().array()
+ }
+
+ override fun readPrefetchedPixel(pixels: IntArray, x: Int, y: Int): Int? {
+ return pixels.getOrNull(y * imageBitmap.width + x)
+ }
+
+ override fun getBandOffsetsToSRGB(): IntArray {
+ val skia = imageBitmap.asSkiaBitmap()
+ return when (skia.colorInfo.colorType) {
+ ColorType.RGB_888X -> intArrayOf(0, 1, 2, 3)
+ ColorType.BGRA_8888 -> intArrayOf(2, 1, 0, 3)
+ else -> throw UnsupportedOperationException("unsupported color type ${skia.colorInfo.colorType}")
+ }
+ }
+
+ override val width: Int = imageBitmap.width
+ override val height: Int = imageBitmap.height
+}
+
+internal fun BitmapImageResource.composeBitmap(): ImageBitmap {
+ if (this is ComposeBitmapImageResource) return imageBitmap
+ val cache = (this as? GPUImageResourceHolder)
+ return cache?.getOrGenerateBitmap(ImageBitmap::class) {
+ composeBitmapWithoutCaching().second
+ } ?: composeBitmapWithoutCaching().second
+}
+
+internal fun RescalableImageResource.composeBitmap(scale: ImageScale): ImageBitmapView {
+ val cache = (this as? GPUImageResourceHolder)
+ val cached = cache?.getOrGenerateBitmap(SingleBitmapCache::class) { SingleBitmapCache() }
+ if (cached != null) {
+ return cached.getOrPut(this, scale)
+ } else {
+ return scale(scale).composeBitmap().toView()
+ }
+}
+
+internal fun ImageBitmap.toView(): ImageBitmapView {
+ return ImageBitmapView(this, Dimensions(this.width, this.height))
+}
+
+internal class ImageBitmapView(
+ val imageBitmap: ImageBitmap,
+ val size: Dimensions
+)
+
+private class SingleBitmapCache {
+ private var lastBitmap: CachedBitmap? = null
+
+ private class CachedBitmap(
+ val dimensions: Bounds,
+ var composeBitmap: ImageBitmapView,
+ val bitmap: Bitmap
+ )
+
+ fun getOrPut(image: RescalableImageResource, scale: ImageScale): ImageBitmapView {
+ val last = lastBitmap
+ val expectedDimensions = image.calculateExpectedDimensions(scale)
+ if (last == null) {
+ return createNewBitmap(image, scale, expectedDimensions)
+ } else {
+ if (last.composeBitmap.size == expectedDimensions) {
+ return last.composeBitmap
+ } else if (last.dimensions.canFit(expectedDimensions)) {
+ last.bitmap.setPixelsFrom(image.scale(scale))
+ last.composeBitmap = last.composeBitmap.adjustTo(expectedDimensions)
+ return last.composeBitmap
+ } else {
+ return createNewBitmap(image, scale, expectedDimensions)
+ }
+ }
+ }
+
+ private fun createNewBitmap(image: RescalableImageResource, imageScale: ImageScale, dimensions: Bounds): ImageBitmapView {
+ val (skia, compose) = image.scale(imageScale).composeBitmapWithoutCaching()
+ val new = CachedBitmap(
+ dimensions,
+ compose.toView(),
+ skia
+ )
+ lastBitmap = new
+ return new.composeBitmap
+ }
+}
+
+private fun ImageBitmapView.adjustTo(dimensions: Dimensions): ImageBitmapView {
+ return ImageBitmapView(imageBitmap, dimensions)
+}
+
+private fun BitmapImageResource.composeBitmapWithoutCaching(): Pair {
+ val bitmap = Bitmap()
+ bitmap.allocPixels(ImageInfo.makeS32(width, height, ColorAlphaType.UNPREMUL))
+ bitmap.setPixelsFrom(this)
+ return bitmap to bitmap.asComposeImageBitmap()
+}
+
+private fun Bitmap.setPixelsFrom(image: BitmapImageResource) {
+ val bytesPerPixel = 4
+ val pixels = ByteArray(width * height * bytesPerPixel)
+ val prefetchedPixels = image.getRGBPixels()
+
+ var k = 0
+ for (y in 0 until height) {
+ for (x in 0 until width) {
+ val argb = image.readPrefetchedPixel(prefetchedPixels, x, y) ?: 0
+ val a = (argb shr 24) and 0xff
+ val r = (argb shr 16) and 0xff
+ val g = (argb shr 8) and 0xff
+ val b = (argb shr 0) and 0xff
+ pixels[k++] = b.toByte()
+ pixels[k++] = g.toByte()
+ pixels[k++] = r.toByte()
+ pixels[k++] = a.toByte()
+ }
+ }
+
+ installPixels(pixels)
+}
diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposeImageResourceProvider.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposeImageResourceProvider.kt
new file mode 100644
index 0000000000000..0769389be795a
--- /dev/null
+++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposeImageResourceProvider.kt
@@ -0,0 +1,137 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.jewel.ui.icon
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.decodeToImageBitmap
+import androidx.compose.ui.unit.Density
+import java.io.ByteArrayInputStream
+import java.io.InputStream
+import javax.xml.XMLConstants
+import javax.xml.parsers.DocumentBuilderFactory
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.compose.resources.decodeToSvgPainter
+import org.jetbrains.icons.modifiers.svgPatcher
+import org.jetbrains.icons.patchers.SvgPatchOperation
+import org.jetbrains.icons.patchers.SvgPatcher
+import org.jetbrains.icons.patchers.combineWith
+import org.jetbrains.icons.rendering.ImageModifiers
+import org.jetbrains.icons.rendering.ImageResource
+import org.jetbrains.icons.ImageResourceLocation
+import org.jetbrains.icons.rendering.ImageResourceProvider
+import org.jetbrains.icons.impl.rendering.DefaultImageModifiers
+import org.jetbrains.jewel.foundation.InternalJewelApi
+import org.jetbrains.jewel.ui.painter.writeToString
+import org.w3c.dom.Element
+
+@InternalJewelApi
+@ApiStatus.Internal
+public class ComposeImageResourceProvider : ImageResourceProvider {
+ override fun loadImage(location: ImageResourceLocation, imageModifiers: ImageModifiers?): ImageResource {
+ // TODO Support image modifiers
+ if (location is PathImageResourceLocation) {
+ val extension = location.path.substringAfterLast(".").lowercase()
+ val data = location.loadData(imageModifiers)
+ val stream = ByteArrayInputStream(data)
+ return when (extension) {
+ "svg" -> ComposePainterImageResource(patchSvg(imageModifiers, stream).decodeToSvgPainter(Density(1f)), imageModifiers)
+ // "xml" -> loader.loadData().decodeToImageVector()
+ else -> ComposeBitmapImageResource(location.loadData(imageModifiers).decodeToImageBitmap())
+ }
+ } else {
+ error("Unsupported loader: $location")
+ }
+ }
+}
+
+private val documentBuilderFactory =
+ DocumentBuilderFactory.newDefaultInstance().apply { setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) }
+
+private fun patchSvg(modifiers: ImageModifiers?, inputStream: InputStream): ByteArray {
+val builder = documentBuilderFactory.newDocumentBuilder()
+ val document = builder.parse(inputStream)
+
+ val knownModifiers = modifiers as? DefaultImageModifiers
+ val patcher = knownModifiers?.stroke?.let { stroke ->
+ svgPatcher {
+ for (color in backgroundPalette) {
+ replaceIfMatches("fill", color.toIconsColor().toHex(), "transparent")
+ }
+ for (color in strokeColors) {
+ replaceIfMatches("fill", color.toIconsColor().toHex(), stroke.toHex())
+ }
+ }
+ } combineWith modifiers?.svgPatcher
+ patcher?.patch(document.documentElement)
+ return document.writeToString().toByteArray()
+}
+
+private val backgroundPalette =
+ listOf(
+ Color(0xFFEBECF0),
+ Color(0xFFE7EFFD),
+ Color(0xFFDFF2E0),
+ Color(0xFFF2FCF3),
+ Color(0xFFFFE8E8),
+ Color(0xFFFFF5F5),
+ Color(0xFFFFF8E3),
+ Color(0xFFFFF4EB),
+ Color(0xFFEEE0FF),
+ )
+
+private val strokeColors =
+ listOf(
+ Color(0xFF000000),
+ Color(0xFFFFFFFF),
+ Color(0xFF818594),
+ Color(0xFF6C707E),
+ Color(0xFF3574F0),
+ Color(0xFF5FB865),
+ Color(0xFFE35252),
+ Color(0xFFEB7171),
+ Color(0xFFE3AE4D),
+ Color(0xFFFCC75B),
+ Color(0xFFF28C35),
+ Color(0xFF955AE0),
+ )
+
+private fun SvgPatcher.patch(element: Element) {
+ for (operation in operations) {
+ when (operation.operation) {
+ SvgPatchOperation.Operation.Add -> {
+ if (!element.hasAttribute(operation.attributeName)) {
+ element.setAttribute(operation.attributeName, operation.value!!)
+ }
+ }
+ SvgPatchOperation.Operation.Replace -> {
+ if (operation.conditional) {
+ val matches =
+ element.getAttribute(operation.attributeName).equals(operation.expectedValue, ignoreCase = true)
+ if (matches == !operation.negatedCondition) {
+ element.setAttribute(operation.attributeName, operation.value!!)
+ }
+ } else if (element.hasAttribute(operation.attributeName)) {
+ element.setAttribute(operation.attributeName, operation.value!!)
+ }
+ }
+ SvgPatchOperation.Operation.Remove -> {
+ if (operation.conditional) {
+ val matches = element.getAttribute(operation.attributeName) == operation.expectedValue
+ if (matches == !operation.negatedCondition) {
+ element.removeAttribute(operation.attributeName)
+ }
+ } else {
+ element.removeAttribute(operation.attributeName)
+ }
+ }
+ SvgPatchOperation.Operation.Set -> element.setAttribute(operation.attributeName, operation.value!!)
+ }
+ }
+ val nodes = element.childNodes
+ val length = nodes.length
+ for (i in 0 until length) {
+ val item = nodes.item(i)
+ if (item is Element) {
+ patch(item)
+ }
+ }
+}
diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposePainterImageResource.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposePainterImageResource.kt
new file mode 100644
index 0000000000000..9dfba9e281591
--- /dev/null
+++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposePainterImageResource.kt
@@ -0,0 +1,15 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.jewel.ui.icon
+
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.util.fastRoundToInt
+import org.jetbrains.icons.rendering.ImageModifiers
+import org.jetbrains.icons.rendering.ImageResource
+
+internal class ComposePainterImageResource(
+ val painter: Painter,
+ val modifiers: ImageModifiers?
+): ImageResource {
+ override val width: Int = painter.intrinsicSize.width.fastRoundToInt()
+ override val height: Int = painter.intrinsicSize.height.fastRoundToInt()
+}
diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposePaintingApi.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposePaintingApi.kt
new file mode 100644
index 0000000000000..9cc8a3594a73d
--- /dev/null
+++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposePaintingApi.kt
@@ -0,0 +1,232 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.jewel.ui.icon
+
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.FilterQuality
+import androidx.compose.ui.graphics.ImageBitmap
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.DrawScope.Companion.DefaultBlendMode
+import androidx.compose.ui.graphics.drawscope.Fill
+import androidx.compose.ui.graphics.drawscope.translate
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.IntSize
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.filters.ColorFilter
+import org.jetbrains.icons.rendering.BitmapImageResource
+import org.jetbrains.icons.rendering.Bounds
+import org.jetbrains.icons.rendering.FitAreaScale
+import org.jetbrains.icons.rendering.ImageResource
+import org.jetbrains.icons.rendering.RescalableImageResource
+import org.jetbrains.icons.rendering.PaintingApi
+import org.jetbrains.jewel.foundation.InternalJewelApi
+import kotlin.math.roundToInt
+import org.jetbrains.icons.design.Color
+import org.jetbrains.icons.rendering.DrawMode
+import org.jetbrains.icons.rendering.ScalingContext
+import org.jetbrains.jewel.foundation.GenerateDataFunctions
+
+@InternalJewelApi
+@ApiStatus.Internal
+public class ComposePaintingApi(
+ public val drawScope: DrawScope,
+ private val customBounds: Bounds? = null,
+ private val overrideColorFilter: ColorFilter? = null,
+ override val scaling: ScalingContext = ComposeScalingContext(drawScope.drawContext.density.density)
+): PaintingApi {
+ private var usedBounds: Bounds? = null
+
+ override val bounds: Bounds = customBounds ?: Bounds(
+ 0,
+ 0,
+ drawScope.size.width.roundToInt(),
+ drawScope.size.height.roundToInt()
+ )
+
+ override fun withCustomContext(
+ bounds: Bounds,
+ overrideColorFilter: ColorFilter?
+ ): PaintingApi {
+ return ComposePaintingApi(drawScope, bounds, overrideColorFilter ?: this.overrideColorFilter)
+ }
+
+ override fun drawCircle(color: Color, x: Int, y: Int, radius: Float, alpha: Float, mode: DrawMode) {
+ val style = if (mode == DrawMode.Stroke) {
+ Fill
+ } else Fill // TODO Support strokes
+ val blendMode = if (mode == DrawMode.Clear) {
+ BlendMode.Clear
+ } else DefaultBlendMode
+ drawScope.drawCircle(
+ color.toCompose(),
+ radius,
+ Offset(x.toFloat(), y.toFloat()),
+ alpha,
+ style = style,
+ blendMode = blendMode
+ )
+ }
+
+ override fun drawRect(color: Color, x: Int, y: Int, width: Int, height: Int, alpha: Float, mode: DrawMode) {
+ val style = if (mode == DrawMode.Stroke) {
+ Fill
+ } else Fill // TODO Support strokes
+ val blendMode = if (mode == DrawMode.Clear) {
+ BlendMode.Clear
+ } else DefaultBlendMode
+ drawScope.drawRect(
+ color.toCompose(),
+ Offset(x.toFloat(), y.toFloat()),
+ Size(width.toFloat(), height.toFloat()),
+ alpha,
+ style = style,
+ blendMode = blendMode
+ )
+ }
+
+ override fun drawImage(
+ image: ImageResource,
+ x: Int,
+ y: Int,
+ width: Int?,
+ height: Int?,
+ srcX: Int,
+ srcY: Int,
+ srcWidth: Int?,
+ srcHeight: Int?,
+ alpha: Float,
+ colorFilter: ColorFilter?
+ ) {
+ when (image) {
+ is BitmapImageResource -> {
+ drawImage(image, x, y, width, height, srcX, srcY, srcWidth, srcHeight, alpha, colorFilter)
+ }
+ is RescalableImageResource -> {
+ drawImage(image, x, y, width, height, srcX, srcY, srcWidth, srcHeight, alpha, colorFilter)
+ }
+ is ComposePainterImageResource -> {
+ drawImage(image, x, y, width, height, srcX, srcY, srcWidth, srcHeight, alpha, colorFilter)
+ }
+ }
+ }
+
+ override fun getUsedBounds(): Bounds = usedBounds ?: bounds
+
+ private fun drawImage(
+ image: BitmapImageResource,
+ x: Int,
+ y: Int,
+ width: Int?,
+ height: Int?,
+ srcX: Int,
+ srcY: Int,
+ srcWidth: Int?,
+ srcHeight: Int?,
+ alpha: Float,
+ colorFilter: ColorFilter?
+ ) {
+ drawComposeImage(image.composeBitmap().toView(), x, y, width, height, srcX, srcY, srcWidth, srcHeight, alpha, colorFilter)
+ }
+
+ private fun drawImage(
+ image: RescalableImageResource,
+ x: Int,
+ y: Int,
+ width: Int?,
+ height: Int?,
+ srcX: Int,
+ srcY: Int,
+ srcWidth: Int?,
+ srcHeight: Int?,
+ alpha: Float,
+ colorFilter: ColorFilter?
+ ) {
+ val scaledImage = image.composeBitmap(FitAreaScale(width ?: bounds.width, height ?: bounds.width))
+ drawComposeImage(scaledImage, x, y, width, height, srcX, srcY, srcWidth, srcHeight, alpha, colorFilter)
+ }
+
+ private fun drawImage(
+ image: ComposePainterImageResource,
+ x: Int,
+ y: Int,
+ width: Int?,
+ height: Int?,
+ srcX: Int,
+ srcY: Int,
+ srcWidth: Int?,
+ srcHeight: Int?,
+ alpha: Float,
+ colorFilter: ColorFilter?
+ ) {
+ if (image.width == 0 || image.height == 0) return
+ val targetSize = IntSize(width ?: bounds.width, height ?: bounds.height)
+ drawScope.translate(x.toFloat(), y.toFloat()) {
+ with(image.painter) {
+ draw(
+ Size(
+ targetSize.width.toFloat(),
+ targetSize.height.toFloat()
+ ),
+ alpha,
+ convertColorFilter(colorFilter ?: image.modifiers?.colorFilter)
+ )
+ }
+ }
+ }
+
+ private fun drawComposeImage(
+ image: ImageBitmapView,
+ x: Int,
+ y: Int,
+ width: Int?,
+ height: Int?,
+ srcX: Int,
+ srcY: Int,
+ srcWidth: Int?,
+ srcHeight: Int?,
+ alpha: Float,
+ colorFilter: ColorFilter? = null
+ ) {
+ if (image.imageBitmap.width == 0 || image.imageBitmap.height == 0) return
+ if (image.size.width == 0 || image.size.height == 0) return
+ val targetSize = IntSize(width ?: image.size.width, height ?: image.size.height)
+ if (targetSize.width == 0 || targetSize.height == 0) return
+ drawScope.drawImage(
+ image.imageBitmap,
+ IntOffset(srcX, srcY),
+ IntSize(srcWidth ?: image.size.width, srcHeight ?: image.size.height),
+ dstOffset = IntOffset(x, y),
+ dstSize = targetSize,
+ alpha = alpha,
+ colorFilter = convertColorFilter(colorFilter),
+ filterQuality = FilterQuality.Low
+ )
+ }
+
+ private fun convertColorFilter(colorFilter: ColorFilter?): androidx.compose.ui.graphics.ColorFilter? {
+ return (overrideColorFilter ?: colorFilter)?.toCompose()
+ }
+}
+
+@GenerateDataFunctions
+internal class ComposeScalingContext(
+ override val display: Float
+) : ScalingContext {
+ override fun equals(other: Any?): Boolean {
+ if (this === other) return true
+ if (javaClass != other?.javaClass) return false
+
+ other as ComposeScalingContext
+
+ return display == other.display
+ }
+
+ override fun hashCode(): Int {
+ return display.hashCode()
+ }
+
+ override fun toString(): String {
+ return "ComposeScalingContext(display=$display)"
+ }
+}
diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/IconKey.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/IconKey.kt
index 8aaa8bbc0ad2f..3abfba01e95bd 100644
--- a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/IconKey.kt
+++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/IconKey.kt
@@ -1,5 +1,8 @@
package org.jetbrains.jewel.ui.icon
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.design.IconDesigner
+import org.jetbrains.icons.modifiers.IconModifier
import org.jetbrains.jewel.foundation.GenerateDataFunctions
public interface IconKey {
@@ -8,6 +11,11 @@ public interface IconKey {
public fun path(isNewUi: Boolean): String
}
+@ApiStatus.Experimental
+public fun IconDesigner.iconKey(iconKey: IconKey, modifier: IconModifier = IconModifier) {
+ image(iconKey.path(isNewUi = true), iconKey.iconClass.classLoader, modifier)
+}
+
@GenerateDataFunctions
public class PathIconKey(private val path: String, override val iconClass: Class<*>) : IconKey {
override fun path(isNewUi: Boolean): String = path
diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/IconUtils.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/IconUtils.kt
new file mode 100644
index 0000000000000..7170adebd6411
--- /dev/null
+++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/IconUtils.kt
@@ -0,0 +1,103 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.jewel.ui.icon
+
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.Dp
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.design.IconAlign
+import org.jetbrains.icons.design.Shape
+import org.jetbrains.icons.design.IconDesigner
+import org.jetbrains.icons.design.IconUnit
+import org.jetbrains.icons.design.RGBA
+import org.jetbrains.icons.design.badge
+import org.jetbrains.icons.design.dp
+import org.jetbrains.icons.design.relativeTo
+import org.jetbrains.icons.filters.ColorFilter
+import org.jetbrains.icons.filters.TintColorFilter
+import org.jetbrains.icons.modifiers.IconModifier
+import org.jetbrains.icons.modifiers.size
+import org.jetbrains.icons.modifiers.stroke
+import org.jetbrains.icons.modifiers.tintColor
+import org.jetbrains.jewel.foundation.ExperimentalJewelApi
+
+@ExperimentalJewelApi
+@ApiStatus.Experimental
+public fun IconModifier.tintColor(composeColor: Color, blendMode: BlendMode = BlendMode.SrcIn): IconModifier {
+ return tintColor(composeColor.toIconsColor(), blendMode.toIconsBlendMode())
+}
+
+@ExperimentalJewelApi
+@ApiStatus.Experimental
+public fun IconModifier.stroke(composeColor: Color): IconModifier {
+ return stroke(composeColor.toIconsColor())
+}
+
+@ExperimentalJewelApi
+@ApiStatus.Experimental
+public fun IconModifier.size(size: Dp): IconModifier {
+ return size(size.value.dp)
+}
+
+
+@ExperimentalJewelApi
+@ApiStatus.Experimental
+public fun IconDesigner.badge(
+ color: Color,
+ shape: Shape,
+ size: IconUnit = (3.5 * 2).dp relativeTo 20.dp,
+ align: IconAlign = IconAlign.TopRight,
+ cutout: IconUnit = 1.5.dp relativeTo 20.dp,
+ modifier: IconModifier = IconModifier,
+) {
+ badge(color.toIconsColor(), shape, size, align, cutout, modifier)
+}
+
+@ExperimentalJewelApi
+@ApiStatus.Experimental
+public fun ColorFilter.toCompose(): androidx.compose.ui.graphics.ColorFilter {
+ return when (this) {
+ is TintColorFilter -> androidx.compose.ui.graphics.ColorFilter.tint(color.toCompose(), blendMode.toCompose())
+ }
+}
+
+@ExperimentalJewelApi
+@ApiStatus.Experimental
+public fun org.jetbrains.icons.design.Color.toCompose(): Color {
+ return when (this) {
+ is RGBA -> Color(red, green, blue, alpha)
+ }
+}
+
+@ExperimentalJewelApi
+@ApiStatus.Experimental
+public fun org.jetbrains.icons.design.BlendMode.toCompose(): BlendMode {
+ return when (this) {
+ org.jetbrains.icons.design.BlendMode.SrcIn -> BlendMode.SrcIn
+ org.jetbrains.icons.design.BlendMode.Color -> BlendMode.Color
+ org.jetbrains.icons.design.BlendMode.Hue -> BlendMode.Hue
+ org.jetbrains.icons.design.BlendMode.Luminosity -> BlendMode.Luminosity
+ org.jetbrains.icons.design.BlendMode.Saturation -> BlendMode.Saturation
+ org.jetbrains.icons.design.BlendMode.Multiply -> BlendMode.Multiply
+ }
+}
+
+@ExperimentalJewelApi
+@ApiStatus.Experimental
+public fun BlendMode.toIconsBlendMode(): org.jetbrains.icons.design.BlendMode {
+ return when (this) {
+ BlendMode.SrcIn -> org.jetbrains.icons.design.BlendMode.SrcIn
+ BlendMode.Color -> org.jetbrains.icons.design.BlendMode.Color
+ BlendMode.Hue -> org.jetbrains.icons.design.BlendMode.Hue
+ BlendMode.Luminosity -> org.jetbrains.icons.design.BlendMode.Luminosity
+ BlendMode.Saturation -> org.jetbrains.icons.design.BlendMode.Saturation
+ BlendMode.Multiply -> org.jetbrains.icons.design.BlendMode.Multiply
+ else -> error("Unsupported Compose blend mode $this")
+ }
+}
+
+@ExperimentalJewelApi
+@ApiStatus.Experimental
+public fun Color.toIconsColor(): RGBA {
+ return RGBA(red, green, blue, alpha)
+}
diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/PathImageResourceLocation.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/PathImageResourceLocation.kt
new file mode 100644
index 0000000000000..878b78ab6eccd
--- /dev/null
+++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/PathImageResourceLocation.kt
@@ -0,0 +1,36 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.jewel.ui.icon
+
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.ImageResourceLocation
+import org.jetbrains.icons.impl.rendering.DefaultImageModifiers
+import org.jetbrains.icons.rendering.ImageModifiers
+import org.jetbrains.jewel.foundation.InternalJewelApi
+
+// TODO: Replace with ModuleImageResourceLocation and custom Loader for it
+@InternalJewelApi
+@ApiStatus.Internal
+public class PathImageResourceLocation(public val path: String, public val classLoader: ClassLoader?) : ImageResourceLocation {
+ public fun loadData(imageModifiers: ImageModifiers?): ByteArray {
+ val knownMods = imageModifiers as? DefaultImageModifiers
+ val finalPath = applyPathModifiers(path, knownMods)
+ val resourceStream = if (classLoader != null) {
+ classLoader.getResourceAsStream(finalPath)
+ } else ClassLoader.getSystemResourceAsStream(path)
+ return resourceStream?.readBytes() ?: error("Resource not found: $finalPath")
+ }
+
+ private fun applyPathModifiers(path: String, modifiers: DefaultImageModifiers?): String {
+ if (modifiers == null) return path
+ return buildString {
+ append(path.substringBeforeLast('/', ""))
+ append('/')
+ append(path.substringBeforeLast('.').substringAfterLast('/'))
+ if (modifiers.isDark) {
+ append("_dark")
+ }
+ append('.')
+ append(path.substringAfterLast('.'))
+ }
+ }
+}
diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/RendererBasedIconPainter.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/RendererBasedIconPainter.kt
new file mode 100644
index 0000000000000..53d0215460d91
--- /dev/null
+++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/RendererBasedIconPainter.kt
@@ -0,0 +1,59 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package org.jetbrains.jewel.ui.icon
+
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.geometry.Rect
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Paint
+import androidx.compose.ui.graphics.drawscope.DrawScope
+import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.graphics.withSaveLayer
+import androidx.compose.ui.platform.LocalDensity
+import org.jetbrains.annotations.ApiStatus
+import org.jetbrains.icons.rendering.Dimensions
+import org.jetbrains.icons.rendering.IconRenderer
+import org.jetbrains.icons.rendering.ScalingContext
+import org.jetbrains.jewel.foundation.InternalJewelApi
+
+@InternalJewelApi
+@ApiStatus.Internal
+public class RendererBasedIconPainter(private val iconRenderer: IconRenderer, private val scaling: ScalingContext) : Painter() {
+ override val intrinsicSize: Size = iconRenderer.calculateExpectedDimensions(scaling).toComposeSize()
+
+ private var layerPaint: Paint? = null
+ private fun obtainPaint(): Paint {
+ var target = layerPaint
+ if (target == null) {
+ target = Paint()
+ layerPaint = target
+ }
+ return target
+ }
+
+ override fun DrawScope.onDraw() {
+ val api = ComposePaintingApi(this, scaling = scaling)
+ val layerRect = Rect(Offset.Zero, Size(size.width, size.height))
+ drawIntoCanvas { canvas ->
+ canvas.withSaveLayer(layerRect, obtainPaint()) {
+ iconRenderer.render(api)
+ }
+ }
+ }
+
+ public companion object {
+ @Composable
+ @InternalJewelApi
+ @ApiStatus.Internal
+ public fun inferScalingContext(): ScalingContext {
+ return ComposeScalingContext(
+ LocalDensity.current.density
+ )
+ }
+ }
+}
+
+private fun Dimensions.toComposeSize(): Size {
+ return Size(width.toFloat(), height.toFloat())
+}
diff --git a/platform/jewel/ui/src/main/resources/intellij.platform.jewel.ui.xml b/platform/jewel/ui/src/main/resources/intellij.platform.jewel.ui.xml
index e08b82484c815..b8e93d174d476 100644
--- a/platform/jewel/ui/src/main/resources/intellij.platform.jewel.ui.xml
+++ b/platform/jewel/ui/src/main/resources/intellij.platform.jewel.ui.xml
@@ -1,7 +1,9 @@
-
+
+
+
diff --git a/platform/lang-impl/module-content.yaml b/platform/lang-impl/module-content.yaml
index 042e844b3276d..2d863e30ca6d1 100644
--- a/platform/lang-impl/module-content.yaml
+++ b/platform/lang-impl/module-content.yaml
@@ -3,6 +3,8 @@
- name: intellij.platform.configurationStore.impl
- name: intellij.platform.execution.impl
- name: intellij.platform.foldings
+ - name: intellij.platform.icons.api.rendering
+ - name: intellij.platform.icons.impl
- name: intellij.platform.ide.bootstrap
- name: intellij.platform.ide.bootstrap.eel
- name: intellij.platform.ide.bootstrap.kernel
diff --git a/platform/platform-impl/bootstrap/BUILD.bazel b/platform/platform-impl/bootstrap/BUILD.bazel
index 6cdd10ef73437..ecc235f81a228 100644
--- a/platform/platform-impl/bootstrap/BUILD.bazel
+++ b/platform/platform-impl/bootstrap/BUILD.bazel
@@ -46,6 +46,8 @@ jvm_library(
"//platform/eel-impl",
"//platform/platform-impl/initial-config-import",
"//platform/eel-provider",
+ "//platform/icons-impl",
+ "//platform/icons-impl/intellij",
]
)
@@ -92,6 +94,8 @@ jvm_library(
"//platform/eel-impl:eel-impl_test_lib",
"//platform/platform-impl/initial-config-import:initial-config-import_test_lib",
"//platform/eel-provider:eel-provider_test_lib",
+ "//platform/icons-impl:icons-impl_test_lib",
+ "//platform/icons-impl/intellij:intellij_test_lib",
]
)
### auto-generated section `build intellij.platform.ide.bootstrap` end
\ No newline at end of file
diff --git a/platform/platform-impl/bootstrap/intellij.platform.ide.bootstrap.iml b/platform/platform-impl/bootstrap/intellij.platform.ide.bootstrap.iml
index f86862f009fc2..59decad7ed82e 100644
--- a/platform/platform-impl/bootstrap/intellij.platform.ide.bootstrap.iml
+++ b/platform/platform-impl/bootstrap/intellij.platform.ide.bootstrap.iml
@@ -46,5 +46,7 @@
+
+
\ No newline at end of file
diff --git a/platform/platform-impl/bootstrap/src/com/intellij/platform/ide/bootstrap/ui.kt b/platform/platform-impl/bootstrap/src/com/intellij/platform/ide/bootstrap/ui.kt
index 14d51c648b445..a1e5a7fb0ddc7 100644
--- a/platform/platform-impl/bootstrap/src/com/intellij/platform/ide/bootstrap/ui.kt
+++ b/platform/platform-impl/bootstrap/src/com/intellij/platform/ide/bootstrap/ui.kt
@@ -34,6 +34,11 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.jetbrains.annotations.VisibleForTesting
+import org.jetbrains.icons.impl.intellij.IntelliJIconManager
+import org.jetbrains.icons.impl.intellij.rendering.IntelliJIconRendererManager
+import org.jetbrains.icons.impl.intellij.rendering.images.IntelliJImageResourceProvider
+import org.jetbrains.icons.rendering.IconRendererManager
+import org.jetbrains.icons.rendering.ImageResourceProvider
import java.awt.Font
import java.awt.GraphicsEnvironment
import java.awt.Toolkit
@@ -48,7 +53,11 @@ internal suspend fun initUi(initAwtToolkitJob: Job, isHeadless: Boolean, asyncSc
// IdeaLaF uses AllIcons - icon manager must be activated
if (!isHeadless) {
span("icon manager activation") {
- IconManager.activate(CoreIconManager())
+ val iconManager = CoreIconManager()
+ IconManager.activate(iconManager)
+ }
+ span("new icon manager activation") {
+ IntelliJIconManager.activate()
}
}
diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.core.ide.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.core.ide.xml
index 4c4131e9934e4..c3bb9d77bf8d2 100644
--- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.core.ide.xml
+++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.core.ide.xml
@@ -119,6 +119,7 @@
+
diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.core.lang.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.core.lang.xml
index 539f655e28e5a..b1428d6388910 100644
--- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.core.lang.xml
+++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.core.lang.xml
@@ -120,6 +120,7 @@
+
diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.minimal.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.minimal.xml
index 15ac00f3a8694..6cb7098139b16 100644
--- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.minimal.xml
+++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.minimal.xml
@@ -121,6 +121,7 @@
+
diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.xml
index 39e7a937950a1..a841c4ef439d4 100644
--- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.xml
+++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.essential.xml
@@ -122,6 +122,7 @@
+
diff --git a/platform/platform-resources/generated/META-INF/intellij.moduleSets.ide.common.xml b/platform/platform-resources/generated/META-INF/intellij.moduleSets.ide.common.xml
index 8fcdda615fbf0..66299b07654ff 100644
--- a/platform/platform-resources/generated/META-INF/intellij.moduleSets.ide.common.xml
+++ b/platform/platform-resources/generated/META-INF/intellij.moduleSets.ide.common.xml
@@ -125,6 +125,7 @@
+
diff --git a/platform/util/BUILD.bazel b/platform/util/BUILD.bazel
index ca4cd099714ca..3c4b1b6fb05d9 100644
--- a/platform/util/BUILD.bazel
+++ b/platform/util/BUILD.bazel
@@ -40,6 +40,8 @@ jvm_library(
"//platform/util/coroutines",
"//platform/util/multiplatform",
"//platform/eel",
+ "//platform/icons-api",
+ "//platform/icons-api/rendering",
":platform_util_troveCompileOnly_provided",
],
exports = [
@@ -47,6 +49,8 @@ jvm_library(
"//platform/util-rt",
"//platform/util/base",
"//platform/util/multiplatform",
+ "//platform/icons-api",
+ "//platform/icons-api/rendering",
],
runtime_deps = [
"//libraries/commons/io",
@@ -69,6 +73,10 @@ jvm_library(
"//platform/util/base:base_test_lib",
"//platform/util/multiplatform",
"//platform/util/multiplatform:multiplatform_test_lib",
+ "//platform/icons-api",
+ "//platform/icons-api:icons-api_test_lib",
+ "//platform/icons-api/rendering",
+ "//platform/icons-api/rendering:rendering_test_lib",
],
runtime_deps = [
":util",
@@ -93,6 +101,8 @@ jvm_library(
"//platform/util/coroutines:coroutines_test_lib",
"//platform/util/multiplatform:multiplatform_test_lib",
"//platform/eel:eel_test_lib",
+ "//platform/icons-api:icons-api_test_lib",
+ "//platform/icons-api/rendering:rendering_test_lib",
]
)
### auto-generated section `build intellij.platform.util` end
diff --git a/platform/util/intellij.platform.util.iml b/platform/util/intellij.platform.util.iml
index 29aa589b65012..7aab8e448e7b5 100644
--- a/platform/util/intellij.platform.util.iml
+++ b/platform/util/intellij.platform.util.iml
@@ -54,6 +54,8 @@
+
+
diff --git a/platform/util/ui/src/com/intellij/ui/icons/ImageDataByPathLoader.kt b/platform/util/ui/src/com/intellij/ui/icons/ImageDataByPathLoader.kt
index f213baabc0588..4775423176536 100644
--- a/platform/util/ui/src/com/intellij/ui/icons/ImageDataByPathLoader.kt
+++ b/platform/util/ui/src/com/intellij/ui/icons/ImageDataByPathLoader.kt
@@ -14,6 +14,21 @@ import java.net.URL
import java.util.function.Supplier
import javax.swing.Icon
+@Internal
+fun findIconLoaderByPath(path: String, classLoader: ClassLoader): ImageDataLoader {
+ val originalPath = normalizePath(path)
+ val patched = patchIconPath(originalPath, classLoader)
+ val effectivePath = patched?.first ?: originalPath
+ val effectiveClassLoader = patched?.second ?: classLoader
+
+ return ImageDataByPathLoader.createLoader(originalPath = originalPath,
+ originalClassLoader = effectiveClassLoader,
+ patched = patched,
+ path = effectivePath,
+ classLoader = effectiveClassLoader
+ )
+}
+
@Internal
fun findIconByPath(
@NonNls path: String,
@@ -62,14 +77,28 @@ internal class ImageDataByPathLoader private constructor(override val path: Stri
private val classLoader: ClassLoader,
private val original: ImageDataByPathLoader?) : ImageDataLoader {
companion object {
+ internal fun createLoader(originalPath: @NonNls String,
+ originalClassLoader: ClassLoader,
+ patched: Pair?,
+ path: String,
+ classLoader: ClassLoader): ImageDataByPathLoader {
+ val originalLoader = ImageDataByPathLoader(path = originalPath, classLoader = originalClassLoader, original = null)
+ return if (patched == null) originalLoader else ImageDataByPathLoader(path = path, classLoader = classLoader, original = originalLoader)
+ }
+
internal fun createIcon(originalPath: @NonNls String,
originalClassLoader: ClassLoader,
patched: Pair?,
path: String,
classLoader: ClassLoader,
toolTip: Supplier? = null): CachedImageIcon {
- val originalLoader = ImageDataByPathLoader(path = originalPath, classLoader = originalClassLoader, original = null)
- val loader = if (patched == null) originalLoader else ImageDataByPathLoader(path = path, classLoader = classLoader, original = originalLoader)
+ val loader = createLoader(
+ originalPath,
+ originalClassLoader,
+ patched,
+ path,
+ classLoader
+ )
return CachedImageIcon(loader = loader, toolTip = toolTip, originalLoader = loader.original ?: loader)
}
diff --git a/plugins/devkit/intellij.devkit.compose/BUILD.bazel b/plugins/devkit/intellij.devkit.compose/BUILD.bazel
index 4d4ffb192adeb..0de1a9024cd55 100644
--- a/plugins/devkit/intellij.devkit.compose/BUILD.bazel
+++ b/plugins/devkit/intellij.devkit.compose/BUILD.bazel
@@ -63,6 +63,10 @@ jvm_library(
"//platform/util/jdom",
"//platform/external-system-impl:externalSystem-impl",
"//plugins/gradle",
+ "//platform/platform-impl/rpc",
+ "//fleet/util/core",
+ "//libraries/kotlinx/serialization/json",
+ "//libraries/kotlinx/serialization/core",
],
plugins = ["@lib//:compose-plugin"]
)
@@ -112,6 +116,10 @@ jvm_library(
"//platform/util/jdom:jdom_test_lib",
"//platform/external-system-impl:externalSystem-impl_test_lib",
"//plugins/gradle:gradle_test_lib",
+ "//platform/platform-impl/rpc:rpc_test_lib",
+ "//fleet/util/core:core_test_lib",
+ "//libraries/kotlinx/serialization/json:json_test_lib",
+ "//libraries/kotlinx/serialization/core:core_test_lib",
],
plugins = ["@lib//:compose-plugin"]
)
diff --git a/plugins/devkit/intellij.devkit.compose/intellij.devkit.compose.iml b/plugins/devkit/intellij.devkit.compose/intellij.devkit.compose.iml
index 6d956224c0c3f..61f8a6fd1f7f1 100644
--- a/plugins/devkit/intellij.devkit.compose/intellij.devkit.compose.iml
+++ b/plugins/devkit/intellij.devkit.compose/intellij.devkit.compose.iml
@@ -68,5 +68,9 @@
+
+
+
+
\ No newline at end of file
diff --git a/plugins/devkit/intellij.devkit.compose/src/demo/ComponentShowcaseTab.kt b/plugins/devkit/intellij.devkit.compose/src/demo/ComponentShowcaseTab.kt
index effb48ba9ca46..0c5fe5e1a4952 100644
--- a/plugins/devkit/intellij.devkit.compose/src/demo/ComponentShowcaseTab.kt
+++ b/plugins/devkit/intellij.devkit.compose/src/demo/ComponentShowcaseTab.kt
@@ -38,10 +38,14 @@ import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.onFirstVisible
import androidx.compose.ui.unit.dp
+import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
import com.intellij.openapi.project.Project
+import com.intellij.ui.IconDeferrer
import com.intellij.ui.JBColor
import com.intellij.util.ui.JBUI
+import kotlinx.coroutines.delay
+import org.jetbrains.icons.Icon
import org.jetbrains.jewel.bridge.toComposeColor
import org.jetbrains.jewel.foundation.LocalComponent
import org.jetbrains.jewel.foundation.actionSystem.provideData
@@ -457,7 +461,7 @@ private fun IconsShowcase() {
}
IconButton(onClick = {}, Modifier.size(24.dp)) {
- Icon(key = AllIconsKeys.Actions.Close, contentDescription = "Close")
+ Icon(AllIconsKeys.Actions.Close, "Close")
}
IconActionButton(
@@ -468,9 +472,25 @@ private fun IconsShowcase() {
hints = arrayOf(Size(24)),
tooltip = { Text("Hello there") },
)
+
+ Box {
+ Icon(AllIcons.General.OpenDisk as Icon, "Build Load Changes")
+ }
+
+ Box {
+ Icon(deferedIcon as Icon, "Deferred Icon Sample")
+ }
}
}
+private val deferedIcon = IconDeferrer.getInstance().deferAsync(
+ AllIcons.General.Print,
+ "KABOOM-DEF_ICON_TST"
+) {
+ delay(10000)
+ AllIcons.General.GreenCheckmark
+}
+
@Composable
private fun RowScope.ColumnTwo(project: Project) {
Column(Modifier.trackActivation().weight(1f), verticalArrangement = Arrangement.spacedBy(16.dp)) {
diff --git a/plugins/devkit/intellij.devkit.compose/src/showcase/ComposePerformanceDemoAction.kt b/plugins/devkit/intellij.devkit.compose/src/showcase/ComposePerformanceDemoAction.kt
index ac2735366f0b6..b02e89a1f5b43 100644
--- a/plugins/devkit/intellij.devkit.compose/src/showcase/ComposePerformanceDemoAction.kt
+++ b/plugins/devkit/intellij.devkit.compose/src/showcase/ComposePerformanceDemoAction.kt
@@ -1,6 +1,7 @@
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.devkit.compose.showcase
+import androidx.compose.animation.core.EaseInOut
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
@@ -8,7 +9,9 @@ import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -18,8 +21,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.withFrameNanos
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color
@@ -31,18 +37,27 @@ import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
+import com.intellij.icons.AllIcons
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
+import org.jetbrains.icons.modifiers.IconModifier
+import org.jetbrains.icons.modifiers.fillMaxSize
+import org.jetbrains.icons.swing.toNewIcon
import org.jetbrains.jewel.bridge.compose
+import org.jetbrains.jewel.ui.component.Checkbox
+import org.jetbrains.jewel.ui.component.Icon
import org.jetbrains.jewel.ui.component.Slider
import org.jetbrains.jewel.ui.component.Text
+import org.jetbrains.jewel.ui.icon.IconKey
+import org.jetbrains.jewel.ui.icons.AllIconsKeys
import java.awt.BorderLayout
import java.util.LinkedList
import javax.swing.BoxLayout
import javax.swing.ButtonGroup
+import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.JLabel
import javax.swing.JPanel
@@ -69,7 +84,7 @@ private class MyDialog(project: Project?, dialogTitle: String) :
DialogWrapper(project, null, true, IdeModalityType.MODELESS, true) {
val centerPanelWrapper = JPanel(BorderLayout())
- enum class TestCase { TextAnimation, Canvas }
+ enum class TestCase { TextAnimation, Canvas, Icons }
enum class Mode { Swing, AWT }
var mode = Mode.Swing
@@ -94,17 +109,22 @@ private class MyDialog(project: Project?, dialogTitle: String) :
ButtonGroup().let { group ->
val textAnimationButton = JRadioButton("Text animation")
val canvasButton = JRadioButton("Canvas")
+ val iconsButton = JRadioButton("Icons")
group.add(textAnimationButton)
group.add(canvasButton)
+ group.add(iconsButton)
textAnimationButton.isSelected = testCase == TestCase.TextAnimation
canvasButton.isSelected = testCase == TestCase.Canvas
+ iconsButton.isSelected = testCase == TestCase.Icons
textAnimationButton.addActionListener { testCase = TestCase.TextAnimation; initCentralPanel() }
canvasButton.addActionListener { testCase = TestCase.Canvas; initCentralPanel() }
+ iconsButton.addActionListener { testCase = TestCase.Icons; initCentralPanel() }
controlPanel.add(canvasButton)
controlPanel.add(textAnimationButton)
+ controlPanel.add(iconsButton)
}
controlPanel.add(JSeparator(JSeparator.VERTICAL))
@@ -139,6 +159,7 @@ private class MyDialog(project: Project?, dialogTitle: String) :
val comp = when (testCase) {
TestCase.TextAnimation -> createTextAnimationComponent()
TestCase.Canvas -> createClockComponent()
+ TestCase.Icons -> createIconsComponent()
}
centerPanelWrapper.add(comp)
@@ -149,6 +170,111 @@ private class MyDialog(project: Project?, dialogTitle: String) :
}
}
+private fun createIconsComponent(): JComponent {
+ return compose {
+ var minFps by remember { mutableStateOf(Int.MAX_VALUE) }
+ var maxFps by remember { mutableStateOf(Int.MIN_VALUE) }
+ val frameTimes = remember { LinkedList() }
+ SideEffect {
+ frameTimes.add(System.nanoTime())
+ frameTimes.removeAll { it < System.nanoTime() - 1_000_000_000 }
+ }
+
+ val transition = rememberInfiniteTransition("coso")
+ val iconSize by transition.animateFloat(
+ 30f,
+ 50f,
+ infiniteRepeatable(tween(durationMillis = 1000, easing = EaseInOut), repeatMode = RepeatMode.Reverse),
+ )
+
+ Column {
+ val fps = frameTimes.size
+ if (fps > maxFps) {
+ maxFps = fps
+ }
+ if (fps < minFps) {
+ minFps = fps
+ }
+ Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
+ Text(
+ text = "FPS: $fps",
+ modifier = Modifier.padding(10.dp),
+ fontSize = 25.sp,
+ color = Color.Red
+ )
+ Text(
+ text = "MIN: $minFps",
+ modifier = Modifier.padding(10.dp),
+ fontSize = 25.sp,
+ color = Color.Red
+ )
+ Text(
+ text = "MAX: $maxFps",
+ modifier = Modifier.padding(10.dp),
+ fontSize = 25.sp,
+ color = Color.Red
+ )
+ }
+
+ var useComposeIcons by remember { mutableStateOf(false) }
+ Row {
+ Text("Use Compose Icons: ")
+ Checkbox(useComposeIcons, {
+ maxFps = Int.MIN_VALUE
+ minFps = Int.MAX_VALUE
+ useComposeIcons = it
+ })
+ }
+
+ Column(modifier = Modifier.fillMaxSize().clipToBounds(), verticalArrangement = Arrangement.Center) {
+ if (useComposeIcons) {
+ val icons = remember {
+ val icons = mutableListOf()
+ for (nested in AllIconsKeys::class.java.nestMembers) {
+ val fields = nested.declaredFields.filter { it.type.name.contains("IconKey") }
+ icons.addAll(fields.map { it.get(null) as IconKey })
+ }
+ icons
+ }
+
+ var i = 1
+ for (x in 0..30) {
+ Row {
+ for (y in 0..30) {
+ Column(modifier = Modifier.size(55.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
+ val icon = icons[i++ % icons.size]
+ Icon(icon, "Balloon", modifier = Modifier.size(iconSize.dp).padding(10.dp))
+ }
+ }
+ }
+ }
+ } else {
+ val icons = remember {
+ val icons = mutableListOf()
+ for (nested in AllIcons::class.java.nestMembers) {
+ val fields = nested.declaredFields.filter { it.type.name.contains("Icon") }
+ icons.addAll(fields.map { (it.get(null) as Icon).toNewIcon(IconModifier.fillMaxSize()) })
+ }
+ icons
+ }
+
+ var i = 1
+ for (x in 0..30) {
+ Row {
+ for (y in 0..30) {
+ Column(modifier = Modifier.size(55.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.Center) {
+ val icon = icons[i++ % icons.size]
+ Icon(icon, "Balloon", modifier = Modifier.size(iconSize.dp).padding(10.dp))
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
private fun createClockComponent(): JComponent {
return compose {
val mode = if (System.getProperty("compose.swing.render.on.graphics", "false").toBoolean()) "Swing" else "AWT"
diff --git a/plugins/devkit/intellij.devkit.compose/src/showcase/ComposeShowcase.kt b/plugins/devkit/intellij.devkit.compose/src/showcase/ComposeShowcase.kt
index 516312fa006d3..414d7743f93b5 100644
--- a/plugins/devkit/intellij.devkit.compose/src/showcase/ComposeShowcase.kt
+++ b/plugins/devkit/intellij.devkit.compose/src/showcase/ComposeShowcase.kt
@@ -50,6 +50,7 @@ import com.intellij.icons.AllIcons
import com.intellij.openapi.fileChooser.FileChooser
import com.intellij.openapi.fileChooser.FileChooserDescriptorFactory
import com.intellij.openapi.keymap.KeymapUtil
+import com.intellij.openapi.project.Project
import com.intellij.ui.UIBundle
import com.intellij.util.ui.JBUI
import kotlinx.coroutines.Dispatchers.IO
@@ -84,8 +85,9 @@ import java.awt.event.KeyEvent
import java.io.File
import javax.swing.KeyStroke
+
@Composable
-internal fun ComposeShowcase() {
+internal fun ComposeShowcase(project: Project) {
Column(
verticalArrangement = Arrangement.spacedBy(15.dp),
modifier = Modifier.padding(10.dp)
@@ -110,6 +112,7 @@ internal fun ComposeShowcase() {
TextFieldWithButton()
TooltipAreaSimple()
InfiniteAnimation()
+ Icons(project)
}
}
}
diff --git a/plugins/devkit/intellij.devkit.compose/src/showcase/ComposeShowcaseAction.kt b/plugins/devkit/intellij.devkit.compose/src/showcase/ComposeShowcaseAction.kt
index a92106ffecf83..fa35f0c62e1ae 100644
--- a/plugins/devkit/intellij.devkit.compose/src/showcase/ComposeShowcaseAction.kt
+++ b/plugins/devkit/intellij.devkit.compose/src/showcase/ComposeShowcaseAction.kt
@@ -13,9 +13,9 @@ import org.jetbrains.jewel.bridge.compose
import java.awt.Dimension
import javax.swing.JComponent
-private fun createComposeShowcaseComponent(): JComponent {
+private fun createComposeShowcaseComponent(project: Project): JComponent {
return compose {
- ComposeShowcase()
+ ComposeShowcase(project)
}
}
@@ -23,7 +23,7 @@ internal class ComposeShowcaseAction : DumbAwareAction() {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
override fun update(e: AnActionEvent) {
- e.presentation.isEnabledAndVisible = e.project != null && PsiUtil.isPluginProject(e.project!!)
+ e.presentation.isEnabledAndVisible = e.project != null // && PsiUtil.isPluginProject(e.project!!)
}
override fun actionPerformed(e: AnActionEvent) {
@@ -31,7 +31,7 @@ internal class ComposeShowcaseAction : DumbAwareAction() {
}
}
-private class ComposeShowcaseDialog(project: Project?, @NlsSafe dialogTitle: String) :
+private class ComposeShowcaseDialog(val project: Project?, @NlsSafe dialogTitle: String) :
DialogWrapper(project, null, true, IdeModalityType.MODELESS, false) {
init {
@@ -40,7 +40,7 @@ private class ComposeShowcaseDialog(project: Project?, @NlsSafe dialogTitle: Str
}
override fun createCenterPanel(): JComponent {
- return Wrapper(createComposeShowcaseComponent()).apply {
+ return Wrapper(createComposeShowcaseComponent(project!!)).apply {
minimumSize = Dimension(200, 100)
preferredSize = Dimension(800, 600)
}
diff --git a/plugins/devkit/intellij.devkit.compose/src/showcase/Icons.kt b/plugins/devkit/intellij.devkit.compose/src/showcase/Icons.kt
new file mode 100644
index 0000000000000..3b35eb9bf9118
--- /dev/null
+++ b/plugins/devkit/intellij.devkit.compose/src/showcase/Icons.kt
@@ -0,0 +1,257 @@
+// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+package com.intellij.devkit.compose.showcase
+
+import androidx.compose.animation.core.EaseInOut
+import androidx.compose.animation.core.RepeatMode
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.awt.SwingPanel
+import androidx.compose.ui.graphics.BlendMode
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.unit.dp
+import com.intellij.icons.AllIcons
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.project.guessProjectDir
+import com.intellij.openapi.vfs.findFile
+import com.intellij.ui.BadgeDotProvider
+import com.intellij.ui.BadgeIcon
+import com.intellij.ui.SpinningProgressIcon
+import com.intellij.util.IconUtil
+import kotlinx.coroutines.delay
+import kotlinx.serialization.json.Json
+import org.jetbrains.icons.Icon
+import org.jetbrains.icons.IconManager
+import org.jetbrains.icons.deferredIcon
+import org.jetbrains.icons.design.Circle
+import org.jetbrains.icons.design.IconAlign
+import org.jetbrains.icons.design.percent
+import org.jetbrains.icons.icon
+import org.jetbrains.icons.swing.swingIcon
+import org.jetbrains.icons.swing.toNewIcon
+import org.jetbrains.icons.swing.toSwingIcon
+import org.jetbrains.icons.modifiers.IconModifier
+import org.jetbrains.icons.modifiers.align
+import org.jetbrains.icons.modifiers.fillMaxSize
+import org.jetbrains.icons.modifiers.patchSvg
+import org.jetbrains.icons.modifiers.size
+import org.jetbrains.jewel.bridge.toAwtColor
+import org.jetbrains.jewel.foundation.ExperimentalJewelApi
+import org.jetbrains.jewel.ui.component.Icon
+import org.jetbrains.jewel.ui.component.Text
+import org.jetbrains.jewel.ui.icon.badge
+import org.jetbrains.jewel.ui.icon.tintColor
+import javax.swing.BoxLayout
+import javax.swing.JLabel
+import javax.swing.JPanel
+
+@OptIn(ExperimentalJewelApi::class)
+@Composable
+internal fun Icons(project: Project) {
+ val icons = mutableListOf()
+ val missingIcon = remember { AllIcons.General.Error.toNewIcon(modifier = IconModifier.fillMaxSize()) }
+
+ val transition = rememberInfiniteTransition()
+ val iconSize by transition.animateFloat(
+ 30f,
+ 80f,
+ infiniteRepeatable(tween(durationMillis = 1000, easing = EaseInOut), repeatMode = RepeatMode.Reverse),
+ )
+
+ Box(Modifier.size(80.dp)) {
+ Icon(icon {
+ swingIcon(AllIcons.Actions.NewFolder, modifier = IconModifier.fillMaxSize())
+ }, "New Folder", modifier = Modifier.size(iconSize.dp))
+ }
+
+ val duration = 50L
+ val animatedIcon = remember {
+ icon {
+ animation {
+ frame(duration) {
+ swingIcon(AllIcons.Process.Step_1)
+ }
+ frame(duration) {
+ swingIcon(AllIcons.Process.Step_2)
+ }
+ frame(duration) {
+ swingIcon(AllIcons.Process.Step_3)
+ }
+ frame(duration) {
+ swingIcon(AllIcons.Process.Step_4)
+ }
+ frame(duration) {
+ swingIcon(AllIcons.Process.Step_5)
+ }
+ frame(duration) {
+ swingIcon(AllIcons.Process.Step_6)
+ }
+ frame(duration) {
+ swingIcon(AllIcons.Process.Step_7)
+ }
+ frame(duration) {
+ swingIcon(AllIcons.Process.Step_8)
+ }
+ }
+ }
+ }
+
+ icons.add(remember {
+ ShowcaseIcon(
+ icon {
+ icon(missingIcon)
+ badge(Color.Green, Circle)
+ },
+ BadgeIcon(AllIcons.General.Error, Color.Green.toAwtColor(), BadgeDotProvider()),
+ "Icon with Badge"
+ )
+ })
+
+ val size = 30.percent
+ icons.add(remember {
+ ShowcaseIcon(
+ icon {
+ column(spacing = 5.percent, modifier = IconModifier.fillMaxSize()) {
+ row(spacing = 5.percent) {
+ icon(missingIcon, modifier = IconModifier.tintColor(Color.Yellow, BlendMode.Color))
+ icon(missingIcon, modifier = IconModifier.tintColor(Color.Green, BlendMode.Hue))
+ }
+ row(spacing = 5.percent) {
+ icon(missingIcon, modifier = IconModifier.tintColor(Color.Blue, BlendMode.Saturation))
+ icon(missingIcon, modifier = IconModifier.tintColor(Color.Cyan, BlendMode.Multiply))
+ }
+ }
+ icon(animatedIcon, modifier = IconModifier.size(size).align(IconAlign.TopLeft))
+ icon(animatedIcon, modifier = IconModifier.size(size).align(IconAlign.TopCenter))
+ icon(animatedIcon, modifier = IconModifier.size(size).align(IconAlign.TopRight))
+ icon(animatedIcon, modifier = IconModifier.size(size).align(IconAlign.CenterLeft))
+ icon(animatedIcon, modifier = IconModifier.size(size).tintColor(Color.Red).align(IconAlign.Center))
+ icon(animatedIcon, modifier = IconModifier.size(size).align(IconAlign.CenterRight))
+ icon(animatedIcon, modifier = IconModifier.size(size).align(IconAlign.BottomLeft))
+ icon(animatedIcon, modifier = IconModifier.size(size).align(IconAlign.BottomCenter))
+ icon(animatedIcon, modifier = IconModifier.size(size).align(IconAlign.BottomRight))
+ },
+ null,
+ "Complex Layout Icon"
+ )
+ })
+
+ val deferredIcon = remember {
+ project.guessProjectDir()?.findFile("build.gradle.kts")?.let {
+ IconUtil.getIcon(it, 0, project)
+ }
+ } ?: AllIcons.General.Error
+
+ icons.add(
+ ShowcaseIcon(
+ deferredIcon.toNewIcon(),
+ deferredIcon,
+ "Legacy Deferred Icon"
+ )
+ )
+
+ icons.add(remember {
+ ShowcaseIcon(
+ animatedIcon,
+ SpinningProgressIcon(),
+ "Animated Icon"
+ )
+ })
+
+ val json = Json { serializersModule = IconManager.getInstance().getSerializersModule() }
+ val dynIcon = deferredIcon(missingIcon) {
+ delay(5000)
+ icon {
+ icon(missingIcon, modifier = IconModifier.patchSvg {
+ replaceUnlessMatches("fill", "white", "green")
+ })
+ }
+ }
+ val serialized = json.encodeToString(dynIcon)
+ val deserialized = json.decodeFromString(serialized)
+
+ icons.add(remember {
+ ShowcaseIcon(
+ dynIcon,
+ null,
+ "Deferred Icon"
+ )
+ })
+
+ icons.add(remember {
+ ShowcaseIcon(
+ deserialized,
+ null,
+ "Deserialized Deferred Icon"
+ )
+ })
+
+ Column(Modifier.fillMaxWidth()) {
+ // Header
+ Row(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {
+ Text("Title", Modifier.weight(1f))
+ Text("Compose", Modifier.weight(1f))
+ Text("Swing", Modifier.weight(1f))
+ Text("Old Api - Swing", Modifier.weight(1f))
+ }
+
+ // Body
+ Column(Modifier.fillMaxWidth()) {
+ for (icon in icons) {
+ Row(Modifier.fillMaxWidth().padding(vertical = 4.dp)) {
+ Text(icon.title, Modifier.weight(1f))
+ Column(Modifier.weight(1f)) {
+ Icon(icon.icon, "Icon")
+ }
+ wrapStaticSwingIcon(
+ icon.icon.toSwingIcon(),
+ modifier = Modifier.weight(1f)
+ )
+ wrapStaticSwingIcon(
+ icon.swingAlternative ?: AllIcons.General.Warning,
+ modifier = Modifier.weight(1f)
+ )
+ }
+ }
+ }
+ }
+}
+
+class ShowcaseIcon(
+ val icon: Icon,
+ val swingAlternative: javax.swing.Icon?,
+ val title: String
+)
+
+@Composable
+private fun wrapStaticSwingIcon(icon: javax.swing.Icon, modifier: Modifier = Modifier) {
+ SwingPanel(
+ factory = {
+ JPanel().apply {
+ layout = BoxLayout(this, BoxLayout.Y_AXIS)
+ add(
+ JLabel(
+ icon
+ ).apply {
+ border = null
+ isOpaque = false
+ }
+ )
+ }
+ },
+ background = Color.Transparent,
+ modifier = modifier.fillMaxHeight(),
+ )
+}
\ No newline at end of file