Fix 242 icon loading by providing new key-based API (#430)

* Implement icon keys generator

This gradle task generates the icon keys for the platform icons that
we'll need to load icons from a key instead of a path.

* Define IconIdInterpreter and NewUiChecker

* Do icon key generation for AllIcons in ui module

* Implement proper key-based icon loading

* Remove old TODO and unused code

* Undo change to run config

* Rename AllIcons to PlatformIcon, move to its own file

* Remove unnecessary suppression

* Run apiDump

* Made icon generation more maintainable (#432)

* Made icon generation more maintainable

* Cleanup code

* Fix ktlint implicitly trying to check generated code

---------

Co-authored-by: Sebastiano Poggi <sebp@google.com>

---------

Co-authored-by: Lamberto Basti <basti.lamberto@gmail.com>
GitOrigin-RevId: bb5da85d239a72cc907c6edfe873f96629c6e9d2
This commit is contained in:
Sebastiano Poggi
2024-07-04 19:10:32 +02:00
committed by intellij-monorepo-bot
parent b412b503e2
commit d17fbb5d0f
29 changed files with 2161 additions and 79 deletions

View File

@@ -4,9 +4,6 @@
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*

View File

@@ -11,6 +11,7 @@
<inspection_tool class="LanguageDetectionInspection" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NoButtonGroup" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="NoScrollPane" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="StructuralWrap" enabled="true" level="INFORMATION" enabled_by_default="true" />
<inspection_tool class="XsltDeclarations" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="XsltUnusedDeclaration" enabled="false" level="WARNING" enabled_by_default="false" />
<inspection_tool class="XsltVariableShadowing" enabled="false" level="WARNING" enabled_by_default="false" />

View File

@@ -24,6 +24,7 @@ dependencies {
implementation(libs.kotlinx.serialization.json)
implementation(libs.poko.gradlePlugin)
// Enables using type-safe accessors to reference plugins from the [plugins] block defined in
// version catalogs.
// Context: https://github.com/gradle/gradle/issues/15383#issuecomment-779893192

View File

@@ -8,6 +8,9 @@ dependencyResolutionManagement {
repositories {
google()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
maven("https://www.jetbrains.com/intellij-repository/releases")
maven("https://www.jetbrains.com/intellij-repository/snapshots")
maven("https://cache-redirector.jetbrains.com/intellij-dependencies")
gradlePluginPortal()
mavenCentral()
}

View File

@@ -0,0 +1,333 @@
@file:Suppress("UnstableApiUsage")
import com.squareup.kotlinpoet.ClassName
import com.squareup.kotlinpoet.FileSpec
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.TypeSpec
import io.gitlab.arturbosch.detekt.Detekt
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
import org.jetbrains.kotlin.gradle.tasks.BaseKotlinCompile
import java.lang.reflect.Field
import java.net.URLClassLoader
private val defaultOutputDir: Provider<Directory> = layout.buildDirectory.dir("generated/iconKeys")
class IconKeysGeneratorContainer(
container: NamedDomainObjectContainer<IconKeysGeneration>,
) : NamedDomainObjectContainer<IconKeysGeneration> by container
class IconKeysGeneration(
val name: String,
project: Project,
) {
val outputDirectory: DirectoryProperty =
project.objects
.directoryProperty()
.convention(defaultOutputDir)
val sourceClassName: Property<String> = project.objects.property<String>()
val generatedClassName: Property<String> = project.objects.property<String>()
}
val iconGeneration by configurations.registering {
isCanBeConsumed = false
isCanBeResolved = true
}
val extension = IconKeysGeneratorContainer(container<IconKeysGeneration> { IconKeysGeneration(it, project) })
extensions.add("intelliJIconKeysGenerator", extension)
extension.all item@{
val task =
tasks.register<IconKeysGeneratorTask>("generate${name}Keys") task@{
this@task.outputDirectory = this@item.outputDirectory
this@task.sourceClassName = this@item.sourceClassName
this@task.generatedClassName = this@item.generatedClassName
configuration.from(iconGeneration)
dependsOn(iconGeneration)
}
tasks {
withType<BaseKotlinCompile> { dependsOn(task) }
withType<Detekt> { dependsOn(task) }
}
}
pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
the<KotlinJvmProjectExtension>()
.sourceSets["main"]
.kotlin
.srcDir(defaultOutputDir)
}
open class IconKeysGeneratorTask : DefaultTask() {
@get:OutputDirectory
val outputDirectory: DirectoryProperty = project.objects.directoryProperty()
@get:Input
val sourceClassName = project.objects.property<String>()
@get:Input
val generatedClassName = project.objects.property<String>()
@get:InputFiles
val configuration: ConfigurableFileCollection = project.objects.fileCollection()
init {
group = "jewel"
}
@TaskAction
fun generate() {
val guessedSourceClassName = sourceClassName
.map { ClassName.bestGuess(it).canonicalName.replace('.', '/') + ".kt" }
.get()
// The icons artifacts are loaded on the iconGeneration configuration's classpath,
// so we need a classloader that can access these classes.
val classLoader = createClassLoader()
val sourceClass = classLoader.loadClass(sourceClassName.get())
?: throw GradleException(
"Unable to load ${sourceClassName.get()}. " +
"Is the correct dependency declared on the iconGeneration configuration?"
)
// Step 1) load icon mappings from JSON
val mappingsJsonBytes =
classLoader.getResourceAsStream("PlatformIconMappings.json")
?.use { it.readAllBytes() }
?: error("Icon mapping JSON not found")
val iconMappingJson =
json.parseToJsonElement(mappingsJsonBytes.decodeToString())
// Step 2) Transform mappings to a map oldPath -> newPath
val iconMapping = readIconMappingJson(iconMappingJson)
logger.lifecycle("Icon mapping JSON read. It has ${iconMapping.size} entries")
// Step 3) Traverse sourceClass by using reflection, collecting all members
// This step uses the mappings to add the new paths where available.
val dummyIconClass = classLoader.loadClass("com.intellij.ui.DummyIconImpl")
val pathField = dummyIconClass.getPathField()
val rootHolder = IconKeyHolder(sourceClass.simpleName)
pathField.whileForcingAccessible {
visitSourceClass(sourceClass, iconMapping, rootHolder, pathField, classLoader)
}
logger.lifecycle("Read icon keys from ${sourceClass.name}")
// Step 4) Generate output Kotlin file
val fileSpec = generateKotlinCode(rootHolder)
val directory = outputDirectory.get().asFile
fileSpec.writeTo(directory)
logger.lifecycle("Written icon keys for $guessedSourceClassName into $directory")
}
private fun createClassLoader(): URLClassLoader {
val arrayOfURLs = configuration.files
.map { it.toURI().toURL() }
.toTypedArray()
return URLClassLoader(
arrayOfURLs,
IconKeysGeneratorTask::class.java.classLoader
)
}
private fun readIconMappingJson(rawMapping: JsonElement): Map<String, String> {
val flattenedMappings = mutableMapOf<String, Set<String>>()
visitMapping(oldUiPath = "", node = rawMapping, map = flattenedMappings)
return flattenedMappings
.flatMap { (newPath, oldPaths) ->
oldPaths.map { oldPath -> oldPath to newPath }
}.toMap()
}
private fun visitMapping(
oldUiPath: String,
node: JsonElement,
map: MutableMap<String, Set<String>>,
) {
when (node) {
is JsonPrimitive -> {
if (!node.isString) return
map[oldUiPath] = setOf(node.content)
}
is JsonArray -> {
map[oldUiPath] =
node
.filterIsInstance<JsonPrimitive>()
.filter { child -> child.isString }
.map { it.content }
.toSet()
}
is JsonObject -> {
for ((key, value) in node.entries) {
val childOldPath = if (oldUiPath.isNotEmpty()) "$oldUiPath/$key" else key
visitMapping(oldUiPath = childOldPath, node = value, map = map)
}
}
JsonNull -> error("Null nodes not supported")
}
}
private fun Field.whileForcingAccessible(action: () -> Unit) {
@Suppress("DEPRECATION")
val wasAccessible = isAccessible
isAccessible = true
try {
action()
} finally {
isAccessible = wasAccessible
}
}
private fun visitSourceClass(
sourceClass: Class<*>,
iconMappings: Map<String, String>,
parentHolder: IconKeyHolder,
pathField: Field,
classLoader: ClassLoader,
) {
for (child in sourceClass.declaredClasses) {
val childName = "${parentHolder.name}.${child.simpleName}"
val childHolder = IconKeyHolder(childName)
parentHolder.holders += childHolder
visitSourceClass(child, iconMappings, childHolder, pathField, classLoader)
}
parentHolder.holders.sortBy { it.name }
sourceClass.declaredFields
.filter { it.type == javax.swing.Icon::class.java }
.forEach { field ->
val fieldName = "${parentHolder.name}.${field.name}"
if (field.annotations.isNotEmpty()) {
logger.lifecycle(
"$fieldName -> ${field.annotations.joinToString { it!!.annotationClass.qualifiedName!! }}",
)
}
if (field.annotations.any { it.annotationClass == java.lang.Deprecated::class }) {
logger.lifecycle("Ignoring deprecated field: $fieldName")
return
}
val icon = field.get(sourceClass)
val oldPath = pathField.get(icon) as String
val newPath = iconMappings[oldPath]
validatePath(oldPath, fieldName, classLoader)
newPath?.let { validatePath(it, fieldName, classLoader) }
parentHolder.keys += IconKey(fieldName, oldPath, newPath)
}
parentHolder.keys.sortBy { it.name }
}
private fun validatePath(
path: String,
fieldName: String,
classLoader: ClassLoader,
) {
val iconsClass = classLoader.loadClass(sourceClassName.get())
if (iconsClass.getResourceAsStream("/${path.trimStart('/')}") == null) {
logger.warn("Icon $fieldName: '$path' does not exist")
}
}
private fun generateKotlinCode(rootHolder: IconKeyHolder): FileSpec {
val className = ClassName.bestGuess(generatedClassName.get())
return FileSpec
.builder(className)
.apply {
indent(" ")
addFileComment("Generated by the Jewel icon keys generator\n")
addFileComment("Source class: ${sourceClassName.get()}")
addImport(keyClassName.packageName, keyClassName.simpleName)
val objectName = ClassName.bestGuess(generatedClassName.get())
addType(
TypeSpec
.objectBuilder(objectName)
.apply {
for (childHolder in rootHolder.holders) {
generateKotlinCodeInner(childHolder)
}
for (key in rootHolder.keys) {
addProperty(
PropertySpec
.builder(key.name.substringAfterLast('.'), keyClassName)
.initializer(
"%L",
"""IntelliJIconKey("${key.oldPath}", "${key.newPath ?: key.oldPath}")""",
).build(),
)
}
}.build(),
)
}.build()
}
private fun TypeSpec.Builder.generateKotlinCodeInner(holder: IconKeyHolder) {
val objectName = holder.name.substringAfterLast('.')
addType(
TypeSpec
.objectBuilder(objectName)
.apply {
for (childHolder in holder.holders) {
generateKotlinCodeInner(childHolder)
}
for (key in holder.keys) {
addProperty(
PropertySpec
.builder(key.name.substringAfterLast('.'), keyClassName)
.initializer(
"%L",
"""IntelliJIconKey("${key.oldPath}", "${key.newPath ?: key.oldPath}")"""
)
.build(),
)
}
}.build(),
)
}
companion object {
private fun Class<*>.getPathField(): Field = declaredFields.first { it.name == "path" }
private val keyClassName = ClassName("org.jetbrains.jewel.ui.icon", "IntelliJIconKey")
private val json = Json { isLenient = true }
}
}
private data class IconKeyHolder(
val name: String,
val holders: MutableList<IconKeyHolder> = mutableListOf(),
val keys: MutableList<IconKey> = mutableListOf(),
)
private data class IconKey(
val name: String,
val oldPath: String,
val newPath: String?,
)

View File

@@ -25,7 +25,7 @@ class ThemeGeneration(val name: String, project: Project) {
project.objects
.directoryProperty()
.convention(project.layout.buildDirectory.dir("generated/theme"))
val ideaVersion = project.objects.property<String>().convention("232.6734")
val ideaVersion = project.objects.property<String>()
val themeClassName = project.objects.property<String>()
val themeFile = project.objects.property<String>()
}

View File

@@ -26,6 +26,9 @@ kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serializa
jna-core = { module = "net.java.dev.jna:jna", version.ref = "jna" }
intellijPlatform-util-ui = { module = "com.jetbrains.intellij.platform:util-ui", version.ref = "idea" }
intellijPlatform-icons = { module = "com.jetbrains.intellij.platform:icons", version.ref = "idea" }
# Plugin libraries for build-logic's convention plugins to use to resolve the types/tasks coming from these plugins
detekt-gradlePlugin = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" }
dokka-gradlePlugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" }

View File

@@ -87,6 +87,10 @@ public final class org/jetbrains/jewel/bridge/actionSystem/ProvideDataKt {
public static final fun provideData (Landroidx/compose/ui/Modifier;Lkotlin/jvm/functions/Function1;)Landroidx/compose/ui/Modifier;
}
public final class org/jetbrains/jewel/bridge/icon/IntelliJIconKeyKt {
public static final fun fromPlatformIcon (Lorg/jetbrains/jewel/ui/icon/IntelliJIconKey$Companion;Ljavax/swing/Icon;)Lorg/jetbrains/jewel/ui/icon/IconKey;
}
public final class org/jetbrains/jewel/bridge/theme/BridgeGlobalColorsKt {
public static final fun readFromLaF (Lorg/jetbrains/jewel/foundation/BorderColors$Companion;)Lorg/jetbrains/jewel/foundation/BorderColors;
public static final fun readFromLaF (Lorg/jetbrains/jewel/foundation/GlobalColors$Companion;)Lorg/jetbrains/jewel/foundation/GlobalColors;

View File

@@ -0,0 +1,8 @@
package org.jetbrains.jewel.bridge.icon
import org.jetbrains.jewel.bridge.isNewUiTheme
import org.jetbrains.jewel.ui.icon.NewUiChecker
internal object BridgeNewUiChecker : NewUiChecker {
override fun isNewUi(): Boolean = isNewUiTheme()
}

View File

@@ -0,0 +1,20 @@
package org.jetbrains.jewel.bridge.icon
import com.intellij.ui.icons.CachedImageIcon
import org.jetbrains.jewel.ui.icon.IconKey
import org.jetbrains.jewel.ui.icon.IntelliJIconKey
@Suppress("UnstableApiUsage") // We need to use internal APIs
public fun IntelliJIconKey.Companion.fromPlatformIcon(icon: javax.swing.Icon): IconKey {
check(icon is CachedImageIcon) {
"Only resource-backed CachedImageIcons are supported (e.g., coming from AllIcons)"
}
val oldUiPath =
checkNotNull(icon.originalPath) {
"Only resource-backed CachedImageIcons are supported (e.g., coming from AllIcons)"
}
val newUiPath = icon.expUIPath ?: oldUiPath
return IntelliJIconKey(oldUiPath, newUiPath)
}

View File

@@ -8,9 +8,11 @@ import androidx.compose.ui.platform.LocalDensity
import com.intellij.openapi.components.service
import org.jetbrains.jewel.bridge.BridgePainterHintsProvider
import org.jetbrains.jewel.bridge.SwingBridgeService
import org.jetbrains.jewel.bridge.icon.BridgeNewUiChecker
import org.jetbrains.jewel.bridge.scaleDensityWithIdeScale
import org.jetbrains.jewel.foundation.ExperimentalJewelApi
import org.jetbrains.jewel.ui.ComponentStyling
import org.jetbrains.jewel.ui.icon.LocalNewUiChecker
import org.jetbrains.jewel.ui.painter.LocalPainterHintsProvider
import org.jetbrains.jewel.ui.theme.BaseJewelTheme
@@ -29,6 +31,7 @@ public fun SwingBridgeTheme(content: @Composable () -> Unit) {
) {
CompositionLocalProvider(
LocalPainterHintsProvider provides BridgePainterHintsProvider(themeData.themeDefinition.isDark),
LocalNewUiChecker provides BridgeNewUiChecker,
LocalDensity provides scaleDensityWithIdeScale(LocalDensity.current),
) {
content()

View File

@@ -0,0 +1,7 @@
package org.jetbrains.jewel.intui.standalone.icon
import org.jetbrains.jewel.ui.icon.NewUiChecker
internal object StandaloneNewUiChecker : NewUiChecker {
override fun isNewUi(): Boolean = true
}

View File

@@ -13,6 +13,7 @@ import org.jetbrains.jewel.foundation.theme.ThemeIconData
import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme
import org.jetbrains.jewel.intui.core.theme.IntUiLightTheme
import org.jetbrains.jewel.intui.standalone.StandalonePainterHintsProvider
import org.jetbrains.jewel.intui.standalone.icon.StandaloneNewUiChecker
import org.jetbrains.jewel.intui.standalone.styling.Default
import org.jetbrains.jewel.intui.standalone.styling.Editor
import org.jetbrains.jewel.intui.standalone.styling.Outlined
@@ -40,6 +41,7 @@ import org.jetbrains.jewel.ui.component.styling.TabStyle
import org.jetbrains.jewel.ui.component.styling.TextAreaStyle
import org.jetbrains.jewel.ui.component.styling.TextFieldStyle
import org.jetbrains.jewel.ui.component.styling.TooltipStyle
import org.jetbrains.jewel.ui.icon.LocalNewUiChecker
import org.jetbrains.jewel.ui.painter.LocalPainterHintsProvider
import org.jetbrains.jewel.ui.theme.BaseJewelTheme
@@ -233,6 +235,7 @@ public fun IntUiTheme(
) {
CompositionLocalProvider(
LocalPainterHintsProvider provides StandalonePainterHintsProvider(theme),
LocalNewUiChecker provides StandaloneNewUiChecker,
) {
content()
}

View File

@@ -0,0 +1,7 @@
package icons
import org.jetbrains.jewel.ui.icon.IntelliJIconKey
object IdeSampleIconKeys {
val gitHub = IntelliJIconKey("icons/github.svg", "icons/github.svg")
}

View File

@@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -30,7 +31,7 @@ import com.intellij.icons.AllIcons
import com.intellij.ide.BrowserUtil
import com.intellij.ui.JBColor
import com.intellij.util.ui.JBUI
import icons.JewelIcons
import icons.IdeSampleIconKeys
import org.jetbrains.jewel.bridge.LocalComponent
import org.jetbrains.jewel.bridge.toComposeColor
import org.jetbrains.jewel.foundation.lazy.tree.buildTree
@@ -53,6 +54,7 @@ import org.jetbrains.jewel.ui.component.Icon
import org.jetbrains.jewel.ui.component.IconButton
import org.jetbrains.jewel.ui.component.LazyTree
import org.jetbrains.jewel.ui.component.OutlinedButton
import org.jetbrains.jewel.ui.component.PlatformIcon
import org.jetbrains.jewel.ui.component.RadioButtonRow
import org.jetbrains.jewel.ui.component.Slider
import org.jetbrains.jewel.ui.component.Text
@@ -60,6 +62,12 @@ import org.jetbrains.jewel.ui.component.TextField
import org.jetbrains.jewel.ui.component.Tooltip
import org.jetbrains.jewel.ui.component.Typography
import org.jetbrains.jewel.ui.component.separator
import org.jetbrains.jewel.ui.icons.AllIconsKeys
import org.jetbrains.jewel.ui.painter.badge.DotBadgeShape
import org.jetbrains.jewel.ui.painter.hints.Badge
import org.jetbrains.jewel.ui.painter.hints.Size
import org.jetbrains.jewel.ui.painter.hints.Stroke
import org.jetbrains.jewel.ui.theme.colorPalette
@Composable
internal fun ComponentShowcaseTab() {
@@ -68,7 +76,8 @@ internal fun ComponentShowcaseTab() {
val scrollState = rememberScrollState()
Row(
modifier =
Modifier.trackComponentActivation(LocalComponent.current)
Modifier
.trackComponentActivation(LocalComponent.current)
.fillMaxSize()
.background(bgColor)
.verticalScroll(scrollState)
@@ -177,27 +186,7 @@ private fun RowScope.ColumnOne() {
}
}
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
Icon(
"actions/close.svg",
iconClass = AllIcons::class.java,
modifier = Modifier.border(1.dp, Color.Magenta),
contentDescription = "An icon",
)
Icon(
"icons/github.svg",
iconClass = JewelIcons::class.java,
modifier = Modifier.border(1.dp, Color.Magenta),
contentDescription = "An icon",
)
IconButton(onClick = { }) {
Icon("actions/close.svg", contentDescription = "An icon", AllIcons::class.java)
}
IconButton(onClick = { }) {
Icon("actions/addList.svg", contentDescription = "An icon", AllIcons::class.java)
}
}
IconsShowcase()
Row(
verticalAlignment = Alignment.CenterVertically,
@@ -235,6 +224,74 @@ private fun RowScope.ColumnOne() {
}
}
@Composable
private fun IconsShowcase() {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) {
PlatformIcon(AllIconsKeys.Nodes.ConfigFolder, "taskGroup")
}
Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) {
PlatformIcon(AllIconsKeys.Nodes.ConfigFolder, "taskGroup", hint = Badge(Color.Red, DotBadgeShape.Default))
}
Box(
Modifier.size(24.dp).background(JewelTheme.colorPalette.blue(4), shape = RoundedCornerShape(4.dp)),
contentAlignment = Alignment.Center,
) {
PlatformIcon(AllIconsKeys.Nodes.ConfigFolder, "taskGroup", hint = Stroke(Color.White))
}
Box(
Modifier.size(24.dp).background(JewelTheme.colorPalette.blue(4), shape = RoundedCornerShape(4.dp)),
contentAlignment = Alignment.Center,
) {
PlatformIcon(
AllIconsKeys.Nodes.ConfigFolder,
"taskGroup",
hints = arrayOf(Stroke(Color.White), Badge(Color.Red, DotBadgeShape.Default)),
)
}
Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) {
PlatformIcon(AllIconsKeys.Nodes.ConfigFolder, "taskGroup", hint = Size(20))
}
Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) {
PlatformIcon(
AllIconsKeys.Actions.Close,
"An icon",
modifier = Modifier.border(1.dp, Color.Magenta),
hint = Size(20),
)
}
Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) {
PlatformIcon(AllIconsKeys.Nodes.ConfigFolder, "taskGroup", hint = Size(20))
}
Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) {
Icon(
IdeSampleIconKeys.gitHub,
iconClass = IdeSampleIconKeys::class.java,
modifier = Modifier.border(1.dp, Color.Magenta),
contentDescription = "An owned icon",
)
}
IconButton(onClick = { }, Modifier.size(24.dp)) {
PlatformIcon(AllIconsKeys.Actions.Close, "Close")
}
IconButton(onClick = { }, Modifier.size(24.dp)) {
PlatformIcon(AllIconsKeys.Actions.AddList, "Close")
}
}
}
@Composable
private fun RowScope.ColumnTwo() {
Column(
@@ -308,14 +365,18 @@ private fun MarkdownExample() {
|fun hello() = "World"
|```
""".trimMargin(),
Modifier.fillMaxWidth()
.background(JBUI.CurrentTheme.Banner.INFO_BACKGROUND.toComposeColor(), RoundedCornerShape(8.dp))
.border(
1.dp,
JBUI.CurrentTheme.Banner.INFO_BORDER_COLOR.toComposeColor(),
Modifier
.fillMaxWidth()
.background(
JBUI.CurrentTheme.Banner.INFO_BACKGROUND
.toComposeColor(),
RoundedCornerShape(8.dp),
)
.padding(8.dp),
).border(
1.dp,
JBUI.CurrentTheme.Banner.INFO_BORDER_COLOR
.toComposeColor(),
RoundedCornerShape(8.dp),
).padding(8.dp),
enabled = enabled,
onUrlClick = { url -> BrowserUtil.open(url) },
)

View File

@@ -18,6 +18,7 @@ dependencies {
implementation(compose.desktop.currentOs) {
exclude(group = "org.jetbrains.compose.material")
}
implementation(libs.intellijPlatform.icons)
}
compose.desktop {
@@ -48,10 +49,11 @@ tasks {
// afterEvaluate is needed because the Compose Gradle Plugin
// register the task in the afterEvaluate block
afterEvaluate {
javaLauncher = project.javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.JETBRAINS
}
javaLauncher =
project.javaToolchains.launcherFor {
languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.JETBRAINS
}
setExecutable(javaLauncher.map { it.executablePath.asFile.absolutePath }.get())
}
}

View File

@@ -1,3 +1,7 @@
package org.jetbrains.jewel.samples.standalone
object StandaloneSampleIcons
import org.jetbrains.jewel.ui.icon.IntelliJIconKey
object StandaloneSampleIcons {
val gitHub = IntelliJIconKey("icons/github.svg", "icons/github.svg")
}

View File

@@ -20,7 +20,9 @@ import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.samples.standalone.StandaloneSampleIcons
import org.jetbrains.jewel.samples.standalone.viewmodel.View
import org.jetbrains.jewel.ui.component.Icon
import org.jetbrains.jewel.ui.component.PlatformIcon
import org.jetbrains.jewel.ui.component.Text
import org.jetbrains.jewel.ui.icons.AllIconsKeys
import org.jetbrains.jewel.ui.painter.badge.DotBadgeShape
import org.jetbrains.jewel.ui.painter.hints.Badge
import org.jetbrains.jewel.ui.painter.hints.Size
@@ -56,35 +58,30 @@ internal fun Icons() {
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
val iconProvider =
rememberResourcePainterProvider("icons/taskGroup.svg", StandaloneSampleIcons::class.java)
val normal by iconProvider.getPainter()
val stroked by iconProvider.getPainter(Stroke(Color.White))
val badged by iconProvider.getPainter(Badge(Color.Red, DotBadgeShape.Default))
val strokedAndBadged by iconProvider.getPainter(Badge(Color.Red, DotBadgeShape.Default), Stroke(Color.White))
val resized by iconProvider.getPainter(Size(20))
Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) {
Icon(normal, "taskGroup")
PlatformIcon(AllIconsKeys.Nodes.ConfigFolder, "taskGroup")
}
Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) {
Icon(badged, "taskGroup")
PlatformIcon(AllIconsKeys.Nodes.ConfigFolder, "taskGroup", hint = Badge(Color.Red, DotBadgeShape.Default))
}
Box(
Modifier.size(24.dp).background(JewelTheme.colorPalette.blue(4), shape = RoundedCornerShape(4.dp)),
contentAlignment = Alignment.Center,
) {
Icon(stroked, "taskGroup")
PlatformIcon(AllIconsKeys.Nodes.ConfigFolder, "taskGroup", hint = Stroke(Color.White))
}
Box(
Modifier.size(24.dp).background(JewelTheme.colorPalette.blue(4), shape = RoundedCornerShape(4.dp)),
contentAlignment = Alignment.Center,
) {
Icon(strokedAndBadged, "taskGroup")
PlatformIcon(
AllIconsKeys.Nodes.ConfigFolder,
"taskGroup",
hints = arrayOf(Stroke(Color.White), Badge(Color.Red, DotBadgeShape.Default)),
)
}
Box(Modifier.size(24.dp), contentAlignment = Alignment.Center) {
Icon(resized, "taskGroup")
PlatformIcon(AllIconsKeys.Nodes.ConfigFolder, "taskGroup", hint = Size(20))
}
}
}

View File

@@ -87,7 +87,7 @@ internal fun MarkdownPreview(
val lazyListState = rememberLazyListState()
LazyMarkdown(
markdownBlocks = markdownBlocks,
modifier = modifier.background(background),
modifier = Modifier.background(background),
contentPadding = PaddingValues(16.dp),
state = lazyListState,
selectable = true,

View File

@@ -1,8 +0,0 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V7.38947C14.1372 6.82692 13.1068 6.5 12 6.5C8.96243 6.5 6.5 8.96243 6.5 12C6.5 12.7056 6.63286 13.3801 6.87494 14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" fill="#EBECF0"/>
<path d="M6.03823 3L8.09379 5H13C13.5523 5 14 5.44772 14 6V6.87494C14.3525 7.01259 14.6872 7.18555 15 7.38947V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H6.87494C6.75003 13.6801 6.65419 13.3457 6.59069 13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823Z" fill="#6C707E"/>
<path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" fill="#3574F0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.1216 15.6756C13.0484 15.8707 12.8619 16 12.6534 16H11.3464C11.138 16 10.9515 15.8707 10.8783 15.6756L10.6288 15.0102C10.5449 14.7865 10.3143 14.6534 10.0786 14.6926L9.37765 14.8092C9.17206 14.8434 8.96681 14.7464 8.8626 14.5659L8.2091 13.4341C8.10489 13.2536 8.12357 13.0273 8.25599 12.8664L8.70742 12.3177C8.85925 12.1331 8.85925 11.8669 8.70742 11.6824L8.25597 11.1336C8.12355 10.9727 8.10487 10.7464 8.20908 10.5659L8.86258 9.43405C8.96679 9.25355 9.17204 9.15663 9.37763 9.19083L10.0786 9.30742C10.3143 9.34664 10.5449 9.21353 10.6288 8.98976L10.8783 8.32444C10.9515 8.12929 11.138 8 11.3464 8H12.6534C12.8619 8 13.0484 8.12929 13.1216 8.32444L13.3711 8.98976C13.455 9.21353 13.6856 9.34664 13.9213 9.30742L14.6222 9.19083C14.8278 9.15663 15.0331 9.25355 15.1373 9.43405L15.7908 10.5659C15.895 10.7464 15.8763 10.9727 15.7439 11.1336L15.2925 11.6824C15.1406 11.8669 15.1406 12.1331 15.2925 12.3177L15.7439 12.8664C15.8763 13.0273 15.895 13.2536 15.7908 13.4341L15.1373 14.5659C15.0331 14.7464 14.8278 14.8434 14.6222 14.8092L13.9213 14.6926C13.6856 14.6534 13.455 14.7865 13.3711 15.0102L13.1216 15.6756ZM11.6929 15H12.3069L12.4348 14.6591C12.6865 13.9878 13.3781 13.5885 14.0854 13.7061L14.4445 13.7659L14.7515 13.2341L14.5202 12.953C14.0647 12.3993 14.0647 11.6007 14.5202 11.047L14.7515 10.7659L14.4445 10.2341L14.0854 10.2939C13.3781 10.4115 12.6865 10.0122 12.4348 9.34088L12.3069 9H11.6929L11.5651 9.34088C11.3134 10.0122 10.6217 10.4115 9.91449 10.2939L9.55536 10.2341L9.24836 10.7659L9.47966 11.047C9.93517 11.6007 9.93517 12.3993 9.47966 12.953L9.24838 13.2341L9.55538 13.7659L9.91449 13.7061C10.6217 13.5885 11.3134 13.9878 11.5651 14.6591L11.6929 15Z" fill="#3574F0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.3069 15H11.6929L11.5651 14.6591C11.3134 13.9878 10.6217 13.5885 9.91449 13.7061L9.55537 13.7659L9.24837 13.2341L9.47966 12.953C9.93517 12.3993 9.93517 11.6007 9.47966 11.047L9.24835 10.7659L9.55535 10.2341L9.91449 10.2939C10.6217 10.4115 11.3134 10.0122 11.5651 9.34088L11.6929 9H12.3069L12.4348 9.34088C12.6865 10.0122 13.3781 10.4115 14.0854 10.2939L14.4445 10.2341L14.7515 10.7659L14.5202 11.047C14.0647 11.6007 14.0647 12.3993 14.5202 12.953L14.7515 13.2341L14.4445 13.7659L14.0854 13.7061C13.3781 13.5885 12.6865 13.9878 12.4348 14.6591L12.3069 15ZM13 12C13 12.5523 12.5523 13 12 13C11.4477 13 11 12.5523 11 12C11 11.4477 11.4477 11 12 11C12.5523 11 13 11.4477 13 12Z" fill="#E7EFFD"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -1,8 +0,0 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 3.86667C1 2.83574 1.7835 2 2.75 2H6.03823C6.29871 2 6.5489 2.10163 6.73559 2.28327L8.5 4L13 4C14.1046 4 15 4.89543 15 6V7.38947C14.1372 6.82692 13.1068 6.5 12 6.5C8.96243 6.5 6.5 8.96243 6.5 12C6.5 12.7056 6.63286 13.3801 6.87494 14H2.75C1.7835 14 1 13.1643 1 12.1333V3.86667Z" fill="#43454A"/>
<path d="M6.03823 3L8.09379 5H13C13.5523 5 14 5.44772 14 6V6.87494C14.3525 7.01259 14.6872 7.18555 15 7.38947V6C15 4.89543 14.1046 4 13 4L8.5 4L6.73559 2.28327C6.5489 2.10163 6.29871 2 6.03823 2H2.75C1.7835 2 1 2.83574 1 3.86667V12.1333C1 13.1643 1.7835 14 2.75 14H6.87494C6.75003 13.6801 6.65419 13.3457 6.59069 13H2.75C2.3956 13 2 12.6738 2 12.1333V3.86667C2 3.32624 2.3956 3 2.75 3H6.03823Z" fill="#CED0D6"/>
<path d="M12 13C12.5523 13 13 12.5523 13 12C13 11.4477 12.5523 11 12 11C11.4477 11 11 11.4477 11 12C11 12.5523 11.4477 13 12 13Z" fill="#548AF7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.1216 15.6756C13.0484 15.8707 12.8619 16 12.6534 16H11.3464C11.138 16 10.9515 15.8707 10.8783 15.6756L10.6288 15.0102C10.5449 14.7865 10.3143 14.6534 10.0786 14.6926L9.37765 14.8092C9.17206 14.8434 8.96681 14.7464 8.8626 14.5659L8.2091 13.4341C8.10489 13.2536 8.12357 13.0273 8.25599 12.8664L8.70742 12.3177C8.85925 12.1331 8.85925 11.8669 8.70742 11.6824L8.25597 11.1336C8.12355 10.9727 8.10487 10.7464 8.20908 10.5659L8.86258 9.43405C8.96679 9.25355 9.17204 9.15663 9.37763 9.19083L10.0786 9.30742C10.3143 9.34664 10.5449 9.21353 10.6288 8.98976L10.8783 8.32444C10.9515 8.12929 11.138 8 11.3464 8H12.6534C12.8619 8 13.0484 8.12929 13.1216 8.32444L13.3711 8.98976C13.455 9.21353 13.6856 9.34664 13.9213 9.30742L14.6222 9.19083C14.8278 9.15663 15.0331 9.25355 15.1373 9.43405L15.7908 10.5659C15.895 10.7464 15.8763 10.9727 15.7439 11.1336L15.2925 11.6824C15.1406 11.8669 15.1406 12.1331 15.2925 12.3177L15.7439 12.8664C15.8763 13.0273 15.895 13.2536 15.7908 13.4341L15.1373 14.5659C15.0331 14.7464 14.8278 14.8434 14.6222 14.8092L13.9213 14.6926C13.6856 14.6534 13.455 14.7865 13.3711 15.0102L13.1216 15.6756ZM11.6929 15H12.3069L12.4348 14.6591C12.6865 13.9878 13.3781 13.5885 14.0854 13.7061L14.4445 13.7659L14.7515 13.2341L14.5202 12.953C14.0647 12.3993 14.0647 11.6007 14.5202 11.047L14.7515 10.7659L14.4445 10.2341L14.0854 10.2939C13.3781 10.4115 12.6865 10.0122 12.4348 9.34088L12.3069 9H11.6929L11.5651 9.34088C11.3134 10.0122 10.6217 10.4115 9.91449 10.2939L9.55536 10.2341L9.24836 10.7659L9.47966 11.047C9.93517 11.6007 9.93517 12.3993 9.47966 12.953L9.24838 13.2341L9.55538 13.7659L9.91449 13.7061C10.6217 13.5885 11.3134 13.9878 11.5651 14.6591L11.6929 15Z" fill="#548AF7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12.3069 15H11.6929L11.5651 14.6591C11.3134 13.9878 10.6217 13.5885 9.91449 13.7061L9.55537 13.7659L9.24837 13.2341L9.47966 12.953C9.93517 12.3993 9.93517 11.6007 9.47966 11.047L9.24835 10.7659L9.55535 10.2341L9.91449 10.2939C10.6217 10.4115 11.3134 10.0122 11.5651 9.34088L11.6929 9H12.3069L12.4348 9.34088C12.6865 10.0122 13.3781 10.4115 14.0854 10.2939L14.4445 10.2341L14.7515 10.7659L14.5202 11.047C14.0647 11.6007 14.0647 12.3993 14.5202 12.953L14.7515 13.2341L14.4445 13.7659L14.0854 13.7061C13.3781 13.5885 12.6865 13.9878 12.4348 14.6591L12.3069 15ZM13 12C13 12.5523 12.5523 13 12 13C11.4477 13 11 12.5523 11 12C11 11.4477 11.4477 11 12 11C12.5523 11 13 11.4477 13 12Z" fill="#25324D"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.5 KiB

View File

@@ -18,6 +18,9 @@ dependencyResolutionManagement {
repositories {
google()
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
maven("https://www.jetbrains.com/intellij-repository/releases")
maven("https://www.jetbrains.com/intellij-repository/snapshots")
maven("https://cache-redirector.jetbrains.com/intellij-dependencies")
mavenCentral()
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,12 @@
import org.jetbrains.compose.ComposeBuildConfig
import org.jmailen.gradle.kotlinter.tasks.FormatTask
import org.jmailen.gradle.kotlinter.tasks.LintTask
plugins {
jewel
`jewel-publish`
`jewel-check-public-api`
`icon-keys-generator`
alias(libs.plugins.composeDesktop)
alias(libs.plugins.kotlinx.serialization)
}
@@ -13,9 +16,25 @@ private val composeVersion
dependencies {
api(projects.foundation)
iconGeneration(libs.intellijPlatform.util.ui)
iconGeneration(libs.intellijPlatform.icons)
testImplementation(compose.desktop.uiTestJUnit4)
testImplementation(compose.desktop.currentOs) {
exclude(group = "org.jetbrains.compose.material")
}
}
intelliJIconKeysGenerator {
register("AllIcons") {
sourceClassName = "com.intellij.icons.AllIcons"
generatedClassName = "org.jetbrains.jewel.ui.icons.AllIconsKeys"
}
}
tasks.withType<LintTask> {
include("src/**") // Excluding build/ doesn't work for some reason
}
tasks.withType<FormatTask> {
include("src/**") // Excluding build/ doesn't work for some reason
}

View File

@@ -31,6 +31,10 @@ import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.icon.IconKey
import org.jetbrains.jewel.ui.icon.newUiChecker
import org.jetbrains.jewel.ui.painter.PainterHint
import org.jetbrains.jewel.ui.painter.rememberResourcePainterProvider
import org.jetbrains.jewel.ui.util.thenIf
import org.xml.sax.InputSource
@@ -43,9 +47,30 @@ public fun Icon(
iconClass: Class<*>,
colorFilter: ColorFilter?,
modifier: Modifier = Modifier,
vararg hints: PainterHint,
) {
val painterProvider = rememberResourcePainterProvider(resource, iconClass)
val painter by painterProvider.getPainter()
val painter by painterProvider.getPainter(*hints)
Icon(
painter = painter,
contentDescription = contentDescription,
modifier = modifier,
colorFilter = colorFilter,
)
}
@Composable
public fun Icon(
resource: String,
contentDescription: String?,
iconClass: Class<*>,
colorFilter: ColorFilter?,
modifier: Modifier = Modifier,
hint: PainterHint,
) {
val painterProvider = rememberResourcePainterProvider(resource, iconClass)
val painter by painterProvider.getPainter(hint)
Icon(
painter = painter,
@@ -62,9 +87,10 @@ public fun Icon(
iconClass: Class<*>,
modifier: Modifier = Modifier,
tint: Color = Color.Unspecified,
vararg hints: PainterHint,
) {
val painterProvider = rememberResourcePainterProvider(resource, iconClass)
val painter by painterProvider.getPainter()
val painter by painterProvider.getPainter(*hints)
Icon(
painter = painter,
@@ -74,6 +100,54 @@ public fun Icon(
)
}
@Composable
public fun Icon(
resource: String,
contentDescription: String?,
iconClass: Class<*>,
modifier: Modifier = Modifier,
tint: Color = Color.Unspecified,
hint: PainterHint,
) {
val painterProvider = rememberResourcePainterProvider(resource, iconClass)
val painter by painterProvider.getPainter(hint)
Icon(
painter = painter,
contentDescription = contentDescription,
modifier = modifier,
tint = tint,
)
}
@Composable
public fun Icon(
key: IconKey,
contentDescription: String?,
iconClass: Class<*>,
modifier: Modifier = Modifier,
tint: Color = Color.Unspecified,
vararg hints: PainterHint,
) {
val isNewUi = JewelTheme.newUiChecker.isNewUi()
val path = remember(key, isNewUi) { key.path(isNewUi) }
Icon(path, contentDescription, iconClass, modifier, tint, *hints)
}
@Composable
public fun Icon(
key: IconKey,
contentDescription: String?,
iconClass: Class<*>,
modifier: Modifier = Modifier,
tint: Color = Color.Unspecified,
hint: PainterHint,
) {
val isNewUi = JewelTheme.newUiChecker.isNewUi()
val path = remember(key, isNewUi) { key.path(isNewUi) }
Icon(path, contentDescription, iconClass, modifier, tint, hint)
}
/**
* Icon component that draws [imageVector] using [tint], defaulting to
* [Color.Unspecified].
@@ -183,14 +257,14 @@ public fun Icon(
Modifier
}
Box(
modifier.toolingGraphicsLayer()
modifier
.toolingGraphicsLayer()
.defaultSizeFor(painter)
.paint(
painter,
colorFilter = colorFilter,
contentScale = ContentScale.Fit,
)
.then(semantics),
).then(semantics),
)
}

View File

@@ -0,0 +1,30 @@
package org.jetbrains.jewel.ui.component
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import org.jetbrains.jewel.ui.icon.IntelliJIconKey
import org.jetbrains.jewel.ui.icons.AllIconsKeys
import org.jetbrains.jewel.ui.painter.PainterHint
@Composable
public fun PlatformIcon(
key: IntelliJIconKey,
contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = Color.Unspecified,
hint: PainterHint,
) {
PlatformIcon(key, contentDescription, modifier, tint, *arrayOf(hint))
}
@Composable
public fun PlatformIcon(
key: IntelliJIconKey,
contentDescription: String?,
modifier: Modifier = Modifier,
tint: Color = Color.Unspecified,
vararg hints: PainterHint,
) {
Icon(key, contentDescription, AllIconsKeys::class.java, modifier, tint, *hints)
}

View File

@@ -0,0 +1,24 @@
package org.jetbrains.jewel.ui.icon
import org.jetbrains.jewel.foundation.GenerateDataFunctions
public interface IconKey {
public fun path(isNewUi: Boolean): String
}
@GenerateDataFunctions
public class PathIconKey(
private val path: String,
) : IconKey {
override fun path(isNewUi: Boolean): String = path
}
@GenerateDataFunctions
public class IntelliJIconKey(
public val oldUiPath: String,
public val newUiPath: String,
) : IconKey {
override fun path(isNewUi: Boolean): String = if (isNewUi) newUiPath else oldUiPath
public companion object
}

View File

@@ -0,0 +1,16 @@
package org.jetbrains.jewel.ui.icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ProvidableCompositionLocal
import androidx.compose.runtime.staticCompositionLocalOf
import org.jetbrains.jewel.foundation.theme.JewelTheme
public fun interface NewUiChecker {
public fun isNewUi(): Boolean
}
public val LocalNewUiChecker: ProvidableCompositionLocal<NewUiChecker> =
staticCompositionLocalOf { error("No NewUiChecker provided") }
public val JewelTheme.Companion.newUiChecker: NewUiChecker
@Composable get() = LocalNewUiChecker.current

View File

@@ -9,6 +9,7 @@ import org.jetbrains.jewel.ui.painter.PainterHint
import org.jetbrains.jewel.ui.painter.PainterProviderScope
import org.jetbrains.jewel.ui.painter.PainterWrapperHint
import org.jetbrains.jewel.ui.painter.badge.BadgeShape
import org.jetbrains.jewel.ui.painter.badge.DotBadgeShape
@GenerateDataFunctions
private class BadgeImpl(
@@ -22,5 +23,5 @@ private class BadgeImpl(
@Suppress("FunctionName")
public fun Badge(
color: Color,
shape: BadgeShape,
shape: BadgeShape = DotBadgeShape.Default,
): PainterHint = if (color.isSpecified) BadgeImpl(color, shape) else PainterHint.None