From 322b53bfa966a93c2b213789b9032d6afc2220cd Mon Sep 17 00:00:00 2001 From: Sebastiano Poggi Date: Fri, 15 Sep 2023 10:25:40 +0200 Subject: [PATCH] Re-do icon tinting logic, better match Swing --- buildSrc/src/main/kotlin/jewel.gradle.kts | 1 + .../theme/IntUiThemeDescriptorReader.kt | 17 ++ .../theme/IntelliJThemeGeneratorPlugin.kt | 53 ++++--- .../jewel/IntelliJThemeColorPalette.kt | 6 +- .../jetbrains/jewel/IntelliJThemeIconData.kt | 24 +++ .../org/jetbrains/jewel/JewelSvgLoader.kt | 6 +- .../org/jetbrains/jewel/PaletteMapper.kt | 54 ++++--- .../kotlin/org/jetbrains/jewel/SvgPatcher.kt | 2 +- .../jewel/themes/PaletteMapperFactory.kt | 148 ++++++------------ .../themes/StandalonePaletteMapperFactory.kt | 82 ++++++++++ .../jetbrains/jewel/bridge/BridgeIconData.kt | 36 +---- .../jewel/bridge/BridgeIconMapper.kt | 22 ++- .../bridge/BridgePaletteMapperFactory.kt | 27 ++++ .../jewel/bridge/BridgeThemeColorPalette.kt | 72 +++++++-- .../org/jetbrains/jewel/bridge/IconMapper.kt | 3 +- .../jewel/bridge/SwingBridgeService.kt | 3 +- .../jewel/bridge/UiThemeExtensions.kt | 42 +++++ .../intui/core/IntUiThemeColorPalette.kt | 2 +- .../themes/intui/core/IntelliJSvgPatcher.kt | 21 ++- .../themes/intui/standalone/IntUiTheme.kt | 8 +- 20 files changed, 408 insertions(+), 221 deletions(-) create mode 100644 core/src/main/kotlin/org/jetbrains/jewel/themes/StandalonePaletteMapperFactory.kt create mode 100644 ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePaletteMapperFactory.kt create mode 100644 ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/UiThemeExtensions.kt diff --git a/buildSrc/src/main/kotlin/jewel.gradle.kts b/buildSrc/src/main/kotlin/jewel.gradle.kts index 6f02f49e52..e6fc511483 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 7f2c834de1..8ace1fd577 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 @@ -77,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( 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 521ca64752..8b9ac863b8 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) + } } } } @@ -72,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() diff --git a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeColorPalette.kt b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeColorPalette.kt index 7f13cb4352..0ab35707db 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 eb0d4858ff..0531c1f61c 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeIconData.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/IntelliJThemeIconData.kt @@ -1,6 +1,7 @@ package org.jetbrains.jewel import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color @Immutable interface IntelliJThemeIconData { @@ -8,8 +9,31 @@ 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 { diff --git a/core/src/main/kotlin/org/jetbrains/jewel/JewelSvgLoader.kt b/core/src/main/kotlin/org/jetbrains/jewel/JewelSvgLoader.kt index d497fa5777..e93f4749ca 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/JewelSvgLoader.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/JewelSvgLoader.kt @@ -37,13 +37,13 @@ class JewelSvgLoader(private val svgPatcher: SvgPatcher) : SvgLoader { val density = LocalDensity.current val painter = useResource(resourcePath, loader) { - loadSvgPainter(it.patchColors(), density) + loadSvgPainter(it.patchColors(resourcePath), density) } return remember(resourcePath, density, loader) { painter } } - private fun InputStream.patchColors(): InputStream = - svgPatcher.patchSvg(this).byteInputStream() + private fun InputStream.patchColors(resourcePath: String): InputStream = + svgPatcher.patchSvg(this, resourcePath).byteInputStream() // Copied from androidx.compose.ui.res.Resources private inline fun useResource( diff --git a/core/src/main/kotlin/org/jetbrains/jewel/PaletteMapper.kt b/core/src/main/kotlin/org/jetbrains/jewel/PaletteMapper.kt index a258714f8a..5ebe6666ab 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/PaletteMapper.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/PaletteMapper.kt @@ -3,42 +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( - private val colorOverrides: Map, - private val selectedColorOverrides: Map, + private val ui: Scope, + private val checkBoxes: Scope, + private val trees: Scope, ) { - fun mapColorOrNull(originalColor: Color): Color? { - if (colorOverrides.isEmpty()) return null - - return colorOverrides[originalColor] + 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 mapSelectedColorOrNull(originalColor: Color): Color? { - if (selectedColorOverrides.isEmpty()) return null + companion object { - return selectedColorOverrides[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] - if (colorOverrides != other.colorOverrides) return false - if (selectedColorOverrides != other.selectedColorOverrides) return false + override fun toString(): String = "PaletteMapper.Scope(colorOverrides=$colorOverrides)" - return true - } + companion object { - override fun hashCode(): Int { - var result = colorOverrides.hashCode() - result = 31 * result + selectedColorOverrides.hashCode() - return result + val Empty = Scope(emptyMap()) + } } - - override fun toString(): String = - "PaletteMapper(colorOverrides=$colorOverrides, selectedColorOverrides=$selectedColorOverrides)" } diff --git a/core/src/main/kotlin/org/jetbrains/jewel/SvgPatcher.kt b/core/src/main/kotlin/org/jetbrains/jewel/SvgPatcher.kt index 00910c0b46..2cc427fc55 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/themes/PaletteMapperFactory.kt b/core/src/main/kotlin/org/jetbrains/jewel/themes/PaletteMapperFactory.kt index ccee636474..6434771532 100644 --- a/core/src/main/kotlin/org/jetbrains/jewel/themes/PaletteMapperFactory.kt +++ b/core/src/main/kotlin/org/jetbrains/jewel/themes/PaletteMapperFactory.kt @@ -1,105 +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) - val selectionOverrides = iconData.selectionColorPalette - .mapNotNull { (key, value) -> - val keyColor = key.toColorOrNull() ?: return@mapNotNull null - val valueColor = value.toColorOrNull() ?: return@mapNotNull null - keyColor to valueColor - } - .toMap() + // 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(overrides, selectionOverrides) + 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() @@ -117,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 0000000000..bd49dc09d1 --- /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 799b7e31e5..f371e74ee5 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,8 @@ package org.jetbrains.jewel.bridge import androidx.compose.runtime.Immutable -import com.intellij.ide.ui.LafManager import com.intellij.ide.ui.UITheme -import com.intellij.ide.ui.laf.UIThemeBasedLookAndFeelInfo -import com.intellij.openapi.diagnostic.thisLogger import org.jetbrains.jewel.IntelliJThemeIconData -import java.lang.reflect.Field @Immutable internal class BridgeIconData( @@ -42,36 +38,12 @@ internal class BridgeIconData( companion object { fun readFromLaF(): BridgeIconData { - val classUITheme = UITheme::class.java - val iconMap: Map = readMapField(classUITheme.getDeclaredField("icons")) - val selectionColorPalette: Map = - readMapField(classUITheme.getDeclaredField("selectionColorPalette")) + val uiTheme = currentUiThemeOrNull() + val iconMap = uiTheme?.icons ?: emptyMap() + val selectedIconColorPalette = uiTheme?.selectedIconColorPalette ?: emptyMap() val colorPalette = UITheme.getColorPalette() - return BridgeIconData(iconMap.filterKeys { it != "ColorPalette" }, colorPalette, selectionColorPalette) - } - - private fun readMapField(field: Field): Map { - @Suppress("DEPRECATION") // We don't have an alternative API to use - val wasAccessible = field.isAccessible - field.isAccessible = true - - val iconMap: Map = try { - val laf = LafManager.getInstance().currentLookAndFeel as? UIThemeBasedLookAndFeelInfo - - if (laf != null) { - @Suppress("UNCHECKED_CAST") - field.get(laf.theme) as? Map ?: emptyMap() - } else { - emptyMap() - } - } catch (e: IllegalAccessException) { - thisLogger().warn("Error while retrieving LaF", e) - emptyMap() - } finally { - field.isAccessible = wasAccessible - } - return iconMap + 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 index 245dd1cbd9..0c3c245a54 100644 --- 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 @@ -4,6 +4,7 @@ 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 { @@ -11,7 +12,11 @@ internal object BridgeIconMapper : IconMapper { private val dirProvider = DirProvider() - override fun mapPath(originalPath: String, resourceLoader: ResourceLoader): String { + override fun mapPath( + originalPath: String, + iconData: IntelliJThemeIconData, + resourceLoader: ResourceLoader, + ): String { val classLoaders = (resourceLoader as? ClassLoaderProvider)?.classLoaders if (classLoaders == null) { logger.warn( @@ -37,12 +42,19 @@ internal object BridgeIconMapper : IconMapper { patchedPathAndClassLoader as? Pair<*, *> }?.first as? String - if (patchedPath != null) { + val path = if (patchedPath != null) { logger.info("Found icon mapping: '$originalPath' -> '$patchedPath'") - return patchedPath + patchedPath + } else { + logger.debug("Icon '$originalPath' has no available mapping") + originalPath } - logger.debug("Icon '$originalPath' has no available mapping") - return 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 0000000000..3d902a98d5 --- /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/BridgeThemeColorPalette.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeThemeColorPalette.kt index 88d2218b0c..d96efce07f 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/IconMapper.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/IconMapper.kt index a8b94e9024..da88ea8f66 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/SwingBridgeService.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/SwingBridgeService.kt index 5c89fa4698..3858d684b3 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 @@ -18,7 +18,6 @@ import kotlinx.coroutines.flow.stateIn import org.jetbrains.jewel.IntelliJComponentStyling 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 @@ -99,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 JewelSvgLoader(svgPatcher) } 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 0000000000..134b11ff0a --- /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>(classUITheme.getDeclaredField("icons")) + .get("ColorPalette") ?: emptyMap() + +internal val UITheme.selectedIconColorPalette: Map + get() = readMapField(classUITheme.getDeclaredField("iconColorsOnSelection")) + +private fun UITheme.readMapField(field: Field): Map { + @Suppress("DEPRECATION") // We don't have an alternative API to use + val wasAccessible = field.isAccessible + field.isAccessible = true + + return try { + @Suppress("UNCHECKED_CAST") + field.get(this) as? Map ?: emptyMap() + } catch (e: IllegalAccessException) { + logger.warn("Error while retrieving LaF", e) + emptyMap() + } finally { + field.isAccessible = wasAccessible + } +} diff --git a/themes/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/themes/intui/core/IntUiThemeColorPalette.kt b/themes/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/themes/intui/core/IntUiThemeColorPalette.kt index 9820621e21..3406ca601c 100644 --- a/themes/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/themes/intui/core/IntUiThemeColorPalette.kt +++ b/themes/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/themes/intui/core/IntUiThemeColorPalette.kt @@ -92,7 +92,7 @@ internal object EmptyIntUiThemeColorPalette : IntUiThemeColorPalette { override fun teal(index: Int): Color = Color.Unspecified - override fun lookup(colorKey: String): Color? = null + override val rawMap: Map = emptyMap() } private val colorKeyRegex: Regex diff --git a/themes/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/themes/intui/core/IntelliJSvgPatcher.kt b/themes/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/themes/intui/core/IntelliJSvgPatcher.kt index 2a5da73d5a..0fc82510ee 100644 --- a/themes/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/themes/intui/core/IntelliJSvgPatcher.kt +++ b/themes/int-ui/int-ui-core/src/main/kotlin/org/jetbrains/jewel/themes/intui/core/IntelliJSvgPatcher.kt @@ -25,35 +25,40 @@ class IntelliJSvgPatcher(private val mapper: PaletteMapper) : SvgPatcher { private val documentBuilderFactory = DocumentBuilderFactory.newDefaultInstance() .apply { setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true) } - override fun patchSvg(rawSvg: InputStream): String { + override fun patchSvg(rawSvg: InputStream, path: String?): String { val builder = documentBuilderFactory.newDocumentBuilder() val document = builder.parse(rawSvg) - document.documentElement.patchColors(mapper) + + val scope = mapper.getScopeForPath(path) + if (scope != null) { + document.documentElement.patchColors(scope) + } + return document.writeToString() } - private fun Element.patchColors(mapper: PaletteMapper) { - patchColorAttribute("fill", mapper) - patchColorAttribute("stroke", mapper) + private fun Element.patchColors(mapperScope: PaletteMapper.Scope) { + patchColorAttribute("fill", mapperScope) + patchColorAttribute("stroke", mapperScope) val nodes = childNodes val length = nodes.length for (i in 0 until length) { val item = nodes.item(i) if (item is Element) { - item.patchColors(mapper) + item.patchColors(mapperScope) } } } - private fun Element.patchColorAttribute(attrName: String, mapper: PaletteMapper) { + private fun Element.patchColorAttribute(attrName: String, mapperScope: PaletteMapper.Scope) { val color = getAttribute(attrName) val opacity = getAttribute("$attrName-opacity") if (color.isNotEmpty()) { val alpha = opacity.toFloatOrNull() ?: 1.0f val originalColor = tryParseColor(color, alpha) ?: return - val newColor = mapper.mapColorOrNull(originalColor) ?: return + val newColor = mapperScope.mapColorOrNull(originalColor) ?: return setAttribute(attrName, newColor.copy(alpha = alpha).toHexString()) } } diff --git a/themes/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/themes/intui/standalone/IntUiTheme.kt b/themes/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/themes/intui/standalone/IntUiTheme.kt index 7adf61bdad..17023a8e39 100644 --- a/themes/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/themes/intui/standalone/IntUiTheme.kt +++ b/themes/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/themes/intui/standalone/IntUiTheme.kt @@ -38,7 +38,7 @@ import org.jetbrains.jewel.styling.RadioButtonStyle import org.jetbrains.jewel.styling.ScrollbarStyle import org.jetbrains.jewel.styling.TabStyle import org.jetbrains.jewel.styling.TextFieldStyle -import org.jetbrains.jewel.themes.PaletteMapperFactory +import org.jetbrains.jewel.themes.StandalonePaletteMapperFactory import org.jetbrains.jewel.themes.intui.core.BaseIntUiTheme import org.jetbrains.jewel.themes.intui.core.IntUiThemeColorPalette import org.jetbrains.jewel.themes.intui.core.IntUiThemeDefinition @@ -199,7 +199,11 @@ fun IntUiTheme( ) { val svgLoader by remember(themeDefinition.isDark, themeDefinition.iconData, themeDefinition.colorPalette) { val paletteMapper = - PaletteMapperFactory.create(themeDefinition.isDark, themeDefinition.iconData, themeDefinition.colorPalette) + StandalonePaletteMapperFactory.create( + themeDefinition.isDark, + themeDefinition.iconData, + themeDefinition.colorPalette, + ) val svgPatcher = IntelliJSvgPatcher(paletteMapper) mutableStateOf(JewelSvgLoader(svgPatcher)) }