From f204a18d62d04f839625cf1b9a3417404762c906 Mon Sep 17 00:00:00 2001 From: Illia Achour Date: Mon, 28 Sep 2026 01:21:44 -0400 Subject: [PATCH] Read a colorful color's missing hue as 0, as it is drawn The state gave any missing hue the one it remembered, which is right for a grey but not for hsl(none 80 50): that is drawn red, as CSS reads none as 0, while the hue slider showed the remembered 200, and the first saturation edit wrote 200 in and turned the swatch blue. Only a grey's missing hue, one its space calls powerless, now takes the remembered hue; any other reads 0, on the slider and in an edit. --- .../side/colorpicker/state/ColorPickerState.kt | 14 ++++++++++---- .../colorpicker/state/ColorPickerStateTest.kt | 18 ++++++++++++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/colorpicker-foundation/src/commonMain/kotlin/codes/side/colorpicker/state/ColorPickerState.kt b/colorpicker-foundation/src/commonMain/kotlin/codes/side/colorpicker/state/ColorPickerState.kt index f5206bbe..361decd3 100644 --- a/colorpicker-foundation/src/commonMain/kotlin/codes/side/colorpicker/state/ColorPickerState.kt +++ b/colorpicker-foundation/src/commonMain/kotlin/codes/side/colorpicker/state/ColorPickerState.kt @@ -100,8 +100,9 @@ public class ColorPickerState(initialValue: ColorValue) { public operator fun get(channel: ColorChannel): Double? = current.to(channel.space)[channel] /** - * What a slider on [channel] shows: its value; for a missing hue, the one last chosen in its - * family, or 0 when none has been; for any other missing component, 0, as CSS reads `none`. + * What a slider on [channel] shows: its value; for a grey's missing hue, the one last chosen in its + * family, or 0 when none has been; for any other missing component, a colorful color's hue included, + * 0, as CSS reads `none` and as the color is drawn. */ public fun displayValue(channel: ColorChannel): Double = displayComponents(channel.space)[channel.index] @@ -111,10 +112,15 @@ public class ColorPickerState(initialValue: ColorValue) { val color = of.to(space) return DoubleArray(space.channels.size) { index -> val channel = space.channels[index] - color[channel] ?: if (channel.isHue) memory.hue(channel) ?: 0.0 else 0.0 + color[channel] ?: if (channel.isHue) missingHue(color, channel) else 0.0 } } + // What [color]'s missing [hue] reads as. A grey has no hue to draw, so it reads the one last chosen + // in its family; a colorful color is drawn at hue 0, as CSS reads `none`, and reads that. + private fun missingHue(color: ColorValue, hue: ColorChannel): Double = + if (color.space.powerless(color.components()) and (1 shl hue.index) != 0) memory.hue(hue) ?: 0.0 else 0.0 + /** * Sets [channel] to [value], or to `none` when it is null, and leaves the color in [channel]'s * space. A missing hue there takes [displayValue] first, so raising a grey's saturation brings @@ -166,7 +172,7 @@ public class ColorPickerState(initialValue: ColorValue) { private fun edited(channel: ColorChannel, value: Double?): ColorValue { var color = editBase.to(channel.space) val hue = channel.space.hueChannel() - if (hue != null && hue !== channel && color.isMissing(hue)) color = color.with(hue, memory.hue(hue) ?: 0.0) + if (hue != null && hue !== channel && color.isMissing(hue)) color = color.with(hue, missingHue(color, hue)) return color.with(channel, value) } diff --git a/colorpicker-foundation/src/commonTest/kotlin/codes/side/colorpicker/state/ColorPickerStateTest.kt b/colorpicker-foundation/src/commonTest/kotlin/codes/side/colorpicker/state/ColorPickerStateTest.kt index 44c15315..4862a6c5 100644 --- a/colorpicker-foundation/src/commonTest/kotlin/codes/side/colorpicker/state/ColorPickerStateTest.kt +++ b/colorpicker-foundation/src/commonTest/kotlin/codes/side/colorpicker/state/ColorPickerStateTest.kt @@ -112,6 +112,24 @@ class ColorPickerStateTest { assertEquals(200.0, state.displayValue(Hsl.H)) } + @Test + fun aColorfulValueWithAMissingHueReadsHueZeroAsItIsDrawn() { + val state = ColorPickerState(Hsl(200.0, 80.0, 50.0)) + state.value = Hsl(null, 80.0, 50.0) + assertEquals(0.0, state.displayValue(Hsl.H), "drawn red, so shown red rather than the remembered 200") + state[Hsl.S] = 70.0 + assertEquals(Hsl(0.0, 70.0, 50.0), state.value, "a saturation edit keeps it red") + } + + @Test + fun aGreyWrittenWithAMissingHueStillShowsTheRememberedOne() { + val state = ColorPickerState(Hsl(200.0, 80.0, 50.0)) + state.value = Hsl(null, 0.0, 50.0) + assertEquals(200.0, state.displayValue(Hsl.H)) + state[Hsl.S] = 70.0 + assertEquals(Hsl(200.0, 70.0, 50.0), state.value) + } + @Test fun aDeliberateHueAtZeroSaturationIsLeftAlone() { val state = ColorPickerState(Hsl(200.0, 80.0, 50.0))