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