[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
This commit is contained in:
daria.suvorova
2026-07-17 17:01:41 +00:00
committed by intellij-monorepo-bot
parent 0219cef44c
commit 48ad97be3a
10 changed files with 524 additions and 88 deletions
@@ -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()
)
}
+2 -1
View File
@@ -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
- <init>(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:<init>(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
- <init>(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
@@ -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<androidx.compose.ui.graphics.Color> gray, java.util.List<androidx.compose.ui.graphics.Color> blue, java.util.List<androidx.compose.ui.graphics.Color> green, java.util.List<androidx.compose.ui.graphics.Color> red, java.util.List<androidx.compose.ui.graphics.Color> yellow, java.util.List<androidx.compose.ui.graphics.Color> orange, java.util.List<androidx.compose.ui.graphics.Color> purple, java.util.List<androidx.compose.ui.graphics.Color> teal, java.util.Map<java.lang.String,androidx.compose.ui.graphics.Color> rawMap);
ctor public ThemeColorPalette(java.util.List<androidx.compose.ui.graphics.Color> gray, java.util.List<androidx.compose.ui.graphics.Color> blue, java.util.List<androidx.compose.ui.graphics.Color> green, java.util.List<androidx.compose.ui.graphics.Color> red, java.util.List<androidx.compose.ui.graphics.Color> yellow, java.util.List<androidx.compose.ui.graphics.Color> orange, java.util.List<androidx.compose.ui.graphics.Color> purple, java.util.List<androidx.compose.ui.graphics.Color> teal, java.util.Map<java.lang.String,androidx.compose.ui.graphics.Color> 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<androidx.compose.ui.graphics.Color> getBlue();
@@ -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<androidx.compose.ui.graphics.Color> gray, java.util.List<androidx.compose.ui.graphics.Color> blue, java.util.List<androidx.compose.ui.graphics.Color> green, java.util.List<androidx.compose.ui.graphics.Color> red, java.util.List<androidx.compose.ui.graphics.Color> yellow, java.util.List<androidx.compose.ui.graphics.Color> orange, java.util.List<androidx.compose.ui.graphics.Color> purple, java.util.List<androidx.compose.ui.graphics.Color> teal, java.util.Map<java.lang.String,androidx.compose.ui.graphics.Color> rawMap);
ctor public ThemeColorPalette(java.util.List<androidx.compose.ui.graphics.Color> gray, java.util.List<androidx.compose.ui.graphics.Color> blue, java.util.List<androidx.compose.ui.graphics.Color> green, java.util.List<androidx.compose.ui.graphics.Color> red, java.util.List<androidx.compose.ui.graphics.Color> yellow, java.util.List<androidx.compose.ui.graphics.Color> orange, java.util.List<androidx.compose.ui.graphics.Color> purple, java.util.List<androidx.compose.ui.graphics.Color> teal, java.util.Map<java.lang.String,androidx.compose.ui.graphics.Color> 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<androidx.compose.ui.graphics.Color> getBlue();
@@ -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<Color>,
public val teal: List<Color>,
public val rawMap: Map<String, Color>,
private val isIslands: Boolean,
) {
@Suppress("DEPRECATION")
@Deprecated("Use the constructor with isIslands parameter", level = DeprecationLevel.HIDDEN)
public constructor(
gray: List<Color>,
blue: List<Color>,
green: List<Color>,
red: List<Color>,
yellow: List<Color>,
orange: List<Color>,
purple: List<Color>,
teal: List<Color>,
rawMap: Map<String, Color>,
) : 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<Color>, 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,
)
}
}
@@ -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}")
@@ -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<String, Color> {
val defaults = uiDefaults
val allKeys = defaults.keys
val colorNameKeyPrefix = "ColorPalette.$colorName"
private fun readPaletteColors(colorName: String, isIslands: Boolean): Map<String, Color> {
val defaults: UIDefaults = uiDefaults
val allKeys: Set<Any> = 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<String>()
.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<String, Color> {
}
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
}
@@ -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<String, Color> {
val expectedColors = mutableMapOf<String, Color>()
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<String, Color> {
val expectedColors = mutableMapOf<String, Color>()
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<String, Any>, 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)
}
}
}
@@ -238,6 +238,7 @@ public object IntUiDarkTheme : ThemeDescriptor {
"Teal11" to Color(0xFF9BDDD6),
"Teal12" to Color(0xFFB9EBE6),
),
isIslands = false,
)
override val iconData: ThemeIconData =
@@ -221,6 +221,7 @@ public object IntUiLightTheme : ThemeDescriptor {
"Purple9" to Color(0xFFF5EDFF),
"Purple10" to Color(0xFFFAF5FF),
),
isIslands = false,
)
override val iconData: ThemeIconData =