diff --git a/lawnchair/res/values/strings.xml b/lawnchair/res/values/strings.xml
index 241d3db35a..d7fd16709d 100644
--- a/lawnchair/res/values/strings.xml
+++ b/lawnchair/res/values/strings.xml
@@ -111,6 +111,7 @@
When you swipe up to open Recents, the current app follows your finger, shrinking into a card. Use this slider to adjust the corner radius of the card when it’s nearly full-screen such that it matches the corners of your screen.
System Icons
Accent Color
+ Invalid Color
Done
Loading…
System
@@ -180,6 +181,16 @@
Dynamic
Search for More Apps
Show Search Bar
+ RGB
+ Red
+ Green
+ Blue
+ HSB
+ Hue
+ Saturation
+ Brightness
+ Hex
+ Sliders
Lawnchair Bug Report
%1$s Crashed
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/GeneralPreferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/GeneralPreferences.kt
index 67d17fb213..4588055a65 100644
--- a/lawnchair/src/app/lawnchair/ui/preferences/GeneralPreferences.kt
+++ b/lawnchair/src/app/lawnchair/ui/preferences/GeneralPreferences.kt
@@ -27,7 +27,6 @@ import app.lawnchair.preferences.getAdapter
import app.lawnchair.preferences.preferenceManager
import app.lawnchair.preferences2.asState
import app.lawnchair.preferences2.preferenceManager2
-import app.lawnchair.ui.preferences.components.AccentColorPreference
import app.lawnchair.ui.preferences.components.ExpandAndShrink
import app.lawnchair.ui.preferences.components.FontPreference
import app.lawnchair.ui.preferences.components.IconShapePreference
@@ -38,6 +37,7 @@ import app.lawnchair.ui.preferences.components.PreferenceLayout
import app.lawnchair.ui.preferences.components.SliderPreference
import app.lawnchair.ui.preferences.components.SwitchPreference
import app.lawnchair.ui.preferences.components.ThemePreference
+import app.lawnchair.ui.preferences.components.colorpreference.ColorPreference
import app.lawnchair.ui.preferences.components.notificationDotsEnabled
import app.lawnchair.ui.preferences.components.notificationServiceEnabled
import com.android.launcher3.R
@@ -110,7 +110,10 @@ fun GeneralPreferences() {
}
PreferenceGroup(heading = stringResource(id = R.string.colors)) {
ThemePreference()
- AccentColorPreference()
+ ColorPreference(
+ preference = prefs2.accentColor,
+ label = stringResource(id = R.string.accent_color),
+ )
}
val wrapAdaptiveIcons = prefs.wrapAdaptiveIcons.getAdapter()
PreferenceGroup(
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt b/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt
index a4da801ba6..afcb142b78 100644
--- a/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt
+++ b/lawnchair/src/app/lawnchair/ui/preferences/Preferences.kt
@@ -30,6 +30,7 @@ import app.lawnchair.backup.ui.createBackupGraph
import app.lawnchair.backup.ui.restoreBackupGraph
import app.lawnchair.ui.preferences.about.aboutGraph
import app.lawnchair.ui.preferences.components.SystemUi
+import app.lawnchair.ui.preferences.components.colorpreference.colorSelectionGraph
import app.lawnchair.ui.util.ProvideBottomSheetHandler
import app.lawnchair.util.ProvideLifecycleState
import com.google.accompanist.navigation.animation.AnimatedNavHost
@@ -45,6 +46,7 @@ object Routes {
const val FOLDERS = "folders"
const val QUICKSTEP = "quickstep"
const val FONT_SELECTION = "fontSelection"
+ const val COLOR_SELECTION = "colorSelection"
const val DEBUG_MENU = "debugMenu"
const val SELECT_ICON = "selectIcon"
const val ICON_PICKER = "iconPicker"
@@ -97,6 +99,7 @@ fun Preferences(interactor: PreferenceInteractor = viewModel Unit = {},
+ bottomBar: @Composable () -> Unit = { BottomSpacer() },
backArrowVisible: Boolean = true,
content: @Composable ColumnScope.() -> Unit
) {
@@ -47,6 +48,7 @@ fun PreferenceLayout(
floating = rememberFloatingState(scrollState),
label = label,
actions = actions,
+ bottomBar = bottomBar,
) {
PreferenceColumn(
verticalArrangement = verticalArrangement,
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceScaffold.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceScaffold.kt
index d58d60a5d4..031d9f0997 100644
--- a/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceScaffold.kt
+++ b/lawnchair/src/app/lawnchair/ui/preferences/components/PreferenceScaffold.kt
@@ -15,6 +15,7 @@ fun PreferenceScaffold(
floating: State = remember { mutableStateOf(false) },
label: String,
actions: @Composable RowScope.() -> Unit = {},
+ bottomBar: @Composable () -> Unit = { BottomSpacer() },
content: @Composable (PaddingValues) -> Unit
) {
Scaffold(
@@ -26,7 +27,7 @@ fun PreferenceScaffold(
actions = actions,
)
},
- bottomBar = { BottomSpacer() },
+ bottomBar = bottomBar,
contentPadding = WindowInsets.systemBars.only(WindowInsetsSides.Horizontal).asPaddingValues()
) {
content(it)
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorConverter.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorConverter.kt
new file mode 100644
index 0000000000..c23b49a1d1
--- /dev/null
+++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorConverter.kt
@@ -0,0 +1,21 @@
+package app.lawnchair.ui.preferences.components.colorpreference
+
+import android.graphics.Color.colorToHSV
+import android.graphics.Color.HSVToColor
+import android.graphics.Color.parseColor
+
+fun intColorToHsvColorArray(color: Int) =
+ FloatArray(size = 3).apply { colorToHSV(color, this) }
+
+fun hsvValuesToIntColor(hue: Float, saturation: Float, brightness: Float): Int =
+ HSVToColor(floatArrayOf(hue, saturation, brightness))
+
+fun intColorToColorString(color: Int) =
+ String.format("#%06X", 0xFFFFFF and color).removePrefix("#")
+
+fun colorStringToIntColor(colorString: String): Int? =
+ try {
+ parseColor("#${colorString.removePrefix("#")}")
+ } catch (e: IllegalArgumentException) {
+ null
+ }
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/AccentColorPreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorOptions.kt
similarity index 54%
rename from lawnchair/src/app/lawnchair/ui/preferences/components/AccentColorPreference.kt
rename to lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorOptions.kt
index ae2e8d4ac9..affe805bc3 100644
--- a/lawnchair/src/app/lawnchair/ui/preferences/components/AccentColorPreference.kt
+++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorOptions.kt
@@ -1,12 +1,6 @@
-package app.lawnchair.ui.preferences.components
+package app.lawnchair.ui.preferences.components.colorpreference
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.res.stringResource
-import app.lawnchair.preferences.getAdapter
-import app.lawnchair.preferences2.preferenceManager2
import app.lawnchair.theme.color.ColorOption
-import app.lawnchair.ui.preferences.components.colorpreference.ColorPreference
-import com.android.launcher3.R
val staticColors = listOf(
ColorOption.CustomColor(0xFFF32020),
@@ -23,17 +17,7 @@ val staticColors = listOf(
ColorOption.CustomColor(0xFF67818E)
).map(ColorOption::colorPreferenceEntry)
+
val dynamicColors = listOf(ColorOption.SystemAccent, ColorOption.WallpaperPrimary)
.filter(ColorOption::isSupported)
.map(ColorOption::colorPreferenceEntry)
-
-@Composable
-fun AccentColorPreference() {
- val adapter = preferenceManager2().accentColor.getAdapter()
- ColorPreference(
- adapter = adapter,
- label = stringResource(id = R.string.accent_color),
- dynamicEntries = dynamicColors,
- staticEntries = staticColors,
- )
-}
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreference.kt
index f9f68113d2..58a08153bc 100644
--- a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreference.kt
+++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreference.kt
@@ -17,165 +17,36 @@
package app.lawnchair.ui.preferences.components.colorpreference
import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
-import androidx.compose.material.RadioButton
-import androidx.compose.material3.Button
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
+import androidx.compose.material3.*
import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
+import androidx.compose.ui.graphics.Color
import app.lawnchair.preferences.PreferenceAdapter
+import app.lawnchair.preferences.getAdapter
import app.lawnchair.theme.color.ColorOption
-import app.lawnchair.ui.AlertBottomSheetContent
-import app.lawnchair.ui.preferences.components.Chip
-import app.lawnchair.ui.preferences.components.PreferenceDivider
-import app.lawnchair.ui.preferences.components.PreferenceTemplate
-import app.lawnchair.ui.theme.lightenColor
-import app.lawnchair.ui.util.bottomSheetHandler
-import com.android.launcher3.R
-import com.google.accompanist.pager.ExperimentalPagerApi
-import com.google.accompanist.pager.HorizontalPager
-import com.google.accompanist.pager.rememberPagerState
-import kotlinx.coroutines.launch
+import app.lawnchair.ui.preferences.LocalNavController
+import app.lawnchair.ui.preferences.components.*
+import com.patrykmichalik.opto.domain.Preference
-@OptIn(ExperimentalPagerApi::class)
+/**
+ * A a custom implementation of [PreferenceTemplate] for [ColorOption] preferences.
+ *
+ * @see colorSelectionGraph
+ * @see ColorSelection
+ */
@Composable
fun ColorPreference(
- adapter: PreferenceAdapter,
+ preference: Preference,
label: String,
- dynamicEntries: List>,
- staticEntries: List>,
) {
- var selectedColor by adapter
- val selectedEntry = dynamicEntries.firstOrNull { it.value == selectedColor } ?: staticEntries.firstOrNull { it.value == selectedColor }
- val defaultTabIndex = if (dynamicEntries.any { it.value == selectedColor }) 0 else 1
- val description = selectedEntry?.label?.invoke()
- val bottomSheetHandler = bottomSheetHandler
- var bottomSheetShown by remember { mutableStateOf(false) }
-
+ val adapter: PreferenceAdapter = preference.getAdapter()
+ val navController = LocalNavController.current
PreferenceTemplate(
title = { Text(text = label) },
- endWidget = { ColorDot(color = MaterialTheme.colorScheme.primary) },
- modifier = Modifier.clickable { bottomSheetShown = true },
+ endWidget = { ColorDot(Color(adapter.state.value.colorPreferenceEntry.lightColor())) },
description = {
- if (description != null) {
- Text(text = description)
- }
+ Text(text = adapter.state.value.colorPreferenceEntry.label())
},
+ modifier = Modifier.clickable { navController.navigate(route = "/colorSelection/${preference.key}/") },
)
-
- if (bottomSheetShown) {
- bottomSheetHandler.onDismiss { bottomSheetShown = false }
- bottomSheetHandler.show {
- val pagerState = rememberPagerState(defaultTabIndex)
- val scope = rememberCoroutineScope()
- val scrollToPage = { page: Int -> scope.launch { pagerState.animateScrollToPage(page) } }
- AlertBottomSheetContent(
- title = { Text(text = label) },
- buttons = {
- Button(onClick = { bottomSheetHandler.hide() }) {
- Text(text = stringResource(id = R.string.done))
- }
- }
- ) {
- Column {
- Row(
- horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
- modifier = Modifier.padding(horizontal = 16.dp),
- ) {
- Chip(
- label = stringResource(id = R.string.dynamic),
- onClick = { scrollToPage(0) },
- currentOffset = pagerState.currentPage + pagerState.currentPageOffset,
- page = 0,
- )
- Chip(
- label = stringResource(id = R.string.presets),
- onClick = { scrollToPage(1) },
- currentOffset = pagerState.currentPage + pagerState.currentPageOffset,
- page = 1,
- )
- }
- HorizontalPager(
- count = 2,
- state = pagerState,
- verticalAlignment = Alignment.Top,
- modifier = Modifier.pagerHeight(
- dynamicCount = dynamicEntries.size,
- staticCount = staticEntries.size,
- ),
- ) { page ->
- when (page) {
- 0 -> {
- PresetsList(
- dynamicEntries = dynamicEntries,
- adapter = adapter,
- )
- }
- 1 -> {
- SwatchGrid(
- entries = staticEntries,
- modifier = Modifier.padding(
- start = 16.dp,
- top = 20.dp,
- end = 16.dp,
- bottom = 16.dp,
- ),
- onSwatchClick = { selectedColor = it },
- isSwatchSelected = { it == selectedColor },
- )
- }
- }
- }
- }
- }
- }
- }
}
-
-@Composable
-private fun PresetsList(
- dynamicEntries: List>,
- adapter: PreferenceAdapter,
-) {
- Box(
- modifier = Modifier
- .fillMaxHeight(),
- contentAlignment = Alignment.TopStart
- ) {
- Column(modifier = Modifier.padding(top = 16.dp)) {
- dynamicEntries.mapIndexed { index, entry ->
- key(entry) {
- if (index > 0) {
- PreferenceDivider(startIndent = 40.dp)
- }
- PreferenceTemplate(
- title = { Text(text = entry.label()) },
- verticalPadding = 12.dp,
- modifier = Modifier.clickable { adapter.onChange(entry.value) },
- startWidget = {
- RadioButton(
- selected = entry.value == adapter.state.value,
- onClick = null
- )
- ColorDot(
- entry = entry,
- modifier = Modifier.padding(start = 16.dp)
- )
- }
- )
- }
- }
- }
- }
-}
-
-open class ColorPreferenceEntry(
- val value: T,
- val label: @Composable () -> String,
- val lightColor: @Composable () -> Int,
- val darkColor: @Composable () -> Int = { lightenColor(lightColor()) },
-)
\ No newline at end of file
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreferenceEntry.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreferenceEntry.kt
new file mode 100644
index 0000000000..22b8fe44d3
--- /dev/null
+++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorPreferenceEntry.kt
@@ -0,0 +1,11 @@
+package app.lawnchair.ui.preferences.components.colorpreference
+
+import androidx.compose.runtime.Composable
+import app.lawnchair.ui.theme.lightenColor
+
+open class ColorPreferenceEntry(
+ val value: T,
+ val label: @Composable () -> String,
+ val lightColor: @Composable () -> Int,
+ val darkColor: @Composable () -> Int = { lightenColor(lightColor()) },
+)
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorSelectionPreference.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorSelectionPreference.kt
new file mode 100644
index 0000000000..e10907f6d5
--- /dev/null
+++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorSelectionPreference.kt
@@ -0,0 +1,168 @@
+package app.lawnchair.ui.preferences.components.colorpreference
+
+import androidx.compose.animation.ExperimentalAnimationApi
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.layout.*
+import androidx.compose.material3.Text
+import androidx.compose.material3.Button
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.navigation.NavGraphBuilder
+import androidx.navigation.NavType
+import androidx.navigation.navArgument
+import app.lawnchair.preferences.getAdapter
+import app.lawnchair.preferences2.preferenceManager2
+import app.lawnchair.theme.color.ColorOption
+import app.lawnchair.ui.preferences.components.*
+import app.lawnchair.ui.preferences.components.colorpreference.pickers.CustomColorPicker
+import app.lawnchair.ui.preferences.components.colorpreference.pickers.PresetsList
+import app.lawnchair.ui.preferences.components.colorpreference.pickers.SwatchGrid
+import app.lawnchair.ui.preferences.preferenceGraph
+import com.android.launcher3.R
+import com.google.accompanist.navigation.animation.composable
+import com.google.accompanist.pager.ExperimentalPagerApi
+import com.google.accompanist.pager.HorizontalPager
+import com.google.accompanist.pager.rememberPagerState
+import com.patrykmichalik.opto.domain.Preference
+import kotlinx.coroutines.launch
+
+@OptIn(ExperimentalAnimationApi::class)
+fun NavGraphBuilder.colorSelectionGraph(route: String) {
+ preferenceGraph(route, {}) { subRoute ->
+ composable(
+ route = subRoute("{prefKey}"),
+ arguments = listOf(
+ navArgument("prefKey") { type = NavType.StringType },
+ ),
+ ) { backStackEntry ->
+
+ val args = backStackEntry.arguments!!
+ val prefKey = args.getString("prefKey")!!
+ val preferenceManager2 = preferenceManager2()
+ val pref = when (prefKey) {
+ preferenceManager2.accentColor.key.name -> preferenceManager2.accentColor
+ else -> return@composable
+ }
+ val label = when (prefKey) {
+ preferenceManager2.accentColor.key.name -> stringResource(id = R.string.accent_color)
+ else -> return@composable
+ }
+ ColorSelection(label = label, preference = pref)
+
+ }
+ }
+}
+
+@OptIn(ExperimentalPagerApi::class)
+@Composable
+fun ColorSelection(
+ label: String,
+ preference: Preference,
+ dynamicEntries: List> = dynamicColors,
+ staticEntries: List> = staticColors,
+) {
+
+ val adapter = preference.getAdapter()
+ val appliedColor by adapter
+ val appliedEntry = dynamicEntries.firstOrNull { it.value == appliedColor }
+ ?: staticEntries.firstOrNull { it.value == appliedColor }
+ val selectedColor = remember { mutableStateOf(appliedColor) }
+ val defaultTabIndex = when {
+ dynamicEntries.any { it.value == appliedColor } -> 0
+ appliedEntry?.value is ColorOption.CustomColor -> 2
+ else -> 1
+ }
+
+ PreferenceLayout(
+ label = label,
+ bottomBar = {
+ Column(
+ modifier = Modifier.fillMaxWidth(),
+ horizontalAlignment = Alignment.End
+ ) {
+ Button(
+ modifier = Modifier.padding(horizontal = 16.dp),
+ onClick = {
+ adapter.onChange(newValue = selectedColor.value)
+ },
+ ) {
+ Text(text = stringResource(id = R.string.apply_grid))
+ }
+ BottomSpacer()
+ }
+ },
+ ) {
+
+ val pagerState = rememberPagerState(defaultTabIndex)
+ val scope = rememberCoroutineScope()
+ val scrollToPage =
+ { page: Int -> scope.launch { pagerState.animateScrollToPage(page) } }
+
+ Column {
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
+ modifier = Modifier.padding(horizontal = 16.dp),
+ ) {
+ Chip(
+ label = stringResource(id = R.string.dynamic),
+ onClick = { scrollToPage(0) },
+ currentOffset = pagerState.currentPage + pagerState.currentPageOffset,
+ page = 0,
+ )
+ Chip(
+ label = stringResource(id = R.string.presets),
+ onClick = { scrollToPage(1) },
+ currentOffset = pagerState.currentPage + pagerState.currentPageOffset,
+ page = 1,
+ )
+ Chip(
+ label = stringResource(id = R.string.custom),
+ onClick = { scrollToPage(2) },
+ currentOffset = pagerState.currentPage + pagerState.currentPageOffset,
+ page = 2,
+ )
+ }
+ HorizontalPager(
+ count = 3,
+ state = pagerState,
+ verticalAlignment = Alignment.Top,
+ modifier = Modifier.animateContentSize(),
+ ) { page ->
+ when (page) {
+ 0 -> {
+ PresetsList(
+ dynamicEntries = dynamicEntries,
+ onPresetClick = { selectedColor.value = it },
+ isPresetSelected = { it == selectedColor.value },
+ )
+ }
+ 1 -> {
+ SwatchGrid(
+ modifier = Modifier.padding(top = 12.dp),
+ contentModifier = Modifier.padding(
+ start = 16.dp,
+ top = 20.dp,
+ end = 16.dp,
+ bottom = 16.dp,
+ ),
+ entries = staticEntries,
+ onSwatchClick = { selectedColor.value = it },
+ isSwatchSelected = { it == selectedColor.value },
+ )
+ }
+ 2 -> {
+ CustomColorPicker(
+ selectedColorOption = selectedColor.value,
+ onSelect = { selectedColor.value = it },
+ )
+ }
+ }
+ }
+
+ }
+ }
+}
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorSlider.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorSlider.kt
new file mode 100644
index 0000000000..a61011d989
--- /dev/null
+++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/ColorSlider.kt
@@ -0,0 +1,189 @@
+package app.lawnchair.ui.preferences.components.colorpreference
+
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.ContentAlpha
+import androidx.compose.material.LocalContentAlpha
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Brush
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import androidx.core.util.toRange
+import app.lawnchair.ui.preferences.components.PreferenceTemplate
+import app.lawnchair.ui.preferences.components.getSteps
+import app.lawnchair.ui.preferences.components.snapSliderValue
+import com.android.launcher3.R
+import kotlin.math.roundToInt
+
+@Composable
+fun RgbColorSlider(
+ label: String,
+ colorStart: Color,
+ colorEnd: Color,
+ value: Int,
+ onValueChange: (Float) -> Unit,
+) {
+
+ val step = 0f
+ val rgbRange = 0f..255f
+
+ PreferenceTemplate(
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .padding(bottom = 12.dp),
+ title = {
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(top = 16.dp),
+ ) {
+ Text(text = label)
+ CompositionLocalProvider(
+ LocalContentAlpha provides ContentAlpha.medium,
+ LocalContentColor provides androidx.compose.material.MaterialTheme.colors.onBackground,
+ ) {
+ val valueText = snapSliderValue(rgbRange.start, value.toFloat(), step)
+ .roundToInt().toString()
+ Text(text = valueText)
+ }
+ }
+ },
+ description = {
+ Row(
+ modifier = Modifier.padding(top = 4.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ ) {
+ Box(
+ modifier = Modifier
+ .requiredSize(6.dp)
+ .clip(CircleShape)
+ .background(colorStart),
+ )
+ Slider(
+ value = value.toFloat(),
+ onValueChange = onValueChange,
+ valueRange = rgbRange,
+ steps = getSteps(rgbRange, step),
+ modifier = Modifier
+ .height(24.dp)
+ .weight(1f)
+ .padding(horizontal = 3.dp),
+ )
+ Box(
+ modifier = Modifier
+ .requiredSize(6.dp)
+ .clip(CircleShape)
+ .background(colorEnd),
+ )
+ }
+ },
+ applyPaddings = false,
+ )
+
+}
+
+@Composable
+fun HsbColorSlider(
+ type: HsbSliderType,
+ value: Float,
+ onValueChange: (Float) -> Unit,
+) {
+
+ val step = 0f
+
+ val range = when (type) {
+ HsbSliderType.HUE -> 0f..359f
+ else -> 0f..1f
+ }
+
+ val showAsPercentage = when (type) {
+ HsbSliderType.HUE -> false
+ else -> true
+ }
+
+ val label = when (type) {
+ HsbSliderType.HUE -> stringResource(id = R.string.hsb_hue)
+ HsbSliderType.SATURATION -> stringResource(id = R.string.hsb_saturation)
+ HsbSliderType.BRIGHTNESS -> stringResource(id = R.string.hsb_brightness)
+ }
+
+ PreferenceTemplate(
+ modifier = Modifier.padding(horizontal = 8.dp),
+ title = {
+ Row(
+ horizontalArrangement = Arrangement.SpaceBetween,
+ verticalAlignment = Alignment.CenterVertically,
+ modifier = Modifier
+ .padding(horizontal = 6.dp)
+ .fillMaxWidth()
+ .padding(top = 16.dp),
+ ) {
+ Text(text = label)
+ CompositionLocalProvider(
+ LocalContentAlpha provides ContentAlpha.medium,
+ LocalContentColor provides androidx.compose.material.MaterialTheme.colors.onBackground,
+ ) {
+ val valueText = snapSliderValue(range.start, value, step)
+ Text(
+ text = if (showAsPercentage) stringResource(
+ id = R.string.n_percent,
+ (valueText * 100).roundToInt(),
+ ) else "${valueText.roundToInt()}°",
+ )
+ }
+ }
+ },
+ description = {
+ Column(
+ modifier = Modifier.padding(top = 4.dp, bottom = 12.dp),
+ ) {
+ if (type == HsbSliderType.HUE) {
+ val brushColors = arrayListOf()
+ val stepSize = 6
+ repeat((range.endInclusive - range.start).toInt() / stepSize) {
+ val newColor = Color.hsv(
+ hue = it * stepSize.toFloat(),
+ saturation = 1f,
+ value = 1f,
+ )
+ brushColors.add(newColor)
+ }
+ Box(
+ modifier = Modifier
+ .fillMaxWidth()
+ .padding(bottom = 8.dp)
+ .padding(horizontal = 6.dp)
+ .requiredHeight(24.dp)
+ .clip(RoundedCornerShape(6.dp))
+ .background(brush = Brush.horizontalGradient(brushColors)),
+ )
+ }
+ Slider(
+ value = value,
+ onValueChange = onValueChange,
+ onValueChangeFinished = { },
+ valueRange = range,
+ steps = getSteps(range, step),
+ colors = SliderDefaults.colors(),
+ modifier = Modifier
+ .height(24.dp)
+ .fillMaxWidth(),
+ )
+ }
+ },
+ applyPaddings = false,
+ )
+}
+
+enum class HsbSliderType {
+ HUE, SATURATION, BRIGHTNESS
+}
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/PagerHeightModifier.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/PagerHeightModifier.kt
deleted file mode 100644
index 0a7ef7b11f..0000000000
--- a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/PagerHeightModifier.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-package app.lawnchair.ui.preferences.components.colorpreference
-
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.layout.LayoutModifier
-import androidx.compose.ui.layout.Measurable
-import androidx.compose.ui.layout.MeasureResult
-import androidx.compose.ui.layout.MeasureScope
-import androidx.compose.ui.unit.Constraints
-import androidx.compose.ui.unit.constrainHeight
-import androidx.compose.ui.unit.dp
-import java.util.*
-import kotlin.math.max
-import kotlin.math.min
-
-fun Modifier.pagerHeight(
- dynamicCount: Int,
- staticCount: Int
-) = this.then(
- PagerHeightModifier(
- dynamicCount = dynamicCount,
- staticCount = staticCount
- )
-)
-
-class PagerHeightModifier(
- private val dynamicCount: Int,
- private val staticCount: Int
-) : LayoutModifier {
-
- override fun MeasureScope.measure(
- measurable: Measurable,
- constraints: Constraints
- ): MeasureResult {
- val dynamicItemHeight = 54.dp.roundToPx()
- val dynamicListHeight = 16.dp.roundToPx() +
- dynamicItemHeight * dynamicCount +
- 1.dp.roundToPx() * (dynamicCount - 1)
-
- val columnCount = SwatchGridDefaults.ColumnCount
- val rowCount = (staticCount - 1) / columnCount + 1
-
- val width = constraints.maxWidth
- val horizontalPadding = 16.dp.roundToPx() * 2
- val gutterSizePx = SwatchGridDefaults.GutterSize.roundToPx()
- val totalGutterWidth = gutterSizePx * (columnCount - 1)
- val availableWidth = width - horizontalPadding - totalGutterWidth
- val swatchMaxWidth = SwatchGridDefaults.SwatchMaxWidth.roundToPx()
- val swatchWidth = min(availableWidth / columnCount, swatchMaxWidth)
-
- val swatchGridVerticalPadding = 20.dp.roundToPx() + 16.dp.roundToPx()
- val swatchGridInnerHeight = swatchWidth * rowCount + gutterSizePx * (rowCount - 1)
- val swatchGridHeight = swatchGridInnerHeight + swatchGridVerticalPadding
-
- val height = constraints.constrainHeight(max(dynamicListHeight, swatchGridHeight))
-
- val placeable = measurable.measure(constraints.copy(maxHeight = height))
- return layout(width, constraints.constrainHeight(swatchGridHeight)) {
- placeable.place(0, 0)
- }
- }
-
- override fun hashCode(): Int {
- return Objects.hash(dynamicCount, staticCount)
- }
-
- override fun equals(other: Any?): Boolean {
- return other is PagerHeightModifier
- && dynamicCount == other.dynamicCount
- && staticCount == other.staticCount
- }
-}
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/CustomColorPicker.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/CustomColorPicker.kt
new file mode 100644
index 0000000000..9d27abf8b5
--- /dev/null
+++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/CustomColorPicker.kt
@@ -0,0 +1,350 @@
+package app.lawnchair.ui.preferences.components.colorpreference.pickers
+
+import androidx.compose.animation.Crossfade
+import androidx.compose.animation.animateContentSize
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.text.KeyboardActions
+import androidx.compose.foundation.text.KeyboardOptions
+import androidx.compose.material3.*
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.input.ImeAction
+import androidx.compose.ui.text.input.KeyboardCapitalization
+import androidx.compose.ui.text.input.TextFieldValue
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.core.graphics.blue
+import androidx.core.graphics.green
+import androidx.core.graphics.red
+import app.lawnchair.theme.color.ColorOption
+import app.lawnchair.ui.preferences.components.Chip
+import app.lawnchair.ui.preferences.components.DividerColumn
+import app.lawnchair.ui.preferences.components.PreferenceGroup
+import app.lawnchair.ui.preferences.components.colorpreference.*
+import com.android.launcher3.R
+import com.google.accompanist.pager.ExperimentalPagerApi
+import com.google.accompanist.pager.HorizontalPager
+import com.google.accompanist.pager.rememberPagerState
+import kotlinx.coroutines.launch
+
+/**
+ * Unlike [PresetsList] & [SwatchGrid], This Composable allows the user to select a fully custom [ColorOption] using HEX, HSB & RGB values.
+ *
+ * @see HexColorPicker
+ * @see HsvColorPicker
+ * @see RgbColorPicker
+ */
+@OptIn(ExperimentalPagerApi::class)
+@Composable
+fun CustomColorPicker(
+ modifier: Modifier = Modifier,
+ selectedColorOption: ColorOption,
+ onSelect: (ColorOption) -> Unit,
+) {
+
+ val focusManager = LocalFocusManager.current
+
+ val selectedColor = selectedColorOption.colorPreferenceEntry.lightColor()
+ val selectedColorCompose = Color(selectedColor)
+
+ val textFieldValue = remember {
+ mutableStateOf(
+ TextFieldValue(
+ text = intColorToColorString(color = selectedColor),
+ )
+ )
+ }
+
+ Column(modifier = modifier) {
+
+ PreferenceGroup(heading = stringResource(id = R.string.hex)) {
+
+ Row(
+ modifier = Modifier.padding(16.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.Center,
+ ) {
+
+ Box(
+ modifier = Modifier
+ .requiredSize(48.dp)
+ .clip(CircleShape)
+ .background(selectedColorCompose),
+ )
+
+ Spacer(modifier = Modifier.requiredWidth(16.dp))
+
+ HexColorPicker(
+ textFieldValue = textFieldValue.value,
+ onTextFieldValueChange = { newValue ->
+ val newText = newValue.text.removePrefix("#").take(6).uppercase()
+ textFieldValue.value = newValue.copy(text = newText)
+ val newColor = colorStringToIntColor(colorString = newText)
+ if (newColor != null) {
+ onSelect(ColorOption.CustomColor(newColor))
+ }
+ },
+ )
+
+ }
+ }
+
+ val pagerState = rememberPagerState(0)
+ val scope = rememberCoroutineScope()
+ val scrollToPage =
+ { page: Int -> scope.launch { pagerState.animateScrollToPage(page) } }
+
+ PreferenceGroup(
+ heading = stringResource(id = R.string.color_sliders),
+ ) {
+
+ Column {
+
+ Row(
+ horizontalArrangement = Arrangement.spacedBy(space = 8.dp),
+ modifier = Modifier
+ .padding(horizontal = 16.dp)
+ .padding(top = 12.dp),
+ ) {
+ Chip(
+ label = stringResource(id = R.string.hsb),
+ onClick = { scrollToPage(0) },
+ currentOffset = pagerState.currentPage + pagerState.currentPageOffset,
+ page = 0,
+ )
+ Chip(
+ label = stringResource(id = R.string.rgb),
+ onClick = { scrollToPage(1) },
+ currentOffset = pagerState.currentPage + pagerState.currentPageOffset,
+ page = 1,
+ )
+ }
+
+ HorizontalPager(
+ modifier = Modifier.animateContentSize(),
+ count = 2,
+ state = pagerState,
+ verticalAlignment = Alignment.Top,
+ ) { page ->
+ when (page) {
+ 0 -> {
+ HsvColorPicker(
+ selectedColor = selectedColor,
+ onSelectedColorChange = {
+ textFieldValue.value =
+ textFieldValue.value.copy(
+ text = intColorToColorString(
+ selectedColor,
+ ),
+ )
+ },
+ onSliderValuesChange = { newColor ->
+ focusManager.clearFocus()
+ onSelect(newColor)
+ }
+ )
+ }
+ 1 -> {
+ RgbColorPicker(
+ selectedColor = selectedColor,
+ onSelectedColorChange = {
+ textFieldValue.value =
+ textFieldValue.value.copy(
+ text = intColorToColorString(selectedColor),
+ )
+ },
+ onSliderValuesChange = { newColor ->
+ focusManager.clearFocus()
+ onSelect(newColor)
+ },
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
+@Composable
+private fun HexColorPicker(
+ textFieldValue: TextFieldValue,
+ onTextFieldValueChange: (TextFieldValue) -> Unit,
+) {
+
+ val focusManager = LocalFocusManager.current
+
+ val invalidString = colorStringToIntColor(textFieldValue.text) == null
+
+ OutlinedTextField(
+ textStyle = LocalTextStyle.current.copy(
+ fontSize = 18.sp,
+ textAlign = TextAlign.Start,
+ ),
+ isError = invalidString,
+ value = textFieldValue,
+ onValueChange = onTextFieldValueChange,
+ singleLine = true,
+ keyboardOptions = KeyboardOptions(
+ capitalization = KeyboardCapitalization.Characters,
+ autoCorrect = false,
+ imeAction = ImeAction.Done,
+ ),
+ keyboardActions = KeyboardActions(
+ onDone = {
+ focusManager.clearFocus()
+ },
+ ),
+ trailingIcon = {
+ Crossfade(targetState = invalidString) {
+ if (it) {
+ Icon(
+ painter = painterResource(id = R.drawable.ic_warning),
+ contentDescription = stringResource(id = R.string.invalid_color),
+ )
+ }
+ }
+ },
+ )
+}
+
+@Composable
+private fun HsvColorPicker(
+ selectedColor: Int,
+ onSelectedColorChange: () -> Unit,
+ onSliderValuesChange: (ColorOption.CustomColor) -> Unit,
+) {
+
+ val hue = remember { mutableStateOf(intColorToHsvColorArray(selectedColor)[0]) }
+ val saturation = remember { mutableStateOf(intColorToHsvColorArray(selectedColor)[1]) }
+ val brightness = remember { mutableStateOf(intColorToHsvColorArray(selectedColor)[2]) }
+
+ DividerColumn {
+
+ HsbColorSlider(
+ type = HsbSliderType.HUE,
+ value = hue.value,
+ onValueChange = { newValue ->
+ hue.value = newValue
+ },
+ )
+ HsbColorSlider(
+ type = HsbSliderType.SATURATION,
+ value = saturation.value,
+ onValueChange = { newValue ->
+ saturation.value = newValue
+ },
+ )
+ HsbColorSlider(
+ type = HsbSliderType.BRIGHTNESS,
+ value = brightness.value,
+ onValueChange = { newValue ->
+ brightness.value = newValue
+ },
+ )
+
+ LaunchedEffect(key1 = selectedColor) {
+ val hsv = intColorToHsvColorArray(selectedColor)
+ hue.value = hsv[0]
+ saturation.value = hsv[1]
+ brightness.value = hsv[2]
+ onSelectedColorChange()
+ }
+
+ LaunchedEffect(
+ key1 = hue.value,
+ key2 = saturation.value,
+ key3 = brightness.value,
+ ) {
+ onSliderValuesChange(
+ ColorOption.CustomColor(
+ hsvValuesToIntColor(
+ hue = hue.value,
+ saturation = saturation.value,
+ brightness = brightness.value,
+ ),
+ ),
+ )
+ }
+
+ }
+
+}
+
+@Composable
+private fun RgbColorPicker(
+ selectedColor: Int,
+ selectedColorCompose: Color = Color(selectedColor),
+ onSelectedColorChange: () -> Unit,
+ onSliderValuesChange: (ColorOption.CustomColor) -> Unit,
+) {
+
+ val red = remember { mutableStateOf(selectedColor.red) }
+ val green = remember { mutableStateOf(selectedColor.green) }
+ val blue = remember { mutableStateOf(selectedColor.blue) }
+
+ DividerColumn {
+
+ RgbColorSlider(
+ label = stringResource(id = R.string.rgb_red),
+ value = red.value,
+ colorStart = selectedColorCompose.copy(red = 0f),
+ colorEnd = selectedColorCompose.copy(red = 1f),
+ onValueChange = { newValue ->
+ red.value = newValue.toInt()
+ },
+ )
+ RgbColorSlider(
+ label = stringResource(id = R.string.rgb_green),
+ value = green.value,
+ colorStart = selectedColorCompose.copy(green = 0f),
+ colorEnd = selectedColorCompose.copy(green = 1f),
+ onValueChange = { newValue ->
+ green.value = newValue.toInt()
+ },
+ )
+ RgbColorSlider(
+ label = stringResource(id = R.string.rgb_blue),
+ value = blue.value,
+ colorStart = selectedColorCompose.copy(blue = 0f),
+ colorEnd = selectedColorCompose.copy(blue = 1f),
+ onValueChange = { newValue ->
+ blue.value = newValue.toInt()
+ },
+ )
+
+ LaunchedEffect(key1 = selectedColor) {
+ red.value = selectedColor.red
+ green.value = selectedColor.green
+ blue.value = selectedColor.blue
+ onSelectedColorChange()
+ }
+
+ LaunchedEffect(
+ key1 = red.value,
+ key2 = green.value,
+ key3 = blue.value,
+ ) {
+ onSliderValuesChange(
+ ColorOption.CustomColor(
+ android.graphics.Color.argb(
+ 255,
+ red.value,
+ green.value,
+ blue.value,
+ )
+ )
+ )
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/PresetsList.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/PresetsList.kt
new file mode 100644
index 0000000000..847505c270
--- /dev/null
+++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/PresetsList.kt
@@ -0,0 +1,59 @@
+package app.lawnchair.ui.preferences.components.colorpreference.pickers
+
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.RadioButton
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.key
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import app.lawnchair.theme.color.ColorOption
+import app.lawnchair.ui.preferences.components.PreferenceDivider
+import app.lawnchair.ui.preferences.components.PreferenceGroup
+import app.lawnchair.ui.preferences.components.PreferenceTemplate
+import app.lawnchair.ui.preferences.components.colorpreference.ColorDot
+import app.lawnchair.ui.preferences.components.colorpreference.ColorPreferenceEntry
+
+@OptIn(ExperimentalMaterial3Api::class)
+@Composable
+fun PresetsList(
+ dynamicEntries: List>,
+ onPresetClick: (ColorOption) -> Unit,
+ isPresetSelected: (ColorOption) -> Boolean,
+) {
+
+ PreferenceGroup(
+ modifier = Modifier.padding(top = 12.dp),
+ showDividers = false,
+ ) {
+
+ dynamicEntries.mapIndexed { index, entry ->
+ key(entry) {
+ if (index > 0) {
+ PreferenceDivider(startIndent = 40.dp)
+ }
+ PreferenceTemplate(
+ title = { Text(text = entry.label()) },
+ verticalPadding = 12.dp,
+ modifier = Modifier.clickable { onPresetClick(entry.value) },
+ startWidget = {
+ RadioButton(
+ selected = isPresetSelected(entry.value),
+ onClick = null,
+ )
+ ColorDot(
+ entry = entry,
+ modifier = Modifier.padding(start = 16.dp),
+ )
+ },
+ )
+ }
+ }
+ }
+}
diff --git a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/SwatchGrid.kt b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/SwatchGrid.kt
similarity index 56%
rename from lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/SwatchGrid.kt
rename to lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/SwatchGrid.kt
index b33a511439..7b8e6a2646 100644
--- a/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/SwatchGrid.kt
+++ b/lawnchair/src/app/lawnchair/ui/preferences/components/colorpreference/pickers/SwatchGrid.kt
@@ -1,4 +1,4 @@
-package app.lawnchair.ui.preferences.components.colorpreference
+package app.lawnchair.ui.preferences.components.colorpreference.pickers
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.background
@@ -15,6 +15,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
+import app.lawnchair.ui.preferences.components.PreferenceGroup
+import app.lawnchair.ui.preferences.components.colorpreference.ColorPreferenceEntry
object SwatchGridDefaults {
val GutterSize = 12.dp
@@ -24,41 +26,47 @@ object SwatchGridDefaults {
@Composable
fun SwatchGrid(
+ modifier: Modifier = Modifier,
+ contentModifier: Modifier = Modifier,
entries: List>,
onSwatchClick: (T) -> Unit,
- modifier: Modifier = Modifier,
isSwatchSelected: (T) -> Boolean
) {
val columnCount = SwatchGridDefaults.ColumnCount
val rowCount = (entries.size - 1) / columnCount + 1
val gutter = SwatchGridDefaults.GutterSize
- Column(modifier = modifier) {
- for (rowNo in 1..rowCount) {
- val firstIndex = (rowNo - 1) * columnCount
- val lastIndex = firstIndex + columnCount - 1
- val indices = firstIndex..lastIndex
+ PreferenceGroup(
+ modifier = modifier,
+ showDividers = false,
+ ) {
+ Column(modifier = contentModifier) {
+ for (rowNo in 1..rowCount) {
+ val firstIndex = (rowNo - 1) * columnCount
+ val lastIndex = firstIndex + columnCount - 1
+ val indices = firstIndex..lastIndex
- Row {
- entries.slice(indices).forEachIndexed { index, colorOption ->
- Box(
- modifier = Modifier.weight(1f),
- contentAlignment = Alignment.Center
- ) {
- ColorSwatch(
- entry = colorOption,
- onClick = { onSwatchClick(colorOption.value) },
- modifier = Modifier.widthIn(0.dp, SwatchGridDefaults.SwatchMaxWidth),
- selected = isSwatchSelected(colorOption.value)
- )
- }
- if (index != columnCount - 1) {
- Spacer(modifier = Modifier.width(gutter))
+ Row {
+ entries.slice(indices).forEachIndexed { index, colorOption ->
+ Box(
+ modifier = Modifier.weight(1f),
+ contentAlignment = Alignment.Center,
+ ) {
+ ColorSwatch(
+ entry = colorOption,
+ onClick = { onSwatchClick(colorOption.value) },
+ modifier = Modifier.widthIn(0.dp, SwatchGridDefaults.SwatchMaxWidth),
+ selected = isSwatchSelected(colorOption.value),
+ )
+ }
+ if (index != columnCount - 1) {
+ Spacer(modifier = Modifier.width(gutter))
+ }
}
}
- }
- if (rowNo != rowCount) {
- Spacer(modifier = Modifier.height(gutter))
+ if (rowNo != rowCount) {
+ Spacer(modifier = Modifier.height(gutter))
+ }
}
}
}