diff --git a/.idea/detekt.xml b/.idea/detekt.xml
index a921d6684..efda1cfc1 100644
--- a/.idea/detekt.xml
+++ b/.idea/detekt.xml
@@ -11,5 +11,7 @@
+
+
\ No newline at end of file
diff --git a/.idea/externalDependencies.xml b/.idea/externalDependencies.xml
index 5d8159909..ab326d5c9 100644
--- a/.idea/externalDependencies.xml
+++ b/.idea/externalDependencies.xml
@@ -3,5 +3,6 @@
+
\ No newline at end of file
diff --git a/buildSrc/src/main/kotlin/jewel.gradle.kts b/buildSrc/src/main/kotlin/jewel.gradle.kts
index 6f02f49e5..e6fc51148 100644
--- a/buildSrc/src/main/kotlin/jewel.gradle.kts
+++ b/buildSrc/src/main/kotlin/jewel.gradle.kts
@@ -1,6 +1,7 @@
@file:Suppress("UnstableApiUsage")
import io.gitlab.arturbosch.detekt.Detekt
+import org.gradle.api.attributes.Usage
import org.jmailen.gradle.kotlinter.tasks.LintTask
plugins {
diff --git a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntUiThemeDescriptorReader.kt b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntUiThemeDescriptorReader.kt
index 715236bf7..8ace1fd57 100644
--- a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntUiThemeDescriptorReader.kt
+++ b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntUiThemeDescriptorReader.kt
@@ -30,8 +30,7 @@ internal object IntUiThemeDescriptorReader {
) =
FileSpec.builder(className).apply {
indent(" ")
- this.
- addFileComment("Generated by the Jewel Int UI Palette Generator\n")
+ this.addFileComment("Generated by the Jewel Int UI Palette Generator\n")
addFileComment("Generated from the IntelliJ Platform version $ideaVersion\n")
addFileComment("Source: $descriptorUrl")
@@ -78,6 +77,23 @@ internal object IntUiThemeDescriptorReader {
}.forEach { (group, colors) ->
readColorGroup(className, group, colors)
}
+
+ val rawMapProperty = PropertySpec
+ .builder(
+ "rawMap",
+ Map::class.asClassName().parameterizedBy(String::class.asClassName(), colorClassName),
+ KModifier.OVERRIDE
+ )
+ .initializer(
+ colors
+ .map { (key, value) ->
+ val colorHexString = value.replace("#", "0xFF")
+ CodeBlock.of("%S to Color(%L)", key, colorHexString)
+ }
+ .joinToCode(prefix = "mapOf(", separator = ",\n", suffix = ")")
+ )
+ .build()
+ addProperty(rawMapProperty)
}.build())
addProperty(
@@ -159,6 +175,7 @@ internal object IntUiThemeDescriptorReader {
}
addProperty(createOverrideStringMapProperty("iconOverrides", iconOverrides))
+ addProperty(createOverrideStringMapProperty("selectionColorPalette", theme.iconColorsOnSelection))
}.build())
addProperty(
diff --git a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntelliJThemeGeneratorPlugin.kt b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntelliJThemeGeneratorPlugin.kt
index c6f025d85..8b9ac863b 100644
--- a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntelliJThemeGeneratorPlugin.kt
+++ b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntelliJThemeGeneratorPlugin.kt
@@ -11,6 +11,7 @@ import org.gradle.api.NamedDomainObjectContainer
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.file.DirectoryProperty
+import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
@@ -27,30 +28,32 @@ import java.net.URL
abstract class IntelliJThemeGeneratorPlugin : Plugin {
- final override fun apply(target: Project): Unit = with(target) {
- val extension = ThemeGeneratorContainer(container { ThemeGeneration(it, project) })
- extensions.add("intelliJThemeGenerator", extension)
-
- extension.all {
- val task = tasks.register("generate${GUtil.toCamelCase(name)}Theme") {
- outputFile.set(targetDir.file(this@all.themeClassName.map {
- val className = ClassName.bestGuess(it)
- className.packageName.replace(".", "/")
- .plus("/${className.simpleName}.kt")
- }))
- themeClassName.set(this@all.themeClassName)
- ideaVersion.set(this@all.ideaVersion)
- themeFile.set(this@all.themeFile)
- }
- tasks.withType {
- dependsOn(task)
- }
- tasks.withType {
- dependsOn(task)
- }
- pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
- extensions.getByType().apply {
- sourceSets["main"].kotlin.srcDir(targetDir)
+ final override fun apply(target: Project) {
+ with(target) {
+ val extension = ThemeGeneratorContainer(container { ThemeGeneration(it, project) })
+ extensions.add("intelliJThemeGenerator", extension)
+
+ extension.all {
+ val task = tasks.register("generate${GUtil.toCamelCase(name)}Theme") {
+ outputFile.set(targetDir.file(this@all.themeClassName.map {
+ val className = ClassName.bestGuess(it)
+ className.packageName.replace(".", "/")
+ .plus("/${className.simpleName}.kt")
+ }))
+ themeClassName.set(this@all.themeClassName)
+ ideaVersion.set(this@all.ideaVersion)
+ themeFile.set(this@all.themeFile)
+ }
+ tasks.withType {
+ dependsOn(task)
+ }
+ tasks.withType {
+ dependsOn(task)
+ }
+ pluginManager.withPlugin("org.jetbrains.kotlin.jvm") {
+ extensions.getByType().apply {
+ sourceSets["main"].kotlin.srcDir(targetDir)
+ }
}
}
}
@@ -60,6 +63,7 @@ abstract class IntelliJThemeGeneratorPlugin : Plugin {
class ThemeGeneratorContainer(container: NamedDomainObjectContainer) : NamedDomainObjectContainer by container
class ThemeGeneration(val name: String, project: Project) {
+
val targetDir: DirectoryProperty = project.objects.directoryProperty()
.convention(project.layout.buildDirectory.dir("generated/theme"))
val ideaVersion = project.objects.property()
@@ -71,7 +75,7 @@ class ThemeGeneration(val name: String, project: Project) {
open class IntelliJThemeGeneratorTask : DefaultTask() {
@get:OutputFile
- val outputFile = project.objects.fileProperty()
+ val outputFile: RegularFileProperty = project.objects.fileProperty()
@get:Input
val ideaVersion = project.objects.property()
@@ -120,4 +124,5 @@ data class IntellijThemeDescriptor(
val colors: Map = emptyMap(),
val ui: Map = emptyMap(),
val icons: Map = emptyMap(),
+ val iconColorsOnSelection: Map = emptyMap(),
)
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/ClassLoaderProvider.kt b/core/src/main/kotlin/org/jetbrains/jewel/ClassLoaderProvider.kt
new file mode 100644
index 000000000..5ad576d3f
--- /dev/null
+++ b/core/src/main/kotlin/org/jetbrains/jewel/ClassLoaderProvider.kt
@@ -0,0 +1,6 @@
+package org.jetbrains.jewel
+
+interface ClassLoaderProvider {
+
+ val classLoaders: List
+}
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJSvgLoader.kt b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJSvgLoader.kt
deleted file mode 100644
index bde9c8e66..000000000
--- a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJSvgLoader.kt
+++ /dev/null
@@ -1,119 +0,0 @@
-package org.jetbrains.jewel
-
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.Immutable
-import androidx.compose.runtime.remember
-import androidx.compose.ui.graphics.painter.Painter
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.res.ResourceLoader
-import androidx.compose.ui.res.loadSvgPainter
-import java.io.InputStream
-import java.util.concurrent.ConcurrentHashMap
-
-@Immutable
-class IntelliJSvgLoader(private val svgPatcher: SvgPatcher) : SvgLoader {
-
- private val cache = ConcurrentHashMap()
-
- @Composable
- override fun loadSvgResource(
- originalPath: String,
- resourceLoader: ResourceLoader,
- pathPatcher: @Composable (String) -> String,
- ): Painter {
- val patchedPath = pathPatcher(originalPath)
- cache[patchedPath]?.let { return it }
-
- val painter = rememberPatchedSvgResource(originalPath, patchedPath, resourceLoader)
- cache[patchedPath] = painter
- return painter
- }
-
- @Composable
- private fun rememberPatchedSvgResource(
- basePath: String,
- resourcePath: String,
- loader: ResourceLoader,
- ): Painter {
- val density = LocalDensity.current
-
- val painter =
- try {
- useResource(resourcePath, loader) {
- loadSvgPainter(it.patchColors(), density)
- }
- } catch (e: IllegalArgumentException) {
- val simplerPath = trySimplifyingPath(resourcePath)
- if (simplerPath != null) {
- System.err.println("Unable to load '$resourcePath' (base: $basePath), trying simpler version: $simplerPath")
- return rememberPatchedSvgResource(basePath, simplerPath, loader)
- } else {
- throw IllegalArgumentException(
- "Unable to load '$resourcePath' (base: $basePath), no simpler version available",
- e,
- )
- }
- }
- return remember(resourcePath, density, loader) { painter }
- }
-
- private fun trySimplifyingPath(originalPath: String): String? {
- // Step 1: attempt to remove extended state qualifiers (pressed, hovered)
- val pressedIndex = originalPath.lastIndexOf("Pressed")
- if (pressedIndex > 0) {
- return originalPath.removeRange(pressedIndex, pressedIndex + "Pressed".length)
- }
-
- val hoveredIndex = originalPath.lastIndexOf("Hovered")
- if (hoveredIndex > 0) {
- return originalPath.removeRange(hoveredIndex, hoveredIndex + "Hovered".length)
- }
-
- // Step 2: attempt to remove state qualifiers (indeterminate, selected, focused, disabled)
- val indeterminateIndex = originalPath.lastIndexOf("Indeterminate")
- if (indeterminateIndex > 0) {
- return originalPath.removeRange(indeterminateIndex, indeterminateIndex + "Indeterminate".length)
- }
-
- val selectedIndex = originalPath.lastIndexOf("Selected")
- if (selectedIndex > 0) {
- return originalPath.removeRange(selectedIndex, selectedIndex + "Selected".length)
- }
-
- val focusedIndex = originalPath.lastIndexOf("Focused")
- if (focusedIndex > 0) {
- return originalPath.removeRange(focusedIndex, focusedIndex + "Focused".length)
- }
-
- val disabledIndex = originalPath.lastIndexOf("Disabled")
- if (disabledIndex > 0) {
- return originalPath.removeRange(disabledIndex, disabledIndex + "Disabled".length)
- }
-
- // Step 3: attempt to remove density and size qualifiers
- val retinaIndex = originalPath.lastIndexOf("@2x")
- if (retinaIndex > 0) {
- return originalPath.removeRange(retinaIndex, retinaIndex + "@2x".length)
- }
-
- // Step 4: attempt to remove dark qualifier
- val darkIndex = originalPath.lastIndexOf("_dark")
- if (darkIndex > 0) {
- return originalPath.removeRange(darkIndex, darkIndex + "_dark".length)
- }
-
- // TODO remove size qualifiers (e.g., "@20x20")
-
- return null
- }
-
- private fun InputStream.patchColors(): InputStream =
- svgPatcher.patchSvg(this).byteInputStream()
-
- // Copied from androidx.compose.ui.res.Resources
- private inline fun useResource(
- resourcePath: String,
- loader: ResourceLoader,
- block: (InputStream) -> T,
- ): T = loader.load(resourcePath).use(block)
-}
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeColorPalette.kt b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeColorPalette.kt
index 7f13cb435..0ab35707d 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeColorPalette.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeColorPalette.kt
@@ -7,11 +7,13 @@ import androidx.compose.ui.graphics.Color
@Stable
interface IntelliJThemeColorPalette {
- fun lookup(colorKey: String): Color?
+ fun lookup(colorKey: String): Color? = rawMap[colorKey]
+
+ val rawMap: Map
}
@Immutable
object EmptyThemeColorPalette : IntelliJThemeColorPalette {
- override fun lookup(colorKey: String): Color? = null
+ override val rawMap: Map = emptyMap()
}
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeIconData.kt b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeIconData.kt
index e70ca30c7..0531c1f61 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeIconData.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeIconData.kt
@@ -1,14 +1,39 @@
package org.jetbrains.jewel
import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Color
@Immutable
interface IntelliJThemeIconData {
val iconOverrides: Map
val colorPalette: Map
+ val selectionColorPalette: Map
+
+ fun selectionColorMapping() =
+ selectionColorPalette.mapNotNull { (key, value) ->
+ val keyColor = key.toColorOrNull() ?: return@mapNotNull null
+ val valueColor = value.toColorOrNull() ?: return@mapNotNull null
+ keyColor to valueColor
+ }.toMap()
}
+internal fun String.toColorOrNull() =
+ lowercase()
+ .removePrefix("#")
+ .removePrefix("0x")
+ .let {
+ when (it.length) {
+ 3 -> "ff${it[0]}${it[0]}${it[1]}${it[1]}${it[2]}${it[2]}"
+ 4 -> "${it[0]}${it[0]}${it[1]}${it[1]}${it[2]}${it[2]}${it[3]}${it[3]}"
+ 6 -> "ff$it"
+ 8 -> it
+ else -> null
+ }
+ }
+ ?.toLongOrNull(radix = 16)
+ ?.let { Color(it) }
+
@Immutable
object EmptyThemeIconData : IntelliJThemeIconData {
@@ -16,5 +41,8 @@ object EmptyThemeIconData : IntelliJThemeIconData {
override val colorPalette: Map = emptyMap()
- override fun toString() = "EmptyThemeIconData(iconOverrides=[], colorPalette=[])"
+ override val selectionColorPalette: Map = emptyMap()
+
+ override fun toString() =
+ "EmptyThemeIconData(iconOverrides=[], colorPalette=[], selectionColorPalette=[])"
}
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/JewelResourceLoader.kt b/core/src/main/kotlin/org/jetbrains/jewel/JewelResourceLoader.kt
index 0fc3782db..d6a3c3fb6 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/JewelResourceLoader.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/JewelResourceLoader.kt
@@ -1,6 +1,78 @@
package org.jetbrains.jewel
-interface JewelResourceLoader {
+import androidx.compose.ui.res.ResourceLoader
+import java.io.InputStream
- val searchClasses: List>
+abstract class JewelResourceLoader : ResourceLoader {
+
+ protected var verbose = true
+
+ protected fun loadResourceOrNull(path: String, classLoaders: List): InputStream? {
+ for (classLoader in classLoaders) {
+ val stream = classLoader.getResourceAsStream(path)
+ if (stream != null) {
+ if (verbose) println("Found resource: '$path'")
+ return stream
+ }
+
+ // Didn't work, let's see if we can simplify the icon to a base state
+ val simplifiedPath = trySimplifyingPath(path)
+ if (simplifiedPath != null) {
+ if (verbose) println("Resource not found: '$path'. Trying simplified path: '$simplifiedPath'")
+ return loadResourceOrNull(simplifiedPath, classLoaders)
+ }
+ }
+
+ return null
+ }
+
+ private fun trySimplifyingPath(originalPath: String): String? {
+ // Step 1: attempt to remove dark qualifier
+ val darkIndex = originalPath.lastIndexOf("_dark")
+ if (darkIndex > 0) {
+ return originalPath.removeRange(darkIndex, darkIndex + "_dark".length)
+ }
+
+ // Step 2: attempt to remove extended state qualifiers (pressed, hovered)
+ val pressedIndex = originalPath.lastIndexOf("Pressed")
+ if (pressedIndex > 0) {
+ return originalPath.removeRange(pressedIndex, pressedIndex + "Pressed".length)
+ }
+
+ val hoveredIndex = originalPath.lastIndexOf("Hovered")
+ if (hoveredIndex > 0) {
+ return originalPath.removeRange(hoveredIndex, hoveredIndex + "Hovered".length)
+ }
+
+ // Step 3: attempt to remove state qualifiers (indeterminate, selected, focused, disabled)
+ val indeterminateIndex = originalPath.lastIndexOf("Indeterminate")
+ if (indeterminateIndex > 0) {
+ return originalPath.removeRange(indeterminateIndex, indeterminateIndex + "Indeterminate".length)
+ }
+
+ val selectedIndex = originalPath.lastIndexOf("Selected")
+ if (selectedIndex > 0) {
+ return originalPath.removeRange(selectedIndex, selectedIndex + "Selected".length)
+ }
+
+ val focusedIndex = originalPath.lastIndexOf("Focused")
+ if (focusedIndex > 0) {
+ return originalPath.removeRange(focusedIndex, focusedIndex + "Focused".length)
+ }
+
+ val disabledIndex = originalPath.lastIndexOf("Disabled")
+ if (disabledIndex > 0) {
+ return originalPath.removeRange(disabledIndex, disabledIndex + "Disabled".length)
+ }
+
+ // Step 4: attempt to remove density and size qualifiers
+ val retinaIndex = originalPath.lastIndexOf("@2x")
+ if (retinaIndex > 0) {
+ return originalPath.removeRange(retinaIndex, retinaIndex + "@2x".length)
+ }
+
+ // TODO remove size qualifiers (e.g., "@20x20")
+
+ return null
+ }
}
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/JewelSvgLoader.kt b/core/src/main/kotlin/org/jetbrains/jewel/JewelSvgLoader.kt
new file mode 100644
index 000000000..e93f4749c
--- /dev/null
+++ b/core/src/main/kotlin/org/jetbrains/jewel/JewelSvgLoader.kt
@@ -0,0 +1,54 @@
+package org.jetbrains.jewel
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Immutable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.graphics.painter.Painter
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.ResourceLoader
+import androidx.compose.ui.res.loadSvgPainter
+import java.io.InputStream
+import java.util.concurrent.ConcurrentHashMap
+
+@Immutable
+class JewelSvgLoader(private val svgPatcher: SvgPatcher) : SvgLoader {
+
+ private val cache = ConcurrentHashMap()
+
+ @Composable
+ override fun loadSvgResource(
+ svgPath: String,
+ resourceLoader: ResourceLoader,
+ pathPatcher: @Composable (String) -> String,
+ ): Painter {
+ val patchedPath = pathPatcher(svgPath)
+ cache[patchedPath]?.let { return it }
+
+ val painter = rememberPatchedSvgResource(patchedPath, resourceLoader)
+ cache[patchedPath] = painter
+ return painter
+ }
+
+ @Composable
+ private fun rememberPatchedSvgResource(
+ resourcePath: String,
+ loader: ResourceLoader,
+ ): Painter {
+ val density = LocalDensity.current
+
+ val painter = useResource(resourcePath, loader) {
+ loadSvgPainter(it.patchColors(resourcePath), density)
+ }
+ return remember(resourcePath, density, loader) { painter }
+ }
+
+ private fun InputStream.patchColors(resourcePath: String): InputStream =
+ svgPatcher.patchSvg(this, resourcePath).byteInputStream()
+
+ // Copied from androidx.compose.ui.res.Resources
+ private inline fun useResource(
+ resourcePath: String,
+ loader: ResourceLoader,
+ block: (InputStream) -> T,
+ ): T = loader.load(resourcePath).use(block)
+}
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/LazyTree.kt b/core/src/main/kotlin/org/jetbrains/jewel/LazyTree.kt
index bf7d48a4e..3dce4abe2 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/LazyTree.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/LazyTree.kt
@@ -50,8 +50,8 @@ fun LazyTree(
onSelectionChange = onSelectionChange,
keyActions = keyActions,
chevronContent = { elementState ->
- val painterProvider = style.icons.nodeChevron(elementState.isExpanded)
- val painter by painterProvider.getPainter(resourceLoader, elementState)
+ val painterProvider = style.icons.chevron(elementState.isExpanded, elementState.isSelected)
+ val painter by painterProvider.getPainter(resourceLoader)
Icon(painter = painter, contentDescription = null)
},
) {
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/PaletteMapper.kt b/core/src/main/kotlin/org/jetbrains/jewel/PaletteMapper.kt
index 33373abbf..5ebe6666a 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/PaletteMapper.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/PaletteMapper.kt
@@ -3,28 +3,48 @@ package org.jetbrains.jewel
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
+// Replicates com.intellij.ide.ui.UITheme.PaletteScopeManager's functionality
+// (note that in Swing, there is also a RadioButtons scope, but while it gets
+// written to, it never gets accessed by the actual color patching, so we ignore
+// writing any Radio button-related entries and read the CheckBox values for them)
@Immutable
-class PaletteMapper(val colorOverrides: Map) {
-
- fun mapColor(originalColor: Color): Color =
- mapColorOrNull(originalColor) ?: originalColor
+class PaletteMapper(
+ private val ui: Scope,
+ private val checkBoxes: Scope,
+ private val trees: Scope,
+) {
+
+ fun getScopeForPath(path: String?): Scope? {
+ if (path == null) return ui
+ if (!path.contains("com/intellij/ide/ui/laf/icons/")) return ui
+
+ val file = path.substringAfterLast('/')
+ return when {
+ file == "treeCollapsed.svg" || file == "treeExpanded.svg" -> trees
+ // ⚠️ This next line is not a copy-paste error — the code in UITheme.PaletteScopeManager.getScopeByPath()
+ // says they share the same colors
+ file.startsWith("check") || file.startsWith("radio") -> checkBoxes
+ else -> null
+ }
+ }
- fun mapColorOrNull(originalColor: Color): Color? {
- if (colorOverrides.isEmpty()) return null
+ companion object {
- return colorOverrides[originalColor]
+ val Empty = PaletteMapper(Scope.Empty, Scope.Empty, Scope.Empty)
}
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
+ @Immutable
+ @JvmInline
+ value class Scope(val colorOverrides: Map) {
- other as PaletteMapper
+ fun mapColorOrNull(originalColor: Color): Color? =
+ colorOverrides[originalColor]
- return colorOverrides == other.colorOverrides
- }
+ override fun toString(): String = "PaletteMapper.Scope(colorOverrides=$colorOverrides)"
- override fun hashCode(): Int = colorOverrides.hashCode()
+ companion object {
- override fun toString() = "PaletteMapper(colorOverrides=$colorOverrides)"
+ val Empty = Scope(emptyMap())
+ }
+ }
}
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/SimpleResourceLoader.kt b/core/src/main/kotlin/org/jetbrains/jewel/SimpleResourceLoader.kt
new file mode 100644
index 000000000..c245a0f28
--- /dev/null
+++ b/core/src/main/kotlin/org/jetbrains/jewel/SimpleResourceLoader.kt
@@ -0,0 +1,20 @@
+package org.jetbrains.jewel
+
+import java.io.InputStream
+
+class SimpleResourceLoader(private val classLoader: ClassLoader) : JewelResourceLoader(), ClassLoaderProvider {
+
+ override val classLoaders
+ get() = listOf(javaClass.classLoader, classLoader)
+
+ override fun load(resourcePath: String): InputStream {
+ val path = resourcePath.removePrefix("/")
+ val parentClassLoader =
+ StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
+ .callerClass
+ .classLoader
+ val resource = loadResourceOrNull(path, classLoaders + parentClassLoader)
+
+ return requireNotNull(resource) { "Resource '$resourcePath' not found (tried loading: '$path')" }
+ }
+}
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/SvgLoader.kt b/core/src/main/kotlin/org/jetbrains/jewel/SvgLoader.kt
index 3081f3b13..1653c8668 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/SvgLoader.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/SvgLoader.kt
@@ -8,7 +8,7 @@ interface SvgLoader {
@Composable
fun loadSvgResource(
- originalPath: String,
+ svgPath: String,
resourceLoader: ResourceLoader,
pathPatcher: @Composable (String) -> String,
): Painter
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/SvgPatcher.kt b/core/src/main/kotlin/org/jetbrains/jewel/SvgPatcher.kt
index 00910c0b4..2cc427fc5 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/SvgPatcher.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/SvgPatcher.kt
@@ -4,5 +4,5 @@ import java.io.InputStream
interface SvgPatcher {
- fun patchSvg(rawSvg: InputStream): String
+ fun patchSvg(rawSvg: InputStream, path: String?): String
}
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/TextArea.kt b/core/src/main/kotlin/org/jetbrains/jewel/TextArea.kt
index 01d5934f2..1dbc198e1 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/TextArea.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/TextArea.kt
@@ -124,7 +124,7 @@ fun TextArea(
style = style,
textStyle = textStyle,
interactionSource = interactionSource,
- ) { innerTextField, state ->
+ ) { innerTextField, _ ->
TextAreaDecorationBox(
innerTextField = innerTextField,
contentPadding = style.metrics.contentPadding,
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/Keybindings.kt b/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/Keybindings.kt
index 9b5f343e7..ea382811d 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/Keybindings.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/Keybindings.kt
@@ -100,7 +100,6 @@ open class DefaultSelectableColumnKeybindings : SelectableColumnKeybindings {
override val PointerKeyboardModifiers.isKeyboardMultiSelectionKeyPressed: Boolean
get() = isShiftPressed
-
override fun KeyEvent.selectFirstItem() =
key == Key.Home && !isKeyboardMultiSelectionKeyPressed
@@ -141,5 +140,4 @@ open class DefaultSelectableColumnKeybindings : SelectableColumnKeybindings {
override fun KeyEvent.selectAll(): Boolean? =
key == Key.A && isKeyboardCtrlMetaKeyPressed
-
}
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyColumn.kt b/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyColumn.kt
index 81423d960..1bc0aba2c 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyColumn.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyColumn.kt
@@ -12,10 +12,11 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
-import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.input.key.onPreviewKeyEvent
import androidx.compose.ui.input.pointer.PointerEventType
@@ -28,8 +29,8 @@ import org.jetbrains.jewel.foundation.tree.KeyBindingActions
import org.jetbrains.jewel.foundation.tree.PointerEventActions
/**
- * A composable that displays a scrollable and selectable list of items in a column arrangement.
- *
+ * A composable that displays a scrollable and selectable list of items in
+ * a column arrangement.
*/
@Composable
fun SelectableLazyColumn(
@@ -47,8 +48,6 @@ fun SelectableLazyColumn(
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
content: SelectableLazyListScope.() -> Unit,
) {
- val scope = rememberCoroutineScope()
-
val container = remember(content) {
SelectableLazyListScopeContainer().apply(content)
}
@@ -56,21 +55,26 @@ fun SelectableLazyColumn(
val keys = remember(container) {
container.getKeys()
}
+ var isFocused by remember { mutableStateOf(false) }
- var isActive by remember { mutableStateOf(false) }
+ fun evaluateIndexes(): List {
+ val keyToIndexMap = keys.withIndex().associateBy({ it.value.key }, { it.index })
+ return state.selectedKeys
+ .mapNotNull { selected -> keyToIndexMap[selected] }
+ .sorted()
+ }
remember(state.selectedKeys) {
- onSelectedIndexesChanged(state.selectedKeys.map { selected -> keys.indexOfFirst { it.key == selected } })
+ onSelectedIndexesChanged(evaluateIndexes())
}
-
+ val focusRequester = remember { FocusRequester() }
LazyColumn(
modifier = modifier
+ .onFocusChanged { isFocused = it.hasFocus }
+ .focusRequester(focusRequester)
.focusable(interactionSource = interactionSource)
- .onFocusChanged {
- isActive = it.hasFocus
- }
.onPreviewKeyEvent { event ->
- state.lastActiveItemIndex?.let { _ ->
+ if (state.lastActiveItemIndex != null) {
keyActions.handleOnKeyEvent(event, keys, state, selectionMode).invoke(event)
}
true
@@ -87,11 +91,12 @@ fun SelectableLazyColumn(
is Entry.Item -> item(entry.key, entry.contentType) {
val itemScope = SelectableLazyItemScope(
isSelected = entry.key in state.selectedKeys,
- isActive = isActive,
+ isActive = isFocused,
)
if (keys.any { it.key == entry.key && it is SelectableLazyListKey.Selectable }) {
Box(
modifier = Modifier.selectable(
+ requester = focusRequester,
keybindings = keyActions.keybindings,
actionHandler = pointerEventActions,
selectionMode = selectionMode,
@@ -112,10 +117,11 @@ fun SelectableLazyColumn(
key = { entry.key(entry.startIndex + it) },
contentType = { entry.contentType(it) },
) { index ->
- val itemScope = SelectableLazyItemScope(entry.key(index) in state.selectedKeys, isActive)
+ val itemScope = SelectableLazyItemScope(entry.key(index) in state.selectedKeys, isFocused)
if (keys.any { it.key == entry.key(index) && it is SelectableLazyListKey.Selectable }) {
Box(
modifier = Modifier.selectable(
+ requester = focusRequester,
keybindings = keyActions.keybindings,
actionHandler = pointerEventActions,
selectionMode = selectionMode,
@@ -132,7 +138,7 @@ fun SelectableLazyColumn(
}
is Entry.StickyHeader -> stickyHeader(entry.key, entry.contentType) {
- val itemScope = SelectableLazyItemScope(entry.key in state.selectedKeys, isActive)
+ val itemScope = SelectableLazyItemScope(entry.key in state.selectedKeys, isFocused)
if (keys.any { it.key == entry.key && it is SelectableLazyListKey.Selectable }) {
Box(
modifier = Modifier.selectable(
@@ -147,7 +153,7 @@ fun SelectableLazyColumn(
entry.content.invoke(itemScope)
}
} else {
- SelectableLazyItemScope(entry.key in state.selectedKeys, isActive).apply {
+ SelectableLazyItemScope(entry.key in state.selectedKeys, isFocused).apply {
entry.content.invoke(itemScope)
}
}
@@ -158,6 +164,7 @@ fun SelectableLazyColumn(
}
private fun Modifier.selectable(
+ requester: FocusRequester? = null,
keybindings: SelectableColumnKeybindings,
actionHandler: PointerEventActions,
selectionMode: SelectionMode,
@@ -169,14 +176,17 @@ private fun Modifier.selectable(
while (true) {
val event = awaitPointerEvent()
when (event.type) {
- PointerEventType.Press -> actionHandler.handlePointerEventPress(
- event,
- keybindings,
- selectableState,
- selectionMode,
- allKeys,
- itemKey,
- )
+ PointerEventType.Press -> {
+ requester?.requestFocus()
+ actionHandler.handlePointerEventPress(
+ pointerEvent = event,
+ keyBindings = keybindings,
+ selectableLazyListState = selectableState,
+ selectionMode = selectionMode,
+ allKeys = allKeys,
+ key = itemKey,
+ )
+ }
}
}
}
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyListScope.kt b/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyListScope.kt
index f31d028c1..dab700a25 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyListScope.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/foundation/lazy/SelectableLazyListScope.kt
@@ -6,9 +6,7 @@ import androidx.compose.runtime.Composable
import org.jetbrains.jewel.foundation.lazy.SelectableLazyListKey.NotSelectable
import org.jetbrains.jewel.foundation.lazy.SelectableLazyListKey.Selectable
-/**
- * Interface defining the scope for building a selectable lazy list.
- */
+/** Interface defining the scope for building a selectable lazy list. */
interface SelectableLazyListScope {
/**
@@ -16,7 +14,8 @@ interface SelectableLazyListScope {
*
* @param key The unique identifier for the item.
* @param contentType The type of content displayed in the item.
- * @param selectable Determines if the item is selectable. Default is `true`.
+ * @param selectable Determines if the item is selectable. Default is
+ * `true`.
* @param content The content of the item as a composable function.
*/
fun item(
@@ -30,10 +29,14 @@ interface SelectableLazyListScope {
* Represents a list of items based on the provided parameters.
*
* @param count The number of items in the list.
- * @param key A function that generates a unique key for each item based on its index.
- * @param contentType A function that returns the content type of an item based on its index. Defaults to `null`.
- * @param selectable A function that determines if an item is selectable based on its index. Defaults to `true`.
- * @param itemContent The content of each individual item, specified as a composable function that takes the item's index as a parameter.
+ * @param key A function that generates a unique key for each item based on
+ * its index.
+ * @param contentType A function that returns the content type of an item
+ * based on its index. Defaults to `null`.
+ * @param selectable A function that determines if an item is selectable
+ * based on its index. Defaults to `true`.
+ * @param itemContent The content of each individual item, specified as a
+ * composable function that takes the item's index as a parameter.
*/
fun items(
count: Int,
@@ -49,7 +52,8 @@ interface SelectableLazyListScope {
* @param key The unique identifier for the sticky header.
* @param contentType The type of content in the sticky header.
* @param selectable Specifies whether the sticky header is selectable.
- * @param content The content to be displayed in the sticky header, provided as a composable function
+ * @param content The content to be displayed in the sticky header,
+ * provided as a composable function
*/
fun stickyHeader(
key: Any,
@@ -142,13 +146,15 @@ fun SelectableLazyListScope.items(
contentType: (item: T) -> Any? = { it },
selectable: (item: T) -> Boolean = { true },
itemContent: @Composable SelectableLazyItemScope.(item: T) -> Unit,
-) = items(
- count = items.size,
- key = { key(items[it]) },
- contentType = { contentType(items[it]) },
- selectable = { selectable(items[it]) },
- itemContent = { itemContent(items[it]) }
-)
+) {
+ items(
+ count = items.size,
+ key = { key(items[it]) },
+ contentType = { contentType(items[it]) },
+ selectable = { selectable(items[it]) },
+ itemContent = { itemContent(items[it]) },
+ )
+}
fun SelectableLazyListScope.itemsIndexed(
items: List,
@@ -156,13 +162,15 @@ fun SelectableLazyListScope.itemsIndexed(
contentType: (index: Int, item: T) -> Any? = { _, item -> item },
selectable: (index: Int, item: T) -> Boolean = { _, _ -> true },
itemContent: @Composable SelectableLazyItemScope.(index: Int, item: T) -> Unit,
-) = items(
- count = items.size,
- key = { key(it, items[it]) },
- contentType = { contentType(it, items[it]) },
- selectable = { selectable(it, items[it]) },
- itemContent = { itemContent(it, items[it]) }
-)
+) {
+ items(
+ count = items.size,
+ key = { key(it, items[it]) },
+ contentType = { contentType(it, items[it]) },
+ selectable = { selectable(it, items[it]) },
+ itemContent = { itemContent(it, items[it]) },
+ )
+}
@Composable
fun LazyItemScope.SelectableLazyItemScope(
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/foundation/tree/BasicLazyTree.kt b/core/src/main/kotlin/org/jetbrains/jewel/foundation/tree/BasicLazyTree.kt
index 2d2f87a24..148972856 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/foundation/tree/BasicLazyTree.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/foundation/tree/BasicLazyTree.kt
@@ -106,7 +106,7 @@ fun BasicLazyTree(
.asSequence()
.filter { it.id in treeState.delegate.selectedKeys }
.map { element -> element as Tree.Element }
- .toList()
+ .toList(),
)
}
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/styling/LazyTreeStyling.kt b/core/src/main/kotlin/org/jetbrains/jewel/styling/LazyTreeStyling.kt
index 485af660c..4055bfa92 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/styling/LazyTreeStyling.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/styling/LazyTreeStyling.kt
@@ -56,12 +56,19 @@ interface LazyTreeMetrics {
@Immutable
interface LazyTreeIcons {
- val nodeChevronCollapsed: PainterProvider
- val nodeChevronExpanded: PainterProvider
+ val chevronCollapsed: PainterProvider
+ val chevronExpanded: PainterProvider
+ val chevronSelectedCollapsed: PainterProvider
+ val chevronSelectedExpanded: PainterProvider
@Composable
- fun nodeChevron(isExpanded: Boolean) =
- if (isExpanded) nodeChevronExpanded else nodeChevronCollapsed
+ fun chevron(isExpanded: Boolean, isSelected: Boolean) =
+ when {
+ isSelected && isExpanded -> chevronSelectedExpanded
+ isSelected && !isExpanded -> chevronSelectedCollapsed
+ !isSelected && isExpanded -> chevronExpanded
+ else -> chevronCollapsed
+ }
}
val LocalLazyTreeStyle = staticCompositionLocalOf {
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/styling/PainterProvider.kt b/core/src/main/kotlin/org/jetbrains/jewel/styling/PainterProvider.kt
index 3047d789e..ff0779d9d 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/styling/PainterProvider.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/styling/PainterProvider.kt
@@ -9,6 +9,29 @@ import androidx.compose.ui.res.ResourceLoader
@Immutable
interface PainterProvider {
+ /**
+ * Obtain a painter, with no extra data. It is equivalent to calling
+ * [getPainter] with a `null` value for the `extraData` argument. This
+ * overload should only be used for stateless painter providers (i.e., when
+ * [T] is [Unit]).
+ *
+ * A [resourceLoader] that allows loading the corresponding resource must
+ * be loading. For example, if your resource is in module `my-module`'s
+ * resources, the [resourceLoader] must be pointing to `my-module`s
+ * classloader.
+ *
+ * Passing the wrong [ResourceLoader] will cause your resources not to
+ * load, and you will get cryptic errors. Please also note that using
+ * [ResourceLoader.Default] will probably cause loading to fail if you are
+ * trying to load the icons from a different module. For example, if Jewel
+ * is running in the IDE and you use [ResourceLoader.Default] to try and
+ * load a default IDE resource, it will fail.
+ *
+ * @see getPainter
+ */
+ @Composable
+ fun getPainter(resourceLoader: ResourceLoader): State = getPainter(resourceLoader, null)
+
/**
* Obtain a painter for the provided [extraData].
*
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/styling/ResourcePainterProvider.kt b/core/src/main/kotlin/org/jetbrains/jewel/styling/ResourcePainterProvider.kt
index 276fb7a43..60c8e720c 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/styling/ResourcePainterProvider.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/styling/ResourcePainterProvider.kt
@@ -62,7 +62,8 @@ open class ResourcePainterProvider @InternalJewelApi constructor(
override fun toString(): String =
"ResourcePainterProvider(basePath='$basePath', svgLoader=$svgLoader, pathPatcher=$pathPatcher)"
- companion object {
+ @OptIn(InternalJewelApi::class) // These are the public constructors
+ companion object Factory {
fun stateless(basePath: String, svgLoader: SvgLoader) =
ResourcePainterProvider(basePath, svgLoader, SimpleResourcePathPatcher())
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/themes/PaletteMapperFactory.kt b/core/src/main/kotlin/org/jetbrains/jewel/themes/PaletteMapperFactory.kt
index d22635b32..643477153 100644
--- a/core/src/main/kotlin/org/jetbrains/jewel/themes/PaletteMapperFactory.kt
+++ b/core/src/main/kotlin/org/jetbrains/jewel/themes/PaletteMapperFactory.kt
@@ -1,97 +1,64 @@
package org.jetbrains.jewel.themes
import androidx.compose.ui.graphics.Color
-import org.jetbrains.jewel.IntelliJThemeColorPalette
-import org.jetbrains.jewel.IntelliJThemeIconData
import org.jetbrains.jewel.PaletteMapper
-object PaletteMapperFactory {
+abstract class PaletteMapperFactory {
- // Extracted from com.intellij.ide.ui.UITheme#colorPalette
- private val colorsToMap = mapOf(
- "Actions.Red" to "#DB5860",
- "Actions.Red.Dark" to "#C75450",
- "Actions.Yellow" to "#EDA200",
- "Actions.Yellow.Dark" to "#F0A732",
- "Actions.Green" to "#59A869",
- "Actions.Green.Dark" to "#499C54",
- "Actions.Blue" to "#389FD6",
- "Actions.Blue.Dark" to "#3592C4",
- "Actions.Grey" to "#6E6E6E",
- "Actions.Grey.Dark" to "#AFB1B3",
- "Actions.GreyInline" to "#7F8B91",
- "Actions.GreyInline.Dark" to "#7F8B91",
- "Objects.Grey" to "#9AA7B0",
- "Objects.Blue" to "#40B6E0",
- "Objects.Green" to "#62B543",
- "Objects.Yellow" to "#F4AF3D",
- "Objects.YellowDark" to "#D9A343",
- "Objects.Purple" to "#B99BF8",
- "Objects.Pink" to "#F98B9E",
- "Objects.Red" to "#F26522",
- "Objects.RedStatus" to "#E05555",
- "Objects.GreenAndroid" to "#3DDC84",
- "Objects.BlackText" to "#231F20",
- "Checkbox.Background.Default" to "#FFFFFF",
- "Checkbox.Background.Default.Dark" to "#43494A",
- "Checkbox.Background.Disabled" to "#F2F2F2",
- "Checkbox.Background.Disabled.Dark" to "#3C3F41",
- "Checkbox.Border.Default" to "#b0b0b0",
- "Checkbox.Border.Default.Dark" to "#6B6B6B",
- "Checkbox.Border.Disabled" to "#BDBDBD",
- "Checkbox.Border.Disabled.Dark" to "#545556",
- "Checkbox.Focus.Thin.Default" to "#7B9FC7",
- "Checkbox.Focus.Thin.Default.Dark" to "#466D94",
- "Checkbox.Focus.Wide" to "#97C3F3",
- "Checkbox.Focus.Wide.Dark" to "#3D6185",
- "Checkbox.Foreground.Disabled" to "#ABABAB",
- "Checkbox.Foreground.Disabled.Dark" to "#606060",
- "Checkbox.Background.Selected" to "#4F9EE3",
- "Checkbox.Background.Selected.Dark" to "#43494A",
- "Checkbox.Border.Selected" to "#4B97D9",
- "Checkbox.Border.Selected.Dark" to "#6B6B6B",
- "Checkbox.Foreground.Selected" to "#FEFEFE",
- "Checkbox.Foreground.Selected.Dark" to "#A7A7A7",
- "Checkbox.Focus.Thin.Selected" to "#ACCFF7",
- "Checkbox.Focus.Thin.Selected.Dark" to "#466D94",
- "Tree.iconColor" to "#808080",
- "Tree.iconColor.Dark" to "#AFB1B3",
- )
-
- fun create(
+ protected fun createInternal(
+ iconColorPalette: Map,
+ keyPalette: Map,
+ themeColors: Map,
isDark: Boolean,
- iconData: IntelliJThemeIconData,
- colorPalette: IntelliJThemeColorPalette,
): PaletteMapper {
- val overrides = computeOverrides(isDark, iconData.colorPalette, colorPalette)
- return PaletteMapper(overrides)
+ // This partially emulates what com.intellij.ide.ui.UITheme.loadFromJson does
+ val ui = mutableMapOf()
+ val checkBoxes = mutableMapOf()
+ val trees = mutableMapOf()
+
+ for ((key, value) in iconColorPalette) {
+ val map = selectMap(key, checkBoxes, trees, ui) ?: continue
+
+ // If the value is one of the named colors in the theme, use that named color's value
+ val namedColor = themeColors.get(value) as? String
+ val resolvedValue = namedColor ?: value
+
+ // If either the key or the resolved value aren't valid colors, ignore the entry
+ val keyAsColor = resolveKeyColor(key, keyPalette, isDark) ?: continue
+ val resolvedValueAsColor = resolvedValue.toColorOrNull() ?: continue
+
+ // Save the new entry (oldColor -> newColor) in the map
+ map[keyAsColor] = resolvedValueAsColor
+ }
+
+ return PaletteMapper(
+ ui = PaletteMapper.Scope(ui),
+ checkBoxes = PaletteMapper.Scope(checkBoxes),
+ trees = PaletteMapper.Scope(trees),
+ )
}
- // 1. Load the icons.ColorPalette map from the theme JSON, if it exists, and for each key:
- // 2. Resolve the hex value by looking up the ColorPalette key
- // * Append ".Dark" to the key if in dark theme
- // * Read the corresponding value from the UITheme#colorPalette map (the "oldColor" hex value)
- // 3. Read the "newColor" hex value corresponding to the key from the theme's icons.ColorPalette map
- // 4. Resolve (if needed) the newColor
- // * The "newColor" in the theme may be a theme color palette key, that needs to be resolved to a hex value
- // * Note down the alpha value, if any is specified (that is, if the value is in the #AARRGGBB format)
- // 6. Write a new entry oldColor -> newColor, bringing over the original alpha into the newColor (if any)
- private fun computeOverrides(
- isDark: Boolean,
- iconColorMap: Map,
- colorPalette: IntelliJThemeColorPalette,
- ) =
- buildMap {
- for (colorOverride in iconColorMap) {
- val key = colorHexForKey(colorOverride.key, isDark)
- val overrideValue = colorOverride.value
- val newColor = overrideValue.takeIf { it.startsWith("#") }?.toColorOrNull()
- ?: colorPalette.lookup(overrideValue)
- ?: continue
- val oldColor = key.toColorOrNull() ?: continue
- put(oldColor, newColor)
- }
+ // See com.intellij.ide.ui.UITheme.toColorString
+ private fun resolveKeyColor(key: String, keyPalette: Map, isDark: Boolean): Color? {
+ val darkKey = "$key.Dark"
+ val resolvedKey = if (isDark && keyPalette.containsKey(darkKey)) darkKey else key
+ return keyPalette[resolvedKey]?.toColorOrNull()
+ }
+
+ private fun selectMap(
+ key: String,
+ checkBoxes: MutableMap,
+ trees: MutableMap,
+ ui: MutableMap,
+ ) = when {
+ key.startsWith("Checkbox.") -> checkBoxes
+ key.startsWith("Tree.iconColor.") -> trees
+ key.startsWith("Objects.") || key.startsWith("Actions.") || key.startsWith("#") -> ui
+ else -> {
+ logInfo("No PaletteMapperScope defined for key '$key'")
+ null
}
+ }
private fun String.toColorOrNull() =
lowercase()
@@ -109,12 +76,5 @@ object PaletteMapperFactory {
?.toLongOrNull(radix = 16)
?.let { Color(it) }
- private fun colorHexForKey(key: String, isDark: Boolean): String {
- val resolvedColor = if (isDark) {
- colorsToMap["$key.Dark"] ?: colorsToMap[key]
- } else {
- colorsToMap[key]
- }
- return (resolvedColor ?: key).lowercase()
- }
+ abstract fun logInfo(message: String)
}
diff --git a/core/src/main/kotlin/org/jetbrains/jewel/themes/StandalonePaletteMapperFactory.kt b/core/src/main/kotlin/org/jetbrains/jewel/themes/StandalonePaletteMapperFactory.kt
new file mode 100644
index 000000000..bd49dc09d
--- /dev/null
+++ b/core/src/main/kotlin/org/jetbrains/jewel/themes/StandalonePaletteMapperFactory.kt
@@ -0,0 +1,82 @@
+package org.jetbrains.jewel.themes
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import org.jetbrains.jewel.IntelliJThemeColorPalette
+import org.jetbrains.jewel.IntelliJThemeIconData
+import org.jetbrains.jewel.PaletteMapper
+
+object StandalonePaletteMapperFactory : PaletteMapperFactory() {
+
+ // Extracted from com.intellij.ide.ui.UITheme#colorPalette
+ private val colorsToMap = mapOf(
+ "Actions.Red" to "#DB5860",
+ "Actions.Red.Dark" to "#C75450",
+ "Actions.Yellow" to "#EDA200",
+ "Actions.Yellow.Dark" to "#F0A732",
+ "Actions.Green" to "#59A869",
+ "Actions.Green.Dark" to "#499C54",
+ "Actions.Blue" to "#389FD6",
+ "Actions.Blue.Dark" to "#3592C4",
+ "Actions.Grey" to "#6E6E6E",
+ "Actions.Grey.Dark" to "#AFB1B3",
+ "Actions.GreyInline" to "#7F8B91",
+ "Actions.GreyInline.Dark" to "#7F8B91",
+ "Objects.Grey" to "#9AA7B0",
+ "Objects.Blue" to "#40B6E0",
+ "Objects.Green" to "#62B543",
+ "Objects.Yellow" to "#F4AF3D",
+ "Objects.YellowDark" to "#D9A343",
+ "Objects.Purple" to "#B99BF8",
+ "Objects.Pink" to "#F98B9E",
+ "Objects.Red" to "#F26522",
+ "Objects.RedStatus" to "#E05555",
+ "Objects.GreenAndroid" to "#3DDC84",
+ "Objects.BlackText" to "#231F20",
+ "Checkbox.Background.Default" to "#FFFFFF",
+ "Checkbox.Background.Default.Dark" to "#43494A",
+ "Checkbox.Background.Disabled" to "#F2F2F2",
+ "Checkbox.Background.Disabled.Dark" to "#3C3F41",
+ "Checkbox.Border.Default" to "#b0b0b0",
+ "Checkbox.Border.Default.Dark" to "#6B6B6B",
+ "Checkbox.Border.Disabled" to "#BDBDBD",
+ "Checkbox.Border.Disabled.Dark" to "#545556",
+ "Checkbox.Focus.Thin.Default" to "#7B9FC7",
+ "Checkbox.Focus.Thin.Default.Dark" to "#466D94",
+ "Checkbox.Focus.Wide" to "#97C3F3",
+ "Checkbox.Focus.Wide.Dark" to "#3D6185",
+ "Checkbox.Foreground.Disabled" to "#ABABAB",
+ "Checkbox.Foreground.Disabled.Dark" to "#606060",
+ "Checkbox.Background.Selected" to "#4F9EE3",
+ "Checkbox.Background.Selected.Dark" to "#43494A",
+ "Checkbox.Border.Selected" to "#4B97D9",
+ "Checkbox.Border.Selected.Dark" to "#6B6B6B",
+ "Checkbox.Foreground.Selected" to "#FEFEFE",
+ "Checkbox.Foreground.Selected.Dark" to "#A7A7A7",
+ "Checkbox.Focus.Thin.Selected" to "#ACCFF7",
+ "Checkbox.Focus.Thin.Selected.Dark" to "#466D94",
+ "Tree.iconColor" to "#808080",
+ "Tree.iconColor.Dark" to "#AFB1B3",
+ )
+
+ fun create(
+ isDark: Boolean,
+ iconData: IntelliJThemeIconData,
+ colorPalette: IntelliJThemeColorPalette,
+ ): PaletteMapper =
+ createInternal(
+ iconColorPalette = iconData.colorPalette,
+ keyPalette = colorsToMap,
+ themeColors = colorPalette.rawMap.asColorStringsMap(),
+ isDark = isDark,
+ )
+
+ private fun Map.asColorStringsMap() =
+ mapValues { (_, color) ->
+ "#${color.toArgb().toString(16).padStart(6, '0')}"
+ }
+
+ override fun logInfo(message: String) {
+ println("[${javaClass.simpleName}] $message")
+ }
+}
diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconData.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconData.kt
index 120646adf..3c69af4a1 100644
--- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconData.kt
+++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconData.kt
@@ -1,12 +1,14 @@
package org.jetbrains.jewel.bridge
import androidx.compose.runtime.Immutable
+import com.intellij.ide.ui.UITheme
import org.jetbrains.jewel.IntelliJThemeIconData
@Immutable
internal class BridgeIconData(
override val iconOverrides: Map,
override val colorPalette: Map,
+ override val selectionColorPalette: Map,
) : IntelliJThemeIconData {
override fun equals(other: Any?): Boolean {
@@ -17,6 +19,7 @@ internal class BridgeIconData(
if (iconOverrides != other.iconOverrides) return false
if (colorPalette != other.colorPalette) return false
+ if (selectionColorPalette != other.selectionColorPalette) return false
return true
}
@@ -24,15 +27,23 @@ internal class BridgeIconData(
override fun hashCode(): Int {
var result = iconOverrides.hashCode()
result = 31 * result + colorPalette.hashCode()
+ result = 31 * result + selectionColorPalette.hashCode()
return result
}
override fun toString(): String =
- "BridgeIconData(iconOverrides=$iconOverrides, colorPalette=$colorPalette)"
+ "BridgeIconData(iconOverrides=$iconOverrides, colorPalette=$colorPalette, " +
+ "selectionColorPalette=$selectionColorPalette)"
companion object {
- // TODO retrieve icon data from Swing
- fun readFromLaF() = BridgeIconData(emptyMap(), emptyMap())
+ fun readFromLaF(): BridgeIconData {
+ val uiTheme = currentUiThemeOrNull()
+ val iconMap = uiTheme?.icons.orEmpty()
+ val selectedIconColorPalette = uiTheme?.selectedIconColorPalette.orEmpty()
+
+ val colorPalette = UITheme.getColorPalette()
+ return BridgeIconData(iconMap, colorPalette, selectedIconColorPalette)
+ }
}
}
diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconMapper.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconMapper.kt
new file mode 100644
index 000000000..0c3c245a5
--- /dev/null
+++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeIconMapper.kt
@@ -0,0 +1,60 @@
+package org.jetbrains.jewel.bridge
+
+import androidx.compose.ui.res.ResourceLoader
+import com.intellij.openapi.diagnostic.thisLogger
+import com.intellij.util.ui.DirProvider
+import org.jetbrains.jewel.ClassLoaderProvider
+import org.jetbrains.jewel.IntelliJThemeIconData
+
+internal object BridgeIconMapper : IconMapper {
+
+ private val logger = thisLogger()
+
+ private val dirProvider = DirProvider()
+
+ override fun mapPath(
+ originalPath: String,
+ iconData: IntelliJThemeIconData,
+ resourceLoader: ResourceLoader,
+ ): String {
+ val classLoaders = (resourceLoader as? ClassLoaderProvider)?.classLoaders
+ if (classLoaders == null) {
+ logger.warn(
+ "Tried loading a resource but the provided ResourceLoader is now a JewelResourceLoader; " +
+ "this is probably a bug. Make sure you always use JewelResourceLoaders.",
+ )
+ return originalPath
+ }
+
+ val clazz = Class.forName("com.intellij.ui.icons.CachedImageIconKt")
+ val patchIconPath = clazz.getMethod("patchIconPath", String::class.java, ClassLoader::class.java)
+ patchIconPath.isAccessible = true
+
+ // For all provided classloaders, we try to get the patched path, both using
+ // the original path, and an "abridged" path that has gotten the icon path prefix
+ // removed (the classloader is set up differently in prod IDEs and when running
+ // from Gradle, and the icon could be in either place depending on the environment)
+ val fallbackPath = originalPath.removePrefix(dirProvider.dir())
+ val patchedPath = classLoaders.firstNotNullOfOrNull { classLoader ->
+ val patchedPathAndClassLoader =
+ patchIconPath.invoke(null, originalPath.removePrefix("/"), classLoader)
+ ?: patchIconPath.invoke(null, fallbackPath, classLoader)
+ patchedPathAndClassLoader as? Pair<*, *>
+ }?.first as? String
+
+ val path = if (patchedPath != null) {
+ logger.info("Found icon mapping: '$originalPath' -> '$patchedPath'")
+ patchedPath
+ } else {
+ logger.debug("Icon '$originalPath' has no available mapping")
+ originalPath
+ }
+
+ val overriddenPath = iconData.iconOverrides[path] ?: path
+ if (overriddenPath != path) {
+ logger.info("Found theme icon override: '$path' -> '$overriddenPath'")
+ }
+
+ return overriddenPath
+ }
+}
diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePaletteMapperFactory.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePaletteMapperFactory.kt
new file mode 100644
index 000000000..3d902a98d
--- /dev/null
+++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePaletteMapperFactory.kt
@@ -0,0 +1,27 @@
+package org.jetbrains.jewel.bridge
+
+import com.intellij.ide.ui.UITheme
+import com.intellij.openapi.diagnostic.thisLogger
+import org.jetbrains.jewel.PaletteMapper
+import org.jetbrains.jewel.themes.PaletteMapperFactory
+
+object BridgePaletteMapperFactory : PaletteMapperFactory() {
+
+ private val logger = thisLogger()
+
+ fun create(isDark: Boolean): PaletteMapper {
+ // If we can't read the current theme, no mapping is possible
+ val uiTheme = currentUiThemeOrNull() ?: return PaletteMapper.Empty
+ logger.info("Parsing theme info from theme ${uiTheme.name} (id: ${uiTheme.id}, isDark: ${uiTheme.isDark})")
+
+ val iconColorPalette = uiTheme.iconColorPalette
+ val keyPalette = UITheme.getColorPalette()
+ val themeColors = uiTheme.colors.orEmpty()
+
+ return createInternal(iconColorPalette, keyPalette, themeColors, isDark)
+ }
+
+ override fun logInfo(message: String) {
+ logger.info(message)
+ }
+}
diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeResourceLoader.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeResourceLoader.kt
new file mode 100644
index 000000000..e070efdec
--- /dev/null
+++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeResourceLoader.kt
@@ -0,0 +1,25 @@
+package org.jetbrains.jewel.bridge
+
+import com.intellij.util.ui.DirProvider
+import org.jetbrains.jewel.ClassLoaderProvider
+import org.jetbrains.jewel.JewelResourceLoader
+import java.io.InputStream
+
+object BridgeResourceLoader : JewelResourceLoader(), ClassLoaderProvider {
+
+ private val dirProvider = DirProvider()
+
+ override val classLoaders
+ get() = listOf(dirProvider::class.java.classLoader, javaClass.classLoader)
+
+ override fun load(resourcePath: String): InputStream {
+ val normalizedPath = resourcePath.removePrefix("/")
+ val fallbackPath = resourcePath.removePrefix(dirProvider.dir())
+ val resource = loadResourceOrNull(normalizedPath, classLoaders)
+ ?: loadResourceOrNull(fallbackPath, classLoaders)
+
+ return requireNotNull(resource) {
+ "Resource '$resourcePath' not found (tried using '$normalizedPath' and '$fallbackPath')"
+ }
+ }
+}
diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IntelliJResourcePainterProvider.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeResourcePainterProvider.kt
similarity index 57%
rename from ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IntelliJResourcePainterProvider.kt
rename to ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeResourcePainterProvider.kt
index 671328835..82c271645 100644
--- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IntelliJResourcePainterProvider.kt
+++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeResourcePainterProvider.kt
@@ -2,6 +2,7 @@ package org.jetbrains.jewel.bridge
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.ResourceLoader
+import org.jetbrains.jewel.IntelliJThemeIconData
import org.jetbrains.jewel.InteractiveComponentState
import org.jetbrains.jewel.InternalJewelApi
import org.jetbrains.jewel.SvgLoader
@@ -11,27 +12,39 @@ import org.jetbrains.jewel.styling.SimpleResourcePathPatcher
import org.jetbrains.jewel.styling.StatefulResourcePathPatcher
@OptIn(InternalJewelApi::class)
-class IntelliJResourcePainterProvider @InternalJewelApi constructor(
+internal class BridgeResourcePainterProvider @InternalJewelApi constructor(
basePath: String,
svgLoader: SvgLoader,
pathPatcher: ResourcePathPatcher,
private val iconMapper: IconMapper,
+ private val iconData: IntelliJThemeIconData,
) : ResourcePainterProvider(basePath, svgLoader, pathPatcher) {
@Composable
- override fun patchPath(basePath: String, resourceLoader: ResourceLoader, extraData: T?): String {
+ override fun patchPath(
+ basePath: String,
+ resourceLoader: ResourceLoader,
+ extraData: T?,
+ ): String {
val patchedPath = super.patchPath(basePath, resourceLoader, extraData)
- return iconMapper.mapPath(patchedPath, resourceLoader)
+ return iconMapper.mapPath(patchedPath, iconData, resourceLoader)
}
- companion object {
+ companion object Factory {
- fun stateless(basePath: String, svgLoader: SvgLoader) =
- IntelliJResourcePainterProvider(basePath, svgLoader, SimpleResourcePathPatcher(), IntelliJIconMapper)
+ fun stateless(basePath: String, svgLoader: SvgLoader, iconData: IntelliJThemeIconData) =
+ BridgeResourcePainterProvider(
+ basePath,
+ svgLoader,
+ SimpleResourcePathPatcher(),
+ BridgeIconMapper,
+ iconData,
+ )
fun stateful(
- basePath: String,
+ iconPath: String,
svgLoader: SvgLoader,
+ iconData: IntelliJThemeIconData,
prefixTokensProvider: (state: T) -> String = { "" },
suffixTokensProvider: (state: T) -> String = { "" },
pathPatcher: ResourcePathPatcher = StatefulResourcePathPatcher(
@@ -39,6 +52,6 @@ class IntelliJResourcePainterProvider @InternalJewelApi constructor(
suffixTokensProvider,
),
) =
- IntelliJResourcePainterProvider(basePath, svgLoader, pathPatcher, IntelliJIconMapper)
+ BridgeResourcePainterProvider(iconPath, svgLoader, pathPatcher, BridgeIconMapper, iconData)
}
}
diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeThemeColorPalette.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeThemeColorPalette.kt
index 88d2218b0..d96efce07 100644
--- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeThemeColorPalette.kt
+++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeThemeColorPalette.kt
@@ -2,13 +2,16 @@ package org.jetbrains.jewel.bridge
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.isSpecified
import com.intellij.openapi.diagnostic.Logger
import org.jetbrains.jewel.themes.intui.core.IntUiThemeColorPalette
+import java.util.TreeMap
private val logger = Logger.getInstance("BridgeThemeColorPalette")
@Immutable
-class BridgeThemeColorPalette(
+class BridgeThemeColorPalette private constructor(
+ override val rawMap: Map,
private val grey: List,
private val blue: List,
private val green: List,
@@ -17,6 +20,7 @@ class BridgeThemeColorPalette(
private val orange: List,
private val purple: List,
private val teal: List,
+ private val windowsPopupBorder: Color,
) : IntUiThemeColorPalette {
override fun grey(): List = grey
@@ -53,18 +57,44 @@ class BridgeThemeColorPalette(
companion object {
- fun readFromLaF() = BridgeThemeColorPalette(
- grey = readPaletteColors("Grey"),
- blue = readPaletteColors("Blue"),
- green = readPaletteColors("Green"),
- red = readPaletteColors("Red"),
- yellow = readPaletteColors("Yellow"),
- orange = readPaletteColors("Orange"),
- purple = readPaletteColors("Purple"),
- teal = readPaletteColors("Teal"),
- )
-
- private fun readPaletteColors(colorName: String): List {
+ fun readFromLaF(): BridgeThemeColorPalette {
+ val grey = readPaletteColors("Grey")
+ val blue = readPaletteColors("Blue")
+ val green = readPaletteColors("Green")
+ val red = readPaletteColors("Red")
+ val yellow = readPaletteColors("Yellow")
+ val orange = readPaletteColors("Orange")
+ val purple = readPaletteColors("Purple")
+ val teal = readPaletteColors("Teal")
+ val windowsPopupBorder = readPaletteColor("windowsPopupBorder")
+
+ val rawMap = buildMap {
+ putAll(grey)
+ putAll(blue)
+ putAll(green)
+ putAll(red)
+ putAll(yellow)
+ putAll(orange)
+ putAll(purple)
+ putAll(teal)
+ if (windowsPopupBorder.isSpecified) put("windowsPopupBorder", windowsPopupBorder)
+ }
+
+ return BridgeThemeColorPalette(
+ grey = grey.values.toList(),
+ blue = blue.values.toList(),
+ green = green.values.toList(),
+ red = red.values.toList(),
+ yellow = yellow.values.toList(),
+ orange = orange.values.toList(),
+ purple = purple.values.toList(),
+ teal = teal.values.toList(),
+ windowsPopupBorder = windowsPopupBorder,
+ rawMap = rawMap,
+ )
+ }
+
+ private fun readPaletteColors(colorName: String): Map {
val defaults = uiDefaults
val allKeys = defaults.keys
val colorNameKeyPrefix = "ColorPalette.$colorName"
@@ -77,19 +107,27 @@ class BridgeThemeColorPalette(
val afterName = it.substring(colorNameKeyPrefixLength)
afterName.toIntOrNull()
}
- .maxOrNull() ?: return emptyList()
+ .maxOrNull() ?: return TreeMap()
- return buildList {
+ return buildMap {
for (i in 1..lastColorIndex) {
- val value = defaults["$colorNameKeyPrefix$i"] as? java.awt.Color
+ val key = "$colorNameKeyPrefix$i"
+ val value = defaults[key] as? java.awt.Color
if (value == null) {
logger.error("Unable to find color value for palette key '$colorNameKeyPrefix$i'")
continue
}
- add(value.toComposeColor())
+ put(key, value.toComposeColor())
}
}
}
+
+ private fun readPaletteColor(colorName: String): Color {
+ val defaults = uiDefaults
+ val colorNameKey = "ColorPalette.$colorName"
+ return (defaults[colorNameKey] as? java.awt.Color)
+ ?.toComposeColor() ?: Color.Unspecified
+ }
}
}
diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeUtils.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeUtils.kt
index e6ee32764..9938d35a6 100644
--- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeUtils.kt
+++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeUtils.kt
@@ -155,16 +155,24 @@ internal operator fun TextUnit.plus(delta: Float) =
else -> this
}
-internal fun retrieveIcon(
- baseIconPath: String,
- iconData: IntelliJThemeIconData,
+internal fun retrieveStatefulIcon(
+ iconPath: String,
svgLoader: SvgLoader,
+ iconData: IntelliJThemeIconData,
prefixTokensProvider: (state: T) -> String = { "" },
suffixTokensProvider: (state: T) -> String = { "" },
): PainterProvider =
- IntelliJResourcePainterProvider.stateful(
- basePath = iconData.iconOverrides[baseIconPath] ?: baseIconPath,
+ BridgeResourcePainterProvider.stateful(
+ iconPath = iconPath,
svgLoader,
+ iconData,
prefixTokensProvider,
suffixTokensProvider,
)
+
+internal fun retrieveStatelessIcon(
+ iconPath: String,
+ svgLoader: SvgLoader,
+ iconData: IntelliJThemeIconData,
+): PainterProvider =
+ BridgeResourcePainterProvider.stateless(iconPath, svgLoader, iconData)
diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/ComposePanel.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/ComposePanel.kt
index 0960dfc98..af0764830 100644
--- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/ComposePanel.kt
+++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/ComposePanel.kt
@@ -11,4 +11,3 @@ fun ToolWindow.addComposeTab(
) = ComposePanel()
.apply { setContent(content) }
.also { contentManager.addContent(contentManager.factory.createContent(it, tabDisplayName, isLockable)) }
-
diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IconMapper.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IconMapper.kt
index a8b94e902..da88ea8f6 100644
--- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IconMapper.kt
+++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IconMapper.kt
@@ -1,8 +1,9 @@
package org.jetbrains.jewel.bridge
import androidx.compose.ui.res.ResourceLoader
+import org.jetbrains.jewel.IntelliJThemeIconData
interface IconMapper {
- fun mapPath(originalPath: String, resourceLoader: ResourceLoader): String
+ fun mapPath(originalPath: String, iconData: IntelliJThemeIconData, resourceLoader: ResourceLoader): String
}
diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IntUiBridge.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IntUiBridge.kt
index 1a572d831..5ffe23d04 100644
--- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IntUiBridge.kt
+++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IntUiBridge.kt
@@ -263,10 +263,10 @@ private fun readCheckboxStyle(iconData: IntelliJThemeIconData, svgLoader: SvgLoa
iconContentGap = 5.dp, // See DarculaCheckBoxUI#textIconGap
),
icons = IntUiCheckboxIcons(
- checkbox = retrieveIcon(
- baseIconPath = "${iconsBasePath}checkBox.svg",
- iconData = iconData,
+ checkbox = retrieveStatefulIcon(
+ iconPath = "${iconsBasePath}checkBox.svg",
svgLoader = svgLoader,
+ iconData = iconData,
prefixTokensProvider = { state: CheckboxState ->
if (state.toggleableState == ToggleableState.Indeterminate) "Indeterminate" else ""
},
@@ -381,8 +381,8 @@ private fun readDropdownStyle(
borderWidth = DarculaUIUtil.BW.dp,
),
icons = IntUiDropdownIcons(
- chevronDown = retrieveIcon(
- baseIconPath = "${iconsBasePath}general/chevron-down.svg",
+ chevronDown = retrieveStatefulIcon(
+ iconPath = "${iconsBasePath}general/chevron-down.svg",
iconData = iconData,
svgLoader = svgLoader,
),
@@ -497,13 +497,13 @@ private fun readLinkStyle(
iconSize = DpSize.Unspecified,
),
icons = IntUiLinkIcons(
- dropdownChevron = retrieveIcon(
- baseIconPath = "${iconsBasePath}general/chevron-down.svg",
+ dropdownChevron = retrieveStatefulIcon(
+ iconPath = "${iconsBasePath}general/chevron-down.svg",
iconData = iconData,
svgLoader = svgLoader,
),
- externalLink = retrieveIcon(
- baseIconPath = "${iconsBasePath}ide/external_link_arrow.svg",
+ externalLink = retrieveStatefulIcon(
+ iconPath = "${iconsBasePath}ide/external_link_arrow.svg",
iconData = iconData,
svgLoader = svgLoader,
),
@@ -571,8 +571,8 @@ private fun readMenuStyle(iconData: IntelliJThemeIconData, svgLoader: SvgLoader)
),
),
icons = IntUiMenuIcons(
- submenuChevron = retrieveIcon(
- baseIconPath = "${iconsBasePath}general/chevron-down.svg",
+ submenuChevron = retrieveStatefulIcon(
+ iconPath = "${iconsBasePath}general/chevron-down.svg",
iconData = iconData,
svgLoader = svgLoader,
),
@@ -599,8 +599,8 @@ private fun readRadioButtonStyle(iconData: IntelliJThemeIconData, svgLoader: Svg
iconContentGap = retrieveIntAsDpOrUnspecified("RadioButton.textIconGap").takeOrElse { 4.dp },
),
icons = IntUiRadioButtonIcons(
- radioButton = retrieveIcon(
- baseIconPath = "${iconsBasePath}darcula/radio.svg",
+ radioButton = retrieveStatefulIcon(
+ iconPath = "${iconsBasePath}radio.svg",
iconData = iconData,
svgLoader = svgLoader,
),
@@ -716,6 +716,7 @@ private fun readLazyTreeStyle(iconData: IntelliJThemeIconData, svgLoader: SvgLoa
val normalContent = retrieveColorOrUnspecified("Tree.foreground")
val selectedContent = retrieveColorOrUnspecified("Tree.selectionForeground")
val selectedElementBackground = retrieveColorOrUnspecified("Tree.selectionBackground")
+ val inactiveSelectedElementBackground = retrieveColorOrUnspecified("Tree.selectionInactiveBackground")
val colors = IntUiLazyTreeColors(
content = normalContent,
@@ -723,15 +724,26 @@ private fun readLazyTreeStyle(iconData: IntelliJThemeIconData, svgLoader: SvgLoa
contentSelected = selectedContent,
contentSelectedFocused = selectedContent,
elementBackgroundFocused = Color.Transparent,
- elementBackgroundSelected = selectedElementBackground,
+ elementBackgroundSelected = inactiveSelectedElementBackground,
elementBackgroundSelectedFocused = selectedElementBackground,
)
+ val chevronCollapsed = retrieveStatelessIcon(
+ iconPath = "${iconsBasePath}general/chevron-right.svg",
+ iconData = iconData,
+ svgLoader = svgLoader,
+ )
+ val chevronExpanded = retrieveStatelessIcon(
+ iconPath = "${iconsBasePath}general/chevron-down.svg",
+ iconData = iconData,
+ svgLoader = svgLoader,
+ )
+
return IntUiLazyTreeStyle(
colors = colors,
metrics = IntUiLazyTreeMetrics(
indentSize = retrieveIntAsDpOrUnspecified("Tree.leftChildIndent").takeOrElse { 7.dp } +
- retrieveIntAsDpOrUnspecified("Tree.rightChildIndent").takeOrElse { 11.dp },
+ retrieveIntAsDpOrUnspecified("Tree.rightChildIndent").takeOrElse { 11.dp },
elementBackgroundCornerSize = CornerSize(JBUI.CurrentTheme.Tree.ARC.dp / 2),
elementPadding = PaddingValues(horizontal = 12.dp),
elementContentPadding = PaddingValues(4.dp),
@@ -739,16 +751,10 @@ private fun readLazyTreeStyle(iconData: IntelliJThemeIconData, svgLoader: SvgLoa
chevronContentGap = 2.dp, // See com.intellij.ui.tree.ui.ClassicPainter.GAP
),
icons = IntUiLazyTreeIcons(
- nodeChevronCollapsed = retrieveIcon(
- baseIconPath = "${iconsBasePath}general/chevron-right.svg",
- iconData = iconData,
- svgLoader = svgLoader,
- ),
- nodeChevronExpanded = retrieveIcon(
- baseIconPath = "${iconsBasePath}general/chevron-down.svg",
- iconData = iconData,
- svgLoader = svgLoader,
- ),
+ chevronCollapsed = chevronCollapsed,
+ chevronExpanded = chevronExpanded,
+ chevronSelectedCollapsed = chevronCollapsed,
+ chevronSelectedExpanded = chevronExpanded,
),
)
}
@@ -790,8 +796,8 @@ private fun readDefaultTabStyle(iconData: IntelliJThemeIconData, svgLoader: SvgL
tabHeight = retrieveIntAsDpOrUnspecified("TabbedPane.tabHeight").takeOrElse { 24.dp },
),
icons = IntUiTabIcons(
- close = retrieveIcon(
- baseIconPath = "${iconsBasePath}expui/general/closeSmall.svg",
+ close = retrieveStatefulIcon(
+ iconPath = "${iconsBasePath}expui/general/closeSmall.svg",
iconData = iconData,
svgLoader = svgLoader,
),
@@ -849,8 +855,8 @@ private fun readEditorTabStyle(iconData: IntelliJThemeIconData, svgLoader: SvgLo
tabHeight = retrieveIntAsDpOrUnspecified("TabbedPane.tabHeight").takeOrElse { 24.dp },
),
icons = IntUiTabIcons(
- close = retrieveIcon(
- baseIconPath = "${iconsBasePath}expui/general/closeSmall.svg",
+ close = retrieveStatefulIcon(
+ iconPath = "${iconsBasePath}expui/general/closeSmall.svg",
iconData = iconData,
svgLoader = svgLoader,
),
diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IntelliJIconMapper.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IntelliJIconMapper.kt
deleted file mode 100644
index 29a2a6d3b..000000000
--- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IntelliJIconMapper.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package org.jetbrains.jewel.bridge
-
-import androidx.compose.ui.res.ResourceLoader
-import com.intellij.ide.ui.IconMapLoader
-import com.intellij.openapi.components.service
-import com.intellij.openapi.diagnostic.thisLogger
-import org.jetbrains.jewel.JewelResourceLoader
-
-object IntelliJIconMapper : IconMapper {
-
- private val logger = thisLogger()
-
- private val mappingsByClassLoader
- get() = service().loadIconMapping()
-
- override fun mapPath(originalPath: String, resourceLoader: ResourceLoader): String {
- logger.debug("Loading SVG from '$originalPath'")
- val searchClasses = (resourceLoader as? JewelResourceLoader)?.searchClasses
- if (searchClasses == null) {
- logger.warn(
- "Tried loading a resource but the provided ResourceLoader is now a JewelResourceLoader; " +
- "this is probably a bug. Make sure you always use JewelResourceLoaders.",
- )
- return originalPath
- }
-
- val allMappings = mappingsByClassLoader
- if (allMappings.isEmpty()) {
- logger.info("No mapping info available yet, can't check for '$originalPath' mapping.")
- return originalPath
- }
-
- val applicableMappings = searchClasses.mapNotNull { allMappings[it.classLoader] }
- val mappedPath = applicableMappings.firstNotNullOfOrNull { it[originalPath.removePrefix("/")] }
-
- if (mappedPath == null) {
- logger.debug("Icon '$originalPath' has no mapping defined.")
- return originalPath
- }
-
- logger.debug("Icon '$originalPath' is mapped to '$mappedPath'.")
- return mappedPath
- }
-}
diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeService.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeService.kt
index aeb2e0f98..3858d684b 100644
--- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeService.kt
+++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeService.kt
@@ -10,16 +10,17 @@ import kotlinx.coroutines.CoroutineName
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
+import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.stateIn
import org.jetbrains.jewel.IntelliJComponentStyling
-import org.jetbrains.jewel.IntelliJSvgLoader
+import org.jetbrains.jewel.JewelSvgLoader
import org.jetbrains.jewel.SvgLoader
-import org.jetbrains.jewel.themes.PaletteMapperFactory
import org.jetbrains.jewel.themes.intui.core.IntUiThemeDefinition
import org.jetbrains.jewel.themes.intui.core.IntelliJSvgPatcher
+import kotlin.time.Duration.Companion.milliseconds
@Service(Level.APP)
class SwingBridgeService : Disposable {
@@ -33,9 +34,20 @@ class SwingBridgeService : Disposable {
// TODO we shouldn't assume it's Int UI, but we only have that for now
internal val currentBridgeThemeData: StateFlow =
IntelliJApplication.lookAndFeelChangedFlow(coroutineScope)
- .map { readThemeData() }
+ .mapLatest { getThemeData() }
.stateIn(coroutineScope, SharingStarted.Eagerly, BridgeThemeData.DEFAULT)
+ private suspend fun getThemeData(): BridgeThemeData {
+ var counter = 0
+ while (counter < 20) {
+ delay(20.milliseconds)
+ counter++
+ runCatching { readThemeData() }
+ .onSuccess { return it }
+ }
+ return readThemeData()
+ }
+
private suspend fun readThemeData(): BridgeThemeData {
val isIntUi = NewUI.isEnabled()
if (!isIntUi) {
@@ -86,7 +98,7 @@ class SwingBridgeService : Disposable {
}
private fun createSvgLoader(theme: IntUiThemeDefinition): SvgLoader {
- val paletteMapper = PaletteMapperFactory.create(theme.isDark, theme.iconData, theme.colorPalette)
+ val paletteMapper = BridgePaletteMapperFactory.create(theme.isDark)
val svgPatcher = IntelliJSvgPatcher(paletteMapper)
- return IntelliJSvgLoader(svgPatcher)
+ return JewelSvgLoader(svgPatcher)
}
diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeTheme.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeTheme.kt
index 5182b5243..4b96f03dc 100644
--- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeTheme.kt
+++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeTheme.kt
@@ -4,12 +4,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
-import androidx.compose.ui.res.ResourceLoader
import com.intellij.openapi.components.service
-import com.intellij.util.ui.DirProvider
import org.jetbrains.jewel.ExperimentalJewelApi
import org.jetbrains.jewel.LocalResourceLoader
-import org.jetbrains.jewel.themes.intui.standalone.IntUiDefaultResourceLoader
import org.jetbrains.jewel.themes.intui.standalone.IntUiTheme
private val bridgeService = service()
@@ -21,18 +18,8 @@ fun SwingBridgeTheme(content: @Composable () -> Unit) {
// TODO handle non-Int UI themes, too
IntUiTheme(themeData.themeDefinition, themeData.componentStyling, swingCompatMode = true) {
- CompositionLocalProvider(LocalResourceLoader provides IntelliJResourceLoader) {
+ CompositionLocalProvider(LocalResourceLoader provides BridgeResourceLoader) {
content()
}
}
}
-
-object IntelliJResourceLoader : ResourceLoader {
-
- private val dirProvider = DirProvider()
-
- override fun load(resourcePath: String) =
- IntUiDefaultResourceLoader.loadOrNull(resourcePath)
- ?: IntUiDefaultResourceLoader.load(resourcePath.removePrefix(dirProvider.dir()))
-
-}
diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/UiThemeExtensions.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/UiThemeExtensions.kt
new file mode 100644
index 000000000..44f60c996
--- /dev/null
+++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/UiThemeExtensions.kt
@@ -0,0 +1,42 @@
+package org.jetbrains.jewel.bridge
+
+import com.intellij.ide.ui.LafManager
+import com.intellij.ide.ui.UITheme
+import com.intellij.ide.ui.laf.UIThemeBasedLookAndFeelInfo
+import com.intellij.openapi.diagnostic.Logger
+import java.lang.reflect.Field
+
+private val logger = Logger.getInstance("UiThemeExtensions")
+
+private val classUITheme
+ get() = UITheme::class.java
+
+internal fun currentUiThemeOrNull() =
+ (LafManager.getInstance().currentLookAndFeel as? UIThemeBasedLookAndFeelInfo)?.theme
+
+internal val UITheme.icons: Map
+ get() = readMapField(classUITheme.getDeclaredField("icons"))
+ .filterKeys { it != "ColorPalette" }
+
+internal val UITheme.iconColorPalette: Map
+ get() = readMapField