diff --git a/composeApp/src/commonMain/composeResources/values-it/strings.xml b/composeApp/src/commonMain/composeResources/values-it/strings.xml
index 9303dcb6..6ddf02b5 100644
--- a/composeApp/src/commonMain/composeResources/values-it/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values-it/strings.xml
@@ -180,6 +180,10 @@
Pausa
Salva
Annulla
+ Scegli un colore
+ Esadecimale
+ Usa #RRGGBB o #AARRGGBB
+ Applica
Espandi
Comprimi
Impostazioni: %1$s
diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml
index ba373128..a7c1d7dd 100644
--- a/composeApp/src/commonMain/composeResources/values/strings.xml
+++ b/composeApp/src/commonMain/composeResources/values/strings.xml
@@ -180,6 +180,10 @@
Pause
Save
Cancel
+ Choose a color
+ Hex
+ Use #RRGGBB or #AARRGGBB
+ Apply
Expand
Collapse
Settings: %1$s
@@ -474,4 +478,4 @@
Filter:
Sort:
Sync All
-
\ No newline at end of file
+
diff --git a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/ui/ColorPicker.kt b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/ui/ColorPicker.kt
index 48900e2c..4faf3523 100644
--- a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/ui/ColorPicker.kt
+++ b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/ui/ColorPicker.kt
@@ -21,12 +21,14 @@ import org.wip.plugintoolkit.features.colorpicker.ui.pickers.SimpleRingColorPick
fun ColorPicker(
modifier: Modifier = Modifier,
type: ColorPickerType = ColorPickerType.Classic(),
+ initialColor: Color = Color.White,
onPickedColor: (Color) -> Unit
) {
Box(modifier = modifier) {
when (type) {
is ColorPickerType.Classic -> ClassicColorPicker(
showAlphaBar = type.showAlphaBar,
+ initialColor = initialColor,
onPickedColor = onPickedColor,
)
@@ -62,4 +64,3 @@ fun ColorPicker(
private fun ColorPickerPreview() {
ColorPicker(onPickedColor = {})
}
-
diff --git a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/ui/ColorPickerDialog.kt b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/ui/ColorPickerDialog.kt
index 7d09ca4c..94bc7fda 100644
--- a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/ui/ColorPickerDialog.kt
+++ b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/ui/ColorPickerDialog.kt
@@ -9,12 +9,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
-import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -26,119 +27,103 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.sp
import androidx.compose.ui.window.Dialog
+import org.jetbrains.compose.resources.stringResource
+import org.wip.plugintoolkit.core.theme.ToolkitTheme
import org.wip.plugintoolkit.features.colorpicker.model.ColorPickerType
-import org.wip.plugintoolkit.features.colorpicker.utils.toCMYK
-import org.wip.plugintoolkit.features.colorpicker.utils.toHSL
+import org.wip.plugintoolkit.features.colorpicker.utils.parseHexColor
import org.wip.plugintoolkit.features.colorpicker.utils.toHex
-import org.wip.plugintoolkit.features.colorpicker.utils.toRGB
import org.wip.plugintoolkit.features.colorpicker.utils.transparentBackground
-import org.wip.plugintoolkit.shared.components.SelectedButtonGroup
-import org.wip.plugintoolkit.core.theme.ToolkitTheme
+import plugintoolkit.composeapp.generated.resources.Res
+import plugintoolkit.composeapp.generated.resources.action_cancel
+import plugintoolkit.composeapp.generated.resources.color_picker_apply
+import plugintoolkit.composeapp.generated.resources.color_picker_hex
+import plugintoolkit.composeapp.generated.resources.color_picker_hex_hint
+import plugintoolkit.composeapp.generated.resources.color_picker_title
-/**
- * Color picker wrapped in a dialog.
- *
- * @param show Whether the dialog is visible.
- * @param onDismissRequest Called when the user tries to dismiss the dialog.
- * @param initialType The picker style — defaults to [ColorPickerType.Classic].
- * @param onPickedColor Callback invoked when the user confirms a color selection.
- */
+/** A focused, editable color picker dialog with explicit cancel/apply actions. */
@Composable
fun ColorPickerDialog(
show: Boolean,
onDismissRequest: () -> Unit,
- initialType: ColorPickerType = ColorPickerType.Classic(),
+ initialColor: Color = Color.White,
+ showAlpha: Boolean = false,
onPickedColor: (Color) -> Unit
) {
- var showDialog by remember(show) { mutableStateOf(show) }
- var color by remember { mutableStateOf(Color.White) }
- var selectedFormat by remember { mutableStateOf("HEX") }
- var type by remember { mutableStateOf(initialType) }
+ if (!show) return
- if (showDialog) {
- Dialog(
- onDismissRequest = {
- onDismissRequest()
- showDialog = false
- }) {
- val includeAlpha = when (type) {
- is ColorPickerType.Circle -> (type as ColorPickerType.Circle).showAlphaBar
- is ColorPickerType.Classic -> (type as ColorPickerType.Classic).showAlphaBar
- is ColorPickerType.Ring -> (type as ColorPickerType.Ring).showAlphaBar
- else -> false
- }
+ var color by remember(initialColor) { mutableStateOf(initialColor) }
+ var hexInput by remember(initialColor, showAlpha) {
+ mutableStateOf(initialColor.toHex(hexPrefix = true, includeAlpha = showAlpha).uppercase())
+ }
+ val parsedHex = remember(hexInput) { parseHexColor(hexInput) }
+
+ Dialog(onDismissRequest = onDismissRequest) {
+ Surface(
+ modifier = Modifier.widthIn(max = ToolkitTheme.dimensions.minWidthMedium),
+ shape = MaterialTheme.shapes.extraLarge,
+ color = MaterialTheme.colorScheme.surfaceContainerHigh,
+ tonalElevation = ToolkitTheme.dimensions.elevationHighMedium
+ ) {
+ Column(
+ modifier = Modifier.padding(ToolkitTheme.spacing.extraLarge),
+ verticalArrangement = Arrangement.spacedBy(ToolkitTheme.spacing.medium)
+ ) {
+ Text(
+ text = stringResource(Res.string.color_picker_title),
+ style = MaterialTheme.typography.headlineSmall
+ )
- val colorCode = remember(color, selectedFormat) {
- when (selectedFormat) {
- "HEX" -> color.toHex(hexPrefix = true, includeAlpha = includeAlpha)
- "RGB" -> color.toRGB(rgbPrefix = true, includeAlpha = includeAlpha)
- "HSL" -> color.toHSL(hslPrefix = true, includeAlpha = includeAlpha)
- "CMYK" -> color.toCMYK(cmykPrefix = true, includeAlpha = includeAlpha)
- else -> color.toHex(hexPrefix = true, includeAlpha = includeAlpha)
+ ColorPicker(
+ type = ColorPickerType.Classic(showAlphaBar = showAlpha),
+ initialColor = initialColor,
+ onPickedColor = {
+ color = it
+ hexInput = it.toHex(hexPrefix = true, includeAlpha = showAlpha).uppercase()
+ }
+ )
+
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(ToolkitTheme.spacing.medium)
+ ) {
+ Box(
+ modifier = Modifier
+ .size(ToolkitTheme.dimensions.heightMediumLarge)
+ .clip(RoundedCornerShape(ToolkitTheme.spacing.small))
+ .transparentBackground(verticalBoxesAmount = 4)
+ .background(parsedHex ?: color)
+ )
+ OutlinedTextField(
+ value = hexInput,
+ onValueChange = { input ->
+ hexInput = input.take(9)
+ parseHexColor(hexInput)?.let { color = it }
+ },
+ modifier = Modifier.weight(1f),
+ label = { Text(stringResource(Res.string.color_picker_hex)) },
+ supportingText = if (parsedHex == null) {
+ { Text(stringResource(Res.string.color_picker_hex_hint)) }
+ } else null,
+ isError = parsedHex == null,
+ singleLine = true,
+ textStyle = MaterialTheme.typography.bodyLarge.copy(fontFamily = FontFamily.Monospace)
+ )
}
- }
- Surface(
- modifier = Modifier.widthIn(max = ToolkitTheme.dimensions.minWidthMedium),
- shape = MaterialTheme.shapes.extraLarge,
- color = MaterialTheme.colorScheme.surfaceContainerHigh,
- tonalElevation = ToolkitTheme.dimensions.elevationHighMedium
- ) {
- Box(modifier = Modifier.padding(ToolkitTheme.spacing.extraLarge)) {
- Column(
- horizontalAlignment = Alignment.CenterHorizontally,
- verticalArrangement = Arrangement.spacedBy(ToolkitTheme.spacing.medium)
+ Row(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalArrangement = Arrangement.End
+ ) {
+ TextButton(onClick = onDismissRequest) {
+ Text(stringResource(Res.string.action_cancel))
+ }
+ Button(
+ onClick = { parsedHex?.let(onPickedColor) },
+ enabled = parsedHex != null
) {
- SelectedButtonGroup(
- buttons = listOf("HEX", "RGB", "HSL", "CMYK"),
- startingIndex = 0,
- onButtonSelected = { selectedFormat = it }
- )
- SelectedButtonGroup(
- buttons = listOf("Classic", "Circle", "Ring", "Simple"),
- startingIndex = 0,
- onButtonSelected = {
- type = when (it) {
- "Classic" -> ColorPickerType.Classic()
- "Circle" -> ColorPickerType.Circle()
- "Ring" -> ColorPickerType.Ring()
- "Simple" -> ColorPickerType.SimpleRing()
- else -> ColorPickerType.Classic()
- }
- }
- )
- ColorPicker(type = type, onPickedColor = { color = it })
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(ToolkitTheme.spacing.medium)
- ) {
- Box(
- modifier = Modifier
- .size(ToolkitTheme.dimensions.containerWidthMediumLarge, ToolkitTheme.dimensions.heightMediumLarge)
- .clip(RoundedCornerShape(50))
- .transparentBackground(verticalBoxesAmount = 4)
- .background(color)
- )
- Text(
- text = colorCode,
- color = MaterialTheme.colorScheme.onSurface,
- fontSize = 14.sp,
- fontFamily = FontFamily.Monospace,
- )
- }
- Button(
- modifier = Modifier.fillMaxWidth(),
- onClick = {
- onPickedColor(color)
- showDialog = false
- },
- shape = CircleShape
- ) {
- Text(text = "Select")
- }
+ Text(stringResource(Res.string.color_picker_apply))
}
}
}
@@ -150,11 +135,6 @@ fun ColorPickerDialog(
@Composable
private fun ColorPickerDialogPreview() {
MaterialTheme {
- ColorPickerDialog(
- show = true,
- onDismissRequest = {},
- onPickedColor = {}
- )
+ ColorPickerDialog(show = true, onDismissRequest = {}, onPickedColor = {})
}
}
-
diff --git a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/ui/pickers/ClassicColorPicker.kt b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/ui/pickers/ClassicColorPicker.kt
index 2739e6d4..ade2963b 100644
--- a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/ui/pickers/ClassicColorPicker.kt
+++ b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/ui/pickers/ClassicColorPicker.kt
@@ -35,6 +35,8 @@ import org.wip.plugintoolkit.features.colorpicker.utils.fromHueProgress
import org.wip.plugintoolkit.features.colorpicker.utils.green
import org.wip.plugintoolkit.features.colorpicker.utils.lighten
import org.wip.plugintoolkit.features.colorpicker.utils.red
+import org.wip.plugintoolkit.features.colorpicker.utils.saturationAndValue
+import org.wip.plugintoolkit.features.colorpicker.utils.toHueProgress
import kotlin.math.roundToInt
import org.wip.plugintoolkit.core.theme.ToolkitTheme
@@ -45,18 +47,33 @@ import org.wip.plugintoolkit.core.theme.ToolkitTheme
internal fun ClassicColorPicker(
modifier: Modifier = Modifier,
showAlphaBar: Boolean,
+ initialColor: Color = Color.White,
onPickedColor: (Color) -> Unit
) {
- var pickerLocation by remember { mutableStateOf(Offset.Zero) }
+ val initialSaturationAndValue = remember(initialColor) { initialColor.saturationAndValue() }
+ val initialHue = remember(initialColor) { initialColor.toHueProgress() }
+ var pickerLocation by remember(initialColor) { mutableStateOf(Offset.Zero) }
var colorPickerSize by remember { mutableStateOf(IntSize.Zero) }
- var alpha by remember { mutableStateOf(1f) }
- var rangeColor by remember { mutableStateOf(Color.White) }
- var hueSlider by remember { mutableStateOf(0f) }
+ var pickerInitialized by remember(initialColor) { mutableStateOf(false) }
+ var alpha by remember(initialColor) { mutableStateOf(initialColor.alpha) }
+ var rangeColor by remember(initialColor) { mutableStateOf(Color.fromHueProgress(initialHue)) }
+ var hueSlider by remember(initialColor) { mutableStateOf(initialHue) }
- var color by remember { mutableStateOf(Color.White) }
+ var color by remember(initialColor) { mutableStateOf(initialColor) }
- LaunchedEffect(rangeColor, pickerLocation, colorPickerSize, alpha) {
- if (colorPickerSize.width > 0 && colorPickerSize.height > 0) {
+ LaunchedEffect(colorPickerSize, initialColor) {
+ if (colorPickerSize.width > 0 && colorPickerSize.height > 0 && !pickerInitialized) {
+ val (saturation, value) = initialSaturationAndValue
+ pickerLocation = Offset(
+ x = saturation * colorPickerSize.width,
+ y = (1f - value) * colorPickerSize.height
+ )
+ pickerInitialized = true
+ }
+ }
+
+ LaunchedEffect(rangeColor, pickerLocation, colorPickerSize, alpha, pickerInitialized) {
+ if (pickerInitialized && colorPickerSize.width > 0 && colorPickerSize.height > 0) {
val xProgress = if (colorPickerSize.width > 0) {
(1 - (pickerLocation.x / colorPickerSize.width)).coerceIn(0f, 1f)
} else 0f
@@ -142,4 +159,3 @@ private fun ClassicColorPickerPreview() {
ClassicColorPicker(showAlphaBar = true, onPickedColor = {})
}
}
-
diff --git a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/utils/ColorExt.kt b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/utils/ColorExt.kt
index 16c6e6a6..5ed4a835 100644
--- a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/utils/ColorExt.kt
+++ b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/colorpicker/utils/ColorExt.kt
@@ -8,6 +8,37 @@ import kotlin.math.max
import kotlin.math.min
import kotlin.math.roundToInt
+/** Parses #RGBA, #RRGGBB or #AARRGGBB; long alpha values match the ARGB order emitted by [toHex]. */
+fun parseHexColor(value: String): Color? {
+ val digits = value.trim().removePrefix("#")
+ val normalized = when (digits.length) {
+ 4 -> {
+ val red = digits[0].toString().repeat(2)
+ val green = digits[1].toString().repeat(2)
+ val blue = digits[2].toString().repeat(2)
+ val alpha = digits[3].toString().repeat(2)
+ "$alpha$red$green$blue"
+ }
+ 6 -> "FF$digits"
+ 8 -> digits
+ else -> return null
+ }
+ val argb = normalized.toLongOrNull(16) ?: return null
+ return Color(argb.toInt())
+}
+
+/** Detects alpha-bearing legacy and current color representations without changing their format. */
+fun colorStringHasAlpha(value: String): Boolean {
+ val trimmed = value.trim()
+ if (trimmed.startsWith("rgba(", ignoreCase = true) || trimmed.startsWith("hsla(", ignoreCase = true)) {
+ return true
+ }
+ val candidate = value.split(",").lastOrNull { it.trim().isNotEmpty() }?.trim().orEmpty()
+ val digits = candidate.removePrefix("#")
+ return (candidate.startsWith("#") && digits.length == 4 && digits.toLongOrNull(16) != null) ||
+ (digits.length == 8 && digits.toLongOrNull(16) != null)
+}
+
/**
* Returns an integer array for all color channels value.
*/
@@ -236,5 +267,15 @@ internal fun Color.toHueProgress(): Float {
hue *= 60
if (hue < 0) hue += 360
- return hue
+ return hue / 360f
+}
+
+internal fun Color.saturationAndValue(): Pair {
+ val red = red() / 255f
+ val green = green() / 255f
+ val blue = blue() / 255f
+ val maximum = max(red, max(green, blue))
+ val minimum = min(red, min(green, blue))
+ val saturation = if (maximum == 0f) 0f else (maximum - minimum) / maximum
+ return saturation to maximum
}
diff --git a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/flows/ui/NodeDialogs.kt b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/flows/ui/NodeDialogs.kt
index e567dc53..20141f82 100644
--- a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/flows/ui/NodeDialogs.kt
+++ b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/flows/ui/NodeDialogs.kt
@@ -40,6 +40,7 @@ import org.wip.plugintoolkit.api.parseSemanticTypes
import org.wip.plugintoolkit.core.theme.ToolkitTheme
import org.wip.plugintoolkit.features.colorpicker.utils.toHex
import org.wip.plugintoolkit.features.colorpicker.utils.toRGB
+import org.wip.plugintoolkit.features.colorpicker.utils.colorStringHasAlpha
import org.wip.plugintoolkit.features.flows.model.Node
import org.wip.plugintoolkit.features.flows.model.PortConstraints
import org.wip.plugintoolkit.shared.components.ToolkitTextField
@@ -306,9 +307,13 @@ fun NodeDialogs(
if (showColorPicker && activeColorInputId != null) {
val input = node.inputs.firstOrNull { it.id == activeColorInputId }
val inferredSem = input?.let { inferredSemanticTypes[Pair(node.id, it.id)] ?: it.semanticTypes } ?: emptyList()
- val hasAlpha = inferredSem.any { it.variant?.contains("rgba", ignoreCase = true) == true }
+ val existingValue = input?.let { getPortValueString(it.value ?: it.defaultValue, it.dataType) } ?: ""
+ val hasAlpha = inferredSem.any { it.variant?.contains("rgba", ignoreCase = true) == true } ||
+ colorStringHasAlpha(existingValue)
org.wip.plugintoolkit.features.colorpicker.ui.ColorPickerDialog(
show = showColorPicker,
+ initialColor = parseColorString(existingValue),
+ showAlpha = hasAlpha,
onDismissRequest = onDismissColorPicker,
onPickedColor = { color ->
activeColorInputId.let { inputId ->
@@ -323,8 +328,6 @@ fun NodeDialogs(
color.toHex(hexPrefix = true, includeAlpha = hasAlpha)
}
val isArray = input?.dataType is DataType.Array
- val existingValue =
- input?.let { getPortValueString(it.value ?: it.defaultValue, it.dataType) } ?: ""
val newValue = appendPickedValue(existingValue, formatted, isArray)
onUpdateValue(node.id, inputId, newValue)
}
diff --git a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/flows/ui/NodeHelpers.kt b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/flows/ui/NodeHelpers.kt
index 0c651456..f92d3648 100644
--- a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/flows/ui/NodeHelpers.kt
+++ b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/flows/ui/NodeHelpers.kt
@@ -26,6 +26,7 @@ import kotlinx.serialization.json.booleanOrNull
import org.wip.plugintoolkit.api.DataType
import org.wip.plugintoolkit.features.flows.model.Node
import org.wip.plugintoolkit.core.theme.ToolkitTheme
+import org.wip.plugintoolkit.features.colorpicker.utils.parseHexColor
@Composable
fun PortCircle(
@@ -154,9 +155,17 @@ fun getNodeDescription(node: Node): String {
}
fun parseColorString(colorStr: String): Color {
- val lastColor = colorStr.split(",").lastOrNull { it.trim().isNotEmpty() }?.trim() ?: colorStr
+ val completeValue = colorStr.trim()
+ val isFunctionalColor = completeValue.startsWith("rgb(", ignoreCase = true) ||
+ completeValue.startsWith("rgba(", ignoreCase = true)
+ val lastColor = if (isFunctionalColor) {
+ completeValue
+ } else {
+ colorStr.split(",").lastOrNull { it.trim().isNotEmpty() }?.trim() ?: colorStr
+ }
val trimmed = lastColor.trim()
if (trimmed.isEmpty()) return Color.Transparent
+ parseHexColor(trimmed)?.let { return it }
if (trimmed.startsWith("#")) {
return try {
val hex = trimmed.substring(1)
@@ -184,10 +193,10 @@ fun parseColorString(colorStr: String): Color {
}
8 -> {
- val r = hex.substring(0, 2).toInt(16) / 255f
- val g = hex.substring(2, 4).toInt(16) / 255f
- val b = hex.substring(4, 6).toInt(16) / 255f
- val a = hex.substring(6, 8).toInt(16) / 255f
+ val a = hex.substring(0, 2).toInt(16) / 255f
+ val r = hex.substring(2, 4).toInt(16) / 255f
+ val g = hex.substring(4, 6).toInt(16) / 255f
+ val b = hex.substring(6, 8).toInt(16) / 255f
Color(r, g, b, a)
}
diff --git a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/settings/ui/AccentColorControl.kt b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/settings/ui/AccentColorControl.kt
index 0d55211a..75b37a8d 100644
--- a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/settings/ui/AccentColorControl.kt
+++ b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/features/settings/ui/AccentColorControl.kt
@@ -17,7 +17,6 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.unit.dp
-import org.wip.plugintoolkit.features.colorpicker.model.ColorPickerType
import org.wip.plugintoolkit.features.colorpicker.ui.ColorPickerDialog
import org.wip.plugintoolkit.features.settings.model.AppSettings
import org.wip.plugintoolkit.core.theme.ToolkitTheme
@@ -28,7 +27,7 @@ fun AccentColorControl(settings: AppSettings, onUpdate: (AppSettings) -> Unit) {
ColorPickerDialog(
show = showColorPicker,
- initialType = ColorPickerType.Classic(),
+ initialColor = Color(settings.appearance.accentColor),
onDismissRequest = { showColorPicker = false },
onPickedColor = { color ->
onUpdate(
diff --git a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/shared/components/plugin/inputs/ColorInput.kt b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/shared/components/plugin/inputs/ColorInput.kt
index 0b7da0b7..f355fa46 100644
--- a/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/shared/components/plugin/inputs/ColorInput.kt
+++ b/composeApp/src/commonMain/kotlin/org/wip/plugintoolkit/shared/components/plugin/inputs/ColorInput.kt
@@ -32,6 +32,8 @@ import org.wip.plugintoolkit.api.DataType
import org.wip.plugintoolkit.api.ParameterMetadata
import org.wip.plugintoolkit.core.theme.ToolkitTheme
import org.wip.plugintoolkit.features.colorpicker.ui.ColorPickerDialog
+import org.wip.plugintoolkit.features.colorpicker.utils.colorStringHasAlpha
+import org.wip.plugintoolkit.features.colorpicker.utils.parseHexColor
import org.wip.plugintoolkit.features.colorpicker.utils.toHex
import org.wip.plugintoolkit.features.colorpicker.utils.toRGB
import org.wip.plugintoolkit.shared.components.plugin.StandardTextField
@@ -52,6 +54,9 @@ fun ColorInput(
var showColorPicker by remember { mutableStateOf(false) }
val parsedColor = remember(value) { parseColorString(value) }
val isArray = metadata.type is DataType.Array
+ val isRgba = metadata.semanticTypes.any { it.canonicalId.contains("rgba", ignoreCase = true) } ||
+ colorStringHasAlpha(value)
+ val isRgb = metadata.semanticTypes.any { it.canonicalId.contains("rgb", ignoreCase = true) }
Column(modifier = Modifier
.fillMaxWidth()
@@ -101,17 +106,16 @@ fun ColorInput(
if (showColorPicker && enabled) {
ColorPickerDialog(
show = showColorPicker,
+ initialColor = parsedColor,
+ showAlpha = isRgba,
onDismissRequest = { showColorPicker = false },
onPickedColor = { color ->
showColorPicker = false
- val formatted = if (metadata.semanticTypes.any {
- it.canonicalId.contains("rgb", ignoreCase = true)
- }
- ) {
- color.toRGB()
+ val formatted = if (isRgb) {
+ color.toRGB(rgbPrefix = true, includeAlpha = isRgba)
} else {
- color.toHex()
+ color.toHex(hexPrefix = true, includeAlpha = isRgba)
}
onValueChange(formatted)
}
@@ -125,6 +129,7 @@ fun parseColorString(colorStr: String): Color {
if (trimmed.isEmpty()) return Color.Transparent
try {
+ parseHexColor(trimmed)?.let { return it }
if (trimmed.startsWith("#")) {
val hex = trimmed.substring(1)
when (hex.length) {
diff --git a/composeApp/src/commonTest/kotlin/org/wip/plugintoolkit/features/colorpicker/utils/ColorExtTest.kt b/composeApp/src/commonTest/kotlin/org/wip/plugintoolkit/features/colorpicker/utils/ColorExtTest.kt
new file mode 100644
index 00000000..d852c962
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/org/wip/plugintoolkit/features/colorpicker/utils/ColorExtTest.kt
@@ -0,0 +1,71 @@
+package org.wip.plugintoolkit.features.colorpicker.utils
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import kotlin.test.Test
+import kotlin.test.assertEquals
+import kotlin.test.assertNull
+
+class ColorExtTest {
+ @Test
+ fun `hex parser accepts rgb and argb values`() {
+ assertEquals(Color(0xFF336699.toInt()), parseHexColor("#336699"))
+ assertEquals(Color(0xFF336699.toInt()), parseHexColor("336699"))
+ assertEquals(Color(0x80336699.toInt()), parseHexColor("80336699"))
+ assertEquals(true, colorStringHasAlpha("#80336699"))
+ assertEquals(true, colorStringHasAlpha("80336699"))
+ assertEquals(Color(0xAAFF0000.toInt()), parseHexColor("#F00A"))
+ }
+
+ @Test
+ fun `ARGB values round trip without losing alpha`() {
+ val original = "80336699"
+ val parsed = parseHexColor(original)!!
+
+ assertEquals(original.lowercase(), parsed.toHex(includeAlpha = colorStringHasAlpha(original)))
+ }
+
+ @Test
+ fun `functional alpha colors are detected before component splitting`() {
+ assertEquals(true, colorStringHasAlpha("rgba(10, 20, 30, 0.5)"))
+ assertEquals(true, colorStringHasAlpha("HSLA(120, 50%, 50%, 0.25)"))
+ assertEquals(false, colorStringHasAlpha("rgb(10, 20, 30)"))
+ }
+
+ @Test
+ fun `hex parser rejects malformed values`() {
+ assertNull(parseHexColor("#12345"))
+ assertNull(parseHexColor("#GG3366"))
+ }
+
+ @Test
+ fun `hue progress is normalized`() {
+ assertEquals(0f, Color.Red.toHueProgress(), absoluteTolerance = 0.0001f)
+ assertEquals(1f / 3f, Color.Green.toHueProgress(), absoluteTolerance = 0.0001f)
+ assertEquals(2f / 3f, Color.Blue.toHueProgress(), absoluteTolerance = 0.0001f)
+ }
+
+ @Test
+ fun `picker coordinates reconstruct the initial color including alpha`() {
+ listOf(
+ Color(0xFFFF0000.toInt()),
+ Color(0xFF336699.toInt()),
+ Color(0xFF00FF00.toInt()),
+ Color(0xFFFFFFFF.toInt()),
+ Color(0x80336699.toInt())
+ ).forEach { expected ->
+ assertEquals(expected.toArgb(), reconstructPickerColor(expected).toArgb(), "Failed for ${expected.toHex(true)}")
+ }
+ }
+
+ private fun reconstructPickerColor(color: Color): Color {
+ val (saturation, value) = color.saturationAndValue()
+ val hueColor = Color.fromHueProgress(color.toHueProgress())
+ return Color(
+ hueColor.red().lighten(1f - saturation).darken(1f - value),
+ hueColor.green().lighten(1f - saturation).darken(1f - value),
+ hueColor.blue().lighten(1f - saturation).darken(1f - value),
+ color.alpha()
+ )
+ }
+}
diff --git a/composeApp/src/commonTest/kotlin/org/wip/plugintoolkit/features/flows/ui/NodeColorParsingTest.kt b/composeApp/src/commonTest/kotlin/org/wip/plugintoolkit/features/flows/ui/NodeColorParsingTest.kt
new file mode 100644
index 00000000..3d9f9d95
--- /dev/null
+++ b/composeApp/src/commonTest/kotlin/org/wip/plugintoolkit/features/flows/ui/NodeColorParsingTest.kt
@@ -0,0 +1,25 @@
+package org.wip.plugintoolkit.features.flows.ui
+
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+class NodeColorParsingTest {
+ @Test
+ fun `parses the ARGB order emitted by the color formatter`() {
+ assertEquals(Color(0x80336699.toInt()).toArgb(), parseColorString("#80336699").toArgb())
+ assertEquals(Color(0x80336699.toInt()).toArgb(), parseColorString("80336699").toArgb())
+ }
+
+ @Test
+ fun `parses functional colors before treating commas as array separators`() {
+ assertEquals(Color.Red.toArgb(), parseColorString("rgb(255, 0, 0)").toArgb())
+ assertEquals(Color(0x80FF0000.toInt()).toArgb(), parseColorString("rgba(255, 0, 0, 0.5)").toArgb())
+ }
+
+ @Test
+ fun `parses short RGBA consistently with alpha detection`() {
+ assertEquals(Color(0xAAFF0000.toInt()).toArgb(), parseColorString("#F00A").toArgb())
+ }
+}