diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt index acbf9a08..ae5e1551 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/KeyboardButton.kt @@ -18,6 +18,7 @@ package com.sadellie.unitto.core.ui.common +import android.content.res.Configuration import android.view.HapticFeedbackConstants import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateIntAsState @@ -26,11 +27,9 @@ import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.minimumInteractiveComponentSize import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue @@ -38,8 +37,8 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.unit.dp @Composable fun BasicKeyboardButton( @@ -50,7 +49,7 @@ fun BasicKeyboardButton( icon: ImageVector, iconColor: Color, allowVibration: Boolean, - contentPadding: PaddingValues = PaddingValues(24.dp, 8.dp) + contentHeight: Float ) { val view = LocalView.current val interactionSource = remember { MutableInteractionSource() } @@ -66,10 +65,15 @@ fun BasicKeyboardButton( onLongClick = onLongClick, shape = RoundedCornerShape(cornerRadius), containerColor = containerColor, - contentPadding = contentPadding, + contentPadding = PaddingValues(), interactionSource = interactionSource ) { - Icon(icon, null, modifier = Modifier.fillMaxHeight(), tint = iconColor) + Icon( + imageVector = icon, + contentDescription = null, + modifier = Modifier.fillMaxHeight(contentHeight), + tint = iconColor + ) } LaunchedEffect(key1 = isPressed) { @@ -93,6 +97,7 @@ fun KeyboardButtonLight( icon = icon, iconColor = MaterialTheme.colorScheme.onSurfaceVariant, allowVibration = allowVibration, + contentHeight = if (isPortrait()) 0.5f else 0.85f ) } @@ -111,7 +116,8 @@ fun KeyboardButtonFilled( containerColor = MaterialTheme.colorScheme.primaryContainer, icon = icon, iconColor = MaterialTheme.colorScheme.onSecondaryContainer, - allowVibration = allowVibration + allowVibration = allowVibration, + contentHeight = if (isPortrait()) 0.5f else 0.85f ) } @@ -123,17 +129,17 @@ fun KeyboardButtonAdditional( onLongClick: (() -> Unit)? = null, onClick: () -> Unit ) { - BasicKeyboardButton( - modifier = modifier - .minimumInteractiveComponentSize() - .heightIn(max = 48.dp), + modifier = modifier, onClick = onClick, + onLongClick = onLongClick, containerColor = Color.Transparent, icon = icon, iconColor = MaterialTheme.colorScheme.onSurfaceVariant, allowVibration = allowVibration, - contentPadding = PaddingValues(12.dp, 2.dp), - onLongClick = onLongClick + contentHeight = if (isPortrait()) 0.8f else 0.85f ) } + +@Composable +private fun isPortrait() = LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/PortraitLandscape.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/PortraitLandscape.kt index abf7d731..74d20f99 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/PortraitLandscape.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/PortraitLandscape.kt @@ -19,7 +19,6 @@ package com.sadellie.unitto.core.ui.common import android.content.res.Configuration -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxHeight @@ -42,33 +41,20 @@ fun PortraitLandscape( content2: @Composable (Modifier) -> Unit, ) { if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { - Column( - modifier = modifier.fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - content1(Modifier) - content2( - Modifier - .fillMaxSize() - .padding(horizontal = 8.dp) - ) + ColumnWithConstraints(modifier) { + content1(Modifier.fillMaxHeight(0.38f).padding(horizontal = it.maxWidth * 0.03f)) + content2(Modifier.fillMaxSize().padding(it.maxWidth * 0.03f, it.maxHeight * 0.015f)) } } else { - Row( - modifier = modifier.fillMaxSize(), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - content1( - Modifier - .weight(1f) - .fillMaxHeight() - ) - content2( - Modifier - .weight(1f) - .fillMaxSize() - .padding(horizontal = 8.dp) - ) + RowWithConstraints(modifier) { + val contentModifier = Modifier + .weight(1f) + .fillMaxSize() + .padding( + it.maxWidth * 0.015f, 0.dp, + it.maxHeight * 0.03f, it.maxHeight * 0.03f) + content1(contentModifier) + content2(contentModifier) } } } diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/WithConstraints.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/WithConstraints.kt new file mode 100644 index 00000000..09dc46d0 --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/WithConstraints.kt @@ -0,0 +1,56 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.core.ui.common + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxWithConstraintsScope +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun ColumnWithConstraints( + modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.Top, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + content: @Composable (ColumnScope.(BoxWithConstraintsScope)-> Unit) +) = BoxWithConstraints(modifier) { + Column( + verticalArrangement = verticalArrangement, + horizontalAlignment = horizontalAlignment + ) { content(this@BoxWithConstraints) } +} + +@Composable +fun RowWithConstraints( + modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + verticalAlignment: Alignment.Vertical = Alignment.Top, + content: @Composable (RowScope.(BoxWithConstraintsScope)-> Unit) +) = BoxWithConstraints(modifier) { + Row( + horizontalArrangement = horizontalArrangement, + verticalAlignment = verticalAlignment + ) { content(this@BoxWithConstraints) } +} diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/FloatingTextActionModeCallback.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FloatingTextActionModeCallback.kt similarity index 97% rename from feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/FloatingTextActionModeCallback.kt rename to core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FloatingTextActionModeCallback.kt index ab6711d8..07bbac80 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/FloatingTextActionModeCallback.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FloatingTextActionModeCallback.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.sadellie.unitto.feature.calculator.components +package com.sadellie.unitto.core.ui.common.textfield import android.view.ActionMode import android.view.Menu diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt new file mode 100644 index 00000000..7ef702a6 --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/InputTextField.kt @@ -0,0 +1,267 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2023 Elshan Agaev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU 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 General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.sadellie.unitto.core.ui.common.textfield + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.widthIn +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.material3.MaterialTheme +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.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.layout.layout +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInWindow +import androidx.compose.ui.platform.ClipboardManager +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalTextInputService +import androidx.compose.ui.platform.LocalTextToolbar +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.Paragraph +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.createFontFamilyResolver +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.TextUnit +import androidx.compose.ui.unit.sp +import com.sadellie.unitto.core.base.Separator +import com.sadellie.unitto.core.ui.Formatter +import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge +import kotlin.math.ceil +import kotlin.math.roundToInt + +@Composable +fun InputTextField( + modifier: Modifier, + value: TextFieldValue, + textStyle: TextStyle = NumbersTextStyleDisplayLarge, + minRatio: Float = 1f, + cutCallback: () -> Unit, + pasteCallback: (String) -> Unit, + onCursorChange: (IntRange) -> Unit, + textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, +) { + val clipboardManager = LocalClipboardManager.current + fun copyCallback() = clipboardManager.copyWithoutGrouping(value) + + val textToolbar = UnittoTextToolbar( + view = LocalView.current, + copyCallback = ::copyCallback, + pasteCallback = { + pasteCallback( + Formatter.toSeparator( + clipboardManager.getText()?.text ?: "", Separator.COMMA + ) + ) + }, + cutCallback = { + copyCallback() + cutCallback() + onCursorChange(value.selection.end..value.selection.end) + } + ) + + CompositionLocalProvider( + LocalTextInputService provides null, + LocalTextToolbar provides textToolbar + ) { + AutoSizableTextField( + modifier = modifier, + value = value, + textStyle = textStyle.copy(color = textColor), + minRatio = minRatio, + onValueChange = { + onCursorChange(it.selection.start..it.selection.end) + }, + showToolbar = textToolbar::showMenu, + hideToolbar = textToolbar::hide + ) + } +} + +@Composable +fun InputTextField( + modifier: Modifier = Modifier, + value: String, + textStyle: TextStyle = NumbersTextStyleDisplayLarge, + minRatio: Float = 1f, + textColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + var textFieldValue by remember(value) { + mutableStateOf(TextFieldValue(value, selection = TextRange(value.length))) + } + val clipboardManager = LocalClipboardManager.current + fun copyCallback() { + clipboardManager.copyWithoutGrouping(textFieldValue) + textFieldValue = textFieldValue.copy(selection = TextRange(textFieldValue.selection.end)) + } + + CompositionLocalProvider( + LocalTextInputService provides null, + LocalTextToolbar provides UnittoTextToolbar( + view = LocalView.current, + copyCallback = ::copyCallback, + ) + ) { + AutoSizableTextField( + modifier = modifier, + value = textFieldValue, + onValueChange = { textFieldValue = it }, + textStyle = textStyle.copy(color = textColor), + minRatio = minRatio, + readOnly = true, + interactionSource = interactionSource + ) + } +} + +@Composable +private fun AutoSizableTextField( + modifier: Modifier = Modifier, + value: TextFieldValue, + textStyle: TextStyle = TextStyle(), + scaleFactor: Float = 0.95f, + minRatio: Float = 1f, + onValueChange: (TextFieldValue) -> Unit, + readOnly: Boolean = false, + showToolbar: (rect: Rect) -> Unit = {}, + hideToolbar: () -> Unit = {}, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() } +) { + val focusRequester = remember { FocusRequester() } + val density = LocalDensity.current + + var nFontSize: TextUnit by remember { mutableStateOf(0.sp) } + var minFontSize: TextUnit + + BoxWithConstraints( + modifier = modifier, + contentAlignment = Alignment.BottomStart + ) { + with(density) { + // Cursor handle is not visible without this, 0.836f is the minimum required factor here + nFontSize = maxHeight.toSp() * 0.836f + minFontSize = nFontSize * minRatio + } + + // Modified: https://blog.canopas.com/autosizing-textfield-in-jetpack-compose-7a80f0270853 + val calculateParagraph = @Composable { + Paragraph( + text = value.text, + style = textStyle.copy(fontSize = nFontSize), + constraints = Constraints( + maxWidth = ceil(with(density) { maxWidth.toPx() }).toInt() + ), + density = density, + fontFamilyResolver = createFontFamilyResolver(LocalContext.current), + spanStyles = listOf(), + placeholders = listOf(), + maxLines = 1, + ellipsis = false + ) + } + + var intrinsics = calculateParagraph() + with(density) { + while ((intrinsics.maxIntrinsicWidth > maxWidth.toPx()) && nFontSize >= minFontSize) { + nFontSize *= scaleFactor + intrinsics = calculateParagraph() + } + } + + val nTextStyle = textStyle.copy( + // https://issuetracker.google.com/issues/266470454 + // textAlign = TextAlign.End, + fontSize = nFontSize + ) + var offset = Offset.Zero + + BasicTextField( + value = value, + onValueChange = { + showToolbar(Rect(offset, 0f)) + hideToolbar() + onValueChange(it) + }, + modifier = Modifier + .focusRequester(focusRequester) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { + hideToolbar() + focusRequester.requestFocus() + onValueChange(value.copy(selection = TextRange.Zero)) + showToolbar(Rect(offset, 0f)) + } + ) + .widthIn(max = with(density) { intrinsics.width.toDp() }) + .layout { measurable, constraints -> + val placeable = measurable.measure(constraints) + // TextField size is changed with a delay (text jumps). Here we correct it. + layout(placeable.width, placeable.height) { + placeable.place( + x = (intrinsics.width - intrinsics.maxIntrinsicWidth) + .coerceAtLeast(0f) + .roundToInt(), + y = (placeable.height - intrinsics.height).roundToInt() + ) + } + } + .onGloballyPositioned { layoutCoords -> offset = layoutCoords.positionInWindow() }, + textStyle = nTextStyle, + cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant), + singleLine = true, + readOnly = readOnly, + interactionSource = interactionSource + ) + } +} + +/** + * Copy value to clipboard without grouping symbols. + * + * Example: + * "123.456,789" will be copied as "123456,789" + * + * @param value Formatted value that has grouping symbols. + */ +fun ClipboardManager.copyWithoutGrouping(value: TextFieldValue) = this.setText( + AnnotatedString( + Formatter.removeGrouping(value.annotatedString.subSequence(value.selection).text) + ) +) diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoActionModeCallback.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoActionModeCallback.kt similarity index 97% rename from feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoActionModeCallback.kt rename to core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoActionModeCallback.kt index bb18c16f..f6b8b480 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoActionModeCallback.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoActionModeCallback.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.sadellie.unitto.feature.calculator.components +package com.sadellie.unitto.core.ui.common.textfield import android.view.ActionMode import android.view.Menu diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoPrimaryTextActionModeCallback.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoPrimaryTextActionModeCallback.kt similarity index 96% rename from feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoPrimaryTextActionModeCallback.kt rename to core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoPrimaryTextActionModeCallback.kt index 21750f3a..391c6687 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoPrimaryTextActionModeCallback.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoPrimaryTextActionModeCallback.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.sadellie.unitto.feature.calculator.components +package com.sadellie.unitto.core.ui.common.textfield import android.view.ActionMode import android.view.Menu diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoTextToolbar.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoTextToolbar.kt similarity index 89% rename from feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoTextToolbar.kt rename to core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoTextToolbar.kt index 46d32b8c..93bd577b 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/UnittoTextToolbar.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoTextToolbar.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.sadellie.unitto.feature.calculator.components +package com.sadellie.unitto.core.ui.common.textfield import android.os.Build import android.view.ActionMode @@ -25,7 +25,7 @@ import androidx.compose.ui.geometry.Rect import androidx.compose.ui.platform.TextToolbar import androidx.compose.ui.platform.TextToolbarStatus -internal class UnittoTextToolbar( +class UnittoTextToolbar( private val view: View, private val copyCallback: () -> Unit, private val pasteCallback: (() -> Unit)? = null, @@ -45,10 +45,8 @@ internal class UnittoTextToolbar( onSelectAllRequested: (() -> Unit)? ) { textActionModeCallback.rect = rect - textActionModeCallback.onCopyRequested = { onCopyRequested?.invoke(); copyCallback.invoke() } - textActionModeCallback.onCutRequested = cutCallback?.let { - { it.invoke(); onCutRequested?.invoke() } - } + textActionModeCallback.onCopyRequested = copyCallback + textActionModeCallback.onCutRequested = cutCallback textActionModeCallback.onPasteRequested = pasteCallback textActionModeCallback.onSelectAllRequested = onSelectAllRequested if (actionMode == null) { diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt index 19dc898a..5b90a70e 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/theme/Type.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.em import androidx.compose.ui.unit.sp import com.sadellie.unitto.core.ui.R @@ -42,7 +43,7 @@ val NumbersTextStyleDisplayLarge = TextStyle( fontFamily = Lato, fontWeight = FontWeight.W400, fontSize = 57.sp, - lineHeight = 64.sp, + lineHeight = (1.4).em, letterSpacing = (-0.25).sp, ) diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt index d45f567a..bc8990eb 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/CalculatorScreen.kt @@ -26,17 +26,15 @@ import androidx.compose.foundation.background import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.draggable import androidx.compose.foundation.gestures.rememberDraggableState -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.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.sizeIn -import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.text.selection.SelectionContainer import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.outlined.MoreVert @@ -48,7 +46,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults 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 @@ -58,28 +55,22 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.onPlaced -import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalConfiguration -import androidx.compose.ui.platform.LocalTextToolbar -import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.sadellie.unitto.core.base.Separator import com.sadellie.unitto.core.ui.Formatter import com.sadellie.unitto.core.ui.common.MenuButton import com.sadellie.unitto.core.ui.common.UnittoScreenWithTopBar -import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium +import com.sadellie.unitto.core.ui.common.textfield.InputTextField import com.sadellie.unitto.data.model.HistoryItem import com.sadellie.unitto.feature.calculator.components.CalculatorKeyboard import com.sadellie.unitto.feature.calculator.components.DragDownView import com.sadellie.unitto.feature.calculator.components.HistoryList -import com.sadellie.unitto.feature.calculator.components.InputTextField -import com.sadellie.unitto.feature.calculator.components.UnittoTextToolbar import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.* @@ -92,39 +83,20 @@ internal fun CalculatorRoute( navigateToSettings: () -> Unit, viewModel: CalculatorViewModel = hiltViewModel() ) { - val clipboardManager = LocalClipboardManager.current val uiState = viewModel.uiState.collectAsStateWithLifecycle() - fun copyToClipboard() { - val clipboardText = clipboardManager.getText() ?: return - // This method is called immediately after copying formatted text, we replace it with the - // the unformatted version. - clipboardManager.setText( - AnnotatedString( - clipboardText.text.replace(Formatter.grouping, "") - ) - ) - } - - CompositionLocalProvider( - LocalTextToolbar provides UnittoTextToolbar( - view = LocalView.current, - copyCallback = ::copyToClipboard - ) - ) { - CalculatorScreen( - uiState = uiState.value, - navigateToMenu = navigateToMenu, - navigateToSettings = navigateToSettings, - addSymbol = viewModel::addSymbol, - clearSymbols = viewModel::clearSymbols, - deleteSymbol = viewModel::deleteSymbol, - onCursorChange = viewModel::onCursorChange, - toggleAngleMode = viewModel::toggleCalculatorMode, - evaluate = viewModel::evaluate, - clearHistory = viewModel::clearHistory - ) - } + CalculatorScreen( + uiState = uiState.value, + navigateToMenu = navigateToMenu, + navigateToSettings = navigateToSettings, + addSymbol = viewModel::addSymbol, + clearSymbols = viewModel::clearSymbols, + deleteSymbol = viewModel::deleteSymbol, + onCursorChange = viewModel::onCursorChange, + toggleAngleMode = viewModel::toggleCalculatorMode, + evaluate = viewModel::evaluate, + clearHistory = viewModel::clearHistory + ) } @Composable @@ -188,18 +160,18 @@ private fun CalculatorScreen( .fillMaxSize(), historyItems = uiState.history, historyItemHeightCallback = { historyItemHeight = it }, - onTextClick = addSymbol ) }, textFields = { maxDragAmount -> Column( Modifier + .fillMaxHeight(0.25f) .onPlaced { textThingyHeight = it.size.height } .background( MaterialTheme.colorScheme.surfaceVariant, RoundedCornerShape( - topStart = 0.dp, topEnd = 0.dp, - bottomStart = 32.dp, bottomEnd = 32.dp + topStartPercent = 0, topEndPercent = 0, + bottomStartPercent = 20, bottomEndPercent = 20 ) ) .draggable( @@ -226,33 +198,32 @@ private fun CalculatorScreen( } } ) - .padding(top = 8.dp), + .padding(top = 12.dp), horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(8.dp) ) { InputTextField( modifier = Modifier + .weight(2f) .fillMaxWidth() .padding(horizontal = 8.dp), - value = uiState.input, - onCursorChange = onCursorChange, + value = uiState.input.copy( + Formatter.fromSeparator(uiState.input.text, Separator.COMMA) + ), + minRatio = 0.5f, + cutCallback = deleteSymbol, pasteCallback = addSymbol, - cutCallback = deleteSymbol + onCursorChange = onCursorChange ) if (LocalConfiguration.current.orientation == Configuration.ORIENTATION_PORTRAIT) { - SelectionContainer { - Text( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp) - .horizontalScroll(rememberScrollState(), reverseScrolling = true), - text = Formatter.format(uiState.output), - textAlign = TextAlign.End, - softWrap = false, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), - style = NumbersTextStyleDisplayMedium, - ) - } + InputTextField( + modifier = Modifier + .weight(1f) + .fillMaxWidth() + .padding(horizontal = 8.dp), + value = Formatter.format(uiState.output), + textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(0.6f) + ) } // Handle Box( @@ -268,7 +239,7 @@ private fun CalculatorScreen( }, numPad = { CalculatorKeyboard( - modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp), + modifier = Modifier.fillMaxSize().padding(horizontal = 8.dp, vertical = 4.dp), radianMode = uiState.radianMode, allowVibration = uiState.allowVibration, addSymbol = addSymbol, @@ -314,7 +285,12 @@ private fun CalculatorScreen( } } -@Preview +@Preview(widthDp = 432, heightDp = 1008, device = "spec:parent=pixel_5,orientation=portrait") +@Preview(widthDp = 432, heightDp = 864, device = "spec:parent=pixel_5,orientation=portrait") +@Preview(widthDp = 597, heightDp = 1393, device = "spec:parent=pixel_5,orientation=portrait") +@Preview(heightDp = 432, widthDp = 1008, device = "spec:parent=pixel_5,orientation=landscape") +@Preview(heightDp = 432, widthDp = 864, device = "spec:parent=pixel_5,orientation=landscape") +@Preview(heightDp = 597, widthDp = 1393, device = "spec:parent=pixel_5,orientation=landscape") @Composable private fun PreviewCalculatorScreen() { val dtf = SimpleDateFormat("dd.MM.yyyy HH:mm:ss", Locale.getDefault()) @@ -332,14 +308,14 @@ private fun PreviewCalculatorScreen() { HistoryItem( date = dtf.parse(it)!!, expression = "12345".repeat(10), - result = "67890" + result = "1234" ) } CalculatorScreen( uiState = CalculatorUIState( - input = TextFieldValue("12345"), - output = "12345", + input = TextFieldValue("1.2345"), + output = "1234", history = historyItems ), navigateToMenu = {}, diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt index 54357868..16d1f1a6 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/CalculatorKeyboard.kt @@ -25,34 +25,39 @@ import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween import androidx.compose.foundation.layout.Arrangement +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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.heightIn +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ExpandLess import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.minimumInteractiveComponentSize 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.rotate import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.Dp import com.sadellie.unitto.core.base.Token import com.sadellie.unitto.core.ui.Formatter +import com.sadellie.unitto.core.ui.common.ColumnWithConstraints import com.sadellie.unitto.core.ui.common.KeyboardButtonAdditional import com.sadellie.unitto.core.ui.common.KeyboardButtonFilled import com.sadellie.unitto.core.ui.common.KeyboardButtonLight +import com.sadellie.unitto.core.ui.common.RowWithConstraints import com.sadellie.unitto.core.ui.common.key.UnittoIcons import com.sadellie.unitto.core.ui.common.key.unittoicons.AcTan import com.sadellie.unitto.core.ui.common.key.unittoicons.ArCos @@ -149,22 +154,26 @@ private fun PortraitKeyboard( animationSpec = tween(easing = FastOutSlowInEasing) ) - Column( + ColumnWithConstraints( modifier = modifier - ) { + ) { constraints -> + fun verticalFraction(fraction: Float): Dp = constraints.maxHeight * fraction + fun horizontalFraction(fraction: Float): Dp = constraints.maxWidth * fraction + val weightModifier = Modifier.weight(1f) val mainButtonModifier = Modifier .fillMaxSize() .weight(1f) - .padding(4.dp) + .padding(horizontalFraction(0.015f), verticalFraction(0.009f)) val additionalButtonModifier = Modifier - .minimumInteractiveComponentSize() .weight(1f) - .heightIn(max = 48.dp) + .height(verticalFraction(0.09f)) + + Spacer(modifier = Modifier.height(verticalFraction(0.025f))) Row( - modifier = Modifier.padding(vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(2.dp) + modifier = Modifier, + horizontalArrangement = Arrangement.spacedBy(horizontalFraction(0.03f)) ) { // Additional buttons Crossfade(invMode, weightModifier) { @@ -191,22 +200,28 @@ private fun PortraitKeyboard( } } - // Expand/Collapse - IconButton( - onClick = { showAdditional = !showAdditional }, - colors = IconButtonDefaults.iconButtonColors(containerColor = MaterialTheme.colorScheme.inverseOnSurface) + Box( + modifier = Modifier.size(verticalFraction(0.09f)), + contentAlignment = Alignment.Center ) { - Icon(Icons.Default.ExpandLess, null, Modifier.rotate(expandRotation)) + // Expand/Collapse + IconButton( + onClick = { showAdditional = !showAdditional }, + colors = IconButtonDefaults.iconButtonColors(containerColor = MaterialTheme.colorScheme.inverseOnSurface) + ) { + Icon(Icons.Default.ExpandLess, null, Modifier.rotate(expandRotation)) + } } } + Spacer(modifier = Modifier.height(verticalFraction(0.025f))) + Row(weightModifier) { KeyboardButtonFilled(mainButtonModifier, UnittoIcons.LeftBracket, allowVibration) { addSymbol(Token.leftBracket) } KeyboardButtonFilled(mainButtonModifier, UnittoIcons.RightBracket, allowVibration) { addSymbol(Token.rightBracket) } KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Percent, allowVibration) { addSymbol(Token.percent) } KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Divide, allowVibration) { addSymbol(Token.divideDisplay) } } - Row(weightModifier) { KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key7, allowVibration) { addSymbol(Token._7) } KeyboardButtonLight(mainButtonModifier, UnittoIcons.Key8, allowVibration) { addSymbol(Token._8) } @@ -231,6 +246,8 @@ private fun PortraitKeyboard( KeyboardButtonLight(mainButtonModifier, UnittoIcons.Backspace, allowVibration, clearSymbols) { deleteSymbol() } KeyboardButtonFilled(mainButtonModifier, UnittoIcons.Equal, allowVibration) { evaluate() } } + + Spacer(modifier = Modifier.height(verticalFraction(0.015f))) } } @@ -245,7 +262,7 @@ private fun AdditionalButtonsPortrait( toggleInvMode: () -> Unit ) { Column { - Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { + Row { KeyboardButtonAdditional(modifier, UnittoIcons.SquareRootWide, allowVibration) { addSymbol(Token.sqrt) } KeyboardButtonAdditional(modifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.pi) } KeyboardButtonAdditional(modifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.exponent) } @@ -253,13 +270,13 @@ private fun AdditionalButtonsPortrait( } AnimatedVisibility(showAdditional) { Column { - Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { + Row { KeyboardButtonAdditional(modifier, if (radianMode) UnittoIcons.Rad else UnittoIcons.Deg, allowVibration) { toggleAngleMode() } KeyboardButtonAdditional(modifier, UnittoIcons.Sin, allowVibration) { addSymbol(Token.sin) } KeyboardButtonAdditional(modifier, UnittoIcons.Cos, allowVibration) { addSymbol(Token.cos) } KeyboardButtonAdditional(modifier, UnittoIcons.Tan, allowVibration) { addSymbol(Token.tan) } } - Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { + Row { KeyboardButtonAdditional(modifier, UnittoIcons.Inv, allowVibration) { toggleInvMode() } KeyboardButtonAdditional(modifier, UnittoIcons.E, allowVibration) { addSymbol(Token.e) } KeyboardButtonAdditional(modifier, UnittoIcons.Ln, allowVibration) { addSymbol(Token.ln) } @@ -281,7 +298,7 @@ private fun AdditionalButtonsPortraitInverse( toggleInvMode: () -> Unit ) { Column { - Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { + Row { KeyboardButtonAdditional(modifier, UnittoIcons.Modulo, allowVibration) { addSymbol(Token.modulo) } KeyboardButtonAdditional(modifier, UnittoIcons.Pi, allowVibration) { addSymbol(Token.pi) } KeyboardButtonAdditional(modifier, UnittoIcons.ExponentWide, allowVibration) { addSymbol(Token.exponent) } @@ -289,13 +306,13 @@ private fun AdditionalButtonsPortraitInverse( } AnimatedVisibility(showAdditional) { Column { - Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { + Row { KeyboardButtonAdditional(modifier, if (radianMode) UnittoIcons.Rad else UnittoIcons.Deg, allowVibration) { toggleAngleMode() } KeyboardButtonAdditional(modifier, UnittoIcons.ArSin, allowVibration) { addSymbol(Token.arSin) } KeyboardButtonAdditional(modifier, UnittoIcons.ArCos, allowVibration) { addSymbol(Token.arCos) } KeyboardButtonAdditional(modifier, UnittoIcons.AcTan, allowVibration) { addSymbol(Token.acTan) } } - Row(horizontalArrangement = Arrangement.spacedBy(2.dp)) { + Row { KeyboardButtonAdditional(modifier, UnittoIcons.Inv, allowVibration) { toggleInvMode() } KeyboardButtonAdditional(modifier, UnittoIcons.E, allowVibration) { addSymbol(Token.e) } KeyboardButtonAdditional(modifier, UnittoIcons.Exp, allowVibration) { addSymbol(Token.exp) } @@ -320,11 +337,11 @@ private fun LandscapeKeyboard( val fractionalIcon = remember { if (Formatter.fractional == Token.dot) UnittoIcons.Dot else UnittoIcons.Comma } var invMode: Boolean by remember { mutableStateOf(false) } - Row(modifier) { + RowWithConstraints(modifier) { constraints -> val buttonModifier = Modifier .fillMaxWidth() .weight(1f) - .padding(4.dp) + .padding(constraints.maxWidth * 0.005f, constraints.maxHeight * 0.02f) Crossfade(invMode, Modifier.weight(3f)) { Row { diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt index b6ab6402..7f68594c 100644 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt +++ b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/HistoryList.kt @@ -19,10 +19,8 @@ package com.sadellie.unitto.feature.calculator.components import androidx.compose.foundation.background -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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -30,24 +28,35 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.text.BasicTextField import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.History import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.derivedStateOf 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.layout.onPlaced +import androidx.compose.ui.platform.LocalClipboardManager +import androidx.compose.ui.platform.LocalTextInputService +import androidx.compose.ui.platform.LocalTextToolbar +import androidx.compose.ui.platform.LocalView import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.sadellie.unitto.core.ui.Formatter +import com.sadellie.unitto.core.ui.common.textfield.UnittoTextToolbar +import com.sadellie.unitto.core.ui.common.textfield.copyWithoutGrouping import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium import com.sadellie.unitto.data.model.HistoryItem import com.sadellie.unitto.feature.calculator.R @@ -59,7 +68,6 @@ internal fun HistoryList( modifier: Modifier, historyItems: List, historyItemHeightCallback: (Int) -> Unit, - onTextClick: (String) -> Unit ) { val verticalArrangement by remember(historyItems) { derivedStateOf { @@ -95,15 +103,13 @@ internal fun HistoryList( item { HistoryListItem( modifier = Modifier.onPlaced { historyItemHeightCallback(it.size.height) }, - historyItem = historyItems.first(), - onTextClick = onTextClick + historyItem = historyItems.first() ) } items(historyItems.drop(1)) { historyItem -> HistoryListItem( modifier = Modifier, - historyItem = historyItem, - onTextClick = onTextClick + historyItem = historyItem ) } } @@ -114,40 +120,56 @@ internal fun HistoryList( private fun HistoryListItem( modifier: Modifier = Modifier, historyItem: HistoryItem, - onTextClick: (String) -> Unit ) { - SelectionContainer { - Column(modifier = modifier) { - Box( - Modifier.clickable { onTextClick(historyItem.expression) } - ) { - Text( - text = Formatter.format(historyItem.expression), - maxLines = 1, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp) - .horizontalScroll(rememberScrollState(), reverseScrolling = true), - style = NumbersTextStyleDisplayMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - textAlign = TextAlign.End - ) - } - Box( - Modifier.clickable { onTextClick(historyItem.result) } - ) { - Text( - text = Formatter.format(historyItem.result), - maxLines = 1, - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 8.dp) - .horizontalScroll(rememberScrollState(), reverseScrolling = true), - style = NumbersTextStyleDisplayMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), - textAlign = TextAlign.End - ) - } + val clipboardManager = LocalClipboardManager.current + val expr = Formatter.format(historyItem.expression) + var textFieldexpr by remember(expr) { + mutableStateOf(TextFieldValue(expr, selection = TextRange(expr.length))) + } + val res = Formatter.format(historyItem.expression) + var textFieldRes by remember(res) { + mutableStateOf(TextFieldValue(res, selection = TextRange(res.length))) + } + + Column(modifier = modifier) { + CompositionLocalProvider( + LocalTextInputService provides null, + LocalTextToolbar provides UnittoTextToolbar( + view = LocalView.current, + copyCallback = { clipboardManager.copyWithoutGrouping(textFieldexpr) } + ) + ) { + BasicTextField( + value = textFieldexpr, + onValueChange = { textFieldexpr = it }, + maxLines = 1, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .horizontalScroll(rememberScrollState(), reverseScrolling = true), + textStyle = NumbersTextStyleDisplayMedium.copy(color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.End), + readOnly = true + ) + } + + CompositionLocalProvider( + LocalTextInputService provides null, + LocalTextToolbar provides UnittoTextToolbar( + view = LocalView.current, + copyCallback = { clipboardManager.copyWithoutGrouping(textFieldRes) } + ) + ) { + BasicTextField( + value = textFieldRes, + onValueChange = { textFieldRes = it }, + maxLines = 1, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp) + .horizontalScroll(rememberScrollState(), reverseScrolling = true), + textStyle = NumbersTextStyleDisplayMedium.copy(color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f), textAlign = TextAlign.End), + readOnly = true + ) } } } @@ -178,8 +200,6 @@ private fun PreviewHistoryList() { modifier = Modifier .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)) .fillMaxSize(), - historyItems = historyItems, - historyItemHeightCallback = {}, - onTextClick = {} - ) + historyItems = historyItems + ) {} } diff --git a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/InputTextField.kt b/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/InputTextField.kt deleted file mode 100644 index e225f790..00000000 --- a/feature/calculator/src/main/java/com/sadellie/unitto/feature/calculator/components/InputTextField.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Unitto is a unit converter for Android - * Copyright (c) 2023 Elshan Agaev - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU 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 General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package com.sadellie.unitto.feature.calculator.components - -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalClipboardManager -import androidx.compose.ui.platform.LocalTextInputService -import androidx.compose.ui.platform.LocalTextToolbar -import androidx.compose.ui.platform.LocalView -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign -import com.sadellie.unitto.core.base.Separator -import com.sadellie.unitto.core.ui.Formatter -import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge - -@Composable -internal fun InputTextField( - modifier: Modifier, - value: TextFieldValue, - onCursorChange: (IntRange) -> Unit, - pasteCallback: (String) -> Unit, - cutCallback: () -> Unit -) { - val clipboardManager = LocalClipboardManager.current - - val formattedInput: TextFieldValue by remember(value) { - derivedStateOf { - value.copy( - // We replace this because internally input value is already formatted, but uses - // comma as separator. - Formatter.fromSeparator(value.text, Separator.COMMA) - ) - } - } - - fun copyToClipboard() = clipboardManager.setText( - AnnotatedString( - Formatter.removeGrouping( - formattedInput.annotatedString.subSequence(formattedInput.selection).text - ) - ) - ) - - CompositionLocalProvider( - LocalTextInputService provides null, - LocalTextToolbar provides UnittoTextToolbar( - view = LocalView.current, - copyCallback = ::copyToClipboard, - pasteCallback = { - pasteCallback( - Formatter.toSeparator( - clipboardManager.getText()?.text ?: "", Separator.COMMA - ) - ) - }, - cutCallback = { copyToClipboard(); cutCallback() } - ) - ) { - BasicTextField( - modifier = modifier, - cursorBrush = SolidColor(MaterialTheme.colorScheme.onSurfaceVariant), - singleLine = true, - value = formattedInput, - onValueChange = { - onCursorChange(it.selection.start..it.selection.end) - }, - textStyle = NumbersTextStyleDisplayLarge.copy( - textAlign = TextAlign.End, - color = MaterialTheme.colorScheme.onSurfaceVariant - ), - minLines = 1, - maxLines = 1, - ) - } -} diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt index f2fe804c..697ba53e 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/ConverterScreen.kt @@ -18,6 +18,7 @@ package com.sadellie.unitto.feature.converter +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.MoreVert @@ -30,6 +31,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.sadellie.unitto.core.ui.R @@ -91,7 +94,7 @@ private fun ConverterScreen( .centerAlignedTopAppBarColors(containerColor = Color.Transparent), content = { padding -> PortraitLandscape( - modifier = Modifier.padding(padding), + modifier = Modifier.padding(padding).fillMaxSize(), content1 = { TopScreenPart( modifier = it, @@ -125,11 +128,26 @@ private fun ConverterScreen( ) } -@Preview +class PreviewUIState: PreviewParameterProvider { + override val values: Sequence + get() = listOf( + ConverterUIState(inputValue = "1234", calculatedValue = null, resultValue = "5678", showLoading = false), + ConverterUIState(inputValue = "1234", calculatedValue = "234", resultValue = "5678", showLoading = false), + ).asSequence() +} + +@Preview(widthDp = 432, heightDp = 1008, device = "spec:parent=pixel_5,orientation=portrait") +@Preview(widthDp = 432, heightDp = 864, device = "spec:parent=pixel_5,orientation=portrait") +@Preview(widthDp = 597, heightDp = 1393, device = "spec:parent=pixel_5,orientation=portrait") +@Preview(heightDp = 432, widthDp = 1008, device = "spec:parent=pixel_5,orientation=landscape") +@Preview(heightDp = 432, widthDp = 864, device = "spec:parent=pixel_5,orientation=landscape") +@Preview(heightDp = 597, widthDp = 1393, device = "spec:parent=pixel_5,orientation=landscape") @Composable -private fun PreviewConverterScreen() { +private fun PreviewConverterScreen( + @PreviewParameter(PreviewUIState::class) uiState: ConverterUIState +) { ConverterScreen( - uiState = ConverterUIState(), + uiState = ConverterUIState(inputValue = "1234", calculatedValue = null, resultValue = "5678", showLoading = false), navigateToLeftScreen = {}, navigateToRightScreen = {_, _, _ -> }, navigateToSettings = {}, diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt index 7c7bbd63..e999fe20 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/Keyboard.kt @@ -19,16 +19,16 @@ package com.sadellie.unitto.feature.converter.components import androidx.compose.animation.Crossfade -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp import com.sadellie.unitto.core.base.Token import com.sadellie.unitto.core.ui.Formatter +import com.sadellie.unitto.core.ui.common.ColumnWithConstraints import com.sadellie.unitto.core.ui.common.KeyboardButtonFilled import com.sadellie.unitto.core.ui.common.KeyboardButtonLight import com.sadellie.unitto.core.ui.common.key.UnittoIcons @@ -85,7 +85,6 @@ internal fun Keyboard( ConverterMode.BASE -> BaseKeyboard(addDigit, clearInput, deleteDigit, allowVibration) } } - } @Composable @@ -96,39 +95,39 @@ private fun DefaultKeyboard( allowVibration: Boolean ) { val fractionalIcon = remember { if (Formatter.fractional == Token.dot) UnittoIcons.Dot else UnittoIcons.Comma } - Column { + ColumnWithConstraints { // Button modifier val bModifier = Modifier .fillMaxSize() .weight(1f) - .padding(4.dp) // Column modifier - val cModifier = Modifier.weight(1f) - Row(cModifier) { + val cModifier = Modifier.weight(1f).padding(vertical = it.maxHeight * 0.01f) + val horizontalArrangement = Arrangement.spacedBy(it.maxWidth * 0.03f) + Row(cModifier, horizontalArrangement) { KeyboardButtonFilled(bModifier, UnittoIcons.LeftBracket, allowVibration) { addDigit(Token.leftBracket) } KeyboardButtonFilled(bModifier, UnittoIcons.RightBracket, allowVibration) { addDigit(Token.rightBracket) } KeyboardButtonFilled(bModifier, UnittoIcons.Exponent, allowVibration) { addDigit(Token.exponent) } KeyboardButtonFilled(bModifier, UnittoIcons.SquareRoot, allowVibration) { addDigit(Token.sqrt) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key7, allowVibration) { addDigit(Token._7) } KeyboardButtonLight(bModifier, UnittoIcons.Key8, allowVibration) { addDigit(Token._8) } KeyboardButtonLight(bModifier, UnittoIcons.Key9, allowVibration) { addDigit(Token._9) } KeyboardButtonFilled(bModifier, UnittoIcons.Divide, allowVibration) { addDigit(Token.divide) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key4, allowVibration) { addDigit(Token._4) } KeyboardButtonLight(bModifier, UnittoIcons.Key5, allowVibration) { addDigit(Token._5) } KeyboardButtonLight(bModifier, UnittoIcons.Key6, allowVibration) { addDigit(Token._6) } KeyboardButtonFilled(bModifier, UnittoIcons.Multiply, allowVibration) { addDigit(Token.multiply) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key1, allowVibration) { addDigit(Token._1) } KeyboardButtonLight(bModifier, UnittoIcons.Key2, allowVibration) { addDigit(Token._2) } KeyboardButtonLight(bModifier, UnittoIcons.Key3, allowVibration) { addDigit(Token._3) } KeyboardButtonFilled(bModifier, UnittoIcons.Minus, allowVibration) { addDigit(Token.minus) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key0, allowVibration) { addDigit(Token._0) } KeyboardButtonLight(bModifier, fractionalIcon, allowVibration) { addDigit(Token.dot) } KeyboardButtonLight(bModifier, UnittoIcons.Backspace, allowVibration, clearInput) { deleteDigit() } @@ -144,43 +143,43 @@ private fun BaseKeyboard( deleteDigit: () -> Unit, allowVibration: Boolean ) { - Column { + ColumnWithConstraints { // Button modifier val bModifier = Modifier .fillMaxSize() .weight(1f) - .padding(4.dp) // Column modifier - val cModifier = Modifier.weight(1f) - - Row(cModifier) { + val cModifier = Modifier.weight(1f).padding(vertical = it.maxHeight * 0.01f) + val horizontalArrangement = Arrangement.spacedBy(it.maxWidth * 0.03f) + Row(cModifier, horizontalArrangement) { KeyboardButtonFilled(bModifier, UnittoIcons.KeyA, allowVibration) { addDigit(Token.baseA) } KeyboardButtonFilled(bModifier, UnittoIcons.KeyB, allowVibration) { addDigit(Token.baseB) } KeyboardButtonFilled(bModifier, UnittoIcons.KeyC, allowVibration) { addDigit(Token.baseC) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonFilled(bModifier, UnittoIcons.KeyD, allowVibration) { addDigit(Token.baseD) } KeyboardButtonFilled(bModifier, UnittoIcons.KeyE, allowVibration) { addDigit(Token.baseE) } KeyboardButtonFilled(bModifier, UnittoIcons.KeyF, allowVibration) { addDigit(Token.baseF) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key7, allowVibration) { addDigit(Token._7) } KeyboardButtonLight(bModifier, UnittoIcons.Key8, allowVibration) { addDigit(Token._8) } KeyboardButtonLight(bModifier, UnittoIcons.Key9, allowVibration) { addDigit(Token._9) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key4, allowVibration) { addDigit(Token._4) } KeyboardButtonLight(bModifier, UnittoIcons.Key5, allowVibration) { addDigit(Token._5) } KeyboardButtonLight(bModifier, UnittoIcons.Key6, allowVibration) { addDigit(Token._6) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key1, allowVibration) { addDigit(Token._1) } KeyboardButtonLight(bModifier, UnittoIcons.Key2, allowVibration) { addDigit(Token._2) } KeyboardButtonLight(bModifier, UnittoIcons.Key3, allowVibration) { addDigit(Token._3) } } - Row(cModifier) { + Row(cModifier, horizontalArrangement) { KeyboardButtonLight(bModifier, UnittoIcons.Key0, allowVibration) { addDigit(Token._0) } - KeyboardButtonLight(Modifier.fillMaxSize().weight(2f).padding(4.dp), UnittoIcons.Backspace, allowVibration, clearInput) { deleteDigit() } + KeyboardButtonLight( + Modifier.fillMaxSize().weight(2f).padding(it.maxWidth * 0.015f, it.maxHeight * 0.008f), UnittoIcons.Backspace, allowVibration, clearInput) { deleteDigit() } } } } diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/MyTextField.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/MyTextField.kt index 675dbba3..e344d063 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/MyTextField.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/MyTextField.kt @@ -50,9 +50,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge -import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayMedium import com.sadellie.unitto.core.ui.R +import com.sadellie.unitto.core.ui.common.textfield.InputTextField +import com.sadellie.unitto.core.ui.theme.NumbersTextStyleDisplayLarge /** * Component for input and output @@ -94,7 +94,8 @@ internal fun MyTextField( ) { LazyRow( modifier = modifier - .wrapContentHeight(), + .wrapContentHeight() + .weight(2f), reverseLayout = true, horizontalArrangement = Arrangement.End, contentPadding = PaddingValues(horizontal = 8.dp) @@ -110,19 +111,17 @@ internal fun MyTextField( .using(SizeTransform(clip = false)) } ) { - Text( + InputTextField( modifier = Modifier.fillMaxWidth(), - // Quick fix to prevent the UI from crashing - text = it.take(1000), - textAlign = TextAlign.End, - softWrap = false, - style = NumbersTextStyleDisplayLarge + value = it.take(1000), + textStyle = NumbersTextStyleDisplayLarge.copy(textAlign = TextAlign.End) ) } } } AnimatedVisibility( + modifier = Modifier.weight(1f), visible = !secondaryText.isNullOrEmpty(), enter = expandVertically(), exit = shrinkVertically() @@ -145,14 +144,14 @@ internal fun MyTextField( .using(SizeTransform(clip = false)) } ) { - Text( - modifier = Modifier, - // Quick fix to prevent the UI from crashing - text = it?.take(1000) ?: "", - textAlign = TextAlign.End, - softWrap = false, - color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f), - style = NumbersTextStyleDisplayMedium + InputTextField( + modifier = Modifier.fillMaxWidth(), + value = it?.take(1000) ?: "", + textStyle = NumbersTextStyleDisplayLarge.copy( + textAlign = TextAlign.End, + color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ), + minRatio = 0.7f ) } } @@ -162,7 +161,8 @@ internal fun MyTextField( AnimatedContent( modifier = Modifier .align(Alignment.End) - .padding(horizontal = 8.dp), + .padding(horizontal = 8.dp) + .weight(1f), targetState = helperText ) { Text( diff --git a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/TopScreen.kt b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/TopScreen.kt index 26a5d7d9..aa40fe21 100644 --- a/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/TopScreen.kt +++ b/feature/converter/src/main/java/com/sadellie/unitto/feature/converter/components/TopScreen.kt @@ -18,18 +18,28 @@ package com.sadellie.unitto.feature.converter.components +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.SizeTransform import androidx.compose.animation.core.FastOutSlowInEasing import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.expandVertically +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.with import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.height import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.SwapHoriz import androidx.compose.material3.Icon import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -40,9 +50,11 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.rotate import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp +import androidx.compose.ui.text.style.TextAlign import com.sadellie.unitto.core.ui.Formatter import com.sadellie.unitto.core.ui.R +import com.sadellie.unitto.core.ui.common.ColumnWithConstraints +import com.sadellie.unitto.core.ui.common.textfield.InputTextField import com.sadellie.unitto.data.model.AbstractUnit import com.sadellie.unitto.data.model.UnitGroup import com.sadellie.unitto.feature.converter.ConverterMode @@ -91,54 +103,89 @@ internal fun TopScreenPart( ) val mContext = LocalContext.current - Column( + ColumnWithConstraints( modifier = modifier, - verticalArrangement = Arrangement.spacedBy(8.dp) ) { - MyTextField( - modifier = Modifier.fillMaxWidth(), - primaryText = { - when (converterMode) { - ConverterMode.BASE -> inputValue.uppercase() - else -> Formatter.format(inputValue) - } + InputTextField( + modifier = Modifier.weight(2f), + value = when (converterMode) { + ConverterMode.BASE -> inputValue.uppercase() + else -> Formatter.format(inputValue) }, - secondaryText = calculatedValue?.let { Formatter.format(it) }, - helperText = stringResource(unitFrom?.shortName ?: R.string.loading_label), - textToCopy = calculatedValue ?: inputValue, + minRatio = 0.7f ) - MyTextField( - modifier = Modifier.fillMaxWidth(), - onClick = onOutputTextFieldClick, - primaryText = { - when { - networkLoading -> stringResource(R.string.loading_label) - networkError -> stringResource(R.string.error_label) - converterMode == ConverterMode.BASE -> outputValue.uppercase() - formatTime and (unitTo?.group == UnitGroup.TIME) -> { - Formatter.formatTime( - context = mContext, - input = calculatedValue ?: inputValue, - basicUnit = unitFrom?.basicUnit - ) - } - else -> Formatter.format(outputValue) - } - }, - secondaryText = null, - helperText = stringResource(unitTo?.shortName ?: R.string.loading_label), - textToCopy = outputValue, - ) - // Unit selection buttons - Row( - modifier = Modifier.padding(horizontal = 8.dp), - verticalAlignment = Alignment.Bottom, + AnimatedVisibility( + visible = !calculatedValue.isNullOrEmpty(), + modifier = Modifier.weight(1f), + enter = expandVertically(clip = false), + exit = shrinkVertically(clip = false) ) { + InputTextField( + value = calculatedValue?.let { value -> Formatter.format(value) } ?: "", + minRatio = 0.7f, + textColor = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f) + ) + } + AnimatedContent( + modifier = Modifier.fillMaxWidth(), + targetState = stringResource(unitFrom?.shortName ?: R.string.loading_label), + transitionSpec = { + // Enter animation + (expandHorizontally(clip = false, expandFrom = Alignment.Start) + fadeIn() + // Exit animation + with fadeOut()) + .using(SizeTransform(clip = false)) + } + ) { value -> + Text( + text = value, + style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.End) + ) + } + + InputTextField( + modifier = Modifier + .weight(2f), + value = when { + networkLoading -> stringResource(R.string.loading_label) + networkError -> stringResource(R.string.error_label) + converterMode == ConverterMode.BASE -> outputValue.uppercase() + formatTime and (unitTo?.group == UnitGroup.TIME) -> { + Formatter.formatTime( + context = mContext, + input = calculatedValue ?: inputValue, + basicUnit = unitFrom?.basicUnit + ) + } + else -> Formatter.format(outputValue) + }, + minRatio = 0.7f, + ) + AnimatedContent( + modifier = Modifier.fillMaxWidth(), + targetState = stringResource(unitTo?.shortName ?: R.string.loading_label), + transitionSpec = { + // Enter animation + (expandHorizontally(clip = false, expandFrom = Alignment.Start) + fadeIn() + // Exit animation + with fadeOut()) + .using(SizeTransform(clip = false)) + } + ) { value -> + Text( + text = value, + style = MaterialTheme.typography.bodyMedium.copy(textAlign = TextAlign.End) + ) + } + + Spacer(modifier = Modifier.height(it.maxHeight * 0.03f)) + + Row(verticalAlignment = Alignment.CenterVertically) { UnitSelectionButton( modifier = Modifier .fillMaxWidth() .weight(1f), - onClick = { unitFrom?.let { navigateToLeftScreen(it.unitId) } }, + onClick = { unitFrom?.let { unit -> navigateToLeftScreen(unit.unitId) } }, label = unitFrom?.displayName ?: R.string.loading_label, ) IconButton(