diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSwatch.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSwatch.kt new file mode 100644 index 00000000..b07a89e1 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ColorSwatch.kt @@ -0,0 +1,66 @@ +package de.mm20.launcher2.ui.settings.colorscheme + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.CheckCircle +import androidx.compose.material3.Icon +import androidx.compose.runtime.Composable +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.graphics.toArgb +import androidx.compose.ui.unit.dp +import de.mm20.launcher2.ui.locals.LocalDarkTheme +import hct.Hct + +@Composable +fun ColorSwatch( + color: Color, + modifier: Modifier = Modifier, + selected: Boolean = false +) { + val darkTheme = LocalDarkTheme.current + val iconColor = Color(Hct.fromInt(color.toArgb()).let { + val tone = if (darkTheme) { + if (it.tone.toInt() > 40) 30f + else 60f + } else { + if (it.tone.toInt() < 60) 80f + else 40f + } + it.apply { + this.tone = tone.toDouble() + }.toInt() + }) + val borderColor = Color(Hct.fromInt(color.toArgb()).let { + val tone = if (darkTheme) 30f else 80f + it.apply { + this.tone = tone.toDouble() + }.toInt() + }) + Box( + modifier = modifier + .clip(CircleShape) + .border( + if (selected) 4.dp else 1.dp, + borderColor, + CircleShape + ) + .background(color), + contentAlignment = Alignment.Center + ) { + if (selected) { + Icon( + Icons.Rounded.CheckCircle, + null, + modifier = Modifier.size(32.dp), + tint = iconColor, + ) + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/CorePaletteColorPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/CorePaletteColorPreference.kt new file mode 100644 index 00000000..38fbcf74 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/CorePaletteColorPreference.kt @@ -0,0 +1,127 @@ +package de.mm20.launcher2.ui.settings.colorscheme + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.expandVertically +import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.AutoFixHigh +import androidx.compose.material.icons.rounded.SettingsSuggest +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.PlainTooltipBox +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.unit.dp +import de.mm20.launcher2.ui.component.BottomSheetDialog +import de.mm20.launcher2.ui.component.colorpicker.HctColorPicker +import de.mm20.launcher2.ui.component.colorpicker.rememberHctColorPickerState +import de.mm20.launcher2.ui.component.preferences.SwitchPreference + +@Composable +fun CorePaletteColorPreference( + title: String, + value: Int?, + onValueChange: (Int?) -> Unit, + defaultValue: Int, + modifier: Modifier = Modifier, + autoGenerate: (() -> Int?)? = null, +) { + var showDialog by remember { mutableStateOf(false) } + + PlainTooltipBox(tooltip = { Text(title) }) { + ColorSwatch( + color = Color(value ?: defaultValue), + modifier = modifier + .size(48.dp) + .tooltipTrigger() + .clickable { showDialog = true }, + ) + } + + if (showDialog) { + BottomSheetDialog(onDismissRequest = { showDialog = false }) { + Column( + modifier = Modifier.padding(it) + ) { + SwitchPreference( + icon = Icons.Rounded.SettingsSuggest, + title = "Use system default", + value = value == null, + onValueChanged = { + onValueChange(if (it) null else defaultValue) + } + ) + AnimatedVisibility( + value != null, + enter = expandVertically( + expandFrom = Alignment.Top, + ), + exit = shrinkVertically( + shrinkTowards = Alignment.Top, + ) + ) { + Column { + HorizontalDivider( + modifier = Modifier.padding(bottom = 24.dp) + ) + val colorPickerState = rememberHctColorPickerState( + initialColor = Color(value ?: defaultValue), + onColorChanged = { + onValueChange(it.toArgb()) + } + ) + HctColorPicker( + modifier = Modifier + .fillMaxWidth() + .align(Alignment.CenterHorizontally), + state = colorPickerState, + ) + + if (autoGenerate != null) { + HorizontalDivider( + modifier = Modifier.padding(top = 16.dp) + ) + + TextButton( + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.End), + contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, + onClick = { + val autoGenerated = autoGenerate() + onValueChange(autoGenerated) + if (autoGenerated != null) { + colorPickerState.setColor(Color(autoGenerated)) + } + } + ) { + Icon( + Icons.Rounded.AutoFixHigh, null, + modifier = Modifier + .padding(ButtonDefaults.IconSpacing) + .size(ButtonDefaults.IconSize) + ) + Text("From primary color") + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeColorPreference.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeColorPreference.kt new file mode 100644 index 00000000..7f8c6ec0 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeColorPreference.kt @@ -0,0 +1,427 @@ +package de.mm20.launcher2.ui.settings.colorscheme + +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.Box +import androidx.compose.foundation.layout.Column +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.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Colorize +import androidx.compose.material.icons.rounded.Palette +import androidx.compose.material.icons.rounded.RestartAlt +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.PlainTooltipBox +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Slider +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.draw.shadow +import androidx.compose.ui.geometry.CornerRadius +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Fill +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import de.mm20.launcher2.themes.Color as ThemeColor +import de.mm20.launcher2.themes.ColorRef +import de.mm20.launcher2.themes.CorePaletteColor +import de.mm20.launcher2.themes.FullCorePalette +import de.mm20.launcher2.themes.StaticColor +import de.mm20.launcher2.themes.atTone +import de.mm20.launcher2.themes.get +import de.mm20.launcher2.ui.component.BottomSheetDialog +import de.mm20.launcher2.ui.component.colorpicker.HctColorPicker +import de.mm20.launcher2.ui.component.colorpicker.rememberHctColorPickerState +import de.mm20.launcher2.ui.ktx.hct +import hct.Hct +import kotlin.math.roundToInt + +@Composable +fun ThemeColorPreference( + title: String, + value: de.mm20.launcher2.themes.Color?, + corePalette: FullCorePalette, + onValueChange: (ThemeColor?) -> Unit, + defaultValue: ThemeColor, + modifier: Modifier = Modifier, +) { + var showDialog by remember { mutableStateOf(false) } + + val actualValue = value ?: defaultValue + + PlainTooltipBox(tooltip = { Text(title) }) { + ColorSwatch( + color = Color(actualValue.get(corePalette)), + modifier = modifier + .size(48.dp) + .tooltipTrigger() + .clickable { showDialog = true }, + ) + } + + if (showDialog) { + BottomSheetDialog(onDismissRequest = { showDialog = false }) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(it), + ) { + SingleChoiceSegmentedButtonRow( + modifier = Modifier.fillMaxWidth() + ) { + SegmentedButton( + selected = actualValue is ColorRef, + onClick = { + if (actualValue is ColorRef) return@SegmentedButton + onValueChange(defaultValue) + }, + icon = { + SegmentedButtonDefaults.SegmentedButtonIcon( + active = actualValue is ColorRef, + ) { + Icon( + Icons.Rounded.Palette, + null, + modifier = Modifier + .size(SegmentedButtonDefaults.IconSize) + ) + } + }, + shape = SegmentedButtonDefaults.shape(position = 0, count = 2) + ) { + Text("From palette") + } + SegmentedButton( + selected = actualValue is StaticColor, + onClick = { + onValueChange(StaticColor(actualValue.get(corePalette))) + }, + icon = { + SegmentedButtonDefaults.SegmentedButtonIcon( + active = actualValue is StaticColor, + ) { + Icon( + Icons.Rounded.Colorize, + null, + modifier = Modifier + .size(SegmentedButtonDefaults.IconSize) + ) + } + }, + shape = SegmentedButtonDefaults.shape(position = 1, count = 2) + ) { + Text("Custom") + } + } + AnimatedContent( + actualValue, + label = "AnimatedContent", + contentKey = { it is StaticColor } + ) { themeColor -> + Column { + if (themeColor is StaticColor) { + val colorPickerState = rememberHctColorPickerState( + initialColor = Color(themeColor.color), + onColorChanged = { + onValueChange(StaticColor(it.toArgb())) + } + ) + HctColorPicker( + modifier = Modifier + .fillMaxWidth() + .padding(top = 24.dp) + .align(Alignment.CenterHorizontally), + state = colorPickerState + ) + } else if (themeColor is ColorRef) { + val hct = Hct.fromInt(corePalette.get(themeColor.color)) + val hue = hct.hue.toFloat() + val chroma = hct.chroma.toFloat() + var tone by remember(value == null) { mutableStateOf(themeColor.tone.toFloat()) } + Row( + modifier = Modifier.padding(top = 24.dp, bottom = 8.dp) + ) { + ColorSwatch( + color = Color( + corePalette + .get(CorePaletteColor.Primary) + .atTone(tone.toInt()) + ), + modifier = Modifier + .padding(8.dp) + .size(64.dp) + .clickable { + onValueChange( + ColorRef( + CorePaletteColor.Primary, + tone.roundToInt() + ) + ) + }, + selected = themeColor.color == CorePaletteColor.Primary, + ) + Spacer(modifier = Modifier.weight(1f)) + ColorSwatch( + color = Color( + corePalette + .get(CorePaletteColor.Secondary) + .atTone(tone.toInt()) + ), + modifier = Modifier + .padding(8.dp) + .size(64.dp) + .clickable { + onValueChange( + ColorRef( + CorePaletteColor.Secondary, + tone.roundToInt() + ) + ) + }, + selected = themeColor.color == CorePaletteColor.Secondary, + ) + Spacer(modifier = Modifier.weight(1f)) + ColorSwatch( + color = Color( + corePalette + .get(CorePaletteColor.Tertiary) + .atTone(tone.toInt()) + ), + modifier = Modifier + .padding(8.dp) + .size(64.dp) + .clickable { + onValueChange( + ColorRef( + CorePaletteColor.Tertiary, + tone.roundToInt() + ) + ) + }, + selected = themeColor.color == CorePaletteColor.Tertiary, + ) + } + Row( + modifier = Modifier.padding(bottom = 16.dp) + ) { + ColorSwatch( + color = Color( + corePalette + .get(CorePaletteColor.Neutral) + .atTone(tone.toInt()) + ), + modifier = Modifier + .padding(8.dp) + .size(64.dp) + .clickable { + onValueChange( + ColorRef( + CorePaletteColor.Neutral, + tone.roundToInt() + ) + ) + }, + selected = themeColor.color == CorePaletteColor.Neutral, + ) + Spacer(modifier = Modifier.weight(1f)) + ColorSwatch( + color = Color( + corePalette + .get(CorePaletteColor.NeutralVariant) + .atTone(tone.toInt()) + ), + modifier = Modifier + .padding(8.dp) + .size(64.dp) + .clickable { + onValueChange( + ColorRef( + CorePaletteColor.NeutralVariant, + tone.roundToInt() + ) + ) + }, + selected = themeColor.color == CorePaletteColor.NeutralVariant, + ) + Spacer(modifier = Modifier.weight(1f)) + ColorSwatch( + color = Color( + corePalette + .get(CorePaletteColor.Error) + .atTone(tone.toInt()) + ), + modifier = Modifier + .padding(8.dp) + .size(64.dp) + .clickable { + onValueChange( + ColorRef( + CorePaletteColor.Error, + tone.roundToInt() + ) + ) + }, + selected = themeColor.color == CorePaletteColor.Error, + ) + } + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp) + ) { + Text( + text = "C", + modifier = Modifier.width(32.dp), + style = MaterialTheme.typography.labelMedium, + textAlign = TextAlign.Center + ) + Slider( + modifier = Modifier.weight(1f), + value = tone, + valueRange = 0f..100f, + onValueChange = { + tone = it + onValueChange(themeColor.copy(tone = it.roundToInt())) + }, + track = { + Canvas( + modifier = Modifier + .fillMaxWidth() + .height(20.dp) + ) { + drawRoundRect( + brush = Brush.horizontalGradient( + colors = listOf( + androidx.compose.ui.graphics.Color.hct( + hue, + chroma, + 0f + ), + androidx.compose.ui.graphics.Color.hct( + hue, + chroma, + 10f + ), + androidx.compose.ui.graphics.Color.hct( + hue, + chroma, + 20f + ), + androidx.compose.ui.graphics.Color.hct( + hue, + chroma, + 30f + ), + androidx.compose.ui.graphics.Color.hct( + hue, + chroma, + 40f + ), + androidx.compose.ui.graphics.Color.hct( + hue, + chroma, + 50f + ), + androidx.compose.ui.graphics.Color.hct( + hue, + chroma, + 60f + ), + androidx.compose.ui.graphics.Color.hct( + hue, + chroma, + 70f + ), + androidx.compose.ui.graphics.Color.hct( + hue, + chroma, + 80f + ), + androidx.compose.ui.graphics.Color.hct( + hue, + chroma, + 90f + ), + androidx.compose.ui.graphics.Color.hct( + hue, + chroma, + 100f + ), + ) + ), + style = Fill, + cornerRadius = CornerRadius( + 10.dp.toPx(), + 10.dp.toPx() + ) + ) + } + }, + thumb = { + Box( + modifier = Modifier + .padding(vertical = 2.dp, horizontal = 8.dp) + .size(16.dp) + .shadow(1.dp, CircleShape) + .clip(CircleShape) + .background(androidx.compose.ui.graphics.Color.White) + ) + } + ) + Text( + text = tone.roundToInt().toString(), + modifier = Modifier.width(32.dp), + style = MaterialTheme.typography.labelMedium, + textAlign = TextAlign.Center, + ) + } + } + HorizontalDivider( + modifier = Modifier.padding(top = 16.dp) + ) + + TextButton( + modifier = Modifier + .padding(top = 8.dp) + .align(Alignment.End), + contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, + onClick = { onValueChange(null) } + ) { + Icon( + Icons.Rounded.RestartAlt, null, + modifier = Modifier + .padding(ButtonDefaults.IconSpacing) + .size(ButtonDefaults.IconSize) + ) + Text("Restore default") + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemePreferenceCategory.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemePreferenceCategory.kt new file mode 100644 index 00000000..f99cd8f3 --- /dev/null +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemePreferenceCategory.kt @@ -0,0 +1,105 @@ +package de.mm20.launcher2.ui.settings.colorscheme + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.FlowRowScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.DarkMode +import androidx.compose.material.icons.rounded.LightMode +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp + +@Composable +fun ThemePreferenceCategory( + title: String, + previewColorScheme: ColorScheme, + darkMode: Boolean, + onDarkModeChanged: (Boolean) -> Unit, + colorPreferences: @Composable () -> Unit = {}, + preview: @Composable FlowRowScope.() -> Unit +) { + Column( + modifier = Modifier.padding(top = 16.dp) + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + title, + style = MaterialTheme.typography.titleSmall, + color = MaterialTheme.colorScheme.secondary, + modifier = Modifier.weight(1f) + ) + SingleChoiceSegmentedButtonRow { + SegmentedButton( + shape = SegmentedButtonDefaults.shape(position = 0, count = 2), + selected = !darkMode, + onClick = { onDarkModeChanged(false) } + ) { + Icon(Icons.Rounded.LightMode, null) + } + SegmentedButton( + shape = SegmentedButtonDefaults.shape(position = 1, count = 2), + selected = darkMode, + onClick = { onDarkModeChanged(true) } + ) { + Icon(Icons.Rounded.DarkMode, null) + } + } + } + MaterialTheme( + colorScheme = previewColorScheme + ) { + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerLow + ) + ) { + + FlowRow( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.Center, + ) { + preview() + } + } + } + + Row( + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(16.dp) + ) { + colorPreferences() + } + HorizontalDivider() + } + +} \ No newline at end of file diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeSettingsScreen.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeSettingsScreen.kt index 13256067..0e44406a 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeSettingsScreen.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/settings/colorscheme/ThemeSettingsScreen.kt @@ -1,47 +1,22 @@ package de.mm20.launcher2.ui.settings.colorscheme -import androidx.compose.animation.AnimatedContent -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.expandVertically -import androidx.compose.animation.shrinkVertically -import androidx.compose.foundation.Canvas -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.FlowRow -import androidx.compose.foundation.layout.FlowRowScope import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -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.material.icons.Icons -import androidx.compose.material.icons.rounded.AutoFixHigh -import androidx.compose.material.icons.rounded.CheckCircle -import androidx.compose.material.icons.rounded.Colorize -import androidx.compose.material.icons.rounded.DarkMode import androidx.compose.material.icons.rounded.Edit -import androidx.compose.material.icons.rounded.LightMode -import androidx.compose.material.icons.rounded.Palette import androidx.compose.material.icons.rounded.PlayArrow -import androidx.compose.material.icons.rounded.RestartAlt import androidx.compose.material.icons.rounded.Search -import androidx.compose.material.icons.rounded.SettingsSuggest import androidx.compose.material.icons.rounded.Tag import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.ColorScheme import androidx.compose.material3.ElevatedCard import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.FilterChip @@ -53,11 +28,6 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedCard import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.PlainTooltipBox -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow -import androidx.compose.material3.Slider import androidx.compose.material3.Snackbar import androidx.compose.material3.Switch import androidx.compose.material3.Text @@ -71,16 +41,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.draw.shadow -import androidx.compose.ui.geometry.CornerRadius -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.drawscope.Fill -import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.core.content.ContextCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -88,32 +50,18 @@ import androidx.lifecycle.viewmodel.compose.viewModel import de.mm20.launcher2.icons.ColorLayer import de.mm20.launcher2.icons.StaticLauncherIcon import de.mm20.launcher2.icons.TintedIconLayer -import de.mm20.launcher2.themes.ColorRef -import de.mm20.launcher2.themes.CorePaletteColor import de.mm20.launcher2.themes.DefaultDarkColorScheme import de.mm20.launcher2.themes.DefaultLightColorScheme -import de.mm20.launcher2.themes.FullCorePalette -import de.mm20.launcher2.themes.StaticColor -import de.mm20.launcher2.themes.atTone -import de.mm20.launcher2.themes.get import de.mm20.launcher2.themes.merge import de.mm20.launcher2.ui.R -import de.mm20.launcher2.ui.component.BottomSheetDialog import de.mm20.launcher2.ui.component.ShapedLauncherIcon -import de.mm20.launcher2.ui.component.colorpicker.HctColorPicker -import de.mm20.launcher2.ui.component.colorpicker.rememberHctColorPickerState import de.mm20.launcher2.ui.component.preferences.PreferenceScreen -import de.mm20.launcher2.ui.component.preferences.SwitchPreference -import de.mm20.launcher2.ui.ktx.hct import de.mm20.launcher2.ui.locals.LocalDarkTheme import de.mm20.launcher2.ui.theme.colorscheme.darkColorSchemeOf import de.mm20.launcher2.ui.theme.colorscheme.lightColorSchemeOf import de.mm20.launcher2.ui.theme.colorscheme.systemCorePalette -import hct.Hct import palettes.CorePalette import java.util.UUID -import kotlin.math.roundToInt -import de.mm20.launcher2.themes.Color as ThemeColor @Composable fun ThemeSettingsScreen(themeId: UUID) { @@ -986,26 +934,26 @@ fun ThemeSettingsScreen(themeId: UUID) { ) ThemeColorPreference( title = "On Surface Variant", - value = selectedColorScheme.surfaceVariant, + value = selectedColorScheme.onSurfaceVariant, corePalette = mergedCorePalette, onValueChange = { viewModel.updateTheme( if (previewDarkTheme) { theme!!.copy( darkColorScheme = theme!!.darkColorScheme.copy( - surfaceVariant = it + onSurfaceVariant = it ) ) } else { theme!!.copy( lightColorScheme = theme!!.lightColorScheme.copy( - surfaceVariant = it + onSurfaceVariant = it ) ) } ) }, - defaultValue = selectedDefaultScheme.surfaceVariant, + defaultValue = selectedDefaultScheme.onSurfaceVariant, modifier = Modifier.padding(end = 12.dp), ) }, @@ -1098,7 +1046,7 @@ fun ThemeSettingsScreen(themeId: UUID) { .align(Alignment.CenterVertically) ) {} VerticalDivider( - modifier = Modifier.padding(end = 16.dp), + modifier = Modifier.height(64.dp).padding(end = 16.dp), ) } } @@ -1314,543 +1262,3 @@ fun ThemeSettingsScreen(themeId: UUID) { } } -@Composable -fun CorePaletteColorPreference( - title: String, - value: Int?, - onValueChange: (Int?) -> Unit, - defaultValue: Int, - modifier: Modifier = Modifier, - autoGenerate: (() -> Int?)? = null, -) { - var showDialog by remember { mutableStateOf(false) } - - PlainTooltipBox(tooltip = { Text(title) }) { - ColorSwatch( - color = Color(value ?: defaultValue), - modifier = modifier - .size(48.dp) - .tooltipTrigger() - .clickable { showDialog = true }, - ) - } - - if (showDialog) { - BottomSheetDialog(onDismissRequest = { showDialog = false }) { - Column( - modifier = Modifier.padding(it) - ) { - SwitchPreference( - icon = Icons.Rounded.SettingsSuggest, - title = "Use system default", - value = value == null, - onValueChanged = { - onValueChange(if (it) null else defaultValue) - } - ) - AnimatedVisibility( - value != null, - enter = expandVertically( - expandFrom = Alignment.Top, - ), - exit = shrinkVertically( - shrinkTowards = Alignment.Top, - ) - ) { - Column { - HorizontalDivider( - modifier = Modifier.padding(bottom = 24.dp) - ) - val colorPickerState = rememberHctColorPickerState( - initialColor = Color(value ?: defaultValue), - onColorChanged = { - onValueChange(it.toArgb()) - } - ) - HctColorPicker( - modifier = Modifier - .fillMaxWidth() - .align(Alignment.CenterHorizontally), - state = colorPickerState, - ) - - if (autoGenerate != null) { - HorizontalDivider( - modifier = Modifier.padding(top = 16.dp) - ) - - TextButton( - modifier = Modifier - .padding(top = 8.dp) - .align(Alignment.End), - contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, - onClick = { - val autoGenerated = autoGenerate() - onValueChange(autoGenerated) - if (autoGenerated != null) { - colorPickerState.setColor(Color(autoGenerated)) - } - } - ) { - Icon( - Icons.Rounded.AutoFixHigh, null, - modifier = Modifier - .padding(ButtonDefaults.IconSpacing) - .size(ButtonDefaults.IconSize) - ) - Text("From primary color") - } - } - } - } - } - } - } -} - -@Composable -fun ThemePreferenceCategory( - title: String, - previewColorScheme: ColorScheme, - darkMode: Boolean, - onDarkModeChanged: (Boolean) -> Unit, - colorPreferences: @Composable () -> Unit = {}, - preview: @Composable FlowRowScope.() -> Unit -) { - Column( - modifier = Modifier.padding(top = 16.dp) - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - title, - style = MaterialTheme.typography.titleSmall, - color = MaterialTheme.colorScheme.secondary, - modifier = Modifier.weight(1f) - ) - SingleChoiceSegmentedButtonRow { - SegmentedButton( - shape = SegmentedButtonDefaults.shape(position = 0, count = 2), - selected = !darkMode, - onClick = { onDarkModeChanged(false) } - ) { - Icon(Icons.Rounded.LightMode, null) - } - SegmentedButton( - shape = SegmentedButtonDefaults.shape(position = 1, count = 2), - selected = darkMode, - onClick = { onDarkModeChanged(true) } - ) { - Icon(Icons.Rounded.DarkMode, null) - } - } - } - MaterialTheme( - colorScheme = previewColorScheme - ) { - Card( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerLow - ) - ) { - - FlowRow( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalArrangement = Arrangement.Center, - ) { - preview() - } - } - } - - Row( - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()) - .padding(16.dp) - ) { - colorPreferences() - } - HorizontalDivider() - } - -} - -@Composable -fun ThemeColorPreference( - title: String, - value: ThemeColor?, - corePalette: FullCorePalette, - onValueChange: (ThemeColor?) -> Unit, - defaultValue: ThemeColor, - modifier: Modifier = Modifier, -) { - var showDialog by remember { mutableStateOf(false) } - - val actualValue = value ?: defaultValue - - PlainTooltipBox(tooltip = { Text(title) }) { - ColorSwatch( - color = Color(actualValue.get(corePalette)), - modifier = modifier - .size(48.dp) - .tooltipTrigger() - .clickable { showDialog = true }, - ) - } - - if (showDialog) { - BottomSheetDialog(onDismissRequest = { showDialog = false }) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(it), - ) { - SingleChoiceSegmentedButtonRow( - modifier = Modifier.fillMaxWidth() - ) { - SegmentedButton( - selected = actualValue is ColorRef, - onClick = { - if (actualValue is ColorRef) return@SegmentedButton - onValueChange(defaultValue) - }, - icon = { - SegmentedButtonDefaults.SegmentedButtonIcon( - active = actualValue is ColorRef, - ) { - Icon( - Icons.Rounded.Palette, - null, - modifier = Modifier - .size(SegmentedButtonDefaults.IconSize) - ) - } - }, - shape = SegmentedButtonDefaults.shape(position = 0, count = 2) - ) { - Text("From palette") - } - SegmentedButton( - selected = actualValue is StaticColor, - onClick = { - onValueChange(StaticColor(actualValue.get(corePalette))) - }, - icon = { - SegmentedButtonDefaults.SegmentedButtonIcon( - active = actualValue is StaticColor, - ) { - Icon( - Icons.Rounded.Colorize, - null, - modifier = Modifier - .size(SegmentedButtonDefaults.IconSize) - ) - } - }, - shape = SegmentedButtonDefaults.shape(position = 1, count = 2) - ) { - Text("Custom") - } - } - AnimatedContent( - actualValue, - label = "AnimatedContent", - contentKey = { it is StaticColor } - ) { themeColor -> - Column { - if (themeColor is StaticColor) { - val colorPickerState = rememberHctColorPickerState( - initialColor = Color(themeColor.color), - onColorChanged = { - onValueChange(StaticColor(it.toArgb())) - } - ) - HctColorPicker( - modifier = Modifier - .fillMaxWidth() - .padding(top = 24.dp) - .align(Alignment.CenterHorizontally), - state = colorPickerState - ) - } else if (themeColor is ColorRef) { - val hct = Hct.fromInt(corePalette.get(themeColor.color)) - val hue = hct.hue.toFloat() - val chroma = hct.chroma.toFloat() - var tone by remember(value == null) { mutableStateOf(themeColor.tone.toFloat()) } - Row( - modifier = Modifier.padding(top = 24.dp, bottom = 8.dp) - ) { - ColorSwatch( - color = Color( - corePalette - .get(CorePaletteColor.Primary) - .atTone(tone.toInt()) - ), - modifier = Modifier - .padding(8.dp) - .size(64.dp) - .clickable { - onValueChange( - ColorRef( - CorePaletteColor.Primary, - tone.roundToInt() - ) - ) - }, - selected = themeColor.color == CorePaletteColor.Primary, - ) - Spacer(modifier = Modifier.weight(1f)) - ColorSwatch( - color = Color( - corePalette - .get(CorePaletteColor.Secondary) - .atTone(tone.toInt()) - ), - modifier = Modifier - .padding(8.dp) - .size(64.dp) - .clickable { - onValueChange( - ColorRef( - CorePaletteColor.Secondary, - tone.roundToInt() - ) - ) - }, - selected = themeColor.color == CorePaletteColor.Secondary, - ) - Spacer(modifier = Modifier.weight(1f)) - ColorSwatch( - color = Color( - corePalette - .get(CorePaletteColor.Tertiary) - .atTone(tone.toInt()) - ), - modifier = Modifier - .padding(8.dp) - .size(64.dp) - .clickable { - onValueChange( - ColorRef( - CorePaletteColor.Tertiary, - tone.roundToInt() - ) - ) - }, - selected = themeColor.color == CorePaletteColor.Tertiary, - ) - } - Row( - modifier = Modifier.padding(bottom = 16.dp) - ) { - ColorSwatch( - color = Color( - corePalette - .get(CorePaletteColor.Neutral) - .atTone(tone.toInt()) - ), - modifier = Modifier - .padding(8.dp) - .size(64.dp) - .clickable { - onValueChange( - ColorRef( - CorePaletteColor.Neutral, - tone.roundToInt() - ) - ) - }, - selected = themeColor.color == CorePaletteColor.Neutral, - ) - Spacer(modifier = Modifier.weight(1f)) - ColorSwatch( - color = Color( - corePalette - .get(CorePaletteColor.NeutralVariant) - .atTone(tone.toInt()) - ), - modifier = Modifier - .padding(8.dp) - .size(64.dp) - .clickable { - onValueChange( - ColorRef( - CorePaletteColor.NeutralVariant, - tone.roundToInt() - ) - ) - }, - selected = themeColor.color == CorePaletteColor.NeutralVariant, - ) - Spacer(modifier = Modifier.weight(1f)) - ColorSwatch( - color = Color( - corePalette - .get(CorePaletteColor.Error) - .atTone(tone.toInt()) - ), - modifier = Modifier - .padding(8.dp) - .size(64.dp) - .clickable { - onValueChange( - ColorRef( - CorePaletteColor.Error, - tone.roundToInt() - ) - ) - }, - selected = themeColor.color == CorePaletteColor.Error, - ) - } - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp) - ) { - Text( - text = "C", - modifier = Modifier.width(32.dp), - style = MaterialTheme.typography.labelMedium, - textAlign = TextAlign.Center - ) - Slider( - modifier = Modifier.weight(1f), - value = tone, - valueRange = 0f..100f, - onValueChange = { - tone = it - onValueChange(themeColor.copy(tone = it.roundToInt())) - }, - track = { - Canvas( - modifier = Modifier - .fillMaxWidth() - .height(20.dp) - ) { - drawRoundRect( - brush = Brush.horizontalGradient( - colors = listOf( - Color.hct(hue, chroma, 0f), - Color.hct(hue, chroma, 10f), - Color.hct(hue, chroma, 20f), - Color.hct(hue, chroma, 30f), - Color.hct(hue, chroma, 40f), - Color.hct(hue, chroma, 50f), - Color.hct(hue, chroma, 60f), - Color.hct(hue, chroma, 70f), - Color.hct(hue, chroma, 80f), - Color.hct(hue, chroma, 90f), - Color.hct(hue, chroma, 100f), - ) - ), - style = Fill, - cornerRadius = CornerRadius( - 10.dp.toPx(), - 10.dp.toPx() - ) - ) - } - }, - thumb = { - Box( - modifier = Modifier - .padding(vertical = 2.dp, horizontal = 8.dp) - .size(16.dp) - .shadow(1.dp, CircleShape) - .clip(CircleShape) - .background(Color.White) - ) - } - ) - Text( - text = tone.roundToInt().toString(), - modifier = Modifier.width(32.dp), - style = MaterialTheme.typography.labelMedium, - textAlign = TextAlign.Center, - ) - } - } - HorizontalDivider( - modifier = Modifier.padding(top = 16.dp) - ) - - TextButton( - modifier = Modifier - .padding(top = 8.dp) - .align(Alignment.End), - contentPadding = ButtonDefaults.TextButtonWithIconContentPadding, - onClick = { onValueChange(null) } - ) { - Icon( - Icons.Rounded.RestartAlt, null, - modifier = Modifier - .padding(ButtonDefaults.IconSpacing) - .size(ButtonDefaults.IconSize) - ) - Text("Restore default") - } - } - } - } - } - } -} - -@Composable -fun ColorSwatch( - color: Color, - modifier: Modifier = Modifier, - selected: Boolean = false -) { - val darkTheme = LocalDarkTheme.current - val iconColor = Color(Hct.fromInt(color.toArgb()).let { - val tone = if (darkTheme) { - if (it.tone.toInt() > 40) 30f - else 60f - } else { - if (it.tone.toInt() < 60) 80f - else 40f - } - it.apply { - this.tone = tone.toDouble() - }.toInt() - }) - val borderColor = Color(Hct.fromInt(color.toArgb()).let { - val tone = if (darkTheme) 30f else 80f - it.apply { - this.tone = tone.toDouble() - }.toInt() - }) - Box( - modifier = modifier - .clip(CircleShape) - .border( - if (selected) 4.dp else 1.dp, - borderColor, - CircleShape - ) - .background(color), - contentAlignment = Alignment.Center - ) { - if (selected) { - Icon( - Icons.Rounded.CheckCircle, - null, - modifier = Modifier.size(32.dp), - tint = iconColor, - ) - } - } -} \ No newline at end of file