diff --git a/platform/icons-api/api-dump.txt b/platform/icons-api/api-dump.txt index 0fb0dc7d03fc..f445d93cc81b 100644 --- a/platform/icons-api/api-dump.txt +++ b/platform/icons-api/api-dump.txt @@ -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 diff --git a/platform/icons-api/rendering/src/com/intellij/platform/icons/rendering/LayerPaintingContext.kt b/platform/icons-api/rendering/src/com/intellij/platform/icons/rendering/LayerPaintingContext.kt index 370b3ae00bc8..c64cdc625c6d 100644 --- a/platform/icons-api/rendering/src/com/intellij/platform/icons/rendering/LayerPaintingContext.kt +++ b/platform/icons-api/rendering/src/com/intellij/platform/icons/rendering/LayerPaintingContext.kt @@ -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 } diff --git a/platform/icons-api/src/com/intellij/platform/icons/design/IconDesigner.kt b/platform/icons-api/src/com/intellij/platform/icons/design/IconDesigner.kt index 89925443377b..4d099c521d6e 100644 --- a/platform/icons-api/src/com/intellij/platform/icons/design/IconDesigner.kt +++ b/platform/icons-api/src/com/intellij/platform/icons/design/IconDesigner.kt @@ -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) } diff --git a/platform/icons-impl/api-dump.txt b/platform/icons-impl/api-dump.txt index 8064b30d6eaa..326c23092f9c 100644 --- a/platform/icons-impl/api-dump.txt +++ b/platform/icons-impl/api-dump.txt @@ -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 -- (java.util.List,J):V +- (java.util.List,J,J,J):V +- b:(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 - (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 - (com.intellij.platform.icons.impl.layers.AnimatedIconLayer,com.intellij.platform.icons.impl.rendering.DefaultRenderingContext):V diff --git a/platform/icons-impl/intellij/BUILD.bazel b/platform/icons-impl/intellij/BUILD.bazel index 5e514bbfb248..e61be3c38d0b 100644 --- a/platform/icons-impl/intellij/BUILD.bazel +++ b/platform/icons-impl/intellij/BUILD.bazel @@ -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", ], diff --git a/platform/icons-impl/intellij/api-dump.txt b/platform/icons-impl/intellij/api-dump.txt index 9a07b57fdd09..a5f01e030596 100644 --- a/platform/icons-impl/intellij/api-dump.txt +++ b/platform/icons-impl/intellij/api-dump.txt @@ -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 -- (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:(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 +- (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:(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 diff --git a/platform/icons-impl/intellij/intellij.platform.icons.impl.intellij.iml b/platform/icons-impl/intellij/intellij.platform.icons.impl.intellij.iml index 0b558ba26ae2..577a5876c076 100644 --- a/platform/icons-impl/intellij/intellij.platform.icons.impl.intellij.iml +++ b/platform/icons-impl/intellij/intellij.platform.icons.impl.intellij.iml @@ -28,6 +28,7 @@ + @@ -39,7 +40,7 @@ - + diff --git a/platform/icons-impl/intellij/src/com/intellij/platform/icons/impl/intellij/IntelliJIconManager.kt b/platform/icons-impl/intellij/src/com/intellij/platform/icons/impl/intellij/IntelliJIconManager.kt index 911814aee7c0..19eca1534a7e 100644 --- a/platform/icons-impl/intellij/src/com/intellij/platform/icons/impl/intellij/IntelliJIconManager.kt +++ b/platform/icons-impl/intellij/src/com/intellij/platform/icons/impl/intellij/IntelliJIconManager.kt @@ -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 } diff --git a/platform/icons-impl/intellij/src/com/intellij/platform/icons/impl/intellij/rendering/SwingLayerPaintingContext.kt b/platform/icons-impl/intellij/src/com/intellij/platform/icons/impl/intellij/rendering/SwingLayerPaintingContext.kt index e595028681f1..185d1b97d5da 100644 --- a/platform/icons-impl/intellij/src/com/intellij/platform/icons/impl/intellij/rendering/SwingLayerPaintingContext.kt +++ b/platform/icons-impl/intellij/src/com/intellij/platform/icons/impl/intellij/rendering/SwingLayerPaintingContext.kt @@ -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, diff --git a/platform/icons-impl/intellij/tests/test/com/intellij/platform/icons/impl/intellij/IconTester.kt b/platform/icons-impl/intellij/tests/test/com/intellij/platform/icons/impl/intellij/IconTester.kt index 2102f8627df8..ed7169e16f4a 100644 --- a/platform/icons-impl/intellij/tests/test/com/intellij/platform/icons/impl/intellij/IconTester.kt +++ b/platform/icons-impl/intellij/tests/test/com/intellij/platform/icons/impl/intellij/IconTester.kt @@ -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 ) } diff --git a/platform/icons-impl/src/com/intellij/platform/icons/impl/IconAnimationFrame.kt b/platform/icons-impl/src/com/intellij/platform/icons/impl/IconAnimationFrame.kt index e0f51b4b15b4..19c83fa65a25 100644 --- a/platform/icons-impl/src/com/intellij/platform/icons/impl/IconAnimationFrame.kt +++ b/platform/icons-impl/src/com/intellij/platform/icons/impl/IconAnimationFrame.kt @@ -5,7 +5,7 @@ import com.intellij.platform.icons.layers.IconLayer import kotlinx.serialization.Serializable @Serializable -class IconAnimationFrame(val layers: List, val duration: Long) { +class IconAnimationFrame(val layers: List, 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, val duration: Long) { } override fun toString(): String = "IconAnimationFrame(duration=$duration, layers=$layers)" -} +} \ No newline at end of file diff --git a/platform/icons-impl/src/com/intellij/platform/icons/impl/design/DefaultIconAnimationDesigner.kt b/platform/icons-impl/src/com/intellij/platform/icons/impl/design/DefaultIconAnimationDesigner.kt index 563a865be821..da7215c1fd9f 100644 --- a/platform/icons-impl/src/com/intellij/platform/icons/impl/design/DefaultIconAnimationDesigner.kt +++ b/platform/icons-impl/src/com/intellij/platform/icons/impl/design/DefaultIconAnimationDesigner.kt @@ -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 = frames } diff --git a/platform/icons-impl/src/com/intellij/platform/icons/impl/rendering/layers/AnimatedIconLayerRenderer.kt b/platform/icons-impl/src/com/intellij/platform/icons/impl/rendering/layers/AnimatedIconLayerRenderer.kt index b0c86f5dc52e..d57461b7d049 100644 --- a/platform/icons-impl/src/com/intellij/platform/icons/impl/rendering/layers/AnimatedIconLayerRenderer.kt +++ b/platform/icons-impl/src/com/intellij/platform/icons/impl/rendering/layers/AnimatedIconLayerRenderer.kt @@ -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 { + val renderers = mutableListOf() + 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) + } + } +} \ No newline at end of file diff --git a/platform/ide-core/src/com/intellij/ui/AnimatedIcon.java b/platform/ide-core/src/com/intellij/ui/AnimatedIcon.java index afc7c93da30e..57790893725c 100644 --- a/platform/ide-core/src/com/intellij/ui/AnimatedIcon.java +++ b/platform/ide-core/src/com/intellij/ui/AnimatedIcon.java @@ -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 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; + }); + } } diff --git a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposeLayerPaintingContext.kt b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposeLayerPaintingContext.kt index 3e2964747262..aa203820d6f6 100644 --- a/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposeLayerPaintingContext.kt +++ b/platform/jewel/ui/src/main/kotlin/org/jetbrains/jewel/ui/icon/ComposeLayerPaintingContext.kt @@ -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, ) diff --git a/platform/platform-impl/internal/src/com/intellij/internal/ui/sandbox/components/IconsPanel.kt b/platform/platform-impl/internal/src/com/intellij/internal/ui/sandbox/components/IconsPanel.kt index 6710b9d5e4c6..d235b5d8b15d 100644 --- a/platform/platform-impl/internal/src/com/intellij/internal/ui/sandbox/components/IconsPanel.kt +++ b/platform/platform-impl/internal/src/com/intellij/internal/ui/sandbox/components/IconsPanel.kt @@ -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) + ) + } + } } } } \ No newline at end of file