diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/autosize/AutoSizeTextStyleBox.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/autosize/AutoSizeTextStyleBox.kt new file mode 100644 index 00000000..2292844a --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/autosize/AutoSizeTextStyleBox.kt @@ -0,0 +1,299 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2024 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.autosize + +import androidx.compose.foundation.layout.BoxWithConstraints +import androidx.compose.foundation.layout.BoxWithConstraintsScope +import androidx.compose.foundation.text.InternalFoundationTextApi +import androidx.compose.foundation.text.TextDelegate +import androidx.compose.material3.LocalTextStyle +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalFontFamilyResolver +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.DpSize +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.TextUnit +import kotlin.math.min +import kotlin.math.roundToInt + +// HUGE performance drop. Don't use for buttons +///** +// * Composable function that automatically adjusts the text size to fit within given constraints using AnnotatedString, considering the ratio of line spacing to text size. +// * +// * Features: +// * Similar to AutoSizeText(String), with support for AnnotatedString. +// * +// * @param inlineContent a map storing composables that replaces certain ranges of the text, used to +// * insert composables into text layout. See [InlineTextContent]. +// * @see AutoSizeText +// */ +//@Composable +//internal fun AutoSizeText( +// text: AnnotatedString, +// modifier: Modifier = Modifier, +// minRatio: Float = 1f, +// maxTextSize: TextUnit = TextUnit.Unspecified, +// alignment: Alignment = Alignment.TopStart, +// overflow: TextOverflow = TextOverflow.Clip, +// softWrap: Boolean = true, +// maxLines: Int = Int.MAX_VALUE, +// minLines: Int = 1, +// inlineContent: Map = mapOf(), +// onTextLayout: (TextLayoutResult) -> Unit = {}, +// style: TextStyle = LocalTextStyle.current, +//) = AutoSizeTextStyleBox( +// modifier = modifier, +// text = text, +// maxTextSize = maxTextSize, +// maxLines = maxLines, +// minLines = minLines, +// softWrap = softWrap, +// style = style, +// minRatio = minRatio, +// alignment = alignment +//) { +// Text( +// text = text, +// overflow = overflow, +// softWrap = softWrap, +// maxLines = maxLines, +// minLines = minLines, +// inlineContent = inlineContent, +// onTextLayout = onTextLayout, +// style = LocalTextStyle.current, +// ) +//} + +/** + * [BoxWithConstraints] with [autoTextStyle] passed via [LocalTextStyle]. + * + * @param modifier Modifier the [Modifier] to be applied to this layout node, + * @param text Text the text to be displayed, + * @param maxTextSize The maximum text size allowed. + * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary. + * If the text exceeds the given number of lines, it will be truncated according to overflow and + * [softWrap]. It is required that 1 <= [minLines] <= [maxLines]. + * @param minLines The minimum height in terms of minimum number of visible lines. It is required + * that 1 <= [minLines] <= [maxLines]. + * @param softWrap whether the text should break at soft line breaks. If false, the glyphs in the + * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false, + * overflow and TextAlign may have unexpected effects. + * @param style Original style. + * @param minRatio How small font can be. Percentage from calculated max font size. + * @param alignment The alignment of the text within its container. + * @param content Content in [BoxWithConstraints]. Use [LocalTextStyle] to access calculated + * [TextStyle]. + */ +@Composable +internal fun AutoSizeTextStyleBox( + modifier: Modifier, + text: AnnotatedString, + maxTextSize: TextUnit, + maxLines: Int, + minLines: Int, + softWrap: Boolean, + style: TextStyle, + minRatio: Float, + alignment: Alignment, + content: @Composable () -> Unit +) { + val density = LocalDensity.current + CompositionLocalProvider( + LocalDensity provides Density(density = density.density, fontScale = 1F) + ) { + BoxWithConstraints( + modifier = modifier, + contentAlignment = alignment, + ) { + val autoTextStyle = autoTextStyle( + text = text, + maxTextSize = maxTextSize, + maxLines = maxLines, + minLines = minLines, + softWrap = softWrap, + style = style, + alignment = alignment, + density = density, + minRatio = minRatio + ) + + CompositionLocalProvider( + value = LocalTextStyle.provides(autoTextStyle), + content = content + ) + } + } +} + +/** + * Calculates text size that will fill available space in [BoxWithConstraints]. + * + * @param text Text the text to be displayed, + * @param maxTextSize The maximum text size allowed. + * @param maxLines An optional maximum number of lines for the text to span, wrapping if necessary. + * If the text exceeds the given number of lines, it will be truncated according to overflow and + * [softWrap]. It is required that 1 <= [minLines] <= [maxLines]. + * @param minLines The minimum height in terms of minimum number of visible lines. It is required + * that 1 <= [minLines] <= [maxLines]. + * @param softWrap whether the text should break at soft line breaks. If false, the glyphs in the + * text will be positioned as if there was unlimited horizontal space. If [softWrap] is false, + * overflow and TextAlign may have unexpected effects. + * @param style Original style. + * @param alignment The alignment of the text within its container. + * @param density Original [Density]. This is required since [AutoSizeTextStyleBox] overrides + * [Density.fontScale] to `1F`. + * @param minRatio How small font can be. Percentage from calculated max font size. + * @return Calculated [TextStyle]. + */ +@Composable +private fun BoxWithConstraintsScope.autoTextStyle( + text: AnnotatedString, + maxTextSize: TextUnit = TextUnit.Unspecified, + maxLines: Int, + minLines: Int, + softWrap: Boolean, + style: TextStyle, + alignment: Alignment, + density: Density, + minRatio: Float, +): TextStyle { + val combinedTextStyle = LocalTextStyle.current + style.copy( + textAlign = when (alignment) { + Alignment.TopStart, Alignment.CenterStart, Alignment.BottomStart -> TextAlign.Start + Alignment.TopCenter, Alignment.Center, Alignment.BottomCenter -> TextAlign.Center + Alignment.TopEnd, Alignment.CenterEnd, Alignment.BottomEnd -> TextAlign.End + else -> TextAlign.Unspecified + }, + ) + + val layoutDirection = LocalLayoutDirection.current + val currentDensity = LocalDensity.current + val fontFamilyResolver = LocalFontFamilyResolver.current + val coercedLineSpacingRatio = (style.lineHeight.value / style.fontSize.value) + .takeIf { it.isFinite() && it >= 1 } ?: 1F + val shouldMoveBackward: (TextUnit) -> Boolean = { + shouldShrink( + text = text, + textStyle = combinedTextStyle.copy( + fontSize = it, + lineHeight = it * coercedLineSpacingRatio, + ), + maxLines = maxLines, + minLines = minLines, + softWrap = softWrap, + layoutDirection = layoutDirection, + density = currentDensity, + fontFamilyResolver = fontFamilyResolver, + ) + } + + val electedFontSize = rememberCandidateFontSizesIntProgress( + density = density, + dpSize = DpSize(maxWidth, maxHeight), + maxTextSize = maxTextSize, + minRatio = minRatio, + ).findElectedValue( + transform = { density.toSp(it) }, + shouldMoveBackward = shouldMoveBackward, + ) + + return combinedTextStyle.copy( + fontSize = electedFontSize, + lineHeight = electedFontSize * coercedLineSpacingRatio, + ) +} + +@OptIn(InternalFoundationTextApi::class) +private fun BoxWithConstraintsScope.shouldShrink( + text: AnnotatedString, + textStyle: TextStyle, + maxLines: Int, + minLines: Int, + softWrap: Boolean, + layoutDirection: LayoutDirection, + density: Density, + fontFamilyResolver: FontFamily.Resolver, +) = TextDelegate( + text = text, + style = textStyle, + maxLines = maxLines, + minLines = minLines, + softWrap = softWrap, + overflow = TextOverflow.Clip, + density = density, + fontFamilyResolver = fontFamilyResolver, +).layout( + constraints = constraints, + layoutDirection = layoutDirection, +).hasVisualOverflow + +@Composable +private fun rememberCandidateFontSizesIntProgress( + density: Density, + dpSize: DpSize, + maxTextSize: TextUnit = TextUnit.Unspecified, + minRatio: Float = 1f, +): IntProgression { + val max = remember(maxTextSize, dpSize, density) { + val intSize = density.toIntSize(dpSize) + min(intSize.width, intSize.height).let { max -> + maxTextSize + .takeIf { it.isSp } + ?.let { density.roundToPx(it) } + ?.coerceIn(range = 0..max) + ?: max + } + } + + val min = remember(minRatio, max, density) { + (max * minRatio).roundToInt() + } + + return remember(min, max) { + min..max step 1 + } +} + +// This function works by using a binary search algorithm +private fun IntProgression.findElectedValue( + transform: (Int) -> E, + shouldMoveBackward: (E) -> Boolean, +) = run { + var low = first + var high = last + while (low <= high) { + val mid = low + (high - low) / 2 + if (shouldMoveBackward(transform(mid))) + high = mid - 1 + else + low = mid + 1 + } + transform(high.coerceAtLeast(minimumValue = first)) +} diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/autosize/DensityExt.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/autosize/DensityExt.kt similarity index 95% rename from core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/autosize/DensityExt.kt rename to core/ui/src/main/java/com/sadellie/unitto/core/ui/common/autosize/DensityExt.kt index e2a9cd24..c84da632 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/autosize/DensityExt.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/autosize/DensityExt.kt @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.sadellie.unitto.core.ui.common.textfield.autosize +package com.sadellie.unitto.core.ui.common.autosize import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.DpSize diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/ClipboardManagerExt.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/ClipboardManagerExt.kt new file mode 100644 index 00000000..79f5b9e2 --- /dev/null +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/ClipboardManagerExt.kt @@ -0,0 +1,51 @@ +/* + * Unitto is a unit converter for Android + * Copyright (c) 2024 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.ui.platform.ClipboardManager +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.input.TextFieldValue + +/** + * 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. + */ +internal fun ClipboardManager.copyWithoutGrouping( + value: TextFieldValue, + formatterSymbols: FormatterSymbols +) = this.setText( + AnnotatedString( + value.annotatedString + .subSequence(value.selection) + .text + .replace(formatterSymbols.grouping, "") + ) +) + +internal fun ClipboardManager.copy(value: TextFieldValue) = this.setText( + AnnotatedString( + value.annotatedString + .subSequence(value.selection) + .text + ) +) diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/CursorFixer.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/CursorFixer.kt index 010f8876..abe5ab26 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/CursorFixer.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/CursorFixer.kt @@ -45,6 +45,8 @@ fun String.tokenLengthAhead(pos: Int): Int { if (pos.isAfterToken(this, it)) return it.length } + // We default to 1 here. It means that cursor is not placed after illegal token. Just a number + // or a binary operator or something else, can delete by one symbol. return 1 } diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FixedInputTextFIeld.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FixedInputTextFIeld.kt index 1629a32e..94350f3e 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FixedInputTextFIeld.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FixedInputTextFIeld.kt @@ -42,6 +42,7 @@ import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp +import com.sadellie.unitto.core.ui.common.textfield.texttoolbar.UnittoTextToolbar import com.sadellie.unitto.core.ui.theme.LocalNumberTypography @Composable 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 index 7fa2e719..31abdefa 100644 --- 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 @@ -18,19 +18,41 @@ package com.sadellie.unitto.core.ui.common.textfield +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.remember +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.Brush import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.ClipboardManager 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.platform.TextToolbar +import androidx.compose.ui.text.TextLayoutResult import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue -import com.sadellie.unitto.core.ui.common.textfield.autosize.AutoSizeTextField +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.TextUnit +import com.sadellie.unitto.core.ui.common.autosize.AutoSizeTextStyleBox +import com.sadellie.unitto.core.ui.common.textfield.texttoolbar.UnittoTextToolbar import com.sadellie.unitto.core.ui.theme.LocalNumberTypography @Composable @@ -141,29 +163,95 @@ fun UnformattedTextField( } /** - * Copy value to clipboard without grouping symbols. + * Based on: https://gist.github.com/inidamleader/b594d35362ebcf3cedf81055df519300 * - * Example: - * "123.456,789" will be copied as "123456,789" - * - * @param value Formatted value that has grouping symbols. + * @param placeholder Placeholder text, shown when [value] is empty. + * @param textToolbar [TextToolbar] with modified actions in menu. + * @param alignment The alignment of the text within its container. + * @see [BasicTextField] + * @see [AutoSizeTextStyleBox] */ -fun ClipboardManager.copyWithoutGrouping( +@Composable +private fun AutoSizeTextField( + modifier: Modifier = Modifier, value: TextFieldValue, - formatterSymbols: FormatterSymbols -) = this.setText( - AnnotatedString( - value.annotatedString - .subSequence(value.selection) - .text - .replace(formatterSymbols.grouping, "") - ) -) + onValueChange: (TextFieldValue) -> Unit, + placeholder: String? = null, + textToolbar: TextToolbar = LocalTextToolbar.current, + enabled: Boolean = true, + readOnly: Boolean = false, + textStyle: TextStyle = TextStyle.Default, + keyboardOptions: KeyboardOptions = KeyboardOptions.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = true, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + minLines: Int = 1, + visualTransformation: VisualTransformation = VisualTransformation.None, + onTextLayout: (TextLayoutResult) -> Unit = {}, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + cursorBrush: Brush = SolidColor(Color.Black), + maxTextSize: TextUnit = TextUnit.Unspecified, + minRatio: Float = 1f, + alignment: Alignment = Alignment.BottomEnd, +) = AutoSizeTextStyleBox( + modifier = modifier, + text = visualTransformation.filter(value.annotatedString).text, + maxTextSize = maxTextSize, + maxLines = maxLines, + minLines = minLines, + softWrap = false, + style = textStyle, + minRatio = minRatio, + alignment = alignment +) { + CompositionLocalProvider( + LocalTextInputService provides null, + LocalTextToolbar provides textToolbar + ) { + val currentTextToolbar = LocalTextToolbar.current + val style = LocalTextStyle.current + val focusRequester = remember { FocusRequester() } -private fun ClipboardManager.copy(value: TextFieldValue) = this.setText( - AnnotatedString( - value.annotatedString - .subSequence(value.selection) - .text - ) -) + BasicTextField( + value = value, + onValueChange = onValueChange, + modifier = Modifier + .fillMaxWidth() + .focusRequester(focusRequester) + .clickable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + onClick = { + currentTextToolbar.hide() + focusRequester.requestFocus() + onValueChange(value.copy(selection = TextRange.Zero)) + currentTextToolbar.showMenu(Rect(Offset.Zero, 0f)) + } + ), + enabled = enabled, + readOnly = readOnly, + textStyle = style, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + visualTransformation = visualTransformation, + onTextLayout = onTextLayout, + interactionSource = interactionSource, + cursorBrush = cursorBrush, + decorationBox = { innerTextField -> + if (value.text.isEmpty() and !placeholder.isNullOrEmpty()) { + Text( + text = placeholder!!, + style = style.copy( + textAlign = TextAlign.End, + color = MaterialTheme.colorScheme.onSurface.copy(0.5f) + ) + ) + } + innerTextField() + }, + ) + } +} diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/TextFieldValueExtensions.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/TextFieldValueExtensions.kt index c6ecd876..ac5d152a 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/TextFieldValueExtensions.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/TextFieldValueExtensions.kt @@ -111,13 +111,7 @@ fun TextFieldValue.deleteTokens(): TextFieldValue { // We don't have anything selected (cursor in one position) // like this 1234|56 => after deleting will be like this 123|56 // Cursor moved one symbol left - selection.start -> { - // We default to 1 here. It means that cursor is not placed after illegal token - // Just a number or a binary operator or something else, can delete by one symbol - val symbolsToDelete = text.tokenLengthAhead(selection.end) - - selection.start - symbolsToDelete - } + selection.start -> selection.start - text.tokenLengthAhead(selection.end) // We have multiple symbols selected // like this 123[45]6 => after deleting will be like this 123|6 // Cursor will be placed where selection start was @@ -149,6 +143,7 @@ fun SavedStateHandle.getTextField(key: String): TextFieldValue = */ private fun TextFieldValue.deleteAheadAndAdd(tokens: String): TextFieldValue { var newValue = this + // For cases like: "12+[34]" and "*" symbol is being added. Will delete selected tokens if (!selection.collapsed) newValue = this.deleteTokens() return newValue .deleteTokens() diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/autosize/AutosizeTextField.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/autosize/AutosizeTextField.kt deleted file mode 100644 index 6c7558b9..00000000 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/autosize/AutosizeTextField.kt +++ /dev/null @@ -1,546 +0,0 @@ -/* - * Unitto is a unit converter for Android - * Copyright (c) 2024 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.autosize - -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.Interaction -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.BoxWithConstraints -import androidx.compose.foundation.layout.BoxWithConstraintsScope -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.InternalFoundationTextApi -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.foundation.text.TextDelegate -import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.remember -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.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.SolidColor -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.platform.LocalFontFamilyResolver -import androidx.compose.ui.platform.LocalLayoutDirection -import androidx.compose.ui.platform.LocalTextInputService -import androidx.compose.ui.platform.LocalTextToolbar -import androidx.compose.ui.platform.TextToolbar -import androidx.compose.ui.text.AnnotatedString -import androidx.compose.ui.text.TextLayoutResult -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontFamily -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.text.input.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.Density -import androidx.compose.ui.unit.DpSize -import androidx.compose.ui.unit.LayoutDirection -import androidx.compose.ui.unit.TextUnit -import androidx.compose.ui.unit.sp -import com.sadellie.unitto.core.ui.common.textfield.autosize.SuggestedFontSizesStatus.Companion.suggestedFontSizesStatus -import kotlin.math.min - -/** - * Based on: https://gist.github.com/inidamleader/b594d35362ebcf3cedf81055df519300 - * - * @param value The [androidx.compose.ui.text.input.TextFieldValue] to be shown in the - * [BasicTextField]. - * @param onValueChange Called when the input service updates the values in [TextFieldValue]. - * @param placeholder Placeholder text, shown when [value] is empty. - * @param textToolbar [TextToolbar] with modified actions in menu. - * @param modifier optional [Modifier] for this text field. - * @param enabled controls the enabled state of the [BasicTextField]. When `false`, the text - * field will be neither editable nor focusable, the input of the text field will not be selectable - * @param readOnly controls the editable state of the [BasicTextField]. When `true`, the text - * field can not be modified, however, a user can focus it and copy text from it. Read-only text - * fields are usually used to display pre-filled forms that user can not edit - * @param textStyle Style configuration that applies at character level such as color, font etc. - * @param keyboardOptions software keyboard options that contains configuration such as - * [KeyboardType] and [ImeAction]. - * @param keyboardActions when the input service emits an IME action, the corresponding callback - * is called. Note that this IME action may be different from what you specified in - * [KeyboardOptions.imeAction]. - * @param singleLine when set to true, this text field becomes a single horizontally scrolling - * text field instead of wrapping onto multiple lines. The keyboard will be informed to not show - * the return key as the [ImeAction]. [maxLines] and [minLines] are ignored as both are - * automatically set to 1. - * @param maxLines the maximum height in terms of maximum number of visible lines. It is required - * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. - * @param minLines the minimum height in terms of minimum number of visible lines. It is required - * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. - * @param visualTransformation The visual transformation filter for changing the visual - * representation of the input. By default no visual transformation is applied. - * @param onTextLayout Callback that is executed when a new text layout is calculated. A - * [TextLayoutResult] object that callback provides contains paragraph information, size of the - * text, baselines and other details. The callback can be used to add additional decoration or - * functionality to the text. For example, to draw a cursor or selection around the text. - * @param interactionSource the [MutableInteractionSource] representing the stream of - * [Interaction]s for this TextField. You can create and pass in your own remembered - * [MutableInteractionSource] if you want to observe [Interaction]s and customize the - * appearance / behavior of this TextField in different [Interaction]s. - * @param cursorBrush [Brush] to paint cursor with. If [SolidColor] with [Color.Unspecified] - * provided, there will be no cursor drawn - * @param suggestedFontSizes The suggested font sizes to choose from (Should be sorted from smallest to largest, not empty and contains only sp text unit). - * @param suggestedFontSizesStatus Whether or not suggestedFontSizes is valid: not empty - contains oly sp text unit - sorted. - * You can check validity by invoking [List.suggestedFontSizesStatus] - * @param stepGranularityTextSize The step size for adjusting the text size. - * @param minTextSize The minimum text size allowed. - * @param maxTextSize The maximum text size allowed. - * @param minRatio How small font can be. Percentage from calculated max font size. - * @param lineSpacingRatio The ratio of line spacing to text size. - * @param alignment The alignment of the text within its container. - */ -@Composable -internal fun AutoSizeTextField( - modifier: Modifier = Modifier, - value: TextFieldValue, - onValueChange: (TextFieldValue) -> Unit, - placeholder: String? = "", - textToolbar: TextToolbar, - enabled: Boolean = true, - readOnly: Boolean = false, - textStyle: TextStyle = TextStyle.Default, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - keyboardActions: KeyboardActions = KeyboardActions.Default, - singleLine: Boolean = true, - maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, - minLines: Int = 1, - visualTransformation: VisualTransformation = VisualTransformation.None, - onTextLayout: (TextLayoutResult) -> Unit = {}, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - cursorBrush: Brush = SolidColor(Color.Black), - suggestedFontSizes: ImmutableWrapper> = emptyList().toImmutableWrapper(), - suggestedFontSizesStatus: SuggestedFontSizesStatus = suggestedFontSizes.value.suggestedFontSizesStatus, - stepGranularityTextSize: TextUnit = TextUnit.Unspecified, - minTextSize: TextUnit = TextUnit.Unspecified, - maxTextSize: TextUnit = TextUnit.Unspecified, - minRatio: Float = 1f, - lineSpacingRatio: Float = textStyle.lineHeight.value / textStyle.fontSize.value, - alignment: Alignment = Alignment.BottomEnd, -) = CompositionLocalProvider( - LocalTextInputService provides null, - LocalTextToolbar provides textToolbar -) { - val localTextToolbar = LocalTextToolbar.current - - AutoSizeTextField( - value = value, - onValueChange = { - localTextToolbar.showMenu(Rect(Offset.Zero, 0f)) - localTextToolbar.hide() - onValueChange(it) - }, - modifier = modifier, - enabled = enabled, - readOnly = readOnly, - textStyle = textStyle, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - singleLine = singleLine, - maxLines = maxLines, - minLines = minLines, - visualTransformation = visualTransformation, - onTextLayout = onTextLayout, - interactionSource = interactionSource, - cursorBrush = cursorBrush, - decorationBox = { innerTextField, calculatedStyle -> - if (value.text.isEmpty() and !placeholder.isNullOrEmpty()) { - Text( - text = placeholder!!, - style = calculatedStyle().copy( - textAlign = TextAlign.End, - color = MaterialTheme.colorScheme.onSurface.copy(0.5f) - ) - ) - } - innerTextField() - }, - suggestedFontSizes = suggestedFontSizes, - suggestedFontSizesStatus = suggestedFontSizesStatus, - stepGranularityTextSize = stepGranularityTextSize, - minTextSize = minTextSize, - maxTextSize = maxTextSize, - minRatio = minRatio, - lineSpacingRatio = lineSpacingRatio, - alignment = alignment, - ) -} - -/** - * Based on: https://gist.github.com/inidamleader/b594d35362ebcf3cedf81055df519300 - * - * @param value The [androidx.compose.ui.text.input.TextFieldValue] to be shown in the - * [BasicTextField]. - * @param onValueChange Called when the input service updates the values in [TextFieldValue]. - * @param modifier optional [Modifier] for this text field. - * @param enabled controls the enabled state of the [BasicTextField]. When `false`, the text - * field will be neither editable nor focusable, the input of the text field will not be selectable - * @param readOnly controls the editable state of the [BasicTextField]. When `true`, the text - * field can not be modified, however, a user can focus it and copy text from it. Read-only text - * fields are usually used to display pre-filled forms that user can not edit - * @param textStyle Style configuration that applies at character level such as color, font etc. - * @param keyboardOptions software keyboard options that contains configuration such as - * [KeyboardType] and [ImeAction]. - * @param keyboardActions when the input service emits an IME action, the corresponding callback - * is called. Note that this IME action may be different from what you specified in - * [KeyboardOptions.imeAction]. - * @param singleLine when set to true, this text field becomes a single horizontally scrolling - * text field instead of wrapping onto multiple lines. The keyboard will be informed to not show - * the return key as the [ImeAction]. [maxLines] and [minLines] are ignored as both are - * automatically set to 1. - * @param maxLines the maximum height in terms of maximum number of visible lines. It is required - * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. - * @param minLines the minimum height in terms of minimum number of visible lines. It is required - * that 1 <= [minLines] <= [maxLines]. This parameter is ignored when [singleLine] is true. - * @param visualTransformation The visual transformation filter for changing the visual - * representation of the input. By default no visual transformation is applied. - * @param onTextLayout Callback that is executed when a new text layout is calculated. A - * [TextLayoutResult] object that callback provides contains paragraph information, size of the - * text, baselines and other details. The callback can be used to add additional decoration or - * functionality to the text. For example, to draw a cursor or selection around the text. - * @param interactionSource the [MutableInteractionSource] representing the stream of - * [Interaction]s for this TextField. You can create and pass in your own remembered - * [MutableInteractionSource] if you want to observe [Interaction]s and customize the - * appearance / behavior of this TextField in different [Interaction]s. - * @param cursorBrush [Brush] to paint cursor with. If [SolidColor] with [Color.Unspecified] - * provided, there will be no cursor drawn - * @param decorationBox Composable lambda that allows to add decorations around text field, such - * as icon, placeholder, helper messages or similar, and automatically increase the hit target area - * of the text field. To allow you to control the placement of the inner text field relative to your - * decorations, the text field implementation will pass in a framework-controlled composable - * parameter "innerTextField" to the decorationBox lambda you provide. You must call - * innerTextField exactly once. - * @param suggestedFontSizes The suggested font sizes to choose from (Should be sorted from smallest to largest, not empty and contains only sp text unit). - * @param suggestedFontSizesStatus Whether or not suggestedFontSizes is valid: not empty - contains oly sp text unit - sorted. - * You can check validity by invoking [List.suggestedFontSizesStatus] - * @param stepGranularityTextSize The step size for adjusting the text size. - * @param minTextSize The minimum text size allowed. - * @param maxTextSize The maximum text size allowed. - * @param minRatio How small font can be. Percentage from calculated max font size. - * @param lineSpacingRatio The ratio of line spacing to text size. - * @param alignment The alignment of the text within its container. - */ -@Composable -private fun AutoSizeTextField( - value: TextFieldValue, - onValueChange: (TextFieldValue) -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - readOnly: Boolean = false, - textStyle: TextStyle = TextStyle.Default, - keyboardOptions: KeyboardOptions = KeyboardOptions.Default, - keyboardActions: KeyboardActions = KeyboardActions.Default, - singleLine: Boolean = false, - maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, - minLines: Int = 1, - visualTransformation: VisualTransformation = VisualTransformation.None, - onTextLayout: (TextLayoutResult) -> Unit = {}, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - cursorBrush: Brush = SolidColor(Color.Black), - decorationBox: @Composable ( - innerTextField: @Composable () -> Unit, - textStyle: () -> TextStyle, // Passing lambda for performance - ) -> Unit = { _, _ -> }, - suggestedFontSizes: ImmutableWrapper> = emptyList().toImmutableWrapper(), - suggestedFontSizesStatus: SuggestedFontSizesStatus = suggestedFontSizes.value.suggestedFontSizesStatus, - stepGranularityTextSize: TextUnit = TextUnit.Unspecified, - minTextSize: TextUnit = TextUnit.Unspecified, - maxTextSize: TextUnit = TextUnit.Unspecified, - minRatio: Float = 1f, - lineSpacingRatio: Float = textStyle.lineHeight.value / textStyle.fontSize.value, - alignment: Alignment = Alignment.TopStart, -) { - val focusRequester = remember { FocusRequester() } - val density = LocalDensity.current - val localTextToolbar = LocalTextToolbar.current - // Change font scale to 1F - CompositionLocalProvider( - LocalDensity provides Density(density = density.density, fontScale = 1F) - ) { - BoxWithConstraints( - modifier = modifier, - contentAlignment = alignment, - ) { - val combinedTextStyle = LocalTextStyle.current + textStyle.copy( - textAlign = when (alignment) { - Alignment.TopStart, Alignment.CenterStart, Alignment.BottomStart -> TextAlign.Start - Alignment.TopCenter, Alignment.Center, Alignment.BottomCenter -> TextAlign.Center - Alignment.TopEnd, Alignment.CenterEnd, Alignment.BottomEnd -> TextAlign.End - else -> TextAlign.Unspecified - }, - ) - - val candidateFontSizes = - if (suggestedFontSizesStatus == SuggestedFontSizesStatus.VALID) - suggestedFontSizes.value - else - remember(suggestedFontSizes) { - suggestedFontSizes.value - .filter { it.isSp } - .takeIf { it.isNotEmpty() } - ?.sortedBy { it.value } - } ?: rememberCandidateFontSizes( - density = density, - maxDpSize = DpSize(maxWidth, maxHeight), - maxTextSize = maxTextSize, - minTextSize = minTextSize, - stepGranularityTextSize = stepGranularityTextSize, - ) - - val layoutDirection = LocalLayoutDirection.current - val currentDensity = LocalDensity.current - val fontFamilyResolver = LocalFontFamilyResolver.current - val coercedLineSpacingRatio = lineSpacingRatio.coerceAtLeast(1f).takeIf { !lineSpacingRatio.isNaN() } ?: 1f - if (candidateFontSizes.isEmpty()) return@BoxWithConstraints - val (electedIndex, _) = candidateFontSizes.findElectedIndex { - calculateLayout( - text = visualTransformation.filter(value.annotatedString).text, - textStyle = combinedTextStyle.copy( - fontSize = it, - lineHeight = it * coercedLineSpacingRatio, - ), - maxLines = maxLines, - minLines = minLines, - softWrap = true, - layoutDirection = layoutDirection, - density = currentDensity, - fontFamilyResolver = fontFamilyResolver, - ) - } - val minFontSize = candidateFontSizes.maxBy { it.value } * minRatio - val electedFontSize = candidateFontSizes[electedIndex] - val fSize: TextUnit = listOf(minFontSize, electedFontSize).maxBy { it.value } - val calculatedTextStyle = combinedTextStyle.copy( - fontSize = fSize, - lineHeight = fSize * coercedLineSpacingRatio - ) - - BasicTextField( - value = value, - onValueChange = onValueChange, - modifier = Modifier - .fillMaxWidth() - .focusRequester(focusRequester) - .clickable( - interactionSource = remember { MutableInteractionSource() }, - indication = null, - onClick = { - localTextToolbar.hide() - focusRequester.requestFocus() - onValueChange(value.copy(selection = TextRange.Zero)) - localTextToolbar.showMenu(Rect(Offset.Zero, 0f)) - } - ), - enabled = enabled, - readOnly = readOnly, - textStyle = calculatedTextStyle, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - singleLine = singleLine, - maxLines = maxLines, - minLines = minLines, - visualTransformation = visualTransformation, - onTextLayout = onTextLayout, - interactionSource = interactionSource, - cursorBrush = cursorBrush, - decorationBox = { innerTextField -> - decorationBox(innerTextField) { - combinedTextStyle.copy( - fontSize = electedFontSize, - lineHeight = electedFontSize * coercedLineSpacingRatio - ) - } - }, - ) - } - } -} - -@Composable -private fun rememberCandidateFontSizes( - density: Density, - maxDpSize: DpSize, - minTextSize: TextUnit = TextUnit.Unspecified, - maxTextSize: TextUnit = TextUnit.Unspecified, - stepGranularityTextSize: TextUnit = TextUnit.Unspecified, -): List { - val max = remember(maxTextSize, maxDpSize, density) { - val size = density.toIntSize(maxDpSize) - min(size.width, size.height).let { max -> - maxTextSize - .takeIf { it.isSp } - ?.let { density.roundToPx(it) } - ?.coerceIn(range = 0..max) - ?: max - } - } - - val min = remember(minTextSize, max, density) { - minTextSize - .takeIf { it.isSp } - ?.let { density.roundToPx(it) } - ?.coerceIn(range = 0..max) - ?: 0 - } - - val step = remember(stepGranularityTextSize, min, max, density) { - stepGranularityTextSize - .takeIf { it.isSp } - ?.let { density.roundToPx(it) } - ?.coerceAtLeast(minimumValue = 1) - ?: 1 - } - - return remember(max, min, step) { - buildList { - var current = min - while (current <= max) { - add(density.toSp(current)) - current += step - } - } - } -} - -@OptIn(InternalFoundationTextApi::class) -private fun BoxWithConstraintsScope.calculateLayout( - text: AnnotatedString, - textStyle: TextStyle, - maxLines: Int, - minLines: Int, - softWrap: Boolean, - layoutDirection: LayoutDirection, - density: Density, - fontFamilyResolver: FontFamily.Resolver, -) = TextDelegate( - text = text, - style = textStyle, - maxLines = maxLines, - minLines = minLines, - softWrap = softWrap, - overflow = TextOverflow.Clip, - density = density, - fontFamilyResolver = fontFamilyResolver, -).layout( - constraints = constraints, - layoutDirection = layoutDirection, -) - -// This function works by using a binary search algorithm -fun List.findElectedIndex( - shouldMoveBackward: (TextUnit) -> TextLayoutResult -): Pair { - val elements = this - var low = 0 - var high = elements.lastIndex - var textLayoutResult: TextLayoutResult? = null - while (low <= high) { - val mid = low + (high - low) / 2 - textLayoutResult = shouldMoveBackward(elements[mid]) - if (textLayoutResult.hasVisualOverflow) - high = mid - 1 - else - low = mid + 1 - } - return high.coerceIn(elements.indices) to textLayoutResult -} - -enum class SuggestedFontSizesStatus { - VALID, INVALID; - - companion object { - val List.suggestedFontSizesStatus - get() = if (isNotEmpty() && all { it.isSp } && this.sortedBy { it.value } == this) - VALID - else - INVALID - } -} - -@Preview(widthDp = 200, heightDp = 100) -@Preview(widthDp = 200, heightDp = 30) -@Preview(widthDp = 60, heightDp = 30) -@Composable -fun PreviewAutoSizeTextWithMaxLinesSetToIntMaxValue() { - MaterialTheme { - Surface(color = MaterialTheme.colorScheme.primary) { - AutoSizeTextField( - value = TextFieldValue("This is a bunch of text that will be auto sized"), - onValueChange = {}, - modifier = Modifier.fillMaxSize(), - alignment = Alignment.CenterStart, - textStyle = MaterialTheme.typography.bodyMedium, - ) - } - } -} - -@Preview(widthDp = 200, heightDp = 100) -@Preview(widthDp = 200, heightDp = 30) -@Preview(widthDp = 60, heightDp = 30) -@Composable -fun PreviewAutoSizeTextWithMinSizeSetTo14() { - MaterialTheme { - Surface(color = MaterialTheme.colorScheme.secondary) { - AutoSizeTextField( - value = TextFieldValue("This is a bunch of text that will be auto sized"), - onValueChange = {}, - modifier = Modifier.fillMaxSize(), - minTextSize = 14.sp, - alignment = Alignment.CenterStart, - textStyle = MaterialTheme.typography.bodyMedium, - ) - } - } -} - -@Preview(widthDp = 200, heightDp = 100) -@Preview(widthDp = 200, heightDp = 30) -@Preview(widthDp = 60, heightDp = 30) -@Composable -fun PreviewAutoSizeTextWithMaxLinesSetToOne() { - MaterialTheme { - Surface(color = MaterialTheme.colorScheme.tertiary) { - AutoSizeTextField( - value = TextFieldValue("This is a bunch of text that will be auto sized"), - onValueChange = {}, - modifier = Modifier.fillMaxSize(), - alignment = Alignment.Center, - maxLines = 1, - textStyle = MaterialTheme.typography.bodyMedium - ) - } - } -} diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/autosize/ImmutableWrapper.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/autosize/ImmutableWrapper.kt deleted file mode 100644 index cc91f26e..00000000 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/autosize/ImmutableWrapper.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Unitto is a unit converter for Android - * Copyright (c) 2024 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.autosize - -import androidx.compose.runtime.Immutable -import kotlin.reflect.KProperty - -@Immutable -internal data class ImmutableWrapper(val value: T) - -internal fun T.toImmutableWrapper() = ImmutableWrapper(this) - -internal operator fun ImmutableWrapper.getValue(thisRef: Any?, property: KProperty<*>) = value diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FloatingTextActionModeCallback.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/texttoolbar/FloatingTextActionModeCallback.kt similarity index 91% rename from core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FloatingTextActionModeCallback.kt rename to core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/texttoolbar/FloatingTextActionModeCallback.kt index 07bbac80..68564a7a 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/FloatingTextActionModeCallback.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/texttoolbar/FloatingTextActionModeCallback.kt @@ -1,6 +1,6 @@ /* * Unitto is a unit converter for Android - * Copyright (c) 2023 Elshan Agaev + * Copyright (c) 2023-2024 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 @@ -16,15 +16,16 @@ * along with this program. If not, see . */ -package com.sadellie.unitto.core.ui.common.textfield +package com.sadellie.unitto.core.ui.common.textfield.texttoolbar +import android.os.Build import android.view.ActionMode import android.view.Menu import android.view.MenuItem import android.view.View import androidx.annotation.RequiresApi -@RequiresApi(23) +@RequiresApi(Build.VERSION_CODES.M) internal class FloatingTextActionModeCallback( private val callback: UnittoActionModeCallback ) : ActionMode.Callback2() { diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoActionModeCallback.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/texttoolbar/UnittoActionModeCallback.kt similarity index 96% rename from core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoActionModeCallback.kt rename to core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/texttoolbar/UnittoActionModeCallback.kt index f6b8b480..79481686 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoActionModeCallback.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/texttoolbar/UnittoActionModeCallback.kt @@ -1,6 +1,6 @@ /* * Unitto is a unit converter for Android - * Copyright (c) 2023 Elshan Agaev + * Copyright (c) 2023-2024 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 @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.sadellie.unitto.core.ui.common.textfield +package com.sadellie.unitto.core.ui.common.textfield.texttoolbar import android.view.ActionMode import android.view.Menu diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoPrimaryTextActionModeCallback.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/texttoolbar/UnittoPrimaryTextActionModeCallback.kt similarity index 93% rename from core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoPrimaryTextActionModeCallback.kt rename to core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/texttoolbar/UnittoPrimaryTextActionModeCallback.kt index 391c6687..641fe434 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoPrimaryTextActionModeCallback.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/texttoolbar/UnittoPrimaryTextActionModeCallback.kt @@ -1,6 +1,6 @@ /* * Unitto is a unit converter for Android - * Copyright (c) 2023 Elshan Agaev + * Copyright (c) 2023-2024 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 @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.sadellie.unitto.core.ui.common.textfield +package com.sadellie.unitto.core.ui.common.textfield.texttoolbar import android.view.ActionMode import android.view.Menu diff --git a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoTextToolbar.kt b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/texttoolbar/UnittoTextToolbar.kt similarity index 96% rename from core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoTextToolbar.kt rename to core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/texttoolbar/UnittoTextToolbar.kt index 93bd577b..ca93519a 100644 --- a/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/UnittoTextToolbar.kt +++ b/core/ui/src/main/java/com/sadellie/unitto/core/ui/common/textfield/texttoolbar/UnittoTextToolbar.kt @@ -1,6 +1,6 @@ /* * Unitto is a unit converter for Android - * Copyright (c) 2023 Elshan Agaev + * Copyright (c) 2023-2024 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 @@ -16,7 +16,7 @@ * along with this program. If not, see . */ -package com.sadellie.unitto.core.ui.common.textfield +package com.sadellie.unitto.core.ui.common.textfield.texttoolbar import android.os.Build import android.view.ActionMode