IJPL-176416 New Icons API - fading animated icons, improved interop with old animated icons

(cherry picked from commit 9443723cd2b5839d5ebe0e314cafbe8fc55c58e1)


(cherry picked from commit e94b7edd8e749e93f650b2550da1335123563e5a)

IJ-MR-211495

GitOrigin-RevId: 2f8e88398a6f5a12afd3d75fc492c89d3523457a
This commit is contained in:
Jakub Senohrabek
2026-07-17 17:01:41 +00:00
committed by intellij-monorepo-bot
parent bfb524d7df
commit b9a82bf5c1
16 changed files with 337 additions and 100 deletions
+1
View File
@@ -55,6 +55,7 @@ f:com.intellij.platform.icons.design.IconAlign$Companion
f:com.intellij.platform.icons.design.IconAlignKt
- sf:iconAlign(com.intellij.platform.icons.design.IconVerticalAlign,com.intellij.platform.icons.design.IconHorizontalAlign):com.intellij.platform.icons.design.IconAlign
com.intellij.platform.icons.design.IconAnimationDesigner
- a:frame(J,J,J,kotlin.jvm.functions.Function1):V
- a:frame(J,kotlin.jvm.functions.Function1):V
com.intellij.platform.icons.design.IconDesigner
- a:animation(com.intellij.platform.icons.modifiers.IconModifier,kotlin.jvm.functions.Function1):V
@@ -16,6 +16,7 @@ interface LayerPaintingContext {
val slotWidth: Int?
val slotHeight: Int?
val scaling: ScalingContext
val alpha: Float?
fun drawImage(
image: ImageResource,
@@ -49,6 +50,7 @@ interface LayerPaintingContext {
slotWidth: Int? = null,
slotHeight: Int? = null,
scale: Float = 1f,
alpha: Float = 1f,
overrideColorFilter: ColorFilter? = null,
): LayerPaintingContext
}
@@ -40,4 +40,5 @@ fun IconDesigner.badge(
interface IconAnimationDesigner {
fun frame(duration: Long, builder: IconDesigner.() -> Unit)
fun frame(fadeIn: Long, stay: Long, fadeOut: Long, builder: IconDesigner.() -> Unit)
}
+10 -1
View File
@@ -82,9 +82,12 @@ com.intellij.platform.icons.impl.DeferredIconResolver
- a:resolve(kotlin.coroutines.Continuation):java.lang.Object
f:com.intellij.platform.icons.impl.IconAnimationFrame
- sf:Companion:com.intellij.platform.icons.impl.IconAnimationFrame$Companion
- <init>(java.util.List,J):V
- <init>(java.util.List,J,J,J):V
- b:<init>(java.util.List,J,J,J,I,kotlin.jvm.internal.DefaultConstructorMarker):V
- equals(java.lang.Object):Z
- f:getDuration():J
- f:getFadeIn():J
- f:getFadeOut():J
- f:getLayers():java.util.List
- hashCode():I
bf:com.intellij.platform.icons.impl.IconAnimationFrame$$serializer
@@ -226,6 +229,7 @@ f:com.intellij.platform.icons.impl.design.DefaultIconAnimationDesigner
- com.intellij.platform.icons.design.IconAnimationDesigner
- <init>(com.intellij.platform.icons.impl.design.DefaultIconDesigner):V
- f:build():java.util.List
- frame(J,J,J,kotlin.jvm.functions.Function1):V
- frame(J,kotlin.jvm.functions.Function1):V
- f:getRootDesigner():com.intellij.platform.icons.impl.design.DefaultIconDesigner
a:com.intellij.platform.icons.impl.design.DefaultIconDesigner
@@ -814,6 +818,11 @@ f:com.intellij.platform.icons.impl.rendering.ResolvedScalingContext
- f:getFinalDimensions():com.intellij.platform.icons.rendering.Dimensions
f:com.intellij.platform.icons.impl.rendering.ScalingKt
- sf:resolve(com.intellij.platform.icons.rendering.IconRenderer,F,com.intellij.platform.icons.scale.IconScale):com.intellij.platform.icons.impl.rendering.ResolvedScalingContext
com.intellij.platform.icons.impl.rendering.layers.AnimatedIconFrameRenderer
- a:getDuration():J
- a:getLayout():com.intellij.platform.icons.impl.rendering.layers.LayerLayout
- a:getRefreshDuration():J
- a:render(com.intellij.platform.icons.rendering.LayerPaintingContext,J):V
f:com.intellij.platform.icons.impl.rendering.layers.AnimatedIconLayerRenderer
- com.intellij.platform.icons.impl.rendering.layers.IconLayerRenderer
- <init>(com.intellij.platform.icons.impl.layers.AnimatedIconLayer,com.intellij.platform.icons.impl.rendering.DefaultRenderingContext):V
+13 -5
View File
@@ -2,6 +2,11 @@ load("@rules_jvm//:jvm.bzl", "jvm_library", "jvm_provided_library")
### auto-generated section `build intellij.platform.icons.impl.intellij` start
jvm_provided_library(
name = "platform_core-ui_provided",
lib = "//platform/core-ui",
)
jvm_provided_library(
name = "platform_icons-api_provided",
lib = "//platform/icons-api",
@@ -38,8 +43,8 @@ jvm_provided_library(
)
jvm_provided_library(
name = "platform_core-ui_provided",
lib = "//platform/core-ui",
name = "platform_ide-core_provided",
lib = "//platform/ide-core",
)
jvm_provided_library(
@@ -75,6 +80,7 @@ jvm_library(
"//libraries/kotlinx/coroutines/core",
"//libraries/kotlinx/serialization/core",
"@lib//:jetbrains-annotations",
":platform_core-ui_provided",
":platform_icons-api_provided",
":platform_icons-api_rendering_provided",
":platform_icons-impl_provided",
@@ -82,7 +88,7 @@ jvm_library(
":platform_util-ui_provided",
":platform_core_provided",
":platform_core-impl_provided",
":platform_core-ui_provided",
":platform_ide-core_provided",
":platform_ide-impl_provided",
],
)
@@ -108,6 +114,8 @@ jvm_library(
runtime_deps = [
# do not sort,
":intellij",
"//platform/core-ui",
"//platform/core-ui:core-ui_test_lib",
"//libraries/kotlinx/coroutines/core:core_test_lib",
"//libraries/kotlinx/serialization/core:core_test_lib",
"//platform/icons-api",
@@ -124,8 +132,8 @@ jvm_library(
"//platform/core-api:core_test_lib",
"//platform/core-impl",
"//platform/core-impl:core-impl_test_lib",
"//platform/core-ui",
"//platform/core-ui:core-ui_test_lib",
"//platform/ide-core",
"//platform/ide-core:ide-core_test_lib",
"//platform/platform-impl:ide-impl",
"//platform/platform-impl:ide-impl_test_lib",
],
+4 -3
View File
@@ -98,12 +98,13 @@ f:com.intellij.platform.icons.impl.intellij.rendering.SwingIconLayerRenderer
- setImage(com.intellij.platform.icons.rendering.ImageResource):V
- setLayout(com.intellij.platform.icons.impl.rendering.layers.LayerLayout):V
f:com.intellij.platform.icons.impl.intellij.rendering.SwingLayerPaintingContext
- <init>(java.awt.Component,java.awt.Graphics,I,I,java.lang.Integer,java.lang.Integer,java.lang.Integer,java.lang.Integer,com.intellij.platform.icons.filters.ColorFilter,com.intellij.platform.icons.rendering.ScalingContext):V
- b:<init>(java.awt.Component,java.awt.Graphics,I,I,java.lang.Integer,java.lang.Integer,java.lang.Integer,java.lang.Integer,com.intellij.platform.icons.filters.ColorFilter,com.intellij.platform.icons.rendering.ScalingContext,I,kotlin.jvm.internal.DefaultConstructorMarker):V
- createNestedLayer(java.lang.Integer,java.lang.Integer,java.lang.Integer,java.lang.Integer,F,com.intellij.platform.icons.filters.ColorFilter):com.intellij.platform.icons.rendering.LayerPaintingContext
- <init>(java.awt.Component,java.awt.Graphics,I,I,java.lang.Integer,java.lang.Integer,java.lang.Integer,java.lang.Integer,com.intellij.platform.icons.filters.ColorFilter,F,com.intellij.platform.icons.rendering.ScalingContext):V
- b:<init>(java.awt.Component,java.awt.Graphics,I,I,java.lang.Integer,java.lang.Integer,java.lang.Integer,java.lang.Integer,com.intellij.platform.icons.filters.ColorFilter,F,com.intellij.platform.icons.rendering.ScalingContext,I,kotlin.jvm.internal.DefaultConstructorMarker):V
- createNestedLayer(java.lang.Integer,java.lang.Integer,java.lang.Integer,java.lang.Integer,F,F,com.intellij.platform.icons.filters.ColorFilter):com.intellij.platform.icons.rendering.LayerPaintingContext
- drawCircle(com.intellij.platform.icons.design.Color,I,I,F,F,com.intellij.platform.icons.rendering.DrawMode):V
- drawImage(com.intellij.platform.icons.rendering.ImageResource,I,I,java.lang.Integer,java.lang.Integer,I,I,java.lang.Integer,java.lang.Integer,F,com.intellij.platform.icons.filters.ColorFilter):V
- drawRect(com.intellij.platform.icons.design.Color,I,I,I,I,F,com.intellij.platform.icons.rendering.DrawMode):V
- getAlpha():java.lang.Float
- f:getC():java.awt.Component
- f:getG():java.awt.Graphics
- getOffsetX():I
@@ -28,6 +28,7 @@
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
</content>
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.platform.core.ui" scope="PROVIDED" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="inheritedJdk" />
<orderEntry type="module" module-name="intellij.libraries.kotlinx.coroutines.core" />
@@ -39,7 +40,7 @@
<orderEntry type="module" module-name="intellij.platform.util.ui" scope="PROVIDED" />
<orderEntry type="module" module-name="intellij.platform.core" scope="PROVIDED" />
<orderEntry type="module" module-name="intellij.platform.core.impl" scope="PROVIDED" />
<orderEntry type="module" module-name="intellij.platform.core.ui" scope="PROVIDED" />
<orderEntry type="module" module-name="intellij.platform.ide.core" scope="PROVIDED" />
<orderEntry type="module" module-name="intellij.platform.ide.impl" scope="PROVIDED" />
<orderEntry type="library" name="jetbrains-annotations" level="project" />
</component>
@@ -25,6 +25,7 @@ import com.intellij.platform.icons.modifiers.scale
import com.intellij.platform.icons.rendering.IconRendererManager
import com.intellij.platform.icons.scale.IconScale
import com.intellij.platform.icons.swing.ScalableSwingIcon
import com.intellij.ui.AnimatedIcon
import com.intellij.util.messages.Topic
import com.intellij.util.messages.Topic.BroadcastDirection
import kotlinx.coroutines.Dispatchers
@@ -74,18 +75,9 @@ class IntelliJIconManager : DefaultIconManager() {
override fun toNewIcon(swingIcon: javax.swing.Icon): Icon {
if (swingIcon is SwingIcon && swingIcon.scale == null) return swingIcon.icon
return if (swingIcon is SwingIcon) {
@Suppress("KotlinConstantConditions")
if (swingIcon.scale != null) {
icon {
addSwingLayer(
this,
swingIcon,
IconModifier.scale(swingIcon.scale)
)
}
} else swingIcon.icon
unpackSwingIcon(swingIcon)
} else {
icon {
convertLegacyIcon(swingIcon) ?: icon {
addSwingLayer(
this,
swingIcon,
@@ -95,6 +87,28 @@ class IntelliJIconManager : DefaultIconManager() {
}
}
private fun convertLegacyIcon(swingIcon: javax.swing.Icon): Icon? {
return when (swingIcon) {
is AnimatedIcon -> {
swingIcon.extractAsNewIcon()
}
else -> null
}
}
private fun unpackSwingIcon(swingIcon: SwingIcon): Icon {
@Suppress("KotlinConstantConditions")
return if (swingIcon.scale != null) {
icon {
addSwingLayer(
this,
swingIcon,
IconModifier.scale(swingIcon.scale)
)
}
} else swingIcon.icon
}
override fun markDeferredIconUnused(id: IconIdentifier) {
// TODO delete unused deferred icons
}
@@ -34,6 +34,7 @@ class SwingLayerPaintingContext(
override val slotHeight: Int? = null,
override val slotWidth: Int? = null,
private val overrideColorFilter: ColorFilter? = null,
override val alpha: Float = 1f,
override val scaling: ScalingContext = DefaultScalingContext(1f, 1f),
) : LayerPaintingContext {
override val offsetX: Int = customX ?: x
@@ -45,6 +46,7 @@ class SwingLayerPaintingContext(
slotWidth: Int?,
slotHeight: Int?,
scale: Float,
alpha: Float,
overrideColorFilter: ColorFilter?
): LayerPaintingContext {
return SwingLayerPaintingContext(
@@ -57,6 +59,7 @@ class SwingLayerPaintingContext(
slotHeight,
slotWidth,
overrideColorFilter ?: this.overrideColorFilter,
alpha,
DefaultScalingContext(scaling.displayDensity, scaling.contextScale * scale)
)
}
@@ -72,7 +75,7 @@ class SwingLayerPaintingContext(
@Suppress("unused")
private fun drawShape(color: Color, shape: Shape, alpha: Float, mode: DrawMode) {
setDrawingDefaults()
setDrawingDefaults(alpha)
if (g is Graphics2D) {
val oldComposite = g.composite
val oldPaint = g.paint
@@ -118,10 +121,14 @@ class SwingLayerPaintingContext(
}
}
private fun setDrawingDefaults() {
private fun setDrawingDefaults(alpha: Float) {
if (g !is Graphics2D) return
g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
g.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE)
val realAlpha = alpha * this.alpha
if (realAlpha < 1f) {
g.composite = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, realAlpha)
}
}
private fun drawImage(
@@ -188,13 +195,13 @@ class SwingLayerPaintingContext(
alpha: Float,
colorFilter: ColorFilter?,
) {
// TODO apply alpha & color filters
// TODO apply color filters
val imageWidth = image.getWidth(null)
val imageHeight = image.getHeight(null)
if (imageWidth == 0 || imageHeight == 0) return
setDrawingDefaults()
setDrawingDefaults(alpha)
g.drawImage(
image,
x,
@@ -176,6 +176,7 @@ class TestPaintingContext(
override val offsetY: Int = 0,
override val slotWidth: Int? = null,
override val slotHeight: Int? = null,
override val alpha: Float = 1f
) : LayerPaintingContext {
override fun drawImage(
image: ImageResource,
@@ -222,6 +223,7 @@ class TestPaintingContext(
slotWidth: Int?,
slotHeight: Int?,
scale: Float,
alpha: Float,
overrideColorFilter: ColorFilter?,
): LayerPaintingContext {
return TestPaintingContext(
@@ -230,7 +232,8 @@ class TestPaintingContext(
x ?: offsetX,
y ?: offsetY,
slotWidth,
slotHeight
slotHeight,
alpha
)
}
@@ -5,7 +5,7 @@ import com.intellij.platform.icons.layers.IconLayer
import kotlinx.serialization.Serializable
@Serializable
class IconAnimationFrame(val layers: List<IconLayer>, val duration: Long) {
class IconAnimationFrame(val layers: List<IconLayer>, val duration: Long, val fadeIn: Long = 0L, val fadeOut: Long = 0L) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
@@ -25,4 +25,4 @@ class IconAnimationFrame(val layers: List<IconLayer>, val duration: Long) {
}
override fun toString(): String = "IconAnimationFrame(duration=$duration, layers=$layers)"
}
}
@@ -14,5 +14,11 @@ class DefaultIconAnimationDesigner(val rootDesigner: DefaultIconDesigner) : Icon
frames.add(IconAnimationFrame(designer.buildLayers(), duration))
}
override fun frame(fadeIn: Long, stay: Long, fadeOut: Long, builder: IconDesigner.() -> Unit) {
val designer = rootDesigner.createNestedDesigner()
designer.builder()
frames.add(IconAnimationFrame(designer.buildLayers(), stay, fadeIn, fadeOut))
}
fun build(): List<IconAnimationFrame> = frames
}
@@ -7,82 +7,158 @@ import com.intellij.platform.icons.impl.layers.AnimatedIconLayer
import com.intellij.platform.icons.impl.rendering.DefaultRenderingContext
import com.intellij.platform.icons.rendering.LayerPaintingContext
import com.intellij.platform.icons.rendering.RenderingContext
import kotlin.math.cos
class AnimatedIconLayerRenderer(
private val layer: AnimatedIconLayer,
private val renderingContext: DefaultRenderingContext,
private val layer: AnimatedIconLayer,
private val renderingContext: DefaultRenderingContext,
) : IconLayerRenderer {
private val currentContext = renderingContext.adjustTo(layer)
private val frameRenderers = layer.frames.map { AnimatedIconFrameRenderer(it, currentContext) }
private var lastFrame = FrameData(-1, 0, 0)
private val currentContext = renderingContext.adjustTo(layer)
private val frameRenderers = createFrameRenderers()
private var lastFrame = FrameData(-1, 0, 0, 0)
override val layout: LayerLayout = applyLayout()
override val layout: LayerLayout = applyLayout()
private fun applyLayout(): LayerLayout {
val dimensions = frameRenderers.maxCompoundSize { it.layout.consumedSpace() }
return applyLayout(layer.modifier, dimensions.width, dimensions.height)
private fun createFrameRenderers(): List<AnimatedIconFrameRenderer> {
val renderers = mutableListOf<AnimatedIconFrameRenderer>()
for (frame in layer.frames) {
val base = BaseAnimatedIconFrameRenderer(frame, currentContext)
if (frame.fadeIn > 0L) {
renderers.add(FadingAnimatedIconFrameRenderer(base, frame.fadeIn, FadingAnimatedIconFrameRenderer.FadeType.FadeIn))
}
if (frame.duration > 0L) {
renderers.add(base)
}
if (frame.fadeOut > 0L) {
renderers.add(FadingAnimatedIconFrameRenderer(base, frame.fadeOut, FadingAnimatedIconFrameRenderer.FadeType.FadeOut))
}
}
return renderers
}
override fun render(paintingContext: LayerPaintingContext) {
val placement = layout.placement(paintingContext)
val nestedLayerApi =
paintingContext.createNestedLayer(
placement.bounds.x,
placement.bounds.y,
placement.bounds.width,
placement.bounds.height,
scale = placement.scale,
)
val currentFrameData = calculateAndSetNewFrameData()
frameRenderers[currentFrameData.frame].render(nestedLayerApi)
if (currentFrameData.remainingDuration > 0L) {
renderingContext.updateFlow.triggerDelayedUpdate(currentFrameData.remainingDuration)
}
private fun applyLayout(): LayerLayout {
val dimensions = frameRenderers.maxCompoundSize { it.layout.consumedSpace() }
return applyLayout(layer.modifier, dimensions.width, dimensions.height)
}
override fun render(paintingContext: LayerPaintingContext) {
val placement = layout.placement(paintingContext)
val nestedLayerApi =
paintingContext.createNestedLayer(
placement.bounds.x,
placement.bounds.y,
placement.bounds.width,
placement.bounds.height,
scale = placement.scale,
)
val currentFrameData = calculateAndSetNewFrameData()
val elapsedTime = System.currentTimeMillis() - currentFrameData.timestamp
frameRenderers[currentFrameData.frame].render(nestedLayerApi, elapsedTime)
if (currentFrameData.refreshDuration > 0L) {
renderingContext.updateFlow.triggerDelayedUpdate(currentFrameData.refreshDuration)
}
}
private fun calculateAndSetNewFrameData(): FrameData {
val currentLastFrame = lastFrame
if (currentLastFrame.timestamp == -1L) {
val remaining = if (frameRenderers.size > 0) frameRenderers[0].frame.duration else 0L
val newData = FrameData(System.currentTimeMillis(), 0, remaining)
lastFrame = newData
return newData
}
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 fun calculateAndSetNewFrameData(): FrameData {
val currentLastFrame = lastFrame
// Handle first frame
if (currentLastFrame.timestamp == -1L) {
val remaining = if (frameRenderers.isNotEmpty()) frameRenderers[0].getDuration() else 0L
val refresh = if (frameRenderers.isNotEmpty()) frameRenderers[0].getRefreshDuration() else 0L
val newData = FrameData(System.currentTimeMillis(), 0, remaining, refresh)
lastFrame = newData
return newData
}
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)
val layout: LayerLayout = applyLayout()
private fun applyLayout(): LayerLayout {
val dimensions = renderers.maxCompoundSize { it.layout.consumedSpace() }
return LayerLayout(
0.dp.compoundSize(),
0.dp.compoundSize(),
0.dp.compoundSize(),
0.dp.compoundSize(),
dimensions.width,
dimensions.height,
)
}
fun render(paintingContext: LayerPaintingContext) {
for (layer in renderers) {
layer.render(paintingContext)
}
}
// Handle next frame
val elapsedMillis = System.currentTimeMillis() - lastFrame.timestamp
if (elapsedMillis > currentLastFrame.remainingDuration) {
val index = (currentLastFrame.frame + 1) % frameRenderers.size
val remaining = frameRenderers[index].getDuration()
val refresh = frameRenderers[index].getRefreshDuration()
val newData = FrameData(System.currentTimeMillis(), index, remaining, refresh)
lastFrame = newData
return newData
}
else {
return lastFrame
}
}
private class FrameData(val timestamp: Long, val frame: Int, val remainingDuration: Long, val refreshDuration: Long)
}
interface AnimatedIconFrameRenderer {
fun getDuration(): Long
fun getRefreshDuration(): Long
val layout: LayerLayout
fun render(paintingContext: LayerPaintingContext, elapsedMillis: Long)
}
private class FadingAnimatedIconFrameRenderer(val base: BaseAnimatedIconFrameRenderer, private val duration: Long, val fadeType: FadeType) : AnimatedIconFrameRenderer {
override val layout: LayerLayout = base.layout
override fun getDuration(): Long {
return duration
}
override fun getRefreshDuration(): Long {
return 50 // refresh rate for fading animation
}
override fun render(paintingContext: LayerPaintingContext, elapsedMillis: Long) {
val alpha = when (fadeType) {
FadeType.FadeIn -> fadeInAlpha(elapsedMillis, duration)
FadeType.FadeOut -> fadeOutAlpha(elapsedMillis, duration)
}
val nested = paintingContext.createNestedLayer(alpha = alpha)
base.render(nested, elapsedMillis)
}
private fun fadeInAlpha(elapsedMillis: Long, durationMillis: Long): Float {
if (durationMillis <= 0) return 1f
val progress = (elapsedMillis.toDouble() / durationMillis).coerceIn(0.0, 1.0)
return ((1 - cos(Math.PI * progress)) / 2).toFloat()
}
private fun fadeOutAlpha(elapsedMillis: Long, durationMillis: Long): Float {
if (durationMillis <= 0) return 0f
val progress = (elapsedMillis.toDouble() / durationMillis).coerceIn(0.0, 1.0)
return ((cos(Math.PI * progress) + 1) / 2).toFloat()
}
enum class FadeType { FadeIn, FadeOut }
}
private class BaseAnimatedIconFrameRenderer(val frame: IconAnimationFrame, renderingContext: RenderingContext) : AnimatedIconFrameRenderer {
private val renderers = IconLayerManager.createRenderers(frame.layers, renderingContext)
override fun getDuration(): Long {
return frame.duration
}
override fun getRefreshDuration(): Long {
return frame.duration
}
override val layout: LayerLayout = applyLayout()
private fun applyLayout(): LayerLayout {
val dimensions = renderers.maxCompoundSize { it.layout.consumedSpace() }
return LayerLayout(
0.dp.compoundSize(),
0.dp.compoundSize(),
0.dp.compoundSize(),
0.dp.compoundSize(),
dimensions.width,
dimensions.height,
)
}
override fun render(paintingContext: LayerPaintingContext, elapsedMillis: Long) {
for (layer in renderers) {
layer.render(paintingContext)
}
}
}
@@ -5,9 +5,11 @@ import com.intellij.icons.AllIcons;
import com.intellij.openapi.application.CoroutineSupport.UiDispatcherKind;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Key;
import com.intellij.platform.icons.modifiers.IconModifier;
import com.intellij.ui.icons.IconUtilKt;
import com.intellij.util.concurrency.EdtScheduler;
import com.intellij.util.containers.ContainerUtil;
import kotlin.Unit;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -26,6 +28,9 @@ import java.util.List;
import java.util.Set;
import java.util.function.Function;
import static com.intellij.platform.icons.IconManagerKt.icon;
import static com.intellij.platform.icons.swing.SwingIconKt.swingIcon;
public class AnimatedIcon implements Icon {
private static final Logger LOG = Logger.getInstance(AnimatedIcon.class);
/**
@@ -172,11 +177,16 @@ public class AnimatedIcon implements Icon {
@ApiStatus.Internal
public static final class Fading extends AnimatedIcon {
private final Icon icon;
private final int period;
public Fading(@NotNull Icon icon) {
this(1000, icon);
}
public Fading(int period, @NotNull Icon icon) {
this.period = period;
this.icon = icon;
super(50, new Icon() {
private final long time = System.currentTimeMillis();
@@ -213,9 +223,30 @@ public class AnimatedIcon implements Icon {
}
});
}
@ApiStatus.Internal
@Override
public com.intellij.platform.icons.Icon extractAsNewIcon() {
final int fadeTime = Math.max(period / 2, 0);
return icon((designer) -> {
designer.animation(IconModifier.Companion, (animation) -> {
animation.frame(fadeTime, 0, fadeTime, (frameDesigner) -> {
swingIcon(frameDesigner, icon, IconModifier.Companion);
return Unit.INSTANCE;
});
animation.frame(fadeTime, 0, fadeTime, (frameDesigner) -> {
swingIcon(frameDesigner, icon, IconModifier.Companion);
return Unit.INSTANCE;
});
return Unit.INSTANCE;
});
return Unit.INSTANCE;
});
}
}
final Frame[] frames;
@ApiStatus.Internal
public final Frame[] frames;
private final Set<Component> requested = Collections.newSetFromMap(new IdentityHashMap<>());
private long time;
private int index;
@@ -341,4 +372,20 @@ public class AnimatedIcon implements Icon {
protected @Nullable Component getRendererOwner(@Nullable Component component) {
return ClientProperty.isTrue(component, ANIMATION_IN_RENDERER_ALLOWED) ? component : null;
}
@ApiStatus.Internal
public com.intellij.platform.icons.Icon extractAsNewIcon() {
return icon((designer) -> {
designer.animation(IconModifier.Companion, (animation) -> {
for (Frame frame : frames) {
animation.frame(frame.getDelay(), (frameDesigner) -> {
swingIcon(frameDesigner, frame.getIcon(), IconModifier.Companion);
return Unit.INSTANCE;
});
}
return Unit.INSTANCE;
});
return Unit.INSTANCE;
});
}
}
@@ -34,6 +34,7 @@ public class ComposeLayerPaintingContext(
override val slotWidth: Int? = null,
override val slotHeight: Int? = null,
private val overrideColorFilter: ColorFilter? = null,
override val alpha: Float = 1f,
override val scaling: ScalingContext = DefaultScalingContext(drawScope.drawContext.density.density, 1f),
) : LayerPaintingContext {
override fun createNestedLayer(
@@ -42,6 +43,7 @@ public class ComposeLayerPaintingContext(
slotWidth: Int?,
slotHeight: Int?,
scale: Float,
alpha: Float,
overrideColorFilter: ColorFilter?,
): LayerPaintingContext {
return ComposeLayerPaintingContext(
@@ -51,6 +53,7 @@ public class ComposeLayerPaintingContext(
slotWidth,
slotHeight,
overrideColorFilter ?: this.overrideColorFilter,
alpha,
DefaultScalingContext(scaling.displayDensity, scaling.contextScale * scale),
)
}
@@ -68,7 +71,7 @@ public class ComposeLayerPaintingContext(
color.toCompose(),
radius,
Offset(x.toFloat(), y.toFloat()),
alpha,
alpha * this.alpha,
style = style,
blendMode = blendMode,
)
@@ -87,7 +90,7 @@ public class ComposeLayerPaintingContext(
color.toCompose(),
Offset(x.toFloat(), y.toFloat()),
Size(width.toFloat(), height.toFloat()),
alpha,
alpha * this.alpha,
style = style,
blendMode = blendMode,
)
@@ -189,7 +192,7 @@ public class ComposeLayerPaintingContext(
with(image.painter) {
draw(
Size(targetSize.width.toFloat(), targetSize.height.toFloat()),
alpha,
alpha * this@ComposeLayerPaintingContext.alpha,
convertColorFilter(colorFilter ?: image.modifiers?.colorFilter),
)
}
@@ -219,7 +222,7 @@ public class ComposeLayerPaintingContext(
IntSize(srcWidth ?: image.size.width, srcHeight ?: image.size.height),
dstOffset = IntOffset(x, y),
dstSize = targetSize,
alpha = alpha,
alpha = alpha * this.alpha,
colorFilter = convertColorFilter(colorFilter),
filterQuality = FilterQuality.Low,
)
@@ -22,9 +22,14 @@ import com.intellij.platform.icons.scale.factor
import com.intellij.platform.icons.scale.fitArea
import com.intellij.platform.icons.swing.swingIcon
import com.intellij.platform.icons.swing.toNewIcon
import com.intellij.ui.AnimatedIcon
import com.intellij.ui.IconDeferrer
import com.intellij.ui.dsl.builder.panel
import kotlinx.coroutines.delay
import javax.swing.Icon
import javax.swing.JComponent
import javax.swing.JLabel
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.Duration.Companion.seconds
internal class IconsPanel : UISandboxPanel {
@@ -64,6 +69,22 @@ internal class IconsPanel : UISandboxPanel {
}
}
}
icon {
animation {
frame(fadeIn = 500, stay = 500, fadeOut = 500) {
swingIcon(AllIcons.General.Add)
}
frame(fadeIn = 500, stay = 500, fadeOut = 500) {
swingIcon(AllIcons.General.Remove)
}
frame(fadeIn = 500, stay = 500, fadeOut = 500) {
swingIcon(AllIcons.General.ArrowUp)
}
frame(fadeIn = 500, stay = 500, fadeOut = 500) {
swingIcon(AllIcons.General.ArrowDown)
}
}
}
}
}
group("Deferred Icon") {
@@ -120,6 +141,43 @@ internal class IconsPanel : UISandboxPanel {
}
}
}
var i = 0
fun createDeferredIcon(): Icon {
return IconDeferrer.getInstance().deferAsync(AllIcons.FileTypes.Unknown, i++) {
delay(5000.milliseconds)
AllIcons.FileTypes.Bazel
}
}
group("Legacy Icon Support") {
lateinit var deferredIconLabel: JLabel
row {
icon(
AllIcons.General.GearPlain.toNewIcon()
)
icon(
AnimatedIcon.Default().toNewIcon()
)
deferredIconLabel = icon(
createDeferredIcon()
).component
}
row {
button("Recreate Deferred Icon") {
deferredIconLabel.icon = createDeferredIcon()
}
}
}
group("Legacy Icon Tests") {
row {
icon(
AnimatedIcon.Blinking(AllIcons.General.Remove)
)
icon(
AnimatedIcon.Fading(AllIcons.General.Add)
)
}
}
}
}
}