From 2825d71507b629fbc645bef04f22deb24d49313f Mon Sep 17 00:00:00 2001 From: Infi Date: Mon, 5 Aug 2024 00:36:44 +0200 Subject: [PATCH] feat: graduate built-in colour picker to GA Signed-off-by: Infi --- app/build.gradle | 1 - .../chat/revolt/api/settings/FeatureFlags.kt | 32 -- .../revolt/internals/TailwindColourScheme.kt | 318 +++++++++++ .../settings/AppearanceSettingsScreen.kt | 178 +----- .../chat/revolt/sheets/ColourPickerSheet.kt | 520 +++++++++++++----- app/src/main/res/values/strings.xml | 7 +- 6 files changed, 726 insertions(+), 330 deletions(-) create mode 100644 app/src/main/java/chat/revolt/internals/TailwindColourScheme.kt diff --git a/app/build.gradle b/app/build.gradle index bdb006f6..8685b396 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -262,7 +262,6 @@ dependencies { implementation "androidx.media3:media3-ui:$media3_version" // Compose libraries - implementation "com.github.skydoves:colorpicker-compose:1.0.5" implementation "me.saket.telephoto:zoomable-image:1.0.0-alpha02" implementation "me.saket.telephoto:zoomable-image-glide:1.0.0-alpha02" diff --git a/app/src/main/java/chat/revolt/api/settings/FeatureFlags.kt b/app/src/main/java/chat/revolt/api/settings/FeatureFlags.kt index 4545c836..da014bcb 100644 --- a/app/src/main/java/chat/revolt/api/settings/FeatureFlags.kt +++ b/app/src/main/java/chat/revolt/api/settings/FeatureFlags.kt @@ -17,24 +17,6 @@ sealed class LabsAccessControlVariates { data class Restricted(val predicate: () -> Boolean) : LabsAccessControlVariates() } -@FeatureFlag("BuiltInColourPicker") -sealed class BuiltInColourPickerVariates { - @Treatment( - "Use the built-in colour picker" - ) - object Enabled : BuiltInColourPickerVariates() - - @Treatment( - "Use the built-in colour picker for users that meet certain or all criteria (implementation-specific)" - ) - data class Restricted(val predicate: () -> Boolean) : BuiltInColourPickerVariates() - - @Treatment( - "Use the colour picker from the external library" - ) - object Disabled : BuiltInColourPickerVariates() -} - @FeatureFlag("MediaConversations") sealed class MediaConversationsVariates { @Treatment( @@ -61,20 +43,6 @@ object FeatureFlags { is LabsAccessControlVariates.Restricted -> (labsAccessControl as LabsAccessControlVariates.Restricted).predicate() } - @FeatureFlag("BuiltInColourPicker") - var builtInColourPicker by mutableStateOf( - BuiltInColourPickerVariates.Restricted { - RevoltAPI.selfId == SpecialUsers.JENNIFER - } - ) - - val builtInColourPickerGranted: Boolean - get() = when (builtInColourPicker) { - is BuiltInColourPickerVariates.Enabled -> true - is BuiltInColourPickerVariates.Restricted -> (builtInColourPicker as BuiltInColourPickerVariates.Restricted).predicate() - is BuiltInColourPickerVariates.Disabled -> false - } - @FeatureFlag("MediaConversations") var mediaConversations by mutableStateOf( MediaConversationsVariates.Restricted { diff --git a/app/src/main/java/chat/revolt/internals/TailwindColourScheme.kt b/app/src/main/java/chat/revolt/internals/TailwindColourScheme.kt new file mode 100644 index 00000000..741e23ef --- /dev/null +++ b/app/src/main/java/chat/revolt/internals/TailwindColourScheme.kt @@ -0,0 +1,318 @@ +package chat.revolt.internals + +import androidx.compose.ui.graphics.Color + +// Auto-generated, do not touch +// Generator: https://gist.github.com/infi/0aea10320069f0ca71d0c49cde5ad13c + +object TailwindColourScheme { + val slate = listOf( + 0xFFF8FAFC, + 0xFFF1F5F9, + 0xFFE2E8F0, + 0xFFCBD5E1, + 0xFF94A3B8, + 0xFF64748B, + 0xFF475569, + 0xFF334155, + 0xFF1E293B, + 0xFF0F172A, + 0xFF020617, + ).map { Color(it) } + + val gray = listOf( + 0xFFF9FAFB, + 0xFFF3F4F6, + 0xFFE5E7EB, + 0xFFD1D5DB, + 0xFF9CA3AF, + 0xFF6B7280, + 0xFF4B5563, + 0xFF374151, + 0xFF1F2937, + 0xFF111827, + 0xFF030712, + ).map { Color(it) } + + val zinc = listOf( + 0xFFFAFAFA, + 0xFFF4F4F5, + 0xFFE4E4E7, + 0xFFD4D4D8, + 0xFFA1A1AA, + 0xFF71717A, + 0xFF52525B, + 0xFF3F3F46, + 0xFF27272A, + 0xFF18181B, + 0xFF09090B, + ).map { Color(it) } + + val neutral = listOf( + 0xFFFAFAFA, + 0xFFF5F5F5, + 0xFFE5E5E5, + 0xFFD4D4D4, + 0xFFA3A3A3, + 0xFF737373, + 0xFF525252, + 0xFF404040, + 0xFF262626, + 0xFF171717, + 0xFF0A0A0A, + ).map { Color(it) } + + val stone = listOf( + 0xFFFAFAF9, + 0xFFF5F5F4, + 0xFFE7E5E4, + 0xFFD6D3D1, + 0xFFA8A29E, + 0xFF78716C, + 0xFF57534E, + 0xFF44403C, + 0xFF292524, + 0xFF1C1917, + 0xFF0C0A09, + ).map { Color(it) } + + val red = listOf( + 0xFFFEF2F2, + 0xFFFEE2E2, + 0xFFFECACA, + 0xFFFCA5A5, + 0xFFF87171, + 0xFFEF4444, + 0xFFDC2626, + 0xFFB91C1C, + 0xFF991B1B, + 0xFF7F1D1D, + 0xFF450A0A, + ).map { Color(it) } + + val orange = listOf( + 0xFFFFF7ED, + 0xFFFFEDD5, + 0xFFFED7AA, + 0xFFFDBA74, + 0xFFFB923C, + 0xFFF97316, + 0xFFEA580C, + 0xFFC2410C, + 0xFF9A3412, + 0xFF7C2D12, + 0xFF431407, + ).map { Color(it) } + + val amber = listOf( + 0xFFFFFBEB, + 0xFFFEF3C7, + 0xFFFDE68A, + 0xFFFCD34D, + 0xFFFBBF24, + 0xFFF59E0B, + 0xFFD97706, + 0xFFB45309, + 0xFF92400E, + 0xFF78350F, + 0xFF451A03, + ).map { Color(it) } + + val yellow = listOf( + 0xFFFEFCE8, + 0xFFFEF9C3, + 0xFFFEF08A, + 0xFFFDE047, + 0xFFFACC15, + 0xFFEAB308, + 0xFFCA8A04, + 0xFFA16207, + 0xFF854D0E, + 0xFF713F12, + 0xFF422006, + ).map { Color(it) } + + val lime = listOf( + 0xFFF7FEE7, + 0xFFECFCCB, + 0xFFD9F99D, + 0xFFBEF264, + 0xFFA3E635, + 0xFF84CC16, + 0xFF65A30D, + 0xFF4D7C0F, + 0xFF3F6212, + 0xFF365314, + 0xFF1A2E05, + ).map { Color(it) } + + val green = listOf( + 0xFFF0FDF4, + 0xFFDCFCE7, + 0xFFBBF7D0, + 0xFF86EFAC, + 0xFF4ADE80, + 0xFF22C55E, + 0xFF16A34A, + 0xFF15803D, + 0xFF166534, + 0xFF14532D, + 0xFF052E16, + ).map { Color(it) } + + val emerald = listOf( + 0xFFECFDF5, + 0xFFD1FAE5, + 0xFFA7F3D0, + 0xFF6EE7B7, + 0xFF34D399, + 0xFF10B981, + 0xFF059669, + 0xFF047857, + 0xFF065F46, + 0xFF064E3B, + 0xFF022C22, + ).map { Color(it) } + + val teal = listOf( + 0xFFF0FDFA, + 0xFFCCFBF1, + 0xFF99F6E4, + 0xFF5EEAD4, + 0xFF2DD4BF, + 0xFF14B8A6, + 0xFF0D9488, + 0xFF0F766E, + 0xFF115E59, + 0xFF134E4A, + 0xFF042F2E, + ).map { Color(it) } + + val cyan = listOf( + 0xFFECFEFF, + 0xFFCFFAFE, + 0xFFA5F3FC, + 0xFF67E8F9, + 0xFF22D3EE, + 0xFF06B6D4, + 0xFF0891B2, + 0xFF0E7490, + 0xFF155E75, + 0xFF164E63, + 0xFF083344, + ).map { Color(it) } + + val sky = listOf( + 0xFFF0F9FF, + 0xFFE0F2FE, + 0xFFBAE6FD, + 0xFF7DD3FC, + 0xFF38BDF8, + 0xFF0EA5E9, + 0xFF0284C7, + 0xFF0369A1, + 0xFF075985, + 0xFF0C4A6E, + 0xFF082F49, + ).map { Color(it) } + + val blue = listOf( + 0xFFEFF6FF, + 0xFFDBEAFE, + 0xFFBFDBFE, + 0xFF93C5FD, + 0xFF60A5FA, + 0xFF3B82F6, + 0xFF2563EB, + 0xFF1D4ED8, + 0xFF1E40AF, + 0xFF1E3A8A, + 0xFF172554, + ).map { Color(it) } + + val indigo = listOf( + 0xFFEEF2FF, + 0xFFE0E7FF, + 0xFFC7D2FE, + 0xFFA5B4FC, + 0xFF818CF8, + 0xFF6366F1, + 0xFF4F46E5, + 0xFF4338CA, + 0xFF3730A3, + 0xFF312E81, + 0xFF1E1B4B, + ).map { Color(it) } + + val violet = listOf( + 0xFFF5F3FF, + 0xFFEDE9FE, + 0xFFDDD6FE, + 0xFFC4B5FD, + 0xFFA78BFA, + 0xFF8B5CF6, + 0xFF7C3AED, + 0xFF6D28D9, + 0xFF5B21B6, + 0xFF4C1D95, + 0xFF2E1065, + ).map { Color(it) } + + val purple = listOf( + 0xFFFAF5FF, + 0xFFF3E8FF, + 0xFFE9D5FF, + 0xFFD8B4FE, + 0xFFC084FC, + 0xFFA855F7, + 0xFF9333EA, + 0xFF7E22CE, + 0xFF6B21A8, + 0xFF581C87, + 0xFF3B0764, + ).map { Color(it) } + + val fuchsia = listOf( + 0xFFFDF4FF, + 0xFFFAE8FF, + 0xFFF5D0FE, + 0xFFF0ABFC, + 0xFFE879F9, + 0xFFD946EF, + 0xFFC026D3, + 0xFFA21CAF, + 0xFF86198F, + 0xFF701A75, + 0xFF4A044E, + ).map { Color(it) } + + val pink = listOf( + 0xFFFDF2F8, + 0xFFFCE7F3, + 0xFFFBCFE8, + 0xFFF9A8D4, + 0xFFF472B6, + 0xFFEC4899, + 0xFFDB2777, + 0xFFBE185D, + 0xFF9D174D, + 0xFF831843, + 0xFF500724, + ).map { Color(it) } + + val rose = listOf( + 0xFFFFF1F2, + 0xFFFFE4E6, + 0xFFFECDD3, + 0xFFFDA4AF, + 0xFFFB7185, + 0xFFF43F5E, + 0xFFE11D48, + 0xFFBE123C, + 0xFF9F1239, + 0xFF881337, + 0xFF4C0519, + ).map { Color(it) } + +} + diff --git a/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt index 90aa9e38..3e9a0b33 100644 --- a/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt +++ b/app/src/main/java/chat/revolt/screens/settings/AppearanceSettingsScreen.kt @@ -26,11 +26,6 @@ import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight -import androidx.compose.material.icons.filled.Check -import androidx.compose.material.icons.filled.Close -import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.ripple.LocalRippleTheme -import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -44,17 +39,14 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection @@ -71,25 +63,17 @@ import androidx.navigation.NavController import chat.revolt.R import chat.revolt.api.RevoltCbor import chat.revolt.api.RevoltJson -import chat.revolt.api.settings.FeatureFlags import chat.revolt.api.settings.GlobalState import chat.revolt.api.settings.SyncedSettings import chat.revolt.components.generic.ListHeader -import chat.revolt.components.generic.SheetEnd import chat.revolt.components.screens.settings.appearance.ColourChip import chat.revolt.components.screens.settings.appearance.CornerRadiusPicker import chat.revolt.internals.extensions.BottomSheetInsets import chat.revolt.sheets.ColourPickerSheet -import chat.revolt.ui.theme.ClearRippleTheme import chat.revolt.ui.theme.OverridableColourScheme import chat.revolt.ui.theme.Theme import chat.revolt.ui.theme.getFieldByName import chat.revolt.ui.theme.systemSupportsDynamicColors -import com.github.skydoves.colorpicker.compose.AlphaSlider -import com.github.skydoves.colorpicker.compose.BrightnessSlider -import com.github.skydoves.colorpicker.compose.ColorEnvelope -import com.github.skydoves.colorpicker.compose.HsvColorPicker -import com.github.skydoves.colorpicker.compose.rememberColorPickerController import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.launch @@ -259,8 +243,9 @@ fun AppearanceSettingsScreen( }, windowInsets = BottomSheetInsets ) { - if (FeatureFlags.builtInColourPickerGranted) { - ColourPickerSheet(initialValue = viewModel.selectedOverrideInitialValue ?: 0) { + ColourPickerSheet( + initialValue = viewModel.selectedOverrideInitialValue ?: 0, + onColourSelected = { viewModel.updateColourOverrides( viewModel.selectedOverrideName ?: return@ColourPickerSheet, it @@ -269,28 +254,24 @@ fun AppearanceSettingsScreen( sheetState.hide() viewModel.overridePickerSheetVisible = false } - } - } else { - ColourSelectorSheet( - initialValue = Color(viewModel.selectedOverrideInitialValue ?: 0), - onConfirm = { color -> - viewModel.updateColourOverrides( - viewModel.selectedOverrideName ?: return@ColourSelectorSheet, - color?.toArgb() - ) - scope.launch { - sheetState.hide() - viewModel.overridePickerSheetVisible = false - } - }, - onDismiss = { - scope.launch { - sheetState.hide() - viewModel.overridePickerSheetVisible = false - } + }, + onUseDefaultColour = { + viewModel.updateColourOverrides( + viewModel.selectedOverrideName ?: return@ColourPickerSheet, + null + ) + scope.launch { + sheetState.hide() + viewModel.overridePickerSheetVisible = false } - ) - } + }, + onDismiss = { + scope.launch { + sheetState.hide() + viewModel.overridePickerSheetVisible = false + } + } + ) } } @@ -535,123 +516,4 @@ fun AppearanceSettingsScreen( } } } -} - -@Composable -fun ColourSelectorSheet( - initialValue: Color, - onConfirm: (Color?) -> Unit, - onDismiss: () -> Unit -) { - val controller = rememberColorPickerController() - val colour = remember { mutableStateOf(initialValue) } - - Column( - modifier = Modifier - .padding(20.dp) - .verticalScroll(rememberScrollState()) - ) { - HsvColorPicker( - modifier = Modifier - .fillMaxWidth() - .height(450.dp) - .padding(10.dp), - controller = controller, - onColorChanged = { colorEnvelope: ColorEnvelope -> - colour.value = colorEnvelope.color - }, - ) - - AlphaSlider( - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - controller = controller, - ) - - BrightnessSlider( - modifier = Modifier - .fillMaxWidth() - .height(50.dp), - controller = controller, - ) - - CompositionLocalProvider( - LocalRippleTheme provides ClearRippleTheme - ) { - ColourChip( - color = colour.value, - text = "#${ - (0xFFFFFF and colour.value.toArgb()).toString(16).padStart(6, '0').uppercase() - }", - modifier = Modifier - .padding(10.dp) - .fillMaxWidth() - ) {} - } - - Column( - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - TextButton( - onClick = { - onConfirm(null) - }, - modifier = Modifier.fillMaxWidth() - ) { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = null - ) - - Spacer(modifier = Modifier.width(8.dp)) - - Text( - text = stringResource(id = R.string.settings_appearance_colour_overrides_reset) - ) - } - - Row( - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - TextButton( - onClick = { - onDismiss() - }, - modifier = Modifier.weight(1f) - ) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = null - ) - - Spacer(modifier = Modifier.width(8.dp)) - - Text( - text = stringResource(id = R.string.cancel) - ) - } - - Button( - onClick = { - onConfirm(colour.value) - }, - enabled = colour.value != initialValue, - modifier = Modifier.weight(1f) - ) { - Icon( - imageVector = Icons.Default.Check, - contentDescription = null - ) - - Spacer(modifier = Modifier.width(8.dp)) - - Text( - text = stringResource(id = R.string.settings_appearance_colour_overrides_apply) - ) - } - } - } - } - SheetEnd() } \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/sheets/ColourPickerSheet.kt b/app/src/main/java/chat/revolt/sheets/ColourPickerSheet.kt index 337601fa..0aa7be23 100644 --- a/app/src/main/java/chat/revolt/sheets/ColourPickerSheet.kt +++ b/app/src/main/java/chat/revolt/sheets/ColourPickerSheet.kt @@ -1,20 +1,35 @@ package chat.revolt.sheets +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.ExperimentalLayoutApi +import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Check +import androidx.compose.material.icons.filled.CheckCircle +import androidx.compose.material.icons.filled.Close +import androidx.compose.material3.Button import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SegmentedButton @@ -22,21 +37,26 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Slider import androidx.compose.material3.SliderDefaults import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.onFocusChanged import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import chat.revolt.R import chat.revolt.components.generic.SheetEnd +import chat.revolt.internals.TailwindColourScheme enum class ColourPickerMode { Sliders, @@ -44,42 +64,187 @@ enum class ColourPickerMode { Hex } -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) -> Unit) { - var selectedColour by remember { mutableIntStateOf(initialValue and 0xFFFFFF) } +private fun Color.asHsv(): Triple { + val max = maxOf(red, green, blue) + val min = minOf(red, green, blue) + val delta = max - min + val hue: Float = if (max == min) { + 0f + } else { + when (max) { + red -> (60 * ((green - blue) / delta) + 360) % 360 + green -> (60 * ((blue - red) / delta) + 120) % 360 + blue -> (60 * ((red - green) / delta) + 240) % 360 + else -> throw IllegalStateException("Unexpected max value: $max") + } + } + val saturation: Float = if (max == 0f) 0f else delta / max + + return Triple(hue, saturation, max) +} + +private fun Color.asHexString(): String { + val alpha = (alpha * 255).toInt() + val red = (red * 255).toInt() + val green = (green * 255).toInt() + val blue = (blue * 255).toInt() + return String.format("#%02X%02X%02X%02X", alpha, red, green, blue) +} + +// 11x11 palette of Tailwind colours +val palette = listOf( + TailwindColourScheme.neutral[0], + TailwindColourScheme.red[0], + TailwindColourScheme.red[1], + TailwindColourScheme.red[2], + TailwindColourScheme.red[3], + TailwindColourScheme.red[4], + TailwindColourScheme.red[5], + TailwindColourScheme.red[6], + TailwindColourScheme.red[7], + TailwindColourScheme.red[9], + TailwindColourScheme.red[10], + + TailwindColourScheme.neutral[1], + TailwindColourScheme.orange[0], + TailwindColourScheme.orange[1], + TailwindColourScheme.orange[2], + TailwindColourScheme.orange[3], + TailwindColourScheme.orange[4], + TailwindColourScheme.orange[5], + TailwindColourScheme.orange[6], + TailwindColourScheme.orange[7], + TailwindColourScheme.orange[9], + TailwindColourScheme.orange[10], + + TailwindColourScheme.neutral[2], + TailwindColourScheme.yellow[0], + TailwindColourScheme.yellow[1], + TailwindColourScheme.yellow[2], + TailwindColourScheme.yellow[3], + TailwindColourScheme.yellow[4], + TailwindColourScheme.yellow[5], + TailwindColourScheme.yellow[6], + TailwindColourScheme.yellow[7], + TailwindColourScheme.yellow[9], + TailwindColourScheme.yellow[10], + + TailwindColourScheme.neutral[3], + TailwindColourScheme.green[0], + TailwindColourScheme.green[1], + TailwindColourScheme.green[2], + TailwindColourScheme.green[3], + TailwindColourScheme.green[4], + TailwindColourScheme.green[5], + TailwindColourScheme.green[6], + TailwindColourScheme.green[7], + TailwindColourScheme.green[9], + TailwindColourScheme.green[10], + + TailwindColourScheme.neutral[4], + TailwindColourScheme.teal[0], + TailwindColourScheme.teal[1], + TailwindColourScheme.teal[2], + TailwindColourScheme.teal[3], + TailwindColourScheme.teal[4], + TailwindColourScheme.teal[5], + TailwindColourScheme.teal[6], + TailwindColourScheme.teal[7], + TailwindColourScheme.teal[9], + TailwindColourScheme.teal[10], + + TailwindColourScheme.neutral[5], + TailwindColourScheme.sky[0], + TailwindColourScheme.sky[1], + TailwindColourScheme.sky[2], + TailwindColourScheme.sky[3], + TailwindColourScheme.sky[4], + TailwindColourScheme.sky[5], + TailwindColourScheme.sky[6], + TailwindColourScheme.sky[7], + TailwindColourScheme.sky[9], + TailwindColourScheme.sky[10], + + TailwindColourScheme.neutral[6], + TailwindColourScheme.blue[0], + TailwindColourScheme.blue[1], + TailwindColourScheme.blue[2], + TailwindColourScheme.blue[3], + TailwindColourScheme.blue[4], + TailwindColourScheme.blue[5], + TailwindColourScheme.blue[6], + TailwindColourScheme.blue[7], + TailwindColourScheme.blue[9], + TailwindColourScheme.blue[10], + + TailwindColourScheme.neutral[7], + TailwindColourScheme.violet[0], + TailwindColourScheme.violet[1], + TailwindColourScheme.violet[2], + TailwindColourScheme.violet[3], + TailwindColourScheme.violet[4], + TailwindColourScheme.violet[5], + TailwindColourScheme.violet[6], + TailwindColourScheme.violet[7], + TailwindColourScheme.violet[9], + TailwindColourScheme.violet[10], + + TailwindColourScheme.neutral[8], + TailwindColourScheme.purple[0], + TailwindColourScheme.purple[1], + TailwindColourScheme.purple[2], + TailwindColourScheme.purple[3], + TailwindColourScheme.purple[4], + TailwindColourScheme.purple[5], + TailwindColourScheme.purple[6], + TailwindColourScheme.purple[7], + TailwindColourScheme.purple[9], + TailwindColourScheme.purple[10], + + TailwindColourScheme.neutral[9], + TailwindColourScheme.pink[0], + TailwindColourScheme.pink[1], + TailwindColourScheme.pink[2], + TailwindColourScheme.pink[3], + TailwindColourScheme.pink[4], + TailwindColourScheme.pink[5], + TailwindColourScheme.pink[6], + TailwindColourScheme.pink[7], + TailwindColourScheme.pink[9], + TailwindColourScheme.pink[10], + + TailwindColourScheme.neutral[10], + TailwindColourScheme.rose[0], + TailwindColourScheme.rose[1], + TailwindColourScheme.rose[2], + TailwindColourScheme.rose[3], + TailwindColourScheme.rose[4], + TailwindColourScheme.rose[5], + TailwindColourScheme.rose[6], + TailwindColourScheme.rose[7], + TailwindColourScheme.rose[9], + TailwindColourScheme.rose[10], +) + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class) +@Composable +fun ColumnScope.ColourPickerSheet( + initialValue: Int, + onColourSelected: (Int) -> Unit, + onUseDefaultColour: () -> Unit, + onDismiss: () -> Unit +) { + var color by remember { mutableStateOf(Color(initialValue)) } var mode by remember { mutableStateOf(ColourPickerMode.Sliders) } - val hueComponent by remember(selectedColour) { derivedStateOf { selectedColour shr 16 and 0xFF } } - val saturationComponent by remember(selectedColour) { derivedStateOf { selectedColour shr 8 and 0xFF } } - val valueComponent by remember(selectedColour) { derivedStateOf { selectedColour and 0xFF } } - val hueTrackColours = remember { - (0..255).map { - Color( - android.graphics.Color.HSVToColor( - floatArrayOf( - it.toFloat(), - 1f, - 1f - ) - ) - ) + (0..359).map { + Color.hsv(it.toFloat(), 1f, 1f, 1f) } } - var pendingHexColourString by remember(selectedColour) { - // First we convert the colour from HHSSVV to #RRGGBB. - val asRgb = android.graphics.Color.HSVToColor( - floatArrayOf( - hueComponent.toFloat(), - saturationComponent / 255f, - valueComponent / 255f - ) - ) - mutableStateOf("#${asRgb.toString(16).padStart(6, '0')}") - } + val colorHsv by remember(color) { derivedStateOf { color.asHsv() } } Column( Modifier @@ -132,7 +297,10 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) -> style = MaterialTheme.typography.labelLarge ) Text( - hueComponent.toString(), + stringResource( + R.string.colour_picker_hue_value_fmt, + colorHsv.first.toInt() + ), style = MaterialTheme.typography.labelLarge.copy( color = MaterialTheme.colorScheme.onSurface.copy( alpha = 0.6f @@ -142,22 +310,17 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) -> } Slider( - value = hueComponent.toFloat(), + value = colorHsv.first, onValueChange = { - selectedColour = - (selectedColour and 0x00FFFF) or (it.toInt() shl 16) + val (_, saturation, value) = colorHsv + color = Color.hsv(it, saturation, value) }, - valueRange = 0f..255f, + valueRange = 0f..359f, colors = SliderDefaults.colors().copy( - // The thumb colour is the current hue at full saturation and value. - thumbColor = Color( - android.graphics.Color.HSVToColor( - floatArrayOf( - hueComponent.toFloat(), - 1f, - 1f - ) - ) + thumbColor = Color.hsv( + colorHsv.first, + 1f, + 1f ) ), track = { @@ -167,7 +330,10 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) -> .height(4.dp) ) { drawRect( - Brush.horizontalGradient(hueTrackColours, endX = size.width) + Brush.horizontalGradient( + hueTrackColours, + endX = size.width + ) ) } } @@ -182,7 +348,7 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) -> style = MaterialTheme.typography.labelLarge ) Text( - saturationComponent.toString(), + (colorHsv.second * 100).toInt().toString(), style = MaterialTheme.typography.labelLarge.copy( color = MaterialTheme.colorScheme.onSurface.copy( alpha = 0.6f @@ -192,21 +358,17 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) -> } Slider( - value = saturationComponent.toFloat(), + value = colorHsv.second, onValueChange = { - selectedColour = - (selectedColour and 0xFF00FF) or (it.toInt() shl 8) + val (hue, _, value) = colorHsv + color = Color.hsv(hue, it, value) }, - valueRange = 0f..255f, + valueRange = 0f..1f, colors = SliderDefaults.colors().copy( - thumbColor = Color( - android.graphics.Color.HSVToColor( - floatArrayOf( - hueComponent.toFloat(), - (selectedColour shr 8 and 0xFF) / 255f, - 1f - ) - ) + thumbColor = Color.hsv( + colorHsv.first, + colorHsv.second, + 1f ) ), track = { @@ -218,23 +380,15 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) -> drawRect( Brush.horizontalGradient( listOf( - Color( - android.graphics.Color.HSVToColor( - floatArrayOf( - hueComponent.toFloat(), - 0f, - 1f - ) - ) + Color.hsv( + colorHsv.first, + 0f, + 1f ), - Color( - android.graphics.Color.HSVToColor( - floatArrayOf( - hueComponent.toFloat(), - 1f, - 1f - ) - ) + Color.hsv( + colorHsv.first, + 1f, + 1f ) ), endX = size.width @@ -253,7 +407,7 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) -> style = MaterialTheme.typography.labelLarge ) Text( - valueComponent.toString(), + (colorHsv.third * 100).toInt().toString(), style = MaterialTheme.typography.labelLarge.copy( color = MaterialTheme.colorScheme.onSurface.copy( alpha = 0.6f @@ -263,21 +417,17 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) -> } Slider( - value = valueComponent.toFloat(), + value = colorHsv.third, onValueChange = { - selectedColour = - (selectedColour and 0xFFFF00) or it.toInt() + val (hue, saturation, _) = colorHsv + color = Color.hsv(hue, saturation, it) }, - valueRange = 0f..255f, + valueRange = 0f..1f, colors = SliderDefaults.colors().copy( - thumbColor = Color( - android.graphics.Color.HSVToColor( - floatArrayOf( - hueComponent.toFloat(), - (selectedColour shr 8 and 0xFF) / 255f, - (selectedColour and 0xFF) / 255f - ) - ) + thumbColor = Color.hsv( + colorHsv.first, + colorHsv.second, + colorHsv.third ) ), track = { @@ -289,23 +439,15 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) -> drawRect( Brush.horizontalGradient( listOf( - Color( - android.graphics.Color.HSVToColor( - floatArrayOf( - hueComponent.toFloat(), - (selectedColour shr 8 and 0xFF) / 255f, - 0f - ) - ) + Color.hsv( + colorHsv.first, + colorHsv.second, + 0f ), - Color( - android.graphics.Color.HSVToColor( - floatArrayOf( - hueComponent.toFloat(), - (selectedColour shr 8 and 0xFF) / 255f, - 1f - ) - ) + Color.hsv( + colorHsv.first, + colorHsv.second, + 1f ) ), endX = size.width @@ -314,36 +456,120 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) -> } } ) + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier.fillMaxWidth() + ) { + Text( + stringResource(R.string.colour_picker_alpha), + style = MaterialTheme.typography.labelLarge + ) + Text( + (color.alpha * 100).toInt().toString(), + style = MaterialTheme.typography.labelLarge.copy( + color = MaterialTheme.colorScheme.onSurface.copy( + alpha = 0.6f + ) + ) + ) + } + + Slider( + value = color.alpha, + onValueChange = { color = color.copy(alpha = it) }, + valueRange = 0f..1f, + colors = SliderDefaults.colors().copy( + thumbColor = color + ), + track = { + Canvas( + Modifier + .fillMaxWidth() + .height(4.dp) + ) { + drawRect( + Brush.horizontalGradient( + listOf( + color.copy(alpha = 0f), + color.copy(alpha = 1f) + ), + endX = size.width + ) + ) + } + } + ) } } ColourPickerMode.Palette -> { - Text("TODO: Palette picker", Modifier.fillMaxWidth()) + BoxWithConstraints { + val boxMaxWidth = this.maxWidth + + FlowRow( + maxItemsInEachRow = 11, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + for (colour in palette) { + Box( + Modifier + .clip(CircleShape) + .clickable { color = colour } + .size((boxMaxWidth - 80.dp) / 11) + .background(colour) + ) + } + } + } } ColourPickerMode.Hex -> { - OutlinedTextField( - value = pendingHexColourString, - onValueChange = { - pendingHexColourString = it + var hex by remember(color) { + mutableStateOf(color.asHexString()) + } + var isFocused by remember { mutableStateOf(false) } + val focusManager = LocalFocusManager.current - if ("#[0-9a-fA-F]{6}".toRegex().matches(it)) { - val newColour = - it.substring(1).toIntOrNull(16) ?: return@OutlinedTextField - val floatArr = FloatArray(3) - android.graphics.Color.RGBToHSV( - newColour shr 16 and 0xFF, - newColour shr 8 and 0xFF, - newColour and 0xFF, - floatArr - ) - selectedColour = floatArr.fold(0) { acc, f -> - (acc shl 8) or (f.toInt() and 0xFF) + BackHandler(enabled = isFocused) { + focusManager.clearFocus() + } + + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + OutlinedTextField( + value = hex, + onValueChange = { + hex = if (it.isNotEmpty() && it[0] == '#') { + it + } else { + "#$it" } - } - }, - modifier = Modifier.fillMaxWidth() - ) + }, + label = { Text(stringResource(R.string.colour_picker_hex_template)) }, + modifier = Modifier + .fillMaxWidth() + .onFocusChanged { + isFocused = it.isFocused + }, + ) + + TextButton( + onClick = { + try { + color = Color(android.graphics.Color.parseColor(hex)) + } catch (e: IllegalArgumentException) { + // Ignore + } + }, + enabled = (hex.length == 9 || hex.length == 7) && hex[0] == '#' && hex.substring( + 1 + ).all { it in '0'..'9' || it in 'a'..'f' || it in 'A'..'F' }, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.colour_picker_hex_use)) + } + } } } } @@ -357,21 +583,43 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) -> Canvas( Modifier + .clip(MaterialTheme.shapes.large) .fillMaxWidth() .height(64.dp) ) { - drawRect( - Color( - android.graphics.Color.HSVToColor( - floatArrayOf( - hueComponent.toFloat(), - saturationComponent / 255f, - valueComponent / 255f - ) - ) - ), - size = size - ) + drawRect(color) + } + + Spacer(Modifier.height(8.dp)) + + Column( + Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + TextButton(onClick = onUseDefaultColour, Modifier.fillMaxWidth()) { + Icon(Icons.Default.CheckCircle, null) + Spacer(Modifier.width(8.dp)) + Text(stringResource(R.string.colour_picker_use_default)) + } + + Row( + Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + TextButton(onClick = onDismiss, modifier = Modifier.weight(1f)) { + Icon(Icons.Default.Close, null) + Spacer(Modifier.width(8.dp)) + Text(stringResource(R.string.colour_picker_cancel)) + } + Button( + onClick = { onColourSelected(color.toArgb()) }, + modifier = Modifier.weight(1f) + ) { + Icon(Icons.Default.Check, null) + Spacer(Modifier.width(8.dp)) + Text(stringResource(R.string.colour_picker_apply)) + } + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 968d6f50..00c14626 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -456,15 +456,18 @@ Sliders Palette Hue + %1$d° Saturation Value Red Green Blue Hex + #AARRGGBB + Use Alpha Preview - Reset to default + Default Cancel Apply @@ -581,8 +584,6 @@ Outline Outline Variant Scrim - Apply - Reset Export Import This file is not a valid colour override file.