Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .idea/markdown.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions .idea/modules.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .idea/runConfigurations/IDEA.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions build/bazel-generated-file-list.txt
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,7 @@ platform/jbr
platform/jewel/decorated-window
platform/jewel/detekt-plugin
platform/jewel/foundation
platform/jewel/icons
platform/jewel/ide-laf-bridge
platform/jewel/int-ui/int-ui-decorated-window
platform/jewel/int-ui/int-ui-standalone
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ internal suspend fun createPlatformLayout(projectLibrariesUsedByPlugins: SortedS
"intellij.platform.util.classLoader",
"intellij.platform.util.zip",
"intellij.platform.boot",
"intellij.platform.icons.api",
"intellij.platform.runtime.repository",
"intellij.platform.runtime.loader",
), productLayout = productLayout, layout = layout)
Expand Down
3 changes: 2 additions & 1 deletion platform/core-ui/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ jvm_library(
"//platform/util",
"//platform/core-api:core",
"//platform/util:util-ui",
"@lib//:kotlin-stdlib",
"@lib//:hash4j",
"@lib//:kotlin-stdlib",
"//platform/jewel/icons",
]
)
### auto-generated section `build intellij.platform.core.ui` end
3 changes: 2 additions & 1 deletion platform/core-ui/intellij.platform.core.ui.iml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
<orderEntry type="module" module-name="intellij.platform.util" />
<orderEntry type="module" module-name="intellij.platform.core" />
<orderEntry type="module" module-name="intellij.platform.util.ui" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="library" name="hash4j" level="project" />
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="module" module-name="intellij.platform.icons.api" />
</component>
</module>
3 changes: 2 additions & 1 deletion platform/core-ui/src/ui/DeferredIcon.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
// Copyright 2000-2020 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.ui;

import org.jetbrains.icons.api.DynamicIcon;
import com.intellij.ui.icons.UpdatableIcon;
import org.jetbrains.annotations.NotNull;

import javax.swing.*;

