diff --git a/src/main/kotlin/androidx/compose/material/ExposedDropDownMenu.kt b/src/main/kotlin/androidx/compose/material/ExposedDropDownMenu.kt new file mode 100644 index 0000000000000000000000000000000000000000..92450c6cfb65c85c90b92b8508824b12e4ca0d8d --- /dev/null +++ b/src/main/kotlin/androidx/compose/material/ExposedDropDownMenu.kt @@ -0,0 +1,226 @@ +/* + * Briar Desktop + * Copyright (C) 2021-2022 The Briar Project + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +package androidx.compose.material + +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.ZeroCornerSize +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowDropDown +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.drawBehind +import androidx.compose.ui.draw.rotate +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.toSize +import org.briarproject.briar.desktop.utils.PreviewUtils.preview + +// ExposedDropDownMenu is not yet available in JetBrains Compose +// This is a reimplementation while waiting. +// Taken from https://stackoverflow.com/a/69042850 and slightly adapted to follow Material theming +// See https://github.com/JetBrains/compose-jb/issues/1673 as tracking issue + +fun main() = preview { + val values = (0..5).map { "Test $it" } + var selected by remember { mutableStateOf(-1) } + + ExposedDropDownMenu( + values = values, + selectedIndex = selected, + onChange = { selected = it }, + ) + + OutlinedExposedDropDownMenu( + values = values, + selectedIndex = selected, + onChange = { selected = it }, + ) +} + +@Composable +fun ExposedDropDownMenu( + values: List<String>, + selectedIndex: Int, + onChange: (Int) -> Unit, + label: @Composable () -> Unit = {}, + modifier: Modifier = Modifier, + backgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.BackgroundOpacity), + shape: Shape = MaterialTheme.shapes.small.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize) +) { + ExposedDropDownMenuImpl( + values = values, + selectedIndex = selectedIndex, + onChange = onChange, + label = label, + modifier = modifier, + backgroundColor = backgroundColor, + shape = shape, + decorator = { color, width, content -> + Box( + Modifier + .drawBehind { + val strokeWidth = width.value * density + val y = size.height - strokeWidth / 2 + drawLine( + color, + Offset(0f, y), + Offset(size.width, y), + strokeWidth + ) + } + ) { + content() + } + } + ) +} + +@Composable +fun OutlinedExposedDropDownMenu( + values: List<String>, + selectedIndex: Int, + onChange: (Int) -> Unit, + label: @Composable () -> Unit = {}, + modifier: Modifier = Modifier, + backgroundColor: Color = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.BackgroundOpacity), + shape: Shape = MaterialTheme.shapes.small +) { + ExposedDropDownMenuImpl( + values = values, + selectedIndex = selectedIndex, + onChange = onChange, + label = label, + modifier = modifier, + backgroundColor = backgroundColor, + shape = shape, + decorator = { color, width, content -> + Box( + Modifier + .border(width, color, shape) + ) { + content() + } + } + ) +} + +@Composable +private fun ExposedDropDownMenuImpl( + values: List<String>, + selectedIndex: Int, + onChange: (Int) -> Unit, + label: @Composable () -> Unit, + modifier: Modifier, + backgroundColor: Color, + shape: Shape, + decorator: @Composable (Color, Dp, @Composable () -> Unit) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + var boxSize by remember { mutableStateOf(Size.Zero) } + + val indicatorColor = + if (expanded) MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high) + else MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.UnfocusedIndicatorLineOpacity) + val indicatorWidth = if (expanded) 2.dp else 1.dp + val labelColor = + if (expanded) MaterialTheme.colors.primary.copy(alpha = ContentAlpha.high) + else MaterialTheme.colors.onSurface.copy(alpha = ContentAlpha.medium) + val trailingIconColor = MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.IconOpacity) + + val rotation: Float by animateFloatAsState(if (expanded) 180f else 0f) + + val focusManager = LocalFocusManager.current + + Box(modifier = modifier.width(IntrinsicSize.Min)) { + decorator(indicatorColor, indicatorWidth) { + Box( + Modifier + .fillMaxWidth() + .background(color = backgroundColor, shape = shape) + .onGloballyPositioned { boxSize = it.size.toSize() } + .clip(shape) + .clickable { + expanded = !expanded + focusManager.clearFocus() + } + .padding(start = 16.dp, end = 12.dp, top = 7.dp, bottom = 10.dp) + ) { + Column(Modifier.padding(end = 32.dp).align(Alignment.CenterStart).width(IntrinsicSize.Max)) { + ProvideTextStyle(value = MaterialTheme.typography.caption.copy(color = labelColor)) { + label() + } + if (selectedIndex >= 0 && selectedIndex <= values.size) { + Text( + text = values[selectedIndex], + modifier = Modifier.padding(top = 1.dp) + ) + } + } + Icon( + imageVector = Icons.Filled.ArrowDropDown, + contentDescription = "Change", + tint = trailingIconColor, + modifier = Modifier + .align(Alignment.CenterEnd) + .padding(top = 4.dp) + .rotate(rotation) + ) + } + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + modifier = Modifier + .width(with(LocalDensity.current) { boxSize.width.toDp() }) + ) { + values.forEachIndexed { i, v -> + DropdownMenuItem( + onClick = { + onChange(i) + expanded = false + } + ) { + Text(v) + } + } + } + } +} diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingDetails.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingDetails.kt index 658177089dc844ed69f3d592e938fe5ec45c013d..83f75609aa95e1e0789b3c6fb62a06d425f483dd 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingDetails.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingDetails.kt @@ -27,9 +27,10 @@ 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.widthIn import androidx.compose.material.MaterialTheme +import androidx.compose.material.OutlinedExposedDropDownMenu import androidx.compose.material.Surface -import androidx.compose.material.Switch import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -49,9 +50,14 @@ fun SettingDetails(viewModel: SettingsViewModel) { // TODO: Change this to `settings.display.title` once more categories are implemented SettingDetail(i18n("settings.title")) { DetailItem { - Text(i18n("settings.display.theme")) - val isDarkMode = viewModel.isDarkMode.value - Switch(checked = isDarkMode, onCheckedChange = { viewModel.toggleTheme() }) + Text(i18n("settings.display.theme.title")) + + OutlinedExposedDropDownMenu( + values = viewModel.themesList.map { i18n("settings.display.theme.${it.name.lowercase()}") }, + selectedIndex = viewModel.selectedTheme.value.ordinal, + onChange = { viewModel.selectTheme(viewModel.themesList[it]) }, + modifier = Modifier.widthIn(min = 200.dp) + ) } } } diff --git a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt index 3c3aec2b22b10f5bb049fb2c76a815b279bc25f7..529f95039be0f3a7c9fcbd35f96e320bc4de0869 100644 --- a/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt +++ b/src/main/kotlin/org/briarproject/briar/desktop/settings/SettingsViewModel.kt @@ -19,8 +19,6 @@ package org.briarproject.briar.desktop.settings import androidx.compose.runtime.mutableStateOf -import org.briarproject.briar.desktop.settings.Settings.Theme.DARK -import org.briarproject.briar.desktop.settings.Settings.Theme.LIGHT import org.briarproject.briar.desktop.viewmodel.ViewModel import org.briarproject.briar.desktop.viewmodel.asState import javax.inject.Inject @@ -42,15 +40,17 @@ constructor( private val _selectedSetting = mutableStateOf(SettingCategory.DISPLAY) val selectedSetting = _selectedSetting.asState() - private val _isDarkMode = mutableStateOf(settings.theme == DARK) - val isDarkMode = _isDarkMode.asState() + val themesList = Settings.Theme.values() + + private val _selectedTheme = mutableStateOf(settings.theme) + val selectedTheme = _selectedTheme.asState() fun selectSetting(selectedOption: SettingCategory) { _selectedSetting.value = selectedOption } - fun toggleTheme() { // todo: set theme instead - settings.theme = if (settings.theme == DARK) LIGHT else DARK - _isDarkMode.value = settings.theme == DARK + fun selectTheme(theme: Settings.Theme) { + settings.theme = theme + _selectedTheme.value = theme } } diff --git a/src/main/resources/strings/BriarDesktop.properties b/src/main/resources/strings/BriarDesktop.properties index 67c428179df2bfed55558ac2e9e1ac30381e37ee..48f1732f85d29cd7c8c93a1f3413ef710a4c0ce4 100644 --- a/src/main/resources/strings/BriarDesktop.properties +++ b/src/main/resources/strings/BriarDesktop.properties @@ -219,8 +219,6 @@ settings.general.title=General # Settings Display settings.display.title=Display -# todo: to be removed -settings.display.theme=Dark Theme settings.display.theme.title=Theme settings.display.theme.auto=System default settings.display.theme.dark=Dark