From 48ad97be3ac201c86bf85630c3ca42bf4bb54704 Mon Sep 17 00:00:00 2001 From: "daria.suvorova" Date: Thu, 19 Feb 2026 15:59:29 +0100 Subject: [PATCH] [JEWEL-1257] Add support for new islands palette closes https://github.com/JetBrains/intellij-community/pull/3523 (cherry picked from commit 96498d9ec3a2da68d726e4611a390259f65fa5b9) (cherry picked from commit f8511c28627d60cd15e2825ea393caff5058a499) IJ-MR-211495 GitOrigin-RevId: bc164364d244e96ecb2a1373dda0a7d5b02120a2 --- .../theme/IntUiThemeDescriptorReader.kt | 2 +- platform/jewel/foundation/api-dump.txt | 3 +- .../metalava/foundation-api-0.39.0.txt | 2 +- .../metalava/foundation-api-stable-0.39.0.txt | 2 +- .../foundation/theme/ThemeColorPalette.kt | 189 +++++---- .../bridge/clipboard/JewelBridgeClipboard.kt | 1 + .../bridge/theme/BridgeThemeColorPalette.kt | 47 ++- .../theme/BridgeThemeColorPaletteTest.kt | 364 ++++++++++++++++++ .../jewel/intui/core/theme/IntUiDarkTheme.kt | 1 + .../jewel/intui/core/theme/IntUiLightTheme.kt | 1 + 10 files changed, 524 insertions(+), 88 deletions(-) create mode 100644 platform/jewel/ide-laf-bridge/src/test/kotlin/org/jetbrains/jewel/bridge/theme/BridgeThemeColorPaletteTest.kt diff --git a/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntUiThemeDescriptorReader.kt b/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntUiThemeDescriptorReader.kt index ff6e24161479..c1565bfb3d6b 100644 --- a/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntUiThemeDescriptorReader.kt +++ b/platform/jewel/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntUiThemeDescriptorReader.kt @@ -88,7 +88,7 @@ internal object IntUiThemeDescriptorReader { addProperty( PropertySpec.builder("colors", colorPaletteClassName, KModifier.OVERRIDE) - .initializer("ThemeColorPalette(%L,\n%L\n)", colorGroups.joinToCode(","), rawMap) + .initializer("ThemeColorPalette(%L,\n%L,\nisIslands = false\n)", colorGroups.joinToCode(","), rawMap) .build() ) } diff --git a/platform/jewel/foundation/api-dump.txt b/platform/jewel/foundation/api-dump.txt index 0ca933dd9efc..7a28eceeddc6 100644 --- a/platform/jewel/foundation/api-dump.txt +++ b/platform/jewel/foundation/api-dump.txt @@ -769,7 +769,8 @@ f:org.jetbrains.jewel.foundation.theme.JewelThemeKt f:org.jetbrains.jewel.foundation.theme.ThemeColorPalette - sf:$stable:I - sf:Companion:org.jetbrains.jewel.foundation.theme.ThemeColorPalette$Companion -- (java.util.List,java.util.List,java.util.List,java.util.List,java.util.List,java.util.List,java.util.List,java.util.List,java.util.Map):V +- b:(java.util.List,java.util.List,java.util.List,java.util.List,java.util.List,java.util.List,java.util.List,java.util.List,java.util.Map):V +- (java.util.List,java.util.List,java.util.List,java.util.List,java.util.List,java.util.List,java.util.List,java.util.List,java.util.Map,Z):V - f:blue-vNxB06k(I):J - f:blueOrNull-ijrfgN4(I):androidx.compose.ui.graphics.Color - equals(java.lang.Object):Z diff --git a/platform/jewel/foundation/metalava/foundation-api-0.39.0.txt b/platform/jewel/foundation/metalava/foundation-api-0.39.0.txt index 68a4c2c80d74..bd331e21be24 100644 --- a/platform/jewel/foundation/metalava/foundation-api-0.39.0.txt +++ b/platform/jewel/foundation/metalava/foundation-api-0.39.0.txt @@ -972,7 +972,7 @@ package org.jetbrains.jewel.foundation.theme { } @androidx.compose.runtime.Immutable @org.jetbrains.jewel.foundation.GenerateDataFunctions public final class ThemeColorPalette { - ctor public ThemeColorPalette(java.util.List gray, java.util.List blue, java.util.List green, java.util.List red, java.util.List yellow, java.util.List orange, java.util.List purple, java.util.List teal, java.util.Map rawMap); + ctor public ThemeColorPalette(java.util.List gray, java.util.List blue, java.util.List green, java.util.List red, java.util.List yellow, java.util.List orange, java.util.List purple, java.util.List teal, java.util.Map rawMap, boolean isIslands); method @Deprecated public long blue(int index); method public androidx.compose.ui.graphics.Color? blueOrNull(int index); method public java.util.List getBlue(); diff --git a/platform/jewel/foundation/metalava/foundation-api-stable-0.39.0.txt b/platform/jewel/foundation/metalava/foundation-api-stable-0.39.0.txt index 8872fd85a7d5..9fe0215d68a2 100644 --- a/platform/jewel/foundation/metalava/foundation-api-stable-0.39.0.txt +++ b/platform/jewel/foundation/metalava/foundation-api-stable-0.39.0.txt @@ -940,7 +940,7 @@ package org.jetbrains.jewel.foundation.theme { } @androidx.compose.runtime.Immutable @org.jetbrains.jewel.foundation.GenerateDataFunctions public final class ThemeColorPalette { - ctor public ThemeColorPalette(java.util.List gray, java.util.List blue, java.util.List green, java.util.List red, java.util.List yellow, java.util.List orange, java.util.List purple, java.util.List teal, java.util.Map rawMap); + ctor public ThemeColorPalette(java.util.List gray, java.util.List blue, java.util.List green, java.util.List red, java.util.List yellow, java.util.List orange, java.util.List purple, java.util.List teal, java.util.Map rawMap, boolean isIslands); method @Deprecated public long blue(int index); method public androidx.compose.ui.graphics.Color? blueOrNull(int index); method public java.util.List getBlue(); diff --git a/platform/jewel/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/theme/ThemeColorPalette.kt b/platform/jewel/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/theme/ThemeColorPalette.kt index 09849afb3f65..4fc33969398e 100644 --- a/platform/jewel/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/theme/ThemeColorPalette.kt +++ b/platform/jewel/foundation/src/main/kotlin/org/jetbrains/jewel/foundation/theme/ThemeColorPalette.kt @@ -4,9 +4,12 @@ import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color import org.jetbrains.jewel.foundation.GenerateDataFunctions -private val colorKeyRegex: Regex +private val nonIslandsColorKeyRegex: Regex get() = "([a-z]+)(\\d+)".toRegex(RegexOption.IGNORE_CASE) +private val islandsColorKeyRegex: Regex + get() = "([a-z]+)-(\\d+)".toRegex(RegexOption.IGNORE_CASE) + /** * A palette of colors provided by the theme. * @@ -14,7 +17,7 @@ private val colorKeyRegex: Regex * * Note that not all Look and Feel themes are guaranteed to have a full palette, and some may not have one at all. The * number of colors in each list depends on the implementation in the LaF. It is therefore important to use the - * `*OrNull` accessors to avoid [IndexOutOfBoundsException]s. + * `*OrNull` accessors to avoid [NullPointerException]s. * * @property gray A list of gray colors, from lightest to darkest. * @property blue A list of blue colors. @@ -39,10 +42,36 @@ public class ThemeColorPalette( public val purple: List, public val teal: List, public val rawMap: Map, + private val isIslands: Boolean, ) { + @Suppress("DEPRECATION") + @Deprecated("Use the constructor with isIslands parameter", level = DeprecationLevel.HIDDEN) + public constructor( + gray: List, + blue: List, + green: List, + red: List, + yellow: List, + orange: List, + purple: List, + teal: List, + rawMap: Map, + ) : this( + gray = gray, + blue = blue, + green = green, + red = red, + yellow = yellow, + orange = orange, + purple = purple, + teal = teal, + rawMap = rawMap, + isIslands = false, + ) + /** - * Retrieves a gray color from the palette by its index. Note that this function is not safe to use and can throw an - * [IndexOutOfBoundsException] at runtime if the Look and Feel does not provide a full palette. + * Retrieves a gray color from the palette by its index. Note that this function is not safe to use and can throw a + * [NullPointerException] at runtime if the Look and Feel does not provide a color for the requested index. * * You should use [grayOrNull] instead. * @@ -51,28 +80,30 @@ public class ThemeColorPalette( * * @param index The 1-based index of the color to retrieve. * @return The [Color] at the specified index. - * @throws IndexOutOfBoundsException if the index is out of bounds. + * @throws NullPointerException if the Look and Feel does not provide a color for the requested index. */ @Deprecated( "This can throw exceptions if the LaF does not have a full palette, use grayOrNull() instead", ReplaceWith("grayOrNull(index)"), ) - public fun gray(index: Int): Color = gray[index - 1] + @Suppress("UnsafeCallOnNullableType") + public fun gray(index: Int): Color = grayOrNull(index)!! /** * Retrieves a gray color from the palette by its index, or `null` if the index is out of bounds. * - * Palette indices start at 1; how many entries exist for a color depends on the Look and Feel. Some LaFs may only - * have a partial palette, or none at all. + * Palette indices start at 1 (or 10 for Islands themes); how many entries exist for a color depends on the Look and + * Feel. Some LaFs may only have a partial palette, or none at all. * - * @param index The 1-based index of the color to retrieve. Only values of 1 and above are valid. + * @param index The 1-based (or 10-based for Islands themes) index of the color to retrieve. Only values of 1 and + * above are valid. * @return The [Color] at the specified index, or `null` if the index is out of bounds. */ - public fun grayOrNull(index: Int): Color? = gray.getOrNull(index - 1) + public fun grayOrNull(index: Int): Color? = getByIndexOrNull(gray, index) /** - * Retrieves a blue color from the palette by its index. Note that this function is not safe to use and can throw an - * [IndexOutOfBoundsException] at runtime if the Look and Feel does not provide a full palette. + * Retrieves a blue color from the palette by its index. Note that this function is not safe to use and can throw a + * [NullPointerException] at runtime if the Look and Feel does not provide a color for the requested index. * * You should use [blueOrNull] instead. * @@ -81,28 +112,30 @@ public class ThemeColorPalette( * * @param index The 1-based index of the color to retrieve. * @return The [Color] at the specified index. - * @throws IndexOutOfBoundsException if the index is out of bounds. + * @throws NullPointerException if the Look and Feel does not provide a color for the requested index. */ @Deprecated( "This can throw exceptions if the LaF does not have a full palette, use blueOrNull() instead", ReplaceWith("blueOrNull(index)"), ) - public fun blue(index: Int): Color = blue[index - 1] + @Suppress("UnsafeCallOnNullableType") + public fun blue(index: Int): Color = blueOrNull(index)!! /** * Retrieves a blue color from the palette by its index, or `null` if the index is out of bounds. * - * Palette indices start at 1; how many entries exist for a color depends on the Look and Feel. Some LaFs may only - * have a partial palette, or none at all. + * Palette indices start at 1 (or 10 for Islands themes); how many entries exist for a color depends on the Look and + * Feel. Some LaFs may only have a partial palette, or none at all. * - * @param index The 1-based index of the color to retrieve. Only values of 1 and above are valid. + * @param index The 1-based (or 10-based for Islands themes) index of the color to retrieve. Only values of 1 and + * above are valid. * @return The [Color] at the specified index, or `null` if the index is out of bounds. */ - public fun blueOrNull(index: Int): Color? = blue.getOrNull(index - 1) + public fun blueOrNull(index: Int): Color? = getByIndexOrNull(blue, index) /** - * Retrieves a green color from the palette by its index. Note that this function is not safe to use and can throw - * an [IndexOutOfBoundsException] at runtime if the Look and Feel does not provide a full palette. + * Retrieves a green color from the palette by its index. Note that this function is not safe to use and can throw a + * [NullPointerException] at runtime if the Look and Feel does not provide a color for the requested index. * * You should use [greenOrNull] instead. * @@ -111,28 +144,30 @@ public class ThemeColorPalette( * * @param index The 1-based index of the color to retrieve. * @return The [Color] at the specified index. - * @throws IndexOutOfBoundsException if the index is out of bounds. + * @throws NullPointerException if the Look and Feel does not provide a color for the requested index. */ @Deprecated( "This can throw exceptions if the LaF does not have a full palette, use greenOrNull() instead", ReplaceWith("greenOrNull(index)"), ) - public fun green(index: Int): Color = green[index - 1] + @Suppress("UnsafeCallOnNullableType") + public fun green(index: Int): Color = greenOrNull(index)!! /** * Retrieves a green color from the palette by its index, or `null` if the index is out of bounds. * - * Palette indices start at 1; how many entries exist for a color depends on the Look and Feel. Some LaFs may only - * have a partial palette, or none at all. + * Palette indices start at 1 (or 10 for Islands themes); how many entries exist for a color depends on the Look and + * Feel. Some LaFs may only have a partial palette, or none at all. * - * @param index The 1-based index of the color to retrieve. Only values of 1 and above are valid. + * @param index The 1-based (or 10-based for Islands themes) index of the color to retrieve. Only values of 1 and + * above are valid. * @return The [Color] at the specified index, or `null` if the index is out of bounds. */ - public fun greenOrNull(index: Int): Color? = green.getOrNull(index - 1) + public fun greenOrNull(index: Int): Color? = getByIndexOrNull(green, index) /** - * Retrieves a red color from the palette by its index. Note that this function is not safe to use and can throw an - * [IndexOutOfBoundsException] at runtime if the Look and Feel does not provide a full palette. + * Retrieves a red color from the palette by its index. Note that this function is not safe to use and can throw a + * [NullPointerException] at runtime if the Look and Feel does not provide a color for the requested index. * * You should use [redOrNull] instead. * @@ -141,28 +176,30 @@ public class ThemeColorPalette( * * @param index The 1-based index of the color to retrieve. * @return The [Color] at the specified index. - * @throws IndexOutOfBoundsException if the index is out of bounds. + * @throws NullPointerException if the Look and Feel does not provide a color for the requested index. */ @Deprecated( "This can throw exceptions if the LaF does not have a full palette, use redOrNull() instead", ReplaceWith("redOrNull(index)"), ) - public fun red(index: Int): Color = red[index - 1] + @Suppress("UnsafeCallOnNullableType") + public fun red(index: Int): Color = redOrNull(index)!! /** * Retrieves a red color from the palette by its index, or `null` if the index is out of bounds. * - * Palette indices start at 1; how many entries exist for a color depends on the Look and Feel. Some LaFs may only - * have a partial palette, or none at all. + * Palette indices start at 1 (or 10 for Islands themes); how many entries exist for a color depends on the Look and + * Feel. Some LaFs may only have a partial palette, or none at all. * - * @param index The 1-based index of the color to retrieve. Only values of 1 and above are valid. + * @param index The 1-based (or 10-based for Islands themes) index of the color to retrieve. Only values of 1 and + * above are valid. * @return The [Color] at the specified index, or `null` if the index is out of bounds. */ - public fun redOrNull(index: Int): Color? = red.getOrNull(index - 1) + public fun redOrNull(index: Int): Color? = getByIndexOrNull(red, index) /** * Retrieves a yellow color from the palette by its index. Note that this function is not safe to use and can throw - * an [IndexOutOfBoundsException] at runtime if the Look and Feel does not provide a full palette. + * a [NullPointerException] at runtime if the Look and Feel does not provide a color for the requested index. * * You should use [yellowOrNull] instead. * @@ -171,28 +208,30 @@ public class ThemeColorPalette( * * @param index The 1-based index of the color to retrieve. * @return The [Color] at the specified index. - * @throws IndexOutOfBoundsException if the index is out of bounds. + * @throws NullPointerException if the Look and Feel does not provide a color for the requested index. */ @Deprecated( "This can throw exceptions if the LaF does not have a full palette, use yellowOrNull() instead", ReplaceWith("yellowOrNull(index)"), ) - public fun yellow(index: Int): Color = yellow[index - 1] + @Suppress("UnsafeCallOnNullableType") + public fun yellow(index: Int): Color = yellowOrNull(index)!! /** * Retrieves a yellow color from the palette by its index, or `null` if the index is out of bounds. * - * Palette indices start at 1; how many entries exist for a color depends on the Look and Feel. Some LaFs may only - * have a partial palette, or none at all. + * Palette indices start at 1 (or 10 for Islands themes); how many entries exist for a color depends on the Look and + * Feel. Some LaFs may only have a partial palette, or none at all. * - * @param index The 1-based index of the color to retrieve. Only values of 1 and above are valid. + * @param index The 1-based (or 10-based for Islands themes) index of the color to retrieve. Only values of 1 and + * above are valid. * @return The [Color] at the specified index, or `null` if the index is out of bounds. */ - public fun yellowOrNull(index: Int): Color? = yellow.getOrNull(index - 1) + public fun yellowOrNull(index: Int): Color? = getByIndexOrNull(yellow, index) /** * Retrieves an orange color from the palette by its index. Note that this function is not safe to use and can throw - * an [IndexOutOfBoundsException] at runtime if the Look and Feel does not provide a full palette. + * a [NullPointerException] at runtime if the Look and Feel does not provide a color for the requested index. * * You should use [orangeOrNull] instead. * @@ -201,28 +240,30 @@ public class ThemeColorPalette( * * @param index The 1-based index of the color to retrieve. * @return The [Color] at the specified index. - * @throws IndexOutOfBoundsException if the index is out of bounds. + * @throws NullPointerException if the Look and Feel does not provide a color for the requested index. */ @Deprecated( "This can throw exceptions if the LaF does not have a full palette, use orangeOrNull() instead", ReplaceWith("orangeOrNull(index)"), ) - public fun orange(index: Int): Color = orange[index - 1] + @Suppress("UnsafeCallOnNullableType") + public fun orange(index: Int): Color = orangeOrNull(index)!! /** * Retrieves an orange color from the palette by its index, or `null` if the index is out of bounds. * - * Palette indices start at 1; how many entries exist for a color depends on the Look and Feel. Some LaFs may only - * have a partial palette, or none at all. + * Palette indices start at 1 (or 10 for Islands themes); how many entries exist for a color depends on the Look and + * Feel. Some LaFs may only have a partial palette, or none at all. * - * @param index The 1-based index of the color to retrieve. Only values of 1 and above are valid. + * @param index The 1-based (or 10-based for Islands themes) index of the color to retrieve. Only values of 1 and + * above are valid. * @return The [Color] at the specified index, or `null` if the index is out of bounds. */ - public fun orangeOrNull(index: Int): Color? = orange.getOrNull(index - 1) + public fun orangeOrNull(index: Int): Color? = getByIndexOrNull(orange, index) /** * Retrieves a purple color from the palette by its index. Note that this function is not safe to use and can throw - * an [IndexOutOfBoundsException] at runtime if the Look and Feel does not provide a full palette. + * a [NullPointerException] at runtime if the Look and Feel does not provide a color for the requested index. * * You should use [purpleOrNull] instead. * @@ -231,28 +272,30 @@ public class ThemeColorPalette( * * @param index The 1-based index of the color to retrieve. * @return The [Color] at the specified index. - * @throws IndexOutOfBoundsException if the index is out of bounds. + * @throws NullPointerException if the Look and Feel does not provide a color for the requested index. */ @Deprecated( "This can throw exceptions if the LaF does not have a full palette, use purpleOrNull() instead", ReplaceWith("purpleOrNull(index)"), ) - public fun purple(index: Int): Color = purple[index - 1] + @Suppress("UnsafeCallOnNullableType") + public fun purple(index: Int): Color = purpleOrNull(index)!! /** * Retrieves a purple color from the palette by its index, or `null` if the index is out of bounds. * - * Palette indices start at 1; how many entries exist for a color depends on the Look and Feel. Some LaFs may only - * have a partial palette, or none at all. + * Palette indices start at 1 (or 10 for Islands themes); how many entries exist for a color depends on the Look and + * Feel. Some LaFs may only have a partial palette, or none at all. * - * @param index The 1-based index of the color to retrieve. Only values of 1 and above are valid. + * @param index The 1-based (or 10-based for Islands themes) index of the color to retrieve. Only values of 1 and + * above are valid. * @return The [Color] at the specified index, or `null` if the index is out of bounds. */ - public fun purpleOrNull(index: Int): Color? = purple.getOrNull(index - 1) + public fun purpleOrNull(index: Int): Color? = getByIndexOrNull(purple, index) /** - * Retrieves a teal color from the palette by its index. Note that this function is not safe to use and can throw an - * [IndexOutOfBoundsException] at runtime if the Look and Feel does not provide a full palette. + * Retrieves a teal color from the palette by its index. Note that this function is not safe to use and can throw a + * [NullPointerException] at runtime if the Look and Feel does not provide a color for the requested index. * * You should use [tealOrNull] instead. * @@ -261,33 +304,43 @@ public class ThemeColorPalette( * * @param index The 1-based index of the color to retrieve. * @return The [Color] at the specified index. - * @throws IndexOutOfBoundsException if the index is out of bounds. + * @throws NullPointerException if the Look and Feel does not provide a color for the requested index. */ @Deprecated( "This can throw exceptions if the LaF does not have a full palette, use tealOrNull() instead", ReplaceWith("tealOrNull(index)"), ) - public fun teal(index: Int): Color = teal[index - 1] + @Suppress("UnsafeCallOnNullableType") + public fun teal(index: Int): Color = tealOrNull(index)!! /** * Retrieves a teal color from the palette by its index, or `null` if the index is out of bounds. * - * Palette indices start at 1; how many entries exist for a color depends on the Look and Feel. Some LaFs may only - * have a partial palette, or none at all. + * Palette indices start at 1 (or 10 for Islands themes); how many entries exist for a color depends on the Look and + * Feel. Some LaFs may only have a partial palette, or none at all. * - * @param index The 1-based index of the color to retrieve. Only values of 1 and above are valid. + * @param index The 1-based (or 10-based for Islands themes) index of the color to retrieve. Only values of 1 and + * above are valid. * @return The [Color] at the specified index, or `null` if the index is out of bounds. */ - public fun tealOrNull(index: Int): Color? = teal.getOrNull(index - 1) + public fun tealOrNull(index: Int): Color? = getByIndexOrNull(teal, index) + + private fun getByIndexOrNull(list: List, index: Int): Color? = + if (isIslands) { + if (index % 10 != 0) null else list.getOrNull(index / 10 - 1) + } else { + list.getOrNull(index - 1) + } /** - * Looks up a color in the palette by its key. The key can be in the format "colorNameN" (e.g., "gray1", "blue12") - * or a raw key from the Look and Feel theme. + * Looks up a color in the palette by its key. The key can be in the format "colorNameN" (e.g., "gray1", "blue12"), + * "colorName-N" for Islands themes (e.g., "gray-10", "blue-120"), or a raw key from the Look and Feel theme. * * @param colorKey The key of the color to look up. * @return The [Color] associated with the key, or `null` if the key is not found. */ public fun lookup(colorKey: String): Color? { + val colorKeyRegex = if (isIslands) islandsColorKeyRegex else nonIslandsColorKeyRegex val result = colorKeyRegex.matchEntire(colorKey.trim()) val colorGroup = result?.groupValues?.getOrNull(1)?.lowercase() val colorIndex = result?.groupValues?.getOrNull(2)?.toIntOrNull() @@ -325,6 +378,7 @@ public class ThemeColorPalette( if (purple != other.purple) return false if (teal != other.teal) return false if (rawMap != other.rawMap) return false + if (isIslands != other.isIslands) return false return true } @@ -339,6 +393,7 @@ public class ThemeColorPalette( result = 31 * result + purple.hashCode() result = 31 * result + teal.hashCode() result = 31 * result + rawMap.hashCode() + result = 31 * result + isIslands.hashCode() return result } @@ -352,7 +407,8 @@ public class ThemeColorPalette( "orange=$orange, " + "purple=$purple, " + "teal=$teal, " + - "rawMap=$rawMap" + + "rawMap=$rawMap, " + + "isIslands=$isIslands" + ")" } @@ -368,6 +424,7 @@ public class ThemeColorPalette( purple = emptyList(), teal = emptyList(), rawMap = emptyMap(), + isIslands = false, ) } } diff --git a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/clipboard/JewelBridgeClipboard.kt b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/clipboard/JewelBridgeClipboard.kt index 000be8857f16..2b0c43aeff2e 100644 --- a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/clipboard/JewelBridgeClipboard.kt +++ b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/clipboard/JewelBridgeClipboard.kt @@ -39,6 +39,7 @@ internal class JewelBridgeClipboard : Clipboard { // itself. Hence, we build a facade Clipboard that delegates to the IntelliJ CopyPasteManager. override val nativeClipboard: NativeClipboard by lazy { JewelAwtClipboardBridge(copyPasteManager) } + @Suppress("UnreachableCode") override suspend fun getClipEntry(): ClipEntry? { logger.debug("getClipEntry called. CopyPasteManager available: ${copyPasteManager != null}") diff --git a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/BridgeThemeColorPalette.kt b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/BridgeThemeColorPalette.kt index 6f452fd6167c..51d05563f844 100644 --- a/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/BridgeThemeColorPalette.kt +++ b/platform/jewel/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/BridgeThemeColorPalette.kt @@ -3,24 +3,27 @@ package org.jetbrains.jewel.bridge.theme import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.isSpecified import com.intellij.openapi.diagnostic.Logger +import com.intellij.ui.IslandsState import java.util.TreeMap +import javax.swing.UIDefaults import org.jetbrains.jewel.bridge.toComposeColor import org.jetbrains.jewel.foundation.theme.ThemeColorPalette -private val logger = Logger.getInstance("BridgeThemeColorPalette") +private val logger: Logger = Logger.getInstance("BridgeThemeColorPalette") public val ThemeColorPalette.windowsPopupBorder: Color? get() = lookup("windowsPopupBorder") public fun ThemeColorPalette.Companion.readFromLaF(): ThemeColorPalette { - val gray = readPaletteColors("Gray") - 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 isIslands = IslandsState.isEnabled() + val gray = readPaletteColors("Gray", isIslands) + val blue = readPaletteColors("Blue", isIslands) + val green = readPaletteColors("Green", isIslands) + val red = readPaletteColors("Red", isIslands) + val yellow = readPaletteColors("Yellow", isIslands) + val orange = readPaletteColors("Orange", isIslands) + val purple = readPaletteColors("Purple", isIslands) + val teal = readPaletteColors("Teal", isIslands) val windowsPopupBorder = readPaletteColor("windowsPopupBorder") val rawMap = buildMap { @@ -45,28 +48,36 @@ public fun ThemeColorPalette.Companion.readFromLaF(): ThemeColorPalette { purple = purple.values.toList(), teal = teal.values.toList(), rawMap = rawMap, + isIslands = isIslands, ) } -private fun readPaletteColors(colorName: String): Map { - val defaults = uiDefaults - val allKeys = defaults.keys - val colorNameKeyPrefix = "ColorPalette.$colorName" +private fun readPaletteColors(colorName: String, isIslands: Boolean): Map { + val defaults: UIDefaults = uiDefaults + val allKeys: Set = defaults.keys + val colorNameKeyPrefix = if (isIslands) "ColorPalette.${colorName.lowercase()}-" else "ColorPalette.$colorName" val colorNameKeyPrefixLength = colorNameKeyPrefix.length val lastColorIndex = allKeys - .asSequence() - .filterIsInstance(String::class.java) + .filterIsInstance() .filter { it.startsWith(colorNameKeyPrefix) } .mapNotNull { val afterName = it.substring(colorNameKeyPrefixLength) afterName.toIntOrNull() } - .maxOrNull() ?: return TreeMap() + .maxOrNull() + if (lastColorIndex == null) return TreeMap() + + val indices = + if (isIslands) { + (10..lastColorIndex step 10) + } else { + (1..lastColorIndex) + } return buildMap { - for (i in 1..lastColorIndex) { + for (i in indices) { val key = "$colorNameKeyPrefix$i" val value = defaults[key] as? java.awt.Color if (value == null) { @@ -80,7 +91,7 @@ private fun readPaletteColors(colorName: String): Map { } private fun readPaletteColor(colorName: String): Color { - val defaults = uiDefaults + val defaults: UIDefaults = uiDefaults val colorNameKey = "ColorPalette.$colorName" return (defaults[colorNameKey] as? java.awt.Color)?.toComposeColor() ?: Color.Unspecified } diff --git a/platform/jewel/ide-laf-bridge/src/test/kotlin/org/jetbrains/jewel/bridge/theme/BridgeThemeColorPaletteTest.kt b/platform/jewel/ide-laf-bridge/src/test/kotlin/org/jetbrains/jewel/bridge/theme/BridgeThemeColorPaletteTest.kt new file mode 100644 index 000000000000..afd143c87f45 --- /dev/null +++ b/platform/jewel/ide-laf-bridge/src/test/kotlin/org/jetbrains/jewel/bridge/theme/BridgeThemeColorPaletteTest.kt @@ -0,0 +1,364 @@ +// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. +package org.jetbrains.jewel.bridge.theme + +import com.intellij.ui.IslandsState +import java.awt.Color +import javax.swing.UIManager +import kotlin.collections.set +import org.jetbrains.jewel.bridge.toComposeColor +import org.jetbrains.jewel.foundation.theme.ThemeColorPalette +import org.junit.Assert.assertTrue +import org.junit.Test + +internal class BridgeThemeColorPaletteTest { + private val colorCount = 14 + + @Test + fun `should save non-islands gray colors to ThemeColorPalette`() { + val colorName = "Gray" + val expectedColors = createNonIslandThemeColors(colorName, colorCount) + + withUiDefaults(expectedColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 1..colorCount) { + val expected = expectedColors.getValue("ColorPalette.$colorName$i").toComposeColor() + val actual = palette.grayOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + + @Test + fun `should save islands gray colors to ThemeColorPalette`() { + val colorName = "Gray" + val expectedIslandColors = createIslandThemeColors(colorName, colorCount) + + inIslandsTheme { + withUiDefaults(expectedIslandColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 10..colorCount * 10 step 10) { + val expected = + expectedIslandColors.getValue("ColorPalette.${colorName.lowercase()}-$i").toComposeColor() + val actual = palette.grayOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + } + + @Test + fun `should save blue colors to ThemeColorPalette`() { + val colorName = "Blue" + val expectedColors = createNonIslandThemeColors(colorName, colorCount) + + withUiDefaults(expectedColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 1..colorCount) { + val expected = expectedColors.getValue("ColorPalette.$colorName$i").toComposeColor() + val actual = palette.blueOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + + @Test + fun `should save islands blue colors to ThemeColorPalette`() { + val colorName = "Blue" + val expectedIslandColors = createIslandThemeColors(colorName, colorCount) + + inIslandsTheme { + withUiDefaults(expectedIslandColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 10..colorCount * 10 step 10) { + val expected = + expectedIslandColors.getValue("ColorPalette.${colorName.lowercase()}-$i").toComposeColor() + val actual = palette.blueOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + } + + @Test + fun `should save green colors to ThemeColorPalette`() { + val colorName = "Green" + val expectedColors = createNonIslandThemeColors(colorName, colorCount) + + withUiDefaults(expectedColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 1..colorCount) { + val expected = expectedColors.getValue("ColorPalette.$colorName$i").toComposeColor() + val actual = palette.greenOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + + @Test + fun `should save islands green colors to ThemeColorPalette`() { + val colorName = "Green" + val expectedIslandColors = createIslandThemeColors(colorName, colorCount) + + inIslandsTheme { + withUiDefaults(expectedIslandColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 10..colorCount * 10 step 10) { + val expected = + expectedIslandColors.getValue("ColorPalette.${colorName.lowercase()}-$i").toComposeColor() + val actual = palette.greenOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + } + + @Test + fun `should save red colors to ThemeColorPalette`() { + val colorName = "Red" + val expectedColors = createNonIslandThemeColors(colorName, colorCount) + + withUiDefaults(expectedColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 1..colorCount) { + val expected = expectedColors.getValue("ColorPalette.$colorName$i").toComposeColor() + val actual = palette.redOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + + @Test + fun `should save islands red colors to ThemeColorPalette`() { + val colorName = "Red" + val expectedIslandColors = createIslandThemeColors(colorName, colorCount) + + inIslandsTheme { + withUiDefaults(expectedIslandColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 10..colorCount * 10 step 10) { + val expected = + expectedIslandColors.getValue("ColorPalette.${colorName.lowercase()}-$i").toComposeColor() + val actual = palette.redOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + } + + @Test + fun `should save yellow colors to ThemeColorPalette`() { + val colorName = "Yellow" + val expectedColors = createNonIslandThemeColors(colorName, colorCount) + + withUiDefaults(expectedColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 1..colorCount) { + val expected = expectedColors.getValue("ColorPalette.$colorName$i").toComposeColor() + val actual = palette.yellowOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + + @Test + fun `should save islands yellow colors to ThemeColorPalette`() { + val colorName = "Yellow" + val expectedIslandColors = createIslandThemeColors(colorName, colorCount) + + inIslandsTheme { + withUiDefaults(expectedIslandColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 10..colorCount * 10 step 10) { + val expected = + expectedIslandColors.getValue("ColorPalette.${colorName.lowercase()}-$i").toComposeColor() + val actual = palette.yellowOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + } + + @Test + fun `should save orange colors to ThemeColorPalette`() { + val colorName = "Orange" + val expectedColors = createNonIslandThemeColors(colorName, colorCount) + + withUiDefaults(expectedColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 1..colorCount) { + val expected = expectedColors.getValue("ColorPalette.$colorName$i").toComposeColor() + val actual = palette.orangeOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + + @Test + fun `should save islands orange colors to ThemeColorPalette`() { + val colorName = "Orange" + val expectedIslandColors = createIslandThemeColors(colorName, colorCount) + + inIslandsTheme { + withUiDefaults(expectedIslandColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 10..colorCount * 10 step 10) { + val expected = + expectedIslandColors.getValue("ColorPalette.${colorName.lowercase()}-$i").toComposeColor() + val actual = palette.orangeOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + } + + @Test + fun `should save purple colors to ThemeColorPalette`() { + val colorName = "Purple" + val expectedColors = createNonIslandThemeColors(colorName, colorCount) + + withUiDefaults(expectedColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 1..colorCount) { + val expected = expectedColors.getValue("ColorPalette.$colorName$i").toComposeColor() + val actual = palette.purpleOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + + @Test + fun `should save islands purple colors to ThemeColorPalette`() { + val colorName = "Purple" + val expectedIslandColors = createIslandThemeColors(colorName, colorCount) + + inIslandsTheme { + withUiDefaults(expectedIslandColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 10..colorCount * 10 step 10) { + val expected = + expectedIslandColors.getValue("ColorPalette.${colorName.lowercase()}-$i").toComposeColor() + val actual = palette.purpleOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + } + + @Test + fun `should save teal colors to ThemeColorPalette`() { + val colorName = "Teal" + val expectedColors = createNonIslandThemeColors(colorName, colorCount) + + withUiDefaults(expectedColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 1..colorCount) { + val expected = expectedColors.getValue("ColorPalette.$colorName$i").toComposeColor() + val actual = palette.tealOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + + @Test + fun `should save islands teal colors to ThemeColorPalette`() { + val colorName = "Teal" + val expectedIslandColors = createIslandThemeColors(colorName, colorCount) + + inIslandsTheme { + withUiDefaults(expectedIslandColors) { + val palette = ThemeColorPalette.readFromLaF() + for (i in 10..colorCount * 10 step 10) { + val expected = + expectedIslandColors.getValue("ColorPalette.${colorName.lowercase()}-$i").toComposeColor() + val actual = palette.tealOrNull(i) + assertColorEquals("Color $colorName$i is not correct in the palette", expected, actual) + } + } + } + } + + @Test + fun `should save windows popup border color to ThemeColorPalette`() { + val windowsPopupBorder = Color(100, 100, 100) + + withUiDefaults(mapOf("ColorPalette.windowsPopupBorder" to windowsPopupBorder)) { + val palette = ThemeColorPalette.readFromLaF() + assertColorEquals( + "Windows popup border is not correct in the palette", + windowsPopupBorder.toComposeColor(), + palette.windowsPopupBorder, + ) + } + } + + private fun assertColorEquals( + message: String, + expected: androidx.compose.ui.graphics.Color, + actual: androidx.compose.ui.graphics.Color?, + ) { + assertTrue(message, actual != null && expected.value == actual.value) + } + + private fun createNonIslandThemeColors(colorName: String, count: Int): Map { + val expectedColors = mutableMapOf() + for (i in 1..count) { + val color = + Color( + (colorName.hashCode() and 0xFF0000) shr 16, + (colorName.hashCode() and 0x00FF00) shr 8, + (i * 10) % 256, + ) + val key = "ColorPalette.$colorName$i" + expectedColors[key] = color + } + + return expectedColors + } + + private fun createIslandThemeColors(colorName: String, count: Int): Map { + val expectedColors = mutableMapOf() + for (i in 10..count * 10 step 10) { + val color = + Color( + (colorName.hashCode() and 0xFF0000) shr 16, + (colorName.hashCode() and 0x00FF00) shr 8, + (i * 10) % 256, + ) + val key = "ColorPalette.${colorName.lowercase()}-$i" + expectedColors[key] = color + } + + return expectedColors + } + + private fun withUiDefaults(entries: Map, block: () -> Unit) { + val defaults = UIManager.getDefaults() + val existingEntries = entries.keys.associateWith { defaults.containsKey(it) } + val previousEntries = entries.keys.associateWith { defaults[it] } + + entries.forEach { (k, v) -> defaults[k] = v } + try { + block() + } finally { + entries.keys.forEach { key -> + if (existingEntries.getValue(key)) { + defaults[key] = previousEntries[key] + } else { + defaults.remove(key) + } + } + } + } + + private fun inIslandsTheme(block: () -> Unit) { + val wasEnabled = IslandsState.isEnabled() + val wasCustomEnabled = IslandsState.isCustomEnabled() + + IslandsState.setEnabled(true, false) + try { + block() + } finally { + IslandsState.setEnabled(wasEnabled, wasCustomEnabled) + } + } +} diff --git a/platform/jewel/int-ui/int-ui-standalone/generated/theme/org/jetbrains/jewel/intui/core/theme/IntUiDarkTheme.kt b/platform/jewel/int-ui/int-ui-standalone/generated/theme/org/jetbrains/jewel/intui/core/theme/IntUiDarkTheme.kt index 7af3fdfa1f9f..d9aafdcf083b 100644 --- a/platform/jewel/int-ui/int-ui-standalone/generated/theme/org/jetbrains/jewel/intui/core/theme/IntUiDarkTheme.kt +++ b/platform/jewel/int-ui/int-ui-standalone/generated/theme/org/jetbrains/jewel/intui/core/theme/IntUiDarkTheme.kt @@ -238,6 +238,7 @@ public object IntUiDarkTheme : ThemeDescriptor { "Teal11" to Color(0xFF9BDDD6), "Teal12" to Color(0xFFB9EBE6), ), + isIslands = false, ) override val iconData: ThemeIconData = diff --git a/platform/jewel/int-ui/int-ui-standalone/generated/theme/org/jetbrains/jewel/intui/core/theme/IntUiLightTheme.kt b/platform/jewel/int-ui/int-ui-standalone/generated/theme/org/jetbrains/jewel/intui/core/theme/IntUiLightTheme.kt index 18f59d288feb..71a9eb2dd9ce 100644 --- a/platform/jewel/int-ui/int-ui-standalone/generated/theme/org/jetbrains/jewel/intui/core/theme/IntUiLightTheme.kt +++ b/platform/jewel/int-ui/int-ui-standalone/generated/theme/org/jetbrains/jewel/intui/core/theme/IntUiLightTheme.kt @@ -221,6 +221,7 @@ public object IntUiLightTheme : ThemeDescriptor { "Purple9" to Color(0xFFF5EDFF), "Purple10" to Color(0xFFFAF5FF), ), + isIslands = false, ) override val iconData: ThemeIconData =