From 685ba724631515c12f80cb2d545b972f214f9e79 Mon Sep 17 00:00:00 2001 From: Sebastiano Poggi Date: Thu, 1 Feb 2024 20:19:39 +0100 Subject: [PATCH] Fix icon loading and errors in bridge for 241 --- .editorconfig | 1 + .github/workflows/build.yml | 2 +- buildSrc/src/main/kotlin/LocalProperties.kt | 12 -- buildSrc/src/main/kotlin/jewel.gradle.kts | 2 +- .../demodata/AndroidStudioReleases.kt | 4 +- .../ideversion/CheckIdeaVersionTask.kt | 4 +- .../theme/IntelliJThemeGeneratorPlugin.kt | 4 +- gradle/libs.versions.toml | 2 +- ide-laf-bridge/api/ide-laf-bridge.api | 4 +- .../jetbrains/jewel/bridge/BridgeOverride.kt | 8 +- .../bridge/BridgePainterHintsProvider.kt | 125 +++++++++++-- .../jewel/bridge/theme/IntUiBridge.kt | 169 ++++++++++++++++-- .../api/int-ui-standalone.api | 10 +- int-ui/int-ui-standalone/build.gradle.kts | 1 + .../StandalonePainterHintsProvider.kt | 113 ++++++++++-- .../styling/IntUiCheckboxStyling.kt | 29 +-- .../styling/IntUiRadioButtonStyling.kt | 15 +- .../themes/expUI/icons/dark/checkBox.svg | 4 +- .../expUI/icons/dark/checkBoxDisabled.svg | 4 +- .../expUI/icons/dark/checkBoxFocused.svg | 5 +- .../expUI/icons/dark/checkBoxFocusedTemp.svg | 6 - .../dark/checkBoxIndeterminateSelected.svg | 6 +- .../checkBoxIndeterminateSelectedDisabled.svg | 6 +- .../checkBoxIndeterminateSelectedFocused.svg | 8 +- .../expUI/icons/dark/checkBoxSelected.svg | 6 +- .../icons/dark/checkBoxSelectedDisabled.svg | 6 +- .../icons/dark/checkBoxSelectedFocused.svg | 8 +- .../themes/expUI/icons/dark/checkBoxTemp.svg | 5 - .../themes/expUI/icons/dark/radio.svg | 4 +- .../themes/expUI/icons/dark/radioDisabled.svg | 4 +- .../themes/expUI/icons/dark/radioFocused.svg | 4 +- .../expUI/icons/dark/radioFocusedTemp.svg | 6 - .../themes/expUI/icons/dark/radioSelected.svg | 6 +- .../icons/dark/radioSelectedDisabled.svg | 6 +- .../expUI/icons/dark/radioSelectedFocused.svg | 6 +- .../themes/expUI/icons/dark/radioTemp.svg | 5 - .../samples/ideplugin/ComponentShowcaseTab.kt | 35 +++- .../ideplugin/JewelDemoToolWindowFactory.kt | 1 + ui/api/ui.api | 62 +++++-- .../jetbrains/jewel/ui/component/Checkbox.kt | 45 +++-- .../jewel/ui/component/RadioButton.kt | 40 +++-- .../ui/component/styling/CheckboxStyling.kt | 30 +++- .../component/styling/RadioButtonStyling.kt | 14 ++ .../jewel/ui/painter/BadgePainter.kt | 9 +- .../ui/painter/BasePainterHintsProvider.kt | 81 --------- .../jewel/ui/painter/DelegatePainter.kt | 4 + .../jetbrains/jewel/ui/painter/PainterHint.kt | 4 +- .../ui/painter/PalettePainterHintsProvider.kt | 73 ++++++++ .../jewel/ui/painter/ResizedPainter.kt | 1 + .../ui/painter/ResourcePainterProvider.kt | 67 ++++--- .../jewel/ui/painter/badge/BadgeShape.kt | 11 ++ .../jetbrains/jewel/ui/painter/hints/Badge.kt | 2 + ...tte.kt => ColorBasedPaletteReplacement.kt} | 12 +- .../{DarkOrStroke.kt => DarkAndStroke.kt} | 25 +++ .../jetbrains/jewel/ui/painter/hints/HiDpi.kt | 24 ++- .../hints/KeyBasedPaletteReplacement.kt | 68 +++++++ .../jewel/ui/painter/hints/Override.kt | 21 --- .../jewel/ui/painter/hints/PathOverride.kt | 35 ++++ .../jewel/ui/painter/hints/Selected.kt | 13 ++ .../jetbrains/jewel/ui/painter/hints/Size.kt | 41 ++++- .../jewel/ui/painter/hints/Stateful.kt | 26 +++ .../jewel/ui/util/ColorExtensions.kt | 26 ++- .../org/jetbrains/jewel/PainterHintTest.kt | 89 +++++---- 63 files changed, 1067 insertions(+), 402 deletions(-) delete mode 100644 buildSrc/src/main/kotlin/LocalProperties.kt delete mode 100644 int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxFocusedTemp.svg delete mode 100644 int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxTemp.svg delete mode 100644 int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioFocusedTemp.svg delete mode 100644 int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioTemp.svg delete mode 100644 ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/BasePainterHintsProvider.kt create mode 100644 ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/PalettePainterHintsProvider.kt rename ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/{Palette.kt => ColorBasedPaletteReplacement.kt} (86%) rename ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/{DarkOrStroke.kt => DarkAndStroke.kt} (70%) create mode 100644 ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/KeyBasedPaletteReplacement.kt delete mode 100644 ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Override.kt create mode 100644 ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/PathOverride.kt diff --git a/.editorconfig b/.editorconfig index 1b0fa69959..6086feba12 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5760faeb7f..433b2655f0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/buildSrc/src/main/kotlin/LocalProperties.kt b/buildSrc/src/main/kotlin/LocalProperties.kt deleted file mode 100644 index 260845cb26..0000000000 --- a/buildSrc/src/main/kotlin/LocalProperties.kt +++ /dev/null @@ -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) -} diff --git a/buildSrc/src/main/kotlin/jewel.gradle.kts b/buildSrc/src/main/kotlin/jewel.gradle.kts index ee92d92f24..715f9e2204 100644 --- a/buildSrc/src/main/kotlin/jewel.gradle.kts +++ b/buildSrc/src/main/kotlin/jewel.gradle.kts @@ -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( diff --git a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/demodata/AndroidStudioReleases.kt b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/demodata/AndroidStudioReleases.kt index edab5bbfbe..110ad0f503 100644 --- a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/demodata/AndroidStudioReleases.kt +++ b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/demodata/AndroidStudioReleases.kt @@ -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(it) } val className = ClassName.bestGuess(STUDIO_RELEASES_OUTPUT_CLASS_NAME) diff --git a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/ideversion/CheckIdeaVersionTask.kt b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/ideversion/CheckIdeaVersionTask.kt index 735a734699..089bd1e2da 100644 --- a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/ideversion/CheckIdeaVersionTask.kt +++ b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/ideversion/CheckIdeaVersionTask.kt @@ -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>(it) } .first() diff --git a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntelliJThemeGeneratorPlugin.kt b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntelliJThemeGeneratorPlugin.kt index 33559e23ef..a835322622 100644 --- a/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntelliJThemeGeneratorPlugin.kt +++ b/buildSrc/src/main/kotlin/org/jetbrains/jewel/buildlogic/theme/IntelliJThemeGeneratorPlugin.kt @@ -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) : NamedDomainObjectContainer 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(it) } val className = ClassName.bestGuess(themeClassName.get()) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index aa0097a6a7..d49e0e7153 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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-EAP-CANDIDATE-SNAPSHOT" ideaGradlePlugin = "1.17.0" jna = "5.14.0" kotlin = "1.8.21" diff --git a/ide-laf-bridge/api/ide-laf-bridge.api b/ide-laf-bridge/api/ide-laf-bridge.api index b0d51fad0a..42235da257 100644 --- a/ide-laf-bridge/api/ide-laf-bridge.api +++ b/ide-laf-bridge/api/ide-laf-bridge.api @@ -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 (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 { diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeOverride.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeOverride.kt index e6a9af4231..5291ab3043 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeOverride.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgeOverride.kt @@ -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) } diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePainterHintsProvider.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePainterHintsProvider.kt index 839a666573..d87425c581 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePainterHintsProvider.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePainterHintsProvider.kt @@ -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 = emptyMap(), themeIconPalette: Map = emptyMap(), themeColorPalette: Map = emptyMap(), -) : BasePainterHintsProvider(isDark, intellijIconPalette, themeIconPalette, themeColorPalette) { +) : PalettePainterHintsProvider(isDark, intellijIconPalette, themeIconPalette, themeColorPalette) { - @Composable - override fun hints(path: String): List = buildList { - add(getPaletteHint(path)) - add(BridgeOverride) - add(HiDpi()) - add(Dark(JewelTheme.isDark)) + override val checkBoxByColorPaletteHint: PainterHint + override val checkBoxByKeyPaletteHint: PainterHint + override val treePaletteHint: PainterHint + override val uiPaletteHint: PainterHint + + init { + val ui = mutableMapOf() + val checkBoxesByColor = mutableMapOf() + val checkBoxesByKey = mutableMapOf() + val trees = mutableMapOf() + + @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, + 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, + 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 = + 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).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 = + 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", + ) } } diff --git a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridge.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridge.kt index 107e6b1f26..d27b083a43 100644 --- a/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridge.kt +++ b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/theme/IntUiBridge.kt @@ -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" +} diff --git a/int-ui/int-ui-standalone/api/int-ui-standalone.api b/int-ui/int-ui-standalone/api/int-ui-standalone.api index 66183c5b5a..1967c2698f 100644 --- a/int-ui/int-ui-standalone/api/int-ui-standalone.api +++ b/int-ui/int-ui-standalone/api/int-ui-standalone.api @@ -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 (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; diff --git a/int-ui/int-ui-standalone/build.gradle.kts b/int-ui/int-ui-standalone/build.gradle.kts index b7eefadad2..bf7c3d54b4 100644 --- a/int-ui/int-ui-standalone/build.gradle.kts +++ b/int-ui/int-ui-standalone/build.gradle.kts @@ -27,6 +27,7 @@ tasks { dependsOn("generateIntUiDarkTheme") dependsOn("generateIntUiLightTheme") } + named("sourcesJar") { dependsOn("generateIntUiDarkTheme") dependsOn("generateIntUiLightTheme") diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/StandalonePainterHintsProvider.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/StandalonePainterHintsProvider.kt index 052c067222..141afaaddc 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/StandalonePainterHintsProvider.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/StandalonePainterHintsProvider.kt @@ -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 = buildList { - add(getPaletteHint(path)) - add(overrideHint) - add(HiDpi()) - add(Dark(JewelTheme.isDark)) + init { + val ui = mutableMapOf() + val checkBoxesByColor = mutableMapOf() + val checkBoxesByKey = mutableMapOf() + val trees = mutableMapOf() + + @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, + 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, + 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 = + 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 = + 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", + ) } } diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiCheckboxStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiCheckboxStyling.kt index f4729f20fc..a65c80b593 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiCheckboxStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiCheckboxStyling.kt @@ -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 diff --git a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiRadioButtonStyling.kt b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiRadioButtonStyling.kt index 0f683234ce..842c35d243 100644 --- a/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiRadioButtonStyling.kt +++ b/int-ui/int-ui-standalone/src/main/kotlin/org/jetbrains/jewel/intui/standalone/styling/IntUiRadioButtonStyling.kt @@ -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 = diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBox.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBox.svg index f6a69e272d..4fd5cfef2c 100644 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBox.svg +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBox.svg @@ -1,3 +1,3 @@ - - + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxDisabled.svg index 54369e3934..ee471fb486 100644 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxDisabled.svg +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxDisabled.svg @@ -1,3 +1,3 @@ - - + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxFocused.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxFocused.svg index cd844a2a4b..a4f97b8ef8 100644 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxFocused.svg +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxFocused.svg @@ -1,4 +1,3 @@ - - - + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxFocusedTemp.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxFocusedTemp.svg deleted file mode 100644 index 8615c4cc45..0000000000 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxFocusedTemp.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelected.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelected.svg index fa7ec87d18..fa33475d79 100644 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelected.svg +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelected.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelectedDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelectedDisabled.svg index b4390406d3..2f644360e8 100644 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelectedDisabled.svg +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelectedDisabled.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelectedFocused.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelectedFocused.svg index 52fff945f1..984c7718ed 100644 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelectedFocused.svg +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxIndeterminateSelectedFocused.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelected.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelected.svg index e45634c828..0c4b4c054e 100644 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelected.svg +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelected.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelectedDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelectedDisabled.svg index f8bcd2ae59..9d9bc40681 100644 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelectedDisabled.svg +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelectedDisabled.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelectedFocused.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelectedFocused.svg index 0023014d26..effc5248d9 100644 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelectedFocused.svg +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxSelectedFocused.svg @@ -1,5 +1,5 @@ - - - - + + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxTemp.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxTemp.svg deleted file mode 100644 index 0a87d5a3d0..0000000000 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/checkBoxTemp.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radio.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radio.svg index 736c4d3f2d..ed961c8d8f 100644 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radio.svg +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radio.svg @@ -1,3 +1,3 @@ - - + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioDisabled.svg index da3a396a0a..f4614a6583 100644 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioDisabled.svg +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioDisabled.svg @@ -1,3 +1,3 @@ - - + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioFocused.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioFocused.svg index 2d23cdfe86..1cfe677f41 100644 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioFocused.svg +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioFocused.svg @@ -1,3 +1,3 @@ - - + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioFocusedTemp.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioFocusedTemp.svg deleted file mode 100644 index 2e303ca374..0000000000 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioFocusedTemp.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelected.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelected.svg index 6b9b836dfe..54b3f93b97 100644 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelected.svg +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelected.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelectedDisabled.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelectedDisabled.svg index df5695d44a..0d74216795 100644 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelectedDisabled.svg +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelectedDisabled.svg @@ -1,4 +1,4 @@ - - - + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelectedFocused.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelectedFocused.svg index fc446c25b5..8838954056 100644 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelectedFocused.svg +++ b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioSelectedFocused.svg @@ -1,5 +1,5 @@ - - - + + + diff --git a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioTemp.svg b/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioTemp.svg deleted file mode 100644 index c59b724291..0000000000 --- a/int-ui/int-ui-standalone/src/main/resources/themes/expUI/icons/dark/radioTemp.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt index f801a42e8b..6264bd882a 100644 --- a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/ComponentShowcaseTab.kt @@ -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") } } diff --git a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/JewelDemoToolWindowFactory.kt b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/JewelDemoToolWindowFactory.kt index 4cbafee94c..f6fe65fcde 100644 --- a/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/JewelDemoToolWindowFactory.kt +++ b/samples/ide-plugin/src/main/kotlin/org/jetbrains/jewel/samples/ideplugin/JewelDemoToolWindowFactory.kt @@ -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 { diff --git a/ui/api/ui.api b/ui/api/ui.api index 4cc34977a0..e39190711c 100644 --- a/ui/api/ui.api +++ b/ui/api/ui.api @@ -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 (JLandroidx/compose/foundation/shape/CornerSize;JJFLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (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 (JFLkotlin/jvm/internal/DefaultConstructorMarker;)V + public synthetic fun (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 (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 (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 (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 (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; } diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Checkbox.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Checkbox.kt index 2b364f7e2b..c8f927e124 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Checkbox.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/Checkbox.kt @@ -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, diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/RadioButton.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/RadioButton.kt index 7c992493ad..e23d71d3ed 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/RadioButton.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/RadioButton.kt @@ -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 diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/CheckboxStyling.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/CheckboxStyling.kt index cbe3822d34..31fa37520c 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/CheckboxStyling.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/CheckboxStyling.kt @@ -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 = 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 = rememberUpdatedState( + when { + state.isFocused && state.isSelected -> outlineSelectedFocusedSize + !state.isFocused && state.isSelected -> outlineSelectedSize + state.isFocused && !state.isSelected -> outlineFocusedSize + else -> outlineSize + }, + ) + public companion object } diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/RadioButtonStyling.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/RadioButtonStyling.kt index a3eb2ba41a..035738a3b4 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/RadioButtonStyling.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/component/styling/RadioButtonStyling.kt @@ -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 = rememberUpdatedState( + when { + state.isFocused && state.isSelected -> outlineSelectedFocusedSize + !state.isFocused && state.isSelected -> outlineSelectedSize + state.isFocused && !state.isSelected -> outlineFocusedSize + else -> outlineSize + }, + ) + public companion object } diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/BadgePainter.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/BadgePainter.kt index 66c229f36e..2576eec515 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/BadgePainter.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/BadgePainter.kt @@ -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) { diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/BasePainterHintsProvider.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/BasePainterHintsProvider.kt deleted file mode 100644 index 9639fdd3c7..0000000000 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/BasePainterHintsProvider.kt +++ /dev/null @@ -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, - themeIconPalette: Map, - themeColorPalette: Map, -) : PainterHintsProvider { - - private val checkBoxPaletteHint: PainterHint - private val treePaletteHint: PainterHint - private val uiPaletteHint: PainterHint - - init { - val ui = mutableMapOf() - val checkBoxes = mutableMapOf() - val trees = mutableMapOf() - - @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, - trees: MutableMap, - ui: MutableMap, - ) = - 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, - 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 - } - } -} diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/DelegatePainter.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/DelegatePainter.kt index ffa5b30d50..3df70e8266 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/DelegatePainter.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/DelegatePainter.kt @@ -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 diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/PainterHint.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/PainterHint.kt index 503dc34895..2a780c80b2 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/PainterHint.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/PainterHint.kt @@ -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 } diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/PalettePainterHintsProvider.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/PalettePainterHintsProvider.kt new file mode 100644 index 0000000000..62c3c9fada --- /dev/null +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/PalettePainterHintsProvider.kt @@ -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, + protected val themeIconPalette: Map, + protected val themeColorPalette: Map, +) : 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, + 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, + trees: MutableMap, + ui: MutableMap, + ): MutableMap? = + 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 + } + } +} diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/ResizedPainter.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/ResizedPainter.kt index 3204b08aff..b22a1ab72d 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/ResizedPainter.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/ResizedPainter.kt @@ -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 diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/ResourcePainterProvider.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/ResourcePainterProvider.kt index dc5c68909f..9a01b3cfa9 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/ResourcePainterProvider.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/ResourcePainterProvider.kt @@ -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): InputStream { + private fun patchSvg( + scope: Scope, + inputStream: InputStream, + hints: List, + ): 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 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) } diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/badge/BadgeShape.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/badge/BadgeShape.kt index c3b29038a0..c87883cf28 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/badge/BadgeShape.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/badge/BadgeShape.kt @@ -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 } diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Badge.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Badge.kt index eda3c14bf2..01b4f5bc4b 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Badge.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Badge.kt @@ -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 diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Palette.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/ColorBasedPaletteReplacement.kt similarity index 86% rename from ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Palette.kt rename to ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/ColorBasedPaletteReplacement.kt index 4eb40b6990..a781e8d4a4 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Palette.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/ColorBasedPaletteReplacement.kt @@ -12,7 +12,7 @@ import kotlin.math.roundToInt @Immutable @GenerateDataFunctions -private class PaletteImpl(val map: Map) : PainterSvgPatchHint { +private class ColorBasedReplacementPainterSvgPatchHint(val map: Map) : 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): 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): PainterHint = + if (paletteMap.isEmpty()) PainterHint.None else ColorBasedReplacementPainterSvgPatchHint(paletteMap) diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/DarkOrStroke.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/DarkAndStroke.kt similarity index 70% rename from ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/DarkOrStroke.kt rename to ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/DarkAndStroke.kt index a1721c498f..d1c1797036 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/DarkOrStroke.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/DarkAndStroke.kt @@ -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 diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/HiDpi.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/HiDpi.kt index 316de4af01..5c2c1116b0 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/HiDpi.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/HiDpi.kt @@ -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 diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/KeyBasedPaletteReplacement.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/KeyBasedPaletteReplacement.kt new file mode 100644 index 0000000000..18f1f2ab5c --- /dev/null +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/KeyBasedPaletteReplacement.kt @@ -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) : PainterSvgPatchHint { + + override fun PainterProviderScope.patch(element: Element) { + element.patchPalette(map) + } +} + +private fun Element.patchPalette(replacementColors: Map) { + 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 { + 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): PainterHint = + if (paletteMap.isEmpty()) PainterHint.None else KeyBasedReplacementPainterSvgPatchHint(paletteMap) diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Override.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Override.kt deleted file mode 100644 index e9b82493df..0000000000 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Override.kt +++ /dev/null @@ -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) : PainterPathHint { - - override fun PainterProviderScope.patch(): String = iconOverride[path] ?: path -} - -public fun Override(override: Map): PainterHint = - if (override.isEmpty()) { - PainterHint.None - } else { - OverrideImpl(override) - } diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/PathOverride.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/PathOverride.kt new file mode 100644 index 0000000000..0a73de2464 --- /dev/null +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/PathOverride.kt @@ -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) : 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): PainterHint = + if (overrides.isEmpty()) { + PainterHint.None + } else { + PathOverrideImpl(overrides) + } diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Selected.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Selected.kt index 938afd5a2a..58dc830fb9 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Selected.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Selected.kt @@ -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) diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Size.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Size.kt index fdf713c237..58fef8b557 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Size.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Size.kt @@ -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) } diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Stateful.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Stateful.kt index 4fb2bbac59..fc06cff1b3 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Stateful.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/Stateful.kt @@ -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) diff --git a/ui/src/main/kotlin/org/jetbrains/jewel/ui/util/ColorExtensions.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/util/ColorExtensions.kt index b43e1f8aad..2c8eb5f27d 100644 --- a/ui/src/main/kotlin/org/jetbrains/jewel/ui/util/ColorExtensions.kt +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/util/ColorExtensions.kt @@ -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 diff --git a/ui/src/test/kotlin/org/jetbrains/jewel/PainterHintTest.kt b/ui/src/test/kotlin/org/jetbrains/jewel/PainterHintTest.kt index 92be9d1219..f820d562c0 100644 --- a/ui/src/test/kotlin/org/jetbrains/jewel/PainterHintTest.kt +++ b/ui/src/test/kotlin/org/jetbrains/jewel/PainterHintTest.kt @@ -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 = """ - - - - - - """.trimIndent() + | + | + | + | + | + """.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( """ - - - - - - """.trimIndent(), + | + | + | + | + | + """.trimMargin(), patchedSvg, ) }