#291 — Fix icons patching and theming issues in 241 (#293)

Fix icon loading and errors in bridge for 241
GitOrigin-RevId: 4df0132cdadf2d8297fd0fa57573f5630c481ddf
This commit is contained in:
Sebastiano Poggi
2024-02-08 11:42:36 +01:00
committed by intellij-monorepo-bot
parent 719c21c778
commit 372242e6b5
63 changed files with 1067 additions and 402 deletions

View File

@@ -20,6 +20,7 @@ indent_size = 2
ktlint_function_signature_body_expression_wrapping = multiline
ktlint_ignore_back_ticked_identifier = true
ij_kotlin_allow_trailing_comma = true
ktlint_function_naming_ignore_when_annotated_with = Composable
[gradlew.bat]
end_of_line = crlf

View File

@@ -43,7 +43,7 @@ jobs:
- name: Run :check task
run: ./gradlew check --continue
- uses: github/codeql-action/upload-sarif@v2
- uses: github/codeql-action/upload-sarif@v3
if: ${{ always() }}
with:
sarif_file: ${{ github.workspace }}/build/reports/static-analysis.sarif

View File

@@ -1,12 +0,0 @@
import org.gradle.api.Project
import java.util.Properties
internal fun Project.localProperty(propertyName: String): String? {
val localPropertiesFile = rootProject.file("local.properties")
if (!localPropertiesFile.exists()) {
return null
}
val properties = Properties()
localPropertiesFile.inputStream().use { properties.load(it) }
return properties.getProperty(propertyName)
}

View File

@@ -65,7 +65,7 @@ tasks {
formatKotlinMain { exclude { it.file.absolutePath.contains("build/generated") } }
lintKotlinMain {
exclude { it.file.absolutePath.contains("build/generated") }
exclude { it.file.absolutePath.replace('\\', '/').contains("build/generated") }
reports = provider {
mapOf(

View File

@@ -17,7 +17,7 @@ import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.property
import org.gradle.kotlin.dsl.setProperty
import java.io.File
import java.net.URL
import java.net.URI
open class StudioVersionsGenerationExtension(project: Project) {
@@ -69,7 +69,7 @@ open class AndroidStudioReleasesGeneratorTask : DefaultTask() {
"Registered resources directories:\n" +
lookupDirs.joinToString("\n") { " * ${it.absolutePath}" }
)
val releases = URL(url).openStream()
val releases = URI.create(url).toURL().openStream()
.use { json.decodeFromStream<ApiAndroidStudioReleases>(it) }
val className = ClassName.bestGuess(STUDIO_RELEASES_OUTPUT_CLASS_NAME)

View File

@@ -6,7 +6,7 @@ import org.gradle.api.DefaultTask
import org.gradle.api.GradleException
import org.gradle.api.tasks.TaskAction
import java.io.IOException
import java.net.URL
import java.net.URI
open class CheckIdeaVersionTask : DefaultTask() {
@@ -34,7 +34,7 @@ open class CheckIdeaVersionTask : DefaultTask() {
logger.lifecycle("Fetching IntelliJ Platform releases from $releasesUrl...")
val icReleases =
try {
URL(releasesUrl)
URI.create(releasesUrl).toURL()
.openStream()
.use { json.decodeFromStream<List<ApiIdeaReleasesItem>>(it) }
.first()

View File

@@ -14,7 +14,7 @@ import org.gradle.api.tasks.Input
import org.gradle.api.tasks.OutputFile
import org.gradle.api.tasks.TaskAction
import org.gradle.kotlin.dsl.property
import java.net.URL
import java.net.URI
class ThemeGeneratorContainer(container: NamedDomainObjectContainer<ThemeGeneration>) :
NamedDomainObjectContainer<ThemeGeneration> by container
@@ -59,7 +59,7 @@ open class IntelliJThemeGeneratorTask : DefaultTask() {
}
logger.lifecycle("Fetching theme descriptor from $url...")
val themeDescriptor = URL(url).openStream()
val themeDescriptor = URI.create(url).toURL().openStream()
.use { json.decodeFromStream<IntellijThemeDescriptor>(it) }
val className = ClassName.bestGuess(themeClassName.get())

View File

@@ -2,7 +2,7 @@
composeDesktop = "1.6.0-dev1397"
detekt = "1.23.4"
dokka = "1.8.20"
idea = "241.9959.31-EAP-SNAPSHOT"
idea = "241.10840.26-EAP-SNAPSHOT"
ideaGradlePlugin = "1.17.0"
jna = "5.14.0"
kotlin = "1.8.21"

View File

@@ -2,7 +2,7 @@ public final class org/jetbrains/jewel/bridge/BridgeIconDataKt {
public static final fun readFromLaF (Lorg/jetbrains/jewel/foundation/theme/ThemeIconData$Companion;)Lorg/jetbrains/jewel/foundation/theme/ThemeIconData;
}
public final class org/jetbrains/jewel/bridge/BridgePainterHintsProvider : org/jetbrains/jewel/ui/painter/BasePainterHintsProvider {
public final class org/jetbrains/jewel/bridge/BridgePainterHintsProvider : org/jetbrains/jewel/ui/painter/PalettePainterHintsProvider {
public static final field $stable I
public static final field Companion Lorg/jetbrains/jewel/bridge/BridgePainterHintsProvider$Companion;
public synthetic fun <init> (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -10,7 +10,7 @@ public final class org/jetbrains/jewel/bridge/BridgePainterHintsProvider : org/j
}
public final class org/jetbrains/jewel/bridge/BridgePainterHintsProvider$Companion {
public final fun invoke (Z)Lorg/jetbrains/jewel/ui/painter/BasePainterHintsProvider;
public final fun invoke (Z)Lorg/jetbrains/jewel/ui/painter/PalettePainterHintsProvider;
}
public final class org/jetbrains/jewel/bridge/BridgeResourceResolverKt {

View File

@@ -7,6 +7,11 @@ import org.jetbrains.jewel.ui.painter.PainterPathHint
import org.jetbrains.jewel.ui.painter.PainterProviderScope
import org.jetbrains.jewel.ui.painter.ResourcePainterProviderScope
/**
* A [PainterPathHint] that implements the
* [New UI Icon Mapping](https://plugins.jetbrains.com/docs/intellij/icons.html#mapping-entries)
* by delegating to the IntelliJ Platform.
*/
internal object BridgeOverride : PainterPathHint {
private val dirProvider = DirProvider()
@@ -28,7 +33,8 @@ internal object BridgeOverride : PainterPathHint {
// 233 EAP 4 broke path patching horribly; now it can return a
// "reflective path", which is a FQN to an ExpUIIcons entry.
// As a (hopefully) temporary solution, we undo this transformation
// back into the original path.
// back into the original path. The initial transform is lossy, and
// this attempt might fail.
if (patchedPath?.startsWith("com.intellij.icons.ExpUiIcons") == true) {
return inferActualPathFromReflectivePath(patchedPath)
}

View File

@@ -4,35 +4,121 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import com.intellij.ide.ui.UITheme
import com.intellij.openapi.diagnostic.thisLogger
import com.intellij.ui.NewUI
import org.jetbrains.jewel.bridge.theme.isNewUiTheme
import org.jetbrains.jewel.foundation.InternalJewelApi
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.painter.BasePainterHintsProvider
import org.jetbrains.jewel.ui.painter.PainterHint
import org.jetbrains.jewel.ui.painter.PalettePainterHintsProvider
import org.jetbrains.jewel.ui.painter.hints.ColorBasedPaletteReplacement
import org.jetbrains.jewel.ui.painter.hints.Dark
import org.jetbrains.jewel.ui.painter.hints.HiDpi
import org.jetbrains.jewel.ui.painter.hints.KeyBasedPaletteReplacement
import org.jetbrains.jewel.ui.util.inDebugMode
import org.jetbrains.jewel.ui.util.toRgbaHexString
/**
* Provide the default [PainterHint]s to use in the IDE.
*
* This is an internal Jewel API and should not be used directly.
*/
@InternalJewelApi
public class BridgePainterHintsProvider private constructor(
isDark: Boolean,
intellijIconPalette: Map<String, String?> = emptyMap(),
themeIconPalette: Map<String, String?> = emptyMap(),
themeColorPalette: Map<String, Color?> = emptyMap(),
) : BasePainterHintsProvider(isDark, intellijIconPalette, themeIconPalette, themeColorPalette) {
) : PalettePainterHintsProvider(isDark, intellijIconPalette, themeIconPalette, themeColorPalette) {
override val checkBoxByColorPaletteHint: PainterHint
override val checkBoxByKeyPaletteHint: PainterHint
override val treePaletteHint: PainterHint
override val uiPaletteHint: PainterHint
init {
val ui = mutableMapOf<Color, Color>()
val checkBoxesByColor = mutableMapOf<Color, Color>()
val checkBoxesByKey = mutableMapOf<String, Color>()
val trees = mutableMapOf<Color, Color>()
@Suppress("LoopWithTooManyJumpStatements")
for ((key, value) in themeIconPalette) {
if (value == null) continue
// Checkbox (and radio button) entries work differently: the ID field
// for each element that needs patching has a "[fillKey]_[strokeKey]"
// format, starting from IJP 241. This is only enabled for the New UI.
if (key.startsWith("Checkbox.") && NewUI.isEnabled()) {
registerIdBasedReplacement(checkBoxesByKey, key, value)
}
val map = selectMap(key, checkBoxesByColor, trees, ui) ?: continue
registerColorBasedReplacement(map, key, value)
}
checkBoxByKeyPaletteHint = KeyBasedPaletteReplacement(checkBoxesByKey)
checkBoxByColorPaletteHint = ColorBasedPaletteReplacement(checkBoxesByColor)
treePaletteHint = ColorBasedPaletteReplacement(trees)
uiPaletteHint = ColorBasedPaletteReplacement(ui)
}
private fun registerColorBasedReplacement(
map: MutableMap<Color, Color>,
key: String,
value: String,
) {
// If either the key or the resolved value aren't valid colors, ignore the entry
val keyAsColor = resolveKeyColor(key, intellijIconPalette, isDark) ?: return
val resolvedColor = resolveColor(value) ?: return
// Save the new entry (oldColor -> newColor) in the map
map[keyAsColor] = resolvedColor
}
private fun registerIdBasedReplacement(
map: MutableMap<String, Color>,
key: String,
value: String,
) {
val adjustedKey = if (isDark) key.removeSuffix(".Dark") else key
if (adjustedKey !in supportedCheckboxKeys) {
if (inDebugMode) {
logger.warn("${if (isDark) "Dark" else "Light"} theme: color key $key is not supported, will be ignored")
}
return
}
if (adjustedKey != key && inDebugMode) {
logger.warn("${if (isDark) "Dark" else "Light"} theme: color key $key is deprecated, use $adjustedKey instead")
}
val parsedValue = resolveColor(value)
if (parsedValue == null) {
if (inDebugMode) {
logger.warn("${if (isDark) "Dark" else "Light"} theme: color key $key has invalid value: '$value'")
}
return
}
map[adjustedKey] = parsedValue
}
@Composable
override fun hints(path: String): List<PainterHint> = buildList {
add(getPaletteHint(path))
add(BridgeOverride)
add(HiDpi())
add(Dark(JewelTheme.isDark))
}
override fun hints(path: String): List<PainterHint> =
buildList {
add(BridgeOverride)
add(getPaletteHint(path, isNewUi = isNewUiTheme()))
add(HiDpi())
add(Dark(JewelTheme.isDark))
}
public companion object {
private val logger = thisLogger()
@Suppress("UnstableApiUsage") // We need to call @Internal APIs
public operator fun invoke(isDark: Boolean): BasePainterHintsProvider {
public operator fun invoke(isDark: Boolean): PalettePainterHintsProvider {
val uiTheme = currentUiThemeOrNull() ?: return BridgePainterHintsProvider(isDark)
logger.info("Parsing theme info from theme ${uiTheme.name} (id: ${uiTheme.id}, isDark: ${uiTheme.isDark})")
@@ -41,13 +127,32 @@ public class BridgePainterHintsProvider private constructor(
(bean.colorPalette as Map<String, Any?>).mapValues {
when (val value = it.value) {
is String -> value
is java.awt.Color -> value.toRgbaHexString()
else -> null
}
}
val keyPalette = UITheme.getColorPalette()
val themeColors = bean.colors.mapValues { (_, v) -> Color(v) }
return BridgePainterHintsProvider(isDark, keyPalette, iconColorPalette, themeColors)
return BridgePainterHintsProvider(
isDark = isDark,
intellijIconPalette = keyPalette,
themeIconPalette = iconColorPalette,
themeColorPalette = themeColors,
)
}
private val supportedCheckboxKeys: Set<String> =
setOf(
"Checkbox.Background.Default",
"Checkbox.Border.Default",
"Checkbox.Foreground.Selected",
"Checkbox.Background.Selected",
"Checkbox.Border.Selected",
"Checkbox.Focus.Wide",
"Checkbox.Foreground.Disabled",
"Checkbox.Background.Disabled",
"Checkbox.Border.Disabled",
)
}
}

View File

@@ -7,15 +7,18 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.isSpecified
import androidx.compose.ui.unit.takeOrElse
import com.intellij.ide.ui.LafManager
import com.intellij.ide.ui.laf.darcula.DarculaUIUtil
import com.intellij.ide.ui.laf.darcula.ui.DarculaCheckBoxUI
import com.intellij.ide.ui.laf.intellij.IdeaPopupMenuUI
import com.intellij.openapi.diagnostic.Logger
import com.intellij.ui.JBColor
import com.intellij.ui.NewUI
import com.intellij.util.ui.DirProvider
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.NamedColorUtil
@@ -222,12 +225,13 @@ private fun readDefaultButtonStyle(): ButtonStyle {
borderHovered = normalBorder,
)
val minimumSize = JBUI.CurrentTheme.Button.minimumSize()
return ButtonStyle(
colors = colors,
metrics = ButtonMetrics(
cornerSize = retrieveArcAsCornerSizeWithFallbacks("Button.default.arc", "Button.arc"),
padding = PaddingValues(horizontal = 14.dp), // see DarculaButtonUI.HORIZONTAL_PADDING
minSize = DpSize(DarculaUIUtil.MINIMUM_WIDTH.dp, DarculaUIUtil.MINIMUM_HEIGHT.dp),
minSize = DpSize(minimumSize.width.dp, minimumSize.height.dp),
borderWidth = DarculaUIUtil.LW.dp,
),
)
@@ -264,13 +268,14 @@ private fun readOutlinedButtonStyle(): ButtonStyle {
borderHovered = normalBorder,
)
val minimumSize = JBUI.CurrentTheme.Button.minimumSize()
return ButtonStyle(
colors = colors,
metrics =
ButtonMetrics(
cornerSize = CornerSize(DarculaUIUtil.BUTTON_ARC.dp / 2),
padding = PaddingValues(horizontal = 14.dp), // see DarculaButtonUI.HORIZONTAL_PADDING
minSize = DpSize(DarculaUIUtil.MINIMUM_WIDTH.dp, DarculaUIUtil.MINIMUM_HEIGHT.dp),
minSize = DpSize(minimumSize.width.dp, minimumSize.height.dp),
borderWidth = DarculaUIUtil.LW.dp,
),
)
@@ -284,19 +289,86 @@ private fun readCheckboxStyle(): CheckboxStyle {
contentSelected = textColor,
)
val newUiTheme = isNewUiTheme()
val metrics = if (newUiTheme) NewUiCheckboxMetrics else ClassicUiCheckboxMetrics
// This value is not normally defined in the themes, but Swing checks it anyway.
// The default hardcoded in com.intellij.ide.ui.laf.darcula.ui.DarculaCheckBoxUI.getDefaultIcon()
// is not correct though, the SVG is 19x19 and is missing 1px on the right
val checkboxSize = retrieveIntAsDpOrUnspecified("CheckBox.iconSize")
.let {
when {
it.isSpecified -> DpSize(it, it)
else -> metrics.checkboxSize
}
}
return CheckboxStyle(
colors = colors,
metrics = CheckboxMetrics(
checkboxSize = DarculaCheckBoxUI().defaultIcon.let { DpSize(it.iconWidth.dp, it.iconHeight.dp) },
checkboxCornerSize = CornerSize(3.dp), // See DarculaCheckBoxUI#drawCheckIcon
outlineSize = DpSize(15.dp, 15.dp), // Extrapolated from SVG
outlineOffset = DpOffset(2.5.dp, 1.5.dp), // Extrapolated from SVG
iconContentGap = 5.dp, // See DarculaCheckBoxUI#textIconGap
checkboxSize = checkboxSize,
outlineCornerSize = CornerSize(metrics.outlineCornerSize),
outlineFocusedCornerSize = CornerSize(metrics.outlineFocusedCornerSize),
outlineSelectedCornerSize = CornerSize(metrics.outlineSelectedCornerSize),
outlineSelectedFocusedCornerSize = CornerSize(metrics.outlineSelectedFocusedCornerSize),
outlineSize = metrics.outlineSize,
outlineSelectedSize = metrics.outlineSelectedSize,
outlineFocusedSize = metrics.outlineFocusedSize,
outlineSelectedFocusedSize = metrics.outlineSelectedFocusedSize,
iconContentGap = metrics.iconContentGap,
),
icons = CheckboxIcons(checkbox = bridgePainterProvider("${iconsBasePath}checkBox.svg")),
)
}
private interface BridgeCheckboxMetrics {
val outlineSize: DpSize
val outlineFocusedSize: DpSize
val outlineSelectedSize: DpSize
val outlineSelectedFocusedSize: DpSize
val outlineCornerSize: Dp
val outlineFocusedCornerSize: Dp
val outlineSelectedCornerSize: Dp
val outlineSelectedFocusedCornerSize: Dp
val checkboxSize: DpSize
val iconContentGap: Dp
}
private object ClassicUiCheckboxMetrics : BridgeCheckboxMetrics {
override val outlineSize = DpSize(14.dp, 14.dp)
override val outlineFocusedSize = DpSize(15.dp, 15.dp)
override val outlineSelectedSize = outlineSize
override val outlineSelectedFocusedSize = outlineFocusedSize
override val outlineCornerSize = 2.dp
override val outlineFocusedCornerSize = 3.dp
override val outlineSelectedCornerSize = outlineCornerSize
override val outlineSelectedFocusedCornerSize = outlineFocusedCornerSize
override val checkboxSize = DpSize(20.dp, 19.dp)
override val iconContentGap = 4.dp
}
private object NewUiCheckboxMetrics : BridgeCheckboxMetrics {
override val outlineSize = DpSize(16.dp, 16.dp)
override val outlineFocusedSize = outlineSize
override val outlineSelectedSize = DpSize(20.dp, 20.dp)
override val outlineSelectedFocusedSize = outlineSelectedSize
override val outlineCornerSize = 3.dp
override val outlineFocusedCornerSize = outlineCornerSize
override val outlineSelectedCornerSize = 4.5.dp
override val outlineSelectedFocusedCornerSize = outlineSelectedCornerSize
override val checkboxSize = DpSize(24.dp, 24.dp)
override val iconContentGap = 5.dp
}
// Note: there isn't a chip spec, nor a chip UI, so we're deriving this from the
// styling defined in com.intellij.ide.ui.experimental.meetNewUi.MeetNewUiButton
// To note:
@@ -394,12 +466,13 @@ private fun readDefaultDropdownStyle(
iconTintHovered = Color.Unspecified,
)
val arrowWidth = DarculaUIUtil.ARROW_BUTTON_WIDTH.dp
val minimumSize = JBUI.CurrentTheme.ComboBox.minimumSize()
val arrowWidth = JBUI.CurrentTheme.Component.ARROW_AREA_WIDTH.dp
return DropdownStyle(
colors = colors,
metrics = DropdownMetrics(
arrowMinSize = DpSize(arrowWidth, DarculaUIUtil.MINIMUM_HEIGHT.dp),
minSize = DpSize(DarculaUIUtil.MINIMUM_WIDTH.dp + arrowWidth, DarculaUIUtil.MINIMUM_HEIGHT.dp),
arrowMinSize = DpSize(arrowWidth, minimumSize.height.dp),
minSize = DpSize(minimumSize.width.dp + arrowWidth, minimumSize.height.dp),
cornerSize = CornerSize(DarculaUIUtil.COMPONENT_ARC.dp),
contentPadding = retrieveInsetsAsPaddingValues("ComboBox.padding"),
borderWidth = DarculaUIUtil.BW.dp,
@@ -441,12 +514,14 @@ private fun readUndecoratedDropdownStyle(
iconTintHovered = Color.Unspecified,
)
val arrowWidth = DarculaUIUtil.ARROW_BUTTON_WIDTH.dp
val arrowWidth = JBUI.CurrentTheme.Component.ARROW_AREA_WIDTH.dp
val minimumSize = JBUI.CurrentTheme.Button.minimumSize()
return DropdownStyle(
colors = colors,
metrics = DropdownMetrics(
arrowMinSize = DpSize(arrowWidth, DarculaUIUtil.MINIMUM_HEIGHT.dp),
minSize = DpSize(DarculaUIUtil.MINIMUM_WIDTH.dp + arrowWidth, DarculaUIUtil.MINIMUM_HEIGHT.dp),
arrowMinSize = DpSize(arrowWidth, minimumSize.height.dp),
minSize = DpSize(minimumSize.width.dp + arrowWidth, minimumSize.height.dp),
cornerSize = CornerSize(JBUI.CurrentTheme.MainToolbar.Dropdown.hoverArc().dp),
contentPadding = JBUI.CurrentTheme.MainToolbar.Dropdown.borderInsets().toPaddingValues(),
borderWidth = 0.dp,
@@ -609,17 +684,66 @@ private fun readRadioButtonStyle(): RadioButtonStyle {
contentSelectedDisabled = disabledContent,
)
val newUiTheme = isNewUiTheme()
val metrics = if (newUiTheme) NewUiRadioButtonMetrics else ClassicUiRadioButtonMetrics
// This value is not normally defined in the themes, but Swing checks it anyway
// The default hardcoded in com.intellij.ide.ui.laf.darcula.ui.DarculaRadioButtonUI.getDefaultIcon()
// is not correct though, the SVG is 19x19 and is missing 1px on the right
val radioButtonSize = retrieveIntAsDpOrUnspecified("RadioButton.iconSize")
.takeOrElse { metrics.radioButtonSize }
.let { DpSize(it, it) }
// val outlineSize = if (isNewUiButNotDarcula() DpSize(17.dp, 17.dp) else
return RadioButtonStyle(
colors = colors,
metrics = RadioButtonMetrics(
radioButtonSize = DpSize(19.dp, 19.dp),
radioButtonSize = radioButtonSize,
outlineSize = metrics.outlineSize,
outlineFocusedSize = metrics.outlineFocusedSize,
outlineSelectedSize = metrics.outlineSelectedSize,
outlineSelectedFocusedSize = metrics.outlineSelectedFocusedSize,
iconContentGap = retrieveIntAsDpOrUnspecified("RadioButton.textIconGap")
.takeOrElse { 4.dp },
.takeOrElse { metrics.iconContentGap },
),
icons = RadioButtonIcons(radioButton = bridgePainterProvider("${iconsBasePath}radio.svg")),
)
}
private interface BridgeRadioButtonMetrics {
val outlineSize: DpSize
val outlineFocusedSize: DpSize
val outlineSelectedSize: DpSize
val outlineSelectedFocusedSize: DpSize
val radioButtonSize: Dp
val iconContentGap: Dp
}
private object ClassicUiRadioButtonMetrics : BridgeRadioButtonMetrics {
override val outlineSize = DpSize(17.dp, 17.dp)
override val outlineFocusedSize = DpSize(19.dp, 19.dp)
override val outlineSelectedSize = outlineSize
override val outlineSelectedFocusedSize = outlineFocusedSize
override val radioButtonSize = 19.dp
override val iconContentGap = 4.dp
}
private object NewUiRadioButtonMetrics : BridgeRadioButtonMetrics {
override val outlineSize = DpSize(17.dp, 17.dp)
override val outlineFocusedSize = outlineSize
override val outlineSelectedSize = DpSize(22.dp, 22.dp)
override val outlineSelectedFocusedSize = outlineSelectedSize
override val radioButtonSize = 24.dp
override val iconContentGap = 4.dp
}
private fun readScrollbarStyle(isDark: Boolean) =
ScrollbarStyle(
colors = ScrollbarColors(
@@ -723,12 +847,13 @@ private fun readTextFieldStyle(textFieldStyle: TextStyle): TextFieldStyle {
placeholder = NamedColorUtil.getInactiveTextColor().toComposeColor(),
)
val minimumSize = JBUI.CurrentTheme.TextField.minimumSize()
return TextFieldStyle(
colors = colors,
metrics = TextFieldMetrics(
cornerSize = CornerSize(DarculaUIUtil.COMPONENT_ARC.dp),
cornerSize = CornerSize(DarculaUIUtil.COMPONENT_ARC.dp / 2),
contentPadding = PaddingValues(horizontal = 9.dp, vertical = 2.dp),
minSize = DpSize(DarculaUIUtil.MINIMUM_WIDTH.dp, DarculaUIUtil.MINIMUM_HEIGHT.dp),
minSize = DpSize(minimumSize.width.dp, minimumSize.height.dp),
borderWidth = DarculaUIUtil.LW.dp,
),
textStyle = textFieldStyle,
@@ -921,3 +1046,11 @@ private fun readIconButtonStyle(): IconButtonStyle =
borderHovered = retrieveColorOrUnspecified("ActionButton.hoverBorderColor"),
),
)
@Suppress("UnstableApiUsage")
internal fun isNewUiTheme(): Boolean {
if (!NewUI.isEnabled()) return false
val lafInfo = LafManager.getInstance().currentUIThemeLookAndFeel
return lafInfo.name == "Light" || lafInfo.name == "Dark" || lafInfo.name == "Light with Light Header"
}

View File

@@ -24,7 +24,7 @@ public final class org/jetbrains/jewel/intui/standalone/PainterProviderKt {
public static final fun standalonePainterProvider (Ljava/lang/String;)Lorg/jetbrains/jewel/ui/painter/ResourcePainterProvider;
}
public final class org/jetbrains/jewel/intui/standalone/StandalonePainterHintsProvider : org/jetbrains/jewel/ui/painter/BasePainterHintsProvider {
public final class org/jetbrains/jewel/intui/standalone/StandalonePainterHintsProvider : org/jetbrains/jewel/ui/painter/PalettePainterHintsProvider {
public static final field $stable I
public static final field Companion Lorg/jetbrains/jewel/intui/standalone/StandalonePainterHintsProvider$Companion;
public fun <init> (Lorg/jetbrains/jewel/foundation/theme/ThemeDefinition;)V
@@ -49,8 +49,8 @@ public final class org/jetbrains/jewel/intui/standalone/styling/IntUiCheckboxSty
public static final fun dark (Lorg/jetbrains/jewel/ui/component/styling/CheckboxIcons$Companion;Lorg/jetbrains/jewel/ui/painter/PainterProvider;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/CheckboxIcons;
public static final fun dark (Lorg/jetbrains/jewel/ui/component/styling/CheckboxStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/CheckboxColors;Lorg/jetbrains/jewel/ui/component/styling/CheckboxMetrics;Lorg/jetbrains/jewel/ui/component/styling/CheckboxIcons;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/CheckboxStyle;
public static final fun dark-GyCwops (Lorg/jetbrains/jewel/ui/component/styling/CheckboxColors$Companion;JJJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/CheckboxColors;
public static final fun defaults-RRvNTYw (Lorg/jetbrains/jewel/ui/component/styling/CheckboxMetrics$Companion;JLandroidx/compose/foundation/shape/CornerSize;JJF)Lorg/jetbrains/jewel/ui/component/styling/CheckboxMetrics;
public static synthetic fun defaults-RRvNTYw$default (Lorg/jetbrains/jewel/ui/component/styling/CheckboxMetrics$Companion;JLandroidx/compose/foundation/shape/CornerSize;JJFILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/CheckboxMetrics;
public static final fun defaults-xtx8w0A (Lorg/jetbrains/jewel/ui/component/styling/CheckboxMetrics$Companion;JLandroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/shape/CornerSize;JJJJF)Lorg/jetbrains/jewel/ui/component/styling/CheckboxMetrics;
public static synthetic fun defaults-xtx8w0A$default (Lorg/jetbrains/jewel/ui/component/styling/CheckboxMetrics$Companion;JLandroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/shape/CornerSize;JJJJFILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/CheckboxMetrics;
public static final fun light (Lorg/jetbrains/jewel/ui/component/styling/CheckboxIcons$Companion;Lorg/jetbrains/jewel/ui/painter/PainterProvider;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/CheckboxIcons;
public static final fun light (Lorg/jetbrains/jewel/ui/component/styling/CheckboxStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/CheckboxColors;Lorg/jetbrains/jewel/ui/component/styling/CheckboxMetrics;Lorg/jetbrains/jewel/ui/component/styling/CheckboxIcons;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/CheckboxStyle;
public static final fun light-GyCwops (Lorg/jetbrains/jewel/ui/component/styling/CheckboxColors$Companion;JJJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/CheckboxColors;
@@ -237,8 +237,8 @@ public final class org/jetbrains/jewel/intui/standalone/styling/IntUiRadioButton
public static final fun dark (Lorg/jetbrains/jewel/ui/component/styling/RadioButtonStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/RadioButtonColors;Lorg/jetbrains/jewel/ui/component/styling/RadioButtonMetrics;Lorg/jetbrains/jewel/ui/component/styling/RadioButtonIcons;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/RadioButtonStyle;
public static synthetic fun dark$default (Lorg/jetbrains/jewel/ui/component/styling/RadioButtonIcons$Companion;Lorg/jetbrains/jewel/ui/painter/PainterProvider;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/RadioButtonIcons;
public static final fun dark-dPtIKUs (Lorg/jetbrains/jewel/ui/component/styling/RadioButtonColors$Companion;JJJJJJLandroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/RadioButtonColors;
public static final fun defaults-Q6CdCac (Lorg/jetbrains/jewel/ui/component/styling/RadioButtonMetrics$Companion;JF)Lorg/jetbrains/jewel/ui/component/styling/RadioButtonMetrics;
public static synthetic fun defaults-Q6CdCac$default (Lorg/jetbrains/jewel/ui/component/styling/RadioButtonMetrics$Companion;JFILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/RadioButtonMetrics;
public static final fun defaults-Wf7Cy8o (Lorg/jetbrains/jewel/ui/component/styling/RadioButtonMetrics$Companion;JJJJJF)Lorg/jetbrains/jewel/ui/component/styling/RadioButtonMetrics;
public static synthetic fun defaults-Wf7Cy8o$default (Lorg/jetbrains/jewel/ui/component/styling/RadioButtonMetrics$Companion;JJJJJFILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/RadioButtonMetrics;
public static final fun light (Lorg/jetbrains/jewel/ui/component/styling/RadioButtonIcons$Companion;Lorg/jetbrains/jewel/ui/painter/PainterProvider;)Lorg/jetbrains/jewel/ui/component/styling/RadioButtonIcons;
public static final fun light (Lorg/jetbrains/jewel/ui/component/styling/RadioButtonStyle$Companion;Lorg/jetbrains/jewel/ui/component/styling/RadioButtonColors;Lorg/jetbrains/jewel/ui/component/styling/RadioButtonMetrics;Lorg/jetbrains/jewel/ui/component/styling/RadioButtonIcons;Landroidx/compose/runtime/Composer;II)Lorg/jetbrains/jewel/ui/component/styling/RadioButtonStyle;
public static synthetic fun light$default (Lorg/jetbrains/jewel/ui/component/styling/RadioButtonIcons$Companion;Lorg/jetbrains/jewel/ui/painter/PainterProvider;ILjava/lang/Object;)Lorg/jetbrains/jewel/ui/component/styling/RadioButtonIcons;

View File

@@ -27,6 +27,7 @@ tasks {
dependsOn("generateIntUiDarkTheme")
dependsOn("generateIntUiLightTheme")
}
named<Jar>("sourcesJar") {
dependsOn("generateIntUiDarkTheme")
dependsOn("generateIntUiLightTheme")

View File

@@ -1,38 +1,118 @@
package org.jetbrains.jewel.intui.standalone
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.foundation.theme.ThemeDefinition
import org.jetbrains.jewel.ui.painter.BasePainterHintsProvider
import org.jetbrains.jewel.ui.painter.PainterHint
import org.jetbrains.jewel.ui.painter.PalettePainterHintsProvider
import org.jetbrains.jewel.ui.painter.hints.ColorBasedPaletteReplacement
import org.jetbrains.jewel.ui.painter.hints.Dark
import org.jetbrains.jewel.ui.painter.hints.HiDpi
import org.jetbrains.jewel.ui.painter.hints.Override
import org.jetbrains.jewel.ui.painter.hints.KeyBasedPaletteReplacement
import org.jetbrains.jewel.ui.painter.hints.PathOverride
import org.jetbrains.jewel.ui.util.inDebugMode
/** Provides the default [PainterHint]s to use to load images. */
public class StandalonePainterHintsProvider(
theme: ThemeDefinition,
) : BasePainterHintsProvider(
) : PalettePainterHintsProvider(
theme.isDark,
intellijColorPalette,
theme.iconData.colorPalette,
theme.colorPalette.rawMap,
) {
override val checkBoxByColorPaletteHint: PainterHint
override val checkBoxByKeyPaletteHint: PainterHint
override val treePaletteHint: PainterHint
override val uiPaletteHint: PainterHint
private val overrideHint: PainterHint =
Override(
PathOverride(
theme.iconData.iconOverrides.entries.associate { (k, v) ->
k.removePrefix("/") to v.removePrefix("/")
},
)
@Composable
override fun hints(path: String): List<PainterHint> = buildList {
add(getPaletteHint(path))
add(overrideHint)
add(HiDpi())
add(Dark(JewelTheme.isDark))
init {
val ui = mutableMapOf<Color, Color>()
val checkBoxesByColor = mutableMapOf<Color, Color>()
val checkBoxesByKey = mutableMapOf<String, Color>()
val trees = mutableMapOf<Color, Color>()
@Suppress("LoopWithTooManyJumpStatements")
for ((key, value) in themeIconPalette) {
if (value == null) continue
// Checkbox (and radio button) entries work differently: the ID field
// for each element that needs patching has a "[fillKey]_[strokeKey]"
// format, starting from IJP 241.
if (key.startsWith("Checkbox.")) {
registerIdBasedReplacement(checkBoxesByKey, key, value)
}
val map = selectMap(key, checkBoxesByColor, trees, ui) ?: continue
registerColorBasedReplacement(map, key, value)
}
checkBoxByKeyPaletteHint = KeyBasedPaletteReplacement(checkBoxesByKey)
checkBoxByColorPaletteHint = ColorBasedPaletteReplacement(checkBoxesByColor)
treePaletteHint = ColorBasedPaletteReplacement(trees)
uiPaletteHint = ColorBasedPaletteReplacement(ui)
}
private fun registerColorBasedReplacement(
map: MutableMap<Color, Color>,
key: String,
value: String,
) {
// If either the key or the resolved value aren't valid colors, ignore the entry
val keyAsColor = resolveKeyColor(key, intellijIconPalette, isDark) ?: return
val resolvedColor = resolveColor(value) ?: return
// Save the new entry (oldColor -> newColor) in the map
map[keyAsColor] = resolvedColor
}
private fun registerIdBasedReplacement(
map: MutableMap<String, Color>,
key: String,
value: String,
) {
val adjustedKey = if (isDark) key.removeSuffix(".Dark") else key
if (adjustedKey !in supportedCheckboxKeys) {
if (inDebugMode) {
println("${if (isDark) "Dark" else "Light"} theme: color key $key is not supported, will be ignored")
}
return
}
if (adjustedKey != key && inDebugMode) {
println("${if (isDark) "Dark" else "Light"} theme: color key $key is deprecated, use $adjustedKey instead")
}
val parsedValue = resolveColor(value)
if (parsedValue == null) {
if (inDebugMode) {
println("${if (isDark) "Dark" else "Light"} theme: color key $key has invalid value: '$value'")
}
return
}
map[adjustedKey] = parsedValue
}
@Composable
override fun hints(path: String): List<PainterHint> =
buildList {
add(overrideHint)
add(getPaletteHint(path, isNewUi = true))
add(HiDpi())
add(Dark(JewelTheme.isDark))
}
public companion object {
// Extracted from com.intellij.ide.ui.UITheme#colorPalette
@@ -86,5 +166,18 @@ public class StandalonePainterHintsProvider(
"Tree.iconColor" to "#808080",
"Tree.iconColor.Dark" to "#AFB1B3",
)
private val supportedCheckboxKeys: Set<String> =
setOf(
"Checkbox.Background.Default",
"Checkbox.Border.Default",
"Checkbox.Foreground.Selected",
"Checkbox.Background.Selected",
"Checkbox.Border.Selected",
"Checkbox.Focus.Wide",
"Checkbox.Foreground.Disabled",
"Checkbox.Background.Disabled",
"Checkbox.Border.Disabled",
)
}
}

View File

@@ -4,7 +4,6 @@ import androidx.compose.foundation.shape.CornerSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import org.jetbrains.jewel.intui.core.theme.IntUiDarkTheme
@@ -49,18 +48,28 @@ public fun CheckboxColors.Companion.dark(
CheckboxColors(content, contentDisabled, contentSelected)
public fun CheckboxMetrics.Companion.defaults(
checkboxSize: DpSize = DpSize(19.dp, 19.dp),
checkboxCornerSize: CornerSize = CornerSize(3.dp),
outlineSize: DpSize = DpSize(15.dp, 15.dp),
outlineOffset: DpOffset = DpOffset(2.5.dp, 1.5.dp),
checkboxSize: DpSize = DpSize(24.dp, 24.dp),
outlineCornerSize: CornerSize = CornerSize(3.dp),
outlineFocusedCornerSize: CornerSize = outlineCornerSize,
outlineSelectedCornerSize: CornerSize = CornerSize(4.5.dp),
outlineSelectedFocusedCornerSize: CornerSize = outlineSelectedCornerSize,
outlineSize: DpSize = DpSize(16.dp, 16.dp),
outlineFocusedSize: DpSize = outlineSize,
outlineSelectedSize: DpSize = DpSize(20.dp, 20.dp),
outlineSelectedFocusedSize: DpSize = outlineSelectedSize,
iconContentGap: Dp = 5.dp,
): CheckboxMetrics =
CheckboxMetrics(
checkboxSize,
checkboxCornerSize,
outlineSize,
outlineOffset,
iconContentGap,
checkboxSize = checkboxSize,
outlineCornerSize = outlineCornerSize,
outlineFocusedCornerSize = outlineFocusedCornerSize,
outlineSelectedCornerSize = outlineSelectedCornerSize,
outlineSelectedFocusedCornerSize = outlineSelectedFocusedCornerSize,
outlineSize = outlineSize,
outlineFocusedSize = outlineFocusedSize,
outlineSelectedSize = outlineSelectedSize,
outlineSelectedFocusedSize = outlineSelectedFocusedSize,
iconContentGap = iconContentGap,
)
@Composable

View File

@@ -67,10 +67,21 @@ public fun RadioButtonColors.Companion.dark(
)
public fun RadioButtonMetrics.Companion.defaults(
radioButtonSize: DpSize = DpSize(19.dp, 19.dp),
radioButtonSize: DpSize = DpSize(24.dp, 24.dp),
outlineSize: DpSize = DpSize(17.dp, 17.dp),
outlineFocusedSize: DpSize = outlineSize,
outlineSelectedSize: DpSize = DpSize(22.dp, 22.dp),
outlineSelectedFocusedSize: DpSize = outlineSelectedSize,
iconContentGap: Dp = 8.dp,
): RadioButtonMetrics =
RadioButtonMetrics(radioButtonSize, iconContentGap)
RadioButtonMetrics(
radioButtonSize = radioButtonSize,
outlineSize = outlineSize,
outlineFocusedSize = outlineFocusedSize,
outlineSelectedSize = outlineSelectedSize,
outlineSelectedFocusedSize = outlineSelectedFocusedSize,
iconContentGap = iconContentGap,
)
public fun RadioButtonIcons.Companion.light(
radioButton: PainterProvider =

View File

@@ -1,3 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2.5" y="2.5" width="15" height="15" rx="2.5" fill="#2B2D30" stroke="#5A5D63"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect id="Checkbox.Background.Default_Checkbox.Border.Default" x="4.5" y="4.5" width="15" height="15" rx="2.5" fill="#2B2D30" stroke="#5A5D63"/>
</svg>

Before

Width:  |  Height:  |  Size: 191 B

After

Width:  |  Height:  |  Size: 248 B

View File

@@ -1,3 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2.5" y="2.5" width="15" height="15" rx="2.5" fill="#393B40" stroke="#5A5D63"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect id="Checkbox.Background.Disabled_Checkbox.Border.Disabled" x="4.5" y="4.5" width="15" height="15" rx="2.5" fill="#393B40" stroke="#5A5D63"/>
</svg>

Before

Width:  |  Height:  |  Size: 191 B

After

Width:  |  Height:  |  Size: 250 B

View File

@@ -1,4 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="16" height="16" transform="translate(2 2)" fill="#2B2D30"/>
<rect x="2" y="2" width="16" height="16" rx="3" stroke="#3574F0" stroke-width="2"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect id="Checkbox.Background.Default_Checkbox.Border.Selected" x="4" y="4" width="16" height="16" rx="3" fill="#2B2D30" stroke="#3574F0" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 260 B

After

Width:  |  Height:  |  Size: 260 B

View File

@@ -1,6 +0,0 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="2" width="14" height="14" fill="#2B2D30"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 0C2.79086 0 1 1.79086 1 4V14C1 16.2091 2.79086 18 5 18H15C17.2091 18 19 16.2091 19 14V4C19 1.79086 17.2091 0 15 0H5ZM5 3C4.44772 3 4 3.44772 4 4V14C4 14.5523 4.44772 15 5 15H15C15.5523 15 16 14.5523 16 14V4C16 3.44772 15.5523 3 15 3H5Z" fill="#3574F0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 2C3.89543 2 3 2.89543 3 4V14C3 15.1046 3.89543 16 5 16H15C16.1046 16 17 15.1046 17 14V4C17 2.89543 16.1046 2 15 2H5ZM5 3C4.44772 3 4 3.44772 4 4V14C4 14.5523 4.44772 15 5 15H15C15.5523 15 16 14.5523 16 14V4C16 3.44772 15.5523 3 15 3H5Z" fill="#2B2D30"/>
</svg>

Before

Width:  |  Height:  |  Size: 900 B

View File

@@ -1,4 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="16" height="16" rx="3" fill="#3574F0"/>
<rect x="4.5" y="9" width="11" height="2" rx="1" fill="white"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect id="Checkbox.Background.Selected_Checkbox.Border.Selected" x="4.5" y="4.5" width="15" height="15" rx="2.5" fill="#3574F0" stroke="#3574F0"/>
<rect id="Checkbox.Foreground.Selected" x="6.5" y="11" width="11" height="2" rx="1" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 232 B

After

Width:  |  Height:  |  Size: 349 B

View File

@@ -1,4 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2.5" y="2.5" width="15" height="15" rx="2.5" fill="#393B40" stroke="#393B41"/>
<rect x="4.5" y="9" width="11" height="2" rx="1" fill="#868A91"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect id="Checkbox.Background.Disabled_Checkbox.Border.Disabled" x="4.5" y="4.5" width="15" height="15" rx="2.5" fill="#393B40" stroke="#5A5D63"/>
<rect id="Checkbox.Foreground.Disabled" x="6.5" y="11" width="11" height="2" rx="1" fill="#868A91"/>
</svg>

Before

Width:  |  Height:  |  Size: 257 B

After

Width:  |  Height:  |  Size: 351 B

View File

@@ -1,5 +1,5 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3" width="16" height="16" rx="3" fill="#3574F0"/>
<rect x="5.5" y="10" width="11" height="2" rx="1" fill="white"/>
<rect x="1" y="1" width="20" height="20" rx="5" stroke="#3574F0" stroke-width="2"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect id="Checkbox.Background.Selected_Checkbox.Border.Selected" x="4.5" y="4.5" width="15" height="15" rx="2.5" fill="#3574F0" stroke="#3574F0"/>
<rect id="Checkbox.Focus.Wide" x="2" y="2" width="20" height="20" rx="4.5" stroke="#3574F0" stroke-width="2"/>
<rect id="Checkbox.Foreground.Selected" x="6.5" y="11" width="11" height="2" rx="1" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 317 B

After

Width:  |  Height:  |  Size: 460 B

View File

@@ -1,4 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="2" width="16" height="16" rx="3" fill="#3574F0"/>
<path d="M6 10.5L9 13.5L14.5 7" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect id="Checkbox.Background.Selected_Checkbox.Border.Selected" x="4.5" y="4.5" width="15" height="15" rx="2.5" fill="#3574F0" stroke="#3574F0"/>
<path id="Checkbox.Foreground.Selected" d="M8 12.5L11 15.5L16.5 9" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 281 B

After

Width:  |  Height:  |  Size: 398 B

View File

@@ -1,4 +1,4 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2.5" y="2.5" width="15" height="15" rx="2.5" fill="#393B40" stroke="#393B41"/>
<path d="M6 10.5L9 13.5L14.5 7" stroke="#868A91" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect id="Checkbox.Background.Disabled_Checkbox.Border.Disabled" x="4.5" y="4.5" width="15" height="15" rx="2.5" fill="#393B40" stroke="#5A5D63"/>
<path id="Checkbox.Foreground.Disabled" d="M8 12.5L11 15.5L16.5 9" stroke="#6F737A" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 306 B

After

Width:  |  Height:  |  Size: 400 B

View File

@@ -1,5 +1,5 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="3" width="16" height="16" rx="3" fill="#3574F0"/>
<path d="M7 11.5L10 14.5L15.5 8" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<rect x="1" y="1" width="20" height="20" rx="5" stroke="#3574F0" stroke-width="2"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect id="Checkbox.Background.Selected_Checkbox.Border.Selected" x="4.5" y="4.5" width="15" height="15" rx="2.5" fill="#3574F0" stroke="#3574F0"/>
<rect id="Checkbox.Focus.Wide" x="2" y="2" width="20" height="20" rx="4.5" stroke="#3574F0" stroke-width="2"/>
<path id="Checkbox.Foreground.Selected" d="M8 12.5L11 15.5L16.5 9" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 366 B

After

Width:  |  Height:  |  Size: 509 B

View File

@@ -1,5 +0,0 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="3" y="2" width="14" height="14" rx="2" fill="#2B2D30"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M5 2C3.89543 2 3 2.89543 3 4V14C3 15.1046 3.89543 16 5 16H15C16.1046 16 17 15.1046 17 14V4C17 2.89543 16.1046 2 15 2H5ZM5 3C4.44772 3 4 3.44772 4 4V14C4 14.5523 4.44772 15 5 15H15C15.5523 15 16 14.5523 16 14V4C16 3.44772 15.5523 3 15 3H5Z" fill="#6F737A"/>
</svg>

Before

Width:  |  Height:  |  Size: 601 B

View File

@@ -1,3 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="9" cy="9" r="8" stroke="#5A5D63"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle id="Checkbox.Background.Default_Checkbox.Border.Default" cx="12" cy="12" r="8" fill="#2B2D30" stroke="#5A5D63"/>
</svg>

Before

Width:  |  Height:  |  Size: 150 B

After

Width:  |  Height:  |  Size: 224 B

View File

@@ -1,3 +1,3 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="9" cy="9" r="8" fill="#393B40" stroke="#5A5D63"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle id="Checkbox.Background.Disabled_Checkbox.Border.Disabled" cx="12" cy="12" r="8" fill="#393B40" stroke="#5A5D63"/>
</svg>

Before

Width:  |  Height:  |  Size: 165 B

After

Width:  |  Height:  |  Size: 226 B

View File

@@ -1,3 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="8.5" stroke="#3574F0" stroke-width="2"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle id="Checkbox.Background.Default_Checkbox.Border.Selected" cx="12" cy="12" r="8.5" fill="#2B2D30" stroke="#3574F0" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 171 B

After

Width:  |  Height:  |  Size: 244 B

View File

@@ -1,6 +0,0 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.5 17C13.6421 17 17 13.6421 17 9.5C17 5.35786 13.6421 2 9.5 2C5.35786 2 2 5.35786 2 9.5C2 13.6421 5.35786 17 9.5 17Z" fill="#2B2D30"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M19 9.5C19 14.7467 14.7467 19 9.5 19C4.25329 19 0 14.7467 0 9.5C0 4.25329 4.25329 0 9.5 0C14.7467 0 19 4.25329 19 9.5ZM16 9.5C16 13.0899 13.0899 16 9.5 16C5.91015 16 3 13.0899 3 9.5C3 5.91015 5.91015 3 9.5 3C13.0899 3 16 5.91015 16 9.5Z" fill="#3574F0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 9.5C17 13.6421 13.6421 17 9.5 17C5.35786 17 2 13.6421 2 9.5C2 5.35786 5.35786 2 9.5 2C13.6421 2 17 5.35786 17 9.5ZM16 9.5C16 13.0899 13.0899 16 9.5 16C5.91015 16 3 13.0899 3 9.5C3 5.91015 5.91015 3 9.5 3C13.0899 3 16 5.91015 16 9.5Z" fill="#2B2D30"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -1,4 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="9" cy="9" r="8.5" fill="#3574F0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 12C10.6569 12 12 10.6569 12 9C12 7.34315 10.6569 6 9 6C7.34315 6 6 7.34315 6 9C6 10.6569 7.34315 12 9 12Z" fill="white"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle id="Checkbox.Background.Selected_Checkbox.Border.Selected" cx="12" cy="12" r="8" fill="#3574F0" stroke="#3574F0"/>
<path id="Checkbox.Foreground.Selected" fill-rule="evenodd" clip-rule="evenodd" d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 324 B

After

Width:  |  Height:  |  Size: 439 B

View File

@@ -1,4 +1,4 @@
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="9" cy="9" r="8" fill="#393B40" stroke="#5A5D63"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9 12C10.6569 12 12 10.6569 12 9C12 7.34315 10.6569 6 9 6C7.34315 6 6 7.34315 6 9C6 10.6569 7.34315 12 9 12Z" fill="#6F737A"/>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle id="Checkbox.Background.Disabled_Checkbox.Border.Disabled" cx="12" cy="12" r="8" fill="#393B40" stroke="#5A5D63"/>
<path id="Checkbox.Foreground.Disabled" fill-rule="evenodd" clip-rule="evenodd" d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" fill="#6F737A"/>
</svg>

Before

Width:  |  Height:  |  Size: 341 B

After

Width:  |  Height:  |  Size: 441 B

View File

@@ -1,5 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="12" cy="12" r="8.5" fill="#3574F0"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" fill="#F0F1F2"/>
<circle cx="12" cy="12" r="11" stroke="#3574F0" stroke-width="2"/>
<circle id="Checkbox.Background.Selected_Checkbox.Border.Selected" cx="12" cy="12" r="8" fill="#3574F0" stroke="#3574F0"/>
<circle id="Checkbox.Focus.Wide" cx="12" cy="12" r="11" stroke="#3574F0" stroke-width="2"/>
<path id="Checkbox.Foreground.Selected" fill-rule="evenodd" clip-rule="evenodd" d="M12 15C13.6569 15 15 13.6569 15 12C15 10.3431 13.6569 9 12 9C10.3431 9 9 10.3431 9 12C9 13.6569 10.3431 15 12 15Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 400 B

After

Width:  |  Height:  |  Size: 531 B

View File

@@ -1,5 +0,0 @@
<!-- Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license. -->
<svg width="19" height="19" viewBox="0 0 19 19" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.5 17C13.6421 17 17 13.6421 17 9.5C17 5.35786 13.6421 2 9.5 2C5.35786 2 2 5.35786 2 9.5C2 13.6421 5.35786 17 9.5 17Z" fill="#2B2D30"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 9.5C17 13.6421 13.6421 17 9.5 17C5.35786 17 2 13.6421 2 9.5C2 5.35786 5.35786 2 9.5 2C13.6421 2 17 5.35786 17 9.5ZM16 9.5C16 13.0899 13.0899 16 9.5 16C5.91015 16 3 13.0899 3 9.5C3 5.91015 5.91015 3 9.5 3C13.0899 3 16 5.91015 16 9.5Z" fill="#6F737A"/>
</svg>

Before

Width:  |  Height:  |  Size: 720 B

View File

@@ -33,6 +33,7 @@ import org.jetbrains.jewel.foundation.modifier.onActivated
import org.jetbrains.jewel.foundation.modifier.trackActivation
import org.jetbrains.jewel.foundation.modifier.trackComponentActivation
import org.jetbrains.jewel.foundation.theme.JewelTheme
import org.jetbrains.jewel.ui.Outline
import org.jetbrains.jewel.ui.component.CheckboxRow
import org.jetbrains.jewel.ui.component.CircularProgressIndicator
import org.jetbrains.jewel.ui.component.CircularProgressIndicatorBig
@@ -49,7 +50,7 @@ import org.jetbrains.jewel.ui.component.Tooltip
@Composable
internal fun ComponentShowcaseTab() {
val bgColor by remember(JewelTheme.isDark) { mutableStateOf(JBColor.PanelBackground.toComposeColor()) }
val bgColor by remember(JBColor.PanelBackground.rgb) { mutableStateOf(JBColor.PanelBackground.toComposeColor()) }
val scrollState = rememberScrollState()
Row(
@@ -109,19 +110,37 @@ private fun RowScope.ColumnOne() {
)
var checked by remember { mutableStateOf(false) }
CheckboxRow(
checked = checked,
onCheckedChange = { checked = it },
) {
Text("Hello, I am a themed checkbox")
var validated by remember { mutableStateOf(false) }
val outline = if (validated) Outline.Error else Outline.None
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
CheckboxRow(
checked = checked,
onCheckedChange = { checked = it },
outline = outline,
) {
Text("Hello, I am a themed checkbox")
}
CheckboxRow(checked = validated, onCheckedChange = { validated = it }) {
Text("Show validation")
}
}
Row(horizontalArrangement = Arrangement.spacedBy(16.dp)) {
var index by remember { mutableStateOf(0) }
RadioButtonRow(selected = index == 0, onClick = { index = 0 }) {
RadioButtonRow(
selected = index == 0,
onClick = { index = 0 },
outline = outline,
) {
Text("I am number one")
}
RadioButtonRow(selected = index == 1, onClick = { index = 1 }) {
RadioButtonRow(
selected = index == 1,
onClick = { index = 1 },
outline = outline,
) {
Text("Sad second")
}
}

View File

@@ -15,6 +15,7 @@ import org.jetbrains.jewel.bridge.addComposeTab
import org.jetbrains.jewel.samples.ideplugin.releasessample.ReleasesSampleCompose
import org.jetbrains.jewel.samples.ideplugin.releasessample.ReleasesSamplePanel
@Suppress("unused")
@ExperimentalCoroutinesApi
internal class JewelDemoToolWindowFactory : ToolWindowFactory, DumbAware {

View File

@@ -855,14 +855,21 @@ public final class org/jetbrains/jewel/ui/component/styling/CheckboxIcons$Compan
public final class org/jetbrains/jewel/ui/component/styling/CheckboxMetrics {
public static final field $stable I
public static final field Companion Lorg/jetbrains/jewel/ui/component/styling/CheckboxMetrics$Companion;
public synthetic fun <init> (JLandroidx/compose/foundation/shape/CornerSize;JJFLkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (JLandroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/shape/CornerSize;JJJJFLkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z
public final fun getCheckboxCornerSize ()Landroidx/compose/foundation/shape/CornerSize;
public final fun getCheckboxSize-MYxV2XQ ()J
public final fun getIconContentGap-D9Ej5fM ()F
public final fun getOutlineOffset-RKDOV3M ()J
public final fun getOutlineCornerSize ()Landroidx/compose/foundation/shape/CornerSize;
public final fun getOutlineFocusedCornerSize ()Landroidx/compose/foundation/shape/CornerSize;
public final fun getOutlineFocusedSize-MYxV2XQ ()J
public final fun getOutlineSelectedCornerSize ()Landroidx/compose/foundation/shape/CornerSize;
public final fun getOutlineSelectedFocusedCornerSize ()Landroidx/compose/foundation/shape/CornerSize;
public final fun getOutlineSelectedFocusedSize-MYxV2XQ ()J
public final fun getOutlineSelectedSize-MYxV2XQ ()J
public final fun getOutlineSize-MYxV2XQ ()J
public fun hashCode ()I
public final fun outlineCornerSizeFor-f7CD9uA (JLandroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State;
public final fun outlineSizeFor-f7CD9uA (JLandroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State;
public fun toString ()Ljava/lang/String;
}
@@ -1631,11 +1638,16 @@ public final class org/jetbrains/jewel/ui/component/styling/RadioButtonIcons$Com
public final class org/jetbrains/jewel/ui/component/styling/RadioButtonMetrics {
public static final field $stable I
public static final field Companion Lorg/jetbrains/jewel/ui/component/styling/RadioButtonMetrics$Companion;
public synthetic fun <init> (JFLkotlin/jvm/internal/DefaultConstructorMarker;)V
public synthetic fun <init> (JJJJJFLkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun equals (Ljava/lang/Object;)Z
public final fun getIconContentGap-D9Ej5fM ()F
public final fun getOutlineFocusedSize-MYxV2XQ ()J
public final fun getOutlineSelectedFocusedSize-MYxV2XQ ()J
public final fun getOutlineSelectedSize-MYxV2XQ ()J
public final fun getOutlineSize-MYxV2XQ ()J
public final fun getRadioButtonSize-MYxV2XQ ()J
public fun hashCode ()I
public final fun outlineSizeFor-ehnS_G0 (JLandroidx/compose/runtime/Composer;I)Landroidx/compose/runtime/State;
public fun toString ()Ljava/lang/String;
}
@@ -2102,13 +2114,6 @@ public final class org/jetbrains/jewel/ui/painter/BadgePainter : org/jetbrains/j
public synthetic fun <init> (Landroidx/compose/ui/graphics/painter/Painter;JLorg/jetbrains/jewel/ui/painter/badge/BadgeShape;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
}
public abstract class org/jetbrains/jewel/ui/painter/BasePainterHintsProvider : org/jetbrains/jewel/ui/painter/PainterHintsProvider {
public static final field $stable I
public fun <init> (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;)V
protected final fun getPaletteHint (Ljava/lang/String;)Lorg/jetbrains/jewel/ui/painter/PainterHint;
public fun priorityHints (Ljava/lang/String;Landroidx/compose/runtime/Composer;I)Ljava/util/List;
}
public abstract interface class org/jetbrains/jewel/ui/painter/BitmapPainterHint : org/jetbrains/jewel/ui/painter/PainterHint {
public abstract fun canApply (Lorg/jetbrains/jewel/ui/painter/PainterProviderScope;)Z
}
@@ -2218,6 +2223,24 @@ public final class org/jetbrains/jewel/ui/painter/PainterWrapperHint$DefaultImpl
public static fun canApply (Lorg/jetbrains/jewel/ui/painter/PainterWrapperHint;Lorg/jetbrains/jewel/ui/painter/PainterProviderScope;)Z
}
public abstract class org/jetbrains/jewel/ui/painter/PalettePainterHintsProvider : org/jetbrains/jewel/ui/painter/PainterHintsProvider {
public static final field $stable I
public fun <init> (ZLjava/util/Map;Ljava/util/Map;Ljava/util/Map;)V
protected abstract fun getCheckBoxByColorPaletteHint ()Lorg/jetbrains/jewel/ui/painter/PainterHint;
protected abstract fun getCheckBoxByKeyPaletteHint ()Lorg/jetbrains/jewel/ui/painter/PainterHint;
protected final fun getIntellijIconPalette ()Ljava/util/Map;
protected final fun getPaletteHint (Ljava/lang/String;Z)Lorg/jetbrains/jewel/ui/painter/PainterHint;
protected final fun getThemeColorPalette ()Ljava/util/Map;
protected final fun getThemeIconPalette ()Ljava/util/Map;
protected abstract fun getTreePaletteHint ()Lorg/jetbrains/jewel/ui/painter/PainterHint;
protected abstract fun getUiPaletteHint ()Lorg/jetbrains/jewel/ui/painter/PainterHint;
protected final fun isDark ()Z
public fun priorityHints (Ljava/lang/String;Landroidx/compose/runtime/Composer;I)Ljava/util/List;
protected final fun resolveColor-ijrfgN4 (Ljava/lang/String;)Landroidx/compose/ui/graphics/Color;
protected final fun resolveKeyColor-8tov2TA (Ljava/lang/String;Ljava/util/Map;Z)Landroidx/compose/ui/graphics/Color;
protected final fun selectMap (Ljava/lang/String;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Ljava/util/Map;
}
public final class org/jetbrains/jewel/ui/painter/ResizedPainter : org/jetbrains/jewel/ui/painter/DelegatePainter {
public static final field $stable I
public synthetic fun <init> (Landroidx/compose/ui/graphics/painter/Painter;JLkotlin/jvm/internal/DefaultConstructorMarker;)V
@@ -2283,7 +2306,11 @@ public final class org/jetbrains/jewel/ui/painter/hints/BadgeKt {
public static final fun Badge-DxMtmZc (JLorg/jetbrains/jewel/ui/painter/badge/BadgeShape;)Lorg/jetbrains/jewel/ui/painter/PainterHint;
}
public final class org/jetbrains/jewel/ui/painter/hints/DarkOrStrokeKt {
public final class org/jetbrains/jewel/ui/painter/hints/ColorBasedPaletteReplacementKt {
public static final fun ColorBasedPaletteReplacement (Ljava/util/Map;)Lorg/jetbrains/jewel/ui/painter/PainterHint;
}
public final class org/jetbrains/jewel/ui/painter/hints/DarkAndStrokeKt {
public static final fun Dark (Z)Lorg/jetbrains/jewel/ui/painter/PainterHint;
public static synthetic fun Dark$default (ZILjava/lang/Object;)Lorg/jetbrains/jewel/ui/painter/PainterHint;
public static final fun Stroke-8_81llA (J)Lorg/jetbrains/jewel/ui/painter/PainterHint;
@@ -2293,12 +2320,12 @@ public final class org/jetbrains/jewel/ui/painter/hints/HiDpiKt {
public static final fun HiDpi ()Lorg/jetbrains/jewel/ui/painter/PainterHint;
}
public final class org/jetbrains/jewel/ui/painter/hints/OverrideKt {
public static final fun Override (Ljava/util/Map;)Lorg/jetbrains/jewel/ui/painter/PainterHint;
public final class org/jetbrains/jewel/ui/painter/hints/KeyBasedPaletteReplacementKt {
public static final fun KeyBasedPaletteReplacement (Ljava/util/Map;)Lorg/jetbrains/jewel/ui/painter/PainterHint;
}
public final class org/jetbrains/jewel/ui/painter/hints/PaletteKt {
public static final fun Palette (Ljava/util/Map;)Lorg/jetbrains/jewel/ui/painter/PainterHint;
public final class org/jetbrains/jewel/ui/painter/hints/PathOverrideKt {
public static final fun PathOverride (Ljava/util/Map;)Lorg/jetbrains/jewel/ui/painter/PainterHint;
}
public final class org/jetbrains/jewel/ui/painter/hints/SelectedKt {
@@ -2308,8 +2335,8 @@ public final class org/jetbrains/jewel/ui/painter/hints/SelectedKt {
}
public final class org/jetbrains/jewel/ui/painter/hints/SizeKt {
public static final fun Size (I)Lorg/jetbrains/jewel/ui/painter/PainterHint;
public static final fun Size (II)Lorg/jetbrains/jewel/ui/painter/PainterHint;
public static synthetic fun Size$default (IIILjava/lang/Object;)Lorg/jetbrains/jewel/ui/painter/PainterHint;
}
public final class org/jetbrains/jewel/ui/painter/hints/StatefulKt {
@@ -2347,6 +2374,7 @@ public final class org/jetbrains/jewel/ui/theme/JewelThemeKt {
public final class org/jetbrains/jewel/ui/util/ColorExtensionsKt {
public static final fun fromRGBAHexStringOrNull (Landroidx/compose/ui/graphics/Color$Companion;Ljava/lang/String;)Landroidx/compose/ui/graphics/Color;
public static final fun isDark-8_81llA (J)Z
public static final fun toRgbaHexString (Ljava/awt/Color;)Ljava/lang/String;
public static final fun toRgbaHexString-8_81llA (J)Ljava/lang/String;
}

View File

@@ -1,6 +1,5 @@
package org.jetbrains.jewel.ui.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.HoverInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -9,7 +8,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.triStateToggleable
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -23,6 +21,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.paint
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.semantics.Role
@@ -261,6 +260,7 @@ private fun CheckboxImpl(
is PressInteraction.Cancel,
is PressInteraction.Release,
-> checkboxState = checkboxState.copy(pressed = false)
is HoverInteraction.Enter -> checkboxState = checkboxState.copy(hovered = true)
is HoverInteraction.Exit -> checkboxState = checkboxState.copy(hovered = false)
is FocusInteraction.Focus -> checkboxState = checkboxState.copy(focused = true)
@@ -283,15 +283,14 @@ private fun CheckboxImpl(
indication = null,
)
val checkBoxImageModifier = Modifier.size(metrics.checkboxSize)
val outlineModifier = Modifier.size(metrics.outlineSize)
.offset(metrics.outlineOffset.x, metrics.outlineOffset.y)
.outline(
state = checkboxState,
outline = outline,
outlineShape = RoundedCornerShape(metrics.checkboxCornerSize),
alignment = Stroke.Alignment.Center,
)
val outlineModifier =
Modifier.size(metrics.outlineSizeFor(checkboxState).value)
.outline(
state = checkboxState,
outline = outline,
outlineShape = RoundedCornerShape(metrics.outlineCornerSizeFor(checkboxState).value),
alignment = Stroke.Alignment.Center,
)
val checkboxPainter by icons.checkbox.getPainter(
if (checkboxState.toggleableState == ToggleableState.Indeterminate) {
@@ -303,10 +302,12 @@ private fun CheckboxImpl(
Stateful(checkboxState),
)
val checkboxBoxModifier = Modifier.size(metrics.checkboxSize)
if (content == null) {
Box(contentAlignment = Alignment.TopStart) {
CheckBoxImage(wrapperModifier, checkboxPainter, checkBoxImageModifier)
Box(outlineModifier)
Box(checkboxBoxModifier, contentAlignment = Alignment.TopStart) {
CheckBoxImage(checkboxPainter)
Box(outlineModifier.align(Alignment.Center))
}
} else {
Row(
@@ -314,9 +315,9 @@ private fun CheckboxImpl(
horizontalArrangement = Arrangement.spacedBy(metrics.iconContentGap),
verticalAlignment = Alignment.CenterVertically,
) {
Box(contentAlignment = Alignment.TopStart) {
CheckBoxImage(Modifier, checkboxPainter, checkBoxImageModifier)
Box(outlineModifier)
Box(checkboxBoxModifier, contentAlignment = Alignment.TopStart) {
CheckBoxImage(checkboxPainter)
Box(outlineModifier.align(Alignment.Center))
}
val contentColor by colors.contentFor(checkboxState)
@@ -331,25 +332,20 @@ private fun CheckboxImpl(
}
private object CheckBoxIndeterminate : PainterSuffixHint() {
override fun PainterProviderScope.suffix(): String = "Indeterminate"
}
@Composable
private fun CheckBoxImage(
modifier: Modifier,
checkboxPainter: Painter,
checkBoxModifier: Modifier,
modifier: Modifier = Modifier,
) {
Box(modifier, contentAlignment = Alignment.Center) {
Image(checkboxPainter, contentDescription = null, modifier = checkBoxModifier)
}
Box(modifier.paint(checkboxPainter, alignment = Alignment.TopStart))
}
@Immutable
@JvmInline
public value class CheckboxState(private val state: ULong) : ToggleableComponentState, FocusableComponentState {
override val toggleableState: ToggleableState
get() = state.readToggleableState()
@@ -393,7 +389,6 @@ public value class CheckboxState(private val state: ULong) : ToggleableComponent
"isHovered=$isHovered, isPressed=$isPressed, isSelected=$isSelected, isActive=$isActive)"
public companion object {
public fun of(
toggleableState: ToggleableState,
enabled: Boolean = true,

View File

@@ -1,6 +1,5 @@
package org.jetbrains.jewel.ui.component
import androidx.compose.foundation.Image
import androidx.compose.foundation.interaction.FocusInteraction
import androidx.compose.foundation.interaction.HoverInteraction
import androidx.compose.foundation.interaction.MutableInteractionSource
@@ -22,6 +21,7 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.paint
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.semantics.Role
@@ -172,27 +172,37 @@ private fun RadioButtonImpl(
val colors = style.colors
val metrics = style.metrics
val radioButtonModifier = Modifier.size(metrics.radioButtonSize)
.outline(
radioButtonState,
outline,
outlineShape = CircleShape,
alignment = Stroke.Alignment.Inside,
)
val outlineModifier =
Modifier.size(metrics.outlineSizeFor(radioButtonState).value)
.outline(
state = radioButtonState,
outline = outline,
outlineShape = CircleShape,
alignment = Stroke.Alignment.Center,
)
val radioButtonPainter by style.icons.radioButton.getPainter(
Selected(radioButtonState),
Stateful(radioButtonState),
)
val radioButtonBoxModifier = Modifier.size(metrics.radioButtonSize)
if (content == null) {
RadioButtonImage(wrapperModifier, radioButtonPainter, radioButtonModifier)
Box(radioButtonBoxModifier, contentAlignment = Alignment.Center) {
RadioButtonImage(radioButtonPainter)
Box(outlineModifier)
}
} else {
Row(
wrapperModifier,
horizontalArrangement = Arrangement.spacedBy(metrics.iconContentGap),
verticalAlignment = Alignment.CenterVertically,
) {
RadioButtonImage(Modifier, radioButtonPainter, radioButtonModifier)
Box(radioButtonBoxModifier, contentAlignment = Alignment.Center) {
RadioButtonImage(radioButtonPainter)
Box(outlineModifier)
}
val contentColor by colors.contentFor(radioButtonState)
val resolvedContentColor = contentColor.takeOrElse { textStyle.color }
@@ -209,14 +219,8 @@ private fun RadioButtonImpl(
}
@Composable
private fun RadioButtonImage(
outerModifier: Modifier,
radioButtonPainter: Painter,
radioButtonModifier: Modifier,
) {
Box(outerModifier) {
Image(radioButtonPainter, contentDescription = null, modifier = radioButtonModifier)
}
private fun RadioButtonImage(radioButtonPainter: Painter, modifier: Modifier = Modifier) {
Box(modifier.paint(radioButtonPainter, alignment = Alignment.TopStart))
}
@Immutable

View File

@@ -10,7 +10,6 @@ import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.state.ToggleableState
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.DpSize
import org.jetbrains.jewel.foundation.GenerateDataFunctions
import org.jetbrains.jewel.ui.component.CheckboxState
@@ -52,12 +51,37 @@ public class CheckboxColors(
@GenerateDataFunctions
public class CheckboxMetrics(
public val checkboxSize: DpSize,
public val checkboxCornerSize: CornerSize,
public val outlineCornerSize: CornerSize,
public val outlineFocusedCornerSize: CornerSize,
public val outlineSelectedCornerSize: CornerSize,
public val outlineSelectedFocusedCornerSize: CornerSize,
public val outlineSize: DpSize,
public val outlineOffset: DpOffset,
public val outlineFocusedSize: DpSize,
public val outlineSelectedSize: DpSize,
public val outlineSelectedFocusedSize: DpSize,
public val iconContentGap: Dp,
) {
@Composable
public fun outlineCornerSizeFor(state: CheckboxState): State<CornerSize> = rememberUpdatedState(
when {
state.isFocused && state.isSelected -> outlineSelectedFocusedCornerSize
!state.isFocused && state.isSelected -> outlineSelectedCornerSize
state.isFocused && !state.isSelected -> outlineFocusedCornerSize
else -> outlineCornerSize
},
)
@Composable
public fun outlineSizeFor(state: CheckboxState): State<DpSize> = rememberUpdatedState(
when {
state.isFocused && state.isSelected -> outlineSelectedFocusedSize
!state.isFocused && state.isSelected -> outlineSelectedSize
state.isFocused && !state.isSelected -> outlineFocusedSize
else -> outlineSize
},
)
public companion object
}

View File

@@ -55,9 +55,23 @@ public class RadioButtonColors(
@GenerateDataFunctions
public class RadioButtonMetrics(
public val radioButtonSize: DpSize,
public val outlineSize: DpSize,
public val outlineFocusedSize: DpSize,
public val outlineSelectedSize: DpSize,
public val outlineSelectedFocusedSize: DpSize,
public val iconContentGap: Dp,
) {
@Composable
public fun outlineSizeFor(state: RadioButtonState): State<DpSize> = rememberUpdatedState(
when {
state.isFocused && state.isSelected -> outlineSelectedFocusedSize
!state.isFocused && state.isSelected -> outlineSelectedSize
state.isFocused && !state.isSelected -> outlineFocusedSize
else -> outlineSize
},
)
public companion object
}

View File

@@ -15,8 +15,15 @@ import androidx.compose.ui.graphics.withSaveLayer
import androidx.compose.ui.unit.Density
import org.jetbrains.jewel.ui.painter.badge.BadgeShape
/**
* Paints a badge over the [source].
*
* An area corresponding to the result of [BadgeShape.createHoleOutline]
* is cleared out first, to allow for visual separation with the badge,and
* then the [BadgeShape.createOutline] is filled with the [color].
*/
public class BadgePainter(
source: Painter,
private val source: Painter,
private val color: Color,
private val shape: BadgeShape,
) : DelegatePainter(source) {

View File

@@ -1,81 +0,0 @@
package org.jetbrains.jewel.ui.painter
import androidx.compose.ui.graphics.Color
import org.jetbrains.jewel.ui.painter.hints.Palette
import org.jetbrains.jewel.ui.util.fromRGBAHexStringOrNull
public abstract class BasePainterHintsProvider(
isDark: Boolean,
intellijIconPalette: Map<String, String?>,
themeIconPalette: Map<String, String?>,
themeColorPalette: Map<String, Color?>,
) : PainterHintsProvider {
private val checkBoxPaletteHint: PainterHint
private val treePaletteHint: PainterHint
private val uiPaletteHint: PainterHint
init {
val ui = mutableMapOf<Color, Color>()
val checkBoxes = mutableMapOf<Color, Color>()
val trees = mutableMapOf<Color, Color>()
@Suppress("LoopWithTooManyJumpStatements")
for ((key, value) in themeIconPalette) {
value ?: continue
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 = themeColorPalette[value]
// If either the key or the resolved value aren't valid colors, ignore the entry
val keyAsColor = resolveKeyColor(key, intellijIconPalette, isDark) ?: continue
val resolvedColor = namedColor ?: Color.fromRGBAHexStringOrNull(value) ?: continue
// Save the new entry (oldColor -> newColor) in the map
map[keyAsColor] = resolvedColor
}
checkBoxPaletteHint = Palette(checkBoxes)
treePaletteHint = Palette(trees)
uiPaletteHint = Palette(ui)
}
private fun selectMap(
key: String,
checkBoxes: MutableMap<Color, Color>,
trees: MutableMap<Color, Color>,
ui: MutableMap<Color, Color>,
) =
when {
key.startsWith("Checkbox.") -> checkBoxes
key.startsWith("Tree.iconColor.") -> trees
key.startsWith("Objects.") || key.startsWith("Actions.") || key.startsWith("#") -> ui
else -> null
}
// See com.intellij.ide.ui.UITheme.toColorString
private fun resolveKeyColor(
key: String,
keyPalette: Map<String, String?>,
isDark: Boolean,
): Color? {
val darkKey = "$key.Dark"
val resolvedKey = if (isDark && keyPalette.containsKey(darkKey)) darkKey else key
return Color.fromRGBAHexStringOrNull(keyPalette[resolvedKey] ?: return null)
}
protected fun getPaletteHint(path: String): PainterHint {
if (!path.contains("com/intellij/ide/ui/laf/icons/")) return uiPaletteHint
val file = path.substringAfterLast('/')
return when {
file == "treeCollapsed.svg" || file == "treeExpanded.svg" -> treePaletteHint
// ⚠️ 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") -> checkBoxPaletteHint
else -> PainterHint.None
}
}
}

View File

@@ -6,6 +6,10 @@ import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.LayoutDirection
/**
* A painter that delegates drawing to another [Painter], but can apply
* custom alphas, filters and layoutDirection to it.
*/
public open class DelegatePainter(private val delegate: Painter) : Painter() {
override val intrinsicSize: Size

View File

@@ -75,9 +75,7 @@ public interface XmlPainterHint : PainterHint {
@Immutable
public interface PainterPathHint : PainterHint {
/**
* Replace the entire path with the given value.
*/
/** Patch the path, if needed. */
public fun PainterProviderScope.patch(): String
}

View File

@@ -0,0 +1,73 @@
package org.jetbrains.jewel.ui.painter
import androidx.compose.ui.graphics.Color
import org.jetbrains.jewel.ui.util.fromRGBAHexStringOrNull
/** Provides the default [PainterHint]s to use to load images. */
public abstract class PalettePainterHintsProvider(
protected val isDark: Boolean,
protected val intellijIconPalette: Map<String, String?>,
protected val themeIconPalette: Map<String, String?>,
protected val themeColorPalette: Map<String, Color?>,
) : PainterHintsProvider {
protected abstract val checkBoxByKeyPaletteHint: PainterHint
protected abstract val checkBoxByColorPaletteHint: PainterHint
protected abstract val treePaletteHint: PainterHint
protected abstract val uiPaletteHint: PainterHint
protected fun resolveColor(value: String): Color? {
// If the value is one of the named colors in the theme, use that named color's value
val namedColor = themeColorPalette[value]
return namedColor ?: Color.fromRGBAHexStringOrNull(value)
}
// See com.intellij.ide.ui.UITheme.toColorString
protected fun resolveKeyColor(
key: String,
keyPalette: Map<String, String?>,
isDark: Boolean,
): Color? {
val darkKey = "$key.Dark"
val resolvedKey = if (isDark && keyPalette.containsKey(darkKey)) darkKey else key
return Color.fromRGBAHexStringOrNull(keyPalette[resolvedKey] ?: return null)
}
protected fun selectMap(
key: String,
checkboxes: MutableMap<Color, Color>,
trees: MutableMap<Color, Color>,
ui: MutableMap<Color, Color>,
): MutableMap<Color, Color>? =
when {
key.startsWith("Checkbox.") -> checkboxes
key.startsWith("Tree.iconColor.") -> trees
key.startsWith("Objects.") || key.startsWith("Actions.") || key.startsWith("#") -> ui
else -> null
}
/**
* Returns a [PainterHint] that can be used to patch colors for a resource
* with a given [path].
*
* The implementations vary depending on the path, and when running on the
* IntelliJ Platform, also on the IDE version and the current theme (New UI
* vs Classic UI).
*/
protected fun getPaletteHint(path: String, isNewUi: Boolean): PainterHint {
if (!path.contains("com/intellij/ide/ui/laf/icons/") && !path.contains("themes/expUI/icons/dark/")) return uiPaletteHint
val file = path.substringAfterLast('/')
// ⚠️ This next line is not a copy-paste error — the code in
// UITheme.PaletteScopeManager.getScopeByPath()
// says they share the same colors
val isCheckboxScope = file.startsWith("check") || file.startsWith("radio")
return when {
file == "treeCollapsed.svg" || file == "treeExpanded.svg" -> treePaletteHint
isNewUi && isCheckboxScope -> checkBoxByKeyPaletteHint
!isNewUi && isCheckboxScope -> checkBoxByColorPaletteHint
else -> PainterHint.None
}
}
}

View File

@@ -4,6 +4,7 @@ import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.painter.Painter
/** A delegate painter that overrides the intrinsic size of the [delegate]. */
public class ResizedPainter(delegate: Painter, private val size: Size) : DelegatePainter(delegate) {
override val intrinsicSize: Size

View File

@@ -87,13 +87,13 @@ public class ResourcePainterProvider(
val cacheKey = scope.acceptedHints.hashCode() * 31 + LocalDensity.current.hashCode()
if (inDebugMode && cache[cacheKey] != null) {
println("Cache hit for $basePath(${scope.acceptedHints.joinToString()})")
println("Cache hit for $basePath (accepted hints: ${scope.acceptedHints.joinToString()})")
}
val painter =
cache.getOrPut(cacheKey) {
if (inDebugMode) {
println("Cache miss for $basePath(${scope.acceptedHints.joinToString()})")
println("Cache miss for $basePath (accepted hints: ${scope.acceptedHints.joinToString()})")
}
loadPainter(scope)
}
@@ -110,14 +110,15 @@ public class ResourcePainterProvider(
scopes = scopes.flatMap { listOfNotNull(it.apply(hint), it) }
}
val (chosenScope, url) = scopes.firstNotNullOfOrNull { resolveResource(it) }
?: run {
if (inDebugMode) {
error("Resource '$basePath(${scope.acceptedHints.joinToString()})' not found")
} else {
return errorPainter
val (chosenScope, url) =
scopes.firstNotNullOfOrNull { resolveResource(it) }
?: run {
if (inDebugMode) {
error("Resource '$basePath(${scope.acceptedHints.joinToString()})' not found")
} else {
return errorPainter
}
}
}
val extension = basePath.substringAfterLast(".").lowercase()
@@ -151,7 +152,10 @@ public class ResourcePainterProvider(
}
@Composable
private fun createSvgPainter(scope: Scope, url: URL): Painter =
private fun createSvgPainter(
scope: Scope,
url: URL,
): Painter =
tryLoadingResource(
url = url,
loadingAction = { resourceUrl ->
@@ -165,7 +169,11 @@ public class ResourcePainterProvider(
rememberAction = { remember(url, scope.density) { it } },
)
private fun patchSvg(scope: Scope, inputStream: InputStream, hints: List<PainterHint>): InputStream {
private fun patchSvg(
scope: Scope,
inputStream: InputStream,
hints: List<PainterHint>,
): InputStream {
if (hints.all { it !is PainterSvgPatchHint }) {
return inputStream
}
@@ -179,12 +187,19 @@ public class ResourcePainterProvider(
with(hint) { scope.patch(document.documentElement) }
}
return document.writeToString().byteInputStream()
return document.writeToString()
.also { patchedSvg ->
if (inDebugMode) println("Patched SVG:\n\n$patchedSvg")
}
.byteInputStream()
}
}
@Composable
private fun createVectorDrawablePainter(scope: Scope, url: URL): Painter =
private fun createVectorDrawablePainter(
scope: Scope,
url: URL,
): Painter =
tryLoadingResource(
url = url,
loadingAction = { resourceUrl ->
@@ -194,15 +209,17 @@ public class ResourcePainterProvider(
)
@Composable
private fun createBitmapPainter(scope: Scope, url: URL) =
tryLoadingResource(
url = url,
loadingAction = { resourceUrl ->
val bitmap = resourceUrl.openStream().use { loadImageBitmap(it) }
BitmapPainter(bitmap)
},
rememberAction = { remember(url, scope.density) { it } },
)
private fun createBitmapPainter(
scope: Scope,
url: URL,
) = tryLoadingResource(
url = url,
loadingAction = { resourceUrl ->
val bitmap = resourceUrl.openStream().use { loadImageBitmap(it) }
BitmapPainter(bitmap)
},
rememberAction = { remember(url, scope.density) { it } },
)
@Composable
private fun <T> tryLoadingResource(
@@ -272,5 +289,7 @@ internal fun Document.writeToString(): String {
}
@Composable
public fun rememberResourcePainterProvider(path: String, iconClass: Class<*>): PainterProvider =
remember(path, iconClass.classLoader) { ResourcePainterProvider(path, iconClass.classLoader) }
public fun rememberResourcePainterProvider(
path: String,
iconClass: Class<*>,
): PainterProvider = remember(path, iconClass.classLoader) { ResourcePainterProvider(path, iconClass.classLoader) }

View File

@@ -8,9 +8,20 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.LayoutDirection
/**
* A shape used to draw badges. Badge shapes have a clear area surrounding
* them, whose outline is determined by [createHoleOutline].
*
* @see org.jetbrains.jewel.ui.painter.hints.Badge
* @see org.jetbrains.jewel.ui.painter.BadgePainter
*/
@Immutable
public interface BadgeShape : Shape {
/**
* Create the outline of the clear area (or "hole") surrounding this badge
* shape.
*/
public fun createHoleOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline
}

View File

@@ -19,5 +19,7 @@ private class BadgeImpl(
override fun PainterProviderScope.wrap(painter: Painter): Painter = BadgePainter(painter, color, shape)
}
/** Adds a colored badge to the image being loaded. */
@Suppress("FunctionName")
public fun Badge(color: Color, shape: BadgeShape): PainterHint =
if (color.isSpecified) BadgeImpl(color, shape) else PainterHint.None

View File

@@ -12,7 +12,7 @@ import kotlin.math.roundToInt
@Immutable
@GenerateDataFunctions
private class PaletteImpl(val map: Map<Color, Color>) : PainterSvgPatchHint {
private class ColorBasedReplacementPainterSvgPatchHint(val map: Map<Color, Color>) : PainterSvgPatchHint {
override fun PainterProviderScope.patch(element: Element) {
element.patchPalette(map)
@@ -94,5 +94,11 @@ private fun fromHexOrNull(rawColor: String, alpha: Float): Color? {
}
}
public fun Palette(map: Map<Color, Color>): PainterHint =
if (map.isEmpty()) PainterHint.None else PaletteImpl(map)
/**
* Creates a PainterHint that replaces all colors in the [paletteMap] with their
* corresponding new value. It is used in IJ up to 23.3 to support patching the
* SVG colors for checkboxes and radio buttons.
*/
@Suppress("FunctionName")
public fun ColorBasedPaletteReplacement(paletteMap: Map<Color, Color>): PainterHint =
if (paletteMap.isEmpty()) PainterHint.None else ColorBasedReplacementPainterSvgPatchHint(paletteMap)

View File

@@ -62,8 +62,33 @@ private class StrokeImpl(private val color: Color) : PainterSuffixHint(), Painte
)
}
/**
* Transforms an SVG image to only draw its borders in the provided
* [color]. All fills are removed.
*/
@Suppress("FunctionName")
public fun Stroke(color: Color): PainterHint =
if (color.isSpecified) StrokeImpl(color) else PainterHint.None
/**
* Switches between the light and dark variants of an image based on
* [isDark]. If no dark image exists, the light image will be used.
*
* All images that aren't dark images are base, or light, images.
*
* Dark images must be named in exactly the same way as the corresponding
* light image, but add a `_dark` suffix right before the extension. Dark
* images must be placed in the same directory and have the same extension
* as their light counterparts.
*
* Examples:
*
* | Light image name | Dark image name |
* |---------------------|--------------------------|
* | `my-icon.png` | `my-icon_dark.png` |
* | `my-icon@20x20.svg` | `my-icon@20x20_dark.svg` |
* | `my-icon@2x.png` | `my-icon@2x_dark.png` |
*/
@Suppress("FunctionName")
public fun Dark(isDark: Boolean = true): PainterHint =
if (isDark) DarkImpl else PainterHint.None

View File

@@ -11,12 +11,28 @@ private object HiDpiImpl : PainterSuffixHint() {
override fun PainterProviderScope.suffix(): String = "@2x"
override fun PainterProviderScope.canApply(): Boolean =
density > 1f && when (path.substringAfterLast('.').lowercase()) {
"svg", "xml" -> false
else -> true
}
density > 1f
override fun toString(): String = "HiDpi"
}
/**
* Selects the `@2x` HiDPI variant for bitmap images when running on a
* HiDPI screen.
*
* If an image does not have a HiDPI variant, the base image will be used.
*
* Note that combining a [Size] with [HiDpi] could lead to unexpected
* results and is not supported as of now. Generally speaking, however, the
* IntelliJ Platform tends to use only [Size] for SVGs and only [HiDpi]
* for PNGs, even though both are in theory supported for all formats.
*
* | Base image name | HiDPI image name |
* |-----------------|------------------|
* | `my-icon.png` | `my-icon@2x.png` |
* | `my-icon.svg` | N/A |
*
* @see Size
*/
@Suppress("FunctionName")
public fun HiDpi(): PainterHint = HiDpiImpl

View File

@@ -0,0 +1,68 @@
package org.jetbrains.jewel.ui.painter.hints
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import org.jetbrains.jewel.foundation.GenerateDataFunctions
import org.jetbrains.jewel.ui.painter.PainterHint
import org.jetbrains.jewel.ui.painter.PainterProviderScope
import org.jetbrains.jewel.ui.painter.PainterSvgPatchHint
import org.jetbrains.jewel.ui.util.toRgbaHexString
import org.w3c.dom.Element
@Immutable
@GenerateDataFunctions
private class KeyBasedReplacementPainterSvgPatchHint(val map: Map<String, Color>) : PainterSvgPatchHint {
override fun PainterProviderScope.patch(element: Element) {
element.patchPalette(map)
}
}
private fun Element.patchPalette(replacementColors: Map<String, Color>) {
val id = getAttribute("id").ifEmpty { null }
if (id != null) {
val (fillKey, strokeKey) = parseKeysFromId(id)
patchColorAttribute("fill", replacementColors[fillKey])
patchColorAttribute("stroke", replacementColors[strokeKey])
}
val nodes = childNodes
val length = nodes.length
for (i in 0 until length) {
val item = nodes.item(i)
if (item is Element) {
item.patchPalette(replacementColors)
}
}
}
private fun parseKeysFromId(id: String): Pair<String, String> {
val parts = id.split('_')
return if (parts.size == 2) {
parts.first() to parts.last()
} else {
id to id
}
}
private fun Element.patchColorAttribute(attrName: String, newColor: Color?) {
if (newColor == null) return
if (!hasAttribute(attrName)) return
setAttribute(attrName, newColor.copy(alpha = 1.0f).toRgbaHexString())
if (newColor.alpha != 1f) {
setAttribute("$attrName-opacity", newColor.alpha.toString())
} else {
removeAttribute("$attrName-opacity")
}
}
/**
* Creates a PainterHint that replaces colors with their corresponding new
* value, based on the IDs of each element. It is used in IJ 24.1 and later
* to support patching the SVG colors for checkboxes and radio buttons.
*/
@Suppress("FunctionName")
public fun KeyBasedPaletteReplacement(paletteMap: Map<String, Color>): PainterHint =
if (paletteMap.isEmpty()) PainterHint.None else KeyBasedReplacementPainterSvgPatchHint(paletteMap)

View File

@@ -1,21 +0,0 @@
package org.jetbrains.jewel.ui.painter.hints
import androidx.compose.runtime.Immutable
import org.jetbrains.jewel.foundation.GenerateDataFunctions
import org.jetbrains.jewel.ui.painter.PainterHint
import org.jetbrains.jewel.ui.painter.PainterPathHint
import org.jetbrains.jewel.ui.painter.PainterProviderScope
@Immutable
@GenerateDataFunctions
private class OverrideImpl(private val iconOverride: Map<String, String>) : PainterPathHint {
override fun PainterProviderScope.patch(): String = iconOverride[path] ?: path
}
public fun Override(override: Map<String, String>): PainterHint =
if (override.isEmpty()) {
PainterHint.None
} else {
OverrideImpl(override)
}

View File

@@ -0,0 +1,35 @@
package org.jetbrains.jewel.ui.painter.hints
import androidx.compose.runtime.Immutable
import org.jetbrains.jewel.foundation.GenerateDataFunctions
import org.jetbrains.jewel.ui.painter.PainterHint
import org.jetbrains.jewel.ui.painter.PainterPathHint
import org.jetbrains.jewel.ui.painter.PainterProviderScope
@Immutable
@GenerateDataFunctions
private class PathOverrideImpl(private val overrides: Map<String, String>) : PainterPathHint {
override fun PainterProviderScope.patch(): String =
overrides[path] ?: path
override fun toString(): String = "PathOverrideImpl(overrides=$overrides)"
}
/**
* A [PainterPathHint] that will override the paths passed as keys in the
* [overrides] map with the corresponding map values.
*
* This is used, for example, to implement the
* [New UI Icon Mapping](https://plugins.jetbrains.com/docs/intellij/icons.html#mapping-entries)
* when running in standalone mode. In the IntelliJ Platform, this logic is
* delegated to the platform by
* [org.jetbrains.jewel.bridge.BridgeOverride].
*/
@Suppress("FunctionName")
public fun PathOverride(overrides: Map<String, String>): PainterHint =
if (overrides.isEmpty()) {
PainterHint.None
} else {
PathOverrideImpl(overrides)
}

View File

@@ -14,7 +14,20 @@ private object SelectedImpl : PainterSuffixHint() {
override fun toString(): String = "Selected"
}
/**
* Selects the "selected" variant of an image when [selected] is true. If
* an image does not have a selected variant, the base version will be
* used.
*
* | Base image name | Selected image name |
* |-----------------------|-------------------------------|
* | `my-icon.png` | `my-iconSelected.png` |
* | `myIcon@20x20.svg` | `myIconSelected@20x20.svg` |
* | `my-icon@2x_dark.png` | `my-iconSelected@2x_dark.png` |
*/
@Suppress("FunctionName")
public fun Selected(selected: Boolean = true): PainterHint =
if (selected) SelectedImpl else PainterHint.None
@Suppress("FunctionName")
public fun Selected(state: SelectableComponentState): PainterHint = Selected(state.isSelected)

View File

@@ -35,7 +35,46 @@ private class SizeImpl(
}
}
public fun Size(width: Int, height: Int = width): PainterHint {
/**
* Selects a size variant for the image. If the specific size that was
* requested is not available, the base image will be used.
*
* Note that combining a [Size] with [HiDpi] could lead to unexpected
* results and is not supported as of now. Generally speaking, however, the
* IntelliJ Platform tends to use only [Size] for SVGs and only [HiDpi]
* for PNGs, even though both are in theory supported for all formats.
*
* | Base image name | Sized image name |
* |--------------------|--------------------------|
* | `my-icon.svg` | `my-icon@20x20.svg` |
* | `my-icon_dark.png` | `my-icon@14x14_dark.png` |
*
* @see HiDpi
*/
@Suppress("FunctionName")
public fun Size(size: Int): PainterHint {
require(size > 0) { "Size must be positive" }
return SizeImpl(size, size)
}
/**
* Selects a size variant for the image. If the specific size that was
* requested is not available, the base image will be used.
*
* Note that combining a [Size] with [HiDpi] could lead to unexpected
* results and is not supported as of now. Generally speaking, however, the
* IntelliJ Platform tends to use only [Size] for SVGs and only [HiDpi]
* for PNGs, even though both are in theory supported for all formats.
*
* | Base image name | Sized image name |
* |--------------------|--------------------------|
* | `my-icon.svg` | `my-icon@20x20.svg` |
* | `my-icon_dark.png` | `my-icon@14x14_dark.png` |
*
* @see HiDpi
*/
@Suppress("FunctionName")
public fun Size(width: Int, height: Int): PainterHint {
require(width > 0 && height > 0) { "Width and height must be positive" }
return SizeImpl(width, height)
}

View File

@@ -25,4 +25,30 @@ private class StatefulImpl(private val state: InteractiveComponentState) : Paint
}
}
/**
* Selects a stateful variant of an image, based on the current [state].
* Stateful variants are `Focused`, `Pressed`, `Hovered`, and `Disabled`.
* If an image does not have the required stateful variant, the base one
* will be used.
*
* If the [state] is a [FocusableComponentState] and its
* [`isFocused`][FocusableComponentState.isFocused] property
* is true, then the `Focused` variant will be used.
*
* For the base image name `myIcon.svg`, for example:
*
* | State | Stateful image names |
* |------------------|----------------------|
* | Disabled | `myIconDisabled.svg` |
* | Enabled, focused | `myIconFocused.svg` |
* | Enabled, pressed | `myIconPressed.svg` |
* | Enabled, hovered | `myIconHovered.svg` |
* | Enabled, at rest | `myIcon.svg` |
*
* Note that the
* [Swing Compat mode][org.jetbrains.jewel.foundation.theme.JewelTheme.isSwingCompatMode]
* value might prevent the selection of the pressed and hovered states,
* when true.
*/
@Suppress("FunctionName")
public fun Stateful(state: InteractiveComponentState): PainterHint = StatefulImpl(state)

View File

@@ -4,6 +4,28 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import kotlin.math.roundToInt
/**
* Converts a [java.awt.Color] to a RGBA formatted color `#RRGGBBAA` hex
* string; e.g., `#FFFFFF1A` (a translucent white).
*/
public fun java.awt.Color.toRgbaHexString(): String {
val r = Integer.toHexString(red)
val g = Integer.toHexString(green)
val b = Integer.toHexString(blue)
return buildString {
append('#')
append(r.padStart(2, '0'))
append(g.padStart(2, '0'))
append(b.padStart(2, '0'))
if (alpha < 255) {
val a = Integer.toHexString(alpha)
append(a.padStart(2, '0'))
}
}
}
/**
* Converts a [Color] to a RGBA formatted color `#RRGGBBAA` hex string;
* e.g., `#FFFFFF1A` (a translucent white).
@@ -46,8 +68,6 @@ public fun Color.Companion.fromRGBAHexStringOrNull(rgba: String): Color? =
?.toLongOrNull(radix = 16)
?.let { Color(it) }
/**
* Heuristically determines if the color can be thought of as "dark".
*/
/** Heuristically determines if the color can be thought of as "dark". */
public fun Color.isDark(): Boolean =
(luminance() + 0.05) / 0.05 < 4.5

View File

@@ -10,10 +10,10 @@ import org.jetbrains.jewel.ui.painter.PainterHint
import org.jetbrains.jewel.ui.painter.PainterPathHint
import org.jetbrains.jewel.ui.painter.PainterProviderScope
import org.jetbrains.jewel.ui.painter.PainterSvgPatchHint
import org.jetbrains.jewel.ui.painter.hints.ColorBasedPaletteReplacement
import org.jetbrains.jewel.ui.painter.hints.Dark
import org.jetbrains.jewel.ui.painter.hints.HiDpi
import org.jetbrains.jewel.ui.painter.hints.Override
import org.jetbrains.jewel.ui.painter.hints.Palette
import org.jetbrains.jewel.ui.painter.hints.PathOverride
import org.jetbrains.jewel.ui.painter.hints.Selected
import org.jetbrains.jewel.ui.painter.hints.Size
import org.jetbrains.jewel.ui.painter.hints.Stateful
@@ -21,6 +21,7 @@ import org.jetbrains.jewel.ui.painter.hints.Stroke
import org.jetbrains.jewel.ui.painter.rememberResourcePainterProvider
import org.jetbrains.jewel.ui.painter.writeToString
import org.junit.Assert
import org.junit.Assert.assertEquals
import org.junit.Test
import javax.xml.XMLConstants
import javax.xml.parsers.DocumentBuilderFactory
@@ -41,8 +42,8 @@ class PainterHintTest : BasicJewelUiTest() {
// must be ignored the None and hit cache too
val painter3 by provider.getPainter(PainterHint.None, PainterHint.None)
Assert.assertEquals(painter1, painter2)
Assert.assertEquals(painter3, painter2)
assertEquals(painter1, painter2)
assertEquals(painter3, painter2)
}
}) {
awaitIdle()
@@ -91,79 +92,72 @@ class PainterHintTest : BasicJewelUiTest() {
fun `dark painter hint should append suffix when isDark is true`() {
val basePath = "icons/github.svg"
val patchedPath = testScope(basePath).applyPathHints(Dark(true))
Assert.assertEquals("icons/github_dark.svg", patchedPath)
assertEquals("icons/github_dark.svg", patchedPath)
}
@Test
fun `dark painter hint should not append suffix when isDark is false`() {
val basePath = "icons/github.svg"
val patchedPath = testScope(basePath).applyPathHints(Dark(false))
Assert.assertEquals(basePath, patchedPath)
assertEquals(basePath, patchedPath)
}
@Test
fun `override painter hint should replace path entirely`() {
val basePath = "icons/github.svg"
val patchedPath = testScope(basePath)
.applyPathHints(Override(mapOf("icons/github.svg" to "icons/search.svg")))
Assert.assertEquals("icons/search.svg", patchedPath)
.applyPathHints(PathOverride(mapOf("icons/github.svg" to "icons/search.svg")))
assertEquals("icons/search.svg", patchedPath)
}
@Test
fun `override painter hint should not replace path when not matched`() {
val basePath = "icons/github.svg"
val patchedPath = testScope(basePath)
.applyPathHints(Override(mapOf("icons/settings.svg" to "icons/search.svg")))
Assert.assertEquals(basePath, patchedPath)
.applyPathHints(PathOverride(mapOf("icons/settings.svg" to "icons/search.svg")))
assertEquals(basePath, patchedPath)
}
@Test
fun `selected painter hint should append suffix when selected is true`() {
val basePath = "icons/checkbox.svg"
val patchedPath = testScope(basePath).applyPathHints(Selected(true))
Assert.assertEquals("icons/checkboxSelected.svg", patchedPath)
assertEquals("icons/checkboxSelected.svg", patchedPath)
}
@Test
fun `selected painter hint should not append suffix when selected is false`() {
val basePath = "icons/checkbox.svg"
val patchedPath = testScope(basePath).applyPathHints(Selected(false))
Assert.assertEquals(basePath, patchedPath)
assertEquals(basePath, patchedPath)
}
@Test
fun `size painter hint should append suffix`() {
val basePath = "icons/github.svg"
val patchedPath = testScope(basePath).applyPathHints(Size(20))
Assert.assertEquals("icons/github@20x20.svg", patchedPath)
}
@Test
fun `highDpi painter hint should not append suffix for svg`() {
val basePath = "icons/github.svg"
val patchedPath = testScope(basePath, 2f).applyPathHints(HiDpi())
Assert.assertEquals(basePath, patchedPath)
assertEquals("icons/github@20x20.svg", patchedPath)
}
@Test
fun `highDpi painter hint should append suffix when isHiDpi is true`() {
val basePath = "icons/github.png"
val patchedPath = testScope(basePath, 2f).applyPathHints(HiDpi())
Assert.assertEquals("icons/github@2x.png", patchedPath)
assertEquals("icons/github@2x.png", patchedPath)
}
@Test
fun `highDpi painter hint should not append suffix when isHiDpi is false`() {
val basePath = "icons/github.png"
val patchedPath = testScope(basePath, 1f).applyPathHints(HiDpi())
Assert.assertEquals(basePath, patchedPath)
assertEquals(basePath, patchedPath)
}
@Test
fun `size painter hint should not append suffix for bitmap`() {
val basePath = "icons/github.png"
val patchedPath = testScope(basePath).applyPathHints(Size(20))
Assert.assertEquals(basePath, patchedPath)
assertEquals(basePath, patchedPath)
}
@Test
@@ -184,11 +178,11 @@ class PainterHintTest : BasicJewelUiTest() {
val basePath = "icons/checkbox.svg"
val state = CheckboxState.of(toggleableState = ToggleableState.Off)
val patchedPath = testScope(basePath).applyPathHints(Stateful(state.copy(enabled = false)))
Assert.assertEquals("icons/checkboxDisabled.svg", patchedPath)
assertEquals("icons/checkboxDisabled.svg", patchedPath)
testScope(basePath)
.applyPathHints(Stateful(state.copy(enabled = false, pressed = true, hovered = true, focused = true)))
.let { Assert.assertEquals("icons/checkboxDisabled.svg", it) }
.let { assertEquals("icons/checkboxDisabled.svg", it) }
}
@Test
@@ -197,7 +191,7 @@ class PainterHintTest : BasicJewelUiTest() {
val state = CheckboxState.of(toggleableState = ToggleableState.Off)
val patchedPath = testScope(basePath)
.applyPathHints(Stateful(state.copy(enabled = false, pressed = true, hovered = true, focused = true)))
Assert.assertEquals("icons/checkboxDisabled.svg", patchedPath)
assertEquals("icons/checkboxDisabled.svg", patchedPath)
}
@Test
@@ -205,7 +199,7 @@ class PainterHintTest : BasicJewelUiTest() {
val basePath = "icons/checkbox.svg"
val state = CheckboxState.of(toggleableState = ToggleableState.Off)
val patchedPath = testScope(basePath).applyPathHints(Stateful(state.copy(focused = true)))
Assert.assertEquals("icons/checkboxFocused.svg", patchedPath)
assertEquals("icons/checkboxFocused.svg", patchedPath)
}
@Test
@@ -214,7 +208,7 @@ class PainterHintTest : BasicJewelUiTest() {
val state = CheckboxState.of(toggleableState = ToggleableState.Off)
val patchedPath = testScope(basePath)
.applyPathHints(Stateful(state.copy(pressed = true, hovered = true, focused = true)))
Assert.assertEquals("icons/checkboxFocused.svg", patchedPath)
assertEquals("icons/checkboxFocused.svg", patchedPath)
}
@Test
@@ -222,7 +216,7 @@ class PainterHintTest : BasicJewelUiTest() {
val basePath = "icons/checkbox.svg"
val state = CheckboxState.of(toggleableState = ToggleableState.Off)
val patchedPath = testScope(basePath).applyPathHints(Stateful(state.copy(pressed = true)))
Assert.assertEquals("icons/checkboxPressed.svg", patchedPath)
assertEquals("icons/checkboxPressed.svg", patchedPath)
}
@Test
@@ -231,7 +225,7 @@ class PainterHintTest : BasicJewelUiTest() {
val state = CheckboxState.of(toggleableState = ToggleableState.Off)
val patchedPath = testScope(basePath)
.applyPathHints(Stateful(state.copy(pressed = true, hovered = true)))
Assert.assertEquals("icons/checkboxPressed.svg", patchedPath)
assertEquals("icons/checkboxPressed.svg", patchedPath)
}
@Test
@@ -239,38 +233,38 @@ class PainterHintTest : BasicJewelUiTest() {
val basePath = "icons/checkbox.svg"
val state = CheckboxState.of(toggleableState = ToggleableState.Off)
val patchedPath = testScope(basePath).applyPathHints(Stateful(state.copy(hovered = true)))
Assert.assertEquals("icons/checkboxHovered.svg", patchedPath)
assertEquals("icons/checkboxHovered.svg", patchedPath)
}
@Test
fun `stroke painter hint should append suffix when color is Specified`() {
val basePath = "icons/rerun.svg"
val patchedPath = testScope(basePath).applyPathHints(Stroke(Color.White))
Assert.assertEquals("icons/rerun_stroke.svg", patchedPath)
assertEquals("icons/rerun_stroke.svg", patchedPath)
}
@Test
fun `stroke painter hint should not append suffix when color is Unspecified`() {
val basePath = "icons/rerun.svg"
val patchedPath = testScope(basePath).applyPathHints(Stroke(Color.Unspecified))
Assert.assertEquals(basePath, patchedPath)
assertEquals(basePath, patchedPath)
}
@Test
fun `palette painter hint should patch colors correctly in SVG`() {
val baseSvg =
"""
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect fill="#000000" height="20" stroke="#000000" stroke-opacity="0.5" width="20" x="2" y="2"/>
<rect fill="#00ff00" height="16" width="16" x="4" y="4"/>
<rect fill="#123456" height="12" width="12" x="6" y="6"/>
</svg>
""".trimIndent()
|<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
| <rect fill="#000000" height="20" stroke="#000000" stroke-opacity="0.5" width="20" x="2" y="2"/>
| <rect fill="#00ff00" height="16" width="16" x="4" y="4"/>
| <rect fill="#123456" height="12" width="12" x="6" y="6"/>
|</svg>
""".trimMargin()
val patchedSvg = testScope("fake_icon.svg")
.applyPaletteHints(
baseSvg,
Palette(
ColorBasedPaletteReplacement(
mapOf(
Color(0x80000000) to Color(0xFF123456),
Color.Black to Color.White,
@@ -278,15 +272,16 @@ class PainterHintTest : BasicJewelUiTest() {
),
),
)
.replace("\r\n", "\n")
Assert.assertEquals(
assertEquals(
"""
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<rect fill="#ffffff" height="20" stroke="#123456" stroke-opacity="1.0" width="20" x="2" y="2"/>
<rect fill="#ff0000" height="16" width="16" x="4" y="4"/>
<rect fill="#123456" height="12" width="12" x="6" y="6"/>
</svg>
""".trimIndent(),
|<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
| <rect fill="#ffffff" height="20" stroke="#123456" stroke-opacity="1.0" width="20" x="2" y="2"/>
| <rect fill="#ff0000" height="16" width="16" x="4" y="4"/>
| <rect fill="#123456" height="12" width="12" x="6" y="6"/>
|</svg>
""".trimMargin(),
patchedSvg,
)
}