public interface DeferredIcon extends UpdatableIcon {
public interface DeferredIcon extends UpdatableIcon, DynamicIcon {
@NotNull Icon evaluate();

@NotNull Icon getBaseIcon();
Expand Down
3 changes: 2 additions & 1 deletion platform/core-ui/src/ui/icons/IconWithOverlay.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.Icon;
import javax.swing.*;
import java.awt.*;
import java.awt.image.BufferedImage;
Expand All @@ -17,7 +18,7 @@
* @author Konstantin Bulenkov
*/
public abstract class IconWithOverlay extends LayeredIcon {
public IconWithOverlay(@NotNull Icon main, @NotNull Icon overlay) {
public IconWithOverlay(@NotNull javax.swing.Icon main, @NotNull javax.swing.Icon overlay) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd revert changes to this file

super(main, overlay);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import javax.swing.Icon;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

import javax.swing.*;
import java.awt.*;
import java.util.Objects;
Expand Down
1 change: 1 addition & 0 deletions platform/core-ui/src/util/IconUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import java.util.*
import java.util.concurrent.ConcurrentHashMap
import java.util.function.Supplier
import java.util.function.ToIntFunction
import javax.swing.Icon
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

import javax.swing.*
import kotlin.math.abs
import kotlin.math.max
Expand Down
18 changes: 18 additions & 0 deletions platform/icons-api/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -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.
import org.jetbrains.compose.ComposeBuildConfig

plugins {
jewel
`jewel-check-public-api`
alias(libs.plugins.composeDesktop)
alias(libs.plugins.compose.compiler)
}

private val composeVersion
get() = ComposeBuildConfig.composeVersion

dependencies {
api("org.jetbrains.compose.foundation:foundation-desktop:$composeVersion")

testImplementation(compose.desktop.currentOs) { exclude(group = "org.jetbrains.compose.material") }
}
Comment on lines +14 to +18
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't look like we need these. The iml only uses coroutines and kotlin stdlib (which in Gradle you can probably mark as compileOnly as they'll be put on the classpath by Jewel)

30 changes: 30 additions & 0 deletions platform/icons-api/intellij.platform.icons.api.iml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="kotlin-language" name="Kotlin">
<configuration version="5" platform="JVM 17" allPlatforms="JVM [17]" useProjectSettings="false">
<compilerSettings>
<option name="additionalArguments" value="-Xjvm-default=all" />
</compilerSettings>
<compilerArguments>
<stringArguments>
<stringArg name="jvmTarget" arg="17" />
<stringArg name="apiVersion" arg="2.2" />
<stringArg name="languageVersion" arg="2.2" />
</stringArguments>
</compilerArguments>
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/resources" type="java-resource" />
<sourceFolder url="file://$MODULE_DIR$/src/main/kotlin" isTestSource="false" />
</content>
<orderEntry type="library" name="kotlin-stdlib" level="project" />
<orderEntry type="library" name="kotlinx-coroutines-core" level="project" />
<orderEntry type="sourceFolder" forTests="false" />
<orderEntry type="inheritedJdk" />
</component>
</module>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// 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.api

import kotlinx.coroutines.flow.StateFlow

interface DynamicIcon : Icon {
val onUpdate: StateFlow<IconState>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the current impl of IconState, this would likely cause missed redraws due to StateFlow's implicit distinct() call. This should be a Flow<IconState> imo

}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: missing empty line at EOF — will not repeat for all files, but there are several

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 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.api

interface Icon {
val identifier: IconIdentifier
val state: IconState

fun render(api: PaintingApi)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need to have fully abstract painting api?
It seems (please correct me if I'm wrong) the vast majority of icons go through the following stages:

  1. An element (PsiElement, action, tool window) determines how it should be presented. Often it just returns a field from one of *Icons classes which wraps a path to the svg file. For PsiElement the presentation may be composed of multiple layers. Sometimes calculating the proper presentation requires some time, so it shouldn't be done synchronously.
  2. The presentation is adjusted according to device-specific configuration: light or dark theme, scaling.
  3. The resulting image is actually painted.

The problem with the current implementation is that the same interface javax.swing.Icon is used during all these stages.
And code which determines presentation during the first stage shouldn't know specify how the icon should be rendered.
We would need this if we want to have absolute flexibility here and allow elements to generate bitmaps on the fly, for example. But do we really need it? If in 99% of cases the icons are svg files with few transformations applied, painting them all via such a generic API may be not efficient.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// 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.api

interface IconManager {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like it's only extended in the IJP by another interface, and in turn has an empty implementation. Do we need it?

fun loadIcon(id: IconIdentifier, path: String, aClass: Class<*>): Icon?
fun registerIcon(id: IconIdentifier, icon: Icon)
Comment on lines +5 to +6
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: for consistency

Suggested change
fun loadIcon(id: IconIdentifier, path: String, aClass: Class<*>): Icon?
fun registerIcon(id: IconIdentifier, icon: Icon)
fun loadIcon(identifier: IconIdentifier, path: String, aClass: Class<*>): Icon?
fun registerIcon(identifier: IconIdentifier, icon: Icon)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

registerIcon method is supposed to be used to register all available icons in advance, right? I think it's better to avoid such a requirement and allow locating icons by demand only.

}

interface IconIdentifier
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One of the things we can solve with new icons API is getting rid of dependency on javax.swing from *.psi modules. It seems that javax.swing.Icon is the only class from javax.swing which is used there (PsiElement interface extends Iconable with getIcon method). So if we introduce IconIdentifier, we can use it in PsiElement instead of javax.swing.Icon and eventually get rid of the latter.

However, to do that we probably need to put IconIdentifier in a different module, so it can be added to dependencies of *.psi modules without having any actual rendering API there.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// 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.api

interface IconState

object DefaultIconState : IconState
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// 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.api

import kotlin.reflect.KClass

interface ImageResourceProvider {
companion object {
@Volatile
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this to be volatile? It may become clearer later why we do, but it is suspicious that we need this for now

private var instance: ImageResourceProvider? = null

@JvmStatic
fun getInstance(): ImageResourceProvider = instance ?: DummyImageResourceProvider

fun activate(provider: ImageResourceProvider) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be internal? Doesn't look like something we want anyone to be able to do at any time

instance = provider
}
}
}

object DummyImageResourceProvider : ImageResourceProvider

interface ImageResource {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this should expose an intrinsic size

companion object
}

interface ImageResourceWithCrossApiCache {
val crossApiCache: CrossApiImageBitmapCache
}

inline fun <reified TBitmap : Any> CrossApiImageBitmapCache.cachedBitmap(noinline generator: () -> TBitmap): TBitmap =
cachedBitmap(TBitmap::class, generator)

interface CrossApiImageBitmapCache {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering there are no actual cross-API implementations (there is a Compose one, and there is an AWT one, but neither is cross-API), I'm not sure we actually really need this... It feels like it could be replaced by a Map<KClass<TBitmap>, TBitmap>, or even by a var cache: TBitmap

fun <TBitmap : Any> cachedBitmap(bitmapClass: KClass<TBitmap>, generator: () -> TBitmap): TBitmap
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This name has me really confused. Is it a getOrPut?

}

interface BitmapImageResource : ImageResource {
fun getRGB(x: Int, y: Int): Int
fun getRGBOrNull(x: Int, y: Int): Int?
Comment on lines +38 to +39
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these return RGB? Or ARGB? Or RGBA? It's unclear from the name. Usages seem to suggest ARGB, but... can you clarify?

Also, why would we need orNull? I don't think we should allow anyone to try to read out of the bounds of the image. It makes no sense.

val width: Int
val height: Int
}

object EmptyBitmapImageResource : BitmapImageResource {
override fun getRGB(x: Int, y: Int): Int = 0
override fun getRGBOrNull(x: Int, y: Int): Int? = 0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this return null?

override val width: Int = 0
override val height: Int = 0
}

interface RescalableImageResource : ImageResource {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this signify that an image can be scaled losslessly? And that by contrast other ImageResources will have lossy scaling?

I'm asking because that's a major gripe I have with the current IJP Swing icon APIs — all images are inherently scalable, the question is whether that's a lossless or lossy scaling. I would prefer being more precise/explicit in this new API if we can.

I would imagine that if my interpretation is correct, then vector-based and runtime-generated icons are inherently losslessly scalable, whereas bitmaps are not.

fun scale(scale: ImageScale): BitmapImageResource
fun calculateExpectedDimensions(scale: ImageScale): Bounds
}

sealed interface ImageScale {
fun calculateScalingFactorByOriginalDimensions(width: Int, height: Int? = null): Float
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a fan of the nullable height. I'd rather pass in a default that's equal to the width, or have separate functions to calculate the scaling by height, width, or square (smallest dimension)

Suggested change
fun calculateScalingFactorByOriginalDimensions(width: Int, height: Int? = null): Float
fun calculateScalingFactorByOriginalDimensions(width: Int, height: Int = width): Float

I'm also not sure what ByOriginalDimensions means

}

class FixedScale(val scale: Float) : ImageScale {
override fun calculateScalingFactorByOriginalDimensions(width: Int, height: Int?): Float = scale

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as FixedScale

return scale == other.scale
}

override fun hashCode(): Int {
return scale.hashCode()
}
}

class FitAreaScale(val width: Int, val height: Int) : ImageScale {
override fun calculateScalingFactorByOriginalDimensions(width: Int, height: Int?): Float {
val scale = this.width / width.toFloat()
if (height != null) {
return minOf(scale, this.height / height.toFloat())
}
return scale
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as FitAreaScale

if (width != other.width) return false
if (height != other.height) return false

return true
}

override fun hashCode(): Int {
var result = width
result = 31 * result + height
return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// 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.api

interface PaintingApi {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this supposed to be an abstraction over Canvas? If so, we may need a bunch more primitives; this only makes it possible to draw a resource image, but does not cover things like icons drawn at runtime.

I'd also consider calling this something like IconPaintingScope given it's supposed to be used like a DSL

val bounds: Bounds
fun drawImage(image: BitmapImageResource, x: Int, y: Int, width: Int? = null, height: Int? = null)
fun drawImage(image: RescalableImageResource, x: Int, y: Int, width: Int? = null, height: Int? = null)
Comment on lines +6 to +7
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto here — can we use non-nullable values for width and height? Using the resource's intrinsic size as defaults looks better to me.

}

class Bounds(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we not using an AWT Dimension to avoid confusion with IJP's own scaling?

val width: Int,
val height: Int
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as Bounds

if (width != other.width) return false
if (height != other.height) return false

return true
}

override fun hashCode(): Int {
var result = width
result = 31 * result + height
return result
}

override fun toString(): String {
return "Bounds(width=$width, height=$height)"
}

fun canFit(other: Bounds): Boolean = other.width <= width && other.height <= height
}
Loading
Loading