Skip to content

Commit 230c5c6

Browse files
committed
library: Add ColorPicker Component
1 parent 3c81599 commit 230c5c6

File tree

12 files changed

+371
-13
lines changed

12 files changed

+371
-13
lines changed

composeApp/src/commonMain/kotlin/ThirdPage.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import top.yukonga.miuix.kmp.extra.SuperDialog
1717
import top.yukonga.miuix.kmp.extra.SuperDropdown
1818
import top.yukonga.miuix.kmp.extra.SuperSwitch
1919
import top.yukonga.miuix.kmp.theme.MiuixTheme
20-
import top.yukonga.miuix.kmp.utils.MiuixPopupUtil.Companion.dismissDialog
20+
import top.yukonga.miuix.kmp.utils.MiuixPopupUtils.Companion.dismissDialog
2121
import top.yukonga.miuix.kmp.utils.getWindowSize
2222
import utils.VersionInfo
2323

composeApp/src/commonMain/kotlin/UITest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ import top.yukonga.miuix.kmp.icon.icons.useful.More
5858
import top.yukonga.miuix.kmp.icon.icons.useful.NavigatorSwitch
5959
import top.yukonga.miuix.kmp.icon.icons.useful.Order
6060
import top.yukonga.miuix.kmp.icon.icons.useful.Settings
61-
import top.yukonga.miuix.kmp.utils.MiuixPopupUtil.Companion.dismissPopup
61+
import top.yukonga.miuix.kmp.utils.MiuixPopupUtils.Companion.dismissPopup
6262
import utils.FPSMonitor
6363

6464
@OptIn(FlowPreview::class)

composeApp/src/commonMain/kotlin/component/OtherComponent.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package component
22

33
import androidx.compose.foundation.layout.Arrangement
4+
import androidx.compose.foundation.layout.Column
45
import androidx.compose.foundation.layout.FlowRow
56
import androidx.compose.foundation.layout.PaddingValues
67
import androidx.compose.foundation.layout.Row
@@ -27,6 +28,7 @@ import androidx.compose.ui.unit.sp
2728
import top.yukonga.miuix.kmp.basic.ButtonDefaults
2829
import top.yukonga.miuix.kmp.basic.Card
2930
import top.yukonga.miuix.kmp.basic.CircularProgressIndicator
31+
import top.yukonga.miuix.kmp.basic.ColorPicker
3032
import top.yukonga.miuix.kmp.basic.Icon
3133
import top.yukonga.miuix.kmp.basic.InfiniteProgressIndicator
3234
import top.yukonga.miuix.kmp.basic.LinearProgressIndicator
@@ -79,6 +81,7 @@ import top.yukonga.miuix.kmp.icon.icons.useful.Undo
7981
import top.yukonga.miuix.kmp.icon.icons.useful.Unstick
8082
import top.yukonga.miuix.kmp.icon.icons.useful.Update
8183
import top.yukonga.miuix.kmp.theme.MiuixTheme
84+
import kotlin.math.round
8285

8386
@Composable
8487
fun OtherComponent(padding: PaddingValues) {
@@ -138,6 +141,8 @@ fun OtherComponent(padding: PaddingValues) {
138141
MiuixIcons.Useful.Unstick,
139142
MiuixIcons.Useful.Update
140143
)
144+
val miuixColor = MiuixTheme.colorScheme.primary
145+
var selectedColor by remember { mutableStateOf(miuixColor) }
141146

142147
SmallTitle(text = "Button")
143148
Row(
@@ -310,6 +315,31 @@ fun OtherComponent(padding: PaddingValues) {
310315
}
311316
}
312317

318+
SmallTitle(text = "ColorPicker")
319+
Card(
320+
modifier = Modifier
321+
.fillMaxWidth()
322+
.padding(horizontal = 12.dp)
323+
.padding(bottom = 12.dp),
324+
insideMargin = PaddingValues(16.dp)
325+
) {
326+
Column {
327+
Text(
328+
text = "Color: RGBA(" +
329+
"${(selectedColor.red * 255).toInt()}," +
330+
"${(selectedColor.green * 255).toInt()}," +
331+
"${(selectedColor.blue * 255).toInt()}," +
332+
"${(round(selectedColor.alpha * 100) / 100.0)})",
333+
modifier = Modifier.padding(bottom = 6.dp)
334+
)
335+
336+
ColorPicker(
337+
initialColor = selectedColor,
338+
onColorChanged = { selectedColor = it }
339+
)
340+
}
341+
}
342+
313343
SmallTitle(text = "Card")
314344
Card(
315345
modifier = Modifier
@@ -340,4 +370,5 @@ fun OtherComponent(padding: PaddingValues) {
340370
style = MiuixTheme.textStyles.paragraph
341371
)
342372
}
373+
343374
}

composeApp/src/commonMain/kotlin/component/TextComponent.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ import top.yukonga.miuix.kmp.extra.SuperSwitch
4747
import top.yukonga.miuix.kmp.icon.MiuixIcons
4848
import top.yukonga.miuix.kmp.icon.icons.useful.Personal
4949
import top.yukonga.miuix.kmp.theme.MiuixTheme
50-
import top.yukonga.miuix.kmp.utils.MiuixPopupUtil.Companion.dismissDialog
50+
import top.yukonga.miuix.kmp.utils.MiuixPopupUtils.Companion.dismissDialog
5151

5252
@Composable
5353
fun TextComponent(
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
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+
}

miuix/src/commonMain/kotlin/top/yukonga/miuix/kmp/basic/ListPopup.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,8 @@ import androidx.compose.ui.unit.LayoutDirection
4343
import androidx.compose.ui.unit.dp
4444
import top.yukonga.miuix.kmp.theme.MiuixTheme
4545
import top.yukonga.miuix.kmp.utils.BackHandler
46-
import top.yukonga.miuix.kmp.utils.MiuixPopupUtil.Companion.dismissPopup
47-
import top.yukonga.miuix.kmp.utils.MiuixPopupUtil.Companion.showPopup
46+
import top.yukonga.miuix.kmp.utils.MiuixPopupUtils.Companion.dismissPopup
47+
import top.yukonga.miuix.kmp.utils.MiuixPopupUtils.Companion.showPopup
4848
import top.yukonga.miuix.kmp.utils.SmoothRoundedCornerShape
4949
import top.yukonga.miuix.kmp.utils.getWindowSize
5050
import kotlin.math.min

0 commit comments

Comments
 (0)