feat: graduate built-in colour picker to GA

Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
Infi 2024-08-05 00:36:44 +02:00
parent 2116c75fc1
commit 2825d71507
6 changed files with 726 additions and 330 deletions

View File

@ -262,7 +262,6 @@ dependencies {
implementation "androidx.media3:media3-ui:$media3_version" implementation "androidx.media3:media3-ui:$media3_version"
// Compose libraries // 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:1.0.0-alpha02"
implementation "me.saket.telephoto:zoomable-image-glide:1.0.0-alpha02" implementation "me.saket.telephoto:zoomable-image-glide:1.0.0-alpha02"

View File

@ -17,24 +17,6 @@ sealed class LabsAccessControlVariates {
data class Restricted(val predicate: () -> Boolean) : 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") @FeatureFlag("MediaConversations")
sealed class MediaConversationsVariates { sealed class MediaConversationsVariates {
@Treatment( @Treatment(
@ -61,20 +43,6 @@ object FeatureFlags {
is LabsAccessControlVariates.Restricted -> (labsAccessControl as LabsAccessControlVariates.Restricted).predicate() is LabsAccessControlVariates.Restricted -> (labsAccessControl as LabsAccessControlVariates.Restricted).predicate()
} }
@FeatureFlag("BuiltInColourPicker")
var builtInColourPicker by mutableStateOf<BuiltInColourPickerVariates>(
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") @FeatureFlag("MediaConversations")
var mediaConversations by mutableStateOf<MediaConversationsVariates>( var mediaConversations by mutableStateOf<MediaConversationsVariates>(
MediaConversationsVariates.Restricted { MediaConversationsVariates.Restricted {

View File

@ -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) }
}

View File

@ -26,11 +26,6 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight 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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
@ -44,17 +39,14 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
@ -71,25 +63,17 @@ import androidx.navigation.NavController
import chat.revolt.R import chat.revolt.R
import chat.revolt.api.RevoltCbor import chat.revolt.api.RevoltCbor
import chat.revolt.api.RevoltJson import chat.revolt.api.RevoltJson
import chat.revolt.api.settings.FeatureFlags
import chat.revolt.api.settings.GlobalState import chat.revolt.api.settings.GlobalState
import chat.revolt.api.settings.SyncedSettings import chat.revolt.api.settings.SyncedSettings
import chat.revolt.components.generic.ListHeader 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.ColourChip
import chat.revolt.components.screens.settings.appearance.CornerRadiusPicker import chat.revolt.components.screens.settings.appearance.CornerRadiusPicker
import chat.revolt.internals.extensions.BottomSheetInsets import chat.revolt.internals.extensions.BottomSheetInsets
import chat.revolt.sheets.ColourPickerSheet import chat.revolt.sheets.ColourPickerSheet
import chat.revolt.ui.theme.ClearRippleTheme
import chat.revolt.ui.theme.OverridableColourScheme import chat.revolt.ui.theme.OverridableColourScheme
import chat.revolt.ui.theme.Theme import chat.revolt.ui.theme.Theme
import chat.revolt.ui.theme.getFieldByName import chat.revolt.ui.theme.getFieldByName
import chat.revolt.ui.theme.systemSupportsDynamicColors 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.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -259,8 +243,9 @@ fun AppearanceSettingsScreen(
}, },
windowInsets = BottomSheetInsets windowInsets = BottomSheetInsets
) { ) {
if (FeatureFlags.builtInColourPickerGranted) { ColourPickerSheet(
ColourPickerSheet(initialValue = viewModel.selectedOverrideInitialValue ?: 0) { initialValue = viewModel.selectedOverrideInitialValue ?: 0,
onColourSelected = {
viewModel.updateColourOverrides( viewModel.updateColourOverrides(
viewModel.selectedOverrideName ?: return@ColourPickerSheet, viewModel.selectedOverrideName ?: return@ColourPickerSheet,
it it
@ -269,28 +254,24 @@ fun AppearanceSettingsScreen(
sheetState.hide() sheetState.hide()
viewModel.overridePickerSheetVisible = false viewModel.overridePickerSheetVisible = false
} }
} },
} else { onUseDefaultColour = {
ColourSelectorSheet( viewModel.updateColourOverrides(
initialValue = Color(viewModel.selectedOverrideInitialValue ?: 0), viewModel.selectedOverrideName ?: return@ColourPickerSheet,
onConfirm = { color -> null
viewModel.updateColourOverrides( )
viewModel.selectedOverrideName ?: return@ColourSelectorSheet, scope.launch {
color?.toArgb() sheetState.hide()
) viewModel.overridePickerSheetVisible = false
scope.launch {
sheetState.hide()
viewModel.overridePickerSheetVisible = false
}
},
onDismiss = {
scope.launch {
sheetState.hide()
viewModel.overridePickerSheetVisible = false
}
} }
) },
} onDismiss = {
scope.launch {
sheetState.hide()
viewModel.overridePickerSheetVisible = false
}
}
)
} }
} }
@ -536,122 +517,3 @@ 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()
}

View File

@ -1,20 +1,35 @@
package chat.revolt.sheets package chat.revolt.sheets
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.Canvas 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.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope 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.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.rememberScrollState
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.verticalScroll 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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButton
@ -22,21 +37,26 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier 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.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import chat.revolt.R import chat.revolt.R
import chat.revolt.components.generic.SheetEnd import chat.revolt.components.generic.SheetEnd
import chat.revolt.internals.TailwindColourScheme
enum class ColourPickerMode { enum class ColourPickerMode {
Sliders, Sliders,
@ -44,42 +64,187 @@ enum class ColourPickerMode {
Hex Hex
} }
@OptIn(ExperimentalMaterial3Api::class) private fun Color.asHsv(): Triple<Float, Float, Float> {
@Composable val max = maxOf(red, green, blue)
fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) -> Unit) { val min = minOf(red, green, blue)
var selectedColour by remember { mutableIntStateOf(initialValue and 0xFFFFFF) } 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) } 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 { val hueTrackColours = remember {
(0..255).map { (0..359).map {
Color( Color.hsv(it.toFloat(), 1f, 1f, 1f)
android.graphics.Color.HSVToColor(
floatArrayOf(
it.toFloat(),
1f,
1f
)
)
)
} }
} }
var pendingHexColourString by remember(selectedColour) { val colorHsv by remember(color) { derivedStateOf { color.asHsv() } }
// 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')}")
}
Column( Column(
Modifier Modifier
@ -132,7 +297,10 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
style = MaterialTheme.typography.labelLarge style = MaterialTheme.typography.labelLarge
) )
Text( Text(
hueComponent.toString(), stringResource(
R.string.colour_picker_hue_value_fmt,
colorHsv.first.toInt()
),
style = MaterialTheme.typography.labelLarge.copy( style = MaterialTheme.typography.labelLarge.copy(
color = MaterialTheme.colorScheme.onSurface.copy( color = MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.6f alpha = 0.6f
@ -142,22 +310,17 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
} }
Slider( Slider(
value = hueComponent.toFloat(), value = colorHsv.first,
onValueChange = { onValueChange = {
selectedColour = val (_, saturation, value) = colorHsv
(selectedColour and 0x00FFFF) or (it.toInt() shl 16) color = Color.hsv(it, saturation, value)
}, },
valueRange = 0f..255f, valueRange = 0f..359f,
colors = SliderDefaults.colors().copy( colors = SliderDefaults.colors().copy(
// The thumb colour is the current hue at full saturation and value. thumbColor = Color.hsv(
thumbColor = Color( colorHsv.first,
android.graphics.Color.HSVToColor( 1f,
floatArrayOf( 1f
hueComponent.toFloat(),
1f,
1f
)
)
) )
), ),
track = { track = {
@ -167,7 +330,10 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
.height(4.dp) .height(4.dp)
) { ) {
drawRect( 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 style = MaterialTheme.typography.labelLarge
) )
Text( Text(
saturationComponent.toString(), (colorHsv.second * 100).toInt().toString(),
style = MaterialTheme.typography.labelLarge.copy( style = MaterialTheme.typography.labelLarge.copy(
color = MaterialTheme.colorScheme.onSurface.copy( color = MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.6f alpha = 0.6f
@ -192,21 +358,17 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
} }
Slider( Slider(
value = saturationComponent.toFloat(), value = colorHsv.second,
onValueChange = { onValueChange = {
selectedColour = val (hue, _, value) = colorHsv
(selectedColour and 0xFF00FF) or (it.toInt() shl 8) color = Color.hsv(hue, it, value)
}, },
valueRange = 0f..255f, valueRange = 0f..1f,
colors = SliderDefaults.colors().copy( colors = SliderDefaults.colors().copy(
thumbColor = Color( thumbColor = Color.hsv(
android.graphics.Color.HSVToColor( colorHsv.first,
floatArrayOf( colorHsv.second,
hueComponent.toFloat(), 1f
(selectedColour shr 8 and 0xFF) / 255f,
1f
)
)
) )
), ),
track = { track = {
@ -218,23 +380,15 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
drawRect( drawRect(
Brush.horizontalGradient( Brush.horizontalGradient(
listOf( listOf(
Color( Color.hsv(
android.graphics.Color.HSVToColor( colorHsv.first,
floatArrayOf( 0f,
hueComponent.toFloat(), 1f
0f,
1f
)
)
), ),
Color( Color.hsv(
android.graphics.Color.HSVToColor( colorHsv.first,
floatArrayOf( 1f,
hueComponent.toFloat(), 1f
1f,
1f
)
)
) )
), ),
endX = size.width endX = size.width
@ -253,7 +407,7 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
style = MaterialTheme.typography.labelLarge style = MaterialTheme.typography.labelLarge
) )
Text( Text(
valueComponent.toString(), (colorHsv.third * 100).toInt().toString(),
style = MaterialTheme.typography.labelLarge.copy( style = MaterialTheme.typography.labelLarge.copy(
color = MaterialTheme.colorScheme.onSurface.copy( color = MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.6f alpha = 0.6f
@ -263,21 +417,17 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
} }
Slider( Slider(
value = valueComponent.toFloat(), value = colorHsv.third,
onValueChange = { onValueChange = {
selectedColour = val (hue, saturation, _) = colorHsv
(selectedColour and 0xFFFF00) or it.toInt() color = Color.hsv(hue, saturation, it)
}, },
valueRange = 0f..255f, valueRange = 0f..1f,
colors = SliderDefaults.colors().copy( colors = SliderDefaults.colors().copy(
thumbColor = Color( thumbColor = Color.hsv(
android.graphics.Color.HSVToColor( colorHsv.first,
floatArrayOf( colorHsv.second,
hueComponent.toFloat(), colorHsv.third
(selectedColour shr 8 and 0xFF) / 255f,
(selectedColour and 0xFF) / 255f
)
)
) )
), ),
track = { track = {
@ -289,23 +439,15 @@ fun ColumnScope.ColourPickerSheet(initialValue: Int, onColourSelected: (Int) ->
drawRect( drawRect(
Brush.horizontalGradient( Brush.horizontalGradient(
listOf( listOf(
Color( Color.hsv(
android.graphics.Color.HSVToColor( colorHsv.first,
floatArrayOf( colorHsv.second,
hueComponent.toFloat(), 0f
(selectedColour shr 8 and 0xFF) / 255f,
0f
)
)
), ),
Color( Color.hsv(
android.graphics.Color.HSVToColor( colorHsv.first,
floatArrayOf( colorHsv.second,
hueComponent.toFloat(), 1f
(selectedColour shr 8 and 0xFF) / 255f,
1f
)
)
) )
), ),
endX = size.width 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 -> { 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 -> { ColourPickerMode.Hex -> {
OutlinedTextField( var hex by remember(color) {
value = pendingHexColourString, mutableStateOf(color.asHexString())
onValueChange = { }
pendingHexColourString = it var isFocused by remember { mutableStateOf(false) }
val focusManager = LocalFocusManager.current
if ("#[0-9a-fA-F]{6}".toRegex().matches(it)) { BackHandler(enabled = isFocused) {
val newColour = focusManager.clearFocus()
it.substring(1).toIntOrNull(16) ?: return@OutlinedTextField }
val floatArr = FloatArray(3)
android.graphics.Color.RGBToHSV( Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
newColour shr 16 and 0xFF, OutlinedTextField(
newColour shr 8 and 0xFF, value = hex,
newColour and 0xFF, onValueChange = {
floatArr hex = if (it.isNotEmpty() && it[0] == '#') {
) it
selectedColour = floatArr.fold(0) { acc, f -> } else {
(acc shl 8) or (f.toInt() and 0xFF) "#$it"
} }
} },
}, label = { Text(stringResource(R.string.colour_picker_hex_template)) },
modifier = Modifier.fillMaxWidth() 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( Canvas(
Modifier Modifier
.clip(MaterialTheme.shapes.large)
.fillMaxWidth() .fillMaxWidth()
.height(64.dp) .height(64.dp)
) { ) {
drawRect( drawRect(color)
Color( }
android.graphics.Color.HSVToColor(
floatArrayOf( Spacer(Modifier.height(8.dp))
hueComponent.toFloat(),
saturationComponent / 255f, Column(
valueComponent / 255f Modifier.fillMaxWidth(),
) verticalArrangement = Arrangement.spacedBy(4.dp)
) ) {
), TextButton(onClick = onUseDefaultColour, Modifier.fillMaxWidth()) {
size = size 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))
}
}
} }
} }

View File

@ -456,15 +456,18 @@
<string name="colour_picker_mode_sliders">Sliders</string> <string name="colour_picker_mode_sliders">Sliders</string>
<string name="colour_picker_mode_palette">Palette</string> <string name="colour_picker_mode_palette">Palette</string>
<string name="colour_picker_hue">Hue</string> <string name="colour_picker_hue">Hue</string>
<string name="colour_picker_hue_value_fmt">%1$d°</string>
<string name="colour_picker_saturation">Saturation</string> <string name="colour_picker_saturation">Saturation</string>
<string name="colour_picker_value">Value</string> <string name="colour_picker_value">Value</string>
<string name="colour_picker_red">Red</string> <string name="colour_picker_red">Red</string>
<string name="colour_picker_green">Green</string> <string name="colour_picker_green">Green</string>
<string name="colour_picker_blue">Blue</string> <string name="colour_picker_blue">Blue</string>
<string name="colour_picker_hex">Hex</string> <string name="colour_picker_hex">Hex</string>
<string name="colour_picker_hex_template" translatable="false">#AARRGGBB</string>
<string name="colour_picker_hex_use">Use</string>
<string name="colour_picker_alpha">Alpha</string> <string name="colour_picker_alpha">Alpha</string>
<string name="colour_picker_preview">Preview</string> <string name="colour_picker_preview">Preview</string>
<string name="colour_picker_reset_to_default">Reset to default</string> <string name="colour_picker_use_default">Default</string>
<string name="colour_picker_cancel">Cancel</string> <string name="colour_picker_cancel">Cancel</string>
<string name="colour_picker_apply">Apply</string> <string name="colour_picker_apply">Apply</string>
@ -581,8 +584,6 @@
<string name="settings_appearance_colour_overrides_outline">Outline</string> <string name="settings_appearance_colour_overrides_outline">Outline</string>
<string name="settings_appearance_colour_overrides_outline_variant">Outline Variant</string> <string name="settings_appearance_colour_overrides_outline_variant">Outline Variant</string>
<string name="settings_appearance_colour_overrides_scrim">Scrim</string> <string name="settings_appearance_colour_overrides_scrim">Scrim</string>
<string name="settings_appearance_colour_overrides_apply">Apply</string>
<string name="settings_appearance_colour_overrides_reset">Reset</string>
<string name="settings_appearance_colour_overrides_export">Export</string> <string name="settings_appearance_colour_overrides_export">Export</string>
<string name="settings_appearance_colour_overrides_import">Import</string> <string name="settings_appearance_colour_overrides_import">Import</string>
<string name="settings_appearance_colour_overrides_import_error">This file is not a valid colour override file.</string> <string name="settings_appearance_colour_overrides_import_error">This file is not a valid colour override file.</string>