1+ package top.yukonga.miuix.kmp.basic
2+
3+ import androidx.compose.foundation.Canvas
4+ import androidx.compose.foundation.background
5+ import androidx.compose.foundation.border
6+ import androidx.compose.foundation.gestures.detectDragGestures
7+ import androidx.compose.foundation.gestures.detectTapGestures
8+ import androidx.compose.foundation.layout.Box
9+ import androidx.compose.foundation.layout.Spacer
10+ import androidx.compose.foundation.layout.fillMaxSize
11+ import androidx.compose.foundation.layout.fillMaxWidth
12+ import androidx.compose.foundation.layout.height
13+ import androidx.compose.foundation.layout.offset
14+ import androidx.compose.foundation.layout.size
15+ import androidx.compose.runtime.Composable
16+ import androidx.compose.runtime.LaunchedEffect
17+ import androidx.compose.runtime.getValue
18+ import androidx.compose.runtime.mutableStateOf
19+ import androidx.compose.runtime.remember
20+ import androidx.compose.runtime.setValue
21+ import androidx.compose.ui.Alignment
22+ import androidx.compose.ui.Modifier
23+ import androidx.compose.ui.draw.clip
24+ import androidx.compose.ui.geometry.Offset
25+ import androidx.compose.ui.geometry.Size
26+ import androidx.compose.ui.graphics.Color
27+ import androidx.compose.ui.graphics.LinearGradientShader
28+ import androidx.compose.ui.graphics.Shader
29+ import androidx.compose.ui.graphics.ShaderBrush
30+ import androidx.compose.ui.graphics.TileMode
31+ import androidx.compose.ui.input.pointer.pointerInput
32+ import androidx.compose.ui.layout.onGloballyPositioned
33+ import androidx.compose.ui.layout.positionInParent
34+ import androidx.compose.ui.platform.LocalDensity
35+ import androidx.compose.ui.unit.dp
36+ import top.yukonga.miuix.kmp.theme.MiuixTheme
37+ import top.yukonga.miuix.kmp.utils.ColorUtils
38+ import top.yukonga.miuix.kmp.utils.SmoothRoundedCornerShape
39+
40+ /* *
41+ * A [ColorPicker] component with Miuix style.
42+ *
43+ * @param initialColor The initial color of the picker.
44+ * @param onColorChanged The callback to be called when the color changes.
45+ * @param modifier The modifier to be applied to the color picker.
46+ */
47+ @Composable
48+ fun ColorPicker (
49+ initialColor : Color = MiuixTheme .colorScheme.primary,
50+ onColorChanged : (Color ) -> Unit = {},
51+ modifier : Modifier = Modifier
52+ ) {
53+ var initialSetup by remember { mutableStateOf(true ) }
54+ var currentHue by remember { mutableStateOf(0f ) }
55+ var currentSaturation by remember { mutableStateOf(0f ) }
56+ var currentValue by remember { mutableStateOf(0f ) }
57+ var currentAlpha by remember { mutableStateOf(1f ) }
58+
59+ // Set initial HSV values only once
60+ LaunchedEffect (initialColor, initialSetup) {
61+ if (initialSetup) {
62+ val hsv = FloatArray (3 )
63+ ColorUtils .rgbToHsv(
64+ (initialColor.red * 255 ).toInt(),
65+ (initialColor.green * 255 ).toInt(),
66+ (initialColor.blue * 255 ).toInt(),
67+ hsv
68+ )
69+ currentHue = hsv[0 ]
70+ currentSaturation = hsv[1 ]
71+ currentValue = hsv[2 ]
72+ currentAlpha = initialColor.alpha
73+ initialSetup = false
74+ }
75+ }
76+
77+ // Current selected color
78+ val selectedColor = Color .hsv(currentHue, currentSaturation, currentValue, currentAlpha)
79+
80+ // Track previous color to prevent recomposition loops
81+ var previousColor by remember { mutableStateOf(selectedColor) }
82+
83+ // Only trigger callback when colors actually change from user interaction
84+ LaunchedEffect (currentHue, currentSaturation, currentValue, currentAlpha) {
85+ if (! initialSetup && selectedColor != previousColor) {
86+ previousColor = selectedColor
87+ onColorChanged(selectedColor)
88+ }
89+ }
90+
91+ // Color preview
92+ Box (
93+ modifier = Modifier
94+ .fillMaxWidth()
95+ .height(20 .dp)
96+ .clip(SmoothRoundedCornerShape (10 .dp))
97+ .background(selectedColor)
98+ .border(1 .dp, MiuixTheme .colorScheme.outline, SmoothRoundedCornerShape (10 .dp))
99+ )
100+
101+ Spacer (modifier = Modifier .height(6 .dp))
102+
103+ // Hue selection
104+ HueSlider (
105+ currentHue = currentHue,
106+ onHueChanged = { currentHue = it },
107+ modifier = Modifier
108+ .fillMaxWidth()
109+ .height(20 .dp)
110+ .clip(SmoothRoundedCornerShape (10 .dp))
111+ .border(1 .dp, MiuixTheme .colorScheme.outline, SmoothRoundedCornerShape (10 .dp))
112+ )
113+
114+ Spacer (modifier = Modifier .height(6 .dp))
115+
116+ // Saturation-Value picker
117+ SaturationValuePicker (
118+ currentHue = currentHue,
119+ currentSaturation = currentSaturation,
120+ currentValue = currentValue,
121+ onSaturationValueChanged = { s, v ->
122+ println (" Saturation: $s , Value: $v " )
123+ currentSaturation = s
124+ currentValue = v
125+ },
126+ modifier = Modifier
127+ .fillMaxWidth()
128+ .height(100 .dp)
129+ .clip(SmoothRoundedCornerShape (10 .dp))
130+ .border(1 .dp, MiuixTheme .colorScheme.outline, SmoothRoundedCornerShape (10 .dp))
131+ )
132+
133+ Spacer (modifier = Modifier .height(6 .dp))
134+
135+ Slider (
136+ progress = currentAlpha,
137+ onProgressChange = { currentAlpha = it },
138+ modifier = Modifier
139+ .fillMaxWidth()
140+ .height(20 .dp)
141+ )
142+ }
143+
144+ @Composable
145+ private fun HueSlider (
146+ currentHue : Float ,
147+ onHueChanged : (Float ) -> Unit ,
148+ modifier : Modifier = Modifier
149+ ) {
150+ val density = LocalDensity .current
151+ var sliderWidth by remember { mutableStateOf(0 .dp) }
152+ var sliderPosition by remember { mutableStateOf(Offset .Zero ) }
153+
154+ Box (modifier = modifier.height(20 .dp)) {
155+ // Hue gradient
156+ Canvas (
157+ modifier = Modifier
158+ .fillMaxSize()
159+ .onGloballyPositioned { coordinates ->
160+ sliderWidth = with (density) { coordinates.size.width.toDp() }
161+ sliderPosition = coordinates.positionInParent()
162+ }
163+ .pointerInput(Unit ) {
164+ detectTapGestures { offset ->
165+ onHueChanged((offset.x / size.width * 360f ).coerceIn(0f , 360f ))
166+ }
167+ }
168+ .pointerInput(Unit ) {
169+ detectDragGestures { change, _ ->
170+ change.consume()
171+ onHueChanged((change.position.x / size.width * 360f ).coerceIn(0f , 360f ))
172+ }
173+ }
174+ ) {
175+ val width = size.width
176+ for (i in 0 until width.toInt()) {
177+ val hue = i / width * 360f
178+ drawLine(
179+ color = Color .hsv(hue, 1f , 1f ),
180+ start = Offset (i.toFloat(), 0f ),
181+ end = Offset (i.toFloat(), size.height),
182+ strokeWidth = 1f
183+ )
184+ }
185+ }
186+
187+ // Current hue indicator - position calculated in dp units
188+ Box (
189+ modifier = Modifier
190+ .offset(
191+ x = with (density) { (currentHue / 360f * sliderWidth.toPx()).toDp() - 8 .dp }
192+ )
193+ .align(Alignment .CenterStart )
194+ .size(16 .dp)
195+ .clip(SmoothRoundedCornerShape (10 .dp))
196+ .border(1 .dp, MiuixTheme .colorScheme.outline, SmoothRoundedCornerShape (10 .dp))
197+ .background(Color .hsv(currentHue, 1f , 1f ), SmoothRoundedCornerShape (10 .dp))
198+ )
199+ }
200+ }
201+
202+ @Composable
203+ private fun SaturationValuePicker (
204+ currentHue : Float ,
205+ currentSaturation : Float ,
206+ currentValue : Float ,
207+ onSaturationValueChanged : (Float , Float ) -> Unit ,
208+ modifier : Modifier = Modifier
209+ ) {
210+ val density = LocalDensity .current
211+ var pickerWidth by remember { mutableStateOf(0 .dp) }
212+ var pickerHeight by remember { mutableStateOf(0 .dp) }
213+ var pickerPosition by remember { mutableStateOf(Offset .Zero ) }
214+
215+ Box (modifier = modifier) {
216+ Canvas (
217+ modifier = Modifier
218+ .fillMaxSize()
219+ .onGloballyPositioned { coordinates ->
220+ pickerWidth = with (density) { coordinates.size.width.toDp() }
221+ pickerHeight = with (density) { coordinates.size.height.toDp() }
222+ pickerPosition = coordinates.positionInParent()
223+ }
224+ .pointerInput(Unit ) {
225+ detectTapGestures { offset ->
226+ val saturation = (1f - offset.x / size.width).coerceIn(0f , 1f )
227+ val value = (1f - offset.y / size.height).coerceIn(0f , 1f )
228+ onSaturationValueChanged(saturation, value)
229+ }
230+ }
231+ .pointerInput(Unit ) {
232+ detectDragGestures { change, _ ->
233+ change.consume()
234+ val saturation = (1f - change.position.x / size.width).coerceIn(0f , 1f )
235+ val value = (1f - change.position.y / size.height).coerceIn(0f , 1f )
236+ onSaturationValueChanged(saturation, value)
237+ }
238+ }
239+ ) {
240+ // Draw value (brightness) gradient from top to bottom
241+ val shader = object : ShaderBrush () {
242+ override fun createShader (size : Size ): Shader {
243+ return LinearGradientShader (
244+ colors = listOf (Color .hsv(currentHue, 1f , 1f ), Color .White ),
245+ from = Offset .Zero ,
246+ to = Offset (size.width, 0f ),
247+ tileMode = TileMode .Clamp
248+ )
249+ }
250+ }
251+
252+ // Draw base color
253+ drawRect(shader)
254+
255+ // Draw overlay for darkness gradient
256+ val overlayShader = object : ShaderBrush () {
257+ override fun createShader (size : Size ): Shader {
258+ return LinearGradientShader (
259+ colors = listOf (Color .Transparent , Color .Black ),
260+ from = Offset (0f , 0f ),
261+ to = Offset (0f , size.height),
262+ tileMode = TileMode .Clamp
263+ )
264+ }
265+ }
266+
267+ drawRect(overlayShader)
268+ }
269+
270+ // Current saturation/value indicator - position calculated in dp units
271+ Box (
272+ modifier = Modifier
273+ .offset(
274+ x = with (density) { ((1f - currentSaturation) * pickerWidth.toPx()).toDp() - 8 .dp },
275+ y = with (density) { ((1f - currentValue) * pickerHeight.toPx()).toDp() - 8 .dp }
276+ )
277+ .size(16 .dp)
278+ .clip(SmoothRoundedCornerShape (10 .dp))
279+ .border(1 .dp, MiuixTheme .colorScheme.outline, SmoothRoundedCornerShape (10 .dp))
280+ .background(Color .hsv(currentHue, currentSaturation, currentValue), SmoothRoundedCornerShape (10 .dp))
281+ )
282+ }
283+ }
0 commit comments