From a941970cc223c1c23b1f59dc40833e76b25f9960 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 --- .../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 +- .../bridge/BridgePainterHintsProvider.kt | 96 +++++++++- .../jewel/bridge/theme/IntUiBridge.kt | 169 ++++++++++++++++-- .../api/int-ui-standalone.api | 10 +- .../StandalonePainterHintsProvider.kt | 95 +++++++++- .../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 | 33 +++- ui/api/ui.api | 46 +++-- .../jetbrains/jewel/ui/component/Checkbox.kt | 45 +++-- .../jewel/ui/component/RadioButton.kt | 40 +++-- .../ui/component/styling/CheckboxStyling.kt | 30 +++- .../component/styling/RadioButtonStyling.kt | 14 ++ .../ui/painter/BasePainterHintsProvider.kt | 81 --------- .../ui/painter/PalettePainterHintsProvider.kt | 55 ++++++ .../ui/painter/ResourcePainterProvider.kt | 6 +- ...tte.kt => ColorBasedPaletteReplacement.kt} | 11 +- .../hints/KeyBasedPaletteReplacement.kt | 67 +++++++ .../org/jetbrains/jewel/PainterHintTest.kt | 4 +- 42 files changed, 701 insertions(+), 268 deletions(-) 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} (87%) create mode 100644 ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/KeyBasedPaletteReplacement.kt 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..68151a870f 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.26-EAP-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/BridgePainterHintsProvider.kt b/ide-laf-bridge/src/main/kotlin/org/jetbrains/jewel/bridge/BridgePainterHintsProvider.kt index 839a666573..9e5ec8a489 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,12 +4,16 @@ 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.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 @InternalJewelApi public class BridgePainterHintsProvider private constructor( @@ -17,7 +21,81 @@ public class BridgePainterHintsProvider private constructor( intellijIconPalette: Map = emptyMap(), themeIconPalette: Map = emptyMap(), themeColorPalette: Map = emptyMap(), -) : BasePainterHintsProvider(isDark, intellijIconPalette, themeIconPalette, themeColorPalette) { +) : PalettePainterHintsProvider(isDark, intellijIconPalette, themeIconPalette, themeColorPalette) { + + override val checkBoxPaletteHint: PainterHint + override val treePaletteHint: PainterHint + override val uiPaletteHint: PainterHint + + init { + val ui = mutableMapOf() + val checkBoxes = 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(checkBoxes, key, value) + } else { + val map = selectMap(key, trees, ui) ?: continue + registerColorBasedReplacement(map, key, value) + } + } + + checkBoxPaletteHint = KeyBasedPaletteReplacement(checkBoxes) + treePaletteHint = ColorBasedPaletteReplacement(trees) + uiPaletteHint = ColorBasedPaletteReplacement(ui) + } + + private fun selectMap( + key: String, + trees: MutableMap, + ui: MutableMap, + ) = + when { + key.startsWith("Tree.iconColor.") -> trees + key.startsWith("Objects.") || key.startsWith("Actions.") || key.startsWith("#") -> ui + else -> null + } + + 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 { @@ -32,7 +110,7 @@ public class BridgePainterHintsProvider private constructor( 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})") @@ -49,5 +127,17 @@ public class BridgePainterHintsProvider private constructor( return BridgePainterHintsProvider(isDark, keyPalette, iconColorPalette, 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..3aacfe806f 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") +private fun isNewUiTheme(): Boolean { + if (!NewUI.isEnabled()) return false + + val lafInfo = LafManager.getInstance().currentUIThemeLookAndFeel + return lafInfo.name == "Light" || lafInfo.name == "Dark" +} 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..1dc39945f6 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-XIZ8g8E (Lorg/jetbrains/jewel/ui/component/styling/CheckboxMetrics$Companion;JLandroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/shape/CornerSize;JJF)Lorg/jetbrains/jewel/ui/component/styling/CheckboxMetrics; + public static synthetic fun defaults-XIZ8g8E$default (Lorg/jetbrains/jewel/ui/component/styling/CheckboxMetrics$Companion;JLandroidx/compose/foundation/shape/CornerSize;Landroidx/compose/foundation/shape/CornerSize;JJFILjava/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-eb3e7j0 (Lorg/jetbrains/jewel/ui/component/styling/RadioButtonMetrics$Companion;JJJF)Lorg/jetbrains/jewel/ui/component/styling/RadioButtonMetrics; + public static synthetic fun defaults-eb3e7j0$default (Lorg/jetbrains/jewel/ui/component/styling/RadioButtonMetrics$Companion;JJJFILjava/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/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..ec7279589e 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,23 +1,31 @@ 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.KeyBasedPaletteReplacement import org.jetbrains.jewel.ui.painter.hints.Override +import org.jetbrains.jewel.ui.util.inDebugMode public class StandalonePainterHintsProvider( theme: ThemeDefinition, -) : BasePainterHintsProvider( +) : PalettePainterHintsProvider( theme.isDark, intellijColorPalette, theme.iconData.colorPalette, theme.colorPalette.rawMap, ) { + override val checkBoxPaletteHint: PainterHint + override val treePaletteHint: PainterHint + override val uiPaletteHint: PainterHint + private val overrideHint: PainterHint = Override( theme.iconData.iconOverrides.entries.associate { (k, v) -> @@ -25,6 +33,77 @@ public class StandalonePainterHintsProvider( }, ) + init { + val ui = mutableMapOf() + val checkBoxes = 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.")) { + // Note: in the 241 bridge, we also need to check if the theme is New UI or not + registerIdBasedReplacement(checkBoxes, key, value) + } else { + val map = selectMap(key, trees, ui) ?: continue + registerColorBasedReplacement(map, key, value) + } + } + + checkBoxPaletteHint = KeyBasedPaletteReplacement(checkBoxes) + treePaletteHint = ColorBasedPaletteReplacement(trees) + uiPaletteHint = ColorBasedPaletteReplacement(ui) + } + + private fun selectMap( + key: String, + trees: MutableMap, + ui: MutableMap, + ) = + when { + key.startsWith("Tree.iconColor.") -> trees + key.startsWith("Objects.") || key.startsWith("Actions.") || key.startsWith("#") -> ui + else -> null + } + + 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(getPaletteHint(path)) @@ -86,5 +165,17 @@ 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..f2add923dc 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..6c39405b29 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 @@ -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/ui/api/ui.api b/ui/api/ui.api index 4cc34977a0..6ff9f0a4ce 100644 --- a/ui/api/ui.api +++ b/ui/api/ui.api @@ -855,12 +855,13 @@ 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;JJFLkotlin/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 getOutlineSelectedCornerSize ()Landroidx/compose/foundation/shape/CornerSize; + public final fun getOutlineSelectedSize-MYxV2XQ ()J public final fun getOutlineSize-MYxV2XQ ()J public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -1631,9 +1632,11 @@ 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 (JJJFLkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z public final fun getIconContentGap-D9Ej5fM ()F + public final fun getOutlineSelectedSize-MYxV2XQ ()J + public final fun getOutlineSize-MYxV2XQ ()J public final fun getRadioButtonSize-MYxV2XQ ()J public fun hashCode ()I public fun toString ()Ljava/lang/String; @@ -2102,13 +2105,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 +2214,22 @@ 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 getCheckBoxPaletteHint ()Lorg/jetbrains/jewel/ui/painter/PainterHint; + protected final fun getIntellijIconPalette ()Ljava/util/Map; + protected final fun getPaletteHint (Ljava/lang/String;)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; +} + 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,6 +2295,10 @@ 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/ColorBasedPaletteReplacementKt { + public static final fun ColorBasedPaletteReplacement (Ljava/util/Map;)Lorg/jetbrains/jewel/ui/painter/PainterHint; +} + public final class org/jetbrains/jewel/ui/painter/hints/DarkOrStrokeKt { 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; @@ -2293,12 +2309,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/OverrideKt { + public static final fun Override (Ljava/util/Map;)Lorg/jetbrains/jewel/ui/painter/PainterHint; } public final class org/jetbrains/jewel/ui/painter/hints/SelectedKt { 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..4f8f59bffe 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) @@ -336,14 +337,8 @@ private object CheckBoxIndeterminate : PainterSuffixHint() { } @Composable -private fun CheckBoxImage( - modifier: Modifier, - checkboxPainter: Painter, - checkBoxModifier: Modifier, -) { - Box(modifier, contentAlignment = Alignment.Center) { - Image(checkboxPainter, contentDescription = null, modifier = checkBoxModifier) - } +private fun CheckBoxImage(checkboxPainter: Painter, modifier: Modifier = Modifier) { + Box(modifier.paint(checkboxPainter, alignment = Alignment.TopStart)) } @Immutable 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..719ace3f21 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..6ed9de0b4a 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/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/PalettePainterHintsProvider.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/PalettePainterHintsProvider.kt new file mode 100644 index 0000000000..2b86d2288c --- /dev/null +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/PalettePainterHintsProvider.kt @@ -0,0 +1,55 @@ +package org.jetbrains.jewel.ui.painter + +import androidx.compose.ui.graphics.Color +import org.jetbrains.jewel.ui.util.fromRGBAHexStringOrNull + +public abstract class PalettePainterHintsProvider( + protected val isDark: Boolean, + protected val intellijIconPalette: Map, + protected val themeIconPalette: Map, + protected val themeColorPalette: Map, +) : PainterHintsProvider { + + protected abstract val checkBoxPaletteHint: 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) + } + + /** + * 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): 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/ResourcePainterProvider.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/ResourcePainterProvider.kt index dc5c68909f..7b0c7f7071 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 @@ -179,7 +179,11 @@ 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() } } 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 87% 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..b7de5c88d3 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,10 @@ 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. + */ +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/KeyBasedPaletteReplacement.kt b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/KeyBasedPaletteReplacement.kt new file mode 100644 index 0000000000..f422c0d3d6 --- /dev/null +++ b/ui/src/main/kotlin/org/jetbrains/jewel/ui/painter/hints/KeyBasedPaletteReplacement.kt @@ -0,0 +1,67 @@ +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. + */ +public fun KeyBasedPaletteReplacement(paletteMap: Map): PainterHint = + if (paletteMap.isEmpty()) PainterHint.None else KeyBasedReplacementPainterSvgPatchHint(paletteMap) diff --git a/ui/src/test/kotlin/org/jetbrains/jewel/PainterHintTest.kt b/ui/src/test/kotlin/org/jetbrains/jewel/PainterHintTest.kt index 92be9d1219..984d2a7a74 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.Selected import org.jetbrains.jewel.ui.painter.hints.Size import org.jetbrains.jewel.ui.painter.hints.Stateful @@ -270,7 +270,7 @@ class PainterHintTest : BasicJewelUiTest() { val patchedSvg = testScope("fake_icon.svg") .applyPaletteHints( baseSvg, - Palette( + ColorBasedPaletteReplacement( mapOf( Color(0x80000000) to Color(0xFF123456), Color.Black to Color.White